本卡屬 FR-195(Agent 資安修正),第 1 批 F1。修 FR-186 掃描的 S7 F1(3/3 票)+舊觀察 O1,嚴重度:高。
升級時,主機會把新映像裡的 Agent 程式樹取出來,叫現行版 Agent 逐檔算雜湊、跟原廠簽章清單比對,過了才讓 root 執行新版的升級腳本。現在的比對只「照清單逐檔算」,而且算雜湊時會跟著捷徑檔(symlink)走,清單外多出來的檔也完全不看。攻擊者可以把清單上的 upgrade_runner.sh 換成捷徑、指向樹裡另一個清單沒列的檔:驗章時看到的是正版內容,但主機驗完之後又從映像重新取一次腳本來執行,那次取到的可以是另一份。這張卡把入口封住:樹裡只要有捷徑檔、特殊檔、或清單沒列的檔,驗章直接不過。
b8b1158 重核過,仍然有效)guidant_agent/platform/agent_tree_verify.py:24 只檢查「根目錄本身」是不是捷徑;:27 呼叫 common/integrity/startup_gate.py:146 _compare_files,它只照 manifest["files"] 逐檔 _sha256_of(root / rel_path),讀檔會跟著捷徑走,清單外多出來的檔完全不看。 主機端現況(主線 b8b1158,FR-193 改過,只為讓你看懂脈絡,這張卡不改 shell):scripts/install/lib/image_verify.sh:33 用 image_copy_out(deploy/install.sh:240,docker cp -L)把樹取到主機,:48-54 建一個現行映像的容器、用 docker cp 把樹放進容器的 /verify、跑 evidence-agent verify-tree /verify;:37 驗完就把那份樹刪掉。之後 scripts/install/lib/host_lib.sh:17 hl_save 再從映像取一次 install-lib 存成主機副本給 root 執行。guidant-ai-agent:1.7.4-0d7fe252 的 /opt/guidant-agent/current/agent):270 個檔、0 個捷徑檔、0 個特殊檔;簽章清單列 268 個,磁碟多出的兩個正好是 manifest.json 與 manifest.sig。所以「有捷徑一律拒、清單外一律拒(那兩個除外)」不會擋到正版升級,不用再查。scripts/build/gen_agent_manifest.py:45 EXCLUDE_PATTERNS:manifest.json、manifest.sig、*/__pycache__/*、__pycache__/*、*.pyc。驗章端的「清單外允許清單」要跟它一致。guidant_agent/platform/agent_tree_verify.py
common/integrity/startup_gate.py
test/test_agent_tree_verify.py
common/integrity/startup_gate.py 新增一支 _scan_tree(root: Path, manifest: dict) -> list[str]:os.walk(root, followlinks=False) 走整棵樹,對每個項目 os.lstat,回傳人看得懂的不符清單。三種情況各列一筆:①目錄或檔案是捷徑(stat.S_ISLNK);②不是一般檔也不是目錄(裝置檔、FIFO、socket);③一般檔的相對路徑不在 manifest["files"],且不符合下面的允許樣式。_UNLISTED_ALLOWED = ("manifest.json", "manifest.sig", "*/__pycache__/*", "__pycache__/*", "*.pyc"),用 fnmatch 比對,旁邊一行註解寫「與 scripts/build/gen_agent_manifest.py 的 EXCLUDE_PATTERNS 同一份,改一邊要改另一邊」。不要 import 那支建置腳本(它不在出貨產物裡)。agent_tree_verify.py 的 main:在 _load_verified_manifest 之後、_compare_files 之前呼叫 _scan_tree,有任何一筆就 raise _GateFailure(f"程式樹含不允許的項目({len(bad)} 項,例如 {bad[0]})")。先掃樹再算雜湊,順序不要反。run_startup_gate,startup_gate.py:201 附近)這張卡不改:開機時 __pycache__ 以外也可能有執行期產物,改了有把客戶 Agent 鎖死開不了機的風險。只動升級驗章這一條;在 _scan_tree 的 docstring 寫一句「目前只有 verify-tree 呼叫;開機閘要接之前先確認執行期不會在樹裡產生新檔」。_sha256_of 改成用 os.open(path, os.O_RDONLY | os.O_NOFOLLOW) 開檔(多一層保險:就算掃樹漏了,遇到捷徑也會直接 OSError,_compare_files 既有的 except OSError 會記成「讀取失敗」)。O_NOFOLLOW 在 Linux 與 macOS 都有,不用加平台判斷。正版升級包不受影響(已實查)。被拒的只會是被動過手腳的映像,錯誤訊息出現在升級進度與 Agent 主控台的升級失敗原因。
在 test/test_agent_tree_verify.py 加三支簡單斷言(各五行內,沿用檔內既有的 _tree() 建樹 helper):①清單上的檔換成指向樹內另一個檔的捷徑 → 回 EXIT_REJECTED;②樹裡多一個清單沒列的一般檔 → 回 EXIT_REJECTED;③樹裡多 __pycache__/x.pyc 與 manifest.json → 照樣通過。這是「驗章不變量」,屬於准寫測試的那一類;不要寫突變表、不要再多。