Cloudflare Logpush を Syslog に変換して ALog Cloud に連携する

# 参考：ALog Cloud とは
無料トライアルを活用させていただきました。

https://www.amiya.co.jp/lp/alog_cloud/

>ALogはオンプレからクラウドまで、あらゆるシステムのログデータを収集・分析・保管する純国産のSIEM製品です。
>“わかりやすさ”を追及したALogは、あらゆるITシステムのログを自動で収集、一元的に管理するSIEM製品です。
「セキュリティの難しいをカンタンに」を、コンセプトに専門知識やノウハウがなくとも、誰でもカンタンに高度なログ分析を実現。
企業がかかえる課題に沿って、幅広いシーンで活用できるのが特徴です。

# TCP Workers を使って ALog Syslog Receiver に Syslog 送信
ALog Cloud のログ取り込み方式を考慮し、以下のような構成を実装します。

:::note warn
Syslog のプロトコル特性上、認証の機構が限られるため、 送信元 IP アドレスを制限する等、Syslog Receiver の公開には注意が必要です。また、通信を暗号化するには TLS オプションを使う必要があります。

[4.1.8. TLSを使用してsyslogを受信したい — ALog Syslog Receiver User Guide 1.3.0 ドキュメント](https://www.amiya.co.jp/support/alog/product/raw/asruserguide/user_guide/source/UserGuide_SyslogReceiver.html#tlssyslog)
:::

```mermaid
%%{init: {'themeCSS': 'img {width: 48px; height:48px;}'}}%%

graph LR
subgraph Cloudflare
    Logpush["<img src='https://cf-icons.pages.dev/logs.svg' width='48' height='48'><br>Logpush"]
    Workers[<img src='https://cf-icons.pages.dev/workers.svg' width='48' height='48'><br>TCP<br>Workers]
    Logpush --> |"HTTP POST<br>*.log.gz"|Workers
end

Workers --> |Syslog<br>TCP 514|Receiver
subgraph Windows
    Receiver[ALog Syslog Receiver] --> |Save|Local[Local Folder]
    Forwarder[ALog Forwarder] --> |Pull|Local
end

Forwarder --> |"`*.log`" upload|ALog["<img src="https://www.amiya.co.jp/wp-content/themes/Ami-CorporateWeb/assets/old/images/ja/logos/alog.svg" width='48' height='48'><br>#emsp;#emsp;ALog Cloud#emsp;#emsp;"]
```

## 参考： ALog Syslog Receiver とは
https://www.amiya.co.jp/support/alog/product/raw/asruserguide/user_guide/source/UserGuide_SyslogReceiver.html

>ALog Syslog Receiverは、各機器から送信されるSyslogを受信してファイル出力する製品です。 各機器のSyslogデータをSyslog形式のまま、もしくはCSV形式にしてファイル化します。
>ALog Syslog Receiverによってファイル出力されたデータは、ALog EVAで取り込み、用意された専用の標準テンプレートやユーザー自身で作成したテンプレートによってマッピング処理を行います。
> 
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d282ab73-093d-b2d4-9660-9f0d89ef91e8.png)

## ALog Syslog Receiver 準備
以下のリンクから ALog Syslog Receiver をダウンロードしてインストールします。

https://support.alog.app/hc/ja/sections/4403499617807-ALog-Syslog-Receiver

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/9feb1807-d5cd-683e-5e0f-46f422448105.png)

タスクマネージャーやサービスでプロセスが稼働していることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/6431bd80-4730-1b6b-2df6-c94424b0ab87.png)

ALog Syslog Receiver の再起動が必要な場合には、以下の画面から実施できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/abedf78a-2cce-f695-bcc8-05d88b2e3214.png)

## TCP Workers 準備
Cloudflare Workers は Outbound TCP 接続をすることができます。

今回は TCP 514 ポートを宛先として Syslog メッセージを送ります。

https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#tcp-sockets

https://wicg.github.io/direct-sockets/

後でマッピング設定をスムーズにおこなうために Syslog メッセージのフォーマットは RFC 3164（`<%PRI%>%timegenerated% %HOSTNAME% %syslogtag%%msg`）を使います。

https://www.jtc-i.co.jp/syslogroom/message-format.html

https://www.amiya.co.jp/support/alog/product/raw/v8userguide/user_guide/source/alogeva.html#eva-fileformat

