SecretlintはAPIトークンや秘密鍵のようなリポジトリにコミットしてはいけないデータを含んだファイルがないかをチェックするツールです。

Secretlintの出力例: AWSのSecret Access Keyが見つかったケース

Secretlintが見つけられるCredentials(秘匿情報)はプラグインで拡張できるようになっていて、npmAWSGCPSlackSSH秘密鍵ベーシック認証などの検知に対応しています。

Gitのpre-commit hookやCIサービス上でSecretlintを使ってファイルの中身をチェックすることで、 リポジトリにうっかりCredentialsをコミットしてしまうことを防止する目的のLintツールです。

Credentials(秘匿情報)のチェックに特化したESLinttextlintのようなLintツールです。

まずチェックしてみよう

SecretlintはDockerかNode.jsが入っている環境なら次のコマンドで、現在ディレクトリ以下を簡易チェックできます。

Docker:

docker run -v `pwd`:`pwd` -w `pwd` --rm -it secretlint/secretlint secretlint "**/*"

Node.js:

npx @secretlint/quick-start "**/*"

"**/*" は現在のディレクトリ以下をすべてを対象にするglobパターンです。 実行した結果特にエラーが表示されていなければ、そのプロジェクトにはSecretlintが検知できるCredentialsは入っていないはずです。

もしなんらかのエラーが表示された場合は、先ほど紹介した検知できるCredentialsの何かが平文で書かれたものがあるはずです。

次の画像はAWSのSecret Access Keyが見つかったケースの表示です。

Secretlintの出力例: AWSのSecret Access Keyが見つかったケース

一度きりのチェックだと継続的なセキュリティは担保できないので、CIやGitコミットフックなどでプロジェクトに導入する方法を紹介しています。 また、個人環境のグローバルなGitコミットフックに常にSecretlintのチェックを入れることもできます。

Secretlintをプロジェクトに導入する

Secretlintを作った理由の一つでもありますが、Secretlintはプロジェクトに組み込みやすさを意識して作成しています。

Docker

先ほども紹介したようにDockerで使う場合はsecretlint/secretlintのイメージを利用できます。

docker run -v `pwd`:`pwd` -w `pwd` --rm -it secretlint/secretlint secretlint "**/*"

それぞれのタグ付きでpublishしているので、バージョンを固定したい場合は secretlint/secretlint:v1.0.0 のようにバージョン指定して利用してください。

Docker版は、動的なLinkの仕組みが思いつかなかったため、ルールを追加する場合は新しいImageを作成する形になる気がします。

ルールのオプション変更などの設定自体は次に紹介する方法で設定できます。

Node.js

SecretlintはNode.jsで書かれているため、Node.jsのCLI/ライブラリとしても利用可能です。 Secretlintはビルトインルールを一つも持っていませんが、@secretlint/secretlint-rule-preset-recommendという推奨ルールセットを提供しています。

Docker版と先ほどの@secretlint/quick-startはこの推奨ルールセットを組み込んだ形になっています。

より柔軟にNode.jsのプロジェクトに導入する場合は、次のステップでSecretlintをインストールしてください。

npmを使ってsecretlintとpresetルールをインストールする。

npm install secretlint @secretlint/secretlint-rule-preset-recommend --save-dev

インストール後に、次のコマンドで.secretlintrc.jsonという設定ファイルを作成できます。

npx secretlint --init

これで導入できたので、あとはsecretlintコマンドを実行するだけです。

npx secretlint "**/*"

secretlintコマンドの細かい利用方法はsecretlint --helpを参照してください。 結果の出力形式を変更したり、--secretlintrcignore .gitignore.gitignoreに書かれたファイルを対象外にするといった設定もできます。 またsecretlintには国際化の仕組みが入っているのでsecretlint --locale jaで一部日本語の結果を得られたりします。

設定方法

Node.jsで導入した場合はsecretlint --initコマンドによって次のような.secretlintrc.jsonという設定ファイルが作成されています。

.secretlintrc.json:

{
  "rules": [
    {
      "id": "@secretlint/secretlint-rule-preset-recommend"
    }
  ]
}

このファイルはSecretlintで使いたいルール/プリセットを追加していきます。 (Docker版を使ってる場合も、カレントディレクトリに.secretlintrc.jsonがあれば参照されます)

idは導入するnpmのパッケージ名で、このidを追加するとSecretlintはそのルールパッケージを使ってチェックします。 ルールはnpmでインストールするので、node_modules以下にそのルールがインストールされている必要があります。 (Docker版や@secretlint/quick-start@secretlint/secretlint-rule-preset-recommendを同梱している)

{
  "rules": [
    {
      "id": "@secretlint/secretlint-rule-preset-recommend"
    },
    {
      "id": "@secretlint/secretlint-rule-example"
    }
  ]
}

