收集 Linux 命令行執行的命令

本篇文章的目的爲收集在命令行執行的全部命令,除了將全部的命令發送到 Elasticsearch 進行保存以外,還須要爲敏感命令作告警。html

要作到這些的核心在於 PROMPT_COMMAND 這個環境變量,它的做用是,在出現 shell 命令輸入提示符以前,做爲命令來執行這個變量。shell

所以,咱們能夠將這個變量定義爲一個命令,而後看看它的效果:json

# export PROMPT_COMMAND="date '+%F %T'"
2019-04-23 11:25:53 # 在出現下面的提示符以前執行了 date 命令
# a
-bash: a: command not found
2019-04-23 11:25:56 # 再次出現
# v
-bash: v: command not found
2019-04-23 11:25:58 # 每次命令行提示符出現以前它都會出現
複製代碼

這就至關於每執行一次命令就會執行一次 PROMPT_COMMAND。有了這個基礎以後,咱們就可讓其收集全部用戶執行的命令。vim

read 命令

首先,因爲 PROMPT_COMMAND 執行的時機是上個命令結束,命令行輸出以前,所以咱們可使用 histroy 1 獲取上一次執行的命令。可是因爲該命令的輸出結果前面會帶上命令的序號,咱們須要去掉它。ruby

這樣一來,第一個版的 PROMPT_COMMAND 的結果爲:bash

# export PROMPT_COMMAND="history 1 | { read _ cmd; echo \$cmd; }"
複製代碼

看起來挺複雜,其實很簡單:markdown

  • histroy 1 的結果會傳遞到大括號,目的是去掉命令前面的序號;
  • 大括號至關於開啓了一個匿名函數,將這兩個命令做爲一個總體,不過它不會開啓一個子 shell;
  • read 是 shell 內部的子命令,它會將空格做爲分隔符。

咱們通常使用 read 來讀取鍵盤的輸入,不過它還能夠幫咱們去掉命令前的序號。因爲咱們這裏定義兩個變量,所以 read 會對輸入的結果使用空格分割 1 次,分割後的結果第一部分給變量 _,另外一部分給變量 cmd併發

很顯然,序號給了 _,而後咱們 echo $cmd 就可以拿到上一次執行的命令了。只因此這裏的 $ 前面加了轉義符號 \,是由於這個命令是在 shell 環境下輸入的,它會直接將 $cmd 做爲變量解釋了,使用轉義是防止它直接解釋。elasticsearch

其實你能夠直接在命令行來測試 read 的效果:函數

# read x y z
23232 xxxxx ewewewe ssssss zzzzz
# echo "$x | $y | $z"
23232 | xxxxx | ewewewe ssssss zzzzz
複製代碼

收集相關信息

咱們光收集歷史命令是沒什麼用的,還應該收集以下信息:

  • 命令執行的時間
  • 執行命令時所在的目錄
  • 當前執行命令的用戶
  • 登陸的用戶(登陸後可能會 su 切換到其餘用戶)
  • 用戶所在的 tty(可能會同時開多個 shell)
  • 登陸的 ip
  • 執行的命令

以上這些信息都須要執行命令來獲取,組合起來就是這樣的:

date "+%F %T"; pwd; whoami; who -u am i | { read user tty _ _ _ _ ip; echo $user $tty $ip| tr -d "()"; }; history 1 | { read _ cmd; echo $cmd; }
複製代碼

別急着使用,你須要看看有哪些命令,以及這些命令是幹啥的就行,由於這畢竟不是最終版本。

若是定義了 HISTTIMEFORMAT 環境變量,history 的輸出結果可能就不是咱們想要的了。所以咱們應該在用戶登陸以後將 HISTTIMEFORMAT 設置爲空,爲了防止用戶修改,你能夠將它設置爲只讀。可是一旦將其設置爲只讀,那麼在系統重啓以前,這個值沒法修改。

vim /etc/profile
export HISTTIMEFORMAT=""
readonly HISTTIMEFORMAT
複製代碼

