我在一些系統通知信偶爾會發現類似以下文字內容,多半是事件的詳細資訊,突發奇想打算寫些小服務,偵測到特定類型或特定目標相關事件時觸發額外通知。之前我都把這種內容當成某系統才有的專屬格式,寫個 Regular Expression 抓出關鍵欄位能動就好,沒想過要完整解析。

CEF:0|Trend Micro|Apex Central|2019|AV:File renamed|JS_EXPLOIT.SMDN|3|deviceExternalId=104 rt=Feb 18 2016 14:34:00 GMT+00:00 cnt=1 dhost=ApexOneClient01 duser=Admin004 act=File renamed cn1Label=VLF_PatternNumbercn1=920500 cn2Label=VLF_SecondAction cn2=3 cs1Label=VLF_FunctionCode cs1=Manual Scan cs2Label=VLF_EngineVersion cs2=9.500.1005 cs3Label=CLF_ProductVersion cs3=10.6 cs4Label=CLF_ReasonCodecs4=virus log cs5Label=VLF_FirstActionResult cs5=File renamed cs6Label=VLF_SecondActionResult cs6=N/A cat=1703 dvchost=ApexOneServer01 cn3Label=CLF_ServerityCode cn3=2fname=0348C693056617D34FC5B5BAB4643885FEE5FEDF;0xD5D56AC2 filePath=C:\Users\Administrator\Desktop\trend_test_virus\Trojans\ msg=BMAC Schedule of Events.xls shost=ABC-OSCE-WKS12 suser=ABC-OSCE-WKS12 dst=10.201.129.24 deviceFacility=Apex One

因緣際會讀到相關文件,才知道這是資安監控領域蠻常用的 Log 標準格式 - CEF,Common Event Format。延伸閱讀:Common Event Format (CEF): An Introduction

SIEM 科普

對資安監控管理外行的我,趁機了解術語,補充一些常識:

  • Security Information and Event Management,安全性資訊與事件管理 (簡稱 SIEM) 是一種解決方案,可協助組織在威脅傷害企業營運之前,先進行偵測、分析和回應安全性威脅。
  • 企業常用 SIEM 產品:IBM QRADARSplunkArcSight
  • CEF 是 ArcSight 公司 (現已併入 Micro Focus) 發展的 Log 格式,其基於 syslog 格式,目前已成為大部分網路設備及作業系統都支援的通用標準。
  • 提供 CEF 是為了交換整合,故廠商多半會提供文件,像開頭借用的範例,可以在趨勢官方網站找到相關規格
  • Syslog 是 Linux 通用的事件記錄通訊協定,設備或系統可將事件日誌傳送到 SIEM 統一管理,網路傳送時 Syslog 伺服器一般使用 UDP 514 接收,另外也有 TLS 加密版,接聽 Port 為 6514。

CEF 格式

完整 CEF 訊息格式範例如下,Dec 18 20:37:08 <local0.info> 10.217.31.247 部分為 Header,包含 Timestamp、裝置名稱及來源 IP,Header 後面接的是 Message 內文,以 | 分隔成八欄,依序為 CEF 格式版本、裝置供應商、裝置產品、裝置版本、事件Id、事件名稱、嚴重性、擴充欄位 ,前七欄為 Prefix、第八欄為 Extensions。擴充欄位採用 fieldName1=fieldValue1 fieldName2=fieldValue2 ... 之 Key/Value 格式保存自訂欄位資訊,包含哪些欄位由各系統自訂。

Dec 18 20:37:08 <local0.info> 10.217.31.247 CEF:0|Citrix|NetScaler|NS10.0|APPFW|APPFW_STARTURL|6|src=10.217.253.78 spt=53743 method=GET request=http://vpx247.example.net/FFC/login.html msg=Disallow Illegal URL. cn1=233 cn2=205 cs1=profile1 cs2=PPE0 cs3=AjSZM26h2M+xL809pON6C8joebUA000 cs4=ALERT cs5=2012 act=blocked

文章開始的範例即為 Message 部分的內容。怎麼用 C# 解析感覺是個有趣的練習,就當成假日休間暖暖身吧。

基本上 CEF 格式還蠻單純的,唯一要留意的是跳脫字元問題,例如:當名稱用到 | 分隔字元時要寫成 \|、擴充欄位名稱或值出現 = 分隔字元時寫 \= 。參考官方文件(Common Event Format: Event Interoperability Standard),處理規則為:

  • Prefix 有 |,改 \|,Extension 的 | 不用特別處理
  • Prefix 有 \,改 \\,Extension 的 \ 不用特別處理
  • Extension 有 =,改 \=,Prefix 的 = 不用特別處理
  • Extension 允許換行 (\n 或 \r)