id以外にも共通の設定プロパティがいくつかあります。

  • options: ルールのオプションです。ルールごとにオプションの設定をできる。ルールのドキュメントを参照
  • disabled: ルールを無効化したいときはtrueを指定します。
  • allowMessageIds: 各ルールのエラーメッセージにはmessageIdという識別子が用意されています。
    • 特定のルールの特定のエラーメッセージだけを無視したい場合はallowMessageIdsで指定すると無視できます

options の例

@secretlint/secretlint-rule-preset-recommendの中の@secretlint/secretlint-rule-awsルールにはallowsというオプションがあります。これはマッチしたものをエラーにしないオプションです。

次のように指定すれば、wJalrXUtnFEMI/K7MDENG/bPxRfiCYSECRETSKEYというトークンがファイル内に書かれていてもエラーにはなりません。

{
  "rules": [
    {
        "id": "@secretlint/secretlint-rule-preset-recommend",
        "rules": [
            {
                "id": "@secretlint/secretlint-rule-aws",
                "options": {
                    "allows": ["wJalrXUtnFEMI/K7MDENG/bPxRfiCYSECRETSKEY"]
                }
            }
        ]
    }
  ]
}

allowMessageIdsの例

@secretlint/secretlint-rule-awsルールはAWSのSecretAccessKeyを検知してエラーを報告してくれます。

AWSSecretAccessKey

このエラーだけを常に(どのファイルでも)無視したい場合は、AWSSecretAccessKeyallowMessageIdsに指定することで実現できます。

{
    "rules": [
        {
            "id": "@secretlint/secretlint-rule-preset-recommend",
            "rules": [
                {
                     "id": "@secretlint/secretlint-rule-aws",
                     "allowMessageIds": ["AWSSecretAccessKey"]
                }
            ]
        }
    ]
}

理想的にはルールにfalse-positiveのような誤検知をなくせるのが良いのですが、 それが誤検知かはプロジェクトによるので、細かい制御をできる仕組みがSecretlintには含まれています。

Lint対象から外す

.secretlintignoreというファイルを作成すると、そのファイルに書かれたパターンのファイルはLintの対象外とします。 .secretlintignoreの書式は.gitignoreと同じです。

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

次のように指定すれば.gitignore.secretlintignoreとして使うこともできます。

secretlint --secretlintignore .gitignore "**/*"

コミット前にSecretlintでチェックする

Gitでコミットする前にSecretlintでコミットするファイルをチェックできます。

Gitで一度コミットするとgit filter-branchで歴史を改竄しない限り、そのCredentialsはコミット履歴に残ります。 また、GitHubに一度でもpushしたりPull Requestを作ったりすると、そのデータは問い合わせしない限り完全には消せません。

そのため、コミット前に検知して、Credentialsをコミットしようとしたらコミット失敗させるのが単純で効果的です。

プロジェクトにpre-commit hookを導入する

Node.jsのプロジェクトならHusky + lint-stagedでpre-commit hookを実現するのが簡単です。

Husky + lint-stagedをインストールします。

npm install husky lint-staged --save-dev

package.jsonを編集して、pre-commit時にlint-stagedですべてのファイルをsecretlintでチェックするようにします。

{
  // ...
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*": [
      "secretlint"
    ]
  }
}

これで、コミットする前にsecretlintでチェックする仕組みがプロジェクトに導入できます。 チーム開発している際には、プロジェクトにこのようなチェックを導入するのが良いと思います。

他にもpre-commitを使った方法やBash Scriptを使った方法などもあります。 それぞれ次のページで紹介しています。

個人環境にpre-commit hookを導入する

プロジェクトではなく、個人の環境ではSecretlintのチェックを必ずコミット前に通したいというケースです。

Git 2.9+からcore.hooksPathという設定が追加され、いわゆるGlobalなGit hooksが実現できるようになりました。

Secretlintをcore.hooksPathに設定する例は次のリポジトリにあります。

これはDocker版の例ですが、次のようにセットアップするだけで、コミット前には常にSecretlintでのチェックが入ります。 かつ、そのリポジトリに別のcommit hookが設定されている場合はそっちも実行されます。

# clone this repository
git clone https://github.com/secretlint/git-hooks git-hooks
cd git-hooks
# integrate secretlint to git hook globally
git config --global core.hooksPath $(pwd)/hooks

グローバルなpre-commit hookを導入することで、リポジトリにAWSのキーを間違ってコミットするのを防止するなどの効果があります。

自分が利用しているcore.hooksPathの設定は次のリポジトリにあります。

特定のファイルパス(プロジェクト)では無効化したりとか細かいオプションが入っています。

CI

今どきのCIはだいたいDockerをサポートしているので、SecretlintのDocker Imageを使えば簡単に導入できます。

GitHub ActionsでNode.jsを使う場合は次のような形で導入できます。

.github/workflows/secretlint.ymlを作成し、次のように設定する。

name: Secretlint
on: [push, pull_request]
env:
  CI: true
