カウントモードでWafCharmレポートを生成してみた

2023.06.19

こんにちは。たかやまです。

WafCharm はWebに対する攻撃パターンをAIによって学習し、環境に応じたリクエストのパターン等を判別、AWS WAFのルールを最適化してくれるサービスとなります。

そんなWafCharmですが、どのような攻撃をされているか可視化するレポート機能があります。

引用元 : WafCharm月次レポート機能の紹介

こちらのレポート機能ですが、WafCharmをCountモードで運用している場合も生成されるか気になり試してみました。

さきにまとめ

  • Countモード運用時でもレポートは生成される
  • Countモード時は1リクエストで複数のルールにマッチすることがあり、Attack Country Ranking/Attack IP Rankingの検知数Attack Typeの検知数一致しないことがある

確認してみた

そもそもCountモードとは

Countモードはセキュリティルールに一致するリクエストをブロックせずに、一致したリクエストの数をカウントし、分析やルールのテストに役立てることができる設定モードです。

AWS WAFを本番運用する場合は、Countモードで運用し、ルールのテストや分析を行い、ルールの調整を行った後に、ブロックモードに切り替えるのが一般的になります。

WafCharmのCountモード設定はWafCharmコンソールのWeb ACL Configから設定できます。

WafCharmコンソールの設定はすべてのWafCharmが管理するすべてのルールグループに適用されるため、個別のルールごとにアクションを変更したい場合にはAWS WAFのコンソールから設定する必要があります。

WafCharmレポート機能の設定

WafCharmのレポート機能の設定はこちらのブログを参考にしてください。
今回の検証ではログ出力パターンで「S3直接出力」しているログレポート機能を実装しています。

Countモードでのレポート生成

今回はWafCharmをCountモードで設定し、GMOサイバーセキュリティ脆弱性診断をおこなってみました。

結果としてWafCharmレポートはCountモードでも問題なく生成されます*1
1レポートは翌月の月初に閲覧可能になります。※検知がない場合はレポートは作成されません。

こちらが実際のWafCharmレポートです。

各項目は以下のような内容になっています。

  • Attack Type : 攻撃種別ごとの分類
  • Detected Rule Ranking : WafCharmが展開したルールで検知した上位Top10
  • Attack Country Ranking : 攻撃元国ごとの上位Top10
  • Attack IP Ranking : 攻撃元IPごとの上位Top10

レポートはカーソルを合わせると各セグメントの検知数を確認することができます。

こちらのレポートをもとに「どの程度悪意のあるリクエスト」があったのだろうと確認しているとAttack Country Ranking/Attack IP Rankingの検知数Attack Typeの検知数一致しないことに気づきました。
(Attack Country Ranking/Attack IP Rankingの検知数はTop10なので、10件以上ある場合はレポートでリクエストの全数を把握することはできなくなります。)

てっきり全体としての数値は一致するものと思い見ていたので、これではどれがリクエスト数として正しいのかわからなくなりました。。

WAFログについて調べてみるとWafCharmさんのブログに以下の通り記載されていました。

※あまり多いパターンではないかと思いますが、COUNTは複数のルールでマッチしている可能性があります。COUNT の詳細を確認する必要がある場合は該当のログ内容を全て確認することが推奨です。

https://www.wafcharm.com/jp/blog/amazon-cloudwatch-logs-wafcharm-analysis-ja/

なるほど?Countモード時には1リクエストに対して複数のルールがマッチしている可能性があるとのことです。となるとAttack Country Ranking/Attack IP Rankingの検知数に比べてAttack Typeの検知数が多いのは複数のルールでマッチしている可能性が考えられます。

WAFログの確認

詳細なログ情報はWAFログを確認する必要があります。今回はS3に保存しているWAFログに対してAthenaを実行して確認してみたいと思います。

Athenaの事前準備はこちらのブログを参考にしてください。

Athenaでテーブル(ここではwaf_logs)を用意できたら以下のクエリを実行します。

SELECT from_unixtime(timestamp/1000, 'Asia/Tokyo') AS JST,
  rule.excludedrules,
  rule.rulegroupid
FROM "waf_logs"
CROSS JOIN UNNEST(rulegrouplist) AS t(rule)
WHERE rule.excludedrules IS NOT NULL

以下の結果はCountされたリクエストを一覧しています。

excludedrules*2にCountされたルールの内容が記録されますが、一部リクエストには複数のルールがマッチしていることがわかります。
2Countモードの設定方法によってはログがexcludedrulesではなくnonTerminatingMactingRulesに記録されます。詳細はこちら(AWS WAF のログ分析に関する考慮事項)