與 Github Copilot 共舞

這一年來我愈來愈離不開機靈過人的 Github Copilot 小書僮,寫程式時靠它在旁舉一反三,代客輸入,有穿上鋼鐵裝橫掃千軍的暢快。
延伸閱讀:小試 Github Copilot讓會讀心術的 Github Copilot 陪你寫程式GitHub Copilot 開發者訓練營筆記

而且我覺得用 Github Copilot 輔助跟問 ChatGPT 幫你寫程式(或該說是抄程式)是完全不同的體驗,你可以保有自己想要風格,看要遵循古法採用傳統工藝,也可以狂嚼語法糖享受新版 C# 的便利,適合不只求會動就好,對於程式該怎麼寫法有執著(或說執念)的老鳥。

怎麼說呢?剛好這回又有實例可以展示。

我寫了一個 CefData 類別,宣告了 logVer、vender... 等屬性(參考趨勢文件的命名),打算在建構式接入 CEF:0|Citrix|NetScaler|NS10.0|APPFW|APPFW_STARTURL|6|src=10.217.253.78 字串當參數,將 Prefix 分割對映到各屬性。

一開始 Copilot 建議的是最直覺最常見的寫法(灰字部分),Split('|') 然後寫死 msgParts[0]、msgParts[0],平淡無奇,且沒有考慮 | 跳脫字元。(廢話,你又沒說)

老鳥如我,想到可以用 Regex.Split() 依據 | 分割並用零寬度左不合樣(Negative Lookbehind)排除 \|。左左右右合不合樣什麼的語法我從沒記住過,除了查文件,現在我可以選擇偷懶,輸入一段註解許願。

薑!薑!薑!講~~~ Copilot 幫我寫好惹。

接下來,我要計算前七欄的長度,之後不管 | 分隔,全部抓一個字串當成 Extension 拆 Key/Value。宣告了 index、pos,寫了 pos += item.Length + 1; 後,Copilot 自動生出用 switch (index) case 把 string[] 前七個元素對映到七個屬性,聰慧如 Copilot,知道要幫你一次寫完:

如果你能接受數大便是美,整整齊齊簡單易懂就好,可以收下這個版本拍板結束。

我對重複程式碼的厭惡度偏高,總想找更簡潔的寫法。於是改用 LINQ Take(7).Sum() 計算前七欄長度,取代跑迴圈計算。算完長度要把 \| 還原回 |,打完 Select,Copilot 也知道我想做什麼:

按下換行,Copilot 馬上猜我要開始將字串陣列一一對映到屬性,最先建議的是最多人會用的 Hard Coding 寫法:

嘿,一般人是會這樣寫沒錯,但我可是學過 C# 解構賦值(Destructing Assignment)的熟手,起頭寫了 (logVer,Copilot 知道我想吃糖,馬上幫我把整行補完。

這是我覺得 Github Copilot 最迷人的地方,是個稱職貼心的副駕駛,不喧賓奪主,總是搭配你要的風格,跟你合作無間。不管你甘心規規矩矩在車陣魚貫前行,還是想挑戰蜿蜒山路甩尾抄捷徑,Coiplot 總能讓你開到盡興。

而從另個角度,同樣是用 Github Copilot,開發者本身的技能高低也將影響成品效果。Copilot 有放大效果,弱者追平,強者愈強,所以在 AI 輔助時代,不斷學習磨練技能,還是能大幅提高自己的競爭力。

成果展示

最後來看成果:

using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;

