🚨

SnowAlertからSlackに通知を飛ばせるらしいじゃないですか

2021/12/23に公開

はじめに

2021年12月22日、深夜45時になりました。こちらは Snowflake Advent Calendar 2021 22日目の記事になります。
Snowflakeのセキュリティ分析フレームワークであるSnowAlertから、Slackに通知を飛ばしてみるお話です。

背景

SnowAlertからのアラート通知先はJiraしかないのかと思いきや、何やらSlackにも通知できるらしいじゃないですか。
個人的には、仕事や私用でSlackのほうが使い慣れているので、Slackに通知できると非常にうれしい!
というわけで早速使ってみたいと思います。

なお、SnowAlertの簡単な使い方については、所属会社のブログで書いております。
手前味噌ですがよろしければ...

https://datumstudio.jp/blog/1215_snowflake_17/

やってみる

こちらの記事を参考に、やってみます。
https://community.snowflake.com/s/article/PoP-Data-from-SnowAlert-to-Slack

事前に

次を準備してください。

Slack APIトークン設定

SnowAlertの設定ファイル snowflake-<account>.envsに、SlackアプリのOAuthトークンを追加します。
文字列xxxx-XXXX...は、取得したSlack APIトークンに置き換えてください。

SLACK_API_TOKEN=xxxx-XXXXXXXXXXX-XXXXXXXXXXXX-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

SnowAlert アラート作成

次に、アラート用のルールクエリを作成します。
今回は、こちらを参考に「一定時間以上実行されている抽出クエリがあったら通知する」というルールを作ってみます。
とりあえず通知が飛んでくるところが見たいので、時間は10秒にします。[1]

create or replace view snowalert.rules.snowflake_long_running_query_alert_query copy grants
  comment='Queries running for a long time'
as
select
  'Queries running for a long time' as title
  , array_construct('query_history') as sources 
  , 'Snowflake Query' as object
  , 'SnowAlert' as environment
  , current_timestamp() as event_time
  , current_timestamp() as alert_time
  , 'User ' || user_name || ' select ran for ' || total_elapsed_time as description
  , user_name as actor
  , object_construct(*) as event_data
  , 'Medium' as severity
  , 'submitted long running query' as action
  , 'long_running_query' as query_name
  , array_construct(
      object_construct(
        'type', 'slack',
        'channel', 'snowalert-notification',
        'template','rules.long_running_query_template'
      )
    ) as handlers
  , 'SnowAlert' as detector
  , query_id as query_id
from
  -- クエリ履歴ビュー
  snowflake.account_usage.query_history
where 
   -- 直近1時間の間に10s以上かかったselectクエリを抽出
  query_text like 'select%'
  and total_elapsed_time > 10000
  and datediff(minute, end_time, current_timestamp()) <= 3600
;

grant all privileges on view snowalert.rules.snowflake_long_running_query_alert_query to role snowalert;

クエリが正常に実行されたら、handlers で指定されている謎のテンプレ rules.long_running_query_template を作成していきます。
テンプレの正体はfunctionで、このfunctionでSlackに投稿するメッセージを生成します。

SlackのBlock kit builderでメッセージの書式を作っていきます。

今回作ったものはこちら
{
	"blocks": [
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*<alert.title>*"
			}
		},
		{
			"type": "divider"
		},
		{
			"type": "section",
			"fields": [
				{
					"type": "plain_text",
					"text": "Severity"
				},
				{
					"type": "mrkdwn",
					"text": "<severity>"
				},
				{
					"type": "plain_text",
					"text": "Total elapsed time"
				},
				{
					"type": "mrkdwn",
					"text": "<event_data.total_elapsed_time>"
				},
				{
					"type": "plain_text",
					"text": "Query ID"
				},
				{
					"type": "mrkdwn",
					"text": "<event_data.query_id>"
				},
				{
					"type": "plain_text",
					"text": "User"
				},
				{
					"type": "mrkdwn",
					"text": "<event_data.user_name>"
				},
				{
					"type": "plain_text",
					"text": "Warehouse"
				},
				{
					"type": "mrkdwn",
					"text": "<event_data.warehouse_name>"
				}
			]
		},
		{
			"type": "divider"
		},
		{
			"type": "context",
			"elements": [
				{
					"type": "mrkdwn",
					"text": "Credits used: <event_data.credits_used_cloud_services>\nQuery:\n<event_data.query>"
				}
			]
		}
	]
}

