diff --git a/.gitmodules b/.gitmodules index 1febe96..7dc4707 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "include/coreruleset"] path = include/coreruleset url = https://github.com/coreruleset/coreruleset +[submodule "include/gotestwaf"] + path = include/gotestwaf + url = https://github.com/wallarm/gotestwaf.git diff --git a/README.md b/README.md index f18e155..c4dcd5e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,16 @@ SecRule REQUEST_URI "@rx admin" "id:1,phase:1,deny,status:401" ## Dependency -`coreruleset` +### git modules + +- `coreruleset` +- `gotestwaf` + +Install all modules: + +```shell +git submodule update --init --recursive +``` ## Requirements diff --git a/README_Zh-TW.md b/README_Zh-TW.md index e91926a..0bbb9e3 100644 --- a/README_Zh-TW.md +++ b/README_Zh-TW.md @@ -24,7 +24,16 @@ SecRule REQUEST_URI "@rx admin" "id:1,phase:1,deny,status:401" ## 相依套件 -`coreruleset` +### git modules + +- `coreruleset` +- `gotestwaf` + +安裝所有的模組: + +```shell +git submodule update --init --recursive +``` ## 系統需求 diff --git a/api_config.toml b/api_config.toml index 5f3ea21..f8d8de4 100644 --- a/api_config.toml +++ b/api_config.toml @@ -4,4 +4,5 @@ debug_flag = false quantity = 5 [rule] -overwrite = false +overwrite = true +append_type = "mapping" diff --git a/include/gotestwaf b/include/gotestwaf new file mode 160000 index 0000000..68f2433 --- /dev/null +++ b/include/gotestwaf @@ -0,0 +1 @@ +Subproject commit 68f2433f9c0cfdd9fe287d2a31fada66ddf7d624 diff --git a/src/api/fetch.py b/src/api/fetch.py index b523100..585ede3 100644 --- a/src/api/fetch.py +++ b/src/api/fetch.py @@ -8,27 +8,39 @@ class FetchUtil: """ - 負責擷取漏洞情資網站情報 - - ### 成員函數 - - fetching() -> str + 負責擷取漏洞情資網站情報。 + + 屬性 + ---------- + URL_BASE : str + 目標網站的基礎 URL。 + ENDPOINT : str + 漏洞資訊的 API 端點。 + ROUTE : str + 個別漏洞頁面的路由。 """ @staticmethod def __fetch_page(driver: Driver, url: str, output_name: str, debug_flag: bool) -> str: - """ - 使用提供的 driver 抓取單一頁面並返回其 HTML。 - - 如果 `debug_flag` 為 True,則保存 HTML 和截圖。 - - ### 參數 - - driver (Driver) - - url (str) - - output_name (str) - - debug_flag (bool) - - ### 回傳 - str: HTML 原始碼 + """使用提供的 driver 抓取單一頁面並返回其 HTML。 + + 如果 `debug_flag` 為 True,則會保存 HTML 和截圖。 + + 參數 + ---------- + driver : Driver + 用於抓取的 SeleniumBase Driver 實例。 + url : str + 要抓取頁面的 URL。 + output_name : str + 保存 HTML 和截圖檔案的基本名稱。 + debug_flag : bool + 一個布林標誌,用於啟用保存 HTML 和截圖以進行除錯。 + + 回傳 + ------- + str + 抓取頁面的 HTML 原始碼。 """ driver.uc_open_with_reconnect(url, 6) driver.uc_gui_click_captcha() @@ -43,17 +55,21 @@ def __fetch_page(driver: Driver, url: str, output_name: str, debug_flag: bool) - @staticmethod def fetching(quantity: int = 0, debug_flag: bool = False) -> str: - """ - 使用 seleniumbase 開啟網頁,並取得指定數量的原始資料。 + """使用 seleniumbase 開啟網頁,並取得指定數量的原始資料。 若 `debug_flag` 為 True,將會在當前路徑下儲存網頁截圖與 HTML。 - ### 參數 - - quantity (int): 擷取情資頁面數列 - - debug_flag (bool): 測試旗標 - - ### 回傳 - str: 擷取成功的 HTML 原始碼 + 參數 + ---------- + quantity : int, optional + 擷取情資頁面的數量,預設為 0。 + debug_flag : bool, optional + 測試旗標,用於啟用保存截圖與 HTML 以進行除錯,預設為 False。 + + 回傳 + ------- + str + 所有成功擷取的 HTML 原始碼的串聯。 """ raw_data: str = "" @@ -68,4 +84,4 @@ def fetching(quantity: int = 0, debug_flag: bool = False) -> str: if __name__ == "__main__": - FetchUtil.fetching(1, True) + FetchUtil.fetching(1, True) \ No newline at end of file diff --git a/src/api/parser.py b/src/api/parser.py index 88e5b89..92ab039 100644 --- a/src/api/parser.py +++ b/src/api/parser.py @@ -8,17 +8,22 @@ class Parser: """ - 負責解析 HTML 內容,提取漏洞資料,並整理成 API 格式 + 負責解析 HTML 內容,提取漏洞資料,並整理成 API 格式。 + + 屬性 + ---------- + manifest : Dict[int, Dict[str, str]] + 解析後的漏洞資訊,以索引為鍵,包含漏洞 ID、標題、URL 和日期等資訊的字典。 """ def __init__(self, html_raw_data: str) -> None: """ - 解析 HTML,將漏洞資料轉換為 API 格式 + 初始化 Parser 實例,解析 HTML 並將漏洞資料轉換為 API 格式。 - Parameters - --- - html_raw_data: str - HTML 原始碼 + 參數 + ---------- + html_raw_data : str + 包含漏洞資訊的 HTML 原始碼。 """ dates: List[str] = self.__find_date(html_raw_data) endpoints: List[tuple[str, str]] = self.__find_endpoint(html_raw_data) @@ -29,51 +34,51 @@ def __init__(self, html_raw_data: str) -> None: def __find_endpoint(self, data: str) -> List[tuple[str, str]]: """ - 從 HTML 中擷取漏洞 ID 和標題 + 從 HTML 中擷取漏洞 ID 和標題。 - Parameters - --- - data: str - HTML 原始碼 + 參數 + ---------- + data : str + 包含漏洞資訊的 HTML 原始碼。 - Return - --- + 回傳 + ------- List[tuple[str, str]] - 包含 (漏洞 ID, 標題) 的列表 + 包含 (漏洞 ID, 標題) 元組的列表。 """ pattern: str = r'\/vulnerability\/([A-Z]+-\d{4}-\d{5})">(.+)<\/a><\/h4>' return re.findall(pattern, data) def __find_date(self, data: str) -> List[str]: """ - 從 HTML 中擷取漏洞發現日期 + 從 HTML 中擷取漏洞發現日期。 - Parameter - --- - data: str - HTML 原始碼 + 參數 + ---------- + data : str + 包含漏洞資訊的 HTML 原始碼。 - Return - --- + 回傳 + ------- List[str] - 包含日期的列表,格式為 YYYY/MM/DD + 包含日期字串的列表,格式為 YYYY/MM/DD。 """ pattern: str = r"(\d{4}/\d{2}/\d{2})" return re.findall(pattern, data) - def sort_by_date(self, api_data: Dict[int, Dict[str, str]]) -> Dict[int, list]: + def sort_by_date(self, api_data: Dict[int, Dict[str, str]]) -> Dict[int, Dict[str, List[Dict[str, str]]]]: """ - 依年份與月份整理漏洞資料 + 依年份與月份整理漏洞資料。 - Parameters - --- - api_data: Dict[int, Dict[str, str]] - 原始漏洞資料 + 參數 + ---------- + api_data : Dict[int, Dict[str, str]] + 原始漏洞資料,鍵為索引,值為包含漏洞資訊的字典。 - Return - --- - Dict[int, list] - 依年份、月份整理後的漏洞資料 + 回傳 + ------- + Dict[int, Dict[str, List[Dict[str, str]]]] + 依年份和月份整理後的漏洞資料,外層鍵為年份,內層鍵為月份(字串格式),值為該年該月漏洞資訊的列表。 """ sorted_data = defaultdict(lambda: {str(month): [] for month in range(1, 13)}) @@ -93,4 +98,4 @@ def sort_by_date(self, api_data: Dict[int, Dict[str, str]]) -> Dict[int, list]: json.dump(parser.manifest, fp, ensure_ascii=False) with open("api_records_time_sensitive.json", "w") as fp: - json.dump(parser.sort_by_date(parser.manifest), fp, ensure_ascii=False) + json.dump(parser.sort_by_date(parser.manifest), fp, ensure_ascii=False) \ No newline at end of file diff --git a/src/api/rules.py b/src/api/rules.py index 3520158..226c5a3 100644 --- a/src/api/rules.py +++ b/src/api/rules.py @@ -1,79 +1,119 @@ import json import os import tomllib - from mapping import RuleAppendType, Directory, VulnerabilityType from pathlib import Path, PurePath from re import Pattern, compile from subprocess import run from typing import Any, Dict, Optional - -RULE_PATTERN: Pattern = compile(r"^SecRule REQUEST_URI") +RULE_PATTERN: Pattern = compile(r"^SecRule\s.+") +RULE_INCLUSION_PATTERN: Pattern = compile(r"(\s+|.+)\"!*@pmFromFile\s(.+.data)\"\s\\") +INCLUDE: str = "include/coreruleset/rules/" class RuleUtil: """ - 規則集管理工,用於從來源路徑讀取規則檔、更新規則集後輸出到目的路徑 - - Constructor - --- - RuleUtil() - 初始化規則管理工具,設定來源路徑與目的路徑 - - Methods - --- - extract_rule() - 擷取 `.conf` 規則檔案,可選擇擷取所有規則或僅擷取符合 API 記錄的規則。 - rule_dump() - 把規則寫入進規則檔中,如果有 `overwrite` 則不產生備份檔 + 規則集管理工具,用於從來源路徑讀取規則檔,更新規則集後輸出到目的路徑。 + + 屬性 + ---------- + source : Path + 規則來源目錄的路徑。 + destination : Path + 目標輸出檔案的路徑。 + rules : List[str] + 儲存待寫入規則的列表。 """ def __init__(self, source: Path, destination: Path) -> None: """ 初始化 `RuleUtil` 類別。 - Parameters - --- - source: Path - 規則來源目錄 - destination: Path - 目標輸出檔案 - - Methods - --- - extract_rule() - 從 `/CRS/rules` 擷取規則集 - dump_rule() - 把 CRS 規則寫入規則檔 - - Return - --- - None + 參數 + ---------- + source : Path + 規則來源目錄的路徑。 + destination : Path + 目標輸出檔案的路徑。 """ self.source: Path = source self.destination: Path = destination self.rules = [] def __rule_append(self, rule_path: Path, reason: Optional[str] = None) -> None: + """ + 將指定規則檔案中的規則附加到內部規則列表中。 + + 參數 + ---------- + rule_path : Path + 要附加規則的 `.conf` 檔案路徑。 + reason : Optional[str], optional + 附加規則的原因或相關資訊,將作為註解添加到規則前,預設為 None。 + """ with open(rule_path, "r", encoding="utf-8") as fp: - flag: bool = False + multiline: bool = False + for line in fp: - if flag: - self.rules.append(f"{' '*4}{line.replace("block", "deny").strip()}") + content = line.strip() + + # 處理有縮排的多行規則 + if multiline: + if RULE_INCLUSION_PATTERN.search(line.rstrip()): + filename = RULE_INCLUSION_PATTERN.search(line.rstrip()).group(2) + modified_payload = line.rstrip().replace(filename, f"{INCLUDE}{filename}") + print(line.rstrip()) + print(modified_payload) + self.rules.append(f"{modified_payload}") + else: + self.rules.append(line.rstrip()) + + # 處理規則內容 if RULE_PATTERN.match(line): - if reason is not None: - self.rules.append(f"\n# {reason}") - self.rules.append(f"{line.strip()}") - flag = True - if not line.strip().endswith("\\"): - flag = False + self.rules.append("\n") + self.rules.append(f"# {reason}") if reason is not None else None + + # 處理檔案引入的路徑 + if RULE_INCLUSION_PATTERN.match(content): + filename = RULE_INCLUSION_PATTERN.search(content).group(2) + modified_payload = content.replace(filename, f"{INCLUDE}{filename}") + self.rules.append(f"{modified_payload}") + else: + self.rules.append(f"{content}") + multiline = True + + if content == "chain\"": + continue + + if not content.endswith("\\"): + multiline = False def __all(self, root: Path, file: Path) -> None: + """ + 處理所有 `.conf` 檔案,將其規則附加到內部規則列表中。 + + 參數 + ---------- + root : Path + 目前正在處理的目錄路徑。 + file : Path + 目前正在處理的檔案名稱。 + """ rule_path = os.path.join(root, file) self.__rule_append(Path(rule_path)) def __mapping(self, root: Path, file: Path) -> None: + """ + 根據 API 報告中的漏洞類型,有選擇地附加 `.conf` 檔案中的規則。 + + 參數 + ---------- + root : Path + 目前正在處理的目錄路徑。 + file : Path + 目前正在處理的檔案名稱。 + """ v_types: list = [] with open(Directory.API_FILE.value, "r", encoding="utf-8") as report_fp: vulnerability_report = json.load(report_fp) @@ -87,12 +127,13 @@ def __mapping(self, root: Path, file: Path) -> None: def extract_rule(self, option: RuleAppendType = RuleAppendType.ALL) -> None: """ - 擷取 `.conf` 規則檔案,可選擇擷取所有規則或僅擷取符合 API 記錄的規則 + 擷取 `.conf` 規則檔案,可選擇擷取所有規則或僅擷取符合 API 記錄的規則。 - Parameters - --- - option: RuleAppendType = RuleAppendType.MAPPING - 擷取規則的模式,預設為 `AppendRule.MAPPING` + 參數 + ---------- + option : RuleAppendType, optional + 擷取規則的模式,預設為 `RuleAppendType.ALL`。 + 可選值包括 `RuleAppendType.ALL` (擷取所有規則) 和 `RuleAppendType.MAPPING` (僅擷取符合 API 記錄的規則)。 """ option_manifest: Dict[object] = {RuleAppendType.ALL: self.__all, RuleAppendType.MAPPING: self.__mapping} @@ -103,12 +144,13 @@ def extract_rule(self, option: RuleAppendType = RuleAppendType.ALL) -> None: def rule_dump(self, overwrite: bool = False) -> None: """ - 把規則寫入進規則檔中,如果有 `overwrite` 則不產生備份檔 + 將內部規則列表中的規則寫入到目標規則檔案中。 - Parameters - --- - overwrite: bool = False - 決定是否複寫、不產生備份檔,預設為 `False` + 參數 + ---------- + overwrite : bool, optional + 決定是否覆寫目標檔案而不產生備份檔,預設為 `False`。 + 如果為 `False`,則在寫入前會先備份原始目標檔案(加上 `.old` 副檔名)。 """ if not overwrite: run(["cp", self.destination, f"{self.destination}.old"]) @@ -120,13 +162,16 @@ def rule_dump(self, overwrite: bool = False) -> None: def update_rule(): + """ + 更新規則的主要函式,從設定檔讀取配置,提取規則並寫入規則檔。 + """ with open(Directory.CONFIG.value, "rb") as config: config: dict[str, Any] = tomllib.load(config) rule_manager: RuleUtil = RuleUtil(Directory.CRS.value, Directory.RULE.value) - rule_manager.extract_rule() + rule_manager.extract_rule(RuleAppendType(config["rule"]["append_type"])) rule_manager.rule_dump(config["rule"]["overwrite"]) if __name__ == "__main__": - update_rule() + update_rule() \ No newline at end of file