var testCases = new string[] {
    @"CEF:0|Darkthread|Blog|1.0|100|Blog\|Posts|5|date=2012-12-21|00:00:00 \=_\==E\=MC^2 twoline=line 1
line 2",
    "CEF:0|Trend Micro|Apex Central|2019|Spyware Detected|Spyware Detected|3|deviceExternalId=3 rt=Oct 06 2017 08:39:46 GMT+00:00 cnt=1 dhost=ApexOneClient01 cn1Label=PatternType cn1=1073741840 cs1Label=VirusName cs1=ADW_OPENCANDY cs2Label=EngineVersion cs2=6.2.3027 cs5Label=ActionResult cs5=Reboot system successfully cs6Label=PatternVersion cs6=1297 cat=1727 dvchost=ApexOneClient01 fname=F:\\Malware\\psas\\rsrc2.bin filePath=F:\\Malware\\psas\\rsrc2.bin dst=50.8.1.1 deviceFacility=Apex One",
    "CEF:0|Carbon Black|Carbon Black|4.1.0.131118.1540|reason=process_watchlist_-1|SyslogTest|10|dproc=wmiprvse.exe fname=c:\\windows\\system32\\wbem\\wmiprvse.exe start=2014-01-14T20:36:19.526Z dhost=J-8205A0C27A0C4 msg=group:Default Group process_md5:0ffae66e6d5b1c87cbd22d1f3b6079fd last_update:2014-01-14T20:36:19.526Z guid:-5850106436655859636 segment_id:1488563344023",
    "CEF:0|security|threatmanager|1.0|100|worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2 spt=1232",
    "CEF:0|JATP|Cortex|3.6.0.1444|email|Phishing|8|externalId=1504 eventId=14067 lastActivityTime=2016-12-06 23:51:38+00 src= dst= src_hostname= dst_hostname= src_username= dst_username= mailto:src_email_id=src@abc.comdst_email_id={mailto:test@abc.com} startTime=2016-12-06 23:51:38+00 url=http://greatfilesarey.asia/QA/files_to_pcaps/74280968a4917da52b5555351eeda969.bin fileHash=bce00351cfc559afec5beb90ea387b03788e4af5 fileType=PE32 executable (GUI) Intel 80386, for MS Windows",
    "CEF:0|Microsoft|ATA|1.9.0.0|GatewayStartFailureMonitoringAlert|GatewayStartFailureMonitoringAlert|5|externalId=1018 cs1Label=url cs1=https://192.168.0.220/monitoring msg=The Gateway service on DC1 failed to start. It was last seen running on 12/12/2018 3:04:12 PM UTC."
};

var jsonOpt = new JsonSerializerOptions {
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
foreach (var testCase in testCases)
{
    var cefData = new CefData(testCase);
    Console.Clear();
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine(testCase);
    Thread.Sleep(1500);
    Console.Clear();
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine(JsonSerializer.Serialize(cefData, jsonOpt));
    Thread.Sleep(3000);   
}
Console.ResetColor();

public class CefData
{
    public string logVer { get; set; }
    public string vendor { get; set; }
    public string pname { get; set; }
    public string pver { get; set; }
    public string eventid { get; set; }
    public string eventName { get; set; }
    public string severity { get; set; }
    public Dictionary<string, string> extensions { get; set; }
        = new Dictionary<string, string>();

    public CefData(string msg)
    {
        // 用 | 分割欄位,但排除 \\| 這種情況
        var parts = Regex.Split(msg, @"(?<!\\)\|");
        if (parts.Length < 7)
        {
            throw new Exception("Invalid CEF message");
        }
        // 前七欄位的長度總和(需包含分隔符號),計算第八欄起始位置
        var pos = parts.Take(7).Sum(o => o.Length + 1);
        // 欄位值可能包含用 \\| 表示 |,還原之
        parts = parts.Select(o => o.Replace("\\|", "|")).ToArray();
        // 用解構賦值將欄位值指派給各個屬性
        (logVer, vendor, pname, pver, eventid, eventName, severity) = 
            (parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]);
        // 解析第八欄部分 (extensions)
        ParseExtensions(msg.Substring(pos));
    }

    void ParseExtensions(string extensionMsg)
    {
        // 用正規表達式找出 key 的位置,樣式為 xxxx=,但排除 \\= 這種情況
        var keyPos = 
            Regex.Matches(extensionMsg, @"(?<k>\S+?)(?<!\\)=").Cast<Match>()
            .Select(m => (index: m.Index, key: m.Groups["k"].Value)).ToArray();
        // 用 key 位置切割 value
        for (var i = 0; i < keyPos.Length; i++) {
            var key = keyPos[i].key;
            var start = keyPos[i].index + key.Length + 1;
            var end = i == keyPos.Length - 1 ? extensionMsg.Length : keyPos[i + 1].index;
            var val = extensionMsg.Substring(start, end - start);
            key = key.Replace("\\=", "=");
            val = val.Replace("\\=", "=").Trim();
            extensions[key] = val;
        }
    }
}

實測展示:

打完收工。

啊,Github Copilot,我要輕輕為妳唱首歌~ (羞昂風)

A demostration of how to use Github Copilot to help me to write C# code to parse CEF messages.


Comments

Be the first to post a comment

Post a comment