logger 命令

咱們拿到這些信息以後確定不能只是將其輸出,而是將其存放在一個文件中。若是直接將其追加到一個文件中,是會有問題的。操做系統上確定不止一個用戶,不一樣的用戶都會執行命令,那麼這個日誌文件的屬主屬組應該改爲啥?日誌文件要不要切割?要不要刪除?這些都是要考慮的問題。

雖然咱們最終都是要輸出到文件中,直接追加的方式雖然簡單,可是很差控制。最好的方式是使用 logger 命令將其輸出 rsyslog,讓 rsyslog 幫咱們寫到文件中,依託 rsyslog 強大的功能,咱們能夠對日誌文件作更多的事情。

logger 命令咱們只會用到兩個選項:

  • -p:指定輸出的基礎設施和日誌等級;
  • -t:指定 tag

咱們須要修改 rsyslog 的配置文件,讓其接收咱們發送給它的日誌並輸出到文件中。這裏將日誌輸出到 /var/log/bashlog。

# vim /etc/rsyslog.d/bashlog.conf
local6.debug /var/log/bashlog
複製代碼

檢查 rsyslog 配置,而後重啓:

# rsyslogd -N1
# /etc/init.d/rsyslog/restart
複製代碼

而後測試一把,看看 /var/log/bashlog 是否存在你想要的內容。

echo "hehe" | logger -t bashlog -p local6.debug
複製代碼

若是將這些都賦值給 PROMPT_COMMAND 變量,會顯得很複雜,咱們能夠將這些命令定義到一個文件中,而後將這個文件賦值給 PROMPT_COMMAND。

# vim /etc/collect_cmd.sh
echo `date "+%F_%T"; pwd; whoami; who -u am i | { read user tty _ _ _ _ ip; echo $user $tty $ip| tr -d "()"; }; history 1 | { read _ cmd; echo $cmd; }` | logger -t bashlog -p local6.debug

# chmod +x /etc/collect_cmd.sh
# export PROMPT_COMMAND="/etc/collect_cmd.sh"
複製代碼

須要注意的是,腳本就寫這一行,不要加上 #!/bin/bash,不然 history 命令執行不會有任何結果,緣由不明。

最終用戶每在命令行執行一次命令,就會在日誌文件中增長一條相似這樣的行:

Apr 25 14:23:03 localhost bashlog: 2019-04-25_14:23:03 root root pts/0 10.201.2.170 cat /etc/collect_cmd.sh
複製代碼

前面的日期日誌、主機名、程序名都是 rsyslog 自動添加的,後面纔是咱們發送過去的內容。

升級 rsyslog

咱們之因此可以收集用戶執行的命令,核心就是 PROMPT_COMMAND。雖然咱們如今定義好了它,可是不免它被人修改(普通用戶也行),用戶只要登陸後執行 unset PROMPT_COMMAND,那麼你的一切設置都將付諸東流。因此最好的方式就是將這個變量設置爲只讀。

前面咱們已經將所收集到的日誌存放到了文件中了,其實到這一步日誌收集已經完成,可是爲了便於以後發送到 Elasticsearch,我準備將這些日誌以 json 格式寫入到文件中。

怎麼作呢?仍是經過 rsyslog。只不過 CentOS6 默認的 rsyslog 版本過低,功能有限,須要將其升級到最新版才行。

升級 rsyslog 沒有什麼風險,我司生產環境升級到 rsyslog8 跑了兩年多,沒有任何問題。

在官網能夠直接下載對應的 yum repo 文件,而後 yum update rsyslog 就升級到最新版了。

我這裏將全部 rsyslog 相關的包都下載下來,而後建立了一個本地的 yum 倉庫,便於內網機器下載升級。

升級後須要修改一行配置,有些配置不兼容:

# 修改前
*.emerg                                                 *

# 修改後
*.emerg                                                 :omusrmsg:*
複製代碼

修改完成後直接重啓:

service rsyslog restart
複製代碼