jobs:
  test:
    name: "Secretlint"
    runs-on: ubuntu-18.04
    strategy:
      matrix:
        node_version: [12]
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: setup Node $
        uses: actions/setup-node@v1
        with:
          node_version: $
      - name: Install
        run: npm install
      - name: Lint with Secretlint
        run: npx secretlint "**/*"

これだけで、CIでのチェックとチェック結果をコメントが導入できます。

image

ルールを作る

Secretlintは自作のルールを簡単に追加できます。 コアのルールセットも同じ方法で実装されています。

基本的にはTypeScriptやJavaScriptでかけるようになっています。 次の@secretlint/secretlint-rule-examplesecretという文字列を見つけるルールの実装例です。

import { SecretLintRuleCreator, SecretLintSourceCode } from "@secretlint/types";

// MessageIds
export const messages = {
    EXAMPLE_MESSAGE: {
        en: "found secret: ",
        ja: "secret:  がみつかりました"
    }
};

export const creator: SecretLintRuleCreator = {
    messages,
    meta: {
        // rule.meta.id should be same with package.json name
        id: "@secretlint/secretlint-rule-example",
        recommended: false,
        // type
        type: "scanner",
        // support content types: "text" or "binary" or "all"
        supportedContentTypes: ["text"],
        // Documentation Base URL for the package
        docs: {
            url:
                "https://github.com/secretlint/secretlint/blob/master/packages/%40secretlint/secretlint-rule-example/README.md"
        }
    },
    // Rule Logic
    create(context) {
        // Create Traslate instance
        const t = context.createTranslator(messages);
        return {
            // source has `content`, `filePath` etc...
            file(source: SecretLintSourceCode) {
                const pattern = /secret/gi;
                let match;
                while ((match = pattern.exec(source.content)) !== null) {
                    const index = match.index || 0;
                    const matchString = match[0] || "";
                    const range = [index, index + matchString.length];
                    // Report found "secret"
                    context.report({
                        // Replace "found secret: " with `ID` data
                        message: t("EXAMPLE_MESSAGE", {
                            ID: matchString
                        }),
                        range
                    });
                }
            }
        };
    }
};
// export it as default
export default creator;

詳しい作り方は次のドキュメントを参照してください。

なぜSecretlintを作ったのか

Secretlintを作った理由は、プロジェクトに導入しやすいCredentialチェックツールがなかったためです。

git-secretsを導入しようとしたのですが、git-secretsは設定を.git/configで管理するため、設定を管理しにくいという問題がありました。 インストールする際にpre-commit hooksが設定でき、どのようなチェックをするかを設定ファイル(Gitで)管理したいというのがあります。 git-secretsだと、設定を変更するたびに.git/configを変更するコマンドを叩く必要があります。 (git-secretsはグローバルにインストールして使う想定なのかなと思います)

仕組み的にもイメージと近いdetect-secretsというツールもあったのですが、 Opt-inではなくOpt-outで設定させる点やentropyという概念でチェックがあって、検知するかどうかをコントールするのが難しいという問題がありました。

他にも色々なスキャンツールはあるのですが、多くのツールはできるだけ多くのことをチェックする実装になっていることが多く、 過剰な検知(false-positive)が発生しやすい形になっていて、継続的にチェックするのには向いてない感じがしました。 (たとえば、passwordという文字列だけでアラートを出すなど、継続的に出すとただのノイズになってしまう)

これはtextlintを作っていてもそうだったのですが、Lintで99%検知できて1%は誤検知だと使うのは難しい感じです。 誤検知があるとその時点で使うことを諦めてしまうので、できるだけfalse-positiveをなくす仕組みが必要でした。 また、実際に誤検知があった際に簡単に抑制できる仕組みも必要です。

そのためSecretlintでは次のような単位でエラーを抑制できる手段も提供しています。

  • .secretlintignore: ファイル単位
  • disabledオプション: ルール単位
  • allowMessageIdsオプション: ルール内のメッセージ単位

また、内部的にはtextlintのFilter Ruleと同じ仕組みも実装しているため、 ルールによるエラーの抑制も実装できるようになる予定です。 (たとえば、コメントによるコードレベルの抑制なども実装したいです。)

その他にも、Secretlintではルールのエラーを意味ある形で説明したいという気持ちがあったため、ドキュメント周りにも仕組みを色々入れています。 (これはtextlintでまだできてない部分)

たとえば、iTerm など Hyperlinksに対応したターミナルなら、エラー表示に出てくるMessageIdをCmd + Clickすると、直接ドキュメントを開けるようになっています。

Terminal Link

まだ、ウェブサイトもないので、ドキュメント周りはちゃんと理解しやすい説明をする形にしていきたいと思います。

さいごに

ひとまず安定して動くようになったのでSecretlint 1.0をリリースしました。

履歴を全部チェックするHistory Scannerなどまだ実装したい機能は色々あります。

SecretlintへのContributionsはいつでも歓迎しています。 興味ある人は次のラベルを見てみるといいかもしれません。

Icon/logoも適当に作ったやつなので、これもどうにかしたいです。

Sponsor