> ALog EVAで処理可能なSyslogはRFC3164に準拠したもので、「日時」、「ホスト名」、「メッセージ」の順に「スペース区切り」で記録されたフォーマットになっている

`PRESHARED_AUTH_HEADER_VALUE` と `syslogEndpoint` を適宜設定して、コードをデプロイします。

https://github.com/kyouheicf/logpush-to-syslog

```shell:
export URL='https://github.com/kyouheicf/logpush-to-syslog.git'
git clone $URL && cd $(basename $_ .git)
wrangler deploy
```

```typescript:src/index.ts
// For decompressing
import { Base64 } from 'js-base64';
// For TCP socket
import { connect } from 'cloudflare:sockets';

export default {
  async fetch(req): Promise<Response> {
    // Check pre-shared key header
    const PRESHARED_AUTH_HEADER_KEY = 'X-Logpush-Auth';
    const PRESHARED_AUTH_HEADER_VALUE = 'mypresharedkey';
    const psk = req.headers.get(PRESHARED_AUTH_HEADER_KEY);
    const contentEncoding = req.headers.get('content-encoding')
    if (psk !== PRESHARED_AUTH_HEADER_VALUE) {
      return new Response('Sorry, you have submitted an invalid key.', {
        status: 403,
      });
    }
    // Decompress gzipped logpush body to json
    const buf = await req.arrayBuffer();
    const enc = new TextDecoder("utf-8");
    const blob = new Blob([buf])
    const ds = new DecompressionStream('gzip');
    const decompressedStream = blob.stream().pipeThrough(ds);
    const buffer2 = await new Response(decompressedStream).arrayBuffer();
    const decompressed = new Uint8Array(buffer2)
    const ndjson = enc.decode(decompressed)

    // Initial pre-flight Logpush Request to confirm the integration check
    if (ndjson === '{"content":"test"}') {
      return new Response('Initial pre-flight Logpush Request has been confirmed', {
        status: 200,
      })
    }
    // parse ndjson to JSON object
    var json = '[' + ndjson.trim().replace(/\n/g, ',') + ']';
    const jsonobj = JSON.parse(json);

    try {
      // Define Your Syslog Endpoint
      const syslogEndpoint = { hostname: "x.x.x.x", port: 514 };
      const socket = connect(syslogEndpoint /* , { secureTransport: "on" } */);
      let writer = socket.writable.getWriter()

      // Write Syslog RFC 3164 Format message
      //"<34>Oct 11 22:14:15 gateway_http AccountID=\"xxx\" Action=\"bypass\" Datetime=\"1724782330\" DestinationIP=\"x.x.x.x\" DeviceName=\"xxx\" Email=\"xxx@example.com\" HTTPHost=\"www.google.co.jp\" HTTPMethod=\"UNKNOWN\" HTTPStatusCode=\"0\" \n");
      jsonobj.map(jsonelem => {
        const jsonfmt = JSON.stringify(jsonelem, null, 2);
        let jstNow = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000))//.toLocaleString({ timeZone: 'Asia/Tokyo' });
        let MMMddHHmmss = jstNow.toString().slice(4, 7) + ' ' + jstNow.getDate() + ' ' + jstNow.toTimeString().slice(0, 8)
        let message = '<34>' + MMMddHHmmss + ' gateway_http ' + Object.keys(jsonelem).map(function (key) { return key + '=' + JSON.stringify(jsonelem[key]) }).join(' ') + ' \n'
        writer.write(new TextEncoder().encode(message));
      })
      await writer.close();

      // Start reading from the socket.	  
      const decoder = new TextDecoder();
      let syslogResponse = "";
      for await (const chunk of socket.readable) {
        syslogResponse += decoder.decode(chunk, { stream: true });
      }
      syslogResponse += decoder.decode();
      console.log("Read ", syslogResponse.length, " from Syslog server");
      return new Response(syslogResponse, { headers: { "Content-Type": "text/plain" } });
    } catch (error) {
      return new Response("Socket connection failed: " + error, { status: 500 });
    }
  }
} satisfies ExportedHandler;
```

## Windows Firewall 構成

