這 20 件是「設定與信任鏈」批裡散在各小模組的那一半:兩條高風險(上傳的網頁檔被別人點預覽時當程式跑、任何登入者一支查詢就拿到檔案儲存空間的帳密)、一批全站共用底層的小毛病(密碼遮不乾淨、清單筆數沒上限、環境值打錯字悄悄退回開發設定、寫資料庫日誌失敗會連累使用者的請求)、AI 兩塊的同款資源耗盡、以及意見回饋模組的兩條與供應鏈的一條。
現在就能開,不依賴其他批。唯一要留意的重疊:#14(儲存帳密明文回傳)的遮罩機制那一側屬第 5 批 A 子集的 M22-2,本子集只做 M02 那一側(主專案接線+前端表單+盤點寫死帳密),兩張卡要同時在場才算堵住,但可以平行做(動的檔不同)。
兩件已裁定暫緩、本批不開卡:#98(安裝程式每套都種同一組最高權限帳密)、#99(三環境共用同一份公鑰)。
| 卡 | 卡名(做什麼) | repo/套件 | 涵蓋 SUMMARY # | 建議 model/effort | 序列組 |
|---|---|---|---|---|---|
| 5C-1 | 讓別人點預覽時跑不起攻擊者的程式,並在存檔前先把檔名洗乾淨 | 套件 monorepo:jedi-file-upload + jedi-detection | #13(M02-4+M03-13)、#123(M02-9) | opus/high | A(jedi-file-upload) |
| 5C-2 | 檔案儲存空間的帳密不再回傳給前端,連線也改成加密 | BE + FE(+只讀套件 jedi-system-core) | #14(M02-5 側)、#124(M02-11) | opus/high | B(BE managed_file_upload_service.py + install.sh) |
| 5C-3 | AI 助手與 AI 儀表板補上等待逾時、次數上限,並停止把密碼材料整包送出去 | 套件 monorepo:jedi-ai-dashboard + jedi-ai-bot + jedi-common | #21(M15-1)、#94(M14-1)、#95(M15-4) | opus/high | C(jedi-common serialization_util.py) |
| 5C-4 | 全站共用底層五處小修:密碼遮乾淨、清單筆數加上限、設定打錯字改往嚴的倒、寫日誌失敗不連累請求、兩處紀錄等級調回一般 | 套件 monorepo:jedi-common | #82(M04-3)、#83(M04-7)=#136(M19-3)、#84(M04-8)、#103(總表 §3.2-11)、#125(M04-9) | opus/high | D(jedi-common,與 C 不同檔) |
| 5C-5 | 意見回饋模組:連 GitHub 時恢復身分驗證,刪附件時真的用上算好的過濾結果 | 套件 monorepo:jedi-issue | #100(M23-4)、#135(M23-7) | sonnet/medium | — |
| 5C-6 | 公司內部套件倉庫全面改走加密連線,並把版本鎖定檔納入版控 | 套件 monorepo(28 支)+ BE + FE | #101(M23-5) | sonnet/medium | E(所有 pyproject.toml) |
| 5C-7 | 匯出 Word/Excel 時把使用者填的字當資料處理,抽一支共用的中和函式 | BE 主專案 | #102(總表 122)+總表 126(同病另一出口) | opus/high | — |
| 5C-8 | 系統自動產生的密碼補回少掉的那一個字元,並清掉兩支沒人用的複製品 | 套件 monorepo:jedi-iam(已修,只驗)+ jedi-survey + jedi-log | #137(總表 §3.2-12) | sonnet/medium | — |
| 本批不開:裁定暫緩 | #98(M17-7 安裝程式同一組最高權限帳密,「維持記錄、暫不調整」,之後一併處理安裝期憑證策略)、#99(M18-7 三環境共用公鑰,「等正式簽發站建好一併處理」,工單 CM-1582 暫緩) | — | #98、#99 | — | — |
序列組說明:同一個字母的卡動到同一個檔,不可同時派。A/C/D 都在套件 monorepo 但各動不同檔,可平行;E 動到每一支
pyproject.toml,與其他套件卡都會撞(開發期的 poetry path dependency 也寫在同一個檔),建議 E 單獨一輪、或排在最後。
~/Projects/Jedicogy/module/jedi-python-package),兩支套件 jedi-file-upload + jedi-detection。worktree 建議 wt-fix-b5C-file-upload。涵蓋 #13、#123。#13(前半,M02-4)任何有帳號的人上傳一份網頁檔,別人在系統裡點一下預覽,攻擊者的程式就在那個人的瀏覽器裡、用他的身分跑起來
docs/security-report/M02-file-upload.md:311-332):
jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:173-232 — UploadFilePreviewAsPDFRoute.get(),整個預覽出口jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:198 — 宿主物件儲存分支:mimetypes.guess_type(f'preview.{_ext}'),_ext 直接取使用者上傳的副檔名jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:221 — 內建物件儲存分支:同一個寫法再一次jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:195、:199、:229-231 — 三處 as_attachment=Falsejedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:141-170 — UploadFileDownloadRoute,一律 as_attachment + 固定 MIME_OCTET_STREAM:214-218 記著「原檔直出時 mimetype 必須依實際副檔名判定,不可一律 application/pdf,否則圖片被當 PDF 丟進 iframe 開不出來」——改 mime 判定邏輯時不要把這個坑踩回去。#13(後半,M03-13)代理程式回報掃描報告時「檔名叫什麼就存什麼」
jedi-detection/jedi_detection/app/service/detection_result_handler.py:217 — filename = filename_hint or f"detection_report_{agent_task_uid}",filename_hint 來自呼叫端送的 result_ref.upload_uids[].filenamejedi-detection/jedi_detection/app/service/detection_result_handler.py:218-221 — 接著又被 agent 回應的 Content-Disposition 覆蓋(parse_options_header),兩個來源都沒去路徑、沒比白名單OpenSCAP掃描報告_<host>_<日期>.html(含中文與底線),改洗檔名邏輯時不能把中文洗掉、也不能讓日期變成亂碼——否則稽核人員在證據清單上看到的是一堆認不出的檔名。#123(M02-9)使用者在本機儲存模式下刪掉一個檔,轉檔產生的 PDF 備份沒被一起清掉,還留在硬碟上
jedi-file-upload/jedi_file_upload/infra/adapter/local/local_file_adapter.py:82-93 — delete_file(),刪完主檔就結束jedi-file-upload/jedi_file_upload/infra/adapter/local/local_file_adapter.py:96-108 — delete_files_by_uids(),同樣沒清衍生檔jedi-file-upload/jedi_file_upload/infra/adapter/minio/minio_adapter.py:140、:167 呼叫 _delete_derived_files();函式本體在 :170-181(循 ref_id 反查衍生檔)_delete_derived_files 是 best-effort(物件刪失敗只記 log、不擋 DB 紀錄刪除),抽共用時要保留這個寬容度——否則硬碟上少一個檔就會讓整個刪除失敗。upload_files 也沒有殘留的衍生檔列。這張卡的手測總清單:
需決策者先裁的:
html/htm(掃描報告,硬要求)+ pdf/常見圖片格式(png/jpg/jpeg/gif/webp),其餘一律附件下載;清單放在套件 config 讓宿主可調,但預設值由套件給。~/Projects/Billows/Audit-Manager/compliance-manager-be)+ FE(~/Projects/Billows/Audit-Manager/compliance-manager-fe);套件 jedi-system-care/jedi-system-core 只讀不改。worktree 建議 wt-fix-b5C-storage-cred(兩個 repo 各開一個同名 worktree)。涵蓋 #14(M02 側)、#124。#14(M02-5 側)任何登入帳號打一支查系統設定的功能,就拿到雲端檔案儲存空間的帳號密碼明文,之後可以完全繞過我們的系統直連儲存空間
docs/security-report/M02-file-upload.md:375-401;做錯順序會把客戶的密碼洗掉):
••••••••),不填就保持原值。跳過這步直接做第三步,使用者改別的欄位順手存檔就會把密碼存成空的。app/upload_file/service/managed_file_upload_service.py:104-110 — MinIO 分支,secret_key=v.get('minio_secret_key', '') 從設定表讀出明文app/upload_file/service/managed_file_upload_service.py:119-125 — SeaweedFS 分支,同一組鍵名兩種寫法(secret_key 與 minio_secret_key 都吃)src/views/storage-config/StorageConfigForm.vue:33 — passwordHasChange ref(第二步要靠它判斷「使用者有沒有真的改密碼」)src/views/storage-config/StorageConfigForm.vue:131 — minio_secret_key 的驗證規則src/views/storage-config/StorageConfigForm.vue:353、:358 — 送出時把 minio_secret_key 同時塞進通用鍵與前綴鍵兩處src/views/storage-config/StorageConfigForm.vue:469-491 — Password 元件與驗證訊息scripts/installer/install.sh:1206-1207(讀 GUIDANT_S3_ACCESS_KEY/GUIDANT_S3_SECRET_KEY)、:1963(psql 變數帶入)、:2607-2608、:2640-2645(出廠快照那一列一起寫入)jedi-system-core/jedi_system_core/api/guards.py:94-124(mask_secret_value)、jedi_system_core/plugin/contract.py:61(DEFAULT_SECRET_MASKED_GROUPS,目前不含 STORAGE_CONFIG)、:65(DEFAULT_SECRET_VALUE_KEYS 只有 secret/private_token,不含 minio_secret_key/secret_key)STORAGE_CONFIG/FACTORY_DEFAULT 那一列(scripts/installer/install.sh:2630-2655),那一列本來就被 BE 從所有列表/查詢 API 藏起來,改遮罩時不要讓它冒出來、也不要讓恢復功能讀不到值••••••••、不含真值(開瀏覽器網路面板確認回應裡沒有密碼);② 只改 bucket 存檔 → 上傳檔案仍成功(密碼沒被洗掉);③ 真的改一次密碼 → 上傳檔案成功;④ 存心留空存檔 → 密碼保持原值;⑤ 按「恢復系統內建儲存」→ 切回內建 SeaweedFS 且上傳成功;⑥ 用一個沒有儲存設定權限的帳號打查詢端點 → 拿不到密碼欄位。#124(M02-11)系統連到自己裝在客戶機房的檔案儲存空間時走的是不加密連線,檔案內容與帳密在客戶內網上明文跑
app/upload_file/service/managed_file_upload_service.py:109 — MinIO:secure=v.get('secure', False)app/upload_file/service/managed_file_upload_service.py:124 — SeaweedFS:同一個寫法scripts/installer/install.sh:1954 — 裝機把 STORAGE_CONFIG/CONFIG 寫成 'secure', falsescripts/installer/install.sh:2002 — 同一組值的另一處scripts/installer/install.sh:2644 — 出廠快照 FACTORY_DEFAULT 也寫 'secure', falseguidant-seaweedfs:8333)必須確認它有沒有開 TLS——安裝腳本改成 true 而 gateway 沒有憑證的話,裝出來的機器上傳功能整個掛掉、而且是裝完才發現。這是本卡最大的風險點,見 D-b5C-2。secure 是 true;② 舊環境維持 false 不動,確認沒被連帶改掉。這張卡的手測總清單:
需決策者先裁的:
secure 改成 true? 這取決於內建的 SeaweedFS S3 gateway 有沒有開 TLS(要實機查)。我的建議:runner 先查 gateway 現況並回報,不要直接改——若 gateway 沒開 TLS,這條要連帶做「內建儲存也上憑證」,範圍就從一張修正卡變成部署層工作,屬另一個決策。jedi-ai-dashboard + jedi-ai-bot + jedi-common(只動 serialization_util.py 一支檔)。worktree 建議 wt-fix-b5C-ai。涵蓋 #21、#94、#95。docs/security-report/M14-ai-bot.md:98、M15-ai-dashboard.md:152;工單 CM-1638 已開)。#95 的根源是 jedi-common 那支共用序列化工具(M04-common.md:360-371),全產品只有兩個呼叫點、都在 AI 儀表板,所以跟這張卡同一批做最省。#21(M15-1)使用者在 AI 儀表板打字,任何基層員工都能誘導 AI 選中 26 支查詢裡的任何一支,而且能無限次改寫問法重試直到成功
jedi-ai-dashboard/jedi_ai_dashboard/api/routes/ai_dashboard_route.py:101-115 — post() 入口,@require_license + @transaction,沒有任何次數限制jedi-ai-dashboard/jedi_ai_dashboard/api/serializers/ai_dashboard.py:41 — validate=validate.Length(min=1, max=2000)(長度已經有上限,不用再加)jedi-ai-dashboard/jedi_ai_dashboard/app/service/ai_dashboard_app_service.py:145 — _ask_ai_to_select_api(),第一次呼叫 AIjedi-ai-dashboard/jedi_ai_dashboard/app/service/ai_dashboard_app_service.py:168 — _ask_ai_to_design_layout(),第二次呼叫 AIjedi-ai-dashboard/jedi_ai_dashboard/infra/ai_client/claude_client.py:62、:99 — Anthropic(api_key=self.api_key),沒帶 timeoutjedi-ai-dashboard/jedi_ai_dashboard/infra/ai_client/openai_client.py:39 — OpenAI(api_key=self.api_key),同樣沒帶jedi-ai-dashboard/jedi_ai_dashboard/infra/ai_client/google_client.py:39 — genai.configure(api_key=self.api_key),同樣沒帶#94(M14-1)產品的聊天框對訊息長度與呼叫次數完全不管,最基層員工用四到八個連線送超大訊息就能讓整個產品對所有人停止回應
M14-ai-bot.md:92-96):
jedi-ai-bot/jedi_ai_bot/api/routes/ai_bot_route.py:52 — message = (payload.get('message') or '').strip(),只檢查非空、不檢查長度jedi-ai-bot/jedi_ai_bot/api/routes/ai_bot_route.py:55-59 — 空值檢查後直接 rt.service.chat(...)jedi-ai-bot/jedi_ai_bot/app/service/ai_bot_service.py:104-108 — Anthropic(api_key=...) + client.messages.create(...),沒帶 timeoutjedi_ai_bot/app/service/ai_bot_service.py:69 已有 max_messages 的歷史截斷邏輯,加長度上限時不要把歷史累積也一起砍掉(使用者會發現對話突然失憶)。#95(M15-4)儀表板把查到的資料整包原樣送回畫面,夾帶每個帳號的密碼加密鹽值與「是不是超級管理員」旗標,送給外部 AI 的樣本也沒遮蔽
M15-ai-dashboard.md:227-241):
jedi-common/jedi_common/utils/serialization_util.py:12 — _ORM_SKIP_KEYS = frozenset({'metadata', 'registry', 'sa_instance_state'}),只有三個 ORM 內部項jedi-common/jedi_common/utils/serialization_util.py:15 — to_serializable() 本體(第二步要改的地方)jedi-ai-dashboard/jedi_ai_dashboard/app/service/ai_dashboard_app_service.py:190jedi-ai-dashboard/jedi_ai_dashboard/domain/service/dashboard_generation_domain_service.py:249M04-common.md:371 已查證;runner 開工前再 grep 一次確認)to_serializable 是通用工具,加進「不要送」清單的名字若太寬(例如整個 salt)可能誤殺別的合法欄位。加之前先 grep 那些欄位名在別處有沒有合法用途。這張卡的手測總清單:
需決策者先裁的:
jedi-iam/jedi_iam/mfa/app/service/email_service.py:87-92,拋 OtpResendTooFrequentError → 主專案 core/app_factory.py:459-468 轉 429 並帶 retry_after)。我的建議:沿用這個前例的形狀(Redis TTL + 429 帶 retry_after),兩支 AI 套件各開插槽、主專案接同一支實作,不要引進新的第三方限流套件。jedi-common。worktree 建議 wt-fix-b5C-common。涵蓋 #82、#83(=#136)、#84、#103、#125。jedi-common、五支不同檔、彼此不相干但都很小,一個 worktree 一次做完最省。#83 是這頁投報率最高的一條(11 支套件 + 主專案吃同一份設定,改一處十二處一起好)。#82(M04-3)使用者的密碼裡只要有雙引號(強密碼常見),寫進紀錄時只遮掉前半截,後半截原文留在資料庫九十天
ab"cd 這種它以為在第一個引號就結束了。M04-common.md:195-208):那個方向乍看更乾淨,但紀錄檔裡常有半截的、壞掉的東西(連線中斷、寫到一半、格式不符的請求),解不開就整段不遮、原文直接寫進紀錄——在最需要遮的時候失效,比遮不乾淨更糟。效能不是考量點(兩種都是微秒等級,寫檔案本身慢好幾個數量級)。jedi-common/jedi_common/utils/common_utils.py:145-159 — mark_password(),regex 的值部分寫成 "[^"]*"(不認跳脫)jedi-common/jedi_common/utils/common_utils.py:139-142 — _SECRET_KEY_PATTERN(key 名的比對,這部分不動)jedi-common/jedi_common/handler/handler.py:44jedi-log/jedi_api_log/error_tracking/masking.py:33-34(dict/list 先序列化成 JSON 字串再走這支)public.api_logs 與 log/app.log,確認整段密碼都被換成 ***;另外用不含雙引號的密碼測一次確認沒退步。#83(M04-7)=#136(M19-3)任何登入帳號在任何清單畫面把「一頁幾筆」填成超大數字,就能叫資料庫整張表一次撈完,重複幾次整個產品對所有客戶停止回應
M19-asset.md:66 自己也寫「這一條的根源不在這一塊⋯修一處全站生效」)。M04-common.md:268-283):
jedi-common/jedi_common/interfaces/schema/common.py:13 — page_size = fields.Int(missing=25),無 validate.Range(max=...)jedi-common/jedi_common/interfaces/schema/common.py:12 — page = fields.Int(missing=1, validate=validate.Range(min=1))(對照組:下限有做)jedi-common/jedi_common/interfaces/schema/common.py:16-18 — RequestMetaSchema(所有分頁 request 都繼承它)RequestMetaSchema 得到):jedi-detection api/serializers/detection_profile.py、jedi-compliance-audit api/serializers/project.py、jedi-system-core api/serializers/system_menu.py、jedi-evidence-classification api/schemas/evidence_batch_schema.py、jedi-bulletin api/serializers/bulletin.py、jedi-issue api/serializers/feedback.py、jedi-survey api/serializers/survey.py、jedi-iam api/serializers/{user,tenant,role,org_unit}.py、jedi-log jedi_api_log/api_log/api/serializers/api_log.py、jedi-asset api/serializers/{information_system,device}.py(#136 就在這支 device.py:31 的 DevicePageQueryRequest);BE 側 api/flow_control/serializers/job.py、api/project_summary_report/serializers/project_summary_report.py、api/project/serializers/{assessment_plan,project}.py 等(runner 開工先完整 grep 一次,清單以當下為準)page_size: 99999、BE 內部服務互打時撈全部),不是 serializer 本身page_size: 999999 → 收到明確的參數錯誤(不是撈完整張表);③ 帶總數的頁面(有「共 N 筆」的)確認數字沒變小。#84(M04-8)客戶的環境設定值打錯字時程式認不得,就悄悄退回開發用的紀錄設定
production 而不是 prod,程式認不得就悄悄退回開發設定(開發設定會把「寫進資料庫」這條紀錄路徑掛在七種紀錄類別上,正式設定本來完全沒有這段)。jedi-common/jedi_common/logger/config_logger.py:75 — dictConfig(_CONFIGS.get(RUN_ENV, logging_config_dev)),fallback 是 dev(這是要改的那一行)jedi-common/jedi_common/logger/config_logger.py:30 — RUN_ENV = os.getenv("RUN_ENV", "dev")jedi-common/jedi_common/logger/config_logger.py:34-38 — _CONFIGS 三個合法值(dev/stg/prod):28-29 有一段註解寫「出貨機曾經實際依賴過這個預設值(compose 與 installer 都沒設 RUN_ENV),改它之前先確認出貨端已顯式寫出」——模組頁說安裝程式與出貨設定都已經補上了(狀態 ⚠️ 部分修),runner 要親自開檔核對這件事已成立再動,並順手把那段過期註解改成現況os.getenv 的預設值。RUN_ENV 完全沒設時要不要也退回 prod 是另一個判斷(見 D-b5C-6)。改完要確認本機開發(RUN_ENV=dev)的紀錄行為完全沒變。RUN_ENV=dev 起服務 → 紀錄行為與現在一致;② 故意設 RUN_ENV=production(打錯字)起服務 → 服務正常起來,且套用的是正式設定(log/app.log 格式與 DB 紀錄行為比對 prod);③ 設 RUN_ENV=prod → 與 ② 一致。#103(總表 §3.2 第 11 項)把系統日誌寫進資料庫如果失敗,錯誤會往上竄、把使用者原本那個請求一起弄壞——現在能跑純屬運氣(靠處理器排序)
4bf7906(CM-1920)標題是「只放行稽核事件與 ERROR,prod/stg 補掛 db handler」,改的是「寫多少」不是「寫失敗怎麼辦」——所以這條沒有錯誤處理的路現在正式環境也會走。logging.Handler.handleError);資料庫日誌的格式化邏輯補上時間格式設定,或改用另一個一定會有值的時間欄位。jedi-common/jedi_common/logger/db_log/db_handler.py:30-51 — emit() 全函式,無 try/exceptjedi-common/jedi_common/logger/db_log/db_handler.py:39 — datetime.strptime(record.asctime, '%Y-%m-%d %H:%M:%S,%f'),record.asctime 是前面的 handler 格式化時就地補上的(同檔 :23-25 的註解自己講了這件事:掛載順序必須讓 db 排在用 app formatter 的 handler 之後,否則屬性不存在)jedi-common/jedi_common/logger/db_log/db_handler.py:51 — system_log_service.add_system_log(log_dto),DB 寫入那一行emit() 包了 try/except 之後寫入失敗會變成靜默——這是我們要的(記錄失敗不該連累請求),但要確認失敗有寫進檔案紀錄(log/app.log),否則就變成另一種看不見。handleError 的預設行為會往 stderr 印,要確認落地版(唯讀 rootfs)下這條路不會又炸一次。public.system_logs 有稽核事件列;② 故意讓 DB 寫入失敗(例如暫時把 system_logs 改名或撤掉 cm_app 的 INSERT 權限)→ 使用者的請求仍然成功,且 log/app.log 有一行寫入失敗的紀錄;③ 恢復後確認紀錄照寫。#125(M04-9)兩種運作紀錄的詳細程度被寫死成最詳細,紀錄量暴增吃掉磁碟(三處已改一處,還剩兩處)
jedi-common/jedi_common/logger/config_prod.py:80-84 — pymongo.event_loggers 的 'level': 'DEBUG'(在 :82)jedi-common/jedi_common/logger/config_prod.py:85-89 — sqlalchemy.orm 的 'level': 'DEBUG'(在 :87):33 也有一個 'level': 'DEBUG',那是 handler app 的等級、不是紀錄分類——不要一起改,模組頁講的「剩兩處」就是上面那兩個 loggersqlalchemy.orm 調回 INFO 之後,排查 SQL 問題時就看不到那些細節了。這是刻意的取捨(正式環境不該長期開著),但要在 commit message 寫明「要臨時開啟時改哪裡」。RUN_ENV=prod 起服務、操作一輪 → log/app.log 不再有大量 pymongo/sqlalchemy.orm 的 DEBUG 行,而稽核事件與錯誤照樣記得到。這張卡的手測總清單:
api_logs 與 app.log 確認全遮page_size 被退 + 帶總數的頁面數字沒變小(含設備清單,驗 #136)RUN_ENV 三種值(dev/prod/打錯字的 production)各起一次服務,確認都起得來且套用的設定正確RUN_ENV=prod 下確認紀錄量降下來需決策者先裁的:
RUN_ENV 完全沒設時(不是打錯字,是沒設)要不要也退回正式設定? 現況是退回 dev,且同檔註解記著「出貨機曾經實際依賴過這個預設值」。我的建議:兩種情況都退正式設定(方向一致、少一套規則),但要先開檔核對安裝程式與出貨 compose 真的都已顯式寫出 RUN_ENV——runner 核對不成立就停下回報,不自己判斷。page_size 上限抓多少? 我的建議照模組頁:1000。若第一步盤點發現有正當的「撈全部」需求(例如匯出、統計),我的建議是那些改走專用的匯出路徑或內部分頁迴圈,不要為它們開白名單參數——開了參數就等於上限可被繞過,回到原點。jedi-issue。worktree 建議 wt-fix-b5C-issue。涵蓋 #100、#135。#100(M23-4)系統帶著公司的 GitHub 通行證去開問題單時,不確認對方是不是真的 GitHub,網路上的中間人可以假冒 GitHub 把通行證攔走
verify=False 刪掉即可,PyGithub 預設就是驗的)。M23-issue.md:248-257):原始報告寫「同一種問題第四次出現」,重查後真正「程式寫死不驗」的只有三處——GitHub 兩處(本卡)+ 主專案的快取服務一處(不在本卡,共用模組那邊已修、主專案那支漏掉)。GitLab 那一半沒有這個問題;員工帳號目錄已改成可設定且預設有驗;寄信那條不成立(語言內建元件的預設行為,不是我們關掉的)。jedi-issue/jedi_issue/infra/github.py:22 — return Github(auth=auth, per_page=100, timeout=10, verify=False)jedi-issue/jedi_issue/infra/issue/adapter/github/github_issue_adapter.py:40 — self.gh = Github(auth=auth, per_page=100, timeout=10, verify=False)#135(M23-7)程式在刪除附件時算出了「該過濾掉哪些檔案」,接下來卻用回沒過濾的那一份清單
jedi-issue/jedi_issue/infra/issue_upload_files/local/local_issue_attachment.py:87 — matched_file_uids = file_uids_set & issue_file_uid_set(算出過濾結果)jedi-issue/jedi_issue/infra/issue_upload_files/local/local_issue_attachment.py:91 — 刪對照表時用的是 list(matched_file_uids)(正確)jedi-issue/jedi_issue/infra/issue_upload_files/local/local_issue_attachment.py:98 — file_is_deleted = self.file_upload_service.delete_files(file_uids)(用回未過濾的 file_uids,這是要改的那一行)delete_attachment 有沒有同樣的形狀:jedi_issue/infra/issue_upload_files/gitlab/gitlab_issue_attachment.py:78、jedi_issue/infra/issue_upload_files/github/github_issue_attachment.py:112(GitHub 那支走 _filter_attachment_link,機制不同,要開檔看過再判斷,不要假設一樣)matched_file_uids 之後,傳進來但不屬於這張問題單的編號會被靜默忽略(原本會被刪掉)。這正是要的行為,但要確認正常單檔刪除仍然成功(正常流程下那個編號一定在對照表裡)。這張卡的手測總清單:
需決策者先裁的:無。
wt-fix-b5C-supply-chain。涵蓋 #101。pyproject.toml,與其他任何套件卡都會撞(開發期的 poetry path dependency 也寫在同一個檔)。建議單獨一輪、或排在本批最後。#101(M23-5)公司內部網路裡有心人,可以在我們打包機安裝套件的當下偷偷掉包內容,等於在打包機上執行他的程式碼——而打包機產出的就是客戶實際拿到的安裝檔
M23-issue.md:271-274):共用模組那邊仍然沒有——27 支套件裡有 17 支本機產生了這個檔,但一支都沒有進版本控制,全被忽略規則擋掉了;主專案已經補上了,自 2026-08-15 起入版控。grep -rn 'http://192.168.50.171:8082' --include='pyproject.toml' 在兩個 repo 各跑一次得到完整 28 支清單):
pyproject.toml:161 — url = "http://192.168.50.171:8082/repository/pypi-group/simple"jedi-detection/pyproject.toml:66jedi-flow-engine/pyproject.toml:46jedi-compliance-audit/pyproject.toml:63jedi-system-core/pyproject.toml:67archive/jedi-information-system/pyproject.toml:47、archive/jedi-log-forwarding/pyproject.toml:43、archive/jedi-system-config/pyproject.toml:54、archive/jedi-participant/pyproject.toml:34(已封存的也要改——封存不等於不會被拿來裝).gitignore(擋掉 poetry.lock 的那條規則)https:// 之前必須確認 Nexus 那一端真的有 TLS 並且憑證可被信任——沒有的話 poetry install 會在每一台開發機與打包機上直接失敗。這不是程式問題、是基礎設施前置條件,見 D-b5C-8。鎖定檔入版控後,poetry update 一律要指定套件名(既有紀律,裸跑會升整棵樹夾帶沒驗過的第三方)。poetry install 成功;② 在 188 打包機 poetry install 成功(這是最關鍵的一台,它產出的就是客戶拿到的安裝檔);③ 鎖定檔進版控後 git status 乾淨、poetry install 仍成功。這張卡的手測總清單:
poetry install 成功poetry install 成功(含一支 archive 的)poetry install 成功需決策者先裁的:
curl -I https://192.168.50.171:8443 之類的唯讀確認),不要先改檔——若 Nexus 沒有 TLS,本卡要拆成「先讓 Nexus 上憑證(基礎設施工作,不是這張卡)」+「再全面換網址」兩段,而前一段屬另一個決策。wt-fix-b5C-export-escape。涵蓋 #102(掃描總表 122)+ 掃描總表 126(同病另一出口)。#102(總表 122)匯出 Word 時使用者填的文字沒有做跳脫處理就塞進範本,可以讓匯出的稽核文件夾帶偽造內容
tpl.render(context, autoescape=True)(一行),或把每個從資料來的字串先包成套件提供的安全型別。app/oscal/service/export/ssp_docx_generator.py:60 — tpl.render(context),缺 autoescape=Trueapp/oscal/service/export/ssp_docx_generator.py:57-69 — generate() 全函式(渲染後還會 _append_control_sections / _append_reference_docs,那兩段是用 python-docx 直接寫、不走範本渲染,要分別確認有沒有同樣的病)autoescape=True 打開後,範本裡原本刻意用到的標記會一起被跳脫。修完要實際匯出一份含特殊符號的內容(&、<、>、" 與中文全形符號)確認排版沒被打壞。總表 126(同一組修法,同一張卡)匯出控制項現況的 Excel 把使用者寫的字當公式執行
= 開頭,它就判定那是公式,寫進檔案時標成公式而不是文字。於是「使用者打的一段描述」在別人的 Excel 裡變成一行會跑的程式。下載這份 Excel 的同事打開檔案,Excel 會去跑那段公式:可以把檔案內容偷傳出去,也可以在對方電腦上執行指令(需對方按下「啟用外部內容」提示)。= + - @ 中和掉(前置一個單引號是慣用做法)。app/oscal/service/ssp_control_impl_import_service.py:139 — 寫格處(Excel 匯出)app/oscal/service/ssp_control_impl_import_service.py:163 — 同一支檔的第二個寫格處startswith(('=、sanitize_cell、formula、neutral 等都零命中(app/module_frame/excel_template/ 與 app/flow_control/service/job_import_service.py 命中的 formula1 是 Excel 下拉選單的合法用法,不是這件事)。所以是「本卡要新抽這支」,不是「沿用既有那支」;而第 78/82 項在別的批,本卡抽出來的函式要放在它們也拿得到的地方。這件事影響落點,見 D-b5C-10。- 開頭的描述(例如「- 尚未實作」這種條列)在 Excel 裡會多一個看不見的單引號。這是標準做法、不影響列印與閱讀,但要實際打開一份確認眼睛看不出差別。這張卡的手測總清單:
&、<、>、" 與中文全形符號的文字=SUM(1+1) 開頭的字 → 匯出 Excel → 用 Excel 開啟,確認顯示成文字、不是公式,且沒有跳出「啟用外部內容」提示- 開頭的正常條列描述 → 匯出 Excel → 確認看起來沒有異樣需決策者先裁的:
common/util/ 下(最近、但第 78/82 項若在套件側就拿不到);②jedi-common 的 utils(三個出口都拿得到,但動套件要走 path dependency 流程、且要確認 78/82 那兩條的出口在哪個 repo)。我的建議:先查第 78/82 項的出口落在哪個 repo(78 是「匯出操作記錄的 Excel」→ 疑似 jedi-log;82 是「匯出意見回饋的 Excel/CSV」→ 疑似 jedi-issue),若確認在套件側就放 jedi-common,本卡從那裡取用;查證屬本卡可做,落點決定要回報決策者。不要三個出口各寫一份(那就是同一個病三套真相)。jedi-iam(已修,只驗)+ jedi-survey + jedi-log。worktree 建議 wt-fix-b5C-password-gen。涵蓋 #137(總表 §3.2-12)。#137(總表 §3.2 第 12 項)系統自動產生的密碼比自家要求的最短長度少一個字元(要 12 給 11),可能通不過自家的規定
jedi-iam/jedi_iam/common/utils/common_util.py:67 已改成 range(length - len(password)),commit edc9118(「fix(iam): 帳號匯入改必填 + 修密碼產生器少一碼(決策者 2026-09-16 指示)」),同檔 :61-65 還留了說明這個坑的註解。所以這一條在有呼叫者的路徑上已經不成立。- 4 的是兩支沒人用的複製品:jedi-survey/jedi_survey/common/utils/common_util.py:23、jedi-log/jedi_api_log/api_log/common/utils/common_util.py:23(兩支都是逐字複製,含那行被註解掉的 # secrets.choice(string.punctuation))。我實查兩支在自己套件內與主專案都零呼叫者。jedi-system-core/jedi_system_core/common/utils/common_util.py:5 的檔頭註解寫著「generate_complex_password 於 CM-1699 刪除——本套件與主專案皆零呼叫者,密碼生成屬⋯」,同一個函式在那支套件已經按「沒人用的東西不要留著」刪掉了。common_util.py:64 指的是 jedi-iam 那支、已於 edc9118 修畢」講清楚,讓下一棒不會再來一次。jedi-iam/jedi_iam/common/utils/common_util.py:51-71 — 已修的那支(:67 是關鍵行)jedi-iam/jedi_iam/app/service/user_service.py:157 — 呼叫點一:generate_complex_password(12)jedi-iam/jedi_iam/app/service/user_batch_import_service.py:102 — 呼叫點二:user_supplied_password or generate_complex_password(12)jedi-survey/jedi_survey/common/utils/common_util.py:9-28 — 零引用複製品一(:23 是 - 4)jedi-log/jedi_api_log/api_log/common/utils/common_util.py:9-28 — 零引用複製品二(:23 是 - 4)jedi-system-core/jedi_system_core/common/utils/common_util.py:5 — 前例(CM-1699 已刪).venv)與 BE 主專案各 grep 過 generate_complex_password,jedi-survey 與 jedi-log 兩支除了自己的定義行之外零命中。runner 動手前要再擴查 FE、agent repo、scripts/、以及各套件 tests/(jedi-iam 的 tests/unittest/test_user_service.py:1095 有提到這個函式,那是 iam 自己的測試、不影響另兩支)。__init__ 或 re-export 沒有把它對外公開。poetry install + import 確認沒炸。這張卡的手測總清單:
需決策者先裁的:
| # | 問題(白話) | 我的建議 | 卡 | 不裁會怎樣 |
|---|---|---|---|---|
| D-b5C-1 | 檔案預覽要放行哪些格式?清單外一律變成「下載」而不是「在頁面裡打開」 | 只放行網頁報告(html/htm)+ PDF +常見圖片;清單放在套件設定讓客戶可調,預設值由套件給 | 5C-1 | runner 自己決定清單,可能把客戶在用的某種預覽關掉 |
| D-b5C-2 | 安裝程式要不要現在就把「連檔案儲存空間走加密」打開?這取決於內建的物件儲存有沒有開加密 | runner 先查實機並回報,不要先改;若內建儲存沒開加密,這條要連帶做「內建儲存也上憑證」,範圍就超出一張修正卡 | 5C-2 | 改了而內建儲存不支援 → 裝出來的機器上傳功能整個掛掉,而且是裝完才發現 |
| D-b5C-3 | 現有三個環境(DEV/STG/POC)的七筆「不加密」設定要不要跟著切成加密? | 開發期只動 DEV;STG/POC 照環境異動鐵律等放行。本卡只交「切換步驟與回退方式」 | 5C-2 | 誤動 POC(等同 production)會斷掉對外 demo |
| D-b5C-4 | AI 那兩塊,每人每分鐘可以問幾次?等外部 AI 最多等幾秒? | 儀表板 3 次/分(一次問兩次 AI)、AI 助手 10 次/分;逾時 60 秒(要小於系統 120 秒強制砍掉的門檻) | 5C-3 | 抓太嚴會擋到正常使用者;不設就是「整個產品對所有人停止回應」那條路沒堵 |
| D-b5C-5 | 次數限制的機制要用什麼做?目前主專案沒有通用的限流機制 | 沿用唯一的既有前例(OTP 寄送節流:Redis 到期時間 + 回 429 帶剩餘秒數),兩支 AI 套件各開插槽、主專案接同一支實作;不要引進新的第三方限流套件 | 5C-3 | runner 各自造一套,日後兩塊行為不一致 |
| D-b5C-6 | 環境設定值完全沒設時(不是打錯字),也要退回正式設定嗎? | 兩種情況都退正式設定(少一套規則);但要先開檔核對安裝程式與出貨設定真的都已顯式寫出,核對不成立就停下回報 | 5C-4 | 出貨機若真的沒設,客戶機房的紀錄行為會靜默換一份設定 |
| D-b5C-7 | 清單「一頁最多幾筆」的上限抓多少?盤點到有正當的「撈全部」需求怎麼辦? | 上限 1000;有正當需求的改走專用匯出路徑或內部分頁迴圈,不要為它們開白名單參數(開了上限就可被繞過) | 5C-4 | 沒盤完就加上限 → 某些統計數字悄悄變小且不會報錯 |
| D-b5C-8 | 公司內部套件倉庫現在有沒有開加密連線?沒有的話要先做什麼? | runner 第一步只做唯讀查證並回報;若倉庫沒有加密,本卡要拆成「先讓倉庫上憑證(基礎設施工作)」+「再全面換網址」兩段 | 5C-6 | 直接改 → 每一台開發機與打包機的套件安裝立刻全部失敗 |
| D-b5C-9 | 已封存的 6 支套件要不要一起改成加密連線? | 一起改(成本幾乎為零;封存不等於不會被拿來裝,漏改的那支就是下一次的破口) | 5C-6 | 留一個不受保護的安裝來源 |
| D-b5C-10 | 新抽的「公式字元中和」函式放哪裡?(FR-113 說可以沿用既有那支,但我實查那支不存在,是本卡要新抽的) | 先查第 78/82 項的出口落在哪個 repo(那兩條在別的批);若在套件側就放 jedi-common,讓三個出口共用。不要三個出口各寫一份 |
5C-7 | 三個出口三份實作,等於同一個病三套真相,補一處漏兩處 |
| D-b5C-11 | 兩支沒人用的密碼產生器複製品(jedi-survey、jedi-log)要刪掉還是同步修? | 刪掉——原則十五、且同一個函式在 jedi-system-core 已按同樣理由刪過(CM-1699)。runner 先完成擴大零引用查證並回報,不自己刪 | 5C-8 | 留著=同一件事三份真相,下次修 jedi-iam 時這兩支又落後 |
common_util.py:64 少一碼,但真正有人用的那一支(jedi-iam)已於 commit edc9118 修畢(決策者 2026-09-16 親自指示的那次)。仍是 - 4 的兩支都零呼叫者。這條的性質從「修 bug」變成「清複製品」。startswith(('=/sanitize_cell/formula/neutral 皆零命中)。5C-7 是新抽,不是沿用;而第 78/82 項在別的批,落點要跨批協調,否則會長出三份。