ArchiveBox + Pocket + Fessで作る、いつでもどこでもマイ魚拓（全文検索付き）

この記事は [NTTコミュニケーションズ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nttcom) の11日目の記事です。
昨日は @koki-sato さんの [作業手順書を docsify で作成する](https://qiita.com/koki-sato/items/9c659f423e11ebf699d9) でした。

# はじめに
セキュリティ脅威を分析する際に**OSINT**(**O**pen **S**ource **INT**elligence)と呼ばれる手法が用いられることがあります。OSINTとは**公開されている情報源を用いた情報収集・分析活動**のことで、その主たる情報源は**インターネット上で公開されている記事やSNS、脅威フィードなど**です。
OSINTの活用例としては、[OSINTで集めた痕跡情報を参考にしてインシデントレスポンス時に発見したURLやファイルが悪性であるかを判断すること](https://developer.ntt.com/ja/blog/c18174c4-a81a-4c0c-b508-a4d376516b04)などがあります。

日々情報収集をしていると以下のような課題に直面します。

- 元情報がどこにあったか分からなくなる
- 溜め込んだ情報が多すぎて見つけられなくなる

# マイ魚拓作ってみた
そこで今回はそんな課題を解決すべく、**プライベートアーカイブ（名付けて「マイ魚拓」）**を作ってみました。

目指した要件は以下の3点です

- 有料ツール/サービスを使わないこと
- 特定の端末に依存せずにいつでもどこでもアーカイブできること（**端末非依存**）
- アーカイブしたコンテンツの中身まで検索できること（**全文検索**）

これらの要件を満たすために[ArchiveBox](https://archivebox.io/)と[Pocket](https://getpocket.com/)と[Fess](https://fess.codelibs.org/ja/)を組み合わせました。

<img width="1000" alt="ArchiveBox" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/c4cf5ce8-2ba9-f891-cb0e-adfbafcb04cf.png">

<img width="1000" alt="Fess" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/51405995-bc08-3545-7016-57cef49ea74a.png">


# レシピ
### 魚拓サーバを立ち上げる
魚拓サーバにはオープンソースのWebアーカイブツールである[ArchiveBox](https://archivebox.io/)を選びました。
HTMLだけでなくPDFやスクリーンショット（PNG）、WARCなど様々な形式でアーカイブできるのが特徴です。
[ConoHa](https://www.conoha.jp/)では[ArchiveBoxがインストールされたテンプレートイメージ](https://support.conoha.jp/v/archivebox/)が提供されていたりもします。

#### 前提条件

Docker + Docker Composeが動くこと[^1]

#### 準備  
必要なのは`docker-compose.yml`と`nginx.conf`だけです。  
Dockerイメージは全て[DockerHub](https://hub.docker.com/)にある公式イメージを使います。

```
./
├ docker-compose.yml
└ nginx.conf
```

* docker-compose.yml

```docker-compose.yml
version: '3'

volumes:
    archivebox-data:
    elasticsearch-data01:
    elasticsearch-dictionary01:
    elasticsearch-data02:
    elasticsearch-dictionary02:

services:
    archivebox:
        image: nikisweeting/archivebox:latest
        stdin_open: true
        tty: true
        environment:
            - TZ=Asia/Tokyo
            - SUBMIT_ARCHIVE_DOT_ORG=False
            - FETCH_MEDIA=False
        volumes:
            - archivebox-data:/data
        shm_size: 256m
        restart: always
        command: bash -c '[[ -f /data/index.html ]] || ( echo "https://github.com/pirate/ArchiveBox" | /bin/archive ); tail -f /dev/null'

    nginx:
        image: nginx
        ports:
            - '8098:80'
        environment:
            - TZ=Asia/Tokyo
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf:ro
            - archivebox-data:/var/www
        restart: always

    fess:
        image: codelibs/fess:13.4.0
        ports:
            - "8080:8080"
        depends_on:
            - elasticsearch01
            - elasticsearch02
        environment:
            - TZ=Asia/Tokyo
            - RUN_ELASTICSEARCH=false
            - "ES_HTTP_URL=http://elasticsearch01:9200"
            - "FESS_DICTIONARY_PATH=/usr/share/elasticsearch/config/dictionary/"
        restart: always

    elasticsearch01:
        image: codelibs/fess-elasticsearch:7.4.0
        environment:
            - TZ=Asia/Tokyo
            - node.name=elasticsearch01
            - discovery.seed_hosts=elasticsearch02
            - cluster.initial_master_nodes=elasticsearch01,elasticsearch02
            - cluster.name=fess-elasticsearch
            - bootstrap.memory_lock=true
            - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
            - "FESS_DICTIONARY_PATH=/usr/share/elasticsearch/config/dictionary"
        ulimits:
            memlock:
                soft: -1
                hard: -1
        volumes:
            - elasticsearch-data01:/var/lib/elasticsearch
            - elasticsearch-dictionary01:/usr/share/elasticsearch/config/dictionary
        restart: always

    elasticsearch02:
        image: codelibs/fess-elasticsearch:7.4.0
        environment:
            - TZ=Asia/Tokyo
            - node.name=elasticsearch02
            - discovery.seed_hosts=elasticsearch01
            - cluster.initial_master_nodes=elasticsearch01,elasticsearch02
            - cluster.name=fess-elasticsearch
            - bootstrap.memory_lock=true
            - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
            - "FESS_DICTIONARY_PATH=/usr/share/elasticsearch/config/dictionary"
        ulimits:
            memlock:
                soft: -1
                hard: -1
        volumes:
            - elasticsearch-data02:/var/lib/elasticsearch
            - elasticsearch-dictionary02:/usr/share/elasticsearch/config/dictionary
        restart: always
```

ArchiveBoxの[公式Dockerイメージ](https://hub.docker.com/r/nikisweeting/archivebox)は`latest`タグしかないので分かりにくいですが、今回は[12/1時点のバージョンのArchiveBox](https://github.com/pirate/ArchiveBox/tree/94dba225ff157a4f77402e719c3fc7bcfbbe566e)を使用しています。

また、ArchiveBoxの環境変数は[公式ドキュメント](https://github.com/pirate/ArchiveBox/wiki/Configuration)を参考にしつつお好みで。
上記の例では、Archive.orgへのアップロード（`SUBMIT_ARCHIVE_DOT_ORG`）とメディアファイルの取得（`FETCH_MEDIA`）をそれぞれ無効にしています。

* nginx.conf

`nginx.conf`はArchiveBox公式の[設定](https://github.com/pirate/ArchiveBox/blob/master/etc/nginx/nginx.conf)を持ってくるだけです。

#### サーバ起動

```shell
docker-compose up -d
```

#### 動作確認（マイ魚拓の閲覧）
マイ魚拓のトップページは`http://localhost:8098/`からアクセスできます。[^2]
取り込みが問題なく完了していれば、以下のような画面が見えます。
<img width="600" alt="ArchiveBox" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/4f5ee0b4-887e-36b9-c38d-fbaa9841d236.png">

#### 魚拓へのデータ投入

ArchiveBox公式ドキュメントには[dockerコマンドを用いた方法](https://github.com/pirate/ArchiveBox/wiki/Docker#usage-1)は書かれていますが、`docker-compose`コマンドを用いた方法は書かれていません。
公式ドキュメントと同じことを`docker-compose`で実現するコマンドは以下の通りです。

```sh
echo 'https://example.com' | docker-compose exec -T archivebox /bin/archive
# or
cat bookmarks.html | docker-compose exec -T archivebox /bin/archive
# or
docker-compose exec archivebox /bin/archive 'https://example.com/some/rss/feed.xml'
```

### 「いつでもどこでも」の実現（Pocket連携）
このままではArchiveBoxが稼働している端末でコマンドを実行しないと魚拓が取れないので不便です。
人間は忘れる生き物なので移動中の隙間時間で調べ物をしている最中でもすぐに魚拓が取りたくなります。

そこで外部サービスである[Pocket](https://getpocket.com/)と連携させます。
Pocketはインターネット上の記事などのコンテンツの一覧を保存し、あとでまとめて読むためのサービスです。
Pocketには[保存した一覧をRSSで公開する機能](https://help.getpocket.com/article/1074-can-i-subscribe-to-my-list-via-rss)があるため、今回はこれを活用します。

1. Pocket RSS公開設定
無料アカウントを作成後、アカウントオプションのプライバシー設定で`RSS フィードのパスワード保護を解除`します。[^3]
これでPocket側の準備は完了です。

2. RSSの定期取得
あとはArchiveBox側でcronで定期的にRSSを取得・アーカイブするように仕込めば連携は完了です。
具体的には、以下のようなコマンドをcron実行します。[^4]

```sh
docker-compose exec archivebox /bin/archive 'http://getpocket.com/users/PATH-TO-MY-FEED/feed/all'
```

Pocketは主要ブラウザの拡張機能として利用できますし、AndroidやiPhoneアプリも存在します。
複数の端末で同じPocketアカウントを使って気になったページを随時Pocket保存しておけば、**いつでもどこでも**勝手に魚拓を取ってくれるわけです。

### 全文検索の実現（Fess設定）
ここまででいつでもどこでも魚拓が取れるようになりましたが、実はArchiveBox標準の検索機能は**タイトルしか検索できません**。
これでは必要な時に情報を振り返って活用することができません。

そこで最後に登場するのが[Fess](https://fess.codelibs.org/ja/)です。
Fessは「**5分で簡単に構築可能な全文検索サーバー**」を謳ったオープンソースの全文検索サーバーです。
機能が豊富で、Web・ファイルシステム・Windows共有フォルダ・データベースなど様々な方式でクロールができ、MS Office(Word/Excel/PowerPoint)やPDFなど多くのファイル形式に対応しています。

すでに魚拓サーバの起動と共にFessも起動しているため、`http://localhost:8080/`からアクセスすることができます。[^5]
![Fessトップ画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/82a5b0d5-5258-b818-828d-85cbb056fdc6.png)

Fessを設定するには、管理画面（`http://localhost:8080/admin`）に管理者としてログインします。
デフォルトのユーザ名・パスワードは[公式ドキュメント](https://fess.codelibs.org/ja/articles/article-1.html#id13)に記載の通り、どちらも`admin`です。

魚拓の検索に必要な設定は最低限以下の2点です。

1. クローラ設定
2. パスマッピング設定

#### クローラ設定
ArchiveBoxを巡回し、検索インデックスを作成するためのクローラ設定を新規作成します。
`スレッド数`や`間隔`の値は端末のスペック応じて調整してください。
![クローラ設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/c17ff7f6-89f2-e0d6-ab29-63b7d8b6d4d4.png)

#### パスマッピング設定
前述のクローラ設定はDockerの内部ネットワーク経由でArchiveBoxにアクセスさせているため、そのままでは検索結果のリンクが外部からアクセス可能なURLになりません。
そのためパス文字列の置換を行うパスマッピングを設定します。
![パスマッピング設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/2b45c718-ed1c-15cf-8043-1149f2895e0c.png)

#### 動作確認（手動クローリング）
デフォルトでは日次でクローリングが走りますが、スケジュール設定画面から手動でクローリングを回すこともできます。
![Default Crawlerの手動実行](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/a8ab53a3-10fe-de02-95c2-c1206c928754.png)

クローリングの実行完了はジョブログから確認ができます。
![ジョブログの確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531283/bcd87087-212b-12ed-5454-dcd9a79ea380.png)

無事インデックスが作成されれば晴れてコンテンツも含めた**全文検索**ができるようになります。

# おわりに
より効率的なOSINTを実現するためにマイ魚拓を作ってみました。
サイトによってはアーカイブの表示が崩れたり、拡張子の付いていない画像ファイルが検索インデックスに含まれてしまったりするなど、細かいところで気になる部分はまだありますが、個人的には十分なクオリティが実現できて満足です。
次は[OCRを使った画像内文字列の検索インデックス化](https://qiita.com/shinsuke_sugaya/items/a02b102b02fcb8e6c922)をやりたいと企んでいます。

それでは皆さんも良いOSINTライフを！

明日は @nitky さんです。お楽しみに！


[^1]: 筆者はmacOS(Catalina, 10.15.1) / Docker Desktop for Mac(2.1.0.5) で動作を確認しました。
[^2]: 簡単のためにHTTPでlocalhostにアクセスしています。必要に応じてHTTPS化やドメインの設定を行ってください。
[^3]: この設定変更により、RSSはパスワードなしで公開されるため、第三者も閲覧可能になることにご注意ください。
[^4]: 実際のURLはPocketのプライバシー設定画面で確認してください
[^5]: こちらも簡単のためにHTTPでlocalhostにアクセスしています。必要に応じてHTTPS化やドメインの設定を行ってください。