using StellaOps.Cli.Services.Models.Transport; using System; using System.Collections.Generic; using System.Linq; namespace StellaOps.Cli.Output; /// /// 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. /// public sealed record CliError( string Code, string Message, string? TraceId = null, string? Detail = null, IReadOnlyDictionary? Metadata = null, string? RequestId = null, string? HelpUrl = null, int? RetryAfter = null, string? Target = null) { /// /// Exit code to use when this error occurs. /// public int ExitCode => GetExitCode(Code); /// /// Maps error code prefixes to exit codes. /// 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; } /// /// Error code for offline mode violations. /// public const string OfflineMode = "ERR_OFFLINE_MODE"; /// /// Creates an error from an error code. /// public static CliError FromCode(string code, string? message = null) { return new CliError(code, message ?? $"Error: {code}"); } /// /// Creates an error from an exception. /// 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); } /// /// Creates an error from an HTTP status code. /// 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); } /// /// Creates an error from a parsed API error. /// CLI-SDK-62-002: Surfaces standardized API error envelope fields. /// internal static CliError FromParsedApiError(ParsedApiError error) { ArgumentNullException.ThrowIfNull(error); Dictionary? 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); } /// /// Creates an error from an API error envelope. /// CLI-SDK-62-002: Direct conversion from envelope format. /// internal 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? 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); } } /// /// Well-known CLI error codes. /// 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"; // CLI-AIRGAP-341-001: Offline Kit / AirGap error codes (exit code 7) public const string OfflineKitImportFailed = "ERR_AIRGAP_OFFLINE_KIT_IMPORT_FAILED"; public const string OfflineKitStatusFailed = "ERR_AIRGAP_OFFLINE_KIT_STATUS_FAILED"; public const string OfflineKitVerifyFailed = "ERR_AIRGAP_OFFLINE_KIT_VERIFY_FAILED"; public const string OfflineKitHashMismatch = "ERR_AIRGAP_OFFLINE_KIT_HASH_MISMATCH"; public const string OfflineKitCosignSignatureInvalid = "ERR_AIRGAP_OFFLINE_KIT_SIG_FAIL_COSIGN"; public const string OfflineKitManifestSignatureInvalid = "ERR_AIRGAP_OFFLINE_KIT_SIG_FAIL_MANIFEST"; public const string OfflineKitDsseVerifyFailed = "ERR_AIRGAP_OFFLINE_KIT_DSSE_VERIFY_FAIL"; public const string OfflineKitRekorVerifyFailed = "ERR_AIRGAP_OFFLINE_KIT_REKOR_VERIFY_FAIL"; public const string OfflineKitSelfTestFailed = "ERR_AIRGAP_OFFLINE_KIT_SELFTEST_FAIL"; public const string OfflineKitVersionNonMonotonic = "ERR_AIRGAP_OFFLINE_KIT_VERSION_NON_MONOTONIC"; public const string OfflineKitPolicyDenied = "ERR_AIRGAP_OFFLINE_KIT_POLICY_DENY"; 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"; }