Splunk は何をする道具か
Splunk を一言でいうと、バラバラの機器のログを1か所に集めて、まとめて検索できるようにする道具です。SIEM と呼ばれる製品の一種です。
1.1なぜ grep ではダメなのか
ログを読むだけなら grep で足ります。実際、1台のサーバの1本のログを見るなら、そのほうが速い。問題は調査が1台で終わらないことです。
攻撃者が Web サーバに侵入して、そこから内部の端末に横移動して、ドメインコントローラの管理者権限を取った。この流れを追うには、Web サーバのアクセスログ、端末のプロセス起動ログ、DC の認証ログを突き合わせる必要があります。形式も置き場所もバラバラの3種類を。
| grep でやると | Splunk でやると | |
|---|---|---|
| 複数機器 | 1台ずつ SSH して回る | 1回の検索で横断する |
| 形式の違い | ログごとに違う正規表現を書く | フィールド名で統一的に扱える |
| 時系列の突合 | 手で並べ替える | 時刻順に自動で混ざる |
| 集計 | awk や sort で自作 | コマンド1つ |
| ログの消失 | 侵入者に消される | 転送済みなので残る |
最後の行が地味に重要です。攻撃者は痕跡を消しますが、すでに転送されたログには手が届きません。ログを集約する理由の半分はこれです。
1.2取り込みから検索までの流れ
Splunk がやっていることは大きく2段階です。
切り出しルールを後から足しても、過去のログに遡って新しいフィールドが生えます。取り込み直しは不要です。「あのとき見落としていた項目を今から使いたい」が通るのは、この設計のおかげです。
画面の見方 — 6つの領域
SPL の文法より先に、画面のどこに何があるかです。ここを知らないまま本のクエリを写経しても、結果が出たのか失敗したのかすら分かりません。
使うのは Search & Reporting というアプリです。ログイン後の左メニューから開きます。
8/24 16:04:02 40.80.148.42 "POST /uploads/… " 200
8/24 16:11:20 40.80.148.42 "GET /uploads/… " 404
2.1フィールドサイドバーが最重要な理由
本や記事に載っているクエリをコピーしても動かないことがよくあります。原因のほとんどはフィールド名が自分の環境と違うことです。同じ Apache のログでも、環境によって uri だったり uri_path だったり url だったりします。
サイドバーには、いま検索で返ってきたイベントに実際に存在するフィールド名が並びます。ここに無い名前を書いても絶対に動きません。
| 表示 | 中身 |
|---|---|
| Selected Fields | 初期状態では host / source / sourcetype の3つ。表示したいものを自分で追加できる |
| Interesting Fields | 返ってきたイベントの2割以上に出現するフィールドが自動で並ぶ |
新しいログを触るときは、必ずこれを投げてサイドバーを眺めてください。フィールド名を目で確認してから本文を書き始めます。
index=web sourcetype=access_combined | head 20
2.2結果が Events に出るか Statistics に出るか
ここでつまずく人が多い。クエリの内容によって、結果が出るタブが変わります。
| クエリ | 結果が出る場所 |
|---|---|
index=web status=404 | Events タブ(生ログの一覧) |
… | stats count by uri | Statistics タブ(表) |
… | table _time, uri | Statistics タブ(表) |
… | timechart count | Statistics と Visualization |
「クエリを実行したのに何も出ない」と思ったら、Events タブが空なだけで Statistics タブに結果が入っていることがあります。この違いの正体は第9章で説明します。
データの住所 — index と sourcetype
Splunk には何種類ものログが同居しています。だから検索するときは「どのログを見るか」を指定します。その指定に使うのが4つのフィールドです。
| フィールド | 意味 | 例 |
|---|---|---|
| index | 保管庫の名前。ログの置き場所そのもの | web / security / botsv3 |
| sourcetype | ログの形式を表す名札。これでフィールドの切り出し方が決まる | access_combined / WinEventLog:Security |
| source | 取り込み元のパス | /var/log/apache/access.log |
| host | そのログを出した機器 | web01 / dc01 |
3.1index を書く癖をつける
index を書かないと、その利用者が読める全ての index を横断して検索します。データ量が多い環境では、これだけで検索が数分かかることがあります。
# 全部の保管庫を舐める sourcetype=access_combined
# 保管庫を指定する index=web sourcetype=access_combined
どんな index があるか分からないときは、これで一覧が出ます。
| eventcount summarize=false index=* | table index, count
あるいは、どの index にどんな種類のログが入っているかを一気に見るならこちら。新しい環境を渡されたら最初に投げるクエリです。
index=* | stats count by index, sourcetype | sort - count
上のクエリは全 index を舐めるので重いです。時間範囲を直近15分などに絞ってから実行してください。どんな種類のログがあるかを知るのが目的なので、期間は短くて構いません。
3.2sourcetype がフィールドを決める
第1章で「フィールドは検索時に切り出される」と書きました。どの切り出しルールを当てるかを決めているのが sourcetype です。
たとえば sourcetype=access_combined という名札が付いていると、Splunk は「これは Apache のアクセスログ形式だ」と判断して、あらかじめ用意されたルールを当てます。その結果、次のフィールドが自動で生えます。
| フィールド | 値の例 |
|---|---|
| clientip | 40.80.148.42 |
| method | GET |
| uri | /uploads/sp-client-document-manager/1.php |
| version | HTTP/1.1 |
| status | 200 |
| bytes | 1043 |
| useragent | Mozilla/5.0 … |
| referer | http://… |
パスは uri、プロトコルは version と最初から別のフィールドに分かれています。他のログ分析ツールの記事だと HTTP/1.1 を文字列置換で削る処理が出てきますが、Splunk では通常その処理が要りません。
イベントとフィールド
4.11行 = 1イベント
Splunk に取り込まれたログは、1件ずつバラバラの「イベント」として保管されます。Apache のアクセスログなら、リクエスト1件が1イベントです。
192.168.250.70 - - [24/Aug/2016:16:02:51] "GET /wp-admin/admin-ajax.php HTTP/1.1" 200 1043 192.168.250.70 - - [24/Aug/2016:16:02:53] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 88 40.80.148.42 - - [24/Aug/2016:16:03:11] "GET /uploads/sp-client-…/1.php HTTP/1.1" 200 12
この3行は、Splunk の中では独立した3つのイベントです。検索するというのは、この山から条件に合うものを取り出す作業です。
4.2どのログにも必ずある5つのフィールド
| 名前 | 中身 |
|---|---|
| _time | イベントの発生時刻。内部では UNIX 時間(数値)で持っている |
| _raw | 元のログ1行そのまま。切り出す前の文字列 |
| host | そのログを出した機器 |
| source | 取り込み元のパス |
| sourcetype | ログの種類を表す名札 |
アンダースコアで始まる名前は、Splunk が内部で使う特別なフィールドという印です。_time と _raw がそれにあたります。
画面には「8/24 16:03:11」と表示されますが、中身は 1472054591 のような数値です。そのまま表に載せても読めません。読める形にするには変換が必要で、これが第8章の strftime の話につながります。
4.3自動で生えないフィールドは自分で作る
sourcetype のルールでカバーされない項目は、自分で切り出します。使うのは rex(正規表現で抜く)と eval(計算・変換して作る)の2つです。第8章で扱います。
どちらを使うかの判断はこうです。
| やりたいこと | 使うもの |
|---|---|
| 文字列の一部を抜き出したい | rex |
| 既にあるフィールドを計算・変換したい | eval |
検索は3層でできている
ここが本章の山場です。SPL のクエリは必ずこの3層でできています。この構造が頭に入ると、あとは部品を当てはめるだけになります。
5.1層1と層2の文法は違う
ベース検索は field=value やキーワードを並べるだけ。パイプの後ろは必ずコマンド名から始まります。この2つは別の言語だと思ったほうがいいくらい書き方が違います。
┌─ ベース検索:field=value を並べる index=web sourcetype=access_combined status=404 | eval ... ┐ | stats ... ├─ パイプ:コマンド名から始める | sort ... ┘
5.2なぜ層を分けているのか
速度のためです。ベース検索は索引を引いて一気に絞れますが、パイプの後ろのコマンドは絞り込んだ全イベントを1件ずつ処理します。
だから同じ結果になる書き方でも、どちらに条件を書くかで速度が桁で変わります。
遅い ── 150万件を全部取り出してから捨てている index=web | where status=404
速い ── 索引の段階で絞っている index=web status=404
絞れる条件は、できるだけ上の層に、できるだけ左に。この一言で書けるクエリの質がかなり変わります。
絞り込みの書き方
ベース検索で覚えることは7つです。
6.1キーワードをそのまま書く
単語を書くと、その単語を含むイベントが返ります。どのフィールドかは指定しなくて構いません。
index=web failed index=web "POST /admin" ← 空白を含むならクォート
6.2フィールド名=値
ベース検索では不等号も使えます。
status=404 status!=200 bytes>10000 index=web status=404 ← 並べるだけで AND
6.3AND / OR / NOT
並べるだけで AND なので AND は普通は省略します。OR と NOT は必ず大文字で書きます。小文字だとただの検索語として扱われます。
status=404 OR status=500 index=web NOT clientip=10.0.0.5 (status=404 OR status=500) clientip=10.0.0.5
同じフィールドで値を並べるなら IN が読みやすい。
status IN (404, 500, 503)
6.4ワイルドカード
アスタリスクが0文字以上の任意の文字列にあたります。
uri="/uploads/*" host="web*"
uri="*.php" のように先頭に * を置くと索引が効きません。全件を舐めることになるので、他の条件で範囲を狭めてから使ってください。
6.5大文字小文字のルール
| 対象 | 区別する? | 例 |
|---|---|---|
| フィールド名 | する | status と Status は別物 |
| フィールドの値 | しない | method=get で GET も拾う |
| キーワード検索 | しない | error で ERROR も拾う |
| AND / OR / NOT | する | 小文字の or は演算子にならない |
区別して探したいときだけ CASE() で囲みます。
host=CASE(localhost) ← 小文字の localhost だけ
6.6記号を含む文字列の罠
Splunk は取り込み時に、ピリオドやスラッシュやハイフンの位置で単語を区切って索引を作ります。
つまり 192.168.1.1 を検索すると、内部では「192 かつ 168 かつ 1 かつ 1」を含むイベントを探しにいきます。関係ないものが混ざる可能性があるということです。
1つのかたまりとして扱わせたいときは TERM() で囲みます。
TERM(192.168.1.1)
ただし実用上は、クォートで囲んで書けば大抵うまくいきます。候補を広めに拾ってから絞る動きになるだけです。
6.7時間範囲をクエリに書く場合
普段は時間ピッカーで指定しますが、保存する検索などではクエリに書きます。
index=web earliest=-24h latest=now index=web earliest="08/24/2016:00:00:00"
| 書き方 | 意味 |
|---|---|
| -15m / -24h / -7d | 15分前 / 24時間前 / 7日前から |
| -1d@d | 昨日の0時ちょうどから(@d で日単位に切り下げ) |
| @w0 | 今週の日曜0時 |
| now | 今 |
コマンドは2種類しかない
SPL には百個以上のコマンドがあります。全部覚えるのは無理だし、その必要もありません。コマンドを2種類に分類するだけで、大半の混乱が消えます。
7.1この分類が効く理由
TYPE B を通した後は、元のフィールドが存在しません。だからこう書くとエラーも出ないまま結果が空になります。
index=web | stats count by status ← ここで status と count だけになる | table _time, clientip ← どちらも存在しない
逆に言えば、TYPE B は最後に置くのが基本形になります。詳しくは第9章で図解します。
eventstats は集計しつつイベントの束のまま返す珍しいコマンドです。「全体の平均を出しつつ、平均を超えるイベントだけを残す」といった処理ができます。最初は使わなくて構いませんが、存在だけ覚えておくと後で助かります。
| eventstats avg(bytes) as 平均 | where bytes > 平均 * 5
よく使うコマンド12個
実際の調査で使うのは、ほぼこの12個で足ります。
8.1eval — フィールドを作る TYPE A
使用頻度が最も高いコマンドです。= の左が新しく作るフィールド名、右が中身。既存の名前を左に書くと上書きになります。
| eval kb = bytes / 1024 | eval time = strftime(_time, "%Y/%m/%d %H:%M:%S") | eval 判定 = if(status >= 400, "エラー", "正常")
よく使う関数
| 関数 | やること |
|---|---|
| strftime(時刻, 書式) | UNIX 時間を読める文字列に |
| strptime(文字列, 書式) | 文字列を UNIX 時間に戻す |
| replace(元, 正規表現, 置換後) | マッチ部分を置き換える |
| if(条件, 真, 偽) | 2択で振り分ける |
| case(条件1, 値1, 条件2, 値2 …) | 多択で振り分ける |
| lower(x) / upper(x) | 小文字化・大文字化 |
| len(x) | 文字数 |
| coalesce(a, b, c) | 最初に中身があるものを返す |
| round(x, 桁) | 四捨五入 |
| mvcount(x) | 複数値フィールドの個数 |
時刻の書式
| 記号 | 意味 | 記号 | 意味 |
|---|---|---|---|
| %Y | 年(4桁) | %H | 時(24時間) |
| %m | 月 | %M | 分 |
| %d | 日 | %S | 秒 |
第2引数は文字列ではなく正規表現です。. は「任意の1文字」なので、ピリオドそのものを消したいならエスケープが要ります。
✕ . が任意の文字として解釈される eval x = replace(version, "HTTP/1.1", "") ○ 正しい eval x = replace(version, "HTTP/1\.1", "")
8.2rex — 正規表現で切り出す TYPE A
自動で生えていないフィールドを自分で作ります。名前付きキャプチャがそのままフィールド名になります。
| rex field=uri "(?<filename>[^/]+\.\w+)$"
(?<filename>...) の filename が新しいフィールド名です。マッチしなかったイベントには生えません(エラーにはなりません)。field= を省略すると _raw つまりログ全文が対象になります。
置換だけしたいときは mode=sed が使えます。eval の replace より読みやすいことが多い。
| rex field=uri mode=sed "s/ HTTP\/1\.1//"
8.3search / where — 途中で絞る TYPE A
| 書き方 | 使いどころ | |
|---|---|---|
search | ベース検索と同じ(ワイルドカード可) | 単純な値の一致 |
where | eval と同じ式(関数が使える) | フィールド同士の比較、計算結果での絞り込み |
| search status=404 | where len(uri) > 100 | where bytes > 平均 ← フィールド同士は where だけ
where で文字列を比較するときは必ずダブルクォートで囲みます。囲まないとフィールド名として解釈され、静かに0件になります。
where user = "admin" ← 文字列 admin と比較(正しい) where user = admin ← admin という名前のフィールドと比較(0件)
8.4stats — 数える・まとめる TYPE B
SQL の GROUP BY にあたります。調査で最も出番の多い集計手段です。
| stats count by clientip
by を省くと全体でひとつの数字になります。
| 関数 | やること |
|---|---|
| count | 件数 |
| dc(x) | x の種類数(重複を除いた数) |
| values(x) | 出てきた値を全部並べる(重複なし) |
| list(x) | 出てきた値を出現順に全部並べる |
| sum(x) / avg(x) | 合計・平均 |
| min(x) / max(x) | 最小・最大(時刻に使えば初回と最終) |
| earliest(x) / latest(x) | 最初のイベントの値・最後のイベントの値 |
複数の集計を同時に書けます。as で列名を付けられます。
| stats count, dc(uri) as 種類数, min(_time) as 初回 by clientip
8.5table — 列を選ぶ TYPE B
指定した列だけの表を作ります。書いた順に左から並びます。書かなかった列は消えます。
| table _time, clientip, uri, status
8.6fields — 列を残す・捨てる TYPE A
table と似ていますが、こちらは形を変えません。マイナスを付けると「捨てる」。
| fields clientip, uri ← この2つだけ残す | fields - _raw ← _raw を捨てて軽くする
途中で軽くしたいときは fields、最後に見た目を整えるときは table と使い分けます。
8.7sort — 並べ替える TYPE A
| sort count ← 昇順(少ない順) | sort - count ← 降順(多い順) | sort - count, uri ← 同数なら uri で昇順
sort は既定で先頭1万件しか返しません。それ以上あっても黙って切り捨てられ、エラーも警告も出ません。
外すにはコマンド名の直後に 0(上限なし)を書きます。
| sort 0 _time ← 全件 | sort 100 - count ← 上位100件だけ
8.8dedup / head / tail TYPE A
| dedup clientip ← clientip の重複を1つに | head 20 ← 先頭20件 | tail 20 ← 末尾20件
8.9rename — 列名を変える TYPE A
| rename clientip AS "接続元IP"
8.10top / rare TYPE B
stats count by して sort するのを1コマンドでやる省略形。件数と割合も出ます。
| top 10 uri | rare 10 uri ← 珍しいもの=ハンティングの主戦場 | top 5 uri by clientip
8.11timechart — 時間ごとに集計 TYPE B
「時間で区切った stats」です。深夜に跳ねている、といった異常が視覚的に出ます。
| timechart span=1h count by status
span は 1m 5m 1h 1d のように指定します。
8.12eventstats — 集計しつつ形は保つ 例外
集計結果を「全イベントに列として配る」コマンドです。全体平均と各イベントを比較したいときに使います。
| eventstats avg(bytes) as 平均, stdev(bytes) as 標準偏差 | where bytes > 平均 + 標準偏差 * 3
「全体から見て極端に大きいレスポンス」を探す型です。データ持ち出しの検知などに使えます。
データの形が変わる
コマンドを覚えても書けるようにならない人が、ほぼ全員つまずくのがここです。逆にここを掴めば、クエリを自分で組み立てられるようになります。
9.1だから静かに空になる
この2つはどちらもエラーが出ません。結果が空か、列が全部空欄になるだけです。
stats の後で消えたフィールドを使っている index=web | stats count by status | table _time, clientip, status, count
table の後で消えたフィールドで並べ替えている index=web | table uri, status | sort 0 _time
9.2解決は2通り
①捨てる前に処理を済ませる。
index=web | sort 0 _time ← 先に並べる | table uri, status ← 見せる列は最後に決める
②集計の中で一緒に拾っておく。
index=web | stats count, values(clientip) as 接続元, min(_time) as 初回 by status
9.3組み立ての型
ほとんどの調査クエリは、この4段階に収まります。迷ったらこの型に当てはめてください。
| 段 | やること | 使うもの | 種類 |
|---|---|---|---|
| 1 絞る | 対象のログだけ取り出す | ベース検索・時間範囲 | — |
| 2 足す | 必要なフィールドを作る | eval / rex | TYPE A |
| 3 整える | 並べ替え・重複削除・追加の絞り込み | sort / dedup / where | TYPE A |
| 4 見せる | 集計する・列を選ぶ | stats / table / timechart | TYPE B |
TYPE B は最後。これだけ守れば、消えたフィールドを触る事故はほぼ起きません。
実践 — 1行ずつ組み立てる
10.1まず日本語で書く
SPL をいきなり書き始めないでください。やりたいことを日本語で書くと、そのまま構造になります。
Web サーバのアップロード先 /uploads/sp-client-document-manager へのアクセスを全部拾って、いつ・何が・どうなったかを時系列で並べたい。
第9章の4段階に割り当てます。
- 絞る — Web の index で、Apache のログのうちそのパスを含むもの
- 足す — 時刻を読める形にする
- 整える — 古い順に並べる
- 見せる — 時刻・ステータス・URL の3列
集計しない、というのがこの調査の性格です。攻撃の流れを追うときは、数を数えるより1件ずつ時系列で読むほうが分かります。
10.2ボタンで1行ずつ進める
各段階でデータの形がどう変わるかに注目してください。
10.3完成形
index=web sourcetype=access_combined "/uploads/sp-client-document-manager" | eval time = strftime(_time, "%Y/%m/%d %H:%M:%S") | sort 0 _time | table time, status, uri
| 行 | なぜ必要か |
|---|---|
| 1 | 対象を絞る。index を書かないと全保管庫を舐める |
| 2 | _time は数値。人が読む表に載せるには変換が要る |
| 3 | 時系列で読みたい。0 が無いと1万件で切れる。並べ替えは _time(time は文字列なので文字列順になる) |
| 4 | TYPE B は最後。ここで他の列は消えるが、もう使わない |
ハマりどころ
エラーが出れば直せます。厄介なのはエラーが出ないまま間違った結果が返るケースです。ここに挙げるのは全部そのタイプです。
11.1sort が1万件で黙って切れる
件数の多い調査では条件反射で 0 を書いてください。切り捨てられても何も表示されません。
11.2TYPE B の後で消えたフィールドを使う
結果が空になるか、列が全部空欄になります。結果が意味不明なときは、まず TYPE B のコマンドより前に戻して確認。
11.3フィールド名が環境で違う
本や記事のクエリが動かない原因の大半です。ベース検索だけ投げて、サイドバーで実際の名前を確認してからコピーしてください。
11.4_time と time を混同する
| 名前 | 正体 |
|---|---|
| _time | Splunk が持つ本物の時刻。数値。並べ替えや時間計算はこれ |
| time | eval で自分が作った表示用の文字列。並べ替えに使うと文字列順になる |
年月日の書式なら文字列順でも偶然そろいますが、%m/%d のような書式にした瞬間に順番が壊れます。
11.5where で文字列をクォートし忘れる
| where user = admin は admin というフィールドとの比較になります。そんなフィールドは無いので0件。エラーは出ません。
11.6絞り込みをパイプの後ろに書く
結果は同じですが遅くなります。ベース検索に書ける条件はベース検索に。
11.7時間範囲が前回のまま
時間ピッカーは設定が残ります。結果が多すぎる・遅すぎると感じたら、まずここを疑ってください。
11.8index を書き忘れる
全保管庫を横断するので、データ量の多い環境では数分待たされます。
思った結果が返らないときは、後ろのパイプから1本ずつ消して実行してください。どこで壊れたかが必ず特定できます。組み立てた手順を逆にたどるだけです。
脅威ハンティングで使う型
調査で繰り返し出てくる6つの型です。フィールド名を差し替えればそのまま使えます。
12.1環境に何があるか把握する
知らない環境を渡されたら最初に投げます。時間範囲は15分程度に絞ってください。
index=* | stats count by index, sourcetype | sort - count
12.2珍しいものを探す
ハンティングの基本発想です。正常な活動は大量に繰り返されるので、1〜2回しか出ていないものに異常が紛れます。
index=win sourcetype="WinEventLog:Security" EventCode=4688 | stats count, values(host) as ホスト by New_Process_Name | sort count | head 50
「全社で1台しか実行していないプロセス」が上に来ます。Web アクセスなら by uri、DNS なら by query に変えるだけです。
12.3起点の前後を時系列で読む
怪しい IP やホストが1つ見つかったら、そこを軸に周辺を洗います。第10章と同じ型です。
index=web clientip=40.80.148.42 | eval time = strftime(_time, "%H:%M:%S") | sort 0 _time | table time, method, uri, status, bytes
12.41つの主体がやったことを1行にまとめる
「この IP は結局どこにアクセスしたのか」を一望する型。values() が効きます。
index=web sourcetype=access_combined | stats count, dc(uri) as URL種類数, values(status) as ステータス, min(_time) as 初回, max(_time) as 最終 by clientip | eval 初回 = strftime(初回, "%m/%d %H:%M") | eval 最終 = strftime(最終, "%m/%d %H:%M") | sort - URL種類数
短時間に大量の URL を叩いている IP が上に来ます。スキャンや探索の痕跡が出る型です。
min(_time) の結果も数値の UNIX 時間だからです。集計してから読める形に変換します。集計前に文字列にすると min/max が文字列比較になって狂います。
12.5時間ごとの推移を見る
index=web | timechart span=1h count by status
深夜に跳ねている、平日と休日で形が違う、といった異常が視覚的に出ます。
12.6全体から外れたものを拾う
eventstats の出番です。「レスポンスサイズが全体平均から極端に外れているリクエスト」を探します。データ持ち出しの兆候を拾う型です。
index=web sourcetype=access_combined | eventstats avg(bytes) as 平均, stdev(bytes) as 偏差 | where bytes > 平均 + 偏差 * 3 | eval time = strftime(_time, "%m/%d %H:%M") | sort 0 - bytes | table time, clientip, uri, bytes
練習問題
読むだけでは書けるようになりません。手で書いてから答えを開いてください。フィールド名は access_combined の標準的なものを使います。
index=web のアクセスログから、ステータスが404のリクエストだけを取り出したい。
答えを見る
index=web sourcetype=access_combined status=404
パイプは不要。ベース検索だけで済むならそれが最速です。| where status=404 と書いても同じ結果になりますが、遅くなります。
404 が出ている URL ごとの件数を、多い順に出したい。
答えを見る
index=web sourcetype=access_combined status=404 | stats count by uri | sort - count
| top uri でもほぼ同じ結果が出ます(件数に加えて割合も付きます)。結果は Statistics タブに出ます。
次のクエリは0件になる。なぜか、そしてどう直すか。
index=web | table uri, status | sort 0 _time
答えを見る
原因:table は TYPE B なので、書いていない _time がここで捨てられる。次の行で並べ替えに使おうとしても存在しない。
index=web | sort 0 _time | table uri, status
直し方:捨てるコマンドを最後に回す。これが第9章の「TYPE B は最後」です。
アクセスログの uri からファイル名だけを抜き出して、ファイル名ごとの件数を数えたい。
例:/uploads/docs/1.php → 1.php
答えを見る
index=web sourcetype=access_combined | rex field=uri "(?<filename>[^/]+\.\w+)$" | stats count by filename | sort - count
rex は TYPE A なので、この時点ではまだイベントの束。(?<filename>...) の名前がそのままフィールドになります。[^/]+ は「スラッシュ以外の文字が1つ以上」、$ は末尾。マッチしないイベントには filename が生えないので、集計から自然に外れます。
直近24時間で、アクセスした URL の種類が多い順に接続元 IP を並べ、それぞれの初回アクセス時刻も読める形で出したい。
答えを見る
index=web sourcetype=access_combined earliest=-24h | stats dc(uri) as 種類数, count, min(_time) as 初回 by clientip | eval 初回 = strftime(初回, "%m/%d %H:%M:%S") | sort - 種類数
ポイントは strftime を stats の後に置くこと。min(_time) の結果も数値なので、集計してから変換します。先に文字列にすると min が文字列比較になって狂います。時間範囲はピッカーで指定してもかまいません。
チートシートと練習環境
14.1コマンド早見表
| 書き方 | やること | 種類 |
|---|---|---|
| index=x sourcetype=y | 保管庫と形式で絞る | — |
| | eval x = 式 | 新しい列を作る | A |
| | rex field=a "(?<b>…)" | 正規表現で切り出す | A |
| | search a=1 | 途中で絞る(ベース検索の書き方) | A |
| | where 式 | 途中で絞る(eval の書き方) | A |
| | sort 0 a | a で昇順に全件 | A |
| | sort 0 - a | a で降順に全件 | A |
| | dedup a | a の重複を1つに | A |
| | head 10 / tail 10 | 先頭・末尾10件 | A |
| | fields a, b / fields - a | 列を残す / 捨てる | A |
| | rename a AS b | 列名を変える | A |
| | eventstats avg(a) as m | 集計値を全イベントに配る | A |
| | stats count by a | a ごとに数える | B |
| | table a, b, c | この列だけの表にする | B |
| | top 10 a / rare 10 a | 多い順・少ない順 | B |
| | timechart span=1h count | 1時間ごとの件数 | B |
14.2覚えておく6つのルール
- 検索は3層。時間範囲 → ベース検索 → パイプ。
- 絞れる条件はできるだけ上の層に、できるだけ左に。
- コマンドは2種類。TYPE B(stats / table / top / timechart)は最後に置く。
sortには0を付ける。付けないと1万件で切れる。- 並べ替えは
_time、表示は自分で作った文字列。 - フィールド名は環境依存。書く前にサイドバーで確認する。
14.3練習環境
Splunk Enterprise の無償トライアル
公式サイトから落として自分の PC に入れられます。60日間はフル機能で、その後も無償ライセンスに切り替えて使い続けられます(1日あたりの取り込み量に上限あり)。
BOTS データセット
Splunk 公式が CC0 で公開している、実際の攻撃シナリオを含む演習用ログです。GitHub に置かれています。インデックス済みの形で配布されているので、無償ライセンスの取り込み量制限にも引っかかりません。
| データセット | 入手先 / 中身 |
|---|---|
| BOTS v1 | Apache / Suricata / Sysmon など。最初の版で解説記事も多い |
| BOTS v2 | github.com/splunk/botsv2 |
| BOTS v3 | github.com/splunk/botsv3 |
攻撃部分だけを抜き出した小さい版(attack-only)もあるので、ディスクが厳しければそちらから始められます。設問と解答も公式から入手できます。
進め方
index=botsv3 | stats count by sourcetypeを投げて、何のログがあるか見る- 気になった sourcetype を1つ選び、素で検索してサイドバーのフィールド名を眺める
- 第12章の型を、その sourcetype のフィールド名に差し替えて実行する
- BOTS の設問を1問ずつ解く
このページを閉じたら、まずベース検索の1行だけを打って実行してください。結果が返ってきたら、あとは1本ずつパイプを足すだけです。全部いっぺんに書こうとしないこと。それだけで書けるようになります。