本卡屬 FR-195(Agent 資安修正),第 1 批 F3。修 FR-186 掃描的 X2 F1、K2 F2(皆 3/3 票);C4 F2 同條,嚴重度:中。
授權清冊是 Agent 的最後一道牆:只准連清冊上的主機。但清冊現在只比對「要掃的目標」,不比對「掃描工具本身在哪」(SonarQube、ZAP、OpenVAS 的伺服器位址)。有工具設定權限的人把工具位址填成內網任意一台,按「測試連線」,Agent 就替他連過去,還把對方的回應片段顯示回來——等於一個內網探測器。改成:工具伺服器位址也要在清冊內才連。
b8b1158 重核過,仍然有效)guidant_agent/scan/app/probe_runner.py:60 sync_ports.targets().check(params, connect) 只比對 params;credentials["base_url"] 完全沒比。同樣的漏在 guidant_agent/scan/app/local_runs.py:116 與 guidant_agent/scan/app/task_executor.py:82。config.detection_tools 與各 connector——只有三支:sonarqube(base_url)、zap(base_url)、openvas(base_url,另有 port)。其餘(nmap、openscap、inspec、gcb)的連線對象在 params 的 hosts/host,已經有比對;sbom-scan 不連線。127.0.0.1、::1、localhost)不查清冊——那條線出不了這台機器,ZAP 常跟 Agent 同機;判斷用既有的 guidant_agent/scan/adapters/connectors/tool_tls.py:31 is_loopback_host。②其餘一律要在清冊內。③清冊的內容是主系統下發的「設備清冊內網 IP+授權網段+主機名對照」,所以客戶的工具伺服器要先在主系統登記成設備才連得到——這是刻意的行為變更,見下面「行為變更」。guidant_agent/scan/app/tool_server_gate.py(新檔)
guidant_agent/scan/domain/tool_params.py
guidant_agent/scan/app/probe_runner.py
guidant_agent/scan/app/local_runs.py
guidant_agent/scan/app/task_executor.py
.claude/rules/security/external-calls-and-tools.md
test/test_local_runs.py
test/test_console_ops.py
test/test_console_scan_routes.py
test/test_console_view_audit.py
test/test_detection_probe_route.py
test/test_task_executor.py
sync_ports.targets().check(...) 一個字都不要動、也不要搬走。資安守衛 test/test_fr166_guards.py::test_every_entry_checks_allowed_targets 是用讀原始碼的方式,檢查 _run_one、run_probe、start 這三支函式裡直接有清冊呼叫;搬進別的檔守衛就紅(首腦試過)。做法是在那行後面加一行。guidant_agent/scan/app/tool_server_gate.py,只有一支函式 check_tool_server(tool: Optional[str], credentials: Optional[dict]) -> None:用 credential_connect_keys(tool) 拿到要比的鍵,從 credentials 取值;每個值取主機段(沒有 :// 先補 https://,urllib.parse.urlsplit(value).hostname),tool_tls.is_loopback_host(host) 為真就略過;剩下的組成 dict,非空才呼叫 sync_ports.targets().check(<那個 dict>, <鍵>)(不過會拋 TargetNotAuthorized)。import:from guidant_agent.scan.adapters.connectors import tool_tls、from guidant_agent.scan.app import sync_ports(scan/app import scan/adapters 是既有做法,scan/app/catalog.py:10 就這樣寫,分層守衛允許)。首腦試做過,整支約 20 行。guidant_agent/scan/domain/tool_params.py:ToolParams 加一欄 credential_connect: tuple = ()(語意同 connect,但鍵在 credentials 裡)。sonarqube、zap、openvas 三列各加 credential_connect=(("base_url", URL),)。加一支 credential_connect_keys(tool) -> tuple:沒登記的工具回空 tuple (),不是 None(清冊的 check(x, None) 意思是「這工具沒宣告、一律拒」,這裡不要觸發)。檔頭 docstring 補一句現況:「工具伺服器位址在 credentials 裡,一樣要登記才受清冊比對」。guidant_agent/scan/app/probe_runner.py::60 那行後面加 tool_server_gate.check_tool_server(code, credentials)(在同一個 try 裡,:61 的 except TargetNotAuthorized 會接住);:50 的函式內 import 那行加上 tool_server_gate。guidant_agent/scan/app/local_runs.py::117 credential_resolver.resolve({}, tool)(原本只驗有沒有設定、回傳值丟掉)改成 tool_server_gate.check_tool_server(tool, credential_resolver.resolve({}, tool));:23 的 import 加上 tool_server_gate。行數不變。guidant_agent/scan/app/task_executor.py::82 後面加一行 tool_server_gate.check_tool_server(tool, credential_resolver.resolve(task, task.get("detection_tool_code")));:83 改成 except (TargetNotAuthorized, credential_resolver.CredentialsNotConfigured) as e:(帳密沒設定時 resolve 會拋這個;原本是在 :101 建 connector 時才拋、回報成 failed,現在提前到這裡,由 _refuse 回報,內容等價);:80 拆成兩行 tool = tool_code(...)、connect = connect_keys(tool);:24 的 import 加上 tool_server_gate。connect 變數要留著,:88 還在用。_run_one 在函式行數白名單上(上限 86):上面的改法會讓它多 2 行。把 :78-79 那兩行註解併成一行(內容只留「連線前統一比對;ack 之後才擋,因為雲端狀態機不允許 pending 直接轉 failed」),或把 :92-94、:96-98 兩段各三行的施工日誌型註解(開頭是 CM-934、FR-058 T-X.2)各縮成一行——那種註解本來就該進 commit message。改完用 test/test_file_and_function_size.py 確認綠。🔴 這張卡不要改那支測試檔本身(同批的 F6 會改它,兩邊都動合回會衝突);_run_one 行數 ≤ 86 測試就是綠的,不用動白名單數字。:90 那個大 try:它在 running_tasks.register 之後,移進去會讓被拒的任務先被登記成「執行中」。paths 命中,否則 test/test_security_rules_cover_sources.py::test_every_source_file_hit_by_a_rule 會紅(首腦試過):.claude/rules/security/external-calls-and-tools.md 的 paths 加一行 "guidant_agent/scan/app/tool_server_gate.py",並在該檔「目標只能來自授權清冊或主系統下發的設定…」那一節補一句現況(工具伺服器位址也過清冊、本機迴路例外、鍵登記在 tool_params.py 的 credential_connect)。只寫現況,不寫「這次修了什麼」。probe_runner.py:77-81 的 friendly_message(e) 照舊,這張卡不動它。