Secretlint v13.0.0リリース: .gitignore済みをデフォルトで無視、Tailscale/Stripe/Cloudflareの検出に対応
ソースコードや設定ファイルに含まれるAPIトークンやパスワードなどの機密情報を見つけるSecretlintのv13.0.0をリリースしました。
このバージョンの主な変更点は次の3つです。
- ファイル探索時に
.gitignoreをデフォルトで尊重するように変更(Breaking Change) - グロブメタ文字を含むパスが実在する場合はリテラルとして扱うように変更
- Tailscale/Stripeの検出ルールを新規追加、CloudflareをcanaryからrecommendへPromote
Breaking Change: .gitignoreをデフォルトで尊重
v13.0.0では、ファイル探索時に.gitignoreの内容をデフォルトで尊重するようになりました。
ripgrepと同じ挙動で、ネストされた.gitignoreファイルもサブディレクトリへカスケードして適用されます。深い階層のネガティブルール(!)で上位の判定を上書きできます。
.gitignoreに一致したファイルはスキャン対象から除外されます。
そのため、これまでdist/や生成物などを意図的にスキャンしていたプロジェクトでは、出力されるファイル数が減ります。
.secretlintignoreはこれまで通り併用できます。
v12までの挙動に戻したい場合は、--no-gitignoreオプションを指定します。
secretlint --no-gitignore "**/*"
.gitignoreに一致しているはずのファイルが結果に含まれている場合は、Issueで報告してください。
実装: @secretlint/walker
v13.0.0では、ファイル探索の実装をglobbyから、新しく追加した@secretlint/walkerへ置き換えました。
設計はplanドキュメントとしてリポジトリにコミットされています。
Rustにはripgrepのignore crateがあり、oxcなどはこれを使うことで.gitignoreのカスケードを安価に再利用できます。
JavaScript側にも、ネストされた.gitignoreに対応するwalkerが全く無いわけではありません(tiny-readdir-glob-gitignore、ignore-walkなど)。
ただし、ripgrepのignore crateほど枯れた実績や仕様の網羅度を持つものは見当たりませんでした。
また、後述する「グロブメタ文字を含むパスが実在する場合はリテラル扱いにする」のように、走査・マッチ側に独自の制御を入れたい要件もあります。
依存ライブラリもnode-ignoreとpicomatchまで分解すればfs.readdirの上に薄く書ける範囲だったため、Secretlint側でwalkerを実装することにしました。
@secretlint/walkerは、ネストされた.gitignoreのカスケードに対応するPromiseベースのファイルシステムwalkerです。
依存ライブラリはignore(node-ignore)とpicomatchの2つだけで、含めるパターン(include)と除外するパターン(ignore)でセマンティクスを分離しています。
- 含めるパターン(include):
picomatchを使ったグロブマッチ。**/*.{ts,js}のようなブレース展開やドットファイルのマッチに対応する - 除外するパターン(ignore):
node-ignoreを使った.gitignoreセマンティクスのマッチ。.gitignoreの挙動に合わせるため、ブレース展開は意図的に対応しない
Node.js本体にもfs.globやpath.matchesGlobが用意されていますが、これらにはドットファイルをマッチに含めるdotオプションが存在しません。
ドットファイル(.envなど)のスキャンが要件として外せないため、include側はpicomatchにしています。
走査自体はfs.readdir(dir, { withFileTypes: true })をベースにしたシンプルな再帰で、各ディレクトリのエントリをPromise.allで並列処理します。
ディレクトリ単位でignoreを判定し、無視対象に該当したディレクトリはサブツリーごと走査をやめます。
readdir自体を呼ばないため、大きなnode_modules配下などをまるごとスキップできます。
.gitignoreのカスケードはIgnoreStackという構造で扱います。
親ディレクトリのignoreインスタンスに対して、現在のディレクトリの.gitignoreを読み込んでextendIgnore()で重ねるという形でレイヤーを積みます。
.gitignoreがそのディレクトリに存在しない場合は親のインスタンスをそのまま返すため、不要なallocationが起きません。
深い階層のネガティブルール(!pattern)が浅い階層のルールを上書きする挙動も、このスタック上で自然に表現されます。
ネストされた.gitignore内のパターンはそのファイルの置かれたディレクトリにアンカーされます。
そのため、packages/foo/.gitignoreのsrc/**/*.tsはリポジトリルートではなくpackages/foo/src/配下にだけ適用されます。
クロスプラットフォーム対応として、node-ignoreへ渡すパスはWindowsでも/区切りに正規化(toPosix())した上でマッチングします。
返すパスはOSネイティブの区切り文字に戻します。
また、ENOENTやEACCESは探索全体を止めずにスキップする方針で、ファイル削除・パーミッションエラーに対して頑健になっています。
入力パターンは静的なプレフィックス(walk root)と動的なサフィックス(マッチパターン)に分割し、同じrootを持つパターンはグループ化して1度のwalkで処理します。
グロブメタ文字を含む入力でも、それが実際のファイル/ディレクトリとして存在する場合はnoGlob相当の扱いにフォールバックさせるロジックもwalker側に入っています。
主なAPIはwalk(options)で、パッケージ単独でも利用できます。
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
cwd |
string |
必須 | 探索の起点ディレクトリ |
patterns |
string[] |
undefined |
インクルードのグロブパターン |
ignoreFiles |
string[] |
[] |
カスケード対象のignoreファイル名(.gitignoreなど) |
extraIgnorePatterns |
string[] |
[] |
コードから渡す追加のignoreパターン |
noGlob |
boolean |
false |
patternsをリテラルとして扱う |
followSymlinks |
boolean |
true |
シンボリックリンクを追従するか |
CLI側のsecretlintでは、--no-gitignoreが指定された場合はignoreFilesから.gitignoreを外し、それ以外の場合はカスケード有効でwalkします。
.secretlintignoreは別経路のextraIgnorePatterns相当で従来通り適用されるため、.gitignoreとの共存に影響はありません。
パフォーマンス
walker単体の実行時間で見ると、globbyからの置き換えによるパフォーマンスの大きな劣化はありません。
一方で、.gitignoreのカスケードは「親のルールに子のルールを正しく重ねる」ことを満たさないと挙動が壊れる部分なので、ここはripgrepの実装を参考にしながら書いています。
実際のSecretlintの実行時間で見ると、.gitignoreを尊重することでnode_modules/やdist/などをそもそもスキャンしなくなるため、Lint対象のファイル数が減ります。
スキャン+ルール評価のコストはファイル数に比例して効いてくるため、walker自体のコストよりもLint対象が減ることによる削減のほうが支配的になります。
結果として、v12より速く終わるケースが多くなる想定です。
グロブメタ文字を含むパスが実在する場合はリテラル扱いに
Secretlintはコマンドライン引数をデフォルトでグロブパターンとして解釈します。
v13.0.0では、グロブメタ文字(()、[]、{}、?)を含むパスが実際にファイル/ディレクトリとして存在する場合は、リテラルとして扱うように変更しました。
これは、SvelteKitやNext.jsのRoute Groupなど、ディレクトリ名に()や[]を含むプロジェクトで特に有効です。
| パターン | ディスク上 | v12のデフォルト | v13のデフォルト |
|---|---|---|---|
src/(group)/page.tsx |
存在する | グロブとして解釈、マッチしない | リテラルとしてマッチ |
src/(missing)/page.tsx |
存在しない | グロブとして解釈 | グロブとして解釈 |
src/[a-z]ormal.tsx |
normal.tsxが存在 |
グロブ経由でマッチ | グロブ経由でマッチ |
入力ごとに1度だけstatを実行し、存在すればリテラル、存在しなければ従来通りグロブとして扱います。
従来通り常にリテラルとして扱いたい場合は、--no-globオプションを指定します。
新しく追加された検出ルール
@secretlint/secretlint-rule-preset-recommendに、次の3つのルールが追加されました。
- Tailscale - Tailscale APIキー(新規追加)
- Stripe - Stripe APIキー(新規追加)
- Cloudflare - Cloudflare APIトークン(canaryから昇格)
関連PRは次のとおりです。
- Add Tailscale API key detection rule by azu · Pull Request #1536
- feat(secretlint-rule-stripe): add Stripe API key detection rule by azu · Pull Request #1537
- feat(secretlint-rule-preset-recommend): promote cloudflare, stripe, tailscale from canary by azu · Pull Request #1538
@secretlint/secretlint-rule-preset-recommendを使っている場合は、v13.0.0にアップデートすると自動的にこれらのルールが有効になります。
まとめ
Secretlint v13.0.0では、ファイル探索が.gitignoreをデフォルトで尊重するようになりました。
そのため、dist/などをスキャンしていたプロジェクトでは、--no-gitignoreへの切り替えや.secretlintignoreの見直しが必要になります。
これに加えて、グロブメタ文字を含む実在パスをリテラルとして扱うよう調整し、Route Groupなどのディレクトリ構成でもオプションなしに動作します。 検出ルールにはTailscale/Stripe/Cloudflareを追加しています。
フィードバックがあればGitHubのIssueでお知らせください。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。