メインコンテンツまでスキップ

サプライチェーン

Merlon の依存関係、ビルド入力、公開成果物をどう統制し、それぞれにどのような証跡があるか。メンテナ向けではなく、指摘事項を記録する必要があるレビュー担当者向けに書いている。

固定(ピン留め)

すべてのビルド入力を不変の識別子に固定している。

入力固定方法場所
ベースイメージ(Go、Node.js、Alpine、PostgreSQL)タグ かつ sha256 ダイジェストapi/Dockerfile、compose ファイル、.github/workflows/ci.yml
GitHub Actions完全なコミット SHA(末尾コメントにバージョン)全ワークフロー
Go モジュールgo.sumapi/go.sum
npm パッケージpackage-lock.jsonui/website/
Wrangler(ドキュメント配信)厳密なバージョンwebsite/package.jsondocs-deploy.yml

固定は、それを強制する仕組みの分しか価値を持たない。同一のバージョンを複数箇所に意図的に宣言し(たとえば Dockerfile と、それをテストするワークフロー)、写しが食い違った場合にガードスクリプトがビルドを失敗させる。

ガード強制する内容
scripts/check-container-pins.shPostgreSQL イメージのダイジェストが全 compose ファイルと CI で一致すること
scripts/check-toolchain-pins.shGo と Node.js のバージョンが Dockerfile・全ワークフロー・go.mod・開発コンテナで一致すること
scripts/check-wrangler-pin.shpackage.json の Wrangler バージョンとデプロイワークフローが実行するバージョンが一致すること
scripts/check-env-vars.shコードが読むすべての環境変数が文書化され、文書化されたすべての変数が読まれていること
scripts/check-doc-image-assets.mjsdocs/website/ のドキュメント入力に ICNS、JPEG XL、HEIF/HEIC、AVIF が含まれないこと(名前を変えたファイルもシグネチャで検出)
scripts/ruleset-baseline.shコミット済みの Ruleset ベースラインが、ドリフト検査の比較対象フィールド(とりわけ bypass_actors)をすべて保持し、正規のエクスポート形式であること

各ガードは不一致だけでなく、対象を 1件も検出できなかった場合にも失敗する。手順の改名などで検査対象が黙って空になったとき、成功を報告してしまう統制は、統制が無いより悪い。

最後の行は、この規則を破っていたガードに対して規則自体を適用したものである。Rulesets API は repository Administration 権限のない呼び出し元に対して bypass_actors を省略するため、ドリフト検査は見えていないフィールドを比較していた。ゼロ件が失敗ではなく一致として読まれていたことになる。

これらはすべてのプルリクエストで必須チェックとして実行される。

依存関係の更新

Dependabot は月次で3つのレビューレーン(アプリケーション、ドキュメント、インフラ)を実行し、各レーンでオープンなプルリクエストを1件に制限し、新規公開バージョンを提案するまでのクールダウン期間を設けている(既定14日、メジャーは60日)。セキュリティ更新は別途有効であり、これらのスケジュールによる遅延を受けない。

ランタイムの EOL は依存関係のライフサイクルで明示的に追跡し、四半期ごとにレビューし、リリース日ごとに上流の一次情報に対して再確認する。

脆弱性スキャン

すべてのプルリクエストと、週次スケジュールで実行する。

スキャン対象
gitleaksコミットされたシークレット
govulncheckGo の依存関係(到達可能性を考慮)
npm auditscripts/check-npm-audit.mjs 経由)ui/website/ の依存関係
scripts/check-doc-image-assets.mjsmake audit-npm 経由)ドキュメントビルドで脆弱な image-size パーサーに到達しうる画像形式
go-licenses / license-checkerGo・npm 依存関係のライセンス許可リスト
anchore/sbom-actionAPI・UI・website の CycloneDX SBOM

受容した npm アドバイザリ

直ちに解消できないアドバイザリは scripts/npm-audit-exceptions.json に記録する。エントリは抑止ではない。到達可能性の根拠、評価対象とした依存元、そして失効日を伴わなければならない。

例外が失効したとき、アドバイザリのスコープが評価時から変化したとき、そしてアドバイザリ自体が存在しなくなったときにゲートは失敗する。最後のケースが重要なのは、それが「陳腐化した例外が実質何も覆っていない」状態を意味するためである。

現在の image-size 例外には、フェイルクローズする入力ガードを組み合わせている。docs/website/ 配下の追跡済みまたは無視されていない全ファイルを走査し、パーサーが認識するシグネチャも確認して、危険な形式を拡張子変更も含めて拒否する。いずれかのルートやファイル一覧が消えた場合も失敗するため、ビルド入力の改名や壊れた走査によって受容リスクが黙って素通りすることはない。ガードは make audit-npm と Security ワークフローで npm audit より先に実行される。

ビルドとリリース

公開は注釈付き Git タグによってのみ起動され、次の条件を満たさない限りワークフローは処理を進めない。

  1. タグが厳密な SemVer vMAJOR.MINOR.PATCH であること。プレリリース識別子は拒否する。本プロジェクトが公開するチャネルは1本である。
  2. タグが軽量タグではなく注釈付きであること。
  3. タグ付けされたコミットが main の祖先であること。
  4. CHANGELOG.md に当該バージョンの節が存在すること。

ブランチの先頭から公開されるものは無い。latest やローリングタグも存在しない。

リリースを拒否しうるチェックは、すべてイメージが push されるに実行される。これは意図的である。リリースタグでイメージが公開された後は、後段の失敗によってそれを取り消すことができない。

各リリースが生成するもの

成果物目的
マルチアーキテクチャイメージ(linux/amd64linux/arm64ソフトウェア本体
GitHub ビルド来歴証明(レジストリへ push)イメージダイジェストを、それをビルドしたワークフロー・リポジトリ・コミットに結び付ける
イメージの CycloneDX SBOM自組織でのスキャン用のコンポーネント目録
release-manifest.jsonタグ、コミット、イメージ、ダイジェスト、SBOM ハッシュ、来歴 URL、および当該リリースが何を主張しないかを記す governance ブロック
SHA256SUMS添付ファイルの完全性

消費側の検証手順はアップグレードにある。デプロイ前に検証すること。成果物は誰かが確認して初めて意味を持つ。

イメージの再現

イメージは非公開の入力なしにリポジトリからビルドできる。

docker build -f api/Dockerfile \
--build-arg VERSION=vX.Y.Z \
--build-arg REVISION="$(git rev-parse HEAD)" \
-t merlon:verify .

いずれのビルダーステージもエミュレーション下では動作しない。Go バイナリはクロスコンパイルされ、UI バンドルはアーキテクチャ非依存であるため、linux/arm64 イメージは linux/amd64 と同一のコードパスで生成され、別のエミュレートされたビルドによるものではない。

既知のギャップ

省略せずここに記載する。レビュー担当者はいずれにせよ見つけるためである。

ギャップ状況
イメージは来歴証明を持つが cosign/sigstore による署名は無い来歴証明は出所を担保する。分離署名は現時点で存在しない
SBOM は生成するが CI でスキャンしていない利用者がスキャンできるよう公開している。消費するゲートはまだ無い
静的アプリケーションセキュリティテスト(CodeQL 等)が無い現時点で未設定
コンテナイメージの CVE スキャン結果を公開していない公開 SBOM またはイメージを自組織でスキャンすること
アクティブなメンテナが1名MAINTAINERS.md に明記し、全リリースの release-manifest.json とイメージラベルで開示している。マージには Governance Required で強制するセルフレビュー証跡を要する(ADR-0016)

保留中ではなく意図的なものについては受容リスクを参照。