aegis-cli - サプライチェーンscannerを作ったので紹介する
はじめに
aegis-cli という、9つのパッケージエコシステムに対応したサプライチェーンセキュリティscannerを作りました。本記事では作った動機、内部の仕組み、CI gateとしての使い方をまとめます。
特徴を先にあげる以下のようなものがあります↓
- アカウント・APIキー・バックエンド不要、ローカル完結
- npm / PyPI / RubyGems / crates.io / Go / Maven / Packagist / NuGet / Gleamに対応
- lockfileから直接依存と推移的依存をまとめて解析
- tree-sitterによるAST scanで
shell-spawn/net-egress/dynamic-evalなどのcapabilityを検出 - OSV.dev へのbatchクエリでCVE / GHSAをまとめて取得
- 振る舞いheuristicsで
curl|shpostinstall、typosquat、maintainer hijackなどを検知 - CI gateとして
--fail-on閾値でexit 1を返す
きっかけ
ここ数年、JavaScriptとPython界隈でサプライチェーン攻撃が立て続けに起きています。
-
ua-parser-js0.7.29 (2021) ── 悪意あるpostinstall script混入 -
event-stream(2018) ── 依存パッケージ乗っ取りからのBitcoin wallet窃取 -
colors.js/faker.js(2022) ── 作者によるself-sabotage - xz utils backdoor (2024) ── 数年がかりのmaintainer hijack
- PyPIの典型的なtyposquat攻撃 (常時発生)
これらに共通するのは、CVEが発行されるよりはるか前に「振る舞いとして怪しい」シグナルが出ている点です。postinstallで外部サーバーへ通信する、難読化されたpayloadが含まれる、似た名前のパッケージが急に登場する、といった挙動はCVEデータベースには載りません。
既存のscannerはCVE一覧との照合が中心で、advisoryが出るまでの空白を埋めにくい構造でした。そのためAST scanで「ソースの振る舞い」自体を見るアプローチを試したく、aegis-cliを作り始めました。
これは何か
aegis-cliは、lockfileを入口にして次の処理を一気通貫で行うCLIです。
-
Parse ── lockfileを読み、解決済みの
(name, version)を直接依存と推移的依存の両方について列挙します。 -
Fetch ── registryからtarballを取得します。
~/.aegis/cache/sources/にキャッシュします。 -
AST scan ── tree-sitterで各ファイルを歩き、
capability:file:line:snippetの形でevidenceを出します。 -
CVE lookup ── OSV.devにbatch POSTでまとめて問い合わせます。結果は
~/.aegis/cache/advisories/に保存します。 -
Allowlist ── builtin →
~/.aegis/allowlist.yaml→.aegis-allowlist.yamlの順に評価します。具体ルールがwildcardより優先されます。 -
Verdict ──
max(ast, advisory)を--fail-onと比較して判定します。Critical / High はblock、Medium はprompt、Low はreviewの扱いです。
外部に投げるのはOSV.devへの問い合わせのみで、AEGIS_NO_VULN_LOOKUP=1 を立てれば完全offlineで動きます。社内mirrorを使いたい場合は AEGIS_OSV_URL で向き先を変えられます。
背景・動機
既存のSnyk / Dependabot / GitHub Advanced Securityには次の課題を感じていました。
- アカウント前提 ── 評価のためだけにorg連携やtokenが必要な場面が多い。
- CVEベース寄り ── advisoryが出ていない期間の悪意あるpackageを掴めない。
- 言語ごとのscanner分断 ── monorepoに混在するnpmとPyPIとGoをまとめて扱うのが面倒。
- CI連携の不透明さ ── verdictが出るまでの根拠 (どのファイルのどの行か) が見えにくい。
aegis-cliはこの4点を起点に、「アカウントなしでlocalで完結し、9エコシステムを横断的に、evidence付きで」というところを目標にしました。
インストール
Goがあれば go install で入ります。
go install github.com/qwexvf/aegis-cli/cmd/aegis@latest
pre-built binaryもGitHub Releasesにあります。cosign署名とSLSA provenance付きで配布しているため、CIで導入する場合は検証してから使ってください。
cosign verify-blob \
--certificate-identity-regexp 'https://github.com/qwexvf/aegis-cli/.github/workflows/release.yml.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
checksums.txt
sha256sum -c checksums.txt
セットアップ・初期化
特別な初期化はありません。lockfileがあるリポジトリのrootで aegis snapshot save を実行すれば、aegis.lock が生成されます。
cd path/to/your/repo
aegis snapshot save
polyglot monorepoでも、各ecosystemのlockfileを自動検出して1つの aegis.lock にまとめます。
使い方
基本フロー
# 1. lockfileをparseしてsnapshotを保存
aegis snapshot save
# 2. AST scan + CVE lookupでenrich
aegis snapshot enrich
# 3. 結果を確認
aegis snapshot show # 直接依存だけ
aegis snapshot show --all # 推移的依存も含む
# 4. 2つのsnapshotを比較してdriftを見る
aegis snapshot diff baseline.lock
CI gate
CI上では aegis ci を呼びます。--fail-on で閾値を指定し、超えると exit 1 を返します。
aegis ci --fail-on=block
aegis ci --fail-on=prompt --json # 機械可読出力
exit codeは次のとおりです。
| Exit code | 意味 |
|---|---|
0 |
findings が閾値未満で clean |
1 |
findings が閾値以上 |
2 |
verdict失敗 (config / network エラー) |
単発のパッケージ調査
特定のパッケージだけ調べたい場合は aegis analyze を使います。registryから取ってきて scan します。
aegis analyze lodash@4.17.21
aegis analyze --evidence ua-parser-js@0.7.29
手元にソースがある場合は --local で fetch を skip できます。
aegis analyze rubygems/rest-client@1.6.13 \
--local examples/incidents/rubygems/rest-client-1.6.13/
Allowlist
意図的に dynamic-eval などの強い capability を使うpackageは、allowlistで suppress します。
# .aegis-allowlist.yaml
version: 1
rules:
- ecosystem: npm
name: lodash
version: "^4"
capability: dynamic-eval
reason: "_.template uses Function() to compile templates"
allowlistは3層です。builtin (キュレーション済み約20ルール) → user (~/.aegis/allowlist.yaml) → project (.aegis-allowlist.yaml) の順で評価され、具体的なruleがwildcardより優先されます。
aegis allowlist add lodash \
--capability=dynamic-eval \
--version='^4' \
--reason='_.template uses Function() to compile templates'
aegis allowlist list
aegis allowlist test npm/lodash@4.17.21
aegis allowlist verify
Before / After 比較
実際のサプライチェーン事件を題材に、aegis-cliが何を出すか見てみます。事件は examples/incidents/ 配下にfixtureとして同梱しています。
ua-parser-js 0.7.29 (CVE発行前のpostinstall事件)
CVE-onlyのscannerだと「該当advisoryが出ていない初期段階」では検出できませんが、aegis-cliはAST scanとheuristicsで postinstall の挙動を捕まえます。
$ aegis analyze --evidence ua-parser-js@0.7.29
PACKAGE VERDICT CAPABILITIES EVIDENCE
ua-parser-js@0.7.29 block shell-spawn, net-egress, postinstall
- shell-spawn preinstall.js:3 child_process.exec("curl http://...
- net-egress preinstall.js:5 http.get("http://...")
- postinstall package.json "preinstall": "node preinstall.js"
CVEのみ vs AST + heuristics
| 指標 | CVEベースのみ (advisory公開前) | aegis-cli |
|---|---|---|
| ua-parser-js 0.7.29検出 | ❌ (advisory未公開) | ✅ AST + postinstall heuristicsで block |
typosquat (reqeusts, urllib4 等) |
❌ | ✅ Levenshtein距離2でprompt |
| `curl | sh` postinstall | ❌ |
| 難読化payload | ❌ | ✅ dynamic-eval検出 |
| 既知CVE | ✅ | ✅ OSV.dev経由 |
CVEが出てからの照合だけでなく、出る前のevidenceを並列で持てるのが狙いです。
AST scanで見ているもの
tree-sitterのgrammarをecosystemごとに使い分け、9言語をsingle binaryで scan します。検出するcapabilityは次のとおりです。
| Capability | 例 |
|---|---|
shell-spawn |
child_process.exec, subprocess.Popen, Runtime.exec
|
net-egress |
http.get, requests.post, urllib.request
|
dynamic-eval |
eval(), Function(), exec(), instance_eval
|
fs-write-outside-root |
/tmp / /etc への書き込み |
postinstall |
npm preinstall / postinstall script |
各 capability は file:line:snippet の evidence と共に出るため、誤検知の確認が容易です。
ecosystemごとのscanner名は次のとおりです。
| Ecosystem | Scanner |
|---|---|
| npm | jsscan |
| PyPI | pyscan |
| RubyGems | rbscan |
| crates.io | rsscan |
| Go | goscan |
| Maven | jvscan |
| Packagist | phpscan |
| NuGet | csscan |
| Gleam | gleamscan |
Reachability layer ── unusedな依存にCVEがあっても刺さらないように
0.14.0で depusage ベースのreachability layerを入れました。lockfile上は依存しているがソース上は import / require されていないpackageに [unused] markerを付け、--used-only でfilterできます。
これでCVEがあっても実際は呼ばれていない依存をprioritize downしやすくなります。CI gateを通すうえで、本当にreachableな脆弱性に注力するための工夫です。
落とし穴・運用上の注意
-
初回
enrichは重い ── tarball取得 + AST scan のため、大きな monorepoだと数分かかります。2回目以降は~/.aegis/cache/配下にキャッシュされます。 -
AST scanは false positive がある ──
dynamic-evalを正当に使うlibrary (lodashの_.templateなど) は allowlist に入れてください。builtinに約20件は最初から含まれます。 -
offline運用 ──
AEGIS_NO_VULN_LOOKUP=1でOSV問い合わせを skip できます。社内mirrorがある場合はAEGIS_OSV_URLを使います。 - release署名 ── pre-built binaryを使う場合、cosign + SLSA provenanceの検証を CI に必ず組み込んでください。aegis-cli自体がサプライチェーン攻撃の対象になり得ます。
-
lockfileのfreshness ── snapshotは lockfile時点のものです。
pnpm install後はaegis snapshot saveを再実行してください。
おわりに
aegis-cliはまだ0.14系で開発中ですが、9 ecosystem / 30 incident fixture / AST + OSV ハイブリッドのscannerとして実用フェーズに入っています。サプライチェーン対策の現実解として「CVE一覧との照合だけでは足りない」と感じている方に試していただきたいです。
issue / PR / vulnerability report 歓迎します。
参考リンク
- リポジトリ: https://github.com/qwexvf/aegis-cli
- ドキュメント: https://qwexvf.github.io/aegis-cli/
- OSV.dev: https://osv.dev
- tree-sitter: https://tree-sitter.github.io/tree-sitter/
- License: Apache-2.0
「Make it simple」をテーマに、コンサルティング・システム開発を行うomeroid株式会社のテックブログです。 会社HP→ omeroid.com/ 採用情報→ wantedly.com/companies/company_5409883
Discussion