old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -2,12 +2,16 @@
|
||||
// CommandHandlers.Witness.cs
|
||||
// Sprint: SPRINT_3700_0005_0001_witness_ui_cli
|
||||
// Tasks: CLI-001, CLI-002, CLI-003, CLI-004
|
||||
// Sprint: SPRINT_20260112_014_CLI_witness_commands (CLI-WIT-002)
|
||||
// Description: Command handlers for reachability witness CLI.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Spectre.Console;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
@@ -21,6 +25,7 @@ internal static partial class CommandHandlers
|
||||
|
||||
/// <summary>
|
||||
/// Handler for `witness show` command.
|
||||
/// Sprint: SPRINT_20260112_014_CLI_witness_commands (CLI-WIT-002)
|
||||
/// </summary>
|
||||
internal static async Task HandleWitnessShowAsync(
|
||||
IServiceProvider services,
|
||||
@@ -38,52 +43,25 @@ internal static partial class CommandHandlers
|
||||
console.MarkupLine($"[dim]Fetching witness: {witnessId}[/]");
|
||||
}
|
||||
|
||||
// TODO: Replace with actual service call when witness API is available
|
||||
var witness = new WitnessDto
|
||||
using var scope = services.CreateScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
|
||||
|
||||
var response = await client.GetWitnessAsync(witnessId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
WitnessId = witnessId,
|
||||
WitnessSchema = "stellaops.witness.v1",
|
||||
CveId = "CVE-2024-12345",
|
||||
PackageName = "Newtonsoft.Json",
|
||||
PackageVersion = "12.0.3",
|
||||
ConfidenceTier = "confirmed",
|
||||
ObservedAt = DateTimeOffset.UtcNow.AddHours(-2).ToString("O", CultureInfo.InvariantCulture),
|
||||
Entrypoint = new WitnessEntrypointDto
|
||||
{
|
||||
Type = "http",
|
||||
Route = "GET /api/users/{id}",
|
||||
Symbol = "UserController.GetUser()",
|
||||
File = "src/Controllers/UserController.cs",
|
||||
Line = 42
|
||||
},
|
||||
Sink = new WitnessSinkDto
|
||||
{
|
||||
Symbol = "JsonConvert.DeserializeObject<User>()",
|
||||
Package = "Newtonsoft.Json",
|
||||
IsTrigger = true
|
||||
},
|
||||
Path = new[]
|
||||
{
|
||||
new PathStepDto { Symbol = "UserController.GetUser()", File = "src/Controllers/UserController.cs", Line = 42 },
|
||||
new PathStepDto { Symbol = "UserService.GetUserById()", File = "src/Services/UserService.cs", Line = 88 },
|
||||
new PathStepDto { Symbol = "JsonConvert.DeserializeObject<User>()", Package = "Newtonsoft.Json" }
|
||||
},
|
||||
Gates = new[]
|
||||
{
|
||||
new GateDto { Type = "authRequired", Detail = "[Authorize] attribute", Confidence = 0.95m }
|
||||
},
|
||||
Evidence = new WitnessEvidenceDto
|
||||
{
|
||||
CallgraphDigest = "blake3:a1b2c3d4e5f6...",
|
||||
SurfaceDigest = "sha256:9f8e7d6c5b4a...",
|
||||
SignedBy = "attestor-stellaops-ed25519"
|
||||
}
|
||||
};
|
||||
console.MarkupLine($"[red]Witness not found: {witnessId}[/]");
|
||||
Environment.ExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert API response to internal DTO for display
|
||||
var witness = ConvertToWitnessDto(response);
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case "json":
|
||||
var json = JsonSerializer.Serialize(witness, WitnessJsonOptions);
|
||||
var json = JsonSerializer.Serialize(response, WitnessJsonOptions);
|
||||
console.WriteLine(json);
|
||||
break;
|
||||
case "yaml":
|
||||
@@ -93,12 +71,11 @@ internal static partial class CommandHandlers
|
||||
WriteWitnessText(console, witness, pathOnly, noColor);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for `witness verify` command.
|
||||
/// Sprint: SPRINT_20260112_014_CLI_witness_commands (CLI-WIT-004)
|
||||
/// </summary>
|
||||
internal static async Task HandleWitnessVerifyAsync(
|
||||
IServiceProvider services,
|
||||
@@ -119,30 +96,49 @@ internal static partial class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace with actual verification when DSSE verification is wired up
|
||||
await Task.Delay(100, cancellationToken); // Simulate verification
|
||||
|
||||
// Placeholder result
|
||||
var valid = true;
|
||||
var keyId = "attestor-stellaops-ed25519";
|
||||
var algorithm = "Ed25519";
|
||||
|
||||
if (valid)
|
||||
if (offline && publicKeyPath == null)
|
||||
{
|
||||
console.MarkupLine("[green]✓ Signature VALID[/]");
|
||||
console.MarkupLine($" Key ID: {keyId}");
|
||||
console.MarkupLine($" Algorithm: {algorithm}");
|
||||
console.MarkupLine("[yellow]Warning: Offline mode requires --public-key to verify signatures locally.[/]");
|
||||
console.MarkupLine("[dim]Skipping signature verification.[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
using var scope = services.CreateScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
|
||||
|
||||
var response = await client.VerifyWitnessAsync(witnessId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.Verified)
|
||||
{
|
||||
// ASCII-only output per AGENTS.md rules
|
||||
console.MarkupLine("[green][OK] Signature VALID[/]");
|
||||
if (response.Dsse?.SignerIdentities?.Count > 0)
|
||||
{
|
||||
console.MarkupLine($" Signers: {string.Join(", ", response.Dsse.SignerIdentities)}");
|
||||
}
|
||||
if (response.Dsse?.PredicateType != null)
|
||||
{
|
||||
console.MarkupLine($" Predicate Type: {response.Dsse.PredicateType}");
|
||||
}
|
||||
if (response.ContentHash?.Match == true)
|
||||
{
|
||||
console.MarkupLine(" Content Hash: [green]MATCH[/]");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.MarkupLine("[red]✗ Signature INVALID[/]");
|
||||
console.MarkupLine(" Error: Signature verification failed");
|
||||
console.MarkupLine("[red][FAIL] Signature INVALID[/]");
|
||||
if (response.Message != null)
|
||||
{
|
||||
console.MarkupLine($" Error: {response.Message}");
|
||||
}
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for `witness list` command.
|
||||
/// Sprint: SPRINT_20260112_014_CLI_witness_commands (CLI-WIT-002)
|
||||
/// </summary>
|
||||
internal static async Task HandleWitnessListAsync(
|
||||
IServiceProvider services,
|
||||
@@ -165,45 +161,48 @@ internal static partial class CommandHandlers
|
||||
if (reachableOnly) console.MarkupLine("[dim]Showing reachable witnesses only[/]");
|
||||
}
|
||||
|
||||
// TODO: Replace with actual service call
|
||||
var witnesses = new[]
|
||||
using var scope = services.CreateScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
|
||||
|
||||
var request = new WitnessListRequest
|
||||
{
|
||||
new WitnessListItemDto
|
||||
{
|
||||
WitnessId = "wit:sha256:abc123",
|
||||
CveId = "CVE-2024-12345",
|
||||
PackageName = "Newtonsoft.Json",
|
||||
ConfidenceTier = "confirmed",
|
||||
Entrypoint = "GET /api/users/{id}",
|
||||
Sink = "JsonConvert.DeserializeObject()"
|
||||
},
|
||||
new WitnessListItemDto
|
||||
{
|
||||
WitnessId = "wit:sha256:def456",
|
||||
CveId = "CVE-2024-12346",
|
||||
PackageName = "lodash",
|
||||
ConfidenceTier = "likely",
|
||||
Entrypoint = "POST /api/data",
|
||||
Sink = "_.template()"
|
||||
}
|
||||
ScanId = scanId,
|
||||
VulnerabilityId = vuln,
|
||||
Limit = limit
|
||||
};
|
||||
|
||||
var response = await client.ListWitnessesAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Convert to internal DTOs and apply deterministic ordering
|
||||
var witnesses = response.Witnesses
|
||||
.Select(w => new WitnessListItemDto
|
||||
{
|
||||
WitnessId = w.WitnessId,
|
||||
CveId = w.VulnerabilityId ?? "N/A",
|
||||
PackageName = ExtractPackageName(w.ComponentPurl),
|
||||
ConfidenceTier = tier ?? "N/A",
|
||||
Entrypoint = w.Entrypoint ?? "N/A",
|
||||
Sink = w.Sink ?? "N/A"
|
||||
})
|
||||
.OrderBy(w => w.CveId, StringComparer.Ordinal)
|
||||
.ThenBy(w => w.WitnessId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case "json":
|
||||
var json = JsonSerializer.Serialize(new { witnesses, total = witnesses.Length }, WitnessJsonOptions);
|
||||
var json = JsonSerializer.Serialize(new { witnesses, total = response.TotalCount }, WitnessJsonOptions);
|
||||
console.WriteLine(json);
|
||||
break;
|
||||
default:
|
||||
WriteWitnessListTable(console, witnesses);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for `witness export` command.
|
||||
/// Sprint: SPRINT_20260112_014_CLI_witness_commands (CLI-WIT-003)
|
||||
/// </summary>
|
||||
internal static async Task HandleWitnessExportAsync(
|
||||
IServiceProvider services,
|
||||
@@ -222,24 +221,108 @@ internal static partial class CommandHandlers
|
||||
if (outputPath != null) console.MarkupLine($"[dim]Output: {outputPath}[/]");
|
||||
}
|
||||
|
||||
// TODO: Replace with actual witness fetch and export
|
||||
var exportContent = format switch
|
||||
using var scope = services.CreateScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
|
||||
|
||||
var exportFormat = format switch
|
||||
{
|
||||
"sarif" => GenerateWitnessSarif(witnessId),
|
||||
_ => GenerateWitnessJson(witnessId, includeDsse)
|
||||
"sarif" => WitnessExportFormat.Sarif,
|
||||
"dsse" => WitnessExportFormat.Dsse,
|
||||
_ => includeDsse ? WitnessExportFormat.Dsse : WitnessExportFormat.Json
|
||||
};
|
||||
|
||||
if (outputPath != null)
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(outputPath, exportContent, cancellationToken);
|
||||
console.MarkupLine($"[green]Exported to {outputPath}[/]");
|
||||
await using var stream = await client.DownloadWitnessAsync(witnessId, exportFormat, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (outputPath != null)
|
||||
{
|
||||
await using var fileStream = File.Create(outputPath);
|
||||
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
||||
console.MarkupLine($"[green]Exported to {outputPath}[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var reader = new StreamReader(stream);
|
||||
var content = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
console.WriteLine(content);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
console.WriteLine(exportContent);
|
||||
console.MarkupLine($"[red]Export failed: {ex.Message}[/]");
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractPackageName(string? purl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(purl)) return "N/A";
|
||||
// Extract name from PURL like pkg:nuget/Newtonsoft.Json@12.0.3
|
||||
var parts = purl.Split('/');
|
||||
if (parts.Length < 2) return purl;
|
||||
var nameVersion = parts[^1].Split('@');
|
||||
return nameVersion[0];
|
||||
}
|
||||
|
||||
private static WitnessDto ConvertToWitnessDto(WitnessDetailResponse response)
|
||||
{
|
||||
return new WitnessDto
|
||||
{
|
||||
WitnessId = response.WitnessId,
|
||||
WitnessSchema = response.WitnessSchema ?? "stellaops.witness.v1",
|
||||
CveId = response.Vuln?.Id ?? "N/A",
|
||||
PackageName = ExtractPackageName(response.Artifact?.ComponentPurl),
|
||||
PackageVersion = ExtractPackageVersion(response.Artifact?.ComponentPurl),
|
||||
ConfidenceTier = "confirmed", // TODO: map from response
|
||||
ObservedAt = response.ObservedAt.ToString("O", CultureInfo.InvariantCulture),
|
||||
Entrypoint = new WitnessEntrypointDto
|
||||
{
|
||||
Type = response.Entrypoint?.Kind ?? "unknown",
|
||||
Route = response.Entrypoint?.Name ?? "N/A",
|
||||
Symbol = response.Entrypoint?.SymbolId ?? "N/A",
|
||||
File = null,
|
||||
Line = 0
|
||||
},
|
||||
Sink = new WitnessSinkDto
|
||||
{
|
||||
Symbol = response.Sink?.Symbol ?? "N/A",
|
||||
Package = ExtractPackageName(response.Artifact?.ComponentPurl),
|
||||
IsTrigger = true
|
||||
},
|
||||
Path = (response.Path ?? [])
|
||||
.Select(p => new PathStepDto
|
||||
{
|
||||
Symbol = p.Symbol ?? p.SymbolId ?? "N/A",
|
||||
File = p.File,
|
||||
Line = p.Line ?? 0,
|
||||
Package = null
|
||||
})
|
||||
.ToArray(),
|
||||
Gates = (response.Gates ?? [])
|
||||
.Select(g => new GateDto
|
||||
{
|
||||
Type = g.Type ?? "unknown",
|
||||
Detail = g.Detail ?? "",
|
||||
Confidence = (decimal)g.Confidence
|
||||
})
|
||||
.ToArray(),
|
||||
Evidence = new WitnessEvidenceDto
|
||||
{
|
||||
CallgraphDigest = response.Evidence?.CallgraphDigest ?? "N/A",
|
||||
SurfaceDigest = response.Evidence?.SurfaceDigest ?? "N/A",
|
||||
SignedBy = response.DsseEnvelope?.Signatures?.FirstOrDefault()?.KeyId ?? "unsigned"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string ExtractPackageVersion(string? purl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(purl)) return "N/A";
|
||||
var parts = purl.Split('@');
|
||||
return parts.Length > 1 ? parts[^1] : "N/A";
|
||||
}
|
||||
|
||||
private static void WriteWitnessText(IAnsiConsole console, WitnessDto witness, bool pathOnly, bool noColor)
|
||||
{
|
||||
if (!pathOnly)
|
||||
@@ -381,58 +464,6 @@ internal static partial class CommandHandlers
|
||||
console.Write(table);
|
||||
}
|
||||
|
||||
private static string GenerateWitnessJson(string witnessId, bool includeDsse)
|
||||
{
|
||||
var witness = new
|
||||
{
|
||||
witness_schema = "stellaops.witness.v1",
|
||||
witness_id = witnessId,
|
||||
artifact = new { sbom_digest = "sha256:...", component_purl = "pkg:nuget/Newtonsoft.Json@12.0.3" },
|
||||
vuln = new { id = "CVE-2024-12345", source = "NVD" },
|
||||
entrypoint = new { type = "http", route = "GET /api/users/{id}" },
|
||||
path = new[] { new { symbol = "UserController.GetUser" }, new { symbol = "JsonConvert.DeserializeObject" } },
|
||||
evidence = new { callgraph_digest = "blake3:...", surface_digest = "sha256:..." }
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(witness, WitnessJsonOptions);
|
||||
}
|
||||
|
||||
private static string GenerateWitnessSarif(string witnessId)
|
||||
{
|
||||
var sarif = new
|
||||
{
|
||||
version = "2.1.0",
|
||||
schema = "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
runs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
tool = new
|
||||
{
|
||||
driver = new
|
||||
{
|
||||
name = "StellaOps Reachability",
|
||||
version = "1.0.0",
|
||||
informationUri = "https://stellaops.dev"
|
||||
}
|
||||
},
|
||||
results = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
ruleId = "REACH001",
|
||||
level = "warning",
|
||||
message = new { text = "Reachable vulnerability: CVE-2024-12345" },
|
||||
properties = new { witnessId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(sarif, WitnessJsonOptions);
|
||||
}
|
||||
|
||||
// DTO classes for witness commands
|
||||
private sealed record WitnessDto
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user