💭

Macのローカルでdependabot-coreをDockerなしで動かす

に公開

これです。
https://github.com/dependabot/dependabot-core

動機

GitHubじゃないけどDependabotを使いたい。
とりあえずローカルでちょろっと動かしたい。

前提

  • Macで動かしている
  • Rubyはrbenvで管理されている
  • Dockerで動かすべきものをローカルで動かしてるので、npmやpipなどでグローバルにライブラリがインストールされる(具体的にはhelpersのbuildのところでインストールされます)
  • 試行錯誤したあとに書いてるので、もしかすると手順が違うところあるかもしれません(すみません)

作業手順

まずはリポジトリをCloneしてGemをインストールします。

git clone https://github.com/dependabot/dependabot-core.git
cd dependabot-core
rbenv exec bundle install

私の場合、途中でコマンド間違ってたのかなんか変なことになってました。
bundle installでYou don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory.というエラーがでてきて、rbenv使ってるはずなのにシステムのrubyが使われてるエラーがでました。
確認するとなぜかシステムの方が採用されていたので、rm -rf .ruby-versionで不要なものを消すと直りました。

% rbenv versions    
/rbenvのパス/libexec/rbenv-version-file-read: line 11: read: read error: 0: Is a directory
* system (set by /cloneしたパス/dependabot-core/.ruby-version)
  3.2.2

npm/yarnの脆弱性をチェック

途中でNodeのプロセスを起動するようで、事前に依存関係のインストールが必要。
グローバルにインストールされると思うので困る人は注意。

cd npm_and_yarn/helpers
export DEPENDABOT_NATIVE_HELPERS_PATH=/Cloneしたディレクトリ/dependabot-core
zsh build

すると色々インストールされるので終わったら元のディレクトリに戻る。

cd -

このディレクトリでアップデートチェック用のスクリプトを書く。
Ruby書いたことはなく、ChatGPTに書いてもらったものなので良し悪しはよくわかっていません・・・

require "net/http"
require "json"
require "uri"

require "dependabot/npm_and_yarn"
require "dependabot/file_fetchers"
require "dependabot/file_parsers"
require "dependabot/update_checkers"
require "dependabot/file_updaters"
require "dependabot/metadata_finders"
require "dependabot/dependency_file"
require "dependabot/source"


def fetch_advisories(package_name)
  uri = URI("https://api.github.com/graphql")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  query = <<~GRAPHQL
    {
      securityVulnerabilities(ecosystem: NPM, package: "#{package_name}", first: 20) {
        nodes {
          package {
            name
          }
          vulnerableVersionRange
          firstPatchedVersion {
            identifier
          }
        }
      }
    }
  GRAPHQL

  req = Net::HTTP::Post.new(uri)
  req["Authorization"] = "Bearer #{ENV['LOCAL_GITHUB_ACCESS_TOKEN']}"
  req["Content-Type"] = "application/json"
  req.body = { query: query }.to_json

  res = http.request(req)
  JSON.parse(res.body)
end

def convert_to_advisories(data)
  advisories = []
  nodes = data.dig("data", "securityVulnerabilities", "nodes") || []
  nodes.each do |node|
    advisories << Dependabot::SecurityAdvisory.new(
      dependency_name: node["package"]["name"],
      package_manager: "npm_and_yarn",
      vulnerable_versions: [node["vulnerableVersionRange"]],
      safe_versions: [node.dig("firstPatchedVersion", "identifier")].compact
    )
  end
  advisories
end


source = Dependabot::Source.new(
  provider: "source",
  repo: "local/repo",
  directory: "/",
  branch: nil,
  hostname: "github.com",
  api_endpoint: "https://api.github.com/"
)

fetcher = Dependabot::FileFetchers.for_package_manager("npm_and_yarn").new(
  source: source,
  credentials: [],
  repo_contents_path: "/npm プロジェクトのローカルPC上のパスをここに書く。解析対象はpackage.jsonとlockファイルなので、それらがある場所。"
)

files = fetcher.files

parser = Dependabot::FileParsers.for_package_manager("npm_and_yarn").new(
  dependency_files: files,
  source: source,
  credentials: []
)

dependencies = parser.parse

dependencies.each do |dep|
  advisory_data = fetch_advisories(dep.name)
  advisories = convert_to_advisories(advisory_data)

  checker = Dependabot::UpdateCheckers.for_package_manager("npm_and_yarn").new(
    dependency: dep,
    dependency_files: files,
    credentials: [],
    security_advisories: advisories
  )

  if checker.vulnerable?
    puts "🚨 #{dep.name} is vulnerable!"
    if checker.lowest_security_fix_version
      puts "   👉 fix available in #{checker.lowest_security_fix_version}"
    else
      puts "   ❌ no safe version available"
    end
  elsif checker.up_to_date?
    #puts "✅ #{dep.name} is up to date"
  else
    puts "⬆️ #{dep.name} can be updated from #{dep.version} to #{checker.latest_version}"
  end
end

GitHubのAPI呼び出しをするのでTokenの設定が必要です。
classicでやったのですが以下の権限のあるTokenを発行しました。

  • read:packages
  • read:org

