# typed: strict # frozen_string_literal: true require "sorbet-runtime" require "dependabot/utils" # rubocop:disable Metrics/ModuleLength module Dependabot extend T::Sig module ErrorAttributes BACKTRACE = "error-backtrace" CLASS = "error-class" DETAILS = "error-details" FINGERPRINT = "fingerprint" MESSAGE = "error-message" DEPENDENCIES = "job-dependencies" DEPENDENCY_GROUPS = "job-dependency-groups" JOB_ID = "job-id" PACKAGE_MANAGER = "package-manager" SECURITY_UPDATE = "security-update" end # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/CyclomaticComplexity sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.untyped])) } def self.fetcher_error_details(error) case error when Dependabot::ToolVersionNotSupported { "error-type": "tool_version_not_supported", "error-detail": { "tool-name": error.tool_name, "detected-version": error.detected_version, "supported-versions": error.supported_versions } } when Dependabot::ToolFeatureNotSupported { "error-type": "tool_feature_not_supported", "error-detail": { "tool-name": error.tool_name, "tool-type": error.tool_type, feature: error.feature } } when Dependabot::BranchNotFound { "error-type": "branch_not_found", "error-detail": { "branch-name": error.branch_name, message: error.message } } when Dependabot::DirectoryNotFound { "error-type": "directory_not_found", "error-detail": { "directory-name": error.directory_name } } when Dependabot::RepoNotFound # This happens if the repo gets removed after a job gets kicked off. # This also happens when a configured personal access token is not authz'd to fetch files from the job repo. { "error-type": "job_repo_not_found", "error-detail": { message: error.message } } when Dependabot::DependencyFileNotParseable { "error-type": "dependency_file_not_parseable", "error-detail": { message: error.message, "file-path": error.file_path } } when Dependabot::DependencyFileNotFound { "error-type": "dependency_file_not_found", "error-detail": { message: error.message, "file-path": error.file_path } } when Dependabot::OutOfDisk { "error-type": "out_of_disk", "error-detail": {} } when Dependabot::PathDependenciesNotReachable { "error-type": "path_dependencies_not_reachable", "error-detail": { dependencies: error.dependencies } } when Dependabot::PrivateSourceAuthenticationFailure { "error-type": "private_source_authentication_failure", "error-detail": { source: error.source } } when Dependabot::PrivateSourceBadResponse { "error-type": "private_source_bad_response", "error-detail": { source: error.source } } when Dependabot::DependencyNotFound { "error-type": "dependency_not_found", "error-detail": { source: error.source } } when Octokit::Unauthorized { "error-type": "octokit_unauthorized" } when Octokit::ServerError # If we get a 500 from GitHub there's very little we can do about it, # and responsibility for fixing it is on them, not us. As a result we # quietly log these as errors { "error-type": "server_error" } when BadRequirementError { "error-type": "illformed_requirement", "error-detail": { message: error.message } } when *Octokit::RATE_LIMITED_ERRORS # If we get a rate-limited error we let dependabot-api handle the # retry by re-enqueing the update job after the reset { "error-type": "octokit_rate_limited", "error-detail": { "rate-limit-reset": T.cast(error, Octokit::Error).response_headers["X-RateLimit-Reset"] } } end end # rubocop:enable Metrics/CyclomaticComplexity sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.untyped])) } def self.parser_error_details(error) case error when Dependabot::ToolFeatureNotSupported { "error-type": "tool_feature_not_supported", "error-detail": { "tool-name": error.tool_name, "tool-type": error.tool_type, feature: error.feature } } when Dependabot::DependencyFileNotEvaluatable { "error-type": "dependency_file_not_evaluatable", "error-detail": { message: error.message } } when Dependabot::DependencyFileNotResolvable { "error-type": "dependency_file_not_resolvable", "error-detail": { message: error.message } } when Dependabot::BranchNotFound { "error-type": "branch_not_found", "error-detail": { "branch-name": error.branch_name, message: error.message } } when Dependabot::DependencyFileNotParseable { "error-type": "dependency_file_not_parseable", "error-detail": { message: error.message, "file-path": error.file_path } } when Dependabot::DependencyFileNotFound { "error-type": "dependency_file_not_found", "error-detail": { message: error.message, "file-path": error.file_path } } when Dependabot::PathDependenciesNotReachable { "error-type": "path_dependencies_not_reachable", "error-detail": { dependencies: error.dependencies } } when Dependabot::PrivateSourceAuthenticationFailure { "error-type": "private_source_authentication_failure", "error-detail": { source: error.source } } when Dependabot::PrivateSourceBadResponse { "error-type": "private_source_bad_response", "error-detail": { source: error.source } } when Dependabot::GitDependenciesNotReachable { "error-type": "git_dependencies_not_reachable", "error-detail": { "dependency-urls": error.dependency_urls } } when Dependabot::NotImplemented { "error-type": "not_implemented", "error-detail": { message: error.message } } when Octokit::ServerError # If we get a 500 from GitHub there's very little we can do about it, # and responsibility for fixing it is on them, not us. As a result we # quietly log these as errors { "error-type": "server_error" } end end # rubocop:disable Lint/RedundantCopDisableDirective # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/AbcSize sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.untyped])) } def self.updater_error_details(error) case error when Dependabot::ToolFeatureNotSupported { "error-type": "tool_feature_not_supported", "error-detail": { "tool-name": error.tool_name, "tool-type": error.tool_type, feature: error.feature } } when Dependabot::DependencyFileNotResolvable { "error-type": "dependency_file_not_resolvable", "error-detail": { message: error.message } } when Dependabot::DependencyFileNotEvaluatable { "error-type": "dependency_file_not_evaluatable", "error-detail": { message: error.message } } when Dependabot::DependencyFileNotParseable { "error-type": "dependency_file_not_parseable", "error-detail": { message: error.message, "file-path": error.file_path } } when Dependabot::DependencyFileNotSupported { "error-type": "dependency_file_not_supported", "error-detail": { message: error.message } } when Dependabot::GitDependenciesNotReachable { "error-type": "git_dependencies_not_reachable", "error-detail": { "dependency-urls": error.dependency_urls } } when Dependabot::DependencyFileNotFound { "error-type": "dependency_file_not_found", "error-detail": { message: error.message, "file-path": error.file_path } } when Dependabot::DependencyFileContentNotChanged { "error-type": "dependency_file_content_not_changed", "error-detail": { message: error.message } } when Dependabot::ToolVersionNotSupported { "error-type": "tool_version_not_supported", "error-detail": { "tool-name": error.tool_name, "detected-version": error.detected_version, "supported-versions": error.supported_versions } } when Dependabot::MisconfiguredTooling { "error-type": "misconfigured_tooling", "error-detail": { "tool-name": error.tool_name, message: error.tool_message } } when Dependabot::GitDependencyReferenceNotFound { "error-type": "git_dependency_reference_not_found", "error-detail": { dependency: error.dependency } } when Dependabot::PrivateSourceAuthenticationFailure { "error-type": "private_source_authentication_failure", "error-detail": { source: error.source } } when Dependabot::PrivateSourceBadResponse { "error-type": "private_source_bad_response", "error-detail": { source: error.source } } when Dependabot::DependencyNotFound { "error-type": "dependency_not_found", "error-detail": { source: error.source } } when Dependabot::PrivateSourceTimedOut { "error-type": "private_source_timed_out", "error-detail": { source: error.source } } when Dependabot::PrivateSourceCertificateFailure { "error-type": "private_source_certificate_failure", "error-detail": { source: error.source } } when Dependabot::MissingEnvironmentVariable { "error-type": "missing_environment_variable", "error-detail": { "environment-variable": error.environment_variable, "error-message": error.message } } when Dependabot::OutOfDisk { "error-type": "out_of_disk", "error-detail": {} } when Dependabot::GoModulePathMismatch { "error-type": "go_module_path_mismatch", "error-detail": { "declared-path": error.declared_path, "discovered-path": error.discovered_path, "go-mod": error.go_mod } } when Dependabot::UpdateNotPossible { "error-type": "update_not_possible", "error-detail": { dependencies: error.dependencies } } when BadRequirementError { "error-type": "illformed_requirement", "error-detail": { message: error.message } } when RegistryError { "error-type": "registry_error", "error-detail": { status: error.status, msg: error.message } } when IncompatibleCPU, NetworkUnsafeHTTP, SnapshotsUnavailableGraphError error.detail when Dependabot::NotImplemented { "error-type": "not_implemented", "error-detail": { message: error.message } } when Dependabot::InvalidGitAuthToken { "error-type": "git_token_auth_error", "error-detail": { message: error.message } } when *Octokit::RATE_LIMITED_ERRORS # If we get a rate-limited error we let dependabot-api handle the # retry by re-enqueing the update job after the reset { "error-type": "octokit_rate_limited", "error-detail": { "rate-limit-reset": T.cast(error, Octokit::Error).response_headers["X-RateLimit-Reset"] } } end end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Lint/RedundantCopDisableDirective # rubocop:enable Metrics/AbcSize class DependabotError < StandardError extend T::Sig BASIC_AUTH_REGEX = %r{://(?[^:@]*:[^@%\s/]+(@|%40))} # Remove any path segment from fury.io sources FURY_IO_PATH_REGEX = %r{fury\.io/(?.+)} sig { params(message: T.any(T.nilable(String), MatchData)).void } def initialize(message = nil) super(sanitize_message(message)) end private sig { params(message: T.any(T.nilable(String), MatchData)).returns(T.any(T.nilable(String), MatchData)) } def sanitize_message(message) return message unless message.is_a?(String) path_regex = Regexp.escape(Utils::BUMP_TMP_DIR_PATH) + "\\/" + Regexp.escape(Utils::BUMP_TMP_FILE_PREFIX) + "[a-zA-Z0-9-]*" message = message.gsub(/#{path_regex}/, "dependabot_tmp_dir").strip filter_sensitive_data(message) end sig { params(message: String).returns(String) } def filter_sensitive_data(message) replace_capture_groups(message, BASIC_AUTH_REGEX, "") end sig { params(source: String).returns(String) } def sanitize_source(source) source = filter_sensitive_data(source) replace_capture_groups(source, FURY_IO_PATH_REGEX, "") end sig do params( string: String, regex: Regexp, replacement: String ).returns(String) end def replace_capture_groups(string, regex, replacement) string.scan(regex).flatten.compact.reduce(string) do |original_msg, match| original_msg.gsub(match, replacement) end end end class TypedDependabotError < Dependabot::DependabotError extend T::Sig sig { returns(String) } attr_reader :error_type sig { params(error_type: String, message: T.any(T.nilable(String), MatchData)).void } def initialize(error_type, message = nil) @error_type = T.let(error_type, String) super(message || error_type) end sig { params(hash: T.nilable(T::Hash[Symbol, T.untyped])).returns(T::Hash[Symbol, T.untyped]) } def detail(hash = nil) { "error-type": error_type, "error-detail": hash || { message: message } } end end class OutOfDisk < DependabotError; end class OutOfMemory < DependabotError; end class NotImplemented < DependabotError; end class InvalidGitAuthToken < DependabotError; end ##################### # Repo level errors # ##################### class DirectoryNotFound < DependabotError extend T::Sig sig { returns(String) } attr_reader :directory_name sig { params(directory_name: String, msg: T.nilable(String)).void } def initialize(directory_name, msg = nil) @directory_name = directory_name super(msg) end end class BranchNotFound < DependabotError extend T::Sig sig { returns(T.nilable(String)) } attr_reader :branch_name sig { params(branch_name: T.nilable(String), msg: T.nilable(String)).void } def initialize(branch_name, msg = nil) @branch_name = branch_name super(msg) end end class RepoNotFound < DependabotError extend T::Sig sig { returns(T.any(Dependabot::Source, String)) } attr_reader :source sig { params(source: T.any(Dependabot::Source, String), msg: T.nilable(String)).void } def initialize(source, msg = nil) @source = source super(msg) end end ##################### # File level errors # ##################### class MisconfiguredTooling < DependabotError extend T::Sig sig { returns(String) } attr_reader :tool_name sig { returns(String) } attr_reader :tool_message sig do params( tool_name: String, tool_message: String ).void end def initialize(tool_name, tool_message) @tool_name = tool_name @tool_message = tool_message msg = "Dependabot detected that #{tool_name} is misconfigured in this repository. " \ "Running `#{tool_name.downcase}` results in the following error: #{tool_message}" super(msg) end end class ToolVersionNotSupported < DependabotError extend T::Sig sig { returns(String) } attr_reader :tool_name sig { returns(String) } attr_reader :detected_version sig { returns(String) } attr_reader :supported_versions sig do params( tool_name: String, detected_version: String, supported_versions: String ).void end def initialize(tool_name, detected_version, supported_versions) @tool_name = tool_name @detected_version = detected_version @supported_versions = supported_versions msg = "Dependabot detected the following #{tool_name} requirement for your project: '#{detected_version}'." \ "\n\nCurrently, the following #{tool_name} versions are supported in Dependabot: #{supported_versions}." super(msg) end end class ToolFeatureNotSupported < DependabotError extend T::Sig sig { returns(String) } attr_reader :tool_name, :tool_type, :feature sig do params( tool_name: String, tool_type: String, feature: String ).void end def initialize(tool_name:, tool_type:, feature:) @tool_name = tool_name @tool_type = tool_type @feature = feature super(build_message) end private sig { returns(String) } def build_message "Dependabot doesn't support the feature '#{feature}' for #{tool_name} (#{tool_type}). " \ "Please refer to the documentation for supported features." end end class DependencyFileNotFound < DependabotError extend T::Sig sig { returns(T.nilable(String)) } attr_reader :file_path sig { params(file_path: T.nilable(String), msg: T.nilable(String)).void } def initialize(file_path, msg = nil) @file_path = file_path super(msg || "#{file_path} not found") end sig { returns(T.nilable(String)) } def file_name return unless file_path T.must(file_path).split("/").last end sig { returns(T.nilable(String)) } def directory # Directory should always start with a `/` return unless file_path T.must(T.must(file_path).split("/")[0..-2]).join("/").sub(%r{^/*}, "/") end end class DependencyFileNotParseable < DependabotError extend T::Sig sig { returns(String) } attr_reader :file_path sig { params(file_path: String, msg: T.nilable(String)).void } def initialize(file_path, msg = nil) @file_path = file_path super(msg || "#{file_path} not parseable") end sig { returns(String) } def file_name T.must(file_path.split("/").last) end sig { returns(String) } def directory # Directory should always start with a `/` T.must(file_path.split("/")[0..-2]).join("/").sub(%r{^/*}, "/") end end class DependencyFileNotEvaluatable < DependabotError; end class DependencyFileNotResolvable < DependabotError; end class DependencyFileNotSupported < DependabotError; end class DependencyFileContentNotChanged < DependabotError; end class BadRequirementError < Gem::Requirement::BadRequirementError; end ####################### # Source level errors # ####################### class PrivateSourceAuthenticationFailure < DependabotError extend T::Sig sig { returns(String) } attr_reader :source sig { params(source: T.nilable(String)).void } def initialize(source) @source = T.let(sanitize_source(T.must(source)), String) msg = "The following source could not be reached as it requires " \ "authentication (and any provided details were invalid or lacked " \ "the required permissions): #{@source}" super(msg) end end class PrivateSourceBadResponse < DependabotError extend T::Sig sig { returns(String) } attr_reader :source sig { params(source: T.nilable(String)).void } def initialize(source) @source = T.let(sanitize_source(T.must(source)), String) msg = "Bad response error while accessing source: #{@source}" super(msg) end end class PrivateSourceTimedOut < DependabotError extend T::Sig sig { returns(String) } attr_reader :source sig { params(source: String).void } def initialize(source) @source = T.let(sanitize_source(source), String) super("The following source timed out: #{@source}") end end class PrivateSourceCertificateFailure < DependabotError extend T::Sig sig { returns(String) } attr_reader :source sig { params(source: String).void } def initialize(source) @source = T.let(sanitize_source(source), String) super("Could not verify the SSL certificate for #{@source}") end end class MissingEnvironmentVariable < DependabotError extend T::Sig sig { returns(String) } attr_reader :environment_variable sig { returns(String) } attr_reader :message sig { params(environment_variable: String, message: String).void } def initialize(environment_variable, message = "") @environment_variable = environment_variable @message = message super("Missing environment variable #{@environment_variable}. #{@message}") end end class DependencyNotFound < DependabotError extend T::Sig sig { returns(String) } attr_reader :source sig { params(source: T.nilable(String)).void } def initialize(source) @source = T.let(sanitize_source(T.must(source)), String) msg = "The following dependency could not be found : #{@source}" super(msg) end end class InvalidGitAuthToken < DependabotError extend T::Sig sig { returns(String) } attr_reader :source sig { params(source: String).void } def initialize(source) @source = T.let(sanitize_source(source), String) msg = "Missing or invalid authentication token while accessing github package : #{@source}" super(msg) end end class RegistryError < DependabotError extend T::Sig sig { returns(Integer) } attr_reader :status sig { params(status: Integer, msg: String).void } def initialize(status, msg) @status = status super(msg) end end # Useful for JS file updaters, where the registry API sometimes returns # different results to the actual update process class InconsistentRegistryResponse < DependabotError; end ########################### # Dependency level errors # ########################### class UpdateNotPossible < DependabotError extend T::Sig sig { returns(T::Array[String]) } attr_reader :dependencies sig { params(dependencies: T::Array[String]).void } def initialize(dependencies) @dependencies = dependencies msg = "The following dependencies could not be updated: #{@dependencies.join(', ')}" super(msg) end end class GitDependenciesNotReachable < DependabotError extend T::Sig sig { returns(T::Array[String]) } attr_reader :dependency_urls sig { params(dependency_urls: T.any(String, T::Array[String])).void } def initialize(*dependency_urls) @dependency_urls = T.let(dependency_urls.flatten.map { |uri| filter_sensitive_data(uri) }, T::Array[String]) msg = "The following git URLs could not be retrieved: " \ "#{@dependency_urls.join(', ')}" super(msg) end end class GitDependencyReferenceNotFound < DependabotError extend T::Sig sig { returns(String) } attr_reader :dependency sig { params(dependency: String).void } def initialize(dependency) @dependency = dependency msg = "The branch or reference specified for #{@dependency} could not " \ "be retrieved" super(msg) end end class PathDependenciesNotReachable < DependabotError extend T::Sig sig { returns(T::Array[String]) } attr_reader :dependencies sig { params(dependencies: T.any(String, T::Array[String])).void } def initialize(*dependencies) @dependencies = T.let(dependencies.flatten, T::Array[String]) msg = "The following path based dependencies could not be retrieved: " \ "#{@dependencies.join(', ')}" super(msg) end end class GoModulePathMismatch < DependabotError extend T::Sig sig { returns(String) } attr_reader :go_mod sig { returns(String) } attr_reader :declared_path sig { returns(String) } attr_reader :discovered_path sig { params(go_mod: String, declared_path: String, discovered_path: String).void } def initialize(go_mod, declared_path, discovered_path) @go_mod = go_mod @declared_path = declared_path @discovered_path = discovered_path msg = "The module path '#{@declared_path}' found in #{@go_mod} doesn't " \ "match the actual path '#{@discovered_path}' in the dependency's " \ "go.mod" super(msg) end end # Raised by UpdateChecker if all candidate updates are ignored class AllVersionsIgnored < DependabotError; end # Raised by FileParser if processing may execute external code in the update context class UnexpectedExternalCode < DependabotError; end class IncompatibleCPU < TypedDependabotError sig { params(message: T.any(T.nilable(String), MatchData)).void } def initialize(message = nil) super("incompatible_cpu", message) end end class NetworkUnsafeHTTP < TypedDependabotError sig { params(message: T.any(T.nilable(String), MatchData)).void } def initialize(message = nil) super("network_unsafe_http", message) end end class SnapshotsUnavailableGraphError < TypedDependabotError sig { params(message: T.any(T.nilable(String), MatchData)).void } def initialize(message = nil) super("snapshots_unavailable_graph_error", message) end end end # rubocop:enable Metrics/ModuleLength