解析日誌

rsyslog 經過 mmnormalize 模塊進行日誌解析,解析後的內容爲 json 格式,而這個模塊使用的解析功能來自於 Liblognorm。關於 Liblognorm 的解析語法直接看官方文檔便可。

爲何要解析成 json 格式?主要的緣由是 Elasticsearch 存儲的就是 json 格式的數據,咱們能夠直接將解析好的數據直接發送到 Elasticsearch,而無需使用 Logstash 解析。

先下載模塊:

yum install rsyslog-mmnormalize liblognorm5-utils
複製代碼

liblognorm5-utils 用來檢測解析規則是否正確,下面會用到。

當咱們將執行的命令發送給 rsyslog 後,咱們須要解析的是下面的內容,不包括咱們上面看到的 rsyslog 自動添加的時間日期等信息。

2019-04-30_14:01:45 /root root root pts/0 10.201.2.170 vim hehe
複製代碼

它會在開頭添加一個空格,這個空格是怎麼來的我也不清楚,所以咱們要預留一個空格。

mmnormalize 使用時,須要指定一個解析庫。這個解析庫遵循 liblognorm5 的語法:

# vim /etc/bashlog.rb
version=2

# 冒號後面的空格就是上面提到的空格
rule=: %
    time:word
    # 兩個百分號之間的空格是 date 和 pwd 命令之間的空格
    % %
    directory:word
    % %
    exec_user:word
    % %
    login_user:word
    % %
    tty:word
    % %
    src_ip:ipv4
    % %
    command:rest
    %
複製代碼

這個文件就是一個解析庫,用於解析上面的內容。雖然以 rb 結尾,可是和 ruby 沒有關係。

簡單的解釋下它的做用:

  • version=2 必須處於第一行,而且這一行只能是這幾個字符,不能加任意字符進去。它表示使用的是 v2 引擎,官方推薦使用 v2,可是 v2 不必定比 v1 功能更豐富,可是咱們用夠了。若是沒寫,或者寫錯了,將使用 v1 引擎;
  • 規則的寫法就是 rule=,它後面的冒號 : 用來分割 tag 的,也就是說等號和冒號之間能夠加上 tag。咱們不須要 tag,可是得把冒號寫上;
  • 冒號 : 就是字段解析了,要解析的字段使用百分號 % 包起來。百分號中經過冒號 : 進行分割,冒號前是字段的名稱(json 中的對象名),冒號後是 Liblognorm 內置的字段類型,字段類型後面能夠加參數,使用中括號 {} 引用,只是上面沒有使用,每一個字段的參數都不同,有的有,有的沒有;
  • 百分號中容許存在空格和換行符,這樣就能夠寫成多行,而不用都寫在一行,看起來更美觀;
  • 使用 Liblognorm 進行解析時,空格是一對一的。假如兩個字段間有三個空格,那麼寫解析規則時,兩個百分號之間必需要有三個空格。有時你沒法肯定空格數量怎麼辦?使用 whitespace 這種類型;
  • 字段類型(這裏只列出經常使用的,更多的看官方文檔便可):
    • word:空格外的任意字符,也就是看到空格後就終止匹配;
    • whitespace:匹配全部空格,直到碰到第一個非空格字符。也就是在有不止一個空格的狀況下使用它很是合適;
    • date-rfc3164:rsyslog 的時間字段;
    • ipv4:ipv4 地址;
    • rest:直接匹配到行尾;
    • -:匹配但不顯示,它通常用於丟棄字段,好比它和 whitespace 類型配在一塊兒就很是合適。

測試一把解析庫:

# echo " 2019-04-30_14:01:45 /root root root pts/0 10.201.2.170 vim hehe" | lognormalizer -r /etc/bashlog.rb -e json
{ "command": "vim hehe", "src_ip": "10.201.2.170", "tty": "pts\/0", "login_user": "root", "exec_user": "root", "directory": "\/root", "time": "2019-04-30_14:01:45" }
複製代碼