環境変数にTokenを設定してプログラムを実行。

export LOCAL_GITHUB_ACCESS_TOKEN=GitHubのトークン
bundle exec ruby さっき書いたスクリプト.rb

出力結果はこんな感じ。

⬆️ tailwindcss can be updated from 3.4.4 to 4.0.17
⬆️ ts-jest can be updated from 29.1.4 to 29.3.0
⬆️ typescript can be updated from 5.4.5 to 5.8.2
🚨 undici is vulnerable!
   👉 fix available in 6.21.1
🚨 vite is vulnerable!
   👉 fix available in 5.4.15
⬆️ vite-tsconfig-paths can be updated from 4.3.2 to 5.1.4

都度、この出力の1行1行、GitHubのAPIを呼び出してチェックしてるので呼びすぎると制限に引っ掛かるかもしれません。

pythonの脆弱性チェック

npm_and_yarnのようにbuildします。
グローバルにインストールされると思うので困る人は注意。
DEPENDABOT_NATIVE_HELPERS_PATHはnpm_and_yarnと同じなので実行済みなら不要。

cd python/helpers/
export DEPENDABOT_NATIVE_HELPERS_PATH=/Cloneしたディレクトリ/dependabot-core
zsh build

すると色々インストールされるので終わったら元のディレクトリに戻る。

cd -

このディレクトリでアップデートチェック用のスクリプトを書く。
npm/yarnの差分はパッケージマネジャーの名前のところをpythonとかpipとかに変えているだけ。

require "net/http"
require "json"
require "uri"

require "dependabot/python"
require "dependabot/file_fetchers"
require "dependabot/file_parsers"
require "dependabot/update_checkers"
require "dependabot/file_updaters"
require "dependabot/metadata_finders"
require "dependabot/dependency_file"
require "dependabot/source"


def fetch_advisories(package_name)
  uri = URI("https://api.github.com/graphql")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  query = <<~GRAPHQL
    {
      securityVulnerabilities(ecosystem: PYTHON, package: "#{package_name}", first: 20) {
        nodes {
          package {
            name
          }
          vulnerableVersionRange
          firstPatchedVersion {
            identifier
          }
        }
      }
    }
  GRAPHQL

  req = Net::HTTP::Post.new(uri)
  req["Authorization"] = "Bearer #{ENV['LOCAL_GITHUB_ACCESS_TOKEN']}"
  req["Content-Type"] = "application/json"
  req.body = { query: query }.to_json

  res = http.request(req)
  JSON.parse(res.body)
end

def convert_to_advisories(data)
  advisories = []
  nodes = data.dig("data", "securityVulnerabilities", "nodes") || []
  nodes.each do |node|
    advisories << Dependabot::SecurityAdvisory.new(
      dependency_name: node["package"]["name"],
      package_manager: "pip",
      vulnerable_versions: [node["vulnerableVersionRange"]],
      safe_versions: [node.dig("firstPatchedVersion", "identifier")].compact
    )
  end
  advisories
end


source = Dependabot::Source.new(
  provider: "source",
  repo: "local/repo",
  directory: "/",
  branch: nil,
  hostname: "github.com",
  api_endpoint: "https://api.github.com/"
)

fetcher = Dependabot::FileFetchers.for_package_manager("pip").new(
  source: source,
  credentials: [],
  repo_contents_path: "python プロジェクトのパスをこの中に置く"
)

files = fetcher.files

parser = Dependabot::FileParsers.for_package_manager("pip").new(
  dependency_files: files,
  source: source,
  credentials: []
)

dependencies = parser.parse

dependencies.each do |dep|
  advisory_data = fetch_advisories(dep.name)
  advisories = convert_to_advisories(advisory_data)

  checker = Dependabot::UpdateCheckers.for_package_manager("pip").new(
    dependency: dep,
    dependency_files: files,
    credentials: [],
    security_advisories: advisories
  )

  if checker.vulnerable?
    puts "🚨 #{dep.name} is vulnerable!"
    if checker.lowest_security_fix_version
      puts "   👉 fix available in #{checker.lowest_security_fix_version}"
    else
      puts "   ❌ no safe version available"
    end
  elsif checker.up_to_date?
    #puts "✅ #{dep.name} is up to date"
  else
    puts "⬆️ #{dep.name} can be updated from #{dep.version} to #{checker.latest_version}"
  end
end

実行手順と結果はnpm_and_yarnの場合と同じ。

他のパッケージマネージャー

ディレクトリを眺めればなんとなくわかると思います。
nuget(.NET)やpub(flutter)などなど、色々あります。

ChatGPTに聞けばこんな感じで対応表を教えてくれると思います。

パッケージマネージャ for_package_manager(...) の正しい指定名
npm / yarn "npm_and_yarn"
pip / pipenv / poetry "pip"
bundler (Ruby) "bundler"
cargo (Rust) "cargo"
composer (PHP) "composer"
nuget (.NET) "nuget"
maven "maven"
gradle "gradle"
go modules "go_modules"

おわり

導入しづらい環境の場合はこういうやり方も良いかと思って試してみました。

Discussion