WAFにアタッチされたルール別の Count, Block, Allow を表にする(Cloudfront)
1. はじめに
こんにちは。クラスメソッドオペレーションズのあのふじたです。
最近、CloudFrontにアタッチされているWAFルールを大きく変更する必要があり、変更後のルールの状況を1週間程度遡って確認していく機会がありました。
DevOps Agentに調査やレポート作成を任せられる場面も増えてきましたが、そういった場合でも人間側で最低限の裏付けを取れるようにしておきたいと考え、簡易的なScriptを作成しました。
備忘録として記事を書こうと思います。
2. 必要なパッケージのインストール(動作確認した環境のみ)
jq Install
MacOS: brew install jq
Amzn2023: yum install jq
DuckDB Install
MacOS/Amzn2023: curl https://install.duckdb.org | sh
3. ファイル構成
以下のような構成で実装しました。
AIの支援を活用しながら作成しています。
cloudfront_waf_daily_metrics.sh
#!/bin/bash
#
# CloudFront + AWS WAFv2 日別ルールレポート
#
# 対象期間:
# --end-date で指定したJST日付を含む直近7日間
#
# 標準出力:
# 注意事項 + Markdown表
# ※ Markdown表では横幅を抑えるため MetricName / RuleType を省略
#
# CSV:
# cloudfront_waf_daily_report_<DistributionID>_<終了日>.csv
# ※ CSVには MetricName / RuleType を含む
#
# 必須:
# - Bash 3.2
# - AWS CLI v2
# - jq
# - DuckDB CLI
#
set -euo pipefail
export LC_ALL=C
export AWS_PAGER=""
PROFILE=""
DISTRIBUTION=""
END_DATE=""
usage() {
cat <<'EOF'
Usage:
cloudfront_waf_daily_metrics.sh \
--profile PROFILE \
--distribution DISTRIBUTION \
--end-date YYYY-MM-DD
Options:
-p, --profile AWS CLI profile name
-d, --distribution Distribution ID / CloudFront domain / Alias / Comment
-e, --end-date Last date of the 7-day period in JST (YYYY-MM-DD)
-h, --help Show this help
EOF
}
die() {
echo "ERROR: $*" >&2
exit 1
}
require_value() {
[ "$#" -ge 2 ] && [ -n "${2:-}" ] || die "Option $1 requires a value."
}
while [ "$#" -gt 0 ]; do
case "$1" in
-p|--profile) require_value "$@"; PROFILE="$2"; shift 2 ;;
-d|--distribution|--distribution-name) require_value "$@"; DISTRIBUTION="$2"; shift 2 ;;
-e|--end-date) require_value "$@"; END_DATE="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $1" ;;
esac
done
[ -n "$PROFILE" ] || die "--profile is required."
[ -n "$DISTRIBUTION" ] || die "--distribution is required."
[ -n "$END_DATE" ] || die "--end-date is required."
for cmd in aws jq duckdb; do
command -v "$cmd" >/dev/null 2>&1 || die "$cmd is not installed."
done
# 日付の検証と、期間の各境界値の算出をまとめて行う。
# JST 00:00 = 前日UTC 15:00
DATE_INFO="$(
jq -nr --arg d "$END_DATE" '
(try ($d + "T00:00:00Z" | fromdateiso8601) catch empty)
| select(strftime("%Y-%m-%d") == $d)
| [
tostring,
(. - 6 * 86400 | strftime("%Y-%m-%d")),
(. - 6 * 86400 - 9 * 3600 | strftime("%Y-%m-%dT%H:%M:%SZ")),
(. + 86400 - 9 * 3600 | strftime("%Y-%m-%dT%H:%M:%SZ"))
]
| join(" ")
'
)"
[ -n "$DATE_INFO" ] ||
die "--end-date must be a valid calendar date in YYYY-MM-DD: $END_DATE"
read -r END_DATE_EPOCH START_DATE START_UTC END_UTC <<EOF
$DATE_INFO
EOF
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cloudfront-waf-metrics.XXXXXX")"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
DISTRIBUTION_JSON="$TMP_DIR/distribution.json"
WEB_ACL_JSON="$TMP_DIR/web-acl.json"
RULES_TSV="$TMP_DIR/rules.tsv"
DAYS_TSV="$TMP_DIR/days.tsv"
RULE_METRICS_TSV="$TMP_DIR/rule-metrics.tsv"
DAILY_METRICS_TSV="$TMP_DIR/daily-metrics.tsv"
REPORT_DB="$TMP_DIR/report.duckdb"
# CloudWatchからSum統計(1時間粒度)をJSONで標準出力へ返す。
# 使い方: cw_get_sum NAMESPACE METRIC_NAME DIMENSION...
cw_get_sum() {
local namespace="$1" metric="$2"
shift 2
aws cloudwatch get-metric-statistics \
--profile "$PROFILE" \
--region us-east-1 \
--namespace "$namespace" \
--metric-name "$metric" \
--dimensions "$@" \
--start-time "$START_UTC" \
--end-time "$END_UTC" \
--period 3600 \
--statistics Sum \
--output json
}
# 使い方: waf_get_sum METRIC_NAME WEBACL_DIMENSION RULE_DIMENSION
waf_get_sum() {
cw_get_sum AWS/WAFV2 "$1" \
"Name=WebACL,Value=$2" \
"Name=Rule,Value=$3"
}
datapoint_count() {
jq '.Datapoints | length'
}
# 標準入力のCloudWatch JSONをJST日別のTSV行へ変換して追記する。
# RULE_NAMEを渡した場合はrule_name列を含む4列、省略時は3列。
# 使い方: append_datapoints OUTPUT_TSV METRIC_NAME [RULE_NAME]
append_datapoints() {
jq -r --arg metric "$2" --arg rule "${3:-}" '
def tz_offset:
if . == "Z" then 0
else
(if startswith("-") then -1 else 1 end)
* ((.[1:3] | tonumber) * 3600 + (.[4:6] | tonumber) * 60)
end;
def ts_to_epoch:
capture(
"^(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})(?:\\.[0-9]+)?(?<timezone>Z|[+-][0-9]{2}:[0-9]{2})$"
) as $ts
| ($ts.datetime + "Z" | fromdateiso8601) - ($ts.timezone | tz_offset);
.Datapoints[]?
| [(.Timestamp | ts_to_epoch + 9 * 3600 | strftime("%Y-%m-%d"))]
+ (if $rule == "" then [] else [$rule] end)
+ [$metric, (.Sum // 0)]
| @tsv
' >> "$1"
}
# ===== 認証処理 =====
cache_credentials() {
local profile="$1"
local creds
# export-credentials 1回だけで認証チェックとキャッシュを同時に行う
if creds="$(
aws configure export-credentials \
--profile "$profile" \
--format env 2>/dev/null
)" && [ -n "$creds" ]; then
eval "$creds"
echo "認証情報をキャッシュしました"
return 0
fi
# 失敗した場合のみ SSO login を試行
echo "認証が無効です。aws sso login を試みます..."
if ! aws sso login --profile "$profile"; then
echo "ログインに失敗しました。" >&2
return 1
fi
echo "ログインに成功しました。再取得中..."
# 再取得
if creds="$(
aws configure export-credentials \
--profile "$profile" \
--format env 2>/dev/null
)" && [ -n "$creds" ]; then
eval "$creds"
echo "認証情報をキャッシュしました"
return 0
fi
echo "認証情報の取得に失敗しました。" >&2
return 1
}
# 認証情報のキャッシュ
echo "AWS profile: ${PROFILE}"
cache_credentials "$PROFILE" ||
die "AWS authentication failed for profile: $PROFILE"
echo "Resolving CloudFront distribution..." >&2
aws cloudfront list-distributions \
--profile "$PROFILE" \
--output json |
jq --arg target "$DISTRIBUTION" '
[
.DistributionList.Items[]?
| select(
.Id == $target
or .DomainName == $target
or .Comment == $target
or ((.Aliases.Items // []) | index($target) != null)
)
]
' > "$DISTRIBUTION_JSON"
DISTRIBUTION_COUNT="$(jq 'length' "$DISTRIBUTION_JSON")"
[ "$DISTRIBUTION_COUNT" -gt 0 ] ||
die "CloudFront distribution was not found: $DISTRIBUTION"
if [ "$DISTRIBUTION_COUNT" -gt 1 ]; then
echo "The value matched multiple distributions:" >&2
jq -r '.[] | " \(.Id) \(.DomainName) Comment=\(.Comment)"' \
"$DISTRIBUTION_JSON" >&2
die "Specify the Distribution ID instead."
fi
DISTRIBUTION_ID="$(jq -r '.[0].Id' "$DISTRIBUTION_JSON")"
DISTRIBUTION_DOMAIN="$(jq -r '.[0].DomainName' "$DISTRIBUTION_JSON")"
REPORT_CSV="cloudfront_waf_daily_report_${DISTRIBUTION_ID}_${END_DATE}.csv"
WEB_ACL_ARN="$(
aws cloudfront get-distribution-config \
--profile "$PROFILE" \
--id "$DISTRIBUTION_ID" \
--output json |
jq -r '.DistributionConfig.WebACLId // empty'
)"
[ -n "$WEB_ACL_ARN" ] ||
die "No Web ACL is associated with distribution $DISTRIBUTION_ID."
case "$WEB_ACL_ARN" in
arn:*:wafv2:*:global/webacl/*/*) ;;
*) die "The associated Web ACL is not a CloudFront-scoped WAFv2 Web ACL: $WEB_ACL_ARN" ;;
esac
# ARN末尾は ... /webacl / <Name> / <Id>
WEB_ACL_ID="${WEB_ACL_ARN##*/}"
WEB_ACL_NAME="${WEB_ACL_ARN%/*}"
WEB_ACL_NAME="${WEB_ACL_NAME##*/}"
echo "Loading WAFv2 Web ACL..." >&2
aws wafv2 get-web-acl \
--profile "$PROFILE" \
--region us-east-1 \
--scope CLOUDFRONT \
--name "$WEB_ACL_NAME" \
--id "$WEB_ACL_ID" \
--output json > "$WEB_ACL_JSON"
WEB_ACL_METRIC_NAME="$(
jq -r '.WebACL.VisibilityConfig.MetricName // empty' "$WEB_ACL_JSON"
)"
WEB_ACL_METRICS_ENABLED="$(
jq -r '.WebACL.VisibilityConfig.CloudWatchMetricsEnabled // false' \
"$WEB_ACL_JSON"
)"
DEFAULT_ACTION_TYPE="$(
jq -r '.WebACL.DefaultAction | keys[0] | ascii_upcase' "$WEB_ACL_JSON"
)"
case "$DEFAULT_ACTION_TYPE" in
ALLOW) DEFAULT_ACTION_METRIC_NAME="AllowedRequests" ;;
BLOCK) DEFAULT_ACTION_METRIC_NAME="BlockedRequests" ;;
*) die "Unsupported Web ACL DefaultAction: $DEFAULT_ACTION_TYPE" ;;
esac
[ -n "$WEB_ACL_METRIC_NAME" ] ||
die "Web ACL VisibilityConfig.MetricName was not found."
# トップレベルルール一覧
jq -r '
[
"priority",
"rule_name",
"rule_metric_name",
"rule_type",
"configured_action",
"metrics_enabled"
],
(
.WebACL.Rules
| sort_by(.Priority)[]
| [
(.Priority | tostring),
.Name,
.VisibilityConfig.MetricName,
(
if .Statement.ManagedRuleGroupStatement then "ManagedRuleGroup"
elif .Statement.RuleGroupReferenceStatement then "RuleGroupReference"
else "DirectRule"
end
),
(
if .Action != null then (.Action | keys[0] | ascii_upcase)
elif .OverrideAction != null then
if (.OverrideAction | has("Count")) then "OVERRIDE_COUNT"
else "RULE_GROUP"
end
else "-"
end
),
(.VisibilityConfig.CloudWatchMetricsEnabled | tostring)
]
)
| @tsv
' "$WEB_ACL_JSON" > "$RULES_TSV"
RULE_COUNT="$(jq '.WebACL.Rules | length' "$WEB_ACL_JSON")"
[ "$RULE_COUNT" -gt 0 ] ||
die "The Web ACL has no top-level rules: $WEB_ACL_NAME"
printf 'report_date\n' > "$DAYS_TSV"
jq -nr --argjson epoch "$END_DATE_EPOCH" '
range(0; 7) | $epoch + ((. - 6) * 86400) | strftime("%Y-%m-%d")
' >> "$DAYS_TSV"
printf 'report_date\trule_name\tmetric_name\tmetric_value\n' \
> "$RULE_METRICS_TSV"
printf 'report_date\tmetric_name\tmetric_value\n' \
> "$DAILY_METRICS_TSV"
count_all_rule_datapoints() {
local candidate="$1" metric points total=0
for metric in CountedRequests BlockedRequests AllowedRequests; do
points="$(waf_get_sum "$metric" "$candidate" ALL | datapoint_count)"
total=$((total + points))
done
echo "$total"
}
echo "Detecting CloudWatch WebACL dimension..." >&2
METRIC_NAME_POINT_COUNT="$(count_all_rule_datapoints "$WEB_ACL_METRIC_NAME")"
if [ "$WEB_ACL_NAME" = "$WEB_ACL_METRIC_NAME" ]; then
WEB_ACL_NAME_POINT_COUNT="$METRIC_NAME_POINT_COUNT"
else
WEB_ACL_NAME_POINT_COUNT="$(count_all_rule_datapoints "$WEB_ACL_NAME")"
fi
if [ "$METRIC_NAME_POINT_COUNT" -gt 0 ] &&
[ "$METRIC_NAME_POINT_COUNT" -ge "$WEB_ACL_NAME_POINT_COUNT" ]; then
WEB_ACL_CW_DIMENSION="$WEB_ACL_METRIC_NAME"
elif [ "$WEB_ACL_NAME_POINT_COUNT" -gt 0 ]; then
WEB_ACL_CW_DIMENSION="$WEB_ACL_NAME"
else
# 対象期間にデータポイントが無い場合はメトリクス定義の存在で判定する。
WEB_ACL_CW_DIMENSION="$(
aws cloudwatch list-metrics \
--profile "$PROFILE" \
--region us-east-1 \
--namespace AWS/WAFV2 \
--dimensions "Name=Rule,Value=ALL" \
--output json |
jq -r \
--arg metric_name "$WEB_ACL_METRIC_NAME" \
--arg acl_name "$WEB_ACL_NAME" '
def has_webacl($candidate):
any(
.Metrics[]?;
(.Dimensions | map(.Name) | sort) == ["Rule", "WebACL"]
and any(
.Dimensions[];
.Name == "WebACL" and .Value == $candidate
)
);
if has_webacl($metric_name) then $metric_name
elif has_webacl($acl_name) then $acl_name
else empty
end
'
)"
[ -n "$WEB_ACL_CW_DIMENSION" ] ||
die "Could not detect the CloudWatch WebACL dimension."
fi
echo "Web ACL name: $WEB_ACL_NAME" >&2
echo "Web ACL configured MetricName: $WEB_ACL_METRIC_NAME" >&2
echo "CloudWatch WebACL dimension: $WEB_ACL_CW_DIMENSION" >&2
echo "Web ACL Default Action: $DEFAULT_ACTION_TYPE" >&2
echo "Default Action metric: $DEFAULT_ACTION_METRIC_NAME" >&2
echo "Detecting Default Action Rule dimension..." >&2
# Default ActionのRuleディメンションは、設定中のDefault Actionに対応する
# メトリクスだけを使って判定する。
DEFAULT_ACTION_POINT_COUNT="$(
waf_get_sum "$DEFAULT_ACTION_METRIC_NAME" "$WEB_ACL_CW_DIMENSION" \
"Default_Action" | datapoint_count
)"
WEB_ACL_METRIC_POINT_COUNT="$(
waf_get_sum "$DEFAULT_ACTION_METRIC_NAME" "$WEB_ACL_CW_DIMENSION" \
"$WEB_ACL_METRIC_NAME" | datapoint_count
)"
echo "Default Action candidate datapoints:" >&2
echo " Rule=Default_Action: $DEFAULT_ACTION_POINT_COUNT" >&2
echo " Rule=$WEB_ACL_METRIC_NAME: $WEB_ACL_METRIC_POINT_COUNT" >&2
if [ "$DEFAULT_ACTION_POINT_COUNT" -gt 0 ]; then
DEFAULT_ACTION_RULE_DIMENSION="Default_Action"
elif [ "$WEB_ACL_METRIC_POINT_COUNT" -gt 0 ]; then
DEFAULT_ACTION_RULE_DIMENSION="$WEB_ACL_METRIC_NAME"
else
# 対象期間にDefault Actionが0件の場合はAWS標準値へフォールバック。
DEFAULT_ACTION_RULE_DIMENSION="Default_Action"
echo "WARNING: No Default Action datapoints were found; falling back to Rule=Default_Action." >&2
fi
echo "Default Action Rule dimension: $DEFAULT_ACTION_RULE_DIMENSION" >&2
echo "Getting rule metrics for $START_DATE through $END_DATE JST..." >&2
tail -n +2 "$RULES_TSV" |
cut -f2,3,6 |
while IFS="$(printf '\t')" read -r \
RULE_NAME RULE_METRIC_NAME METRICS_ENABLED
do
if [ "$METRICS_ENABLED" != "true" ]; then
echo "Skipping disabled rule metric: $RULE_NAME" >&2
continue
fi
for METRIC_NAME in CountedRequests BlockedRequests AllowedRequests; do
echo " Rule: $RULE_NAME / $METRIC_NAME" >&2
waf_get_sum "$METRIC_NAME" "$WEB_ACL_CW_DIMENSION" "$RULE_METRIC_NAME" |
append_datapoints "$RULE_METRICS_TSV" "$METRIC_NAME" "$RULE_NAME"
done
done
echo "Getting CloudFront Requests..." >&2
cw_get_sum AWS/CloudFront Requests \
"Name=DistributionId,Value=$DISTRIBUTION_ID" \
"Name=Region,Value=Global" |
append_datapoints "$DAILY_METRICS_TSV" "CloudFrontRequests"
echo "Getting WAF daily totals with Rule=ALL..." >&2
for MAPPING in \
CountedRequests:WafCountMatches \
BlockedRequests:WafBlockedRequests \
AllowedRequests:WafAllowedRequests
do
waf_get_sum "${MAPPING%%:*}" "$WEB_ACL_CW_DIMENSION" ALL |
append_datapoints "$DAILY_METRICS_TSV" "${MAPPING#*:}"
done
# Default ActionはAllowとBlockを別々の列へ保存する。
if [ "$WEB_ACL_METRICS_ENABLED" = "true" ]; then
for MAPPING in \
AllowedRequests:WafDefaultAllowedRequests \
BlockedRequests:WafDefaultBlockedRequests
do
echo "Getting WAF Default Action / ${MAPPING%%:*}..." >&2
waf_get_sum "${MAPPING%%:*}" "$WEB_ACL_CW_DIMENSION" \
"$DEFAULT_ACTION_RULE_DIMENSION" |
append_datapoints "$DAILY_METRICS_TSV" "${MAPPING#*:}"
done
else
echo "Skipping WAF Default Action: Web ACL metrics are disabled." >&2
fi
echo "Creating report..." >&2
duckdb "$REPORT_DB" >/dev/null <<SQL
CREATE MACRO tsv(p) AS TABLE
SELECT * FROM read_csv(p, delim = '\t', header = true, all_varchar = true);
CREATE MACRO fmt_count(v) AS
CAST(CAST(ROUND(COALESCE(v, 0)) AS BIGINT) AS VARCHAR);
CREATE TABLE final_report AS
WITH
rule_totals AS (
SELECT * FROM (
PIVOT tsv('$RULE_METRICS_TSV')
ON metric_name IN ('CountedRequests', 'BlockedRequests', 'AllowedRequests')
USING SUM(TRY_CAST(metric_value AS DOUBLE))
GROUP BY report_date, rule_name
)
),
daily_totals AS (
SELECT * FROM (
PIVOT tsv('$DAILY_METRICS_TSV')
ON metric_name IN (
'CloudFrontRequests',
'WafCountMatches',
'WafBlockedRequests',
'WafAllowedRequests',
'WafDefaultAllowedRequests',
'WafDefaultBlockedRequests'
)
USING SUM(TRY_CAST(metric_value AS DOUBLE))
GROUP BY report_date
)
)
SELECT
d.report_date AS "Date (JST)",
TRY_CAST(r.priority AS INTEGER) AS "Priority",
r.rule_name AS "Rule",
r.rule_metric_name AS "MetricName",
r.rule_type AS "RuleType",
r.configured_action AS "ConfiguredAction",
CASE WHEN r.metrics_enabled <> 'true' THEN '-'
ELSE fmt_count(rt.CountedRequests) END AS "Count",
CASE WHEN r.metrics_enabled <> 'true' THEN '-'
ELSE fmt_count(rt.BlockedRequests) END AS "Block",
CASE WHEN r.metrics_enabled <> 'true' THEN '-'
ELSE fmt_count(rt.AllowedRequests) END AS "Allow",
fmt_count(dm.CloudFrontRequests) AS "CloudFrontRequests",
fmt_count(dm.WafCountMatches) AS "WafCountMatches",
fmt_count(dm.WafBlockedRequests) AS "WafBlockedRequests",
fmt_count(dm.WafAllowedRequests) AS "WafAllowedRequests",
'$DEFAULT_ACTION_TYPE' AS "WafDefaultAction",
CASE WHEN '$WEB_ACL_METRICS_ENABLED' <> 'true' THEN '-'
ELSE fmt_count(dm.WafDefaultAllowedRequests) END
AS "WafDefaultAllowedRequests",
CASE WHEN '$WEB_ACL_METRICS_ENABLED' <> 'true' THEN '-'
ELSE fmt_count(dm.WafDefaultBlockedRequests) END
AS "WafDefaultBlockedRequests"
FROM tsv('$DAYS_TSV') AS d
CROSS JOIN tsv('$RULES_TSV') AS r
LEFT JOIN rule_totals AS rt
ON rt.report_date = d.report_date
AND rt.rule_name = r.rule_name
LEFT JOIN daily_totals AS dm
ON dm.report_date = d.report_date;
-- CSVにはMetricNameとRuleTypeを含める。
COPY (
SELECT *
FROM final_report
ORDER BY "Priority" ASC NULLS LAST, "Rule" ASC, "Date (JST)" ASC
)
TO '$REPORT_CSV'
WITH (FORMAT CSV, HEADER);
SQL
echo "# Distribution: $DISTRIBUTION_ID ($DISTRIBUTION_DOMAIN)" >&2
echo "# WebACL: $WEB_ACL_NAME" >&2
echo "# Period (JST): $START_DATE - $END_DATE" >&2
echo "# CloudWatch UTC range: $START_UTC - $END_UTC (end exclusive)" >&2
echo "# CloudWatch WebACL dimension: $WEB_ACL_CW_DIMENSION" >&2
echo "# Default Action: $DEFAULT_ACTION_TYPE" >&2
echo "# Default Action Rule dimension: $DEFAULT_ACTION_RULE_DIMENSION" >&2
echo "# CSV report: $(pwd)/$REPORT_CSV" >&2
cat <<'EOF'
## 集計値に関する注意事項
- `CloudFrontRequests` は対象CloudFront DistributionへのViewer Request総数です。
- `Count` は非終端アクションです。同じリクエストが複数のCountや、後続のAllow/Block/Default Actionに重複して含まれる場合があります。
- `WafCountMatches`、`WafBlockedRequests`、`WafAllowedRequests` は、CloudWatchの `Rule=ALL` から取得した日別集計値です。
- `WafDefaultAction` はWeb ACLに設定されているDefault Action種別です。
- `WafDefaultAllowedRequests` と `WafDefaultBlockedRequests` は、Default ActionのAllow/Blockを分けて表示します。
- CAPTCHA/Challenge/Monetizeは現在の日別集計列には含まれていません。
- Managed Rule Group内部のCountは、トップレベルルールの `Count` で完全に網羅できない場合があります。
- 日別集計列は同じ日付の各ルール行に繰り返し格納されます。CSV全行で単純合計しないでください。
- WAF集計値の合計は、重複計上や未算入アクションにより `CloudFrontRequests` と一致するとは限りません。
## 日別・ルール別レポート
ソート順: `Priority` 昇順 → `Rule` 昇順 → `Date (JST)` 昇順
EOF
# Markdownでは横幅を抑えるため、MetricNameとRuleTypeを省略し、
# 日別集計列には短縮名を使用する。
duckdb "$REPORT_DB" -markdown -c "
SELECT
\"Date (JST)\",
\"Priority\",
\"Rule\",
\"ConfiguredAction\" AS \"Action\",
\"Count\",
\"Block\",
\"Allow\",
\"CloudFrontRequests\" AS \"CFReqs\",
\"WafCountMatches\" AS \"WafCount\",
\"WafBlockedRequests\" AS \"WafBlockedReqs\",
\"WafAllowedRequests\" AS \"WafAllowedReqs\",
\"WafDefaultAction\" AS \"WafDefAction\",
\"WafDefaultAllowedRequests\" AS \"WafDefAllowReqs\",
\"WafDefaultBlockedRequests\" AS \"WafDefBlockReqs\"
FROM final_report
ORDER BY \"Priority\" ASC NULLS LAST, \"Rule\" ASC, \"Date (JST)\" ASC;
"
4. 実行イメージ
$ bash cloudfront_waf_daily_metrics.sh -p dev-dummy-profile -d ABC123DEFGHIJK -e 2026-07-27
Resolving CloudFront distribution...
Loading WAFv2 Web ACL...
Detecting CloudWatch WebACL dimension...
Web ACL name: dummy-dev-shop-wacl
Web ACL configured MetricName: dummy-dev-shop-wacl
CloudWatch WebACL dimension: dummy-dev-shop-wacl
Web ACL Default Action: BLOCK
Default Action metric: BlockedRequests
Detecting Default Action Rule dimension...
Default Action candidate datapoints:
Rule=Default_Action: 0
Rule=dummy-dev-shop-wacl: 0
WARNING: No Default Action datapoints were found; falling back to Rule=Default_Action.
Default Action Rule dimension: Default_Action
Getting rule metrics for 2026-07-21 through 2026-07-27 JST...
Rule: Api-Block / CountedRequests
Rule: Api-Block / BlockedRequests
Rule: Api-Block / AllowedRequests
Rule: BasicAuthRule / CountedRequests
Rule: BasicAuthRule / BlockedRequests
Rule: BasicAuthRule / AllowedRequests
Rule: BlockGetHeaderRule / CountedRequests
Rule: BlockGetHeaderRule / BlockedRequests
Rule: BlockGetHeaderRule / AllowedRequests
Rule: BlockPostHeaderRule / CountedRequests
Rule: BlockPostHeaderRule / BlockedRequests
Rule: BlockPostHeaderRule / AllowedRequests
Rule: DevShopForMaintenanceRule / CountedRequests
Rule: DevShopForMaintenanceRule / BlockedRequests
Rule: DevShopForMaintenanceRule / AllowedRequests
Rule: AllowUploadsURIRule / CountedRequests
Rule: AllowUploadsURIRule / BlockedRequests
Rule: AllowUploadsURIRule / AllowedRequests
Rule: AWS-AWSManagedRulesKnownBadInputsRuleSet / CountedRequests
Rule: AWS-AWSManagedRulesKnownBadInputsRuleSet / BlockedRequests
Rule: AWS-AWSManagedRulesKnownBadInputsRuleSet / AllowedRequests
Rule: AWS-AWSManagedRulesAdminProtectionRuleSet / CountedRequests
Rule: AWS-AWSManagedRulesAdminProtectionRuleSet / BlockedRequests
Rule: AWS-AWSManagedRulesAdminProtectionRuleSet / AllowedRequests
Rule: DevShopAllowIpSetRule / CountedRequests
Rule: DevShopAllowIpSetRule / BlockedRequests
Rule: DevShopAllowIpSetRule / AllowedRequests
Getting CloudFront Requests...
Getting WAF daily totals with Rule=ALL...
Getting WAF Default Action / AllowedRequests...
Getting WAF Default Action / BlockedRequests...
Creating report...
# Distribution: ABC123DEFGHIJK (xxxxxxxxxxxxxx[.]cloudfront[.]net)
# WebACL: dummy-dev-shop-wacl
# Period (JST): 2026-07-21 - 2026-07-27
# CloudWatch UTC range: 2026-07-20T15:00:00Z - 2026-07-27T15:00:00Z (end exclusive)
# CloudWatch WebACL dimension: dummy-dev-shop-wacl
# Default Action: BLOCK
# Default Action Rule dimension: Default_Action
# CSV report: /home/bloguser/cloudfront_waf_daily_report_ABC123DEFGHIJK_2026-07-27.csv
## 集計値に関する注意事項
- `CloudFrontRequests` は対象CloudFront DistributionへのViewer Request総数です。
- `Count` は非終端アクションです。同じリクエストが複数のCountや、後続のAllow/Block/Default Actionに重複して含まれる場合があります。
- `WafCountMatches`、`WafBlockedRequests`、`WafAllowedRequests` は、CloudWatchの `Rule=ALL` から取得した日別集計値です。
- `WafDefaultAction` はWeb ACLに設定されているDefault Action種別です。
- `WafDefaultAllowedRequests` と `WafDefaultBlockedRequests` は、Default ActionのAllow/Blockを分けて表示します。
- CAPTCHA/Challenge/Monetizeは現在の日別集計列には含まれていません。
- Managed Rule Group内部のCountは、トップレベルルールの `Count` で完全に網羅できない場合があります。
- 日別集計列は同じ日付の各ルール行に繰り返し格納されます。CSV全行で単純合計しないでください。
- WAF集計値の合計は、重複計上や未算入アクションにより `CloudFrontRequests` と一致するとは限りません。
## 日別・ルール別レポート
ソート順: `Priority` 昇順 → `Rule` 昇順 → `Date (JST)` 昇順
| Date(JST) | Priority | Rule | Action | Count | Block | Allow | CFReqs | WafCount | WafBlockedReqs | WafAllowedReqs | WafDefAction | WafDefAllowReqs | WafDefBlockReqs |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 2026-07-21 | 100 | Api-Block | BLOCK | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 100 | Api-Block | BLOCK | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 100 | Api-Block | BLOCK | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 100 | Api-Block | BLOCK | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 100 | Api-Block | BLOCK | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 100 | Api-Block | BLOCK | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 100 | Api-Block | BLOCK | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 200 | BasicAuthRule | RULE_GROUP | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 200 | BasicAuthRule | RULE_GROUP | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 200 | BasicAuthRule | RULE_GROUP | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 200 | BasicAuthRule | RULE_GROUP | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 200 | BasicAuthRule | RULE_GROUP | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 200 | BasicAuthRule | RULE_GROUP | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 200 | BasicAuthRule | RULE_GROUP | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 300 | BlockGetHeaderRule | BLOCK | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 300 | BlockGetHeaderRule | BLOCK | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 300 | BlockGetHeaderRule | BLOCK | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 300 | BlockGetHeaderRule | BLOCK | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 300 | BlockGetHeaderRule | BLOCK | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 300 | BlockGetHeaderRule | BLOCK | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 300 | BlockGetHeaderRule | BLOCK | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 400 | BlockPostHeaderRule | BLOCK | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 400 | BlockPostHeaderRule | BLOCK | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 400 | BlockPostHeaderRule | BLOCK | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 400 | BlockPostHeaderRule | BLOCK | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 400 | BlockPostHeaderRule | BLOCK | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 400 | BlockPostHeaderRule | BLOCK | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 400 | BlockPostHeaderRule | BLOCK | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 500 | DevShopForMaintenanceRule | RULE_GROUP | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 600 | AllowUploadsURIRule | COUNT | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 600 | AllowUploadsURIRule | COUNT | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 600 | AllowUploadsURIRule | COUNT | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 600 | AllowUploadsURIRule | COUNT | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 600 | AllowUploadsURIRule | COUNT | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 600 | AllowUploadsURIRule | COUNT | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 600 | AllowUploadsURIRule | COUNT | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 700 | AWS-AWSManagedRulesKnownBadInputsRuleSet | RULE_GROUP | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 111111 | 0 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 222222 | 0 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 333333 | 0 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 444444 | 0 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 555555 | 0 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 666666 | 0 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 800 | AWS-AWSManagedRulesAdminProtectionRuleSet | RULE_GROUP | 0 | 777777 | 0 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
| 2026-07-21 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 111111 | 123456 | 234567 | 345678 | 111111 | BLOCK | 0 | 0 |
| 2026-07-22 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 222222 | 123456 | 234567 | 345678 | 222222 | BLOCK | 0 | 0 |
| 2026-07-23 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 333333 | 123456 | 234567 | 345678 | 333333 | BLOCK | 0 | 0 |
| 2026-07-24 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 444444 | 123456 | 234567 | 345678 | 444444 | BLOCK | 0 | 0 |
| 2026-07-25 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 555555 | 123456 | 234567 | 345678 | 555555 | BLOCK | 0 | 0 |
| 2026-07-26 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 666666 | 123456 | 234567 | 345678 | 666666 | BLOCK | 0 | 0 |
| 2026-07-27 | 900 | DevShopAllowIpSetRule | ALLOW | 0 | 0 | 777777 | 123456 | 234567 | 345678 | 777777 | BLOCK | 0 | 0 |
5. 最後に
DevOps Agentに調査やレポート作成を任せられる場面も増えてきましたが、
今回のScriptはそのような場合でも人間側での裏付け等に利用できたらと考えております。
このブログがどなたかの役に立てば幸いです。
クラスメソッドオペレーションズ株式会社について
クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026年1月 アノテーション㈱から社名変更しました。