這就是解析後的結果,惟一的缺點就是會在 / 前面加上轉譯符 \

如今只須要簡單的配置下 rsyslog 就可以將解析後的 json 數據保存到文件中。

# vim /etc/rsyslog.d/bashlog.conf
# 加載模塊
module(load="mmnormalize")

template(name="all-json" type="list"){
  property(name="$!all-json")
  constant(value="\n") # 若是沒有這行,解析後的信息不會換行
}

if $syslogfacility-text == 'local6' and $syslogseverity-text == 'debug' then {
  action(type="mmnormalize" rulebase="/etc/bashlog.rb")
  action(type="omfile" File="/var/log/bashlog" template="all-json")
}
複製代碼

該文件以前的內容能夠刪掉了。

咱們首先定義了一個模板,這個模板是配合解析用的,解析一條消息,就將解析後的 json 格式的信息保存在 $!all-json 這個變量中,而後就能夠定義 action 將其保存在文件中,或者 NoSQL 中。

原本是打算將其直接發送到 kafka/elasticsearch,可是考慮到 rsyslog 只會當時將消息發送出去,若是發送不成功它不會重發,所以仍是將其保存到文件中,而後經過 filebeat 對文件進行讀取併發送。

經過 omfile 還能定義文件的屬主屬組,文件權限等,默認屬主屬組爲 root,權限 600。

重啓 rsyslog 以後,咱們就能夠在 /var/log/bashlog 中看到咱們執行的命令了。

爲了讓 PROMPT_COMMAND 用戶登陸就生效,咱們能夠將之定義在 /etc/profile 中,且將其定義成只讀。

vim /etc/profile
export PROMPT_COMMAND="/etc/collect_cmd.sh"
readonly PROMPT_COMMAND
複製代碼

改進

經過上面的方式咱們能夠收集命令行日誌並將其解析,可是仍是會存在問題。當你在命令行空回車而不輸入任何東西時,執行 history 1 會得到上一次執行的命令,沒有什麼問題。可是若是你上一次都爲空,那麼你此次的收集的命令就是空,解析會失敗。

你會從解析的日誌文件中看到這樣的內容:

{ "originalmsg": " 2019-05-01_14:19:19 \/home\/user1 user1 root pts\/0 10.201.2.170", "unparsed-data": "" }
複製代碼

咱們解析規則是默認登陸後面的 ip 後面還會有內容,當其沒有內容時就會解析失敗。這種狀況會出如今你使用 su - 命令切換到其餘用戶,且切換後直接空回車。

針對這樣的狀況,咱們應該判斷用戶輸入的命令是否爲空,若是爲空就直接退出腳本。所以咱們的 /etc/collect_cmd.sh 能夠作以下修改:

cmd=`history 1 | { read _ cmd; echo $cmd; }`
[ -z "$cmd" ] && exit # 當其爲空時就直接退出
echo `date "+%F_%T"; pwd; whoami; who -u am i | { read user tty _ _ _ _ ip; echo $user $tty $ip| tr -d "()"; }; echo $cmd` | logger -t bashlog -p local6.debug
複製代碼

還有最後一點是針對 HISTTIMEFORMATPROMPT_COMMAND 這兩個環境變量的,爲了不出現解析異常,最好將這兩個變量都設爲只讀,這樣任何用戶都沒法修改它的內容。若是不設置爲只讀,用戶在本身家目錄下的 .bashrc 或者直接在命令行就能夠對其從新設置。

咱們能夠將之都定義在 /etc/profile,若是想要在 /etc/profile.d 中單獨用一個文件來保存,不要將 HISTTIMEFORMAT 的定義放在其中,否則其餘用戶登陸會報錯,提示修改只讀的變量。

unset HISTTIMEFORMAT
readonly HISTTIMEFORMAT
export PROMPT_COMMAND="/etc/collect_cmd.sh"
readonly PROMPT_COMMAND
複製代碼
相關文章
相關標籤/搜索