# 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 }
}
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 }
}
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
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{://(?<auth>[^:@]*:[^@%\s/]+(@|%40))}
# Remove any path segment from fury.io sources
FURY_IO_PATH_REGEX = %r{fury\.io/(?<path>.+)}
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, "<redacted>")
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
end
# rubocop:enable Metrics/ModuleLength