👻

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|sh postinstall、typosquat、maintainer hijackなどを検知
  • CI gateとして --fail-on 閾値で exit 1 を返す

きっかけ

ここ数年、JavaScriptとPython界隈でサプライチェーン攻撃が立て続けに起きています。

  • ua-parser-js 0.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です。

  1. Parse ── lockfileを読み、解決済みの (name, version) を直接依存と推移的依存の両方について列挙します。
  2. Fetch ── registryからtarballを取得します。~/.aegis/cache/sources/ にキャッシュします。
  3. AST scan ── tree-sitterで各ファイルを歩き、capability:file:line:snippet の形でevidenceを出します。
  4. CVE lookup ── OSV.devにbatch POSTでまとめて問い合わせます。結果は ~/.aegis/cache/advisories/ に保存します。
  5. Allowlist ── builtin → ~/.aegis/allowlist.yaml.aegis-allowlist.yaml の順に評価します。具体ルールがwildcardより優先されます。
  6. 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 歓迎します。

参考リンク

OMEROID TECH BLOG

Discussion