module Dependabot::SharedHelpers

Constants

GIT_CONFIG_GLOBAL_PATH
SIGKILL
USER_AGENT

Public Class Methods

configure_git_to_use_https(host) click to toggle source

rubocop:enable Metrics/AbcSize rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/shared_helpers.rb, line 226
def self.configure_git_to_use_https(host)
  # NOTE: we use --global here (rather than --system) so that Dependabot
  # can be run without privileged access
  run_shell_command(
    "git config --global --replace-all url.https://#{host}/."\
    "insteadOf ssh://git@#{host}/"
  )
  run_shell_command(
    "git config --global --add url.https://#{host}/."\
    "insteadOf ssh://git@#{host}:"
  )
  run_shell_command(
    "git config --global --add url.https://#{host}/."\
    "insteadOf git@#{host}:"
  )
  run_shell_command(
    "git config --global --add url.https://#{host}/."\
    "insteadOf git@#{host}/"
  )
  run_shell_command(
    "git config --global --add url.https://#{host}/."\
    "insteadOf git://#{host}/"
  )
end
configure_git_to_use_https_with_credentials(credentials) click to toggle source

rubocop:disable Metrics/AbcSize rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/shared_helpers.rb, line 173
def self.configure_git_to_use_https_with_credentials(credentials)
  File.open(GIT_CONFIG_GLOBAL_PATH, "w") do |file|
    file << "# Generated by dependabot/dependabot-core"
  end

  # Then add a file-based credential store that loads a file in this repo.
  # Under the hood this uses git credential-store, but it's invoked through
  # a wrapper binary that only allows non-mutating commands. Without this,
  # whenever the credentials are deemed to be invalid, they're erased.
  run_shell_command(
    "git config --global credential.helper "\
    "'!#{credential_helper_path} --file #{Dir.pwd}/git.store'",
    allow_unsafe_shell_command: true
  )

  github_credentials = credentials.
                       select { |c| c["type"] == "git_source" }.
                       select { |c| c["host"] == "github.com" }.
                       select { |c| c["password"] && c["username"] }

  # If multiple credentials are specified for github.com, pick the one that
  # *isn't* just an app token (since it must have been added deliberately)
  github_credential =
    github_credentials.find { |c| !c["password"]&.start_with?("v1.") } ||
    github_credentials.first

  # Make sure we always have https alternatives for github.com.
  configure_git_to_use_https("github.com") if github_credential.nil?

  deduped_credentials = credentials -
                        github_credentials +
                        [github_credential].compact

  # Build the content for our credentials file
  git_store_content = ""
  deduped_credentials.each do |cred|
    next unless cred["type"] == "git_source"
    next unless cred["username"] && cred["password"]

    authenticated_url =
      "https://#{cred.fetch('username')}:#{cred.fetch('password')}"\
      "@#{cred.fetch('host')}"

    git_store_content += authenticated_url + "\n"
    configure_git_to_use_https(cred.fetch("host"))
  end

  # Save the file
  File.write("git.store", git_store_content)
end
credential_helper_path() click to toggle source
# File lib/dependabot/shared_helpers.rb, line 167
def self.credential_helper_path
  File.join(__dir__, "../../bin/git-credential-store-immutable")
end
escape_command(command) click to toggle source

Escapes all special characters, e.g. = & | <>

# File lib/dependabot/shared_helpers.rb, line 67
def self.escape_command(command)
  command_parts = command.split.map(&:strip).reject(&:empty?)
  Shellwords.join(command_parts)
end
excon_defaults(options = nil) click to toggle source
# File lib/dependabot/shared_helpers.rb, line 144
def self.excon_defaults(options = nil)
  options ||= {}
  headers = options.delete(:headers)
  {
    connect_timeout: 5,
    write_timeout: 5,
    read_timeout: 20,
    omit_default_port: true,
    middlewares: excon_middleware,
    headers: excon_headers(headers)
  }.merge(options)
end
excon_headers(headers = nil) click to toggle source
# File lib/dependabot/shared_helpers.rb, line 137
def self.excon_headers(headers = nil)
  headers ||= {}
  {
    "User-Agent" => USER_AGENT
  }.merge(headers)
end
excon_middleware() click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/dependabot/shared_helpers.rb, line 131
def self.excon_middleware
  Excon.defaults[:middlewares] +
    [Excon::Middleware::Decompress] +
    [Excon::Middleware::RedirectFollower]
end
in_a_temporary_directory(directory = "/") { |path| ... } click to toggle source
# File lib/dependabot/shared_helpers.rb, line 41
def self.in_a_temporary_directory(directory = "/")
  Dir.mkdir(Utils::BUMP_TMP_DIR_PATH) unless Dir.exist?(Utils::BUMP_TMP_DIR_PATH)
  Dir.mktmpdir(Utils::BUMP_TMP_FILE_PREFIX, Utils::BUMP_TMP_DIR_PATH) do |dir|
    path = Pathname.new(File.join(dir, directory)).expand_path
    FileUtils.mkpath(path)
    Dir.chdir(path) { yield(path) }
  end
end
in_a_temporary_repo_directory(directory = "/", repo_contents_path = nil) { |path| ... } click to toggle source
# File lib/dependabot/shared_helpers.rb, line 25
def self.in_a_temporary_repo_directory(directory = "/",
                                       repo_contents_path = nil,
                                       &block)
  if repo_contents_path
    path = Pathname.new(File.join(repo_contents_path, directory)).
           expand_path
    reset_git_repo(repo_contents_path)
    # Handle missing directories by creating an empty one and relying on the
    # file fetcher to raise a DependencyFileNotFound error
    FileUtils.mkdir_p(path) unless Dir.exist?(path)
    Dir.chdir(path) { yield(path) }
  else
    in_a_temporary_directory(directory, &block)
  end
