如何用 Python 手擼一個 GitLab 程式碼安全審計工具?

极狐GitLab發表於2024-10-09

本文分享了極狐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 審計事件全貌

需要注意的是:安全審計和安全審計流都屬於極狐GitLab 專業版及以上功能,但是當前可以申請免費試用 60天 https://dl.gitlab.cn/xup0wa40。在官網申請後會立馬收到一個 license,匯入即可!

安全審計功能

極狐GitLab 審計事件可以在例項、群組、專案三個級別檢視,路徑分別為(以 17.4 為例):

  • 例項:管理中心 --> 監控 --> 審計事件
  • 群組:群組 --> 安全 --> 審計事件
  • 專案:專案 --> 安全 --> 審計事件

比如新增一個專案,會產生對應的審計事件:

file

安全審計事件流

極狐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 本身支援新增第三方的流接收器。

可以在例項、群組級別新增事件流外部接收器:

  • 例項:管理中心 --> 監控 --> 審計事件 --> 事件流
  • 群組:群組 --> 安全 --> 審計事件 --> 事件流

比如在例項級別新增了一個事件流外部接收器:

file

主要引數:

  • 目的地名稱:寫明事件流目的地名稱,因為可以新增多個,因此需要用不同的名稱來區分
  • 目的地 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)

這時候對程式碼庫做一次變更,比如來個暴力的,直接刪除倉庫,看看能接收到什麼資料:

可以看到,將倉庫刪除的話,有兩個動作:

  1. 修改倉庫的名稱
{
    "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專案刪除了。

  1. 將倉庫標記為等待刪除
{
    "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被標記為等待刪除,這個可以在專案介面上看到:

file

接下來就要對不同的操作做一些區分了。因為不同的操作 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!")

然後對倉庫做一些操作,比如新建專案、刪除專案、克隆專案、推送程式碼等,就可以看到對應的訊息傳送到了釘釘群:

file

當然,如果覺得上面的這種方式不太容易理解的話,就做一個轉換,把 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)

相關文章