up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -56,6 +56,7 @@ internal static class CommandFactory
|
||||
root.Add(BuildKmsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildVulnCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildVexCommand(services, options, verboseOption, cancellationToken));
|
||||
root.Add(BuildDecisionCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildCryptoCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildAttestCommand(services, verboseOption, cancellationToken));
|
||||
@@ -74,11 +75,13 @@ internal static class CommandFactory
|
||||
root.Add(BuildCvssCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildRiskCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildGraphCommand(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(BuildDevPortalCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSymbolsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(SystemCommandBuilder.BuildSystemCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
|
||||
@@ -3868,11 +3871,32 @@ internal static class CommandFactory
|
||||
{
|
||||
Description = "Emit raw JSON payload instead of formatted output."
|
||||
};
|
||||
// GAP-VEX-006: Evidence display options
|
||||
var showCallPathsOption = new Option<bool>("--call-paths")
|
||||
{
|
||||
Description = "Include reachability call paths in the output."
|
||||
};
|
||||
var showGraphHashOption = new Option<bool>("--graph-hash")
|
||||
{
|
||||
Description = "Include call graph hash and CAS URI in the output."
|
||||
};
|
||||
var showRuntimeHitsOption = new Option<bool>("--runtime-hits")
|
||||
{
|
||||
Description = "Include runtime execution hits from probes."
|
||||
};
|
||||
var showFullEvidenceOption = new Option<bool>("--full-evidence")
|
||||
{
|
||||
Description = "Include all evidence types (call paths, graph hash, runtime hits, DSSE pointers)."
|
||||
};
|
||||
|
||||
show.Add(showVulnIdArg);
|
||||
show.Add(showProductKeyArg);
|
||||
show.Add(showTenantOption);
|
||||
show.Add(showJsonOption);
|
||||
show.Add(showCallPathsOption);
|
||||
show.Add(showGraphHashOption);
|
||||
show.Add(showRuntimeHitsOption);
|
||||
show.Add(showFullEvidenceOption);
|
||||
|
||||
show.SetAction((parseResult, _) =>
|
||||
{
|
||||
@@ -3880,14 +3904,29 @@ internal static class CommandFactory
|
||||
var productKey = parseResult.GetValue(showProductKeyArg) ?? string.Empty;
|
||||
var tenant = parseResult.GetValue(showTenantOption);
|
||||
var emitJson = parseResult.GetValue(showJsonOption);
|
||||
var includeCallPaths = parseResult.GetValue(showCallPathsOption);
|
||||
var includeGraphHash = parseResult.GetValue(showGraphHashOption);
|
||||
var includeRuntimeHits = parseResult.GetValue(showRuntimeHitsOption);
|
||||
var fullEvidence = parseResult.GetValue(showFullEvidenceOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
// Full evidence enables all flags
|
||||
if (fullEvidence)
|
||||
{
|
||||
includeCallPaths = true;
|
||||
includeGraphHash = true;
|
||||
includeRuntimeHits = true;
|
||||
}
|
||||
|
||||
return CommandHandlers.HandleVexConsensusShowAsync(
|
||||
services,
|
||||
vulnId,
|
||||
productKey,
|
||||
tenant,
|
||||
emitJson,
|
||||
includeCallPaths,
|
||||
includeGraphHash,
|
||||
includeRuntimeHits,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
@@ -4269,9 +4308,336 @@ internal static class CommandFactory
|
||||
obs.Add(linkset);
|
||||
vex.Add(obs);
|
||||
|
||||
// UI-VEX-401-032: VEX explain command for comprehensive decision explanation
|
||||
var explain = new Command("explain", "Explain a VEX decision with full reachability evidence and verification status.");
|
||||
|
||||
var explainVulnIdArg = new Argument<string>("vulnerability-id")
|
||||
{
|
||||
Description = "Vulnerability identifier (e.g., CVE-2024-1234)."
|
||||
};
|
||||
var explainProductKeyOption = new Option<string>("--product-key", new[] { "-p" })
|
||||
{
|
||||
Description = "Product key for the decision.",
|
||||
Required = true
|
||||
};
|
||||
var explainTenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
||||
{
|
||||
Description = "Tenant identifier."
|
||||
};
|
||||
var explainCallPathsOption = new Option<bool>("--call-paths")
|
||||
{
|
||||
Description = "Include call path evidence with full frame details."
|
||||
};
|
||||
explainCallPathsOption.SetDefaultValue(true);
|
||||
var explainRuntimeHitsOption = new Option<bool>("--runtime-hits")
|
||||
{
|
||||
Description = "Include runtime execution hit evidence."
|
||||
};
|
||||
explainRuntimeHitsOption.SetDefaultValue(true);
|
||||
var explainGraphOption = new Option<bool>("--graph")
|
||||
{
|
||||
Description = "Include reachability graph metadata."
|
||||
};
|
||||
explainGraphOption.SetDefaultValue(true);
|
||||
var explainDsseOption = new Option<bool>("--dsse")
|
||||
{
|
||||
Description = "Include DSSE envelope details."
|
||||
};
|
||||
var explainRekorOption = new Option<bool>("--rekor")
|
||||
{
|
||||
Description = "Include Rekor transparency log entry details."
|
||||
};
|
||||
var explainVerifyOption = new Option<bool>("--verify")
|
||||
{
|
||||
Description = "Verify attestation signatures and Rekor inclusion proofs."
|
||||
};
|
||||
var explainOfflineOption = new Option<bool>("--offline")
|
||||
{
|
||||
Description = "Perform verification using embedded proofs only (air-gapped mode)."
|
||||
};
|
||||
var explainJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output as JSON for machine processing."
|
||||
};
|
||||
|
||||
explain.Add(explainVulnIdArg);
|
||||
explain.Add(explainProductKeyOption);
|
||||
explain.Add(explainTenantOption);
|
||||
explain.Add(explainCallPathsOption);
|
||||
explain.Add(explainRuntimeHitsOption);
|
||||
explain.Add(explainGraphOption);
|
||||
explain.Add(explainDsseOption);
|
||||
explain.Add(explainRekorOption);
|
||||
explain.Add(explainVerifyOption);
|
||||
explain.Add(explainOfflineOption);
|
||||
explain.Add(explainJsonOption);
|
||||
explain.Add(verboseOption);
|
||||
|
||||
explain.SetAction((parseResult, _) =>
|
||||
{
|
||||
var vulnId = parseResult.GetValue(explainVulnIdArg) ?? string.Empty;
|
||||
var productKey = parseResult.GetValue(explainProductKeyOption) ?? string.Empty;
|
||||
var tenant = parseResult.GetValue(explainTenantOption);
|
||||
var includeCallPaths = parseResult.GetValue(explainCallPathsOption);
|
||||
var includeRuntimeHits = parseResult.GetValue(explainRuntimeHitsOption);
|
||||
var includeGraph = parseResult.GetValue(explainGraphOption);
|
||||
var includeDsse = parseResult.GetValue(explainDsseOption);
|
||||
var includeRekor = parseResult.GetValue(explainRekorOption);
|
||||
var verify = parseResult.GetValue(explainVerifyOption);
|
||||
var offline = parseResult.GetValue(explainOfflineOption);
|
||||
var emitJson = parseResult.GetValue(explainJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleVexExplainAsync(
|
||||
services,
|
||||
vulnId,
|
||||
productKey,
|
||||
tenant,
|
||||
includeCallPaths,
|
||||
includeRuntimeHits,
|
||||
includeGraph,
|
||||
includeDsse,
|
||||
includeRekor,
|
||||
verify,
|
||||
offline,
|
||||
emitJson,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
vex.Add(explain);
|
||||
|
||||
return vex;
|
||||
}
|
||||
|
||||
// CLI-VEX-401-011: VEX decision commands with DSSE/Rekor integration
|
||||
private static Command BuildDecisionCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var decision = new Command("decision", "Manage VEX decisions with DSSE signing and Rekor transparency.");
|
||||
|
||||
// decision export
|
||||
var export = new Command("export", "Export VEX decisions as OpenVEX documents with optional DSSE signing.");
|
||||
|
||||
var expTenantOption = new Option<string>("--tenant", new[] { "-t" })
|
||||
{
|
||||
Description = "Tenant identifier.",
|
||||
Required = true
|
||||
};
|
||||
var expScanIdOption = new Option<string?>("--scan-id")
|
||||
{
|
||||
Description = "Filter by scan identifier."
|
||||
};
|
||||
var expVulnIdsOption = new Option<string[]>("--vuln-id")
|
||||
{
|
||||
Description = "Filter by vulnerability identifiers (repeatable).",
|
||||
Arity = ArgumentArity.ZeroOrMore
|
||||
};
|
||||
var expPurlsOption = new Option<string[]>("--purl")
|
||||
{
|
||||
Description = "Filter by Package URLs (repeatable).",
|
||||
Arity = ArgumentArity.ZeroOrMore
|
||||
};
|
||||
var expStatusesOption = new Option<string[]>("--status")
|
||||
{
|
||||
Description = "Filter by VEX status (not_affected, affected, fixed, under_investigation). Repeatable.",
|
||||
Arity = ArgumentArity.ZeroOrMore
|
||||
};
|
||||
var expOutputOption = new Option<string>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output file path for the OpenVEX document.",
|
||||
Required = true
|
||||
};
|
||||
var expFormatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format (openvex, dsse, ndjson). Default: openvex."
|
||||
};
|
||||
expFormatOption.SetDefaultValue("openvex");
|
||||
var expSignOption = new Option<bool>("--sign", new[] { "-s" })
|
||||
{
|
||||
Description = "Sign the output with DSSE envelope."
|
||||
};
|
||||
var expRekorOption = new Option<bool>("--rekor")
|
||||
{
|
||||
Description = "Submit DSSE envelope to Rekor transparency log."
|
||||
};
|
||||
var expIncludeEvidenceOption = new Option<bool>("--include-evidence")
|
||||
{
|
||||
Description = "Include reachability evidence blocks in output."
|
||||
};
|
||||
expIncludeEvidenceOption.SetDefaultValue(true);
|
||||
var expJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output metadata as JSON to stdout."
|
||||
};
|
||||
|
||||
export.Add(expTenantOption);
|
||||
export.Add(expScanIdOption);
|
||||
export.Add(expVulnIdsOption);
|
||||
export.Add(expPurlsOption);
|
||||
export.Add(expStatusesOption);
|
||||
export.Add(expOutputOption);
|
||||
export.Add(expFormatOption);
|
||||
export.Add(expSignOption);
|
||||
export.Add(expRekorOption);
|
||||
export.Add(expIncludeEvidenceOption);
|
||||
export.Add(expJsonOption);
|
||||
|
||||
export.SetAction((parseResult, _) =>
|
||||
{
|
||||
var tenant = parseResult.GetValue(expTenantOption) ?? string.Empty;
|
||||
var scanId = parseResult.GetValue(expScanIdOption);
|
||||
var vulnIds = parseResult.GetValue(expVulnIdsOption) ?? Array.Empty<string>();
|
||||
var purls = parseResult.GetValue(expPurlsOption) ?? Array.Empty<string>();
|
||||
var statuses = parseResult.GetValue(expStatusesOption) ?? Array.Empty<string>();
|
||||
var output = parseResult.GetValue(expOutputOption) ?? string.Empty;
|
||||
var format = parseResult.GetValue(expFormatOption) ?? "openvex";
|
||||
var sign = parseResult.GetValue(expSignOption);
|
||||
var rekor = parseResult.GetValue(expRekorOption);
|
||||
var includeEvidence = parseResult.GetValue(expIncludeEvidenceOption);
|
||||
var emitJson = parseResult.GetValue(expJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleDecisionExportAsync(
|
||||
services,
|
||||
tenant,
|
||||
scanId,
|
||||
vulnIds,
|
||||
purls,
|
||||
statuses,
|
||||
output,
|
||||
format,
|
||||
sign,
|
||||
rekor,
|
||||
includeEvidence,
|
||||
emitJson,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
decision.Add(export);
|
||||
|
||||
// decision verify
|
||||
var verify = new Command("verify", "Verify DSSE signature and optional Rekor inclusion proof of a VEX decision document.");
|
||||
|
||||
var verifyFileArg = new Argument<string>("file")
|
||||
{
|
||||
Description = "Path to the VEX document or DSSE envelope to verify."
|
||||
};
|
||||
var verifyDigestOption = new Option<string?>("--digest")
|
||||
{
|
||||
Description = "Expected payload digest (sha256:...) to verify."
|
||||
};
|
||||
var verifyRekorOption = new Option<bool>("--rekor")
|
||||
{
|
||||
Description = "Verify Rekor inclusion proof."
|
||||
};
|
||||
var verifyRekorUuidOption = new Option<string?>("--rekor-uuid")
|
||||
{
|
||||
Description = "Rekor entry UUID for inclusion verification."
|
||||
};
|
||||
var verifyPublicKeyOption = new Option<string?>("--public-key")
|
||||
{
|
||||
Description = "Path to public key file for offline signature verification."
|
||||
};
|
||||
var verifyJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output verification result as JSON."
|
||||
};
|
||||
|
||||
verify.Add(verifyFileArg);
|
||||
verify.Add(verifyDigestOption);
|
||||
verify.Add(verifyRekorOption);
|
||||
verify.Add(verifyRekorUuidOption);
|
||||
verify.Add(verifyPublicKeyOption);
|
||||
verify.Add(verifyJsonOption);
|
||||
|
||||
verify.SetAction((parseResult, _) =>
|
||||
{
|
||||
var file = parseResult.GetValue(verifyFileArg) ?? string.Empty;
|
||||
var digest = parseResult.GetValue(verifyDigestOption);
|
||||
var verifyRekor = parseResult.GetValue(verifyRekorOption);
|
||||
var rekorUuid = parseResult.GetValue(verifyRekorUuidOption);
|
||||
var publicKey = parseResult.GetValue(verifyPublicKeyOption);
|
||||
var emitJson = parseResult.GetValue(verifyJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleDecisionVerifyAsync(
|
||||
services,
|
||||
file,
|
||||
digest,
|
||||
verifyRekor,
|
||||
rekorUuid,
|
||||
publicKey,
|
||||
emitJson,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
decision.Add(verify);
|
||||
|
||||
// decision compare
|
||||
var compare = new Command("compare", "Compare two VEX decision documents and show differences.");
|
||||
|
||||
var compareBaseArg = new Argument<string>("base")
|
||||
{
|
||||
Description = "Path to the base VEX document."
|
||||
};
|
||||
var compareTargetArg = new Argument<string>("target")
|
||||
{
|
||||
Description = "Path to the target VEX document to compare against base."
|
||||
};
|
||||
var compareOutputOption = new Option<string?>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output file path for the diff report."
|
||||
};
|
||||
var compareFormatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format (text, json, markdown). Default: text."
|
||||
};
|
||||
compareFormatOption.SetDefaultValue("text");
|
||||
var compareShowUnchangedOption = new Option<bool>("--show-unchanged")
|
||||
{
|
||||
Description = "Include unchanged statements in output."
|
||||
};
|
||||
var compareSummaryOnlyOption = new Option<bool>("--summary-only")
|
||||
{
|
||||
Description = "Show only summary counts, not detailed diffs."
|
||||
};
|
||||
|
||||
compare.Add(compareBaseArg);
|
||||
compare.Add(compareTargetArg);
|
||||
compare.Add(compareOutputOption);
|
||||
compare.Add(compareFormatOption);
|
||||
compare.Add(compareShowUnchangedOption);
|
||||
compare.Add(compareSummaryOnlyOption);
|
||||
|
||||
compare.SetAction((parseResult, _) =>
|
||||
{
|
||||
var basePath = parseResult.GetValue(compareBaseArg) ?? string.Empty;
|
||||
var targetPath = parseResult.GetValue(compareTargetArg) ?? string.Empty;
|
||||
var output = parseResult.GetValue(compareOutputOption);
|
||||
var format = parseResult.GetValue(compareFormatOption) ?? "text";
|
||||
var showUnchanged = parseResult.GetValue(compareShowUnchangedOption);
|
||||
var summaryOnly = parseResult.GetValue(compareSummaryOnlyOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleDecisionCompareAsync(
|
||||
services,
|
||||
basePath,
|
||||
targetPath,
|
||||
output,
|
||||
format,
|
||||
showUnchanged,
|
||||
summaryOnly,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
decision.Add(compare);
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
private static Command BuildConfigCommand(StellaOpsCliOptions options)
|
||||
{
|
||||
var config = new Command("config", "Inspect CLI configuration state.");
|
||||
@@ -10458,6 +10824,120 @@ internal static class CommandFactory
|
||||
return reachability;
|
||||
}
|
||||
|
||||
// UI-CLI-401-007: stella graph command with DSSE pointers, runtime hits, predicates, counterfactuals
|
||||
private static Command BuildGraphCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var graph = new Command("graph", "Call graph evidence commands.");
|
||||
|
||||
var tenantOption = new Option<string?>("--tenant", "-t")
|
||||
{
|
||||
Description = "Tenant context for the operation."
|
||||
};
|
||||
|
||||
var jsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output in JSON format."
|
||||
};
|
||||
|
||||
// stella graph explain
|
||||
var explain = new Command("explain", "Explain call graph reachability with signed evidence.");
|
||||
var graphIdOption = new Option<string>("--graph-id", "-g")
|
||||
{
|
||||
Description = "Call graph identifier.",
|
||||
Required = true
|
||||
};
|
||||
var vulnerabilityIdOption = new Option<string?>("--vuln-id", "-v")
|
||||
{
|
||||
Description = "Vulnerability identifier to explain."
|
||||
};
|
||||
var packagePurlOption = new Option<string?>("--purl")
|
||||
{
|
||||
Description = "Package URL to explain."
|
||||
};
|
||||
var includeCallPathsOption = new Option<bool>("--call-paths")
|
||||
{
|
||||
Description = "Include detailed signed call paths in the explanation."
|
||||
};
|
||||
var includeRuntimeHitsOption = new Option<bool>("--runtime-hits")
|
||||
{
|
||||
Description = "Include runtime execution hits from instrumentation probes."
|
||||
};
|
||||
var includePredicatesOption = new Option<bool>("--predicates")
|
||||
{
|
||||
Description = "Include semantic predicates attached to evidence."
|
||||
};
|
||||
var includeDsseOption = new Option<bool>("--dsse")
|
||||
{
|
||||
Description = "Include DSSE envelope pointers and Rekor log entries."
|
||||
};
|
||||
var includeCounterfactualsOption = new Option<bool>("--counterfactuals")
|
||||
{
|
||||
Description = "Include counterfactual controls showing what-if scenarios."
|
||||
};
|
||||
var fullEvidenceOption = new Option<bool>("--full-evidence")
|
||||
{
|
||||
Description = "Include all evidence types (call paths, runtime hits, predicates, DSSE, counterfactuals)."
|
||||
};
|
||||
|
||||
explain.Add(tenantOption);
|
||||
explain.Add(graphIdOption);
|
||||
explain.Add(vulnerabilityIdOption);
|
||||
explain.Add(packagePurlOption);
|
||||
explain.Add(includeCallPathsOption);
|
||||
explain.Add(includeRuntimeHitsOption);
|
||||
explain.Add(includePredicatesOption);
|
||||
explain.Add(includeDsseOption);
|
||||
explain.Add(includeCounterfactualsOption);
|
||||
explain.Add(fullEvidenceOption);
|
||||
explain.Add(jsonOption);
|
||||
explain.Add(verboseOption);
|
||||
|
||||
explain.SetAction((parseResult, _) =>
|
||||
{
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
var graphId = parseResult.GetValue(graphIdOption) ?? string.Empty;
|
||||
var vulnerabilityId = parseResult.GetValue(vulnerabilityIdOption);
|
||||
var packagePurl = parseResult.GetValue(packagePurlOption);
|
||||
var includeCallPaths = parseResult.GetValue(includeCallPathsOption);
|
||||
var includeRuntimeHits = parseResult.GetValue(includeRuntimeHitsOption);
|
||||
var includePredicates = parseResult.GetValue(includePredicatesOption);
|
||||
var includeDsse = parseResult.GetValue(includeDsseOption);
|
||||
var includeCounterfactuals = parseResult.GetValue(includeCounterfactualsOption);
|
||||
var fullEvidence = parseResult.GetValue(fullEvidenceOption);
|
||||
var emitJson = parseResult.GetValue(jsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
// Full evidence enables all flags
|
||||
if (fullEvidence)
|
||||
{
|
||||
includeCallPaths = true;
|
||||
includeRuntimeHits = true;
|
||||
includePredicates = true;
|
||||
includeDsse = true;
|
||||
includeCounterfactuals = true;
|
||||
}
|
||||
|
||||
return CommandHandlers.HandleGraphExplainAsync(
|
||||
services,
|
||||
tenant,
|
||||
graphId,
|
||||
vulnerabilityId,
|
||||
packagePurl,
|
||||
includeCallPaths,
|
||||
includeRuntimeHits,
|
||||
includePredicates,
|
||||
includeDsse,
|
||||
includeCounterfactuals,
|
||||
emitJson,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
graph.Add(explain);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
// CLI-SDK-63-001: stella api command
|
||||
private static Command BuildApiCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -11071,4 +11551,316 @@ internal static class CommandFactory
|
||||
|
||||
return devportal;
|
||||
}
|
||||
|
||||
// SYMS-BUNDLE-401-014: Symbol bundle commands for air-gapped installations
|
||||
private static Command BuildSymbolsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var symbols = new Command("symbols", "Manage symbol bundles for air-gapped installations.");
|
||||
|
||||
// symbols bundle build
|
||||
var bundleBuild = new Command("bundle", "Build a deterministic symbol bundle.");
|
||||
|
||||
var bundleNameOption = new Option<string>("--name", new[] { "-n" })
|
||||
{
|
||||
Description = "Bundle name.",
|
||||
Required = true
|
||||
};
|
||||
var bundleVersionOption = new Option<string>("--version")
|
||||
{
|
||||
Description = "Bundle version (SemVer).",
|
||||
Required = true
|
||||
};
|
||||
var bundleSourceOption = new Option<string>("--source", new[] { "-s" })
|
||||
{
|
||||
Description = "Source directory containing symbol manifests.",
|
||||
Required = true
|
||||
};
|
||||
var bundleOutputOption = new Option<string>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output directory for bundle archive.",
|
||||
Required = true
|
||||
};
|
||||
var bundlePlatformOption = new Option<string?>("--platform")
|
||||
{
|
||||
Description = "Filter symbols by platform (e.g., linux-x64, win-x64)."
|
||||
};
|
||||
var bundleTenantOption = new Option<string?>("--tenant")
|
||||
{
|
||||
Description = "Filter symbols by tenant ID."
|
||||
};
|
||||
var bundleSignOption = new Option<bool>("--sign")
|
||||
{
|
||||
Description = "Sign the bundle with DSSE."
|
||||
};
|
||||
var bundleKeyPathOption = new Option<string?>("--key")
|
||||
{
|
||||
Description = "Path to signing key (PEM-encoded private key)."
|
||||
};
|
||||
var bundleKeyIdOption = new Option<string?>("--key-id")
|
||||
{
|
||||
Description = "Key ID for DSSE signature."
|
||||
};
|
||||
var bundleAlgorithmOption = new Option<string>("--algorithm")
|
||||
{
|
||||
Description = "Signing algorithm (ecdsa-p256, ed25519, rsa-pss-sha256)."
|
||||
};
|
||||
bundleAlgorithmOption.SetDefaultValue("ecdsa-p256");
|
||||
var bundleRekorOption = new Option<bool>("--rekor")
|
||||
{
|
||||
Description = "Submit to Rekor transparency log."
|
||||
};
|
||||
var bundleRekorUrlOption = new Option<string>("--rekor-url")
|
||||
{
|
||||
Description = "Rekor server URL."
|
||||
};
|
||||
bundleRekorUrlOption.SetDefaultValue("https://rekor.sigstore.dev");
|
||||
var bundleFormatOption = new Option<string>("--format")
|
||||
{
|
||||
Description = "Bundle format (zip, tar.gz)."
|
||||
};
|
||||
bundleFormatOption.SetDefaultValue("zip");
|
||||
var bundleCompressionOption = new Option<int>("--compression")
|
||||
{
|
||||
Description = "Compression level (0-9)."
|
||||
};
|
||||
bundleCompressionOption.SetDefaultValue(6);
|
||||
var bundleJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output result as JSON."
|
||||
};
|
||||
|
||||
bundleBuild.Add(bundleNameOption);
|
||||
bundleBuild.Add(bundleVersionOption);
|
||||
bundleBuild.Add(bundleSourceOption);
|
||||
bundleBuild.Add(bundleOutputOption);
|
||||
bundleBuild.Add(bundlePlatformOption);
|
||||
bundleBuild.Add(bundleTenantOption);
|
||||
bundleBuild.Add(bundleSignOption);
|
||||
bundleBuild.Add(bundleKeyPathOption);
|
||||
bundleBuild.Add(bundleKeyIdOption);
|
||||
bundleBuild.Add(bundleAlgorithmOption);
|
||||
bundleBuild.Add(bundleRekorOption);
|
||||
bundleBuild.Add(bundleRekorUrlOption);
|
||||
bundleBuild.Add(bundleFormatOption);
|
||||
bundleBuild.Add(bundleCompressionOption);
|
||||
bundleBuild.Add(bundleJsonOption);
|
||||
bundleBuild.Add(verboseOption);
|
||||
|
||||
bundleBuild.SetAction((parseResult, _) =>
|
||||
{
|
||||
var name = parseResult.GetValue(bundleNameOption)!;
|
||||
var version = parseResult.GetValue(bundleVersionOption)!;
|
||||
var source = parseResult.GetValue(bundleSourceOption)!;
|
||||
var output = parseResult.GetValue(bundleOutputOption)!;
|
||||
var platform = parseResult.GetValue(bundlePlatformOption);
|
||||
var tenant = parseResult.GetValue(bundleTenantOption);
|
||||
var sign = parseResult.GetValue(bundleSignOption);
|
||||
var keyPath = parseResult.GetValue(bundleKeyPathOption);
|
||||
var keyId = parseResult.GetValue(bundleKeyIdOption);
|
||||
var algorithm = parseResult.GetValue(bundleAlgorithmOption)!;
|
||||
var rekor = parseResult.GetValue(bundleRekorOption);
|
||||
var rekorUrl = parseResult.GetValue(bundleRekorUrlOption)!;
|
||||
var format = parseResult.GetValue(bundleFormatOption)!;
|
||||
var compression = parseResult.GetValue(bundleCompressionOption);
|
||||
var json = parseResult.GetValue(bundleJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleSymbolBundleBuildAsync(
|
||||
services,
|
||||
name,
|
||||
version,
|
||||
source,
|
||||
output,
|
||||
platform,
|
||||
tenant,
|
||||
sign,
|
||||
keyPath,
|
||||
keyId,
|
||||
algorithm,
|
||||
rekor,
|
||||
rekorUrl,
|
||||
format,
|
||||
compression,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
symbols.Add(bundleBuild);
|
||||
|
||||
// symbols verify
|
||||
var verify = new Command("verify", "Verify a symbol bundle's integrity and signatures.");
|
||||
|
||||
var verifyBundleOption = new Option<string>("--bundle", new[] { "-b" })
|
||||
{
|
||||
Description = "Path to bundle archive.",
|
||||
Required = true
|
||||
};
|
||||
var verifyPublicKeyOption = new Option<string?>("--public-key")
|
||||
{
|
||||
Description = "Path to public key for signature verification."
|
||||
};
|
||||
var verifyRekorOfflineOption = new Option<bool>("--rekor-offline")
|
||||
{
|
||||
Description = "Verify Rekor inclusion proof offline."
|
||||
};
|
||||
verifyRekorOfflineOption.SetDefaultValue(true);
|
||||
var verifyRekorKeyOption = new Option<string?>("--rekor-key")
|
||||
{
|
||||
Description = "Path to Rekor public key for offline verification."
|
||||
};
|
||||
var verifyHashesOption = new Option<bool>("--verify-hashes")
|
||||
{
|
||||
Description = "Verify all blob hashes."
|
||||
};
|
||||
verifyHashesOption.SetDefaultValue(true);
|
||||
var verifyJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output result as JSON."
|
||||
};
|
||||
|
||||
verify.Add(verifyBundleOption);
|
||||
verify.Add(verifyPublicKeyOption);
|
||||
verify.Add(verifyRekorOfflineOption);
|
||||
verify.Add(verifyRekorKeyOption);
|
||||
verify.Add(verifyHashesOption);
|
||||
verify.Add(verifyJsonOption);
|
||||
verify.Add(verboseOption);
|
||||
|
||||
verify.SetAction((parseResult, _) =>
|
||||
{
|
||||
var bundlePath = parseResult.GetValue(verifyBundleOption)!;
|
||||
var publicKeyPath = parseResult.GetValue(verifyPublicKeyOption);
|
||||
var rekorOffline = parseResult.GetValue(verifyRekorOfflineOption);
|
||||
var rekorKeyPath = parseResult.GetValue(verifyRekorKeyOption);
|
||||
var verifyHashes = parseResult.GetValue(verifyHashesOption);
|
||||
var json = parseResult.GetValue(verifyJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleSymbolBundleVerifyAsync(
|
||||
services,
|
||||
bundlePath,
|
||||
publicKeyPath,
|
||||
rekorOffline,
|
||||
rekorKeyPath,
|
||||
verifyHashes,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
symbols.Add(verify);
|
||||
|
||||
// symbols extract
|
||||
var extract = new Command("extract", "Extract symbols from a bundle.");
|
||||
|
||||
var extractBundleOption = new Option<string>("--bundle", new[] { "-b" })
|
||||
{
|
||||
Description = "Path to bundle archive.",
|
||||
Required = true
|
||||
};
|
||||
var extractOutputOption = new Option<string>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output directory.",
|
||||
Required = true
|
||||
};
|
||||
var extractVerifyOption = new Option<bool>("--verify")
|
||||
{
|
||||
Description = "Verify bundle before extraction."
|
||||
};
|
||||
extractVerifyOption.SetDefaultValue(true);
|
||||
var extractPlatformOption = new Option<string?>("--platform")
|
||||
{
|
||||
Description = "Extract only symbols for this platform."
|
||||
};
|
||||
var extractOverwriteOption = new Option<bool>("--overwrite")
|
||||
{
|
||||
Description = "Overwrite existing files."
|
||||
};
|
||||
var extractManifestsOnlyOption = new Option<bool>("--manifests-only")
|
||||
{
|
||||
Description = "Extract only manifest files (not blobs)."
|
||||
};
|
||||
var extractJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output result as JSON."
|
||||
};
|
||||
|
||||
extract.Add(extractBundleOption);
|
||||
extract.Add(extractOutputOption);
|
||||
extract.Add(extractVerifyOption);
|
||||
extract.Add(extractPlatformOption);
|
||||
extract.Add(extractOverwriteOption);
|
||||
extract.Add(extractManifestsOnlyOption);
|
||||
extract.Add(extractJsonOption);
|
||||
extract.Add(verboseOption);
|
||||
|
||||
extract.SetAction((parseResult, _) =>
|
||||
{
|
||||
var bundlePath = parseResult.GetValue(extractBundleOption)!;
|
||||
var outputDir = parseResult.GetValue(extractOutputOption)!;
|
||||
var verifyFirst = parseResult.GetValue(extractVerifyOption);
|
||||
var platform = parseResult.GetValue(extractPlatformOption);
|
||||
var overwrite = parseResult.GetValue(extractOverwriteOption);
|
||||
var manifestsOnly = parseResult.GetValue(extractManifestsOnlyOption);
|
||||
var json = parseResult.GetValue(extractJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleSymbolBundleExtractAsync(
|
||||
services,
|
||||
bundlePath,
|
||||
outputDir,
|
||||
verifyFirst,
|
||||
platform,
|
||||
overwrite,
|
||||
manifestsOnly,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
symbols.Add(extract);
|
||||
|
||||
// symbols inspect
|
||||
var inspect = new Command("inspect", "Inspect bundle contents without extracting.");
|
||||
|
||||
var inspectBundleOption = new Option<string>("--bundle", new[] { "-b" })
|
||||
{
|
||||
Description = "Path to bundle archive.",
|
||||
Required = true
|
||||
};
|
||||
var inspectEntriesOption = new Option<bool>("--entries")
|
||||
{
|
||||
Description = "List all entries in the bundle."
|
||||
};
|
||||
var inspectJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output result as JSON."
|
||||
};
|
||||
|
||||
inspect.Add(inspectBundleOption);
|
||||
inspect.Add(inspectEntriesOption);
|
||||
inspect.Add(inspectJsonOption);
|
||||
inspect.Add(verboseOption);
|
||||
|
||||
inspect.SetAction((parseResult, _) =>
|
||||
{
|
||||
var bundlePath = parseResult.GetValue(inspectBundleOption)!;
|
||||
var showEntries = parseResult.GetValue(inspectEntriesOption);
|
||||
var json = parseResult.GetValue(inspectJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleSymbolBundleInspectAsync(
|
||||
services,
|
||||
bundlePath,
|
||||
showEntries,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
symbols.Add(inspect);
|
||||
|
||||
return symbols;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4357,6 +4357,61 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
?? new ReachabilityExplainResult();
|
||||
}
|
||||
|
||||
// UI-CLI-401-007: Graph explain with DSSE pointers, runtime hits, predicates, counterfactuals
|
||||
public async Task<GraphExplainResult> ExplainGraphAsync(GraphExplainRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
EnsureBackendConfigured();
|
||||
OfflineModeGuard.ThrowIfOffline("graph explain");
|
||||
|
||||
var queryParams = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.VulnerabilityId))
|
||||
queryParams.Add($"vulnerabilityId={Uri.EscapeDataString(request.VulnerabilityId)}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.PackagePurl))
|
||||
queryParams.Add($"packagePurl={Uri.EscapeDataString(request.PackagePurl)}");
|
||||
|
||||
if (request.IncludeCallPaths)
|
||||
queryParams.Add("includeCallPaths=true");
|
||||
|
||||
if (request.IncludeRuntimeHits)
|
||||
queryParams.Add("includeRuntimeHits=true");
|
||||
|
||||
if (request.IncludePredicates)
|
||||
queryParams.Add("includePredicates=true");
|
||||
|
||||
if (request.IncludeDsseEnvelopes)
|
||||
queryParams.Add("includeDsseEnvelopes=true");
|
||||
|
||||
if (request.IncludeCounterfactuals)
|
||||
queryParams.Add("includeCounterfactuals=true");
|
||||
|
||||
var query = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : "";
|
||||
var relative = $"api/graphs/{Uri.EscapeDataString(request.GraphId)}/explain{query}";
|
||||
|
||||
using var httpRequest = CreateRequest(HttpMethod.Get, relative);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Tenant))
|
||||
{
|
||||
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", request.Tenant.Trim());
|
||||
}
|
||||
|
||||
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new HttpRequestException($"Explain graph failed: {message}", null, response.StatusCode);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GraphExplainResult>(JsonOptions, cancellationToken).ConfigureAwait(false)
|
||||
?? new GraphExplainResult();
|
||||
}
|
||||
|
||||
// CLI-SDK-63-001: API spec operations
|
||||
public async Task<ApiSpecListResponse> ListApiSpecsAsync(string? tenant, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -4660,4 +4715,121 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
var result = await response.Content.ReadFromJsonAsync<SdkListResponse>(JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return result ?? new SdkListResponse { Success = false, Error = "Empty response" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports VEX decisions as OpenVEX documents with optional DSSE signing.
|
||||
/// </summary>
|
||||
public async Task<DecisionExportResponse> ExportDecisionsAsync(
|
||||
DecisionExportRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
try
|
||||
{
|
||||
var queryParams = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(request.ScanId))
|
||||
{
|
||||
queryParams.Add($"scanId={Uri.EscapeDataString(request.ScanId)}");
|
||||
}
|
||||
|
||||
if (request.VulnIds is { Count: > 0 })
|
||||
{
|
||||
foreach (var vulnId in request.VulnIds)
|
||||
{
|
||||
queryParams.Add($"vulnId={Uri.EscapeDataString(vulnId)}");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Purls is { Count: > 0 })
|
||||
{
|
||||
foreach (var purl in request.Purls)
|
||||
{
|
||||
queryParams.Add($"purl={Uri.EscapeDataString(purl)}");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Statuses is { Count: > 0 })
|
||||
{
|
||||
foreach (var status in request.Statuses)
|
||||
{
|
||||
queryParams.Add($"status={Uri.EscapeDataString(status)}");
|
||||
}
|
||||
}
|
||||
|
||||
queryParams.Add($"format={Uri.EscapeDataString(request.Format)}");
|
||||
queryParams.Add($"sign={request.Sign.ToString().ToLowerInvariant()}");
|
||||
queryParams.Add($"rekor={request.SubmitToRekor.ToString().ToLowerInvariant()}");
|
||||
queryParams.Add($"includeEvidence={request.IncludeEvidence.ToString().ToLowerInvariant()}");
|
||||
|
||||
var queryString = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : "";
|
||||
var url = $"{_options.BackendUrl}/api/v1/decisions/export{queryString}";
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
httpRequest.Headers.TryAddWithoutValidation("X-Tenant-Id", request.TenantId);
|
||||
await AuthorizeRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
return new DecisionExportResponse
|
||||
{
|
||||
Success = false,
|
||||
Error = message
|
||||
};
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Extract metadata from response headers
|
||||
response.Headers.TryGetValues("X-VEX-Digest", out var digestValues);
|
||||
response.Headers.TryGetValues("X-VEX-Rekor-Index", out var rekorIndexValues);
|
||||
response.Headers.TryGetValues("X-VEX-Rekor-UUID", out var rekorUuidValues);
|
||||
response.Headers.TryGetValues("X-VEX-Statement-Count", out var countValues);
|
||||
response.Headers.TryGetValues("X-VEX-Signed", out var signedValues);
|
||||
|
||||
var digest = digestValues?.FirstOrDefault();
|
||||
var rekorUuid = rekorUuidValues?.FirstOrDefault();
|
||||
long? rekorIndex = null;
|
||||
int statementCount = 0;
|
||||
bool signed = false;
|
||||
|
||||
if (rekorIndexValues?.FirstOrDefault() is { } indexStr && long.TryParse(indexStr, out var idx))
|
||||
{
|
||||
rekorIndex = idx;
|
||||
}
|
||||
|
||||
if (countValues?.FirstOrDefault() is { } countStr && int.TryParse(countStr, out var cnt))
|
||||
{
|
||||
statementCount = cnt;
|
||||
}
|
||||
|
||||
if (signedValues?.FirstOrDefault() is { } signedStr)
|
||||
{
|
||||
signed = signedStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return new DecisionExportResponse
|
||||
{
|
||||
Success = true,
|
||||
Content = content,
|
||||
Digest = digest,
|
||||
RekorLogIndex = rekorIndex,
|
||||
RekorUuid = rekorUuid,
|
||||
StatementCount = statementCount,
|
||||
Signed = signed
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new DecisionExportResponse
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,9 @@ internal interface IBackendOperationsClient
|
||||
Task<ReachabilityListResponse> ListReachabilityAnalysesAsync(ReachabilityListRequest request, CancellationToken cancellationToken);
|
||||
Task<ReachabilityExplainResult> ExplainReachabilityAsync(ReachabilityExplainRequest request, CancellationToken cancellationToken);
|
||||
|
||||
// UI-CLI-401-007: Graph explain with DSSE pointers, runtime hits, predicates, counterfactuals
|
||||
Task<GraphExplainResult> ExplainGraphAsync(GraphExplainRequest request, CancellationToken cancellationToken);
|
||||
|
||||
// CLI-SDK-63-001: API spec download
|
||||
Task<ApiSpecListResponse> ListApiSpecsAsync(string? tenant, CancellationToken cancellationToken);
|
||||
Task<ApiSpecDownloadResult> DownloadApiSpecAsync(ApiSpecDownloadRequest request, CancellationToken cancellationToken);
|
||||
|
||||
100
src/Cli/StellaOps.Cli/Services/Models/DecisionModels.cs
Normal file
100
src/Cli/StellaOps.Cli/Services/Models/DecisionModels.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Request to export VEX decisions.
|
||||
/// </summary>
|
||||
public sealed class DecisionExportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional scan identifier to filter decisions.
|
||||
/// </summary>
|
||||
public string? ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional vulnerability identifiers to filter.
|
||||
/// </summary>
|
||||
public List<string>? VulnIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional Package URLs to filter.
|
||||
/// </summary>
|
||||
public List<string>? Purls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional statuses to filter.
|
||||
/// </summary>
|
||||
public List<string>? Statuses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Output format (openvex, dsse, ndjson).
|
||||
/// </summary>
|
||||
public string Format { get; init; } = "openvex";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to sign the output with DSSE.
|
||||
/// </summary>
|
||||
public bool Sign { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to submit DSSE envelope to Rekor.
|
||||
/// </summary>
|
||||
public bool SubmitToRekor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include reachability evidence blocks.
|
||||
/// </summary>
|
||||
public bool IncludeEvidence { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from VEX decision export.
|
||||
/// </summary>
|
||||
public sealed class DecisionExportResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the export was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if export failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The exported document content.
|
||||
/// </summary>
|
||||
public string? Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the output was signed.
|
||||
/// </summary>
|
||||
public bool Signed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 digest of the payload.
|
||||
/// </summary>
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index if submitted to transparency log.
|
||||
/// </summary>
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor entry UUID if submitted to transparency log.
|
||||
/// </summary>
|
||||
public string? RekorUuid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of VEX statements in the export.
|
||||
/// </summary>
|
||||
public int StatementCount { get; init; }
|
||||
}
|
||||
@@ -250,3 +250,272 @@ internal sealed record ReachabilityOverride
|
||||
[JsonPropertyName("score")]
|
||||
public double? Score { get; init; }
|
||||
}
|
||||
|
||||
// UI-CLI-401-007: Graph explain models with DSSE pointers, runtime hits, predicates, counterfactual controls
|
||||
|
||||
/// <summary>
|
||||
/// Request to explain a call graph with signed evidence.
|
||||
/// </summary>
|
||||
internal sealed class GraphExplainRequest
|
||||
{
|
||||
[JsonPropertyName("graphId")]
|
||||
public string GraphId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
[JsonPropertyName("packagePurl")]
|
||||
public string? PackagePurl { get; init; }
|
||||
|
||||
[JsonPropertyName("includeCallPaths")]
|
||||
public bool IncludeCallPaths { get; init; }
|
||||
|
||||
[JsonPropertyName("includeRuntimeHits")]
|
||||
public bool IncludeRuntimeHits { get; init; }
|
||||
|
||||
[JsonPropertyName("includePredicates")]
|
||||
public bool IncludePredicates { get; init; }
|
||||
|
||||
[JsonPropertyName("includeDsseEnvelopes")]
|
||||
public bool IncludeDsseEnvelopes { get; init; }
|
||||
|
||||
[JsonPropertyName("includeCounterfactuals")]
|
||||
public bool IncludeCounterfactuals { get; init; }
|
||||
|
||||
[JsonPropertyName("tenant")]
|
||||
public string? Tenant { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of graph explanation with signed evidence.
|
||||
/// </summary>
|
||||
internal sealed class GraphExplainResult
|
||||
{
|
||||
[JsonPropertyName("graphId")]
|
||||
public string GraphId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("graphHash")]
|
||||
public string GraphHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
[JsonPropertyName("packagePurl")]
|
||||
public string? PackagePurl { get; init; }
|
||||
|
||||
[JsonPropertyName("reachabilityState")]
|
||||
public string ReachabilityState { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("reachabilityScore")]
|
||||
public double? ReachabilityScore { get; init; }
|
||||
|
||||
[JsonPropertyName("confidence")]
|
||||
public string Confidence { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("reasoning")]
|
||||
public string? Reasoning { get; init; }
|
||||
|
||||
[JsonPropertyName("signedCallPaths")]
|
||||
public IReadOnlyList<SignedCallPath> SignedCallPaths { get; init; } = Array.Empty<SignedCallPath>();
|
||||
|
||||
[JsonPropertyName("runtimeHits")]
|
||||
public IReadOnlyList<RuntimeHit> RuntimeHits { get; init; } = Array.Empty<RuntimeHit>();
|
||||
|
||||
[JsonPropertyName("predicates")]
|
||||
public IReadOnlyList<ReachabilityPredicate> Predicates { get; init; } = Array.Empty<ReachabilityPredicate>();
|
||||
|
||||
[JsonPropertyName("dssePointers")]
|
||||
public IReadOnlyList<DssePointer> DssePointers { get; init; } = Array.Empty<DssePointer>();
|
||||
|
||||
[JsonPropertyName("counterfactuals")]
|
||||
public IReadOnlyList<CounterfactualControl> Counterfactuals { get; init; } = Array.Empty<CounterfactualControl>();
|
||||
|
||||
[JsonPropertyName("vexDecision")]
|
||||
public GraphVexDecision? VexDecision { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call path with cryptographic signature.
|
||||
/// </summary>
|
||||
internal sealed class SignedCallPath
|
||||
{
|
||||
[JsonPropertyName("pathId")]
|
||||
public string PathId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("pathHash")]
|
||||
public string PathHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("depth")]
|
||||
public int Depth { get; init; }
|
||||
|
||||
[JsonPropertyName("entryPoint")]
|
||||
public ReachabilityFunction EntryPoint { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("frames")]
|
||||
public IReadOnlyList<ReachabilityFunction> Frames { get; init; } = Array.Empty<ReachabilityFunction>();
|
||||
|
||||
[JsonPropertyName("vulnerableFunction")]
|
||||
public ReachabilityFunction VulnerableFunction { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("dsseEnvelopeId")]
|
||||
public string? DsseEnvelopeId { get; init; }
|
||||
|
||||
[JsonPropertyName("rekorEntryId")]
|
||||
public string? RekorEntryId { get; init; }
|
||||
|
||||
[JsonPropertyName("signedAt")]
|
||||
public DateTimeOffset? SignedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime execution hit from instrumentation probes.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeHit
|
||||
{
|
||||
[JsonPropertyName("hitId")]
|
||||
public string HitId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("functionName")]
|
||||
public string FunctionName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("className")]
|
||||
public string? ClassName { get; init; }
|
||||
|
||||
[JsonPropertyName("packageName")]
|
||||
public string? PackageName { get; init; }
|
||||
|
||||
[JsonPropertyName("hitCount")]
|
||||
public long HitCount { get; init; }
|
||||
|
||||
[JsonPropertyName("firstObserved")]
|
||||
public DateTimeOffset FirstObserved { get; init; }
|
||||
|
||||
[JsonPropertyName("lastObserved")]
|
||||
public DateTimeOffset LastObserved { get; init; }
|
||||
|
||||
[JsonPropertyName("probeSource")]
|
||||
public string ProbeSource { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
|
||||
[JsonPropertyName("observationWindow")]
|
||||
public string? ObservationWindow { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Semantic predicate attached to reachability evidence.
|
||||
/// </summary>
|
||||
internal sealed class ReachabilityPredicate
|
||||
{
|
||||
[JsonPropertyName("predicateType")]
|
||||
public string PredicateType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("predicateUri")]
|
||||
public string PredicateUri { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; init; }
|
||||
|
||||
[JsonPropertyName("signedBy")]
|
||||
public string? SignedBy { get; init; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope pointer for signed evidence.
|
||||
/// </summary>
|
||||
internal sealed class DssePointer
|
||||
{
|
||||
[JsonPropertyName("envelopeId")]
|
||||
public string EnvelopeId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("payloadType")]
|
||||
public string PayloadType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("payloadHash")]
|
||||
public string PayloadHash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("keyId")]
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("algorithm")]
|
||||
public string Algorithm { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rekorLogIndex")]
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
[JsonPropertyName("rekorLogId")]
|
||||
public string? RekorLogId { get; init; }
|
||||
|
||||
[JsonPropertyName("rekorIntegratedTime")]
|
||||
public DateTimeOffset? RekorIntegratedTime { get; init; }
|
||||
|
||||
[JsonPropertyName("verificationUrl")]
|
||||
public string? VerificationUrl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counterfactual control showing what-if scenarios.
|
||||
/// </summary>
|
||||
internal sealed class CounterfactualControl
|
||||
{
|
||||
[JsonPropertyName("controlId")]
|
||||
public string ControlId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("controlType")]
|
||||
public string ControlType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("currentState")]
|
||||
public string CurrentState { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("alternativeState")]
|
||||
public string AlternativeState { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("impact")]
|
||||
public string Impact { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("recommendation")]
|
||||
public string? Recommendation { get; init; }
|
||||
|
||||
[JsonPropertyName("affectedPaths")]
|
||||
public IReadOnlyList<string> AffectedPaths { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("riskReduction")]
|
||||
public double? RiskReduction { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX decision linked to graph evidence.
|
||||
/// </summary>
|
||||
internal sealed class GraphVexDecision
|
||||
{
|
||||
[JsonPropertyName("vexDocumentId")]
|
||||
public string VexDocumentId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("justification")]
|
||||
public string? Justification { get; init; }
|
||||
|
||||
[JsonPropertyName("actionStatement")]
|
||||
public string? ActionStatement { get; init; }
|
||||
|
||||
[JsonPropertyName("dsseEnvelopeId")]
|
||||
public string? DsseEnvelopeId { get; init; }
|
||||
|
||||
[JsonPropertyName("rekorEntryId")]
|
||||
public string? RekorEntryId { get; init; }
|
||||
|
||||
[JsonPropertyName("issuedAt")]
|
||||
public DateTimeOffset IssuedAt { get; init; }
|
||||
}
|
||||
|
||||
130
src/Cli/StellaOps.Cli/Services/Models/SymbolBundleModels.cs
Normal file
130
src/Cli/StellaOps.Cli/Services/Models/SymbolBundleModels.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
// SYMS-BUNDLE-401-014: Symbol bundle CLI models
|
||||
|
||||
/// <summary>
|
||||
/// Request to build a symbol bundle.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleBuildRequest(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("sourceDir")] string SourceDir,
|
||||
[property: JsonPropertyName("outputDir")] string OutputDir,
|
||||
[property: JsonPropertyName("platform")] string? Platform = null,
|
||||
[property: JsonPropertyName("tenantId")] string? TenantId = null,
|
||||
[property: JsonPropertyName("sign")] bool Sign = false,
|
||||
[property: JsonPropertyName("signingKeyPath")] string? SigningKeyPath = null,
|
||||
[property: JsonPropertyName("keyId")] string? KeyId = null,
|
||||
[property: JsonPropertyName("signingAlgorithm")] string SigningAlgorithm = "ecdsa-p256",
|
||||
[property: JsonPropertyName("submitRekor")] bool SubmitRekor = false,
|
||||
[property: JsonPropertyName("rekorUrl")] string RekorUrl = "https://rekor.sigstore.dev",
|
||||
[property: JsonPropertyName("format")] string Format = "zip",
|
||||
[property: JsonPropertyName("compressionLevel")] int CompressionLevel = 6);
|
||||
|
||||
/// <summary>
|
||||
/// Result of symbol bundle build operation.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleBuildResult(
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("bundlePath")] string? BundlePath = null,
|
||||
[property: JsonPropertyName("manifestPath")] string? ManifestPath = null,
|
||||
[property: JsonPropertyName("bundleId")] string? BundleId = null,
|
||||
[property: JsonPropertyName("entryCount")] int EntryCount = 0,
|
||||
[property: JsonPropertyName("totalSizeBytes")] long TotalSizeBytes = 0,
|
||||
[property: JsonPropertyName("signed")] bool Signed = false,
|
||||
[property: JsonPropertyName("rekorLogIndex")] long? RekorLogIndex = null,
|
||||
[property: JsonPropertyName("error")] string? Error = null,
|
||||
[property: JsonPropertyName("warnings")] IReadOnlyList<string>? Warnings = null,
|
||||
[property: JsonPropertyName("durationMs")] long DurationMs = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Request to verify a symbol bundle.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleVerifyRequest(
|
||||
[property: JsonPropertyName("bundlePath")] string BundlePath,
|
||||
[property: JsonPropertyName("publicKeyPath")] string? PublicKeyPath = null,
|
||||
[property: JsonPropertyName("verifyRekorOffline")] bool VerifyRekorOffline = true,
|
||||
[property: JsonPropertyName("rekorPublicKeyPath")] string? RekorPublicKeyPath = null,
|
||||
[property: JsonPropertyName("verifyBlobHashes")] bool VerifyBlobHashes = true);
|
||||
|
||||
/// <summary>
|
||||
/// Result of symbol bundle verification.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleVerifyResult(
|
||||
[property: JsonPropertyName("valid")] bool Valid,
|
||||
[property: JsonPropertyName("bundleId")] string? BundleId = null,
|
||||
[property: JsonPropertyName("name")] string? Name = null,
|
||||
[property: JsonPropertyName("version")] string? Version = null,
|
||||
[property: JsonPropertyName("signatureStatus")] string SignatureStatus = "unsigned",
|
||||
[property: JsonPropertyName("rekorStatus")] string? RekorStatus = null,
|
||||
[property: JsonPropertyName("hashStatus")] SymbolBundleHashStatus? HashStatus = null,
|
||||
[property: JsonPropertyName("errors")] IReadOnlyList<string>? Errors = null,
|
||||
[property: JsonPropertyName("warnings")] IReadOnlyList<string>? Warnings = null);
|
||||
|
||||
/// <summary>
|
||||
/// Hash verification status for a bundle.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleHashStatus(
|
||||
[property: JsonPropertyName("bundleHashValid")] bool BundleHashValid,
|
||||
[property: JsonPropertyName("validEntries")] int ValidEntries,
|
||||
[property: JsonPropertyName("invalidEntries")] int InvalidEntries,
|
||||
[property: JsonPropertyName("totalEntries")] int TotalEntries,
|
||||
[property: JsonPropertyName("invalidEntryIds")] IReadOnlyList<string>? InvalidEntryIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Request to extract a symbol bundle.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleExtractRequest(
|
||||
[property: JsonPropertyName("bundlePath")] string BundlePath,
|
||||
[property: JsonPropertyName("outputDir")] string OutputDir,
|
||||
[property: JsonPropertyName("verifyFirst")] bool VerifyFirst = true,
|
||||
[property: JsonPropertyName("platform")] string? Platform = null,
|
||||
[property: JsonPropertyName("overwrite")] bool Overwrite = false,
|
||||
[property: JsonPropertyName("manifestsOnly")] bool ManifestsOnly = false);
|
||||
|
||||
/// <summary>
|
||||
/// Result of symbol bundle extraction.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleExtractResult(
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("extractedCount")] int ExtractedCount = 0,
|
||||
[property: JsonPropertyName("skippedCount")] int SkippedCount = 0,
|
||||
[property: JsonPropertyName("totalBytesExtracted")] long TotalBytesExtracted = 0,
|
||||
[property: JsonPropertyName("verificationPassed")] bool? VerificationPassed = null,
|
||||
[property: JsonPropertyName("error")] string? Error = null,
|
||||
[property: JsonPropertyName("durationMs")] long DurationMs = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol bundle manifest info for inspection.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleInfo(
|
||||
[property: JsonPropertyName("bundleId")] string BundleId,
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("createdAt")] DateTimeOffset CreatedAt,
|
||||
[property: JsonPropertyName("platform")] string? Platform = null,
|
||||
[property: JsonPropertyName("tenantId")] string? TenantId = null,
|
||||
[property: JsonPropertyName("entryCount")] int EntryCount = 0,
|
||||
[property: JsonPropertyName("totalSizeBytes")] long TotalSizeBytes = 0,
|
||||
[property: JsonPropertyName("hashAlgorithm")] string HashAlgorithm = "blake3",
|
||||
[property: JsonPropertyName("signed")] bool Signed = false,
|
||||
[property: JsonPropertyName("signatureAlgorithm")] string? SignatureAlgorithm = null,
|
||||
[property: JsonPropertyName("signatureKeyId")] string? SignatureKeyId = null,
|
||||
[property: JsonPropertyName("rekorLogIndex")] long? RekorLogIndex = null,
|
||||
[property: JsonPropertyName("entries")] IReadOnlyList<SymbolBundleEntryInfo>? Entries = null);
|
||||
|
||||
/// <summary>
|
||||
/// Individual entry in a symbol bundle.
|
||||
/// </summary>
|
||||
internal sealed record SymbolBundleEntryInfo(
|
||||
[property: JsonPropertyName("debugId")] string DebugId,
|
||||
[property: JsonPropertyName("binaryName")] string BinaryName,
|
||||
[property: JsonPropertyName("platform")] string? Platform = null,
|
||||
[property: JsonPropertyName("format")] string? Format = null,
|
||||
[property: JsonPropertyName("blobHash")] string? BlobHash = null,
|
||||
[property: JsonPropertyName("blobSizeBytes")] long BlobSizeBytes = 0,
|
||||
[property: JsonPropertyName("symbolCount")] int SymbolCount = 0);
|
||||
264
src/Cli/StellaOps.Cli/Services/Models/VexExplainModels.cs
Normal file
264
src/Cli/StellaOps.Cli/Services/Models/VexExplainModels.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
// UI-VEX-401-032: VEX Decision Explanation Models
|
||||
// Provides comprehensive decision explanation with reachability evidence and attestation details
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Complete VEX decision explanation with all supporting evidence.
|
||||
/// </summary>
|
||||
internal sealed class VexDecisionExplanation
|
||||
{
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
[JsonPropertyName("productKey")]
|
||||
public required string ProductKey { get; init; }
|
||||
|
||||
[JsonPropertyName("tenant")]
|
||||
public required string Tenant { get; init; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
[JsonPropertyName("decision")]
|
||||
public required VexDecisionSummary Decision { get; init; }
|
||||
|
||||
[JsonPropertyName("callPathEvidence")]
|
||||
public CallPathEvidence? CallPathEvidence { get; set; }
|
||||
|
||||
[JsonPropertyName("runtimeHitEvidence")]
|
||||
public RuntimeHitEvidence? RuntimeHitEvidence { get; set; }
|
||||
|
||||
[JsonPropertyName("graphMetadata")]
|
||||
public ReachabilityGraphMetadata? GraphMetadata { get; set; }
|
||||
|
||||
[JsonPropertyName("dsseAttestation")]
|
||||
public DsseAttestationInfo? DsseAttestation { get; set; }
|
||||
|
||||
[JsonPropertyName("rekorEntry")]
|
||||
public RekorEntryInfo? RekorEntry { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX decision summary with status and justification.
|
||||
/// </summary>
|
||||
internal sealed class VexDecisionSummary
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public required string Status { get; init; }
|
||||
|
||||
[JsonPropertyName("justification")]
|
||||
public required string Justification { get; init; }
|
||||
|
||||
[JsonPropertyName("impactStatement")]
|
||||
public required string ImpactStatement { get; init; }
|
||||
|
||||
[JsonPropertyName("decisionSource")]
|
||||
public required string DecisionSource { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call path analysis evidence showing reachability status.
|
||||
/// </summary>
|
||||
internal sealed class CallPathEvidence
|
||||
{
|
||||
[JsonPropertyName("analysisMethod")]
|
||||
public required string AnalysisMethod { get; init; }
|
||||
|
||||
[JsonPropertyName("entryPointsAnalyzed")]
|
||||
public int EntryPointsAnalyzed { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerableFunctionsIdentified")]
|
||||
public int VulnerableFunctionsIdentified { get; init; }
|
||||
|
||||
[JsonPropertyName("pathsToVulnerableCode")]
|
||||
public int PathsToVulnerableCode { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerableFunction")]
|
||||
public FunctionReference? VulnerableFunction { get; init; }
|
||||
|
||||
[JsonPropertyName("nearestReachableDistance")]
|
||||
public int? NearestReachableDistance { get; init; }
|
||||
|
||||
[JsonPropertyName("analysisComplete")]
|
||||
public bool AnalysisComplete { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a specific function in code.
|
||||
/// </summary>
|
||||
internal sealed class FunctionReference
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("module")]
|
||||
public required string Module { get; init; }
|
||||
|
||||
[JsonPropertyName("file")]
|
||||
public required string File { get; init; }
|
||||
|
||||
[JsonPropertyName("line")]
|
||||
public int Line { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime execution hit evidence from production telemetry.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeHitEvidence
|
||||
{
|
||||
[JsonPropertyName("collectionPeriod")]
|
||||
public required DateRange CollectionPeriod { get; init; }
|
||||
|
||||
[JsonPropertyName("totalExecutions")]
|
||||
public long TotalExecutions { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerableFunctionHits")]
|
||||
public long VulnerableFunctionHits { get; init; }
|
||||
|
||||
[JsonPropertyName("coveragePercentage")]
|
||||
public decimal CoveragePercentage { get; init; }
|
||||
|
||||
[JsonPropertyName("profilingMethod")]
|
||||
public required string ProfilingMethod { get; init; }
|
||||
|
||||
[JsonPropertyName("confidenceLevel")]
|
||||
public required string ConfidenceLevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Date range for evidence collection period.
|
||||
/// </summary>
|
||||
internal sealed class DateRange
|
||||
{
|
||||
[JsonPropertyName("start")]
|
||||
public DateTimeOffset Start { get; init; }
|
||||
|
||||
[JsonPropertyName("end")]
|
||||
public DateTimeOffset End { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata about the reachability graph used for analysis.
|
||||
/// </summary>
|
||||
internal sealed class ReachabilityGraphMetadata
|
||||
{
|
||||
[JsonPropertyName("graphId")]
|
||||
public required string GraphId { get; init; }
|
||||
|
||||
[JsonPropertyName("buildTimestamp")]
|
||||
public DateTimeOffset BuildTimestamp { get; init; }
|
||||
|
||||
[JsonPropertyName("totalNodes")]
|
||||
public int TotalNodes { get; init; }
|
||||
|
||||
[JsonPropertyName("totalEdges")]
|
||||
public int TotalEdges { get; init; }
|
||||
|
||||
[JsonPropertyName("entryPoints")]
|
||||
public int EntryPoints { get; init; }
|
||||
|
||||
[JsonPropertyName("vulnerableSinks")]
|
||||
public int VulnerableSinks { get; init; }
|
||||
|
||||
[JsonPropertyName("algorithm")]
|
||||
public required string Algorithm { get; init; }
|
||||
|
||||
[JsonPropertyName("analysisDurationMs")]
|
||||
public long AnalysisDurationMs { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE (Dead Simple Signing Envelope) attestation information.
|
||||
/// </summary>
|
||||
internal sealed class DsseAttestationInfo
|
||||
{
|
||||
[JsonPropertyName("payloadType")]
|
||||
public required string PayloadType { get; init; }
|
||||
|
||||
[JsonPropertyName("digestAlgorithm")]
|
||||
public required string DigestAlgorithm { get; init; }
|
||||
|
||||
[JsonPropertyName("payloadDigest")]
|
||||
public required string PayloadDigest { get; init; }
|
||||
|
||||
[JsonPropertyName("signatures")]
|
||||
public required List<VexDsseSignatureInfo> Signatures { get; init; }
|
||||
|
||||
[JsonPropertyName("verificationStatus")]
|
||||
public string? VerificationStatus { get; set; }
|
||||
|
||||
[JsonPropertyName("verifiedAt")]
|
||||
public DateTimeOffset? VerifiedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a DSSE signature for VEX explanations.
|
||||
/// </summary>
|
||||
internal sealed class VexDsseSignatureInfo
|
||||
{
|
||||
[JsonPropertyName("keyId")]
|
||||
public required string KeyId { get; init; }
|
||||
|
||||
[JsonPropertyName("algorithm")]
|
||||
public required string Algorithm { get; init; }
|
||||
|
||||
[JsonPropertyName("signedAt")]
|
||||
public DateTimeOffset SignedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("publicKeyFingerprint")]
|
||||
public required string PublicKeyFingerprint { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rekor transparency log entry information.
|
||||
/// </summary>
|
||||
internal sealed class RekorEntryInfo
|
||||
{
|
||||
[JsonPropertyName("rekorUrl")]
|
||||
public required string RekorUrl { get; init; }
|
||||
|
||||
[JsonPropertyName("logIndex")]
|
||||
public long LogIndex { get; init; }
|
||||
|
||||
[JsonPropertyName("entryUuid")]
|
||||
public required string EntryUuid { get; init; }
|
||||
|
||||
[JsonPropertyName("integratedTime")]
|
||||
public DateTimeOffset IntegratedTime { get; init; }
|
||||
|
||||
[JsonPropertyName("treeSize")]
|
||||
public long TreeSize { get; init; }
|
||||
|
||||
[JsonPropertyName("rootHash")]
|
||||
public required string RootHash { get; init; }
|
||||
|
||||
[JsonPropertyName("inclusionProof")]
|
||||
public InclusionProofInfo? InclusionProof { get; set; }
|
||||
|
||||
[JsonPropertyName("inclusionVerified")]
|
||||
public bool? InclusionVerified { get; set; }
|
||||
|
||||
[JsonPropertyName("verifiedAt")]
|
||||
public DateTimeOffset? VerifiedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merkle tree inclusion proof for Rekor verification.
|
||||
/// </summary>
|
||||
internal sealed class InclusionProofInfo
|
||||
{
|
||||
[JsonPropertyName("logIndex")]
|
||||
public long LogIndex { get; init; }
|
||||
|
||||
[JsonPropertyName("treeSize")]
|
||||
public long TreeSize { get; init; }
|
||||
|
||||
[JsonPropertyName("rootHash")]
|
||||
public required string RootHash { get; init; }
|
||||
|
||||
[JsonPropertyName("hashes")]
|
||||
public required List<string> Hashes { get; init; }
|
||||
}
|
||||
@@ -101,7 +101,9 @@ internal sealed record VexConsensusDetailResponse(
|
||||
[property: JsonPropertyName("quorum")] VexQuorumInfo? Quorum = null,
|
||||
[property: JsonPropertyName("rationale")] VexRationaleInfo? Rationale = null,
|
||||
[property: JsonPropertyName("signature")] VexSignatureInfo? Signature = null,
|
||||
[property: JsonPropertyName("evidence")] IReadOnlyList<VexEvidenceInfo>? Evidence = null);
|
||||
[property: JsonPropertyName("evidence")] IReadOnlyList<VexEvidenceInfo>? Evidence = null,
|
||||
// GAP-VEX-006: Reachability evidence
|
||||
[property: JsonPropertyName("reachabilityEvidence")] VexReachabilityEvidence? ReachabilityEvidence = null);
|
||||
|
||||
/// <summary>
|
||||
/// VEX quorum information showing how consensus was reached.
|
||||
@@ -256,3 +258,42 @@ internal sealed record VexExportVerifyResult(
|
||||
[property: JsonPropertyName("keyId")] string? KeyId = null,
|
||||
[property: JsonPropertyName("signedAt")] DateTimeOffset? SignedAt = null,
|
||||
[property: JsonPropertyName("errors")] IReadOnlyList<string>? Errors = null);
|
||||
|
||||
// GAP-VEX-006: Reachability evidence models for VEX decisions
|
||||
|
||||
/// <summary>
|
||||
/// Reachability evidence linked to VEX decision.
|
||||
/// </summary>
|
||||
internal sealed record VexReachabilityEvidence(
|
||||
[property: JsonPropertyName("graphHash")] string? GraphHash = null,
|
||||
[property: JsonPropertyName("graphCasUri")] string? GraphCasUri = null,
|
||||
[property: JsonPropertyName("graphAlgorithm")] string? GraphAlgorithm = null,
|
||||
[property: JsonPropertyName("graphGeneratedAt")] DateTimeOffset? GraphGeneratedAt = null,
|
||||
[property: JsonPropertyName("reachabilityState")] string? ReachabilityState = null,
|
||||
[property: JsonPropertyName("confidence")] double? Confidence = null,
|
||||
[property: JsonPropertyName("callPaths")] IReadOnlyList<VexCallPath>? CallPaths = null,
|
||||
[property: JsonPropertyName("runtimeHits")] IReadOnlyList<VexRuntimeHit>? RuntimeHits = null,
|
||||
[property: JsonPropertyName("dsseEnvelopeId")] string? DsseEnvelopeId = null,
|
||||
[property: JsonPropertyName("rekorEntryId")] string? RekorEntryId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Call path evidence for VEX decision.
|
||||
/// </summary>
|
||||
internal sealed record VexCallPath(
|
||||
[property: JsonPropertyName("pathId")] string PathId,
|
||||
[property: JsonPropertyName("pathHash")] string? PathHash = null,
|
||||
[property: JsonPropertyName("depth")] int Depth = 0,
|
||||
[property: JsonPropertyName("entryPoint")] string EntryPoint = "",
|
||||
[property: JsonPropertyName("frames")] IReadOnlyList<string> Frames = null!,
|
||||
[property: JsonPropertyName("vulnerableFunction")] string VulnerableFunction = "",
|
||||
[property: JsonPropertyName("dsseEnvelopeId")] string? DsseEnvelopeId = null,
|
||||
[property: JsonPropertyName("rekorEntryId")] string? RekorEntryId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Runtime execution hit evidence for VEX decision.
|
||||
/// </summary>
|
||||
internal sealed record VexRuntimeHit(
|
||||
[property: JsonPropertyName("functionName")] string FunctionName,
|
||||
[property: JsonPropertyName("hitCount")] long HitCount = 0,
|
||||
[property: JsonPropertyName("probeSource")] string? ProbeSource = null,
|
||||
[property: JsonPropertyName("lastObserved")] DateTimeOffset? LastObserved = null);
|
||||
|
||||
@@ -66,6 +66,9 @@ internal static class CliMetrics
|
||||
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 Counter<long> DecisionExportCounter = Meter.CreateCounter<long>("stellaops.cli.decision.export.count");
|
||||
private static readonly Counter<long> DecisionVerifyCounter = Meter.CreateCounter<long>("stellaops.cli.decision.verify.count");
|
||||
private static readonly Counter<long> DecisionCompareCounter = Meter.CreateCounter<long>("stellaops.cli.decision.compare.count");
|
||||
private static readonly Histogram<double> CommandDurationHistogram = Meter.CreateHistogram<double>("stellaops.cli.command.duration.ms");
|
||||
|
||||
public static void RecordScannerDownload(string channel, bool fromCache)
|
||||
@@ -183,6 +186,30 @@ internal static class CliMetrics
|
||||
=> AttestVerifyCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
/// <summary>
|
||||
/// Records a VEX decision export operation.
|
||||
/// </summary>
|
||||
/// <param name="outcome">The export outcome (success, error).</param>
|
||||
public static void RecordDecisionExport(string outcome)
|
||||
=> DecisionExportCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
/// <summary>
|
||||
/// Records a VEX decision verification operation.
|
||||
/// </summary>
|
||||
/// <param name="outcome">The verification outcome (success, failed, error).</param>
|
||||
public static void RecordDecisionVerify(string outcome)
|
||||
=> DecisionVerifyCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
/// <summary>
|
||||
/// Records a VEX decision comparison operation.
|
||||
/// </summary>
|
||||
/// <param name="outcome">The comparison outcome (success, error).</param>
|
||||
public static void RecordDecisionCompare(string outcome)
|
||||
=> DecisionCompareCounter.Add(1, WithSealedModeTag(
|
||||
Tag("outcome", string.IsNullOrWhiteSpace(outcome) ? "unknown" : outcome)));
|
||||
|
||||
public static IDisposable MeasureCommandDuration(string command)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user