DMARCのレポートを要約して表示するシェルスクリプトを作った with Claude Code
環境
- 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