DMARCレポートをdmarc-visualizerで可視化する

# DMARCレポートをdmarc-visualizerで可視化する
Gmail様の仰せに従ってせっかくメールサーバーの設定を変更してDKIMやDMARCに対応してGoogle様から日々DMARCのレポートが送られてくるのに、それをただただメールボックスの肥やしにするのは勿体無い！
と言うことで、DMARCレポートを可視化してくれる[dmarc-visualizer](https://github.com/debricked/dmarc-visualizer)をインストールしてみました

## dmarc-visualizerについて
Githubのdmarc-visualizerページ
https://github.com/debricked/dmarc-visualizer

[dmarc-visualizerのブログ](https://debricked.com/blog/analyze-and-visualize-dmarc-report/）で解説されているようにdmarc-visualizerはDMARCレポートを解析するparsedmarc、解析した情報を格納・検索するためのelasticsearch、elasticsearchに格納されたデータをグラフ化するためのgrafanaの3つのコンテナで構成されています

### dmarc-visualizerのインストールと実行
dmarc-visualizerのインストールは`docker compose`一発で必要なコンテナイメージがDocker Hubからインストールされるので簡単に利用を開始することができます
と言ったものの、実際にはdmarc-visualizerがすでに2年以上もメンテナンスされていないこともあって、諸々の関連アプリkーションのバージョンを調整しないと動作しませんでした。

#### 不要な設定の削除とelasticsearchのバージョンアップ(8.12.0)
```diff:docker-compose.yml
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,22 +5,18 @@ services:
     volumes:
       - ./files:/input:ro
       - ./output_files:/output
-      - ./parsedmarc/parsedmarc.ini:/parsedmarc.ini:ro
-      - /etc/localtime:/etc/localtime:ro
     command: parsedmarc -c /parsedmarc.ini /input/* --debug
-    environment:
-      - TZ=Asia/Tokyo
     depends_on:
       - elasticsearch
     restart: on-failure

   elasticsearch:
-    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5
+    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
     environment:
       - discovery.type=single-node
-      - "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
+      - xpack.security.enabled=false
     volumes:
-      - elasticsearch-data:/usr/share/elasticsearch/data
+      - ./elastic_data:/usr/share/elasticsearch/data

   grafana:
     build: ./grafana/
@@ -30,7 +26,3 @@ services:
     environment:
       GF_INSTALL_PLUGINS: grafana-piechart-panel,grafana-worldmap-panel
       GF_AUTH_ANONYMOUS_ENABLED: 'true'
-
-volumes:
-  elasticsearch-data:
-    driver: local
```

#### grafanaの最新版へのバージョンアップ
```diff:grafana/Dockerfile
--- a/grafana/Dockerfile
+++ b/grafana/Dockerfile
@@ -1,4 +1,4 @@
-FROM grafana/grafana:8.5.2
+FROM grafana/grafana:latest

 ADD --chown=grafana:root https://raw.githubusercontent.com/domainaware/parsedmarc/master/grafana/Grafana-DMARC_Reports.json /var/lib/grafana/dashboards/
 RUN chmod 644 /etc/grafana/provisioning
```

#### elasticsearchのバージョン設定の変更
```diff:grafana/grafana-provisioning/datasources/all.yml
--- a/grafana/grafana-provisioning/datasources/all.yml
+++ b/grafana/grafana-provisioning/datasources/all.yml
@@ -9,7 +9,7 @@ datasources:
   database: '[dmarc_aggregate-]YYYY-MM-DD'
   isDefault: true
   jsonData:
-    esVersion: 70
+    esVersion: 8.12.0
     timeField: 'date_range'
     interval: 'Daily'
   version: 1
@@ -22,7 +22,7 @@ datasources:
   database: '[dmarc_forensic-]YYYY-MM-DD'
   isDefault: false
   jsonData:
-    esVersion: 70
+    esVersion: 8.12.0
     timeField: 'arrival_date'
     interval: 'Daily'
   version: 1
```

#### Pythonのバージョンアップ（3.9.18-alpine3.19）とmsgraph-coreのバージョンダウン（<1.0.0）
```diff:parsedmarc/Dockerfile
--- a/parsedmarc/Dockerfile
+++ b/parsedmarc/Dockerfile
@@ -1,7 +1,6 @@
-FROM python:3.9-alpine3.16
+FROM python:3.9.18-alpine3.19

-RUN apk add build-base libxml2-dev libxslt-dev libffi-dev tzdata \
-    && pip install parsedmarc
+RUN apk add --update --no-cache libxml2-dev libxslt-dev build-base libffi-dev \
+    && pip install parsedmarc 'msgraph-core<1.0.0'

 COPY parsedmarc.ini /
-#COPY GeoLite2-Country.mmdb /usr/share/GeoIP/GeoLite2-Country.mmdb
```

### コンテナのビルド
上記のように必要な設定ファイルの修正が終わったならば`docker compose build`で必要なコンテナをビルドします

```bash:
docker build
```

macOSではDocker Hubのアクセスなどでキーチェーンの情報にアクセスする必要があるため、sshでリモートログインしている場合には以下のようにキーチェーンのロックを解除するように要求される場合があるので、その場合には`security -v unlock-keychain`でコンテナのビルドに先立ってキーチェーンをアンロックしておいてください

#### macOSの場合
macOSでsshリモートログインしてDocker Hubを使うとキーチェーンをアンロックできずエラーになる
```bash:
docker compose build
```

```console:sshでのリモートだとエラーになる
[+] Building 0.6s (4/4) FINISHED                           docker:desktop-linux
 => [grafana internal] load build definition from Dockerfile               0.0s
 => => transferring dockerfile: 316B                                       0.0s
 => [parsedmarc internal] load build definition from Dockerfile            0.0s
 => => transferring dockerfile: 224B                                       0.0s
 => ERROR [grafana internal] load metadata for docker.io/grafana/grafana:  0.6s
 => ERROR [parsedmarc internal] load metadata for docker.io/library/pytho  0.6s
------
 > [grafana internal] load metadata for docker.io/grafana/grafana:latest:
------
------
 > [parsedmarc internal] load metadata for docker.io/library/python:3.9.18-alpine3.19:
------
failed to solve: grafana/grafana:latest: error getting credentials - err: exit status 1, out: `error getting credentials - err: exit status 1, out: `keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again``
```

##### キーチェーンのアンロック
事前にキーチェーンをアンロックしておく
```shell:
security -v unlock-keychain ~/Library/Keychains/login.keychain-db
```

```console:キーチェーンのパスワードを聞かれるので入力
unlock-keychain "/Users/user/Library/Keychains/login.keychain-db"
password to unlock /Users/user/Library/Keychains/login.keychain-db:
```

### parsedmarcの設定ファイルを作成
DMARCレポートを解析するためのコンテナparsedmarcの設定ファイルのサンプルを元に自分が使う環境に合わせて設定ファイル`parsedmarc.ini`を作成します

parsedmarcのサンプル設定ファイル`parsedmarc.sample.ini`を`parsedmarc.ini`にコピーします
```shell:
cp parsedmarc/parsedmarc.sample.ini parsedmarc/parsedmarc.ini
```

#### parsedmarc設定のドキュメント
設定ファイルの使い方については[parsedmarcのGithubページ](https://github.com/domainaware/parsedmarc)や[parsedmarcのドキュメントファイル](https://domainaware.github.io/parsedmarc/usage.html#configuration-file)を参考にして設定します

##### ローカルのDMARCレポートファイルを読み込んでレポートを生成する場合
DMARCレポートは事前に何らかの方法でDMARCレポートを受信しているメールサーバーからダウンロードしておき、dmarc-visualizerを展開したフォルダーに`files`という名前のフォルダを作り、ZIP付圧縮されたままの状態で構わないのでDMARCレポートの添付ファイルをこのフォルダ内にコピーしておきます
```ini:parsedmarc/parsedmarc.ini
[general]
save_aggregate = False
save_forensic = False

[elasticsearch]
hosts = elasticsearch:9200
ssl = False
```

##### IMAP4サーバーから直接DMARCのレポートメールを読み込んでレポートを生成する場合
ただし、parsedmarcのIMAP４クライアントは複数のメールサーバーへのアクセスなど、細かな設定はできないので別途自分でスクリプトを書くなどしてDMARCレポートのメールをダウンロードする方が使いやすいと感じました

```ini:parsedmarc/parsedmarc.ini
[general]
save_aggregate = True
save_forensic = True
output = /output/

[imap]
host = mail.example.com
user = dmarc@mail.example.com
password = password
watch = True

[elasticsearch]
hosts = elasticsearch:9200
ssl = False
```

##### IMAP4サーバーからDMARCレポートを取得するスクリプト
以下はPythonでちょろちょろっと書いた複数のIMAPサーバーからDMARCレポートをダウンロードしてfilesフォルダに集めるためのスクリプトです
先の「ローカルのDMARCレポートファイルを読み込んでレポートを生成する場合」の設定との組み合わせで利用することが出来ます
スクリプトはGmailからのDMARCレポートをZIP圧縮で受け取った場合にしか配慮していないし、パスワードとかハードコードなので色々ツッコミどころ満載ですが、とりあえずの実験用です
このスクリプトでIMAPサーバーからDMARCレポートを取得後に`docker compose up`でコンテナを起動することでダウンロードしたレポートを可視化することが出来ます
```python:fetch_dmarc_reports.py
#!/usr/bin/python3

import imaplib
import email
from email.header import decode_header, make_header
import re

# IMAPユーザー、サーバー、パスワードのペア
imap_accounts = {
    'dmarc-reports@hogehoge.tld': ['imap.hogehoge.tld', 'ABCdEFGhIJ'],
    'dmarc-reports@gmail.com': ['imap.gmail.com', 'abcd 1234 EFgH IjkL']}

# IMAPサーバーを順次処理
for imap_user in imap_accounts:
    imap_server = imap_accounts[imap_user][0]
    imap_password = imap_accounts[imap_user][1]
    imap_search_subject = imap_accounts[imap_user][2]

    # Gmailにログイン
    # ログインにはGmailで発行したアプリパスワードが必要なことに注意
    imap = imaplib.IMAP4_SSL(imap_server)
    imap.login(imap_user, imap_password)

    # IMAP4サーバー(Gmail)からメールボックス一覧を取得
    mboxes = imap.list()
    print(mboxes)

    # メールボックスを順次処理
    for mbox in mboxes[1]:
        # パターンマッチしたいのでバイナリから文字列に
        s = mbox.decode()
        print(s)
        # メールボックスの情報を属性、階層区切り文字、メールボックス名に分割
        m = re.match('\((\\\\.*)\) (".*") (.*)$', s)
        mbox_attribute = m.group(1)
        # 属性が '\Noselect' (選択不可)ならスキップ
        if "\\Noselect" in mbox_attribute:
            continue
        mbox_name = m.group(3)
        # メールボックスを選択
        imap.select(mbox_name)
        # メールボックスからDMARCのレポートを検索
        response, messages = imap.search(None, imap_search_subject)
        messages = messages[0].split(b' ')
        # 検索したメールを順番に処理
        for mail in messages:
            # メールの内容を取得できなければスキップ
            if not mail:
                continue
            _, data = imap.fetch(mail, '(RFC822)')
            # メールの中身をパースしてmsg構造体に格納
            msg = email.message_from_bytes(data[0][1])
            # Subjectからレポート対象ドメインを取得、レポート送信者
            # 、レポートIDを取得
            subject = str(make_header(decode_header(msg["Subject"])))
            subject.replace('\n', '')
            print(subject)
            # と思ったが、予想以上にSubjectのフォーマットが複雑なので後回しw
            #m = re.match('Report domain: (.*) Submitter: (.*) Report-ID: (.*)$', subject)
            #if m is not None:
            #    report_domain = m.group(1)

            # メッセージのパートから添付ファイルを検出
            for part in msg.walk():
                filename = part.get_filename()
                if (filename and
                    (part.get_content_type() == 'application/zip' or
                     part.get_content_type() == 'application/gzip')):
                    # ZIPファイル(DKIMレポート)や
                    #.gzファイル(Aggregate Report)が添付されていたら
                    # ファイルに保存
                    with open('files/' + filename, 'wb') as f:
                        f.write(part.get_payload(decode=True))

imap.close()
imap.logout()
```

DMARCレポート解析用コンテナの設定ファイル(parsedmarc.ini)が用意できてfilesの下へとDMARCレポートをダウンロードしたなら`docker compose　up`でコンテナを実行しレポートを生成すします
```bash:
docker compose up -d
```

```console:
[+] Running 11/11
 ✔ elasticsearch 10 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿]      0B/0B      Pulled            24.1s
   ✔ e4d291082c72 Pull complete                                            1.2s
   ✔ e6947a0eeb2c Pull complete                                            0.8s
   ✔ af7f7cf6eb22 Pull complete                                            0.6s
   ✔ 89732bc75041 Pull complete                                            1.1s
   ✔ d38d8a4afa78 Pull complete                                           10.6s
   ✔ fb65ac828db6 Pull complete                                            1.6s
   ✔ d3b6064b8ab8 Pull complete                                            1.7s
   ✔ f8484251cc74 Pull complete                                            2.1s
   ✔ 9ffe1c016006 Pull complete                                            2.4s
   ✔ e40fcac62c91 Pull complete                                            2.6s
[+] Running 3/4
 ⠙ Network dmarc-visualizer_default            Created                     1.1s
 ✔ Container dmarc-visualizer-grafana-1        Started                     0.8s
 ✔ Container dmarc-visualizer-elasticsearch-1  Started                     0.8s
 ✔ Container dmarc-visualizer-parsedmarc-1     Started                     0.5s
```

`docker compose`でのコンテナ起動直後にはDMARCレポートを解析するためのparsedmarc、データの格納と検索のためのelasticsearch、解析後にelasticsearchに格納されたデータをグラフ化するためのgrafanaの3つのコンテナが起動されます
```bash:
docker ps
```

```console:docker psからの応答
CONTAINER ID   IMAGE                                                  COMMAND                   CREATED         STATUS                  PORTS                    NAMES
0a5f47b805f4   dmarc-visualizer-parsedmarc                            "parsedmarc -c /pars…"   3 seconds ago   Up Less than a second                            dmarc-visualizer-parsedmarc-1
35300cde4ad9   docker.elastic.co/elasticsearch/elasticsearch:8.12.0   "/bin/tini -- /usr/l…"   3 seconds ago   Up 2 seconds            9200/tcp, 9300/tcp       dmarc-visualizer-elasticsearch-1
99e791bd0010   dmarc-visualizer-grafana                               "/run.sh"                 3 seconds ago   Up 2 seconds            0.0.0.0:3000->3000/tcp   dmarc-visualizer-grafana-1
```

parsedmarcによるDMARCレポートの解析が終わるとコンテナparsedmarcは終了し、elasticsearchとgrafinaの2つのコンテナだけが残ります
```bash:
docker ps
```

```console:docker psからの応答
CONTAINER ID   IMAGE                                                  COMMAND                   CREATED          STATUS          PORTS                    NAMES
35300cde4ad9   docker.elastic.co/elasticsearch/elasticsearch:8.12.0   "/bin/tini -- /usr/l…"   36 seconds ago   Up 35 seconds   9200/tcp, 9300/tcp       dmarc-visualizer-elasticsearch-1
99e791bd0010   dmarc-visualizer-grafana                               "/run.sh"                 36 seconds ago   Up 35 seconds   0.0.0.0:3000->3000/tcp   dmarc-visualizer-grafana-1
```

### 解析したDMARCレポートをブラウザで表示する
コンテナを立ち上げたホストOSで `http://localhost:3000/`　へとアクセスすることでgrafanaのレポートを表示することが出来ます