20190521のReactに関する記事は10件です。

Reactの最速チュートリアル

前文

まともなReactのセットアップがなかったので書きます。

Macの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Re:0から始めるReactのチュートリアル

前文

まともなReactのセットアップや説明がなかったので書きます。
周り見てた感じいろんなQiitaの記事を片っ端から試して凄まじい構成になっているものをよく見るので、環境構築から基礎まで書けたらよいと思います。ちょいちょい更新予定

環境構築はMacの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

src/ にあるコードの解説

ここは脳死でそういうもんなんやと考えてもいい
大事なのはせいぜいAppがpublic/index.htmlのrootにRenderされるという情報
後はのちのち考えればよい

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

App.js
index.jsのrenderから呼ばれたコンポーネントはこちら
これはFunctional Componentと言ってちょっと新しい書き方なのでややこしい
要はfunction Appがコンポーネントのように振る舞う
returnの中身が描画して欲しいデータということ

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

しかしこれはわかりずらいので従来のコンポーネントに戻したコードがこちら
割とこっちの方が馴染みのあるコードなはず 
ネットに転がってるチュートリアルもこっちのほうがまだ多い気がする

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
  render() {
    return(
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
           Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    )
  }
}

export default App

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

完全に心が折れた人向けのReactチュートリアル

前文

まともなReactのセットアップや説明がなかったので書きます。
周り見てた感じいろんなQiitaの記事を片っ端から試して凄まじい構成になっているものをよく見るので、環境構築から基礎まで書けたらよいと思います。ちょいちょい更新予定

環境構築はMacの場合だけ Windowsもnodeのインストール以外はだいたい同じなはず

そもそもReactって

React(リアクト)は、Facebookとコミュニティによって開発されているユーザインタフェースを構築するためのJavaScriptライブラリである[3]。React.jsまたはReactJSの名称でも知られている。wikipediaより引用

要はフロントを作るためのライブラリです。htmlとjsとcssの凝ったバージョンだと思えばよし
npmとかを使うから、Node.jsと混同したりサーバーサイドも関係あるんちゃう?って考える人も多いですが、関係ないです。ただのフロントエンドを構築するためのライブラリです。

環境構築

nodeの環境構築

$ brew install node

$ npm install -g n

$ sudo n stable

Reactの雛形作成

$ npm install -g create-react-app

$ npx create-react-app "プロジェクト名"

これでReactの雛形のプロジェクトのフォルダが生成されます。
プロジェクト名は小文字英数字が望ましい というか大文字ではプロジェクト作れない

とりまブラウザで確認

$ cd "プロジェクト名"

$ npm start

後は勝手にビルドされてブラウザが開くはず

ディレクトリ構造の解説

これを解説しないとこの辺のファイルを妙に弄ってプロジェクトを破損させたりする輩がいるので解説

  • node_modules/
    • npm install ... とかで入るライブラリ等の実体がある所
    • いじるな
  • public/
    • index.htmlとかfaviconとかを入れておく場所
  • src/
    • Reactのソースコードを書く場所
  • build/
    • index.htmlとかfaviconとかが入る場所
  • package.json
    • npm等で操作したデータが入っている ここに依存パッケージとかビルドするスクリプトとかが入っている
    • 消すな
  • yarn.lock
    • パッケージ管理にyarnを使っている場合関連パッケージが記載されている 
    • 消すな
  • README.md
    • 読め

ReactというかWebpackの仕組み

正確な説明はしないけどイメージ程度に

Reactの独自の言語であるJSXをコンパイルして、普通のブラウザで見られるhtml,css,javascriptの形式にする。
これがnpm run buildのコマンドのやっていること

npm startはnpm run buildをやってくれ、ブラウザに対してリンクを開いてくれる。
この状態でソースコードを編集すると自動的にビルドしなおしてくれる

Webpackはいろいろなライブラリを導入したり、ReactやVueのソースコードをひとまとめにしてくれるやつのこと
基本的にReactはこのWebpackとかを通して、まとめてビルドしない動かない

詳しい人が見たら怒りそうな解説であるが、イメージが大事...

src/ にあるコードの解説

ここは脳死でそういうもんなんやと考えてもいい
大事なのはせいぜいAppがpublic/index.htmlのrootにRenderされるという情報
後はのちのち考えればよい

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