このクエリの検出数は333件で、WafCharmレポートのAttack Country Ranking/Attack IP Rankingの検出数と一致することがわかります。

クエリ結果
"JST","excludedrules","rulegroupid"
"2023-05-16 10:55:24.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:55:22.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:55:26.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:55:31.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:55:33.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 15:03:41.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 15:04:06.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XSS-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:27.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:26.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:28.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:25.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:27.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:26.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:25.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:26.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:30.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:26.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:28.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 11:08:29.000 Asia/Tokyo","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-qs-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
・・・

次に、excludedrules内でCountされたルールごとにリストしたいと思います。
以下のクエリはexcludedrulesをJSON配列として解析し、ruleidを抽出しています。

SELECT 
  from_unixtime(timestamp/1000, 'Asia/Tokyo') AS JST,
  json_extract_scalar(excludedrule_obj, '$.ruleid') AS ruleid,
  rule.excludedrules,
  rule.rulegroupid
FROM "waf_logs"
CROSS JOIN UNNEST(rulegrouplist) AS t(rule)
CROSS JOIN UNNEST(CAST(json_parse(rule.excludedrules) AS ARRAY(JSON))) AS u(excludedrule_obj)
WHERE rule.excludedrules IS NOT NULL

このクエリの検出数は353件で、WafCharmレポートのAttack Typeの検出数と一致することがわかります。

クエリ結果
"JST","ruleid","excludedrules","rulegroupid"
"2023-05-16 10:40:38.000 Asia/Tokyo","OScmdi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:40:34.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:40:34.000 Asia/Tokyo","XXE-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:40:41.000 Asia/Tokyo","OScmdi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:03.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:03.000 Asia/Tokyo","XXE-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:08.000 Asia/Tokyo","OScmdi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:06.000 Asia/Tokyo","OScmdi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:04.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:04.000 Asia/Tokyo","XXE-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""},{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""XXE-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:13.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:07.000 Asia/Tokyo","OScmdi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:09.000 Asia/Tokyo","OScmdi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""OScmdi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:12.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:11.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:24.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:22.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:23.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
"2023-05-16 10:46:33.000 Asia/Tokyo","SQLi-body-001","[{""rulematchdetails"":""null"",""exclusiontype"":""EXCLUDED_AS_COUNT"",""ruleid"":""SQLi-body-001""}]","arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/rulegroup/WafCharm_Common_Basic_Group/494dbf2c-ad20-4a3f-974f-733f4b989550"
・・・

まとめると、Attack Country Ranking/Attack IP Rankingがリクエスト数になり、Attack Typeが検出されたルール数になります。結果としてリクエスト数を確認する場合はAttack Country Ranking/Attack IP Rankingの検出数を確認することが正しいことがわかります。

(Appendix) WafCharmレポートで確認できないTop11以上の結果を取得する

WafCharmレポートのRankingで確認できる内容はTop10までの結果です。
もしTop11以上の結果を知りたい場合には、以下のようなクエリを実行することで取得することができます。

Detected Rule Ranking

SELECT 
  json_extract_scalar(excludedrule_obj, '$.ruleid') AS ruleid,
  COUNT(*) AS count_ruleid
FROM "waf_logs"
CROSS JOIN UNNEST(rulegrouplist) AS t(rule)
CROSS JOIN UNNEST(CAST(json_parse(rule.excludedrules) AS ARRAY(JSON))) AS u(excludedrule_obj)
WHERE rule.excludedrules IS NOT NULL
GROUP BY
  json_extract_scalar(excludedrule_obj, '$.ruleid')
ORDER BY
  count_ruleid DESC

Attack Country Ranking

SELECT 
  httprequest.country,
  COUNT(*) AS count_country
FROM "waf_logs"
CROSS JOIN UNNEST(rulegrouplist) AS t(rule)
WHERE rule.excludedrules IS NOT NULL
GROUP BY
  httprequest.country
ORDER BY 
  count_country DESC

※今回の検証では日本からのリクエストのみ

Attack IP Ranking

SELECT 
  httprequest.clientip,
  COUNT(*) AS count_clientip
FROM "waf_logs"
CROSS JOIN UNNEST(rulegrouplist) AS t(rule)
WHERE rule.excludedrules IS NOT NULL
GROUP BY
  httprequest.clientip
ORDER BY 
  count_clientip DESC

※今回の検証では単一IPからのリクエストのみ

最後に

最初はCountモードでレポートが生成されるかどうかを調べていましたが、調査過程でレポートの見方に関して若干ハマりポイント?があったので、今回それらをまとめてみることにしました。

こちらの記事がどなたかの助けになれば幸いです。

以上、たかやま(@nyan_kotaroo)でした。