Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Output/CliError.cs
2026-02-01 21:37:40 +02:00

382 lines
17 KiB
C#

using StellaOps.Cli.Services.Models.Transport;
using System;
using System.Collections.Generic;
using System.Linq;
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>
/// Error code for offline mode violations.
/// </summary>
public const string OfflineMode = "ERR_OFFLINE_MODE";
/// <summary>
/// Creates an error from an error code.
/// </summary>
public static CliError FromCode(string code, string? message = null)
{
return new CliError(code, message ?? $"Error: {code}");
}
/// <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>
internal 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>
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<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";
// 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";
}