382 lines
17 KiB
C#
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";
|
|
}
|