ottijp blog

DMARCのレポートを要約して表示するシェルスクリプトを作った with Claude Code

  • 2025-07-28

環境

  • macOS: 15.5 (Sequoia)
  • bash: 3.2
  • xmllint (libxml): 20913
  • Claude Code: 1.0.61

モチベーション

独自ドメインで運用しているメールでDMARCを設定してから,レポートのメールが日々飛んでくるようになったのですが,DKIMやPSFにpass/failしたものをさっと要約して確認したかったので,Claude Codeを使ってシェルスクリプトを作りました.

構成

次のように,シェルスクリプトと同じディレクトリに,受信したレポートのzipファイルを配置します.

$ ls
check.sh*
google.com!example.com!1751328000!1751414399.zip
google.com!example.com!1751414400!1751500799.zip
google.com!example.com!1751587200!1751673599.zip
google.com!example.com!1751846400!1751932799.zip
google.com!example.com!1751932800!1752019199.zip
google.com!example.com!1752105600!1752191999.zip
google.com!example.com!1752796800!1752883199.zip
google.com!example.com!1752969600!1753055999.zip
google.com!example.com!1753228800!1753315199.zip
google.com!example.co.jp!1751673600!1751759999.zip

実行

スクリプトを実行すると,以下のように要約がプリントされます.

$ ./check.sh
file name                                                          source IP count Disposition  DKIM     SPF      domain          report period
------------------------------------------------------------ --------------- ----- ------------ -------- -------- --------------- ---------------
google.com!example.com!1751328000!1751414399.zip             xxx.xxx.xxx.145     1 none         pass     pass     example.com     07/01-07/02
                                                              xxx.xx.xxx.152     1 none         pass     pass     example.com     07/01-07/02
google.com!example.com!1751414400!1751500799.zip              xxx.xx.xxx.148     1 none         pass     pass     example.com     07/02-07/03
google.com!example.com!1751587200!1751673599.zip             xxx.xxx.xxx.144     1 none         pass     pass     example.com     07/04-07/05
                                                             xxx.xxx.xxx.153     1 none         pass     pass     example.com     07/04-07/05
                                                               xxx.xx.xxx.41     1 none         pass     fail     example.com     07/04-07/05
google.com!example.com!1751846400!1751932799.zip             xxx.xxx.xxx.154     1 none         pass     pass     example.com     07/07-07/08
google.com!example.com!1751932800!1752019199.zip              xxx.xx.xxx.158     1 none         pass     pass     example.com     07/08-07/09
google.com!example.com!1752105600!1752191999.zip             xxx.xxx.xxx.151     1 none         pass     pass     example.com     07/10-07/11
google.com!example.com!1752796800!1752883199.zip             xxx.xxx.xxx.152     2 none         pass     pass     example.com     07/18-07/19
                                                             xxx.xxx.xxx.151     1 none         pass     pass     example.com     07/18-07/19
google.com!example.com!1752969600!1753055999.zip             xxx.xxx.xxx.153     1 none         pass     pass     example.com     07/20-07/21
google.com!example.com!1753228800!1753315199.zip              xxx.xx.xxx.155     1 none         pass     pass     example.com     07/23-07/24
                                                              xxx.xx.xxx.152     1 none         pass     pass     example.com     07/23-07/24
google.com!example.co.jp!1751673600!1751759999.zip            xxx.xx.xxx.167     1 none         fail     fail     example.co.jp   07/05-07/06

スクリプト作成

スクラッチでClaude Codeと対話しながら,一部自分で修正などもしながら進めました. 合計 3,103,987 トークンで完了しました. Happy AI coding!!

#!/bin/bash

# エラー時に即座にスクリプトを終了
set -e

# XMLから値を抽出する関数(エラー時はスクリプト終了)
extract_xml_value() {
  local xml_content="$1"
  local xpath="$2"
  local description="$3"
  local filename="$4"

  local result
  result=$(echo "$xml_content" | xmllint --xpath "$xpath" - 2>/dev/null)

  if [[ $? -ne 0 || -z "$result" ]]; then
    echo "Error: Failed to extract $description from XML in file: $filename" >&2
    exit 1
  fi

  echo "$result"
}

# テーブル各行のテンプレート文字列を定義
table_format="%-60s %15s %5s %-12s %-8s %-8s %-15s %-15s"

# テーブルヘッダを表示
printf "${table_format}\n" \
  "file name" "source IP" "count" "Disposition" "DKIM" "SPF" "domain" "report period"
printf "${table_format}\n" \
  "------------------------------------------------------------" "---------------" "-----" "------------" "--------" "--------" "---------------" "---------------"

for i in *.zip; do
  # XMLファイルのコンテンツを取得
  xml_content=$(unzip -p "$i" "*.xml" || echo "Error extracting XML from $i" >&2)

  # XMLが読み込めない場合
  if [[ ! -n "$xml_content" ]]; then
    echo "Error: Failed to extract XML from $i" >&2
    exit 1
  fi

  # レポートメタデータを取得
  domain=$(extract_xml_value "$xml_content" "//policy_published/domain/text()" "domain" "$i")
  begin_date=$(extract_xml_value "$xml_content" "//date_range/begin/text()" "begin_date" "$i")
  end_date=$(extract_xml_value "$xml_content" "//date_range/end/text()" "end_date" "$i")

  # 日付をフォーマット(UNIX時間から読みやすい形式に)
  if [[ -n "$begin_date" && -n "$end_date" ]]; then
    begin_formatted=$(date -r "$begin_date" "+%m/%d" 2>/dev/null || echo "$begin_date")
    end_formatted=$(date -r "$end_date" "+%m/%d" 2>/dev/null || echo "$end_date")
    date_range="${begin_formatted}-${end_formatted}"
  else
    date_range="Unknown"
  fi

  # 各recordの情報を処理
  record_count=$(extract_xml_value "$xml_content" "count(//record)" "record_count" "$i")

  if [[ "$record_count" -eq 0 ]]; then
    printf "${table_format}\n" \
      "$i" "N/A" "0" "unknown" "unknown" "unknown" "${domain:-N/A}" "$date_range"
    continue
  fi

  for ((j=1; j<=record_count; j++)); do
    source_ip=$(extract_xml_value "$xml_content" "//record[$j]/row/source_ip/text()" "source_ip for record $j" "$i")
    count=$(extract_xml_value "$xml_content" "//record[$j]/row/count/text()" "record count" "$i")
    disposition=$(extract_xml_value "$xml_content" "//record[$j]/row/policy_evaluated/disposition/text()" "disposition for record $j" "$i")
    dkim=$(extract_xml_value "$xml_content" "//record[$j]/row/policy_evaluated/dkim/text()" "dkim for record $j" "$i")
    spf=$(extract_xml_value "$xml_content" "//record[$j]/row/policy_evaluated/spf/text()" "spf for record $j" "$i")

    # ファイル名(最初のrecordのみ表示)
    if [[ $j -eq 1 ]]; then
      display_filename="$i"
    else
      display_filename=""
    fi

    # テーブル行を出力
    printf "${table_format}\n" \
      "$display_filename" \
      "${source_ip:-N/A}" \
      "${count:-0}" \
      "${disposition:-unknown}" \
      "${dkim:-unknown}" \
      "${spf:-unknown}" \
      "${domain:-N/A}" \
      "${date_range}"
  done
done

ottijp
都内でアプリケーションエンジニアをしています
© 2026, ottijp