end
reset_git_repo(path) click to toggle source
# File lib/dependabot/shared_helpers.rb, line 251
def self.reset_git_repo(path)
  Dir.chdir(path) do
    run_shell_command("git reset HEAD --hard")
    run_shell_command("git clean -fx")
  end
end
reset_global_git_config(backup_path) click to toggle source
# File lib/dependabot/shared_helpers.rb, line 269
def self.reset_global_git_config(backup_path)
  if backup_path.nil?
    FileUtils.rm(GIT_CONFIG_GLOBAL_PATH)
    return
  end
  return unless File.exist?(backup_path)

  FileUtils.mv(backup_path, GIT_CONFIG_GLOBAL_PATH)
end
run_helper_subprocess(command:, function:, args:, env: nil, stderr_to_stdout: false, allow_unsafe_shell_command: false) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/dependabot/shared_helpers.rb, line 73
def self.run_helper_subprocess(command:, function:, args:, env: nil,
                               stderr_to_stdout: false,
                               allow_unsafe_shell_command: false)
  start = Time.now
  stdin_data = JSON.dump(function: function, args: args)
  cmd = allow_unsafe_shell_command ? command : escape_command(command)

  # NOTE: For debugging native helpers in specs and dry-run: outputs the
  # bash command to run in the tmp directory created by
  # in_a_temporary_directory
  if ENV["DEBUG_FUNCTION"] == function
    puts helper_subprocess_bash_command(stdin_data: stdin_data, command: cmd, env: env)
    # Pause execution so we can run helpers inside the temporary directory
    byebug # rubocop:disable Lint/Debugger
  end

  env_cmd = [env, cmd].compact
  stdout, stderr, process = Open3.capture3(*env_cmd, stdin_data: stdin_data)
  time_taken = Time.now - start

  if ENV["DEBUG_HELPERS"] == "true"
    puts stdout
    puts stderr
  end

  # Some package managers output useful stuff to stderr instead of stdout so
  # we want to parse this, most package manager will output garbage here so
  # would mess up json response from stdout
  stdout = "#{stderr}\n#{stdout}" if stderr_to_stdout

  error_context = {
    command: command,
    function: function,
    args: args,
    time_taken: time_taken,
    stderr_output: stderr ? stderr[0..50_000] : "", # Truncate to ~100kb
    process_exit_value: process.to_s,
    process_termsig: process.termsig
  }

  response = JSON.parse(stdout)
  return response["result"] if process.success?

  raise HelperSubprocessFailed.new(
    message: response["error"],
    error_class: response["error_class"],
    error_context: error_context,
    trace: response["trace"]
  )
rescue JSON::ParserError
  raise HelperSubprocessFailed.new(
    message: stdout || "No output from command",
    error_class: "JSON::ParserError",
    error_context: error_context
  )
end
run_shell_command(command, allow_unsafe_shell_command: false) click to toggle source
# File lib/dependabot/shared_helpers.rb, line 279
def self.run_shell_command(command, allow_unsafe_shell_command: false)
  start = Time.now
  cmd = allow_unsafe_shell_command ? command : escape_command(command)
  stdout, process = Open3.capture2e(cmd)
  time_taken = Time.now - start

  # Raise an error with the output from the shell session if the
  # command returns a non-zero status
  return stdout if process.success?

  error_context = {
    command: cmd,
    time_taken: time_taken,
    process_exit_value: process.to_s
  }

  raise SharedHelpers::HelperSubprocessFailed.new(
    message: stdout,
    error_context: error_context
  )
end
stash_global_git_config() click to toggle source
# File lib/dependabot/shared_helpers.rb, line 258
def self.stash_global_git_config
  return unless File.exist?(GIT_CONFIG_GLOBAL_PATH)

  contents = File.read(GIT_CONFIG_GLOBAL_PATH)
  digest = Digest::SHA2.hexdigest(contents)[0...10]
  backup_path = GIT_CONFIG_GLOBAL_PATH + ".backup-#{digest}"

  FileUtils.mv(GIT_CONFIG_GLOBAL_PATH, backup_path)
  backup_path
end
with_git_configured(credentials:) { || ... } click to toggle source
# File lib/dependabot/shared_helpers.rb, line 157
def self.with_git_configured(credentials:)
  backup_git_config_path = stash_global_git_config
  configure_git_to_use_https_with_credentials(credentials)
  yield
rescue Errno::ENOSPC => e
  raise Dependabot::OutOfDisk, e.message
ensure
  reset_global_git_config(backup_git_config_path)
end

Private Class Methods

helper_subprocess_bash_command(command:, stdin_data:, env:) click to toggle source
# File lib/dependabot/shared_helpers.rb, line 301
def self.helper_subprocess_bash_command(command:, stdin_data:, env:)
  escaped_stdin_data = stdin_data.gsub("\"", "\\\"")
  env_keys = env ? env.compact.map { |k, v| "#{k}=#{v}" }.join(" ") + " " : ""
  "$ cd #{Dir.pwd} && echo \"#{escaped_stdin_data}\" | #{env_keys}#{command}"
end