up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (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
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
353
src/Cli/StellaOps.Cli/Output/CliError.cs
Normal file
353
src/Cli/StellaOps.Cli/Output/CliError.cs
Normal file
@@ -0,0 +1,353 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StellaOps.Cli.Services.Models.Transport;
|
||||
|
||||
namespace StellaOps.Cli.Output;
|
||||
|
||||
/// <summary>
|
||||
/// Structured CLI error with code, message, and optional details.
|
||||
/// Per CLI-CORE-41-001, provides error mapping for standardized API error envelopes.
|
||||
/// CLI-SDK-62-002: Enhanced to surface error.code and trace_id from API responses.
|
||||
/// </summary>
|
||||
public sealed record CliError(
|
||||
string Code,
|
||||
string Message,
|
||||
string? TraceId = null,
|
||||
string? Detail = null,
|
||||
IReadOnlyDictionary<string, string>? Metadata = null,
|
||||
string? RequestId = null,
|
||||
string? HelpUrl = null,
|
||||
int? RetryAfter = null,
|
||||
string? Target = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Exit code to use when this error occurs.
|
||||
/// </summary>
|
||||
public int ExitCode => GetExitCode(Code);
|
||||
|
||||
/// <summary>
|
||||
/// Maps error code prefixes to exit codes.
|
||||
/// </summary>
|
||||
private static int GetExitCode(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return 1;
|
||||
|
||||
// Authentication/authorization errors
|
||||
if (code.StartsWith("AUTH_", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("ERR_AUTH_", StringComparison.OrdinalIgnoreCase))
|
||||
return 2;
|
||||
|
||||
// Invalid scope errors
|
||||
if (code.Contains("SCOPE", StringComparison.OrdinalIgnoreCase))
|
||||
return 3;
|
||||
|
||||
// Not found errors
|
||||
if (code.StartsWith("NOT_FOUND", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("ERR_NOT_FOUND", StringComparison.OrdinalIgnoreCase))
|
||||
return 4;
|
||||
|
||||
// Validation errors
|
||||
if (code.StartsWith("VALIDATION_", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("ERR_VALIDATION_", StringComparison.OrdinalIgnoreCase))
|
||||
return 5;
|
||||
|
||||
// Rate limit errors
|
||||
if (code.StartsWith("RATE_LIMIT", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("ERR_RATE_LIMIT", StringComparison.OrdinalIgnoreCase))
|
||||
return 6;
|
||||
|
||||
// Air-gap errors
|
||||
if (code.StartsWith("AIRGAP_", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("ERR_AIRGAP_", StringComparison.OrdinalIgnoreCase))
|
||||
return 7;
|
||||
|
||||
// AOC errors
|
||||
if (code.StartsWith("ERR_AOC_", StringComparison.OrdinalIgnoreCase))
|
||||
return 8;
|
||||
|
||||
// Aggregation errors
|
||||
if (code.StartsWith("ERR_AGG_", StringComparison.OrdinalIgnoreCase))
|
||||
return 9;
|
||||
|
||||
// Forensic verification errors
|
||||
if (code.StartsWith("ERR_FORENSIC_", StringComparison.OrdinalIgnoreCase))
|
||||
return 12;
|
||||
|
||||
// Determinism errors
|
||||
if (code.StartsWith("ERR_DETER_", StringComparison.OrdinalIgnoreCase))
|
||||
return 13;
|
||||
|
||||
// Observability errors
|
||||
if (code.StartsWith("ERR_OBS_", StringComparison.OrdinalIgnoreCase))
|
||||
return 14;
|
||||
|
||||
// Pack errors
|
||||
if (code.StartsWith("ERR_PACK_", StringComparison.OrdinalIgnoreCase))
|
||||
return 15;
|
||||
|
||||
// Exception governance errors
|
||||
if (code.StartsWith("ERR_EXC_", StringComparison.OrdinalIgnoreCase))
|
||||
return 16;
|
||||
|
||||
// Orchestrator errors
|
||||
if (code.StartsWith("ERR_ORCH_", StringComparison.OrdinalIgnoreCase))
|
||||
return 17;
|
||||
|
||||
// SBOM errors
|
||||
if (code.StartsWith("ERR_SBOM_", StringComparison.OrdinalIgnoreCase))
|
||||
return 18;
|
||||
|
||||
// Notify errors
|
||||
if (code.StartsWith("ERR_NOTIFY_", StringComparison.OrdinalIgnoreCase))
|
||||
return 19;
|
||||
|
||||
// Sbomer errors
|
||||
if (code.StartsWith("ERR_SBOMER_", StringComparison.OrdinalIgnoreCase))
|
||||
return 20;
|
||||
|
||||
// Network/connectivity errors
|
||||
if (code.StartsWith("NETWORK_", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("ERR_NETWORK_", StringComparison.OrdinalIgnoreCase) ||
|
||||
code.StartsWith("CONNECTION_", StringComparison.OrdinalIgnoreCase))
|
||||
return 10;
|
||||
|
||||
// Timeout errors
|
||||
if (code.Contains("TIMEOUT", StringComparison.OrdinalIgnoreCase))
|
||||
return 11;
|
||||
|
||||
// Generic errors
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error from an exception.
|
||||
/// </summary>
|
||||
public static CliError FromException(Exception ex, string? traceId = null)
|
||||
{
|
||||
var code = ex switch
|
||||
{
|
||||
UnauthorizedAccessException => "ERR_AUTH_UNAUTHORIZED",
|
||||
TimeoutException => "ERR_TIMEOUT",
|
||||
OperationCanceledException => "ERR_CANCELLED",
|
||||
InvalidOperationException => "ERR_INVALID_OPERATION",
|
||||
ArgumentException => "ERR_VALIDATION_ARGUMENT",
|
||||
_ => "ERR_UNKNOWN"
|
||||
};
|
||||
|
||||
return new CliError(code, ex.Message, traceId, ex.InnerException?.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error from an HTTP status code.
|
||||
/// </summary>
|
||||
public static CliError FromHttpStatus(int statusCode, string? message = null, string? traceId = null)
|
||||
{
|
||||
var (code, defaultMessage) = statusCode switch
|
||||
{
|
||||
400 => ("ERR_VALIDATION_BAD_REQUEST", "Bad request"),
|
||||
401 => ("ERR_AUTH_UNAUTHORIZED", "Unauthorized"),
|
||||
403 => ("ERR_AUTH_FORBIDDEN", "Forbidden"),
|
||||
404 => ("ERR_NOT_FOUND", "Resource not found"),
|
||||
409 => ("ERR_CONFLICT", "Resource conflict"),
|
||||
422 => ("ERR_VALIDATION_UNPROCESSABLE", "Unprocessable entity"),
|
||||
429 => ("ERR_RATE_LIMIT", "Rate limit exceeded"),
|
||||
500 => ("ERR_SERVER_INTERNAL", "Internal server error"),
|
||||
502 => ("ERR_SERVER_BAD_GATEWAY", "Bad gateway"),
|
||||
503 => ("ERR_SERVER_UNAVAILABLE", "Service unavailable"),
|
||||
504 => ("ERR_SERVER_TIMEOUT", "Gateway timeout"),
|
||||
_ => ($"ERR_HTTP_{statusCode}", $"HTTP error {statusCode}")
|
||||
};
|
||||
|
||||
return new CliError(code, message ?? defaultMessage, traceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error from a parsed API error.
|
||||
/// CLI-SDK-62-002: Surfaces standardized API error envelope fields.
|
||||
/// </summary>
|
||||
public static CliError FromParsedApiError(ParsedApiError error)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(error);
|
||||
|
||||
Dictionary<string, string>? metadata = null;
|
||||
if (error.Metadata is not null && error.Metadata.Count > 0)
|
||||
{
|
||||
metadata = error.Metadata
|
||||
.Where(kvp => kvp.Value is not null)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? "");
|
||||
}
|
||||
|
||||
return new CliError(
|
||||
Code: error.Code,
|
||||
Message: error.Message,
|
||||
TraceId: error.TraceId,
|
||||
Detail: error.Detail,
|
||||
Metadata: metadata,
|
||||
RequestId: error.RequestId,
|
||||
HelpUrl: error.HelpUrl,
|
||||
RetryAfter: error.RetryAfter,
|
||||
Target: error.Target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error from an API error envelope.
|
||||
/// CLI-SDK-62-002: Direct conversion from envelope format.
|
||||
/// </summary>
|
||||
public static CliError FromApiErrorEnvelope(ApiErrorEnvelope envelope, int httpStatus)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(envelope);
|
||||
|
||||
var errorDetail = envelope.Error;
|
||||
var code = errorDetail?.Code ?? $"ERR_HTTP_{httpStatus}";
|
||||
var message = errorDetail?.Message ?? $"HTTP error {httpStatus}";
|
||||
|
||||
Dictionary<string, string>? metadata = null;
|
||||
if (errorDetail?.Metadata is not null && errorDetail.Metadata.Count > 0)
|
||||
{
|
||||
metadata = errorDetail.Metadata
|
||||
.Where(kvp => kvp.Value is not null)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? "");
|
||||
}
|
||||
|
||||
return new CliError(
|
||||
Code: code,
|
||||
Message: message,
|
||||
TraceId: envelope.TraceId,
|
||||
Detail: errorDetail?.Detail,
|
||||
Metadata: metadata,
|
||||
RequestId: envelope.RequestId,
|
||||
HelpUrl: errorDetail?.HelpUrl,
|
||||
RetryAfter: errorDetail?.RetryAfter,
|
||||
Target: errorDetail?.Target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Well-known CLI error codes.
|
||||
/// </summary>
|
||||
public static class CliErrorCodes
|
||||
{
|
||||
public const string Unauthorized = "ERR_AUTH_UNAUTHORIZED";
|
||||
public const string Forbidden = "ERR_AUTH_FORBIDDEN";
|
||||
public const string InvalidScope = "ERR_AUTH_INVALID_SCOPE";
|
||||
public const string NotFound = "ERR_NOT_FOUND";
|
||||
public const string ValidationFailed = "ERR_VALIDATION_FAILED";
|
||||
public const string RateLimited = "ERR_RATE_LIMIT";
|
||||
public const string AirGapBlocked = "ERR_AIRGAP_EGRESS_BLOCKED";
|
||||
public const string AocViolation = "ERR_AOC_001";
|
||||
public const string NetworkError = "ERR_NETWORK_FAILED";
|
||||
public const string Timeout = "ERR_TIMEOUT";
|
||||
public const string Cancelled = "ERR_CANCELLED";
|
||||
public const string ConfigurationMissing = "ERR_CONFIG_MISSING";
|
||||
public const string ProfileNotFound = "ERR_PROFILE_NOT_FOUND";
|
||||
|
||||
// CLI-LNM-22-001: Aggregation error codes (exit code 9)
|
||||
public const string AggNoObservations = "ERR_AGG_NO_OBSERVATIONS";
|
||||
public const string AggConflictDetected = "ERR_AGG_CONFLICT_DETECTED";
|
||||
public const string AggLinksetEmpty = "ERR_AGG_LINKSET_EMPTY";
|
||||
public const string AggSourceMissing = "ERR_AGG_SOURCE_MISSING";
|
||||
public const string AggExportFailed = "ERR_AGG_EXPORT_FAILED";
|
||||
|
||||
// CLI-FORENSICS-54-001: Forensic verification error codes (exit code 12)
|
||||
public const string ForensicBundleNotFound = "ERR_FORENSIC_BUNDLE_NOT_FOUND";
|
||||
public const string ForensicBundleInvalid = "ERR_FORENSIC_BUNDLE_INVALID";
|
||||
public const string ForensicChecksumMismatch = "ERR_FORENSIC_CHECKSUM_MISMATCH";
|
||||
public const string ForensicSignatureInvalid = "ERR_FORENSIC_SIGNATURE_INVALID";
|
||||
public const string ForensicSignatureUntrusted = "ERR_FORENSIC_SIGNATURE_UNTRUSTED";
|
||||
public const string ForensicChainOfCustodyBroken = "ERR_FORENSIC_CHAIN_BROKEN";
|
||||
public const string ForensicTimelineInvalid = "ERR_FORENSIC_TIMELINE_INVALID";
|
||||
public const string ForensicTrustRootMissing = "ERR_FORENSIC_TRUST_ROOT_MISSING";
|
||||
|
||||
// CLI-DETER-70-003: Determinism error codes (exit code 13)
|
||||
public const string DeterminismDockerUnavailable = "ERR_DETER_DOCKER_UNAVAILABLE";
|
||||
public const string DeterminismNoImages = "ERR_DETER_NO_IMAGES";
|
||||
public const string DeterminismScannerMissing = "ERR_DETER_SCANNER_MISSING";
|
||||
public const string DeterminismThresholdFailed = "ERR_DETER_THRESHOLD_FAILED";
|
||||
public const string DeterminismRunFailed = "ERR_DETER_RUN_FAILED";
|
||||
public const string DeterminismManifestInvalid = "ERR_DETER_MANIFEST_INVALID";
|
||||
|
||||
// CLI-OBS-51-001: Observability error codes (exit code 14)
|
||||
public const string ObsConnectionFailed = "ERR_OBS_CONNECTION_FAILED";
|
||||
public const string ObsServiceUnavailable = "ERR_OBS_SERVICE_UNAVAILABLE";
|
||||
public const string ObsNoData = "ERR_OBS_NO_DATA";
|
||||
public const string ObsInvalidFilter = "ERR_OBS_INVALID_FILTER";
|
||||
public const string ObsOfflineViolation = "ERR_OBS_OFFLINE_VIOLATION";
|
||||
|
||||
// CLI-PACKS-42-001: Pack error codes (exit code 15)
|
||||
public const string PackNotFound = "ERR_PACK_NOT_FOUND";
|
||||
public const string PackValidationFailed = "ERR_PACK_VALIDATION_FAILED";
|
||||
public const string PackPlanFailed = "ERR_PACK_PLAN_FAILED";
|
||||
public const string PackRunFailed = "ERR_PACK_RUN_FAILED";
|
||||
public const string PackPushFailed = "ERR_PACK_PUSH_FAILED";
|
||||
public const string PackPullFailed = "ERR_PACK_PULL_FAILED";
|
||||
public const string PackVerifyFailed = "ERR_PACK_VERIFY_FAILED";
|
||||
public const string PackSignatureInvalid = "ERR_PACK_SIGNATURE_INVALID";
|
||||
public const string PackApprovalRequired = "ERR_PACK_APPROVAL_REQUIRED";
|
||||
public const string PackOfflineViolation = "ERR_PACK_OFFLINE_VIOLATION";
|
||||
|
||||
// CLI-EXC-25-001: Exception governance error codes (exit code 16)
|
||||
public const string ExcNotFound = "ERR_EXC_NOT_FOUND";
|
||||
public const string ExcValidationFailed = "ERR_EXC_VALIDATION_FAILED";
|
||||
public const string ExcCreateFailed = "ERR_EXC_CREATE_FAILED";
|
||||
public const string ExcPromoteFailed = "ERR_EXC_PROMOTE_FAILED";
|
||||
public const string ExcRevokeFailed = "ERR_EXC_REVOKE_FAILED";
|
||||
public const string ExcImportFailed = "ERR_EXC_IMPORT_FAILED";
|
||||
public const string ExcExportFailed = "ERR_EXC_EXPORT_FAILED";
|
||||
public const string ExcApprovalRequired = "ERR_EXC_APPROVAL_REQUIRED";
|
||||
public const string ExcExpired = "ERR_EXC_EXPIRED";
|
||||
public const string ExcConflict = "ERR_EXC_CONFLICT";
|
||||
|
||||
// CLI-ORCH-32-001: Orchestrator error codes (exit code 17)
|
||||
public const string OrchSourceNotFound = "ERR_ORCH_SOURCE_NOT_FOUND";
|
||||
public const string OrchSourcePaused = "ERR_ORCH_SOURCE_PAUSED";
|
||||
public const string OrchSourceThrottled = "ERR_ORCH_SOURCE_THROTTLED";
|
||||
public const string OrchTestFailed = "ERR_ORCH_TEST_FAILED";
|
||||
public const string OrchQuotaExceeded = "ERR_ORCH_QUOTA_EXCEEDED";
|
||||
public const string OrchConnectionFailed = "ERR_ORCH_CONNECTION_FAILED";
|
||||
|
||||
// CLI-PARITY-41-001: SBOM error codes (exit code 18)
|
||||
public const string SbomNotFound = "ERR_SBOM_NOT_FOUND";
|
||||
public const string SbomConnectionFailed = "ERR_SBOM_CONNECTION_FAILED";
|
||||
public const string SbomExportFailed = "ERR_SBOM_EXPORT_FAILED";
|
||||
public const string SbomCompareFailed = "ERR_SBOM_COMPARE_FAILED";
|
||||
public const string SbomInvalidFormat = "ERR_SBOM_INVALID_FORMAT";
|
||||
public const string SbomOfflineViolation = "ERR_SBOM_OFFLINE_VIOLATION";
|
||||
|
||||
// CLI-PARITY-41-002: Notify error codes (exit code 19)
|
||||
public const string NotifyChannelNotFound = "ERR_NOTIFY_CHANNEL_NOT_FOUND";
|
||||
public const string NotifyDeliveryNotFound = "ERR_NOTIFY_DELIVERY_NOT_FOUND";
|
||||
public const string NotifyConnectionFailed = "ERR_NOTIFY_CONNECTION_FAILED";
|
||||
public const string NotifySendFailed = "ERR_NOTIFY_SEND_FAILED";
|
||||
public const string NotifyTestFailed = "ERR_NOTIFY_TEST_FAILED";
|
||||
public const string NotifyRetryFailed = "ERR_NOTIFY_RETRY_FAILED";
|
||||
public const string NotifyOfflineViolation = "ERR_NOTIFY_OFFLINE_VIOLATION";
|
||||
|
||||
// CLI-SBOM-60-001: Sbomer error codes (exit code 20)
|
||||
public const string SbomerLayerNotFound = "ERR_SBOMER_LAYER_NOT_FOUND";
|
||||
public const string SbomerCompositionNotFound = "ERR_SBOMER_COMPOSITION_NOT_FOUND";
|
||||
public const string SbomerDsseInvalid = "ERR_SBOMER_DSSE_INVALID";
|
||||
public const string SbomerContentHashMismatch = "ERR_SBOMER_CONTENT_HASH_MISMATCH";
|
||||
public const string SbomerMerkleProofInvalid = "ERR_SBOMER_MERKLE_PROOF_INVALID";
|
||||
public const string SbomerComposeFailed = "ERR_SBOMER_COMPOSE_FAILED";
|
||||
public const string SbomerVerifyFailed = "ERR_SBOMER_VERIFY_FAILED";
|
||||
public const string SbomerNonDeterministic = "ERR_SBOMER_NON_DETERMINISTIC";
|
||||
public const string SbomerOfflineViolation = "ERR_SBOMER_OFFLINE_VIOLATION";
|
||||
|
||||
// CLI-POLICY-27-006: Policy Studio scope error codes (exit code 3)
|
||||
public const string PolicyStudioScopeRequired = "ERR_SCOPE_POLICY_STUDIO_REQUIRED";
|
||||
public const string PolicyStudioScopeInvalid = "ERR_SCOPE_POLICY_STUDIO_INVALID";
|
||||
public const string PolicyStudioScopeWorkflowRequired = "ERR_SCOPE_POLICY_WORKFLOW_REQUIRED";
|
||||
public const string PolicyStudioScopePublishRequired = "ERR_SCOPE_POLICY_PUBLISH_REQUIRED";
|
||||
public const string PolicyStudioScopeSignRequired = "ERR_SCOPE_POLICY_SIGN_REQUIRED";
|
||||
|
||||
// CLI-RISK-66-001: Risk scope error codes (exit code 3)
|
||||
public const string RiskScopeRequired = "ERR_SCOPE_RISK_REQUIRED";
|
||||
public const string RiskScopeProfileRequired = "ERR_SCOPE_RISK_PROFILE_REQUIRED";
|
||||
public const string RiskScopeSimulateRequired = "ERR_SCOPE_RISK_SIMULATE_REQUIRED";
|
||||
|
||||
// CLI-SIG-26-001: Reachability scope error codes (exit code 3)
|
||||
public const string ReachabilityScopeRequired = "ERR_SCOPE_REACHABILITY_REQUIRED";
|
||||
public const string ReachabilityScopeUploadRequired = "ERR_SCOPE_REACHABILITY_UPLOAD_REQUIRED";
|
||||
}
|
||||
Reference in New Issue
Block a user