フロントエンドでもTrivyを使って脆弱性対策したい！

# 伝えたいこと
- フロントエンドもコンテナ経由でアクセスさせよう
- コンテナなら[Trivy](https://github.com/knqyf263/trivy)を使ってヤバそうな脆弱性を知ることができる
- TrivyならCIに組み込むことも簡単
- multi-stage buildの場合は工夫が必要
- いまの状況でできる範囲の脆弱性対策からはじめよう

# 今回の記事でできること

![Trivy_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14275/4713ba9c-bf38-0c1c-98de-f69e96d37868.png)

CI上で、本番用のフロントエンドのコンテナをmulti-satge buildでつくり、
Trivyを利用して脆弱性を検知するところまでを目指します。

最後に、Trivyを利用したレベル別の運用イメージも提案します。


# 導入

週末、[Trivy](https://github.com/knqyf263/trivy)というコンテナ向けの脆弱性検知ツールが正式リリースされました。公開が5日目の5/21 12:00時点で 900star以上獲得しています。

Trivyの詳細は原作者である @knqyf263 さんの｢[CIで使えるコンテナの脆弱性スキャナ](https://qiita.com/knqyf263/items/dc179f9223fc31b5a51c)｣という記事を参照ください。


今回は、フロントエンドをメインに、今回は以下の内容を紹介します。

- コンテナを使ってフロントエンドの本番環境を構築する
- Trivyを利用して、脆弱性が含まれていないかチェックする
- Trivyを通じた脆弱性の運用方法を学ぶ

僕も、普段はフロントエンドも担当するエンジニアで、セキュリティに対して体系だって学んだことはありません。ただ、Trivyのコミッタでもあり、脆弱性の運用も少し知っているので、フロントエンドでもDevSecOpsを定着させるきっかけになるといいなと考えています。

# ソースコード
今回使うコードは、すべて以下のレポジトリにあります。

https://github.com/tomoyamachi/trivy-with-react
![reactrivy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14275/a0152e78-3c0e-7a53-94ce-733dcd656f43.png)

この記事では触れませんが、開発環境でもコンテナ経由でホットリロードするような仕組みになっています。

# 実践 : プロジェクト作成~Trivyの導入

ここからはプロジェクトの作成と、CIにTrivyを組み込むまでの手順を書きます

## 1. プロジェクトの作成

今回は[Create React App](https://facebook.github.io/create-react-app/)を利用して、Reactのプロジェクトをまず作成します。
そして、デフォルトではパッケージ管理ツールにyarnが利用されているので、今回はプロジェクトのパッケージ管理をnpmで実行します。

```
$ npx create-react-app trivy-with-react
$ cd trivy-with-react
$ rm -fr node_modules yarn.lock
$ npm install
```

## 2. 本番用コンテナの作成

1. 本番用のコードを生成
2. nginxコンテナに生成したコードを移し、nginxを起動

という手順をとります。

### 通常であれば1ファイル
そして通常2つのステップは、multi-stage buildを利用して、1つのDockerfileでコンテナを作成します。

```dockerfile:Dockerfile
# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

```
from [Dockerizing a React App
](https://mherman.org/blog/dockerizing-a-react-app/)

### 1ファイルでmulti-stage buildの問題点
multi-stage buildは1つのDockerfileで最終成果物のimageだけを出力してくれるので、通常はベストプラクティスです。
その場合、以下のように、builder側のイメージがIMAGE IDでしか特定できない形で作成されているのがわかります。

```bash

$ docker images
REPOSITORY         TAG    IMAGE ID ...
trivy-with-react   test   96c4fdfac287 # nginx側
<none>             <none> 41fd97016d25 # builder側
```

しかし、今回、脆弱性はbuilder側のコンテナにもあることに気がついた方もいるでしょう。package-lock.json もそうですし、一般的ではないですが、Static Linkでビルドする場合も考えられます。

Trivyは`REPOSITORY:TAG`の形式でコンテナイメージを指定してスキャンするので、multi-stage buildにしてしまうと、builder側の脆弱性が放置されてしまいます。

そのため、builder側と、nginx側のコンテナイメージを独立させて、参照可能にしたいと思います。具体的には、Dockerfileを分けることで、それぞれのイメージにタグを付けます。
また、2回コマンド実行するのも微妙なのでmakefileを作成します。

※ もしも1つのDockerfileでmulti-stage buildをし、builder側のイメージ名を指定することができるのであれば、切実に情報がほしいです!

### Trivy用の構成

コードを見てもらえばわかりますが、Dockerfileをbuilder用(Dockerfile.builder)と、nginx用(Dockerfile.prod)に分けて、makefileは以下のようにしています。

```dockerfile:Dockerfile.builder
FROM node:12.2.0-alpine as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build
```

```dockerfile:Dockerfile.prod
ARG tag
FROM local-builder:${tag} as builder

FROM nginx:1.16.0-alpine
ARG tag
COPY --from=builder /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```

```makefile:makefile

.PHONY: build all

TAG := $(shell git rev-parse HEAD)

all: build
build:
	docker build . -f Dockerfile.builder -t local-builder:$(TAG)
	docker build . --build-arg tag=$(TAG) -f Dockerfile.prod -t built:$(TAG)
```

このため、以下のようにデフォルトではコミットハッシュごとにコンテナを作成し、変数TAGが指定されていればそれを利用します。

```bash
$ make build  # 最新のコミットハッシュをコンテナのタグに利用
$ make build TAG=hoge # hogeをコンテナのタグに指定
```

実際のレポジトリでは、SPA用にnginx.confを置いたりしているので、必要な方はご確認ください。

**1つのDockerfileで、同様のことができる方法をご存知の方いたら教えてください...**

### ビルドしたコンテナのテスト

では、さっそくビルドしたコンテナにアクセスしてみましょう。

```
$ make build TAG=test
$ docker run -p 8000:80 built:test
```

以上を実行し、 http://localhost:8000 を開くと、画面が表示されることが確認できました。

<img width="488" alt="localhost.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14275/7ac6ebbc-dc41-f704-58e7-46165173820f.png">

ついでに、nginxでSPA対応できているかを確認するため、 http://localhost:8000/spa/test が開けることを確認します。

<img width="492" alt="localhost_spa" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14275/9b318883-e422-1723-49a2-73f62b6a7daf.png">

## 3. Trivyでスキャン

では、builder側とnginx側をスキャンしましょう。
Trivyはインストールしてある前提ですが、makefileに以下のコードを追加します。

buildと同じようにTAGを指定することで、指定したイメージをスキャンできるようになります。

```bash
ci-scan:
	trivy --exit-code 1 --severity HIGH,CRITICAL  --quiet --auto-refresh local-builder:$(TAG)
	trivy --exit-code 1 --quiet --auto-refresh built:$(TAG)
```
`--exit-code`オプションで脆弱性を検知した場合の終了コードを指定できるので、スキャンはしたいけど、CIは止めたくない人は `--exit-code 0` (デフォルトなので省略可) を利用しましょう。

## 4. CircleCIで実行

CircleCIの場合は以下のようにします。
Trivyの脆弱性データの取得が重いので、脆弱性データなどはキャッシュに保存すると2回目以降のスキャンが高速になります。

```yaml    
version: 2
jobs:
  build:
    docker:
      - image: docker:18.09-git
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          key: vulnerability-db
      - run:
          name: Build image
          command: |
            apk add --update --no-cache make curl
            make build TAG=${CIRCLE_SHA1}
      - run:
          name: Install latest trivy
          command: |
            VERSION=$(
              curl -I https://github.com/knqyf263/trivy/releases/latest | \
              grep -o '/tag/v[0-9]\+.[0-9]\+\.[0-9]\+' | \
              sed -E 's:/tag/v([0-9\.]+):\1:'
            )
            wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz
            tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz
            mv trivy /usr/local/bin
      - run:
          name: Scan local images with trivy
          command: |
            make ci-scan TAG=${CIRCLE_SHA1}
      - save_cache:
          key: vulnerability-db
          paths:
            - $HOME/.cache/trivy
workflows:
  version: 2
  release:
    jobs:
      - build
```

正常終了の場合、以下のようになります。
builder側とnginx側のどちらにも脆弱性がなかったということがわかりました。

![ci-success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14275/a5bb883b-fd05-8e89-1280-daa3c8677f75.png)


### 脆弱性があった場合CIを止めたい

脆弱性が見つかった場合に、CIを止めたい場合は`--exit-code 1`オプションを利用します。このオプションにより、脆弱性が見つかった場合、異常終了(終了コードが1)になります。
また、 `severity`オプションを利用することで、検知したい脆弱性の危険度を指定できます。

```bash
trivy --exit-code 1 --severity HIGH,CRITICAL \
  --quiet --auto-refresh \ 
  local-builder:$(TAG)
```

この状態で脆弱性のあるパッケージを追加して結果を見ると、CIが失敗することが確認できました!

![ci-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14275/74d27b2d-258e-73d3-fb04-af1e192bf834.png)
https://circleci.com/gh/tomoyamachi/trivy-with-react/8


# Trivyを使った運用スタイル

さて、では脆弱性が見つかった場合、どのようにすればよいでしょうか?

こうしたほうがいい、というのはありますが、それぞれの運用方針にそって決めればいいと思います。

以下に、｢とりあえず導入｣｢ちょっと気になる｣｢がんばる｣のコースとそれぞれによさそうな運用方針をまとめました。

| | とりあえず導入 | ちょっと気になる | がんばる |
|---|---|---|---|
|検知時のCI| とめない(--exit-code 0) | とめる(--exit-code 1) | とめる(--exit-code 1) |
|対応方針| 対応せずSlackなどに通知 | バージョンアップして、駄目なら無視する (.trivyignore) | 脆弱性を調べて、必要なら対応 |
|検知タイミング| ビルド時 | ビルド時 | ビルド時&定期実行 |

全く興味なくても、導入するだけ導入したい、という人がいてもいいと思うので、できる範囲ですすめられるといいかなと思います。

以下は、｢とりあえず導入｣以上の興味がある人に対しての方法論になります。


## 1. 脆弱性を調べ、本当にCRITICALかをチェック

まず、その脆弱性が自分のアプリケーションにとって本当に問題になるのかを理解しましょう。
NVDなどで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は [このスライドの｢トリアージ｣以降の章](https://www.slideshare.net/nakatomoorg/vulnerability-response)が役立つかと思います。

## 2. 重要ではない場合は、その脆弱性を無視する

プロジェクトに `.trivyignore` を作成し、無視したい脆弱性ID(以下、`VULNERABILITY ID`)を記載すると、そのIDを無視してくれます。
さきほどの例でいうと、以下のように指定すれば、それ以降同じ`VULNERABILITY ID`は検知されなくなります。

```plain:.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358
```

この対応で先程停止していたワークフローがとおるようになったのがわかります。
https://circleci.com/gh/tomoyamachi/trivy-with-react/10


## 3. 重要な場合は...

重要な場合、バージョンアップをすれば良いと思うのですが、そうではない場合、自分で修正したコードを利用する方法などもあります。

### アプリケーションパッケージの場合
たとえば、npmのパッケージであれば、 [この記事の方法で](https://qiita.com/shinout/items/c6f12d69e68b312d62fa) でforkした自分のレポジトリを入れることが可能です。

ただし、その場合、バージョン情報が以下のようになり、そのパッケージの脆弱性チェックができなくなる点に留意ください。

```
"jquery": {
      "version": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git#438b1a3e8a52d3e4efd8aba45498477038849c97",
      "from": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git"
    },
```

### OSのパッケージの場合

検出されるパッケージバージョン自体は変わらないので、patchfileを当てるコマンドを実行するのと同時に、`.trivyignore` に対象の`VULNERABILITY ID`を追加してください。

# まとめ

今回は、以下のことを書きました。

- フロントエンドをコンテナで運用する
- Trivyで脆弱性対策できる
- 脆弱性のおおまかな運用方法

ところで、 記事を書いているときにnpmを使うのであれば、`npm audit`で簡単に脆弱性を取得できることを知りました… が、環境側の脆弱性もチェックできるので、コンテナ化しているのであれば、Trivyを使ってみてください。 Trivyはyarnも対応しているので、yarnの人にはTrivyがおすすめです。
S3などにホストしている場合は、`npm audit`だけでよさそうです。



なお、コンテナになっていない場合で実行環境をチェックしたい場合、[Vuls](https://github.com/future-architect/vuls)という素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、[あなたのサーバは本当に安全ですか？今もっともイケてる脆弱性検知ツールVulsを使ってみた](https://qiita.com/sadayuki-matsuno/items/0bb8bb1689425bb9a21c) を参考にお試しください。



# 謝辞

記事を書く際に、 @knqyf263 さん、 @sadayuki-matsuno さん、 @codehex さんにご意見をいただきました。ありがとうございました!!

以上です。