index.jsのrenderから呼ばれたコンポーネントはこちら
これはFunctional Componentと言ってちょっと新しい書き方なのでややこしい
要はfunction Appがコンポーネントのように振る舞う
returnの中身が描画して欲しいデータということ

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

しかしこれはわかりずらいので従来のコンポーネントに戻したコードが以下のコード
割とこっちの方が馴染みのあるコードなはず 
ネットに転がってるチュートリアルもこっちのほうがまだ多い気がする
もしこっちのほうがいいという人は、コピペすればよい

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
  render() {
    return(
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
           Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    )
  }
}

export default App

チュートリアル

読め
https://reactjs.org/tutorial/tutorial.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Failed to execute 'createElement' on 'Document': The tag name provided ('static/media/icon.ac7a4370.svg') is not a valid name.

react-svg-loaderを使い表題のエラーが出た場合はwebpack.configに以下の設定を追加してください

config.resolve.extensions.push('.svg')
config.module.rules = config.module.rules.map(data => {
  if (/svg\|/.test( String(data.test)))
    data.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/
  return data
})
config.module.rules.push({
  test: /\.svg$/,
  use: [
    { loader: require.resolve('babel-loader') },
    { loader: require.resolve('react-svg-loader') }
  ]
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reduxアーキテクチャのサイクルをコードリーディングで一通り確認する

この文書では、React JS Tutorialsのソースコードをコードリーディングすることで、Reduxアーキテクチャのサイクルを一通り確認します。

fluximg.jpeg

事前知識

  • reactについては公式チュートリアルは完了しているものとします。

準備

node.jsのインストールは完了しているものとします。

React JS Tutorialsのソースコードをダウンロードしてください(※バージョン違いが生じないようにフォークしてあります)。

ダウンロードしたらプロジェクトのルートディレクトリで、

cd 5-redux-react
npm i
npm run dev

します。

そして、localhost:8080をブラウザで開いてください。

コードリーディング

まずsrc/js/client.jsを見てください。ここがアプリケーションのエントリポイントになります。普通のReactアプリケーションと同様に、ReactDOM.render()を呼んでいます。

import React from "react"
import ReactDOM from "react-dom"
import { Provider } from "react-redux"

import Layout from "./components/Layout"
import store from "./store"

const app = document.getElementById('app')

ReactDOM.render(<Provider store={store}>
  <Layout />
</Provider>, app);

Layoutコンポーネントを確認します。src/js/components/Layout.jsを見てください。

import React from "react"
import { connect } from "react-redux"

import { fetchUser } from "../actions/userActions"
import { fetchTweets } from "../actions/tweetsActions"

@connect((store) => {
  return {
    user: store.user.user,
    userFetched: store.user.fetched,
    tweets: store.tweets.tweets,
  };
})
export default class Layout extends React.Component {
  componentWillMount() {
    this.props.dispatch(fetchUser())
  }

  fetchTweets() {
    this.props.dispatch(fetchTweets())
  }

  render() {
    const { user, tweets } = this.props;

    if (!tweets.length) {
      return <button onClick={this.fetchTweets.bind(this)}>load tweets</button>
    }

    const mappedTweets = tweets.map(tweet => <li key={tweet.id}>{tweet.text}</li>)

    return <div>
      <h1>{user.name}</h1>
      <ul>{mappedTweets}</ul>
    </div>
  }
}

画面が表示される時(componentWillMount)にユーザーを取得し、画面に表示しています。この動作に沿って説明をしていきます。

src/js/components/Layout.jsの15行目を見てください。

  componentWillMount() {
    this.props.dispatch(fetchUser())
  }

propsのdispatchを呼んでいますね。

ここでpropsにdispatchがあると言うのは、このコンポーネントへconnectしているからです。connectは、7行目にあります。

@connect((store) => {
  return {
    user: store.user.user,
    userFetched: store.user.fetched,
    tweets: store.tweets.tweets,
  };
})
export default class Layout extends React.Component {

@はDecoratorといって、コード上「次」にある要素に対して、影響を及ぼすことができる機能です。今回のコード例では、これは「次」にあるLayoutコンポーネントに対するconnectの記述です。connectすることで、Reactコンポーネントにthis.props.dispatchが生えて、dispatchできるようになります。

ここでは、fetchUser()というActionをdispatchしていると言うことになります。

このdispatchの結果、ユーザーの情報が読み込まれて画面が更新されることになるわけです。ここではその流れを追っていきます。

src/js/actions/userActoins.jsを見てください。

1行目にfetchUser()があります。ここはFETCH_USER_FULFILLEDというタイプのアクションを作成しています。またpayloadにユーザーのデータが入っています。これがdispatchされているわけです。

dipatchされると、それをreducerが処理し、storeを変更します。

src/js/reducers/userReducer.jsを見てください。

19行目がFETCH_USER_FULFILLEDを処理するreducerです。

      case "FETCH_USER_FULFILLED": {
        return {
          ...state,
          fetching: false,
          fetched: true,
          user: action.payload,
        }
      }

reducerが何をやっているかと言うと、stateを変更します。

...state,で現在のstateを展開し、fetching,fetched,userを変更して、returnします。returnされた結果が新しいstateになります。

ここでいうstateというのはアプリケーション全体におけるただ一つのstateです。アプリケーション内のstateを全てまとめて一元管理するのがreduxの考え方です。

reactの感覚でいうとstateは個々のコンポーネントにあるもののように思われますが、それとは違うので考え方の切り替えが必要です。

さて、こうして変更されたstateは画面に反映されなければなりませんが、それはどのようにされるのでしょうか。再度、Layoutコンポーネントを確認します。src/js/components/Layout.jsの7行目を見てください。

@connect((store) => {
  return {
    user: store.user.user,
    userFetched: store.user.fetched,
    tweets: store.tweets.tweets,
  };
})
export default class Layout extends React.Component {

connectが、reduxのstateと画面のpropsの紐付けを行います。returnされた要素が画面のpropsとして渡されます。

stateが更新されると、reduxは以前のstateと比較を行い、更新されている部分のデータを使っている画面コンポーネントについて再描画を行います。

ここまででアクションの発行から画面の更新までの流れを一通り確認できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブロックチェーン上に読書感想文を書けるサービス作った

TL;DR

ブロックチェーンを利用したDappsの開発を勉強中です。
一度サンプルアプリケーションを作ってみたかったので作りました。
とりあえずざっくり作りたかったのでいろいろ適当です。

サービス概要

https://iread-decent.herokuapp.com/

image.png

image.png

image.png

サービス名は「IRead」としました。
いろいろ勉強で本を読むのですが、読むだけで終わってしまい、読んで得た知見・学んだことを共有できずにいました。
そこでIReadを使って本を読んだ感想文をブロックチェーン上に記録することで、学びの証跡を残せ、それを自分の公開鍵で自分の学びであることを証明できます。

解説

GitHub

https://github.com/theMistletoe/IRead

使用した技術

  • Truffle
  • solidity
  • react(Next.js)

工夫した点

solidity

https://github.com/theMistletoe/IRead/blob/master/contracts/BookReportToken.sol

肝となるコントラクトコードについてはERC721の規格を継承しています。
OpenZeppelinのコードを継承する形ですね。

このERC721に準拠したsolidityのコードを書く正解がちょっと分かりませんでした。
同じように他の方のERC721トークンの実装を見ると継承しているものの、コントラクトの属性を継承した子のフィールドに直接もってやり取りしているようだったので、それだとonlyOwner制約などの恩恵を受けられないのではと思いました。

なので今回はアプリケーションの属性についてはトークンのメタ情報(tokenURI)にJSON形式で保持することにしました。

実装方針が正しいかどうかは不明なので詳しい方がいれば教えてください。

react

https://github.com/theMistletoe/IRead/blob/master/client/comps/AllReports.js

reactについてはてんで素人で書いたので間違い・不正があれば突っ込んでください笑

一点だけ、全トークンを表示するときに、トークンがburnされているとトークンlengthが合わなくなって全件取得できなかったので、
burnされたトークン含め過去発行されたトークンの総数の取得をTrensferイベントから取得しています。
(burnされるとtotalSupply関数で取得されるトークン数も-1される)

参考

追記します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.Fragmentの使いどころ

はじめに

Reactを触ることになって、初めて知ることだらけ。
技術用語がまだスッと入って来ず、先輩から何度か説明してもらい、手を動かし、記事を読んでやっとふわっと理解したので一応記事にしておく。
ほぼほぼ自分用のメモなので、説明が足りないところがあったり間違ってるところがあったらごめんなさい。

React.Fragmentってなに

Reactではrenderメソッドのreturnの中身は一つの要素でないといけないらしい。
要素が並列で並んでしまう場合に <React.Fragment> を使うとHTMLタグとして生成されずに親要素として機能してくれる。

使い所と使い方

return (
  <ul>
    <li>hoge</li>
    <li>hoge</li>
  </ul>
)

これは<ul>の1要素だから <React.Fragment> は不要!
<ul> のなかに <li> がたくさんあるけどまとめなくて平気なの?と思ったが、<li><ul> の子要素だから <ul> 1つの要素として見られる。


return (
  hogeリスト
  <ul>
    <li>hoge</li>
    <li>hoge</li>
  </ul>
)

続いてこちらは "hogeリスト" という文字列要素と、 <ul>要素が並列で存在しているのでエラーとなってしまう。

Reactでは、そんな時に <React.Fragment> を使って1つの要素としてまとめることができる。

2つになっちゃたのを1つのものとするためにサ○ンラップで包んであげるイメージ。
早速やってみる。

return (
  <React.Fragment>
    hogeリスト
    <ul>
      <li>hoge</li>
      <li>hoge</li>
    </ul>
  </React.Fragment>
)

これで、 という1要素に収まった。

ん? 待てよ。それなら <div> で囲んであげても同じように親要素としてまとめてあげることはできるぞ...と思ったが、それだとHTMLに不要な <div>タグが生成されてしまった。

そこで <React.Fragment> を使うことで、親要素としての役目を果たしつつ、HTMLタグは生成されなくなった。 便利!

Reactのお作法を守るためだけに使うことができるから、<React.Fragment> を使うと良い。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

伝えたいこと

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

今回の記事でできること

trivy_flow.png

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

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

導入

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

trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

ただフロントエンドの方は「脆弱性はインフラ側が対応してくれるから、自分には関係ない」と思っていないでしょうか。
そのような方をメインに、今回は以下の内容を紹介します。

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

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

ソースコード

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

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

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

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

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

1. プロジェクトの作成

今回は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
# 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

1ファイルでmulti-stage buildの問題点

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

$ 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.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.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
.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が指定されていればそれを利用します。

$ 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 を開くと、画面が表示されることが確認できました。

localhost.png

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

localhost_spa

3. trivyでスキャン

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

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

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回目以降のスキャンが高速になります。

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 --silent "https://api.github.com/repos/knqyf263/trivy/releases/latest" | \
                grep '"tag_name":' | \
                sed -E 's/.*"v([^"]+)".*/\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

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

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

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

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

ci-error.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などで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

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

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

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

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

3. 重要な場合は...

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

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で で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で脆弱性対策できる
  • 脆弱性のおおまかな運用方法

なお、コンテナになっていない場合は、Vulsという素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

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

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

伝えたいこと

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

今回の記事でできること

trivy_flow.png

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

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

導入

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

trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

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

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

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

ソースコード

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

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

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

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

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

1. プロジェクトの作成

今回は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
# 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

1ファイルでmulti-stage buildの問題点

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

$ 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.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.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
.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が指定されていればそれを利用します。

$ 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 を開くと、画面が表示されることが確認できました。

localhost.png

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

localhost_spa

3. trivyでスキャン

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

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

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回目以降のスキャンが高速になります。

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

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

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

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

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

ci-error.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などで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

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

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

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

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

3. 重要な場合は...

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

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で で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を使ってみてください。
S3などにホストしている場合は、npm auditだけでよさそうです。

なお、コンテナになっていない場合で実行環境をチェックしたい場合、Vulsという素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

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

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

伝えたいこと

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

今回の記事でできること

Trivy_flow.png

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

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

導入

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

Trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

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

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

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

ソースコード

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

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

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

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

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

1. プロジェクトの作成

今回は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
# 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

1ファイルでmulti-stage buildの問題点

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

$ 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.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.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
.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が指定されていればそれを利用します。

$ 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 を開くと、画面が表示されることが確認できました。

localhost.png

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

localhost_spa

3. Trivyでスキャン

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

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

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回目以降のスキャンが高速になります。

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

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

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

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

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

ci-error.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などで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

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

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

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

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

3. 重要な場合は...

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

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で で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という素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

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

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む