這批修的是**「系統相信誰」這件事本身壞掉的十四條:代理程式打進來不必證明身分(#1,最嚴重)、防竄改的那把驗章工具可以被整支換掉(M08 五條)、一家客戶底下任何一層的管理員都改得動全系統共用的登入與日誌設定(#19/#22)、交出去的檔案與日誌沒編碼(#20/#93),再加一條停權後通行證照樣能用(M13 第 17 條)。排在第 5 批不是因為不嚴重——#1 是全站最嚴重那條——是因為這批每條都要先定信任鏈的形狀**(拿什麼當身分證明、哪一層才算「客戶自己」),方向不定就會各棒各修出不同的一套。現在可以開,但卡 5A-1(代理程式身分)與卡 5A-2(防竄改)要先等決策者裁方向,其餘三張(客戶層級設定、交出去沒把關、停權即時生效)方向已拍板,可立即派。依賴:不依賴前四批;卡 5A-3 與第 5 批 B/C 子集若也動 system_config 相關檔,序列組要對齊(本子集內已標)。
| 卡 | 卡名(做什麼) | repo/套件 | 涵蓋 SUMMARY # | 建議 model/effort | 序列組 |
|---|---|---|---|---|---|
| 5A-1 | 讓代理程式每次打進來都要證明身分,並讓「撤銷」真的斷得乾淨 | agent repo(evidence-agent)+套件 jedi-remote-agent |
#1(含 1b 撤銷只做一半) | fable/high | A |
| 5A-2 | 把驗簽工具與鎖定紀錄變成拔不掉的,讓防竄改的三層檢查不再共用一個弱點 | 套件 jedi-integrity+BE(common/util/resource_path.py、scripts/build/) |
#17、#18、#90、#91、#133 | fable/high | B |
| 5A-3 | 把「改全公司共用設定」收到客戶總部那一層,並補上儲存設定的密鑰遮蓋 | 套件 jedi-system-core+jedi-log+BE(core/plugins/、app/system_config/) |
#19、#22、#134 | opus/high | C |
| 5A-4 | 交出去的檔案與日誌統一編碼:匯出的試算表不再被當公式執行、日誌不再被讀成兩筆 | 套件 jedi-common(共用函式)+jedi-log+jedi-issue |
#20(M09-2+M21-1)、#93(M09-4) | opus/high | D |
| 5A-5 | 讓停權當下通行證立刻失效,順便把日誌轉送的加密選項給客戶 | 套件 jedi-iam+jedi-log+FE(下拉選項) |
M13 第 17 條(不在 145 件內)、#92 | opus/medium | E |
範圍:兩個 repo 都要動——agent repo ~/Projects/Billows/Audit-Manager/evidence-agent(出站端要帶上身分證明)+套件 monorepo 的 jedi-remote-agent(接收端要驗、歸屬比對要做進核心層)。worktree 建議:agent 側 wt-fix-b5A-agent、套件側 wt-fix-b5A-remote-agent。涵蓋 #1。 ⚠️ 兩個 repo 要同一個 runner 同一棒做完:接收端開始要求身分證明的那一刻,出站端沒帶就是全部 agent 掉線。分兩棒做等於中間必然有一段全斷。
每件的修法:
#1 裝在客戶機房那支代理程式,五條通訊管道沒有一條會問「你是誰」——網路上任何人不必帳號就能冒充它領走客戶整套主機的登入帳密;管理畫面按「撤銷」只斷了三分之一,被撤銷的機器照樣能回報假資料
修法(模組頁定案的五步,第一步要決策者先裁,見本卡 D-b5A-1):
agent_uid。只修其中幾條,剩下的還是門戶洞開。ack 與 result 兩條完全沒查(就是 1b)。既然要動這一層,邊際成本接近零。🔴 入口清單:
needs_admin=False 那五列就是全部洞口) jedi-remote-agent/jedi_remote_agent/api/routing.py:37-41 (/agents/register、/agents/heartbeat、/agents/tasks/<uid>/ack、/agents/tasks/<uid>/result;第五條是同層的憑證/證據下載,runner 開檔時以 needs_admin=False 為判準逐列點名)jedi-remote-agent/jedi_remote_agent/api/routes/remote_agent_route.py:164(register)/:173(heartbeat)/:182(ack)/:188(result)jedi-remote-agent/jedi_remote_agent/api/routes/remote_agent_route.py:11jedi-remote-agent/jedi_remote_agent/app/service/agent_enrollment_service.py:377(if agent.status == "revoked") jedi-remote-agent/jedi_remote_agent/domain/remote_agent/service/remote_agent_domain_service.py:107(派工側的同一判斷,是「歸屬比對做進核心層」的落點候選)jedi-remote-agent/jedi_remote_agent/app/service/remote_agent_service.py:113evidence-agent/core/enroll.py:159(register,httpx.post) evidence-agent/core/enroll.py:236-237(heartbeat,httpx.Client(cert=...)) evidence-agent/core/task_executor.py:167-172(_post,ack 與 result 共用) evidence-agent/core/task_executor.py:179/:210(_get_stream,證據檔下載)evidence-agent/core/cloud_trust.py:20-40(cloud-ca.pem 驗雲端 https ≠ cert_dir/ca.crt 驗 agent client 憑證,混用會寫出「看似正確其實必錯」的碼)evidence-agent/core/enroll.py:36-37、evidence-agent/core/task_executor.py:31-32功能不能壞:這張卡最大的風險是「修完全部 agent 掉線」。
register → heartbeat → 派一張工單 → ack → result → 證據檔上傳全鏈走得通;② 把 agent 的身分證明刻意弄壞(改掉通行證/憑證),確認五條都回 401/403 而不是 500;③ 管理頁按「撤銷」後,讓那台 agent 再打 ack 與 result,兩條都要被拒(這是 1b 的驗收點,修前是照樣進得來);④ 按「取消撤銷」後恢復正常;⑤ 授權過期的 agent 打進來要被擋(模組頁「四個讓事情更嚴重的細節」①)。修完擋得到誰要寫進回報(模組頁已有對照表):擋得到網路上的外人與 A 客戶冒充 B 客戶;擋不到能實際碰到那台機器的人——那是設計本身不是缺陷(agent 要去掃客戶主機就必須持有那些鑰匙)。回報時照抄這個分界,不要寫成「已修好、風險歸零」。
這張卡的手測總清單:
ack/result 兩條被拒(1b 的核心驗收)。需決策者先裁的:
範圍:套件 jedi-integrity(主體)+ BE(common/util/resource_path.py、scripts/build/build_release.sh)。worktree 建議 wt-fix-b5A-integrity(套件)+ BE 同棒改兩檔。涵蓋 #17、#18、#90、#91、#133。 🔴 重要前提:這個套件在掃描之後被重構過,掃描總表與模組頁上的行號多半已漂移。下面入口清單的行號是 2026-09-21 重新開檔核對過的現值,與報告上的舊行號不同屬正常,不要回頭照抄報告的行號。
每件的修法:
#17(M08-1)有主機管理權限的人把「負責核對簽章」的零件換成永遠說沒問題的假貨,之後任意竄改產品程式,三層檢查照樣顯示通過、全程零警示(已實測證實)
cryptography 強制編譯進 core 層,讓它不再是一個可以被單獨換掉的檔案。模組頁與掃描總表列了三個方案(① 在產生清單的工具裡新增「安全關鍵」分層並加進開機必查/② build 腳本把 cryptography 強制拉回 core/③ 最低限度加進效能監控優先清單),總表建議採 ①。另外建議把抽查邏輯改成「保證涵蓋安全關鍵檔案 + 其餘隨機抽」,不要純隨機。→ 三方案要哪個見 D-b5A-3。jedi-integrity/jedi_integrity/domain/config.py:47-48(DEFAULT_BOOT_LAYERS = ("core", "resources")——thirdparty 不在開機必查之列,就是這一行)jedi-integrity/jedi_integrity/domain/config.py:37(DEFAULT_SPOT_CHECK_THIRDPARTY_SAMPLE = 200,每輪隨機抽 200 個)jedi-integrity/jedi_integrity/domain/config.py:84(該值進 config dataclass 的欄位)jedi-integrity/jedi_integrity/app/runtime_check.py(四小時抽查的實作;報告舊指 runtime_check.py:181-191,⚠️ 行號已漂移,runner 開檔以「抽樣那段」為準)scripts/build/build_release.sh:108(pdfminer 在排除清單上)scripts/build/build_release.sh:375(註解明寫 pdfminer.six → charset_normalizer, cryptography 這條牽連鏈)scripts/build/build_release.sh:874/:889/:894/:909/:930(thirdparty 清單如何從產物實況產生並寫進 manifest)jedi_integrity/config.py 現在只是 6 行的相容 shim(from jedi_integrity.domain.config import IntegrityConfig),邏輯不在那裡,別往 shim 裡加東西scripts/build/build_all.sh --all),確認編譯成功且 smoke 過;② 產物開機,確認開機檢查通過且 log 有印出 core 層涵蓋了 cryptography;③ 故意替換 cryptography 的驗簽器(套件自帶測試工具做得到,FR-084 已實測過一次),確認這次開機被擋下;④ 四小時抽查改成「必含安全關鍵」後,確認一輪抽查就會驗到那些檔。 ⚠️ 原則十一:不可出現「正常客戶的產物因為新分層算不出清單而開不了機」。清單產生工具與 build 腳本要同一棒一起改、一起驗。#18(M08-2)客戶端機器因竄改被鎖住後,有主機權限的人只要刪掉一個檔案就能讓機器重新開起來,而那筆鎖定紀錄正是這機制原本要保留的證據
jedi-integrity/jedi_integrity/app/startup_gate.py(開機唯一讀的竄改狀態只有檔案標記;:54 import tamper_marker、:99 _refuse_for_existing_marker、:154 write_marker。⚠️ 報告舊指 startup_gate.py:206,行號已漂移)jedi-integrity/jedi_integrity/infra/tamper_event_repo.py:97(def exists(...)——DB 側的查詢方法。報告說「兩個地方都零呼叫」,⚠️ runner 開工前重跑一次 grep -rn "\.exists(" 於套件與 BE 兩側確認現況,別只信報告)jedi-integrity/jedi_integrity/app/service/tamper_sync_service.py(補同步;程式自陳「補同步是為了把『檔案標記被清掉』的風險降到最低」,就是缺口的自白)jedi-integrity/jedi_integrity/domain/tamper_marker.py:102-129(read_marker 對毀損的處理——這支取的是嚴格側(corrupted: True 即視同鎖定),是 #91 的正確對照範本)core/plugins/integrity.py:48、core/scheduler.py:343(都是 from jedi_integrity.infra.tamper_event_repo import TamperEventRepo)jedi-integrity/jedi_integrity/__init__.py、jedi-integrity/jedi_integrity/domain/tamper_marker.py 檔頭、jedi-integrity/README.md(⚠️ 報告給的是舊行號 __init__.py:11/tamper_marker.py:5-7/README.md:32,開檔以「宣稱有兩道鎖」那句為準)rm 掉 → 開機被擋(這是核心驗收,修前是能開起來的);④ 解鎖流程走完後能正常開機。#90(M08-3)有主機權限的人改一個設定,把「要核對哪個目錄」整個換掉,而且這設定的優先順序比「是不是正式打包版」還高
common/util/resource_path.py:23(_ENV_OVERRIDE = "GUIDANT_RESOURCE_ROOT")common/util/resource_path.py:31-39(resource_root()——先看環境變數、才看 is_packaged(),順序就是問題)common/util/resource_path.py:26-28(is_packaged(),判斷依據 __compiled__ / sys.frozen)jedi-integrity/jedi_integrity/domain/ports.py:84(resource_root port)、jedi_integrity/app/runtime_check.py:105/:164/:228/:268、jedi_integrity/app/startup_gate.py:150/:226.env sample、deploy//Dockerfile、188/189 兩台 /srv/guidant-ai 的 env 檔(唯讀,照 CLAUDE.md 讀寫分界允許),確認沒有實際部署在用這個變數換目錄。查到有在用就停下回報,不自己拿掉。GUIDANT_RESOURCE_ROOT → 仍然生效;② 打包產物設同一個變數 → 被忽略,仍核對 binary 所在目錄;③ 打包產物不設變數 → 行為不變。#91(M08-4)有主機權限的人把記錄「解鎖檔用過沒」的那個檔案改成亂碼,系統會當作一張都還沒用過,已用過的一次性解鎖檔就能重新復活使用
tamper_marker.read_marker 取的正是嚴格側,照它的形狀改(兩處不一致就是這條的成因)。jedi-integrity/jedi_integrity/app/unlock.py:83-100(_read_used_nonces——⚠️ 這一段行號經 2026-09-21 重新核對仍然準確。:92-93 是 FileNotFoundError, OSError → return [](合理側)、:96-97 是 ValueError, TypeError → return [](要改成拒絕的那一側)、:99 是結構不合法 → return [](同樣要改);docstring :84-89 明寫「毀損時取寬鬆側」的理由,改完要一起更新)jedi-integrity/jedi_integrity/app/unlock.py:51(USED_NONCES_FILENAME = "used-nonces.json")、:75(used_nonces_path)jedi-integrity/jedi_integrity/app/unlock.py:103(_append_used_nonce,寫入側)、:218(try_redeem,核銷主流程)jedi-integrity/jedi_integrity/domain/tamper_marker.py:102-129(read_marker 回 {"corrupted": True, ...})與 jedi_integrity/app/startup_gate.py:99-113(_refuse_for_existing_marker 對 corrupted 的處置)used-nonces.json 改成亂碼 → 核銷被拒且訊息可讀(修前是「視同全新、可重複使用」);④ 檔案不存在(首次解鎖)→ 仍然可以核銷(這一側不可改壞)。#133(M08-6)系統向原廠回報「偵測到竄改」時,會把主機紀錄檔最後 50 行原封不動送出,那 50 行若剛好夾帶密碼或登入憑證就一起被送出去
jedi-log 的 jedi_api_log/error_tracking/masking.py 是現成候選,runner 開工前先開檔確認它的簽名吃不吃這種純文字行)。或按總表的另一個選項:只送行數與時間戳記、不送內容。jedi-integrity/jedi_integrity/infra/forensic.py:44(_LOG_TAIL_LINES = 50)jedi-integrity/jedi_integrity/infra/forensic.py:88(result["log_tail"] = _log_tail(context.config)——送出前的組裝點)jedi-integrity/jedi_integrity/infra/forensic.py:108(_log_tail 本體。⚠️ 報告舊指 forensic.py:108-129,:108 這個起點仍準確)jedi-integrity/jedi_integrity/infra/forensic.py:76(collect_trigger_context,呼叫者)jedi-log/jedi_api_log/error_tracking/masking.py這張卡的手測總清單:
cryptography 已在 core 層必查範圍。rm 掉檔案標記 → 開機被擋(#18 核心驗收)。GUIDANT_RESOURCE_ROOT → 被忽略;開發模式設 → 仍生效(#90)。used-nonces.json 改成亂碼 → 核銷被拒且訊息可讀;檔案不存在 → 仍可核銷(#91)。需決策者先裁的:
cryptography 怎麼變成拔不掉的——三方案選一?/我的建議:採方案 ①「在產生防竄改清單的工具裡新增安全關鍵分層」(掃描總表亦建議此案)。理由:方案 ② 直接改 build 排除邏輯會牽動整包產物與 37 個被排除的相依套件,回歸面大;方案 ③ 只是順便驗到、不是保證。①另有一個附帶好處:日後再多一支安全關鍵套件,只要加進分層清單,不必再動 build 腳本。並同時把抽查改成「必含安全關鍵 + 其餘隨機」。範圍:套件 jedi-system-core(系統設定寫入)+ jedi-log(日誌轉送設定寫入)+ BE(core/plugins/system_core.py、core/plugins/api_log.py、app/system_config/)。worktree 建議 wt-fix-b5A-config。涵蓋 #19、#22、#134。 這三條併一張卡的理由:#19 與 #22 是同一個病灶的兩個位置(模組頁明寫「當成同一件事處理、套用同一條規則,兩條一起修」),#134 是 #22 同一個設定頁的遮蓋漏項、改同一批檔。
每件的修法:
#22(M22-1)任何一家客戶(含子公司)的管理員在系統設定頁就能改掉全公司所有人的登入規則,更嚴重的是他能把員工帳號目錄指到自己的機器,等於接管所有人的登入而且看不出來
jedi-system-core/jedi_system_core/app/service/system_config_service.py:54(create_system_config) jedi-system-core/jedi_system_core/app/service/system_config_service.py:80(update_system_config) jedi-system-core/jedi_system_core/app/service/system_config_service.py:165(update_config_by_group_key)jedi-system-core/jedi_system_core/api/routes/system_config_route.py:64(put(uid))/:98(post)core/plugins/system_core.py:62-75(_GROUP_CAPABILITY_RESOURCE,GROUP→能力點對照;THIRD_PARTY_LOGIN→ldap-config、SMTP→smtp-config、RUNTIME_CONFIG→security-policy) core/plugins/system_core.py:93(assert_config_capability——套件經 adapters.config_capability_guard 呼叫這支,這裡就是掛新判斷的正確位置;它已沿用 common.authz 的 viewer_has_capability,含 super_admin break-glass)infra/system_config/system_config_root_reader.py:60/:96/:122(SET LOCAL app.is_super_admin = 't'——明文關掉 RLS 以寫進 ROOT 那一列) infra/system_config/system_config_root_reader.py:136(write_root_config_value)app/system_config/service/shared_config_app_service.py:38-41(SHARED_ROOT_CONFIGS = {"SMTP": None, "THIRD_PARTY_LOGIN": "LDAP"}——共用設定的白名單就是這裡) app/system_config/service/shared_config_app_service.py:68-78(write_shared_config) app/system_config/service/security_policy_app_service.py:108(update_policy,登入安全政策專屬端點)/:162(write_root_config_value 呼叫) app/system_config/service/guarded_system_config_service.py:458(self._shared.write_shared_config(...))/:585(create_system_config)/:597(update_system_config)/:636(update_config_by_group_key)/:651(is_shared_root_config 分流)jedi-iam/jedi_iam/domain/service/org_unit_domain_service.py:194(已有依 parent_id 取子節點的用法,判「是否第一層」從這支的 repo 查 parent_id 即可) ⚠️ runner 開工前先 grep 一次「有沒有現成的『是否客戶總部』判斷」(grep -rn "is_root_tenant\|parent_id is None\|top_level" jedi-iam/ common/authz/)——common/authz/license.py:16 提到 GET /license/status 有 is_root_tenant 旗標,可能已有現成件。有就 reuse,別新造。shared_config_app_service.py docstring 記錄過「寫入釘了 ROOT 但讀取端漏了」的舊傷)。#19(M09-1)管理員在「日誌轉送設定」填一台伺服器位址按儲存(連子公司層級的管理員都按得下去),所有客戶的系統活動紀錄從此持續送往他填的機器
jedi-log/jedi_api_log/forwarding/api/routes/log_forwarding_route.py:57(def put — 更新設定)/:73(def post — 測試發送)jedi-log/jedi_api_log/forwarding/api/routes/log_forwarding_route.py:9(檔頭說明 adapters.settings_required / settings_update_required)、jedi-log/jedi_api_log/forwarding/plugin/contract.py:64-90(LogForwardingAdapters 契約,明寫 read_required / update_required 兩欄分開是刻意的、都沒有預設值)core/plugins/api_log.py:263(from common.authz import require_capability, require_platform_admin_route)/:272(_forwarding_guard 內 jwt_required()(require_capability(capability)(fn)))/:288(forwarding_read_required)/:289(forwarding_update_required — 要補「總部第一層」判斷的就是這一條)core/plugins/api_log.py:37-38(port 對照表,.read / .update 讀寫分權是產品決定)jedi-log/jedi_api_log/forwarding/infra/model/log_forwarding_setting.py(全系統只有這一份設定的那張表)#134(M22-3)有正當權限的客戶管理員打開「儲存設定」頁時,瀏覽器就收到那把共用密鑰的明文——任何看得到他瀏覽器流量、存檔或前端錯誤紀錄的人都拿得到
STORAGE_CONFIG)加進遮蓋名單;② 把帳密欄位名(access_key / secret_key 之類)加進要遮蓋的清單;③ 改用寄信與員工帳號目錄那套「不回密碼、沒帶就沿用」的做法。 先查再寫:這套「沒帶就沿用」的 pattern 主專案已經有兩份現成實作,照抄別新造——guarded_system_config_service.py:300-320 的 AI 供應商金鑰處理(含 clear 旗標、is_set 回報、空值=沿用既有的完整邏輯)。STORAGE_CONFIG 就是漏在這裡): jedi-system-core/jedi_system_core/plugin/contract.py:61(DEFAULT_SECRET_MASKED_GROUPS = ("SMTP", "THIRD_PARTY_LOGIN", "NOTIFY_CONFIG", "ISSUE_INTEGRATE_CONFIG") — 少 STORAGE_CONFIG) jedi-system-core/jedi_system_core/plugin/contract.py:65(DEFAULT_SECRET_VALUE_KEYS = ("secret", "private_token") — 少儲存設定的帳密欄位名) jedi-system-core/jedi_system_core/plugin/contract.py:113-114(兩者進 config dataclass)jedi-system-core/jedi_system_core/api/routes/system_config_route.py:20(import mask_secret_value)/:42-44(清單)/:57(單筆)/:73(PUT 回應)/:104(POST 回應) jedi-system-core/jedi_system_core/api/__init__.py:9(mask_secret_value 出口)app/system_config/service/guarded_system_config_service.py:113(_AI_PROVIDER_SECRET_KEYS = ("api_key",))/:116(_AI_PROVIDER_CLEAR_FLAG = "clear")/:300-320(merge 邏輯:空值=沿用既有密文、clear 旗標才真的清除、is_set 不落地)/:119(_DRIVE_APP_GROUP,同型但密鑰在 value 頂層的第二個範例) jedi-system-core/jedi_system_core/app/service/system_config_service.py:94/:103(套件側既有的 data["value"]["secret"] = existing_config.value["secret"] 沿用寫法)app/system_config/service/guarded_system_config_service.py:106(_HIDDEN_GROUP = "STORAGE_CONFIG")、:8(註解:STORAGE_CONFIG/FACTORY_DEFAULT 出廠快照對外不存在,CM-1333)app/system_config/service/tenant_storage_config_seeder.py:42-43(STORAGE_CONFIG / CONFIG)、:53***)。照抄的 clear 旗標/空值沿用邏輯正是為此存在。 手測:① 開「儲存設定」頁 → 密鑰欄位不再有明文(看瀏覽器 network 回應,不是只看畫面);② 只改「儲存路徑」不動密鑰後存檔 → 檔案上傳下載仍然正常(密鑰沒被洗掉);③ 明確填新密鑰後存檔 → 新密鑰生效;④ 走 clear 指令清除密鑰 → 真的清掉;⑤ 新建租戶時從 ROOT 繼承儲存設定(tenant_storage_config_seeder)仍然正常。這張卡的手測總清單:
需決策者先裁的:
common/authz 加一支共用判斷(軸⑦或併入既有 capability 軸),四個寫入端(三支 system_config + 一支日誌轉送)都呼叫它——CLAUDE.md 授權守門鐵則明禁另立第五套 helper,而這四處要的判斷一模一樣,各寫一份必然漂移。BE 側的兩個接點(core/plugins/system_core.py:93 與 core/plugins/api_log.py:289)都已經在呼叫 common.authz,接進去成本很低。shared_config_app_service.py 檔頭提到 STG 上 tenant 102 有 LDAP 殘留列)另開一張資料清理卡,並照環境異動鐵律只先動 DEV。不要讓 runner 在這一棒順手清資料。範圍:套件 jedi-common(放共用函式的地方)+ jedi-log(操作日誌匯出、日誌行組裝)+ jedi-issue(意見回饋匯出)。worktree 建議 wt-fix-b5A-output-encoding。涵蓋 #20(M09-2+M21-1)、#93(M09-4)。 併一張卡的理由:模組頁明寫這三條「同一個根——進來時照實記,交出去時沒把關」,且 M21 那條的修法欄直接寫「與另一處同樣的問題建議抽一支共用函式一起修」。裁定方向已定:抽一支共用函式、兩處(匯出)一起修;編碼不刪(原則七)。
每件的修法:
#20(M09-2+M21-1)任何人在帳號等欄位填進特殊開頭字元,管理員把操作日誌或意見回饋匯出成 Excel 打開時,那段內容會被當成公式執行,可能把整張表送到外部網址
= + - @ 以及 tab/CR 開頭)標示成純文字——🔴 字元一個都不刪,只是告訴 Excel「這是文字、不要執行」(原則七:交出去的資料要編碼,不是刪掉,資訊一個位元都不能少)。
jedi-common(兩個套件都依賴它,是唯一不會造成套件互相依賴的位置)。⚠️ 先查再寫:開工前 grep -rn "formula\|sanitize\|csv_injection\|^[=+@-]" jedi-common/ 確認沒有現成件;有就 extend,沒有才新增。jedi-log/jedi_api_log/api_log/app/service/api_log_service.py:77(def export_api_log_file) jedi-log/jedi_api_log/api_log/app/service/api_log_service.py:88-89(pd.ExcelWriter(..., engine='openpyxl') → df_clean.to_excel(...)——寫進檔案的那一行,編碼要在這之前) jedi-log/jedi_api_log/api_log/app/service/api_log_service.py:81(檔名)/:87(BytesIO)/:96(zip_file.writestr) jedi-log/jedi_api_log/api_log/common/utils/excel_util.py(ExcelUtil,api_log_service.py:13 import 它——共用函式的掛載候選點) jedi-log/jedi_api_log/api_log/app/dto/api_log_export.py(ApiLogExportDTO,欄位清單在這裡,判哪些是使用者可控) 路由:jedi-log/jedi_api_log/api_log/api/routing.py:18(/log/api-logs/export)jedi-issue/jedi_issue/app/feedback/service/feedback_service.py:337(def export_feedbacks) jedi-issue/jedi_issue/app/feedback/service/feedback_service.py:359(export_rows.append({...})——四個使用者可控欄位在這裡組進 row) jedi-issue/jedi_issue/app/feedback/service/feedback_service.py:370-380(csv 分支:csv.DictWriter → writerow) jedi-issue/jedi_issue/app/feedback/service/feedback_service.py:381-391(excel 分支:openpyxl.Workbook() → 寫 row) ⚠️ 兩個分支都要套(csv 與 excel 各一條路,只修一邊就是典型的「兩入口只改一邊」——CLAUDE.md 記憶 feedback_parallel_runners_two_entrypoints_one_fixed 點名過) 路由:jedi-issue/jedi_issue/plugin/__init__.py:10(/feedback/export/<type>,守 feedback.export)=1+1(不登入成功也會被記)→ 管理員匯出操作日誌 → 用 Excel 打開,該格顯示為文字 =1+1 而不是 2、也不是被刪掉;② 送一則標題以 = 開頭的意見回饋 → 匯出 csv 與 excel 兩種都打開確認同上;③ 正常內容(中文、數字、日期、含逗號與引號的文字)匯出後格式不變、不多出奇怪符號;④ 匯出檔案能被 Excel 與 LibreOffice 兩者打開(兩者對前導字元處理不同)。#93(M09-4)任何人(連登入都不用)在帳號之類欄位填進換行,系統把日誌送到客戶的資安監控系統時原樣帶出,對方會讀成兩筆紀錄,第二筆內容由攻擊者決定(例如偽造「某某管理員授予了超級權限」)
\n → 字面的 \ n),其他看不見的控制字元同理——🔴 內容一個位元都不少,只是讓它不再被讀成「下一筆」(原則七)。jedi-log/jedi_api_log/forwarding/common/forwarder.py:232(return f"1 {ts} {hostname} {app_name} {pid} {msgid} - {body}"——RFC 5424 那一行的組裝點,body 未經編碼就是這條的成因)jedi-log/jedi_api_log/forwarding/common/forwarder.py:222-234(_Rfc5424Formatter.format,整支 formatter)jedi-log/jedi_api_log/forwarding/common/forwarder.py:198(build_rfc5424_formatter)jedi-log/jedi_api_log/forwarding/common/forwarder.py:163-196(build_target_handler——syslog 與 gelf 兩條路都從這裡出去)jedi-log/jedi_api_log/forwarding/common/gelf_handler.py:88(_build_payload,GELF 是 JSON,換行本身會被 JSON 轉義、理論上不受這條影響)/:136(_send_udp)/:142(_send_tcp,以 NUL 分隔訊息)。⚠️ runner 要開檔確認 GELF 側是否真的免疫,別憑推論跳過;若免疫,在 commit message 註明理由。jedi-log/jedi_api_log/error_tracking/masking.py(同套件內既有的文字處理件,看能否共用掛載點)admin\n<偽造的一行日誌>(真的按 Enter 那種換行)→ 登入失敗 → 在 log server 端確認只收到一筆、且那段換行以 \n 兩個字元原樣可見;② 正常的多行 traceback 送出後,log server 端仍能看出完整堆疊(不因編碼而遺失資訊);③ syslog 與 GELF 兩種格式各測一次;④ UDP 與 TCP 兩種傳送方式各測一次(forwarder.py:180 的 socktype 分支)。 ⚠️ forwarder.py:189 有一段既有陷阱註解(關掉 stdlib 的 NUL 結尾,否則 Graylog 整筆丟掉)——改這支時不要把那行 handler.append_nul = False 弄掉。這張卡的手測總清單:
=1+1 → 操作日誌匯出的 Excel 中該格為文字、不執行、不被刪。= 開頭 → csv 與 excel 兩種匯出都不執行、不被刪。\n 兩字元可見。需決策者先裁的:
jedi-common 還是各套件各放一份?/我的建議:放 jedi-common(兩個匯出出口分屬 jedi-log 與 jedi-issue 兩個套件,各放一份就是兩份會漂移的規則,而這正是 M13 第 10 條那種「修一份、漏一份」的病)。代價是動 jedi-common 會牽動全部 consumer,所以這一棒走 poetry path dependency 開發、不發版,發版時機等決策者明示。範圍:套件 jedi-iam(認人與自動續期)+ jedi-log(加密選項)+ FE(下拉選項多一個值)。worktree 建議 wt-fix-b5A-iam-tls。涵蓋 M13 第 17 條(補進、不在 145 件內)、#92。 併一張卡的理由:兩條都小、都在「信任鏈」這批的語意內、改的檔完全不重疊,一個 session 做得完。
每件的修法:
M13 第 17 條 管理員在「帳號管理」把某位員工停權之後,那個人如果還開著瀏覽器、或存了登入憑證,照樣能繼續操作到憑證過期;而且憑證快到期時系統還會自動幫他續期——等於停權沒有立刻生效,管理員以為人已經擋掉了,實際上沒有(🔴 這條在 docs/security-report/M13-iam.md 第 98 行,不在 145 件的 SUMMARY 裡,共同說明指定補進第 5 批;三票一致通過、評中風險,被查出來時追蹤表已停更、沒人替它開卡)
jedi-iam/jedi_iam/middleware/core.py:74-82(is_token_revoked → raise IdentityResolutionError("revoked_token")——只查了撤銷) jedi-iam/jedi_iam/middleware/core.py:84-87(user = _lookup_user(user_service, uid);if user is None: raise IdentityResolutionError("user_not_found")——只查了「帳號還在不在」,沒查停權狀態。新判斷加在這裡) jedi-iam/jedi_iam/middleware/core.py:103(_lookup_user,內含 elevated_readonly_scope 提權——⚠️ docstring 明寫「這一句必須提權」,因為 context 還沒組出來、users 有 RLS,不提權會全站 401。新判斷要在這個提權查詢拿回來的 user 物件上判,不要另開一次查詢) jedi-iam/jedi_iam/middleware/core.py:35(reason 清單的說明,新增 reason 要一起補)jedi-iam/jedi_iam/middleware/jwt_mw.py:162-163(check_if_token_revoked → is_token_revoked) jedi-iam/jedi_iam/middleware/jwt_mw.py:157-158(revoked_token_callback) jedi-iam/jedi_iam/middleware/jwt_mw.py:67(錯誤碼清單) ⚠️ middleware/core.py:60-63 的 docstring 說明:HTTP 路徑傳 None 給 login_token_service(框架自己有 blocklist loader),socket 路徑必須傳——所以「停權」這一眼要確認兩條路徑都補到,不能只補 core.py 那一條。這是典型的兩入口問題。jedi-iam/jedi_iam/api/routes/login_route.py:84(@refresh_auth_required)/:87(c.service("login_token_service").refresh_user_token()) jedi-iam/jedi_iam/api/routing.py:89(/refresh) jedi-iam/jedi_iam/api/guards.py:60-61(refresh_auth_required) jedi-iam/jedi_iam/app/service/login_service.py:133(create_refresh_token)/:137(add_token_to_db)grep -rn "status\|is_active\|enabled" jedi-iam/jedi_iam/domain/entities/user*.py jedi-iam/jedi_iam/infra/models/user.py)——M13 第 13 條(角色停用)已修,修法是「權限查詢時把有效期、狀態、客戶歸屬一起算進去」,那支的寫法是本條的現成範本,先去看它怎麼判。_lookup_user 的 docstring 已警告過同一個位置的類似風險)。手測:① 正常帳號登入、操作、續期 → 全部正常;② 管理員把 B 帳號停權 → B 的瀏覽器下一個請求就 401(不用等憑證過期,這是核心驗收);③ B 拿 refresh token 續期 → 被拒;④ 把 B 復權 → B 重新登入後正常;⑤ socket 連線那條路(即時通訊)也要驗到同樣行為(兩入口);⑥ 平台管理員與 super_admin 帳號不受影響。 ⚠️ 原則十一:錯誤訊息要讓被停權的人知道「你的帳號已被停權」,不是一句無說明的 401——不然客服會收到「我突然被登出了」的電話。#92(M09-3)管理員在日誌轉送設定頁選「傳送方式」時,下拉選單裡只有兩個選項、兩個都是明文,想開加密的客戶根本開不了
jedi-log/jedi_api_log/forwarding/domain/service/log_forwarding_setting_domain_service.py:20(VALID_TRANSPORTS = ("udp", "tcp"))jedi-log/jedi_api_log/forwarding/infra/model/log_forwarding_setting.py:37(transport = Column(String(10), ..., default="udp", comment="udp / tcp"——欄位寬度 10 夠不夠放新值要確認;若要加 migration,紀律段要寫「出貨基線待重產」)jedi-log/jedi_api_log/forwarding/common/forwarder.py:177-182(socktype = SOCK_STREAM if transport == "tcp" else SOCK_DGRAM——加密要在這裡包 TLS) GELF:jedi-log/jedi_api_log/forwarding/common/gelf_handler.py:76(__init__(..., transport="udp", ...))/:80(self._transport = (transport or "udp").lower())/:142(_send_tcp)/:158-159(分派) ⚠️ 兩條路都要支援加密,只做一邊就是兩入口只改一邊jedi-log/jedi_api_log/forwarding/app/service/log_forwarding_app_service.py:158(delivery_confirmed = settings.transport == "tcp" and sent——這行寫死了 == "tcp",加了新值會讓「已確認送達」判斷失準)/:183-185(if settings.transport == "udp" 的提示訊息,同樣寫死)/:105/:145(相關註解)Dropdown,runner 照樣式改即可、不要手刻。
compliance-manager-fe/src/views/log-forwarding/LogForwardingForm.vue :69-72 — transportOptions computed(目前就這兩列:{label: t('lang.log_forwarding.transport_udp'), value: 'udp'} / ..._tcp, 'tcp') :271-276 — template 的 <Dropdown id="transport" v-model="config.transport" :options="transportOptions" optionLabel="label" optionValue="value" :disabled="!canUpdate" />(PrimeVue Dropdown,照抄上面 :258-263 的 protocol 下拉同一形狀) :52 — 表單預設值 transport: 'udp' :111 — 讀回設定時的 fallback transport: res.transport || 'udp' :162 — 送出 payload 的 transport: config.value.transportsrc/config/locales/i18n/zh-tw/log-forwarding.json:11-14(transport / transport_udp / transport_tcp / transport_hint)與 src/config/locales/i18n/en/log-forwarding.json:11-14(同四個 key)。新選項要同時補這兩份,並且 transport_hint 那句現有文案只描述 UDP 與 TCP 的差別(「UDP 送出後不確認對方是否收到;TCP 會建立連線…」),加了加密選項後這句要跟著改,否則畫面說明與選項不符。use_tls 開關」(本卡建議案),FE 要動的就不是 transportOptions 而是新增一個 InputSwitch/Checkbox——同檔 :265 起的 field 區塊有現成的 grid 樣式可照抄。這一點等裁決再定,runner 不要先猜一種做。transport,不要誤改:src/utils/detectionAssignmentParams.js:20(TRANSPORT_PARAM_KEY,弱點掃描派工參數)與 src/components/grc/JobExecutionDrawer.vue:1350-1454(掃描派工摘要顯示)。jedi-log/jedi_api_log/forwarding/api/serializers/log_forwarding.py(request/response schema 的 transport 欄位驗證)udp → 不受影響、照樣送得出去;② 既有客戶用 tcp → 同上;③ 選新的加密選項 → 存得下去、按「測試」送得出去(需要一台收得了加密的 log server;DEV 可用 openssl s_server 或 rsyslog 起一個);④ 對方憑證驗不過時錯誤訊息要說得清楚是憑證問題,不是一句「送不出去」;⑤ syslog 與 GELF 兩種格式都能用加密;⑥ delivery_confirmed 與提示訊息在新選項下的行為正確(不是靜默回 false)。這張卡的手測總清單:
udp/tcp 兩個既有選項不受影響。需決策者先裁的:
tls),還是獨立的「要不要加密」開關?/我的建議:做成獨立開關(transport 維持 udp/tcp,另加一個 use_tls 布林)。理由:transport 這個值在送出端被寫死比對至少四處(forwarder.py:180、app_service.py:158、:183、gelf_handler.py:80),加第三個值等於要逐處改比對邏輯、漏一處就是靜默錯誤;而加密是「在 TCP 之上包一層」的正交概念,做成開關語意也更準(UDP 本來就不能包 TLS,開關可以直接在領域層拒絕 udp + tls 這個組合)。代價是 DB 要加一個欄位(migration,主線 phase=active/envs=* → 出貨基線待重產),請決策者確認可接受。| D 編號 | 問題(白話) | 我的建議 | 卡住哪張卡 |
|---|---|---|---|
| D-b5A-1 | 代理程式每次打進來,要拿什麼證明「我就是那台」?一種是用產品本來就會發的短效通行證,另一種是靠機器憑證、但要進客戶機房改設定 | 用短效通行證——不必碰客戶機房設備;機器憑證之後再補當第二道,但不能當唯一手段 | 5A-1(動手前必裁) |
| D-b5A-2 | 「這張工單是不是你的」這道檢查,要放在最外層的門口,還是放進核心邏輯? | 放進核心邏輯當必要條件——放門口的話,日後多一條呼叫路徑(維運工具、批次補登)就又繞過去了,這正是這個洞的原始成因 | 5A-1(動手前必裁) |
| D-b5A-3 | 驗簽的那支工具怎麼變成「拔不掉」?三個做法:①在產清單的工具裡開一個「安全關鍵」層 ②改打包腳本強制編進核心 ③只把它加進監控優先名單 | ①——②會牽動整包產物與 37 個被排除的套件,回歸面太大;③只是順便驗到、不是保證。並同時把四小時抽查改成「必含安全關鍵檔+其餘隨機」 | 5A-2(動手前必裁) |
| D-b5A-4 | 被鎖住的機器不該靠刪一個檔案就能重開。治本兩條路:①開機時另外連一次資料庫查鎖定紀錄 ②讓補同步發現「資料庫有紀錄、檔案標記不見了」時把標記補回去並終止開機。附帶:開機當下資料庫連不上時,要擋下開機還是放行留警示? | 走①,②當輔助一起做。附帶那點建議連不上就放行但留高等級警示並在管理頁顯示(資料庫連不上本來就整體不可用,不必在這關多擋一次)——但這點請您確認 | 5A-2(動手前必裁) |
| D-b5A-5 | 解鎖紀錄檔毀損時改成「拒絕核銷」之後,客戶的出路是什麼?(原本寬鬆是為了避免「一次檔案損毀就再也解不開、只能重灌」) | 拒絕但訊息明確給出機器指紋+事件編號+聯繫原廠指引,前提是簽發端(license_center)有「重新簽發解鎖檔」的流程。若簽發端目前沒有這個流程,這條就要降級成「先記警示不拒絕」 | 5A-2(動手前必裁) |
| D-b5A-6 | 「這個人是不是客戶總部的管理員」這個判斷,要在共用守門寫一支給四個地方用,還是四個地方各寫一份? | 共用一支(放 common/authz)——四處要的判斷一模一樣,各寫一份必然漂移;而且專案規範明禁另立新的守門 helper |
5A-3(可邊做邊裁) |
| D-b5A-7 | 修完擋住子公司之後,資料庫裡已經存在的「子公司自己存的那些設定列」怎麼處理?(STG 上已知有一筆 LDAP 殘留) | 這一棒不動資料,只擋未來的寫入;殘留列另開一張清理卡,並照環境鐵律只先動 DEV | 5A-3(可邊做邊裁) |
| D-b5A-8 | 防「公式炸彈」的那支共用函式放哪?放共用基礎套件(動它會牽動全部使用者),還是兩個套件各放一份? | 放共用基礎套件——各放一份就是兩份會各自漂移的規則,正是「修一份、漏一份」那個病。這一棒走開發期的本機路徑依賴、不發版,發版時機等您明示 | 5A-4(可邊做邊裁) |
| D-b5A-9 | 日誌轉送的加密選項,做成「傳送方式」下拉的第三個值,還是另加一個「要不要加密」的開關? | 另加開關——「傳送方式」這個值在送出端被寫死比對至少四處,加第三個值要逐處改、漏一處就是靜默錯誤;加密本質是「在 TCP 之上包一層」,做成開關語意也更準。代價是資料庫要加一個欄位(要套 migration,出貨基線待重產),請您確認可接受 | 5A-5(動手前必裁) |
| D-b5A-10 | 開了加密之後,要不要驗對方的憑證?還是讓客戶自己選要不要驗? | 預設驗,但給一個「信任這張指定憑證」的欄位讓自簽的客戶填。不提供「完全不驗」這個選項——那會重演「有加密但不認人」那兩條 | 5A-5(可邊做邊裁) |