Macのローカルでdependabot-coreをDockerなしで動かす
これです。
動機
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