rsyslog で無理矢理 CEF っぽくする

rsyslog で CEF ([Common Event Format](https://kc.mcafee.com/resources/sites/MCAFEE/content/live/CORP_KNOWLEDGEBASE/78000/KB78712/en_US/CEF_White_Paper_20100722.pdf)) っぽくしてみる。CEF にはめ込むための情報がログにすべて含まれているわけじゃない (ベンダーとか製品情報とか…) ので、CEF「っぽい」が限界。

[RainerScript](https://www.rsyslog.com/doc/v8-stable/rainerscript/index.html) でがんばる。RainerScript の概要を掴むなら[こちら](https://knowledge.sakura.ad.jp/8975/)を。公式ドキュメントを読む前に大変おせわになりました。

ログごとに変換処理を行うことになるので、ログ量が多い環境では負荷に注意したほうが良いかもしれない。

# テンプレートの準備

まずはテンプレートを準備する。テンプレートは、ファイル名やログ形式で使える、書式指定みたいなもの。

「$!～」はユーザー定義プロパティ。まぁ変数みたいなもの。

```conf
#### TEMPLATES ####

# CEF (Common Event Format)
template(name="ceftmpl" type="list"){
  property(name="timegenerated")
  constant(value=" ")
  property(name="hostname")
  constant(value=" CEF:0|")
  property(name="$!cefprodvendor")
  constant(value="|")
  property(name="$!cefprodname")
  constant(value="|")
  property(name="$!cefprodversion")
  constant(value="|")
  property(name="syslogfacility")
  constant(value=".")
  property(name="syslogseverity")
  constant(value="|")
  property(name="syslogfacility-text")
  constant(value=".")
  property(name="syslogseverity-text")
  constant(value="|")
  property(name="$!cefsvrty")
  constant(value="|")
  property(name="$!cefext")
  constant(value="\n")
}
```

#各プロパティに情報を格納

で、このテンプレート中で使っているユーザー定義プロパティ

* `$!cefprodvendor` (製品ベンダー)
* `$!cefprodname` (製品名)
* `$!cefprodversion` (製品バージョン)
* `$!cefsvrty` (イベントの優先度。10 ～ 0。10 が最重要)
* `$!cefext` (拡張情報)

を何とかでっちあげる。
RainerScript には「変数の定義」みたいな概念はないから、テンプレートの前に書かなきゃならん、みたいな記載順の前後に気を付ける必要はない。

```conf
#### RULES ####

# Initialize CEF information.
set $!cefprodname = replace(replace($programname, "\\", "\\\\"), "|", "\\|");
set $!cefprodvendor = $!cefprodname;
set $!cefsvrty = 7 - $syslogseverity;

```
* `$!cefprodname`は、rsyslog の既定プロパティ `$programname`を使う。
* `$!cefprodvendor`は、とりあえず`$cefprodname`を流用。SIEM とかで絞りやすくするために「rsyslog」とかの固定値にしてもいいかもしれない。
* `$!cefsvrty`は、CEF の優先度と Syslog の優先度の上下が逆転してるから、とりあえず emerg を最大 (7) として debug が 0 になるようにした。

`replace(str, substr_to_replace, replace_with)` は RainerScript の組み込み関数。これで CEF の特殊文字をエスケープしてる。

# 用意したテンプレートなどを使ってみる

旧来の syslogd 的な書き方でいうと

```conf
*.info;mail.none;authpriv.none;cron.none                /var/log/messages
*.info;mail.none;authpriv.none;cron.none                @@192.0.2.1
```

みたいなのを RainerScript で書きつつ、テンプレートを適用してみる。自サーバーの /var/log/messages には既定の形式のまま書き込み、192.0.2.1 に CEF 形式で転送する。

```conf
if ($syslogseverity < 7
    and $syslogfacility-text != "mail"
    and $syslogfacility-text != "authpriv"
    and $syslogfacility-text != "cron") then {
  set $!cefprodversion = "0";
  set $!cefext = "dvchost=" & $hostname & " msg=" & replace(replace($syslogtag & " " & $msg, "\\", "\\\\"), "=", "\\=");
  
  action(type="omfile" file="/var/log/messages")
  action(type="omfwd"
         target="192.0.2.1" protocol="tcp" port="514"
         keepalive="on"
         template="ceftmpl")
}
```
`$!cefext`は、rsyslog の既定プロパティ`$syslogtag`と`$msg`を使う。
…`$!cefprodversion`は「0」固定。でっち上げるにも元情報がない…。


「action(type="omfile"～」がファイル出力、「action(type="omfwd"～」が転送。今回はファイル出力のほうにテンプレートを適用しなかったけど、したければしてもいい。

# コンフィグ全体

CentOS のコンフィグに上記の変更を加えた、コンフィグ全体を貼っておく。

```conf
# rsyslog configuration file

# For more information see /usr/share/doc/rsyslog-*/rsyslog_conf.html
# If you experience problems, see http://www.rsyslog.com/doc/troubleshoot.html

#### MODULES ####

# The imjournal module bellow is now used as a message source instead of imuxsock.
$ModLoad imuxsock # provides support for local system logging (e.g. via logger command)
$ModLoad imjournal # provides access to the systemd journal
#$ModLoad imklog # reads kernel messages (the same are read from journald)
#$ModLoad immark  # provides --MARK-- message capability

# Provides UDP syslog reception
#$ModLoad imudp
#$UDPServerRun 514

# Provides TCP syslog reception
#$ModLoad imtcp
#$InputTCPServerRun 514


#### GLOBAL DIRECTIVES ####

# Where to place auxiliary files
$WorkDirectory /var/lib/rsyslog

# Use default timestamp format
$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat

# File syncing capability is disabled by default. This feature is usually not required,
# not useful and an extreme performance hit
#$ActionFileEnableSync on

# Include all config files in /etc/rsyslog.d/
$IncludeConfig /etc/rsyslog.d/*.conf

# Turn off message reception via local log socket;
# local messages are retrieved through imjournal now.
$OmitLocalLogging on

# File to store the position in the journal
$IMJournalStateFile imjournal.state


#### TEMPLATES ####

template(name="ceftmpl" type="list"){
  property(name="timegenerated")
  constant(value=" ")
  property(name="hostname")
  constant(value=" CEF:0|")
  property(name="$!cefprodvendor")
  constant(value="|")
  property(name="$!cefprodname")
  constant(value="|")
  property(name="$!cefprodversion")
  constant(value="|")
  property(name="syslogfacility")
  constant(value=".")
  property(name="syslogseverity")
  constant(value="|")
  property(name="syslogfacility-text")
  constant(value=".")
  property(name="syslogseverity-text")
  constant(value="|")
  property(name="$!cefsvrty")
  constant(value="|")
  property(name="$!cefext")
  constant(value="\n")
}


#### RULES ####

# Initialize CEF information.
set $!cefprodname = replace(replace($programname, "\\", "\\\\"), "|", "\\|");
set $!cefprodvendor = $!cefprodname;
set $!cefsvrty = 7 - $syslogseverity;

# Log all kernel messages to the console.
# Logging much else clutters up the screen.
#kern.*                                                 /dev/console

# Log anything (except mail) of level info or higher.
# Don't log private authentication messages!
#*.info;mail.none;authpriv.none;cron.none                /var/log/messages
if ($syslogseverity < 7
    and $syslogfacility-text != "mail"
    and $syslogfacility-text != "authpriv"
    and $syslogfacility-text != "cron") then {
  set $!cefprodversion = "0";  
  set $!cefext = "dvchost=" & $hostname & " msg=" & replace(replace($syslogtag & " " & $msg, "\\", "\\\\"), "=", "\\=");
  
  action(type="omfile" file="/var/log/messages")
  action(type="omfwd"
         target="192.0.2.1" protocol="tcp" port="514"
         keepalive="on"
         template="ceftmpl")
}

# The authpriv file has restricted access.
authpriv.*                                              /var/log/secure

# Log all the mail messages in one place.
mail.*                                                 -/var/log/maillog

# Log cron stuff
cron.*                                                  /var/log/cron

# Everybody gets emergency messages
*.emerg                                                 :omusrmsg:*

# Save news errors of level crit and higher in a special file.
uucp,news.crit                                          /var/log/spooler

# Save boot messages also to boot.log
local7.*                                                /var/log/boot.log


# ### begin forwarding rule ###
# The statement between the begin ... end define a SINGLE forwarding
# rule. They belong together, do NOT split them. If you create multiple
# forwarding rules, duplicate the whole block!
# Remote Logging (we use TCP for reliable delivery)
#
# An on-disk queue is created for this action. If the remote host is
# down, messages are spooled to disk and sent when it is up again.
#$ActionQueueFileName fwdRule1 # unique name prefix for spool files
#$ActionQueueMaxDiskSpace 1g   # 1gb space limit (use as much as possible)
#$ActionQueueSaveOnShutdown on # save messages to disk on shutdown
#$ActionQueueType LinkedList   # run asynchronously
#$ActionResumeRetryCount -1    # infinite retries if host is down
# remote host is: name/ip:port, e.g. 192.168.0.1:514, port optional
#*.* @@remote-host:514
# ### end of the forwarding rule ###
```
もういっそ syslogd 形式はやめて、全部 RainerScript 的に書きたい気もするけど、そこは気が向いたら()

# おまけ
特定プログラムに特化すれば、もうちょいマシに組める。例えば Postfix。

```config
# Log all the mail messages in one place.
#mail.*                                                 -/var/log/maillog
if ($syslogfacility-text == "mail") then {
  set $!cefprodversion = "3.5";
  set $!cefext = "devhost=" & $hostname;
  
  set $!cefext = "dvchost=" & $hostname;
  
  if (re_match($msg, "[0-9A-F]+: ")) then {
    set $!cefext = $!cefext & " cs1="
                 & replace(replace(re_extract($msg, "([0-9A-F]+): ", 0, 1, "(none)"), "\\", "\\\\"), "=", "\\=");
  }
  
  if (re_match($msg, "from=[^ ]+@[^ ,]+")) then {
    set $!cefext = $!cefext & " suser="
                 & replace(replace(re_extract($msg, "(from=)([^ ]+@[^ ,]+)", 0, 2, "(none)"), "\\", "\\\\"), "=", "\\=");
  }
  
  if (re_match($msg, "to=[^ ]+@[^ ,]+")) then {
    set $!cefext = $!cefext & " duser="
                 & replace(replace(re_extract($msg, "(to=)([^ ]+@[^ ,]+)", 0, 2, "(none)"), "\\", "\\\\"), "=", "\\=");
  }

  if (re_match($msg, "connect to [^ :]+")) then {
    set $!cefext = $!cefext & " dhost="
                 & replace(replace(re_extract($msg, "(connect to )([^ :]+)", 0, 2, "(none)"), "\\", "\\\\"), "=", "\\=");
  }
  
  if (re_match($msg, "connect from [^ :]+")) then {
    set $!cefext = $!cefext & " shost="
                 & replace(replace(re_extract($msg, "(connect from )([^ :]+)", 0, 2, "(none)"), "\\", "\\\\"), "=", "\\=");
  }
  
  set $!cefext = $!cefext & " msg=" & replace(replace($syslogtag & " " & $msg, "\\", "\\\\"), "=", "\\=");
  
  action(type="omfile" file="/var/log/maillog")
  action(type="omfwd"
         target="192.0.2.1" protocol="tcp" port="514"
         keepalive="on"
         template="ceftmpl")
  stop
}
```

ちなみに`stop`は、これ以降他に出力したくないなら書く。syslogd の「-出力先」みたいな感じ。

これでログ的には

```text
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost msg=postfix/smtpd[30065]: warning: unknown[80.82.XX.XX]: SASL LOGIN authentication failed: authentication failure
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost shost=unknown[80.82.XX.XX] msg=postfix/smtpd[30065]: disconnect from unknown[80.82.XX.XX]
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost cs1=03D037E257 suser=aaa@aaa.com, msg=postfix/qmgr[1388]: 03D037E257: from\=aaa@aaa.com, size\=20169, nrcpt\=1 (queue active)
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost cs1=03D037E257 duser=bbb@bbb.com, msg=postfix/smtp[29975]: 03D037E257: to\=bbb@bbb.com, relay\=none, delay\=60457, delays\=60456/0.01/0.87/0, dsn\=4.4.3, status\=deferred (Host or domain name not found. Name service error for name\=gmail.co.jp type\=MX: Host not found, try again)
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost cs1=813897E249 suser=ccc@ccc.com, msg=postfix/qmgr[1388]: 813897E249: from\=ccc@ccc.com, size\=15939, nrcpt\=1 (queue active)
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost dhost=aaa.com[63.240.XXX.XXX] msg=postfix/smtp[29975]: connect to aaa.com[63.240.XXX.XXX]: Connection timed out (port 25)
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost cs1=813897E249 duser=ddd@ddd.com, dhost=aaa.com[63.240.178.216] msg=postfix/smtp[29975]: 813897E249: to\=ddd@ddd.com, relay\=none, delay\=60582, delays\=60552/0/30/0, dsn\=4.4.1, status\=deferred (connect to aaa.com[63.240.178.216]: Connection timed out)
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost msg=postfix/anvil[30067]: statistics: max connection rate 1/60s for (smtp:80.82.XX.XX) at Jul 24 16:47:27
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost msg=postfix/anvil[30067]: statistics: max connection count 1 for (smtp:80.82.XX.XX) at Jul 24 16:47:27
Feb  3 07:04:38 testhost CEF:0|postfix|postfix|3.5|2.6|mail.info|1|dvchost=testhost msg=postfix/anvil[30067]: statistics: max cache size 1 at Jul 24 16:47:27
```

みたいになる。Postfix のバージョン情報が直打ちなのはイケてないから、そこは改善したいところ。

`re_extract` の第 3 引数 (match) と第 4 引数 (submatch) の使い方が良く解らず、手探り状態。「なんやねんこれ」と思ってたら、[他の人も同じこと思ってたw](https://github.com/rsyslog/rsyslog-doc/issues/581)

ここまで rsyslog に正規表現処理させて、サーバー負荷がどんなもんかは不明…。

# あとは…

[mmfields モジュール](https://www.rsyslog.com/doc/master/configuration/modules/mmfields.html)でより負荷の少ない方法で実現できそうか検討してみたい。みたいだけ。未定。