:::note warn
TCP Workers は [Cloudflare IP Ranges](https://www.cloudflare.com/ips/) に記載されていない IP アドレスから接続をおこなうことが観測されたため、Windows Firewall の構成時に注意が必要です。

```
2024-08-27 01:33:32 DROP TCP 104.28.157.26 10.146.0.17 23169 514 60 S 1023346287 0 65535 - - - RECEIVE
2024-08-27 01:34:18 DROP TCP 104.28.157.52 10.146.0.17 42193 514 60 S 1289832775 0 65535 - - - RECEIVE
2024-08-27 01:35:40 DROP TCP 104.28.157.33 10.146.0.17 25009 514 60 S 2700424325 0 65535 - - - RECEIVE
2024-08-27 01:36:05 DROP TCP 104.28.157.24 10.146.0.17 58408 514 60 S 3116203926 0 65535 - - - RECEIVE
2024-08-27 01:37:31 DROP TCP 104.28.157.43 10.146.0.17 14550 514 60 S 1785922093 0 65535 - - - RECEIVE
```
:::

観測された範囲では `104.28.0.0/16` を使っているように見受けられたため、以下の受信許可ルールを追加して有効にします。

```bat:cmd
netsh advfirewall firewall add rule ^
name="Cloudflare TCP Workers Syslog Connection" ^
dir=in action=allow enable=yes ^
remoteip=104.28.0.0/16 ^
protocol=tcp localport=514
```

## Logpush ジョブ構成
以下のコマンドで `gateway_http` データセットの Logpush を作成します。

`output_type` で `timestamp_format` は `unix` にし、`destination_conf` で作成した TCP Workers エンドポイント `https://syslog.example.workers.dev` を指定します。


```shell:
export EMAIL='YOUR_EMAIL'
export APIKEY='YOUR_APIKEY'
export ACCOUNT_ID='YOUR_ACCOUNT_ID'

# 全フィールドを指定
export FIELDS=$(curl -s \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $APIKEY" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/logpush/datasets/gateway_http/fields" | jq -r '.result | keys | join("\",\"")')
echo $FIELDS

# Logpush ジョブを作成
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/logpush/jobs" -X POST -d '
{                                              
  "name": "gateway-http-syslog-alog",
  "output_options": {
    "field_names": ["'$FIELDS'"],
    "timestamp_format": "unix"
  },
  "destination_conf": "https://syslog.example.workers.dev?header_X-Logpush-Auth=mypresharedkey&dataset:gateway_http",
  "max_upload_bytes": 5000000,
  "max_upload_records": 1000,
  "dataset": "gateway_http",
  "enabled": true
}' \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $APIKEY"
```

TCP Workers と Logpush の構成がうまく動いていると、以下のフォルダに `*.log` ファイルが保存されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/9e576f40-2c23-7d5c-5e76-4774a7a60944.png)

## ALog Forwarder 設定
以下のガイドに従ってログ連携します。

https://www.amiya.co.jp/support/alog/product/raw/alogcloud/user_guide/source/startup.html#index-1

対象ホストの登録は以下のように設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/06bccf13-d584-f871-328b-4ca135c67c8c.png)

## ALog Cloud マッピング設定
管理＞対象ホストから以下のマッピング設定を行い、ログを見やすい形で取り込みます。

https://www.amiya.co.jp/support/alog/product/raw/alogcloudforwarder/user_guide/source/mapping.html#

Syslog メッセージ内でログを `key="value"` のように変換したことで `KeySearch` 関数を使ったマッピングが可能です。

`TimeStamp` は `unix` シリアル値の `DateTime` フィールドを参照し、`ConvSerialToDateTime` 関数で取り込むことができます。

その他の詳細フィールドは適宜見やすい形に定義できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/5a178645-7405-a566-5b30-b2c7a420d450.png)

検索画面にて以下のようにログを取り込んで活用できる状態になったことが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d1ced25c-99a3-7f5f-a9ae-87ab6e1eeacf.png)

# まとめ

Cloudflare からの Logpush で直接の宛先として対応していない SIEM に対しても、TCP Workers を使って柔軟にファイル形式を変換する処理を実現できました。

こうしたログ処理のパイプラインを一度構築してしまえば、後は自動で取り込みが実行されていくため、SIEM 側でのレポート・アラートを活用した運用に注力していくことができます。

# 参考

https://qiita.com/khayama/items/b9fad32d3f4791f272db

https://github.com/cloudflare/workerd/blob/main/samples/tcp/gopher.js