はじめに
Linuxにはテキスト処理を行うコマンドがたくさんあります。コマンドには多彩は書式やオプションが用意されており、複雑な処理ができるものが多くあります。
ただ、初歩的な書式やオプションだけでも パイプで複数連結すればなんとかなる ことが多いので、基本的な書式だけでも無意識に使えるようにすると効率が桁違いです。
最初から複雑な書式を使いこなすのは難しく、すぐに忘れてしまします。チートシートを片手に多彩なオプションで悩む時間も無駄です。
パイプでつなげればなんとかなる
- grep 1回ですべて検索しようとしない
パイプでつなげればなんとかなる - sed 1回ですべて置換しようとしない
パイプでつなげればなんとかなる - awk 1回でなんでもかんでもやろうとしない
他コマンドをパイプでつなげればなんとかなる。
このエントリで記載している各コマンドの使い方は、ごく基本的な書式と初歩的なオプションに限定しています。
説明文は文献をあたっていないので、いいかげんなものが含まれている可能性があります。ごめんちょ。
処理対象のテキストファイル
Splunkのチュートリアルデータを利用してテキスト処理のためのコマンドを解説します。
あ、いや、Splunkの活用法と違うってのはわかってるんですが。今回はすいません、データだけ利用させてください。
curl https://docs.splunk.com/images/Tutorial/tutorialdata.zip -o ~/tmp/tutorialdata.zip
unzip ~/tmp/tutorialdata.zip -d ~/tmp/sample
cd ~/tmp/sample
テキスト処理コマンドいろいろ
テキスト処理のコマンドは、標準入力→処理→標準出力が多い。なので、パイプで連結できるのです。
cat
cat(きゃっと):conCATenate
ファイルか標準入力を連結して標準出力に出力する
→ ファイルの中身を出力
cat <ファイル名>
cat <STDIN>
$ cat www1/secure.log
grep
grep(ぐれっぷ):Global Regular Expression Print
ファイルか標準入力から正規表現に一致する行を標準出力に出力する
→ 入力されてきた行に対して、フィルタして出力
正規表現はregex(Regular Expression)と表記されることが多くあります。
複数ファイルがある場合は、先にgrepで検索すると、先頭にファイル名がつくので cat 全ファイル | grep xxx
としてもよいです。
grep <正規表現> <ファイル>
grep <正規表現> <STDIN>
$ grep sudo: */secure.log
$ cat */secure.log | grep sudo:
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-v |
inVert | 指定条件以外を検索 |
-i |
Ignore case | 大文字/小文字を無視 |
-r |
Recursive | ディレクトリを再帰的に検索 |
-E |
Extended | 拡張正規表現を利用 |
拡張正規表現は (ABC|XYZ)
などが利用できる書式のことで egrep
コマンドでも同じ効果が得られます。
# sudo: という文字列を「含まない」行を抽出
$ cat */secure.log | grep -v sudo:
# ssh という文字列の大文字/小文字を無視する
$ cat */secure.log | grep -i ssh
# カレントディレクトリ配下の全ファイルに対して検索する
$ grep ssh * -ri
その他の書式
以下の「ステータスコード200」は、想定しない場所も抽出してしまう可能性があるので、完璧ではないですが漏れは無いかと。
# ステータスコード200以外を抽出
$ cat */access.log | egrep -v " 200 [1-9][0-9]* "
# さらに特定のUAを除外する
$ cat */access.log | egrep -v " 200 [1-9][0-9]* " | grep -v Mozilla | grep -v Opera
# さらに特定のUAを除外する
$ cat */access.log | egrep -v " 200 [1-9][0-9]* " | grep -v Mozilla | grep -v Opera
# さらにPOSTリクエストのみに限定する
$ cat */access.log | egrep -v " 200 [1-9][0-9]* " | grep -v Mozilla | grep -v Opera | grep POST
sed
sed(せっど):Stream EDitor
ファイルか標準入力に対して条件文を利用して変換する
→ 入力されてきた行に対して、条件式で置換して出力
だいたい正規表現で置換する用途で利用します。
# 正規表現で置換
sed 's/<正規表現>/<置換後文字列>/' <ファイル>
sed 's/<正規表現>/<置換後文字列>/' <STDIN>
# 正規表現で置換(1行中に出現する複数の正規表現の全てを置換する)
sed 's/<正規表現>/<置換後文字列>/g' <ファイル>
sed 's/<正規表現>/<置換後文字列>/g' <STDIN>
# 正規表現の大文字/小文字を判定しない
sed 's/<正規表現>/<置換後文字列>/gi' <ファイル>
sed 's/<正規表現>/<置換後文字列>/gi' <STDIN>
$ cat */secure.log | grep sudo: | sed 's/;..*//'
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-E |
Extended | 拡張正規表現 |
-i
でファイル指定して上書き、-e
で複数条件の指定などありますが、代替手段でなんとかなるので覚えなくてよいです。
その他の書式
正規表現パターンを \(
と \)
で囲むことでグループ化して、置換文字列として参照させることができます。
「マッチさせたうえで、抽出後にその文字をそのまま残したい」というときに利用します。
$ cat */secure.log | grep sudo: | sed 's/..*sudo: \(..*\) ;/\1/'
一部の文字はエスケープが必要です。
$ cat */secure.log | grep sudo: | sed 's/..*\/home\/\(..*\) ;/\1/'
セパレータは任意に変更できます。
$ cat */secure.log | grep sudo: | sed 's:..*/home/\(..*\) ;:\1:'
# 拡張正規表現のオプションだとグループの()がエスケープ不要になる
$ cat */secure.log | grep sudo: | sed -E 's:..*/home/(..*) ;:\1:'
cut
cut(かっと):CUT
入力されていた行に対して、文字数やデリミタなどの条件式で抽出して出力
cut -d<delimiter> -f<field> <STDIN>
$ cat */secure.log | grep sudo: | cut -d: -f4
# デリミタを空白とする場合はエスケープする
$ cat */secure.log | grep sudo: | sed 's/;..*//' | cut -d\ -f6,8
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-d |
delimiter | デリミタ指定(複数文字NG) |
-f |
field | 抽出するフィールドを指定 |
sort
sort(そーと):SORT
入力されてきた行をソートして出力
sort <ファイル名>
sort <STDIN>
$ cat */secure.log | grep sudo: | sed 's/;..*//' | awk '{print $6,$8}' | sort
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-n |
number | 数値として判定 |
-r |
reverse | 逆順(降順)に表示 |
uniq
uniq(ゆにーく):UNIQue
前後の行が重複している場合に行をまとめる
uniq <ファイル名>
uniq <STDIN>
$ cat */secure.log | grep sudo: | sed 's/;..*//' | awk '{print $6,$8}' | sort | uniq
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-c |
count | 重複回数を表示 |
$ grep sudo: secure.log | awk -F\; '{print $3}' | sort | uniq -c
$ grep sudo: secure.log | awk -F\; '{print $3}' | sort | uniq -c | sort -nr
awk
awk(おーく):Aho Weinberger Kernighan
テキストファイル処理に特化したプログラミング言語
→ 入力されてきた行に対して、いろいろ処理して表示する
awk <code> <ファイル>
awk <code> <STDIN>
# print $n で、n番目のフィールドを抜き出します
$ cat */secure.log | grep sudo: | sed 's/;..*//' | awk '{print $6,$8}'
awk
は使いこなせれば、sort/uniqなどをまとめて処理できるようになりますが、個人的には print
で使うだけです。
$NF
で最後のフィールド、$(NF-1)
で最後から1番目、という使い方ができます。
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-F |
Field | デリミタ指定(複数文字OK) |
デリミタ(=delimiter/de,limiter)は「区切り文字」のことで、セパレータということもあります。デフォルトはスペースです。
nl
nl(えぬえる):Number of Line
入力されてきた行に対して行番号を添えて出力
nl <ファイル名>
nl <STDIN>
$ cat */secure.log | grep sudo: | sed 's/;..*//' | awk '{print $6,$8}' | sort | uniq | nl
head
head(へっど):HEAD
入力されてきた行の「先頭」から指定した分だけ出力
head <ファイル名>
head <STDIN>
$ cat */secure.log | grep sudo: | sed 's/;..*//' | awk '{print $6,$8}' | sort | uniq -c | sort -nr | head
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-n |
number | 表示させる行数を指定 |
$ head access.log -n 30
tail
tail(ている):TAIL
入力されてきた行の「末尾」から指定した分だけ出力
tail <ファイル名>
tail <STDIN>
$ cat */secure.log | grep sudo: | sed 's/;..*//' | awk '{print $6,$8}' | sort | uniq -c | sort -n | tail
よく使うオプション
オプション | 略称 | 意味 |
---|---|---|
-n |
number | 表示させる行数を指定 |
-f |
follow | 入力が続く限り更新して表示 |
$ tail access.log -n 30
読み込み対象ファイルが継続的に追記されるようなファイルの場合は -f
で逐次更新することもできます。
$ tail -f /var/log/nginx/access.log
$ tail -f /var/log/nginx/access.log | grep POST
ログ検索のサンプル問題
セキュアログ調査
Linuxシステムのセキュアログと思われる */secure.log
について、それぞれ次のログ調査を実施してください。
$ head */secure.log -n 2
==> mailsv/secure.log <==
Thu Apr 13 2023 13:34:34 mailsv1 sshd[4351]: Failed password for invalid user guest from 86.212.199.60 port 3771 ssh2
Thu Apr 13 2023 13:34:34 mailsv1 sshd[2716]: Failed password for invalid user postgres from 86.212.199.60 port 4093 ssh2
==> www1/secure.log <==
Thu Apr 13 2023 13:34:30 www1 sshd[4747]: Failed password for invalid user jabber from 118.142.68.222 port 3187 ssh2
Thu Apr 13 2023 13:34:30 www1 sshd[4111]: Failed password for invalid user db2 from 118.142.68.222 port 4150 ssh2
==> www2/secure.log <==
Thu Apr 13 2023 13:34:33 www2 sshd[1319]: Failed password for invalid user varnish from 209.160.24.63 port 3459 ssh2
Thu Apr 13 2023 13:34:33 www2 sshd[1390]: Failed password for invalid user icinga from 209.160.24.63 port 2678 ssh2
==> www3/secure.log <==
Thu Apr 13 2023 13:34:31 www3 sshd[3163]: Failed password for invalid user sysadmin from 202.91.242.117 port 1994 ssh2
Thu Apr 13 2023 13:34:31 www3 sshd[3301]: Failed password for games from 202.91.242.117 port 3741 ssh2
すべての secure.log からSSHのパスワード認証に成功したアカウント名と、その数を調査する
grep sshd */secure.log | grep Accepted \
| awk '{print $11}' \
| sort | uniq -c
すべての secure.log からSSHのパスワード認証に成功したアカウント名と接続元IPアドレスと、その数を調査する
grep sshd */secure.log | grep Accepted \
| awk '{print $11,$13}' \
| sort | uniq -c
すべての secure.log からSSHのパスワード認証に失敗したアカウント名と、その数を調査する
これだと invalid
というユーザ名が多いように見えてしまう。
grep sshd */secure.log | grep Failed \
| awk '{print $11}' \
| sort | uniq -c | sort -n
解決方法その1
ログを見ると以下のことがわかる。
- ユーザ名が存在するときのログは、最後のフィールドから数えて5番目がユーザ名
- ユーザ名が存在しないときのログも、最後のフィールドから数えて5番目がユーザ名
Mon Apr 17 2023 13:34:34 mailsv1 sshd[5586]: Failed password for mail from 142.233.200.21 port 3967 ssh2
Mon Apr 17 2023 13:34:34 mailsv1 sshd[3093]: Failed password for invalid user administrator from 142.233.200.21 port 3229 ssh2
awkの$NF
を利用することで最後から〇番目というフィルタができる。
grep sshd */secure.log | grep Failed \
| awk '{print $(NF-5)}' \
| sort | uniq -c | sort -n
解決方法その2
ログを見ると以下のことがわかる。
- ユーザ名が存在するときのログは
Failed password for XXX from
- ユーザ名が存在しないときのログは
Failed password for invalid user XXX from
Mon Apr 17 2023 13:34:34 mailsv1 sshd[5586]: Failed password for mail from 142.233.200.21 port 3967 ssh2
Mon Apr 17 2023 13:34:34 mailsv1 sshd[3093]: Failed password for invalid user administrator from 142.233.200.21 port 3229 ssh2
Failed password for
のあとにinvalid user
が0回以上存在するとき、を条件に含めるようにすることでユーザ名だけを抽出できる。
grep sshd */secure.log | grep Failed \
| sed 's/.*Failed password for \(invalid user \)*\(.*\) from.*/\2/' \
| sort | uniq -c | sort -n
すべての secure.log からSSHのパスワード認証に失敗したアカウント名と接続元IPアドレスと、その数を調査する
前述の解決方法その1の手法をもとに、IPアドレスが最後から3番目なので以下のように抽出できる
grep sshd */secure.log | grep Failed \
| awk '{print $(NF-5),$(NF-3)}' \
| sort | uniq -c | sort -n
前述の解決方法その1の手法をもとに、IPアドレスの前後の文字からグループ化することで以下のように抽出できる
grep sshd */secure.log | grep Failed \
| sed 's/.*Failed password for \(invalid user \)*\(.*\) from \(.*\) port.*/\2 \3/' \
| sort | uniq -c | sort -n
アクセスログ調査
Webサーバのアクセスログと思われる www[1-3]/access.log
について、それぞれ次のログ調査を実施してください。
$ head www[1-3]/access.log -n 2
==> www1/access.log <==
209.160.24.63 - - [13/Apr/2023:18:22:16] "GET /product.screen?productId=WC-SH-A02&JSESSIONID=SD0SL6FF7ADFF4953 HTTP 1.1" 200 3878 "http://www.google.com" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5" 349
209.160.24.63 - - [13/Apr/2023:18:22:16] "GET /oldlink?itemId=EST-6&JSESSIONID=SD0SL6FF7ADFF4953 HTTP 1.1" 200 1748 "http://www.buttercupgames.com/oldlink?itemId=EST-6" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5" 731
==> www2/access.log <==
199.15.234.66 - - [13/Apr/2023:18:24:31] "GET /cart.do?action=view&itemId=EST-6&productId=SC-MG-G10&JSESSIONID=SD5SL9FF2ADFF4958 HTTP 1.1" 200 3033 "http://www.google.com" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 YFF3 Firefox/3.6.28 ( .NET CLR 3.5.30729; .NET4.0C)" 177
175.44.24.82 - - [13/Apr/2023:18:44:36] "GET /category.screen?categoryId=SHOOTER&JSESSIONID=SD7SL9FF5ADFF5066 HTTP 1.1" 200 2334 "http://www.google.com" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; BOIE9;ENUS)" 546
==> www3/access.log <==
74.125.19.106 - - [13/Apr/2023:18:27:48] "GET /product.screen?productId=FS-SG-G03&JSESSIONID=SD10SL4FF4ADFF4976 HTTP 1.1" 200 3770 "http://www.buttercupgames.com/category.screen?categoryId=STRATEGY" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5" 667
74.125.19.106 - - [13/Apr/2023:18:27:50] "POST /cart.do?action=addtocart&itemId=EST-26&productId=FS-SG-G03&JSESSIONID=SD10SL4FF4ADFF4976 HTTP 1.1" 200 293 "http://www.buttercupgames.com/product.screen?productId=FS-SG-G03" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5" 100
アクセスのピークは何時ごろか?
調査から除外する日時を判断する
調査対象は複数日にわたったログであり、最初の日時以前のログと最後の日時以降のログは存在しないため、その日付以外のログを対象とする。
(現実世界であれば曜日による増減を考慮する必要があるが、ここでは考慮しないことにする)
以下の結果から、2023/4/14~2023/4/19のログを調査対象とする。
$ cat www[1-3]/access.log | awk '{print $4}' | sort | head -n 1
[13/Apr/2023:18:22:16]
$ cat www[1-3]/access.log | awk '{print $4}' | sort | tail -n 1
[20/Apr/2023:18:22:16]
以下のように、egrep -v '(13|20)/Apr'
とすることで除外できることが確認できた。
$ cat www[1-3]/access.log | awk '{print $4}' | egrep -v '(13|20)/Apr' | sort | head -n 1
[14/Apr/2023:00:00:19]
$ cat www[1-3]/access.log | awk '{print $4}' | egrep -v '(13|20)/Apr' | sort | tail -n 1
[19/Apr/2023:23:58:49]
時間帯の順にアクセス数量を表示
cat www[1-3]/access.log | awk '{print $4}' \
| egrep -v '(13|20)/Apr' \
| awk -F: '{print $2}' \
| sort | uniq -c
アクセス数量の順に時間帯を表示
cat www[1-3]/access.log | awk '{print $4}' \
| egrep -v '(13|20)/Apr' \
| awk -F: '{print $2}' \
| sort | uniq -c \
| sort -n
タイムゾーンは不明だが、午前3時がアクセスのピークの模様
正規表現
このエントリでは、様々な正規表現を使ってテキストを操作するものではないので、正規表現自体の解説は本筋ではありません。(として逃げます。)
正規表現の解説記事は世の中にわんさかあるので、ここではよく利用するパターンを記載するだけにしています。
正規表現をチェックできるサイト
よく利用する正規表現
正規表現/拡張正規表現がごちゃまぜに書いてあるので grep
で利用できなかったら egrep
で利用するなど、適宜工夫してください。
表記 | 意味 |
---|---|
^ |
先頭 |
$ |
末尾 |
. |
なにかの文字 |
[ab] |
a もしくはb
|
[a-z] |
a からz までの英字 |
[-az] |
- もしくはa もしくはz
|
[^] |
以外 |
() |
グループ |
(abc|xyz) |
abc もしくはxyz
|
\d |
数字 |
\w |
英字 |
\s |
スペース(空白、タブ、改行) |
* |
0以上の繰り返し |
+ |
1以上の繰り返し |
{n} |
直前のn回繰り返し |
{n,m} |
直前のn~m回の繰り返し |
{n,} |
直前のn回以上の繰り返し |
{,m} |
直前のm回以下の繰り返し |
コンフィグファイルで必要な部分を抜き出すのに
- 空行(
^$
) - 先頭に0個以上のスペースがあって、そのあとが
#
になっていない行を表示する。とかはよくやりますよね。
cat <config> | egrep -v "^($|\s*#)"
# 文字数は多いけど、個人的にはこっちのほうが好き
cat <config> | egrep -v "(^$|^\s*#)"
さいごに
とある勉強会の用途でこの資料を作成しました。
コマンドの活用方法に唯一解は存在しません。
最初は基本のコマンドだけで、初歩のオプションだけで、自然に手が動くようになることが最初の一歩でいいのです。そこから「必要と感じたら」少しずつアレンジしていけばいいし、「それ、違うよ」と言われたらその時にちゃんと調べて使い方をなおせばいいのです。
と私は考えます。