本文分享了極狐GitLab 的程式碼安全審計 & 審計事件流功能,而且演示如何用 Python 編寫一個安全審計流接收器,透過接收安全審計日誌並分析後發出通知。
極狐GitLab 為 GitLab 中文發行版,中文版本對中國使用者更友好,可以一鍵私有化部署,也可以直接使用 SaaS(JihuLab.com)。本文講述的安全審計 & 審計事件流屬於專業版 & 旗艦版功能。可以申請 60 天專業版免費試用 https://dl.gitlab.cn/xup0wa40 來體驗該功能功能。
本文內容比較豐富,主要分為以下幾個部分:
- 關於程式碼安全審計
- 極狐GitLab 安全審計
- 極狐GitLab 安全審計流
- 用 Python 構建審計流目的地接受器
- 結束語
- 程式碼附錄
程式碼安全審計
所謂程式碼安全審計,就是對程式碼倉庫的所有操作進行相應的記錄,目的是為了方便安全部門對程式碼倉庫的操作進行安全審計,或者是程式碼倉庫出問題以後,透過審計日誌發現問題所在。大白話說就是看看誰對倉庫做了什麼操作,比如常規的倉庫克隆、拉取、推送,當然最可怕的就是傳說中的刪除跑路或者修改倉庫的可見性(從私有修改為公開,很多著名的資訊洩露就是由倉庫可見性修改引起的)。還有這些年很常見的,有員工在離職前瘋狂下載程式碼,然後當作自己的智慧財產權,從而帶離公司。
這每一件發生在公司內部都是一件大事,畢竟現在數字化時代,很多企業的核心資產就是“一坨坨”的程式碼。那能夠避免這種事情發生或者在事件發生後能及時找到“肇事者”的方法其實就是程式碼安全審計,這玩意的英文名稱叫做Audit event。當然,國內很多開發者可能也叫做“程式碼追蹤”,“程式碼洩露之類的”。Whatever,不管叫什麼,核心就是希望能夠用一些手段來保護程式碼的安全,不要被偷、不要被刪,所有的操作都要留下痕跡,而且這痕跡至少要包含三個要素:
- Who:事件的操作主體。主要是指對程式碼進行操作的人,一般來講當然就是公司內部的研發人員啦;
- When:事件發生的時間。主要是指操作是什麼時間段發生的;
- What:操作主體做了什麼具體操作。主要就是看看對倉庫程式碼都做了啥,克隆還是推送,拉取還是刪庫等。
說半天,這玩意到底咋做呢?
說白了,只能依靠平臺自身,平臺要是自帶了這個功能,那就方便很多,要是不帶就沒辦法了。
極狐GitLab 安全審計 & 安全審計流
好巧不巧的是,GitLab 本身就自帶了這個功能,而且隨著版本的迭代更新,審計的事件也越來越多,到目前為止(最新為 17.4 版本)審計事件已經多到130+ 項,從例項到群組、到專案,都有。
需要注意的是:安全審計和安全審計流都屬於極狐GitLab 專業版及以上功能,但是當前可以申請免費試用 60天 https://dl.gitlab.cn/xup0wa40。在官網申請後會立馬收到一個 license,匯入即可!
安全審計功能
極狐GitLab 審計事件可以在例項、群組、專案三個級別檢視,路徑分別為(以 17.4 為例):
- 例項:管理中心 --> 監控 --> 審計事件
- 群組:群組 --> 安全 --> 審計事件
- 專案:專案 --> 安全 --> 審計事件
比如新增一個專案,會產生對應的審計事件:
安全審計事件流
極狐GitLab 審計事件流功能可以將審計事件流傳送到外部的流資料系統(可以接受並處理 JSON 格式的資料),然後再由流資料系統對資料進行分析、儲存、視覺化及告警等操作。
{
"severity": "INFO",
"time": "2024-09-26T08:54:16.339Z",
"correlation_id": "01J8PRKGB20R989VA752DN9ES4",
"meta.caller_id": "PostReceive",
"meta.remote_ip": "127.0.0.1",
"meta.feature_category": "source_code_management",
"meta.user": "root",
"meta.user_id": 1,
"meta.project": "devsecops/ai",
"meta.root_namespace": "devsecops",
"meta.client_id": "user/1",
"meta.root_caller_id": "POST /api/:version/internal/post_receive",
"id": 274,
"author_id": 1,
"entity_id": 7,
"entity_type": "Project",
"details": {
"push_access_levels": ["Maintainers"],
"merge_access_levels": ["Maintainers"],
"allow_force_push": false,
"code_owner_approval_required": false,
"event_name": "protected_branch_created",
"author_name": "Administrator",
"author_class": "User",
"target_id": 7,
"target_type": "ProtectedBranch",
"target_details": "main",
"custom_message": "Added protected branch with [allowed to push: [\"Maintainers\"], allowed to merge: [\"Maintainers\"], allow force push: false, code owner approval required: false]",
"ip_address": "218.60.118.175",
"entity_path": "devsecops/ai"
},
"ip_address": "218.60.118.175",
"author_name": "Administrator",
"entity_path": "devsecops/ai",
"target_details": "main",
"created_at": "2024-09-26T08:54:16.308Z",
"target_type": "ProtectedBranch",
"target_id": 7,
"push_access_levels": ["Maintainers"],
"merge_access_levels": ["Maintainers"],
"allow_force_push": false,
"code_owner_approval_required": false,
"event_name": "protected_branch_created",
"author_class": "User",
"custom_message": "Added protected branch with [allowed to push: [\"Maintainers\"], allowed to merge: [\"Maintainers\"], allow force push: false, code owner approval required: false]"
}
極狐GitLab 可以將審計日誌以 JSON 的方式往外發,只要有一個服務能夠接受這些 JSON 格式的資料就可以。而且極狐GitLab 本身支援新增第三方的流接收器。
可以在例項、群組級別新增事件流外部接收器:
- 例項:管理中心 --> 監控 --> 審計事件 --> 事件流
- 群組:群組 --> 安全 --> 審計事件 --> 事件流
比如在例項級別新增了一個事件流外部接收器:
主要引數:
- 目的地名稱:寫明事件流目的地名稱,因為可以新增多個,因此需要用不同的名稱來區分
- 目的地 URL:事件流目的地的地址,也就是接受 JSON 資料的服務地址。這也是本文的核心,這個服務可以自己構建一個。
用 Python 構建審計流目的地接受器
用 Python 主流的 web 框架都可以構建此類接收器,本文使用常用的 fastapi 來構建,程式碼如下:
from fastapi import FastAPI
import uvicorn
app = FastAPI()
@app.post("/jh-gitlab")
async def gitlab_payload(data: dict):
audit_event_info = {
"Action": data['details']['custom_message'],
"Author": data['details']['author_name'],
"IP Address": data['details']['ip_address'],
"Entity Path": data['details']['entity_path'],
"Target Details": data['target_details']
}
print(audit_event_info)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
前面看到實際的審計事件日誌有很多資訊,但是一般想要的就是開頭提到的Who、When、What,對應日誌裡面的欄位基本就是action、author、ipaddress、entity_path、target_details。所以,接收到資料以後,先把這些資料取出來,然後做下一步。
將上面的程式碼存到一個 python 檔案裡面,然後在伺服器上執行起來即可:
python3 main.py
INFO: Started server process [2140728]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
這時候對程式碼庫做一次變更,比如來個暴力的,直接刪除倉庫,看看能接收到什麼資料:
可以看到,將倉庫刪除的話,有兩個動作:
- 修改倉庫的名稱
{
"Action": "Changed name from DevSecOps / ai to DevSecOps / ai-deleted-7",
"Author": "Administrator",
"IP Address": "36.133.246.166",
"Entity Path": "devsecops/ai-deleted-7",
"Target Details": "devsecops/ai-deleted-7"
}
從上面的資訊就能看出,是 adminstor(對,也就是管理員)把 devsecops群組下面的 ai專案刪除了。
- 將倉庫標記為等待刪除
{
"Action": "Project marked for deletion",
"Author": "Administrator",
"IP Address": "36.133.246.166",
"Entity Path": "devsecops/ai-deleted-7",
"Target Details": "ai-deleted-7"
}
從上面的資訊就能看出,專案 ai被標記為等待刪除,這個可以在專案介面上看到:
接下來就要對不同的操作做一些區分了。因為不同的操作 action 的內容也不盡相同。當然,重要的是這些事件發生以後,如果想特別關注,那就搞一個通知傳送機制。下面是一個傳送到釘釘群的參考程式碼:
def notification(payload: dict):
webhook_url = "https://oapi.dingtalk.com/robot/send?access_token=你的釘釘token"
# 傳送訊息的內容
message = {
"msgtype": "text",
"text": {
"content" : "GitLab: {}".format(json.dumps(payload))
}
}
# 傳送 POST 請求
headers = {'Content-Type': 'application/json'}
response = requests.post(webhook_url, data=json.dumps(message), headers=headers)
# 對結果進行判斷
if json.loads(response.text)['errcode'] == 0:
print("Send Message Success!")
else:
print("Send Message Failed!")
然後對倉庫做一些操作,比如新建專案、刪除專案、克隆專案、推送程式碼等,就可以看到對應的訊息傳送到了釘釘群:
當然,如果覺得上面的這種方式不太容易理解的話,就做一個轉換,把 Action 的內容轉化成任何人都能看懂的訊息,畢竟 git-upload-pack對很多人來說都不是很常見。就把這個任務交給對此感興趣的小夥伴吧。
結束語
程式碼安全審計是安全合規非常重要的一環,但是同時也是很多企業容易忽略的一環,究其原因是能夠具備如此完整功能的產品不是很多,因為這需要產品不斷地持續迭代更新,而且得從早期就做好產品規劃。而在這一點上,GitLab 是值得稱讚的。當然,說再多也不去親自去體驗。歡迎感興趣的小夥伴申請專業版免費使用 license 來體驗完整的功能。
附錄
把這個測試用的程式碼完整附錄如下:
from fastapi import FastAPI
import uvicorn
import requests
import json
app = FastAPI()
@app.post("/jh-gitlab")
async def gitlab_payload(data: dict):
# 抓取審計事件中的主要資訊
audit_event_info = {
"Action": data['details']['custom_message'],
"Author": data['details']['author_name'],
"IP Address": data['details']['ip_address'],
"Entity Path": data['details']['entity_path'],
"Target Details": data['target_details']
}
print(audit_event_info)
# 傳送訊息通知
notification(audit_event_info)
def notification(payload: dict):
webhook_url = "https://oapi.dingtalk.com/robot/send?access_token=你的釘釘 webhook token"
# 傳送訊息的內容
message = {
"msgtype": "text",
"text": {
"content" : "GitLab: {}".format(json.dumps(payload))
}
}
# 傳送 POST 請求
headers = {'Content-Type': 'application/json'}
response = requests.post(webhook_url, data=json.dumps(message), headers=headers)
print(response.text)
if json.loads(response.text)['errcode'] == 0:
print("Send Message Success!")
return True
else:
print("Send Message Failed!")
return json.loads(response.text)['errmsg']
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)