blocks の中身を template.blocks に書くと、そのフォーマットでメッセージが投稿されます。
というわけで、今回使用するテンプレートがこちら。

create or replace function snowalert.rules.long_running_query_template(v object)
  returns object
  language javascript
  as
  $$
function template(vars) {
    const alert = vars.alert;
    const event_data = alert.EVENT_DATA; // 上のevent_dataカラムと同じ

    let severity = alert.SEVERITY;
    // 優先度に合わせて絵文字を追加する
    if (severity === "Medium") severity = severity + "⚠️";
    else if (severity === "High" || severity === "Critical") severity = severity + "💀";

    // クエリが長いとき(40文字以上)、表示を省略する
    const query_len_limit = 40;
    let query = event_data.QUERY_TEXT;
    if (query && query.length >= query_len_limit) query = `${query.substr(0, query_len_limit)}...`;
    
    let template = {
        "blocks": {},
        "attachments": {},
        "text": alert.TITLE
    };
    template.blocks = [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": `*${alert.TITLE}*`
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "plain_text",
                    "text": "Severity"
                },
                {
                    "type": "mrkdwn",
                    "text": `${severity}`
                },
                {
                    "type": "plain_text",
                    "text": "Total elapsed time"
                },
                {
                    "type": "mrkdwn",
                    "text": `${event_data.TOTAL_ELAPSED_TIME/1000}(s)`
                },
                {
                    "type": "plain_text",
                    "text": "Query ID"
                },
                {
                    "type": "mrkdwn",
                    "text": `${event_data.QUERY_ID}`
                },
                {
                    "type": "plain_text",
                    "text": "User"
                },
                {
                    "type": "mrkdwn",
                    "text": `${event_data.USER_NAME}`
                },
                {
                    "type": "plain_text",
                    "text": "Warehouse"
                },
                {
                    "type": "mrkdwn",
                    "text": `${event_data.WAREHOUSE_NAME}`
                }
            ]
        },
        {
            "type": "divider"
        },
        {
            "type": "context",
            "elements": [
                {
                    "type": "mrkdwn",
                    "text": `Credits used: ${event_data.CREDITS_USED_CLOUD_SERVICES}\nQuery:\n${query}`
                }
            ]
        }
    ];
    return template;
}
return template(V);
  $$
;

-- 権限付与。functionを再作成したら毎回実行する(1敗)
grant all privileges on function snowalert.rules.long_running_query_template(object) to role snowalert;

これでアラート作成は完了です。

実行

SnowAlertを実行する前に、わざとアラートとして検出されるようなクエリを実行しておきます。

では、実行してみましょう。
いま仕掛けたアラートが動いてくれればよいので、alert だけ実行してみます。

$ docker run --env-file snowalert-<account>.envs snowsec/snowalert ./run alert
running in docker container-like environment
[7] Loaded 6 views, 5 were 'ALERT_QUERY' rules.
[24] SNOWFLAKE_LONG_RUNNING_QUERY_ALERT_QUERY processing...
[24] SNOWFLAKE_LONG_RUNNING_QUERY_ALERT_QUERY created 1, updated 0 rows.
[24] SNOWFLAKE_LONG_RUNNING_QUERY_ALERT_QUERY metadata recorded.
[24] SNOWFLAKE_LONG_RUNNING_QUERY_ALERT_QUERY done.
[23] SNOWFLAKE_RESOURCE_CREATION_ALERT_QUERY processing...
:(中略)
[7] RUN metadata recorded.
[7] Loaded 6 views, 1 were 'ALERT_SUPPRESSION' rules.
:(中略)
[7] RUN metadata recorded.
[7] the correlation id for alert 5ddada38-490a-4902-92d3-cc8ae3420450 is 4dcc83a30acd4316b9cfc57757a8747b
[7] correlation id successfully updated
[7] Found 1 new alerts to handle.
[7] Creating new SLACK message for Queries running for a long time in channel
[7] snowalert-notification

引っかかってくれたようです。Slackを確認します。

ちゃんと通知が飛んでますね!

さいごに

SnowAlertでSnowflake環境をセキュアにしていきましょう💪

脚注
  1. 実際に使用する場合のしきい値は、対象環境で実行されるクエリに対して想定外の長さに設定したほうがよいかと思います。 ↩︎

Discussion