這 29 件全是同一個病:系統知道你是誰(登入過了),但沒問「這筆資料是你的嗎」——於是同一家公司內任何一個能登入的帳號,換個編號、送個空白查詢,就讀得到、甚至改得掉別部門、別專案、別客戶的稽核資料。修法幾乎都是「在原來只問登入的地方,補一行去問第 1 批做好的權限地基」,程式改動很小,但入口要抓全——同一個功能常有兩三個入口(清單/單筆/選單/匯入/AI 儀表板),漏一個就等於沒補。
排在第 2 批的理由:這批是 145 件裡「一般帳號就能打、而且真的拿得到別人資料」的主力,嚴重度最集中。但它依賴第 1 批——第 1 批把「誰能碰這筆資料」的判斷(專案參與者檢查、任務角色規則、檔案歸屬查詢通道)做成可以被呼叫的地基,這批 29 件才有東西可以呼叫。
能不能現在開:🔴 不能。要等兩件事:①第 1 批的程式合回 feature/review;②第 1 批動到的套件(jedi-task-platform、jedi-flow-engine、jedi-file-upload、jedi-survey)發版或至少在本機 path dependency 可用。這兩件到位後,九張卡彼此互不相干、可同時派(唯一要留意的序列組見下表)。
| 卡 | 卡名(做什麼) | repo/套件 | 涵蓋 SUMMARY # | 建議 model/effort | 序列組 |
|---|---|---|---|---|---|
| 2-1 | 檔案下載/預覽/換通行證/刪除四個出口,取檔前先問這個檔掛在哪、你碰不碰得到 | 套件 jedi-file-upload + BE 接線 + FE 驗證 | #3 | opus/high | A |
| 2-2 | 問卷的六支讀取功能補上「編號必填 + 你是這個專案的人」,含多人共編房間 | 套件 jedi-survey | #5、#6、#48、#49、#50、#51 | opus/medium | — |
| 2-3 | 稽核流程的證明清單、留言、階段歷程、當前階段四處補歸屬檢查,流程範本讀取補權限 | 套件 jedi-flow-engine + BE api/flow_engine + 套件 jedi-compliance-audit | #7、#8、#52、#53、#115 | opus/high | B |
| 2-4 | 專案人員名冊與任務指派清單四支查詢補「編號必填 + 你是這個專案的成員」 | 套件 jedi-task-platform | #54、#55、#58、#59 | sonnet/medium | — |
| 2-5 | 專案摘要報告五支讀取與稽核輪次選單補參與者檢查 | BE 主專案 + 套件 jedi-compliance-audit | #56、#57 | sonnet/medium | B |
| 2-6 | AI 儀表板 26 支查詢加「這支要什麼權限」欄位並在呼叫前檢查,拿掉專案清單的視同管理員後門 | 套件 jedi-ai-dashboard + BE di_containers + 套件 jedi-common | #60、#61、#62、#114 | opus/high | — |
| 2-7 | 查任務詳細資料補「這筆任務屬不屬於網址上那個專案」,改與刪那兩支也補同一句 | BE 主專案 | #63 | sonnet/medium | — |
| 2-8 | 合規資源庫三個入口補歸屬檢查、建立入口補權限與用量上限 | BE 主專案(oscal) | #46、#65 | opus/high | C |
| 2-9 | 刪除佐證文件與文件庫文件時,核對「要刪的那筆屬不屬於你管的那份計畫」 | BE 主專案(oscal) | #64 | sonnet/medium | C |
| 2-10 | 弱點檢測「測試連線」補權限宣告,八支讀取功能補權限並修掉誤導的說明文字 | 套件 jedi-detection | #4、#47、#113 | opus/medium | — |
序列組說明:A 只有一張,獨立。B(卡 2-3 與 2-5)都會碰
api/flow_engine/routes/stage_rollback_route.py家族與jedi-compliance-audit的audit_round_app_service.py,不要同時派。C(卡 2-8 與 2-9)都改 BEapp/oscal/service/下的檔,且 2-9 要照抄 2-8 建立的寫法,2-8 先、2-9 後。卡數 10 張、上表寫 9 張——是因為 2-9 是 2-8 的續作、同一個 worktree 接著做(同組 C),計畫上算一個工作區、兩張卡分開驗收。
jedi-file-upload(主體)+ BE core/plugins/file_upload.py(接線)+ FE 手測。worktree 建議 wt-fix-b2-file-upload。涵蓋 #3。#3 任何一個登入帳號,知道檔案編號就能下載、刪除別人的稽核證據,還能換發一張不必登入就能下載的通行證傳到公司外面(同一個洞的四個出口)
docs/security-report/M02-file-upload.md:217 已拍板的形狀):一個共用通道 + 一張登記表。檔案模組留一個空位說「我需要一段能回答『這個人能不能碰這個檔』的程式」,各業務模組把自己的回答程式登記進來;取檔時先查出這個檔屬於哪一類、屬於誰,再呼叫對應那一段。那份登記是程式裡的一張對應表(十幾列),不是資料庫的表——規則是活的邏輯(要查成員、看角色、算狀態),做成資料表等於用資料庫寫程式。ref_id 是轉檔 PDF 的自我參照(全庫 2,821 筆只有 2 筆有值)、owner_user_id 96% 是空的,真正的歸屬在下游三張表反過來指著檔案——job_evidences.file_id(稽核證據)、ssp_reference_documents.file_id(安全計畫佐證)、issue_upload_file_mapping.file_uid(意見單附件)。jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:141(UploadFileDownloadRoute.get,下載;現在只有 @file_access_guard(bind_uid_arg="uid"))jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:173(UploadFilePreviewAsPDFRoute.get,PDF 預覽;同上)jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:123(UploadFileAccessTokenRoute.get,換通行證;現在只有 @auth_required,第 135 行直接簽 st 出去,一句歸屬都沒問)jedi-file-upload/jedi_file_upload/api/routes/upload_file_route.py:91(UploadFileRoute.delete,刪除;只有 @auth_required,第 97 行直接 delete_file(uid))jedi-file-upload/jedi_file_upload/app/service/upload_file_service.py:37(get_upload_file,「取檔那一層」——報告點名這一層最該補,因為它已經把檔案找出來、最有條件問「這是誰的」)jedi-file-upload/jedi_file_upload/app/service/upload_file_service.py:47(delete_file)、:53(delete_files)jedi-file-upload/jedi_file_upload/api/guards.py:42(file_access_guard,委派宿主 adapter 的位置——新增的「問歸屬」空位掛這裡)jedi-file-upload/jedi_file_upload/domain/ports.py(新 port 的落點;現有 IUploadFileProvider:54、IUploadProviderFactory:108 是同一份契約檔)jedi-file-upload/jedi_file_upload/plugin/contract.py(FileUploadAdapters 要加一個欄位收宿主的登記表;⚠️ 該 dataclass 沒有 capability_required 欄位,多塞 key 就是 TypeError,見 core/plugins/file_upload.py:113 的註解)core/plugins/file_upload.py:110-111(BE 接線:file_access_guard=signed_token_or_jwt、signed_token_issuer=issue_signed_token 就在這兩行,登記表要從這裡遞進去)app/flow_engine/service/job_evidence_service.py(稽核證據,可直接複用 app/flow_engine/service/workflow_execution_service.py:126 的 assert_project_participant)、app/oscal/service/ssp_control_implementation_service.py(SSP 佐證)、意見單附件的回答程式在套件 jedi-issue(jedi-issue/jedi_issue/infra/issue_upload_files/models/issue_upload_file.py:8 就是 issue_upload_file_mapping 那張表,BE 側零引用)——這張卡因此會動到第四個套件,開卡時要寫進範圍src/utils/signedDownloadUtil.js、src/components/grc/DocumentPoolPanel.vue、src/components/grc/EvidencePreviewDialog.vue、src/components/grc/ReferenceDocumentList.vue、src/components/grc/JobExecutionDrawer.vue、src/components/grc/AuditControlRef.vue、src/components/grc/ModuleFrameDocumentPoolPanel.vue、src/views/feedback/system/SystemFeedbackView.vue、src/views/feedback/system/SystemFeedbackForm.vue、src/views/module_frame/ModuleFrameTemplateEditView.vue(端點常數在 src/config/api/api.js:91-94)這張卡的手測總清單:
window.open 那條路(原生 GET 帶不了 bearer、先換 st)要實際點過——這是 signedDownloadUtil.js 的主要路徑,最容易被改壞。/file/pdf-preview)要在「檔案存在物件儲存」與「remote_agent 儲存」兩種情況各測一次。需決策者先裁的:見文末 D-b2-1、D-b2-2。
jedi-survey。worktree 建議 wt-fix-b2-survey。涵蓋 #5、#6、#48、#49、#50、#51。docs/security-report/M05-survey.md:135-156):
jedi_survey/plugin/assembly.py:57-59 已經把宿主的 project_role_guard 接進來了),讀取側用主系統共用的「是不是這個專案的參與者」(common/authz/project.py:101 assert_project_participant),兩支都已經存在,沿用同一條接線即可、不必另外拉線。#5 同一家公司內任何有帳號的人,送一個空白請求到問卷的「查填答歷史明細」,就能讀走全公司每一份問卷的每一版答案與稽核審核意見(🟠 高,本批問卷組最嚴重的一支——外洩的是受檢單位坦承的缺失與稽核人員的判斷,是稽核過程本身外洩)
jedi-survey/jedi_survey/api/routes/question_answer_history_detail_route.py:18(QuestionAnswerHistoryDetailsRoute.post,掛在 /project-survey/history-details,見 api/routing.py:107)→ jedi-survey/jedi_survey/app/service/question_answer_history_detail_service.py:19(get_all_question_answer_history_detail(**kwargs),kwargs 全可空就是空白查詢回整張表的成因)#6 任何登入帳號拿一個問卷編號去讀答案,系統不問這份問卷是不是你的,別部門的作答內容、分數與審核意見全看得到(🟠 高)
jedi-survey/jedi_survey/api/routes/question_answer_route.py:28(TaskSurveysAnswersRoute.get,掛 /project-survey/answers/<uid>)→ jedi-survey/jedi_survey/app/service/question_answer_service.py:90(get_answer_list_by_task_survey_uid)。已有檢查可照抄的三支寫入:同檔 :104 update_task_survey_answer、:217 checkpoint_task_survey_answer、:376 patch_task_survey_answer#48 任何登入帳號送空白請求到「列出任務問卷」,就拿到整個客戶全部問卷的編號、所屬專案、狀態、受檢設備與部門(🟡 中,是 #6 的入場券)
jedi-survey/jedi_survey/api/routes/task_survey_route.py:29(TaskSurveysRoute.post,掛 /task-surveys)→ jedi-survey/jedi_survey/app/service/task_survey_service.py:59(get_task_surveys(<strong>kwargs))。單筆那支 task_survey_route.py:40(TaskSurveyRoute.get)→ task_survey_service.py:66(get_task_survey)也要一起補**,否則清單擋住了、單筆還通#49 任何登入帳號送空白請求到「列出填答歷史」,就能看到全公司跨所有專案誰在什麼時候改了哪份問卷(🟡 中;同時提供 #5 與第 1 批 #38 需要的版本編號)
jedi-survey/jedi_survey/api/routes/question_answer_history_route.py:32(QuestionAnswerHistoriesRoute.post,掛 /project-survey/histories)→ jedi-survey/jedi_survey/app/service/question_answer_history_service.py:35(get_all_question_answer_history(<strong>kwargs))。選單那支 question_answer_history_route.py:22(QuestionAnswerHistoryMenuRoute.get)→ 同 service :42(get_question_answer_history_menu)一起補**#50 任何登入帳號送空查詢到「問卷討論列表」,就把全公司的討論串撈回來——裡面是稽核過程的缺失細節與審查意見(🟡 中)
jedi-survey/jedi_survey/api/routes/survey_discussion_route.py:25(SurveyDiscussionsRoute.post,掛 /survey-discussions)→ jedi-survey/jedi_survey/app/service/survey_discussion_service.py:31(get_survey_discussions(survey_uid, task_survey_uid)——兩個參數就是報告說的「兩個查詢條件」,目前都可空)#51 多人同時填問卷的「房間」想進哪間就進哪間,可以旁聽別專案正在編輯中的問卷並抓到當下的答案快照(🟡 中)
jedi-survey/jedi_survey/app/handler/fill_survey_socketio_handler.py:59(on_join,第 66 行 room = data.get('room') 直接吃呼叫端給的房號、第 102 行 join_room(room) 就進去了);同檔要一起看的還有 :130 on_editing、:142 on_update、:182 on_edit、:203 on_endedit(每支都自己 data.get('room'),只守 on_join 不夠——這五支不必先 join 也能指定房號)。⚠️ 這是 SocketIO 路徑、不是 REST,RUN_MODE=socketio 才載(jedi_survey/plugin/__init__.py:149),手測要另起那個模式#59 全系統共用的取資料底層有一條「查詢條件全沒填就不過濾」的規則,送一個空白查詢就等於回整張表(🟡 中,已裁定:底層不動、只修有洞的那幾支)
這張卡的手測總清單:
RUN_MODE=socketio 起一份,兩個瀏覽器同時開同一份問卷確認共編正常;再拿沒參與的帳號指定別人的房號,要進不去、也抓不到答案快照。jedi-flow-engine、BE api/flow_engine/ + app/flow_engine/、套件 jedi-compliance-audit。worktree 建議 wt-fix-b2-flow。涵蓋 #7、#8、#52、#53、#115。序列組 B(與卡 2-5 不同時派)。docs/security-report/M06-flow-engine.md:149-179,稱「統一修法」):套與卡 2-1 同一個形狀——一個共用通道 + 一張登記表,先查出這筆資料屬於哪一類、屬於誰,再呼叫對應的那一段。三條規則同樣定死:查不到登記 → 拒絕;沒人認領 → 拒絕;拒絕回「找不到」不回「沒權限」。
app/flow_engine/service/workflow_execution_service.py:126 的 assert_project_participant(workflow_execution_id, known_project_id=None) 就是「由流程實例反查專案、再問是不是參與者」的現成通道(fail-closed、任何角色都放行、只擋非參與者),:141 的 resolve_project_id_from_workflow 是公開入口。要問「這筆流程資料是誰的」一律走這支,不要另外兜一條鏈(同一個問題兩套查法遲早分歧,而守門判錯的症狀是靜默放行)。#7 任何登入帳號知道一個任務編號,就能讀走別家客戶的稽核證明清單(含檔案編號),拿到編號可以接著直接下載檔案本體(🟠 高,⚠️ 部分修)
workflow_execution_service.assert_project_participant(...)(同檔的新增、更新、刪除都已經有做,只有讀取漏掉,明顯是漏寫不是設計——照抄它們)。api/flow_engine/routes/job_evidence_route.py:27(JobEvidencesRoute.get,清單)、:64(JobEvidenceRoute.get,單筆)→ app/flow_engine/service/job_evidence_service.py:46(get_job_evidences_by_job_execution_uid)、:67(get_job_evidences_uid)。已有檢查可照抄:同 service :79 add_job_evidence、:146 update_job_evidence、:165 delete_job_evidence#8 任何登入帳號能讀走別人專案的整串稽核討論,也能往裡面塞留言——留言會即時推播給該專案全體成員,看起來就像同事發的(🟠 高,這組五條裡唯一「能寫」的)
api/flow_engine/routes/flow_engine_route.py:46(WorkflowExecutionCommentRoute.get,讀;第 52 行拿 uid 直接 get_workflow_execution,一句歸屬都沒問)、:65(同類別的 put,寫)。可照抄的既有寫法:jedi-task-platform/jedi_task_platform/task/app/service/job_comment_service.py:45(它已經在呼叫 self._workflow_execution_service.assert_project_participant(we_id)——同產品內同類留言的正確寫法就在那裡)app/notification/handler/notification_socketio_handler.py:41 join_room),補完要確認正常參與者留言後全體成員仍收到推播。#52 任何登入帳號知道一個稽核輪次編號,就能讀走別家客戶的階段歷程,含退回理由的完整內容(🟡 中;退回理由是稽核人員親筆寫的「哪裡有問題」)
api/flow_engine/routes/stage_rollback_route.py:56(RoundStageTransitionsResource.get,收了 project_uid 與 round_uid 兩個參數,第 63 行只把 round_uid 傳下去、project_uid 丟掉)→ jedi-compliance-audit/jedi_compliance_audit/app/service/audit_round_app_service.py:870(list_stage_transitions(round_uid, locale),第 872 行取輪次、第 877 行列歷程,中間沒有任何守門)。套件側可用的守門:jedi-compliance-audit/jedi_compliance_audit/common/guard.py:47 assert_project_role/:58 assert_project_role_fallback(宿主已接線,plugin/contract.py:99 project_role_guard)#53 「查目前在哪個階段」用你自己填的專案編號查角色、用你自己填的輪次編號撈資料,兩者不核對,角色不符也只是把「能不能推進」標成否、其他資料照樣整包回傳(🟡 中)
api/flow_engine/routes/stage_advance_route.py:27(StageInfoResource.get)→ app/flow_engine/service/stage_advance_service.py:82(get_current_stage_info;第 83 行 _resolve_workflow_context(round_uid) 用輪次撈資料、第 113 行 _get_user_role(project_uid, user_id) 用呼叫端給的專案查角色——兩個來源不同、從不比對,第 114 行只把結果塞進 user_can_advance)。推進:api/flow_engine/routes/stage_advance_route.py:48(StageAdvanceResource.post)→ app/flow_engine/service/stage_advance_service.py:191(advance_stage,第 289 行同一個模式,第 292 行雖然會 raise 但用的仍是呼叫端給的 project_uid)。退回:api/flow_engine/routes/stage_rollback_route.py:28(StageRollbackResource.post)→ app/flow_engine/service/stage_rollback_service.py:73(rollback_stage,第 95 行 _get_user_role(project_uid, user_id) 同病)#115 同一家客戶內沒被授權的帳號可以讀走全部流程範本與系統內建範本的完整內容(階段設計、角色指派、判斷條件)——前端選單擋住了,但功能本身是敞開的(⚪ 低)
api/flow_engine/routes/flow_template_route.py:33(FlowTemplatesRoute.get,清單)、:62(FlowTemplateRoute.get,單筆)——兩支只有 class 層的 method_decorators = [jwt_required()],同檔的 post :49/put :70/delete :80 都掛著 _flow_template_create/_flow_template_update/_flow_template_delete(定義在同檔 :54-56 一帶),讀取兩支照抄即可這張卡的手測總清單:
jedi-task-platform(participant 子模組)。worktree 建議 wt-fix-b2-participant。涵蓋 #54、#55、#58、#59(#59 見卡 2-2 說明,兩張卡合起來才算處置完)。docs/security-report/M10-task-platform.md:153-161):三步,一步都不能省——①查詢與選單兩支都補「你是不是這個專案的成員」(只補一支另一支照樣拿得到同樣資料);②專案編號改必填、沒給就拒絕(不改必填,「空白查詢回整張表」這條路還在);③同型四處一起修(分開修會漏)。已確認所有呼叫這支功能的地方都帶專案或任務編號,改成必填不會影響現有功能。
jedi-task-platform/jedi_task_platform/participant/common/guard.py:32 的 assert_project_manager 是現成的模組級函式(宿主實作是 common/authz/project.py:16,經 participant/domain/ports.py:82 的 IProjectRoleGuard 注入,缺了會拒絕掛載)。⚠️ 但本卡要的是「參與者」不是「manager」——讀取門檻是任一參與者(common/authz/project.py:101 assert_project_participant),port 目前只宣告了 assert_manager,要在 IProjectRoleGuard 加一支 assert_participant 並在宿主 core/plugins/ 對應處接線(見 D-b2-4)。#54 同一家公司裡任何能登入的人,對專案成員查詢送一個連專案編號都不填的空白查詢,一次拿到公司內全部專案的成員名冊(🟡 中)
jedi-task-platform/jedi_task_platform/participant/api/routes/project_participant_route.py:39(ProjectParticipantsRoute.post,掛 /project-participants,見 participant/api/routing.py:61;第 46 行 <strong>req_data 直接餵進 service)、:20(ProjectParticipantsMenuRoute.get,掛 /project-participants/menu,見 routing.py:61;project_id/project_uid 兩個參數都預設 None**)。已有守門可照抄:同檔 :55 post/:71 put/:84 delete#55 同公司裡不是這個專案的人,在網址上換一個編號送出查詢,就讀到別人專案在群組層與控制項層的完整人員配置(🟡 中)
jedi-task-platform/jedi_task_platform/participant/api/routes/project_control_participant_route.py:24(ProjectControlParticipantsMenuRoute.get,掛 /project-control-participants/menu,見 routing.py:71;六個參數全預設 None)、:52(ProjectControlParticipantsRoute.post,掛 /project-control-participants,第 57 行 <strong>req_data;⚠️ 就是這支「順便把專案層成員一起撈出來」**——第 58-60 行走 get_control_participants_with_inherits_by_uid)。已有守門可照抄:同檔 :79 post/:101 put/:123 delete;群組層的 control_group_participant_route.py:19 post/:39 put/:59 delete 也是已有守門的樣本#58 同一家公司裡任何能登入的人,對任務指派清單送一個空白查詢,一次拿到公司內全部一萬多筆任務指派紀錄(🟡 中)
jedi-task-platform/jedi_task_platform/participant/api/routes/task_assignee_route.py:44(TaskAssigneesRoute.post,掛 /task-assignees,見 routing.py:66;第 48 行 **req_data 直進 get_task_assignees)。同檔已有守門可照抄::20 TaskAssigneeBatchRoute.post、:59 post/:73 put/:95 delete這張卡的手測總清單:
app/project_summary_report/、api/project_summary_report/、api/project/routes/audit_round_route.py + 套件 jedi-compliance-audit。worktree 建議 wt-fix-b2-report-round。涵蓋 #56、#57。序列組 B(與卡 2-3 不同時派)。#56 同公司裡不是這個專案的人,直接開專案摘要報告就讀到稽核結論全文與歷史版本(歷史版本常保留後來被刪掉的稽核發現)(🟡 中)
app/project_summary_report/service/project_summary_report_service.py:35 _require_participant(project_id)、:44 _require_participant_by_report_uid(uid)(內部走 common/authz/project.py:101 assert_project_participant)。寫入側 :111 create/:138 update/:164 delete、以及匯出用的 :95 get_project_summary_report_by_uid(:100 有呼叫)都已經在用了,只有讀取這幾支漏掉。app/project_summary_report/service/project_summary_report_service.py:53(get_project_summary_reports_and_pager,清單頁——這支要改成「只列我參與的專案」,不是補必填):73(get_project_summary_reports):78(get_project_summary_report,單筆 by uid,History 頁 getReport() 打的就是這支):89(get_project_summary_report_by_id)app/project_summary_report/service/project_summary_report_history_service.py:29(get_project_summary_report_histories)、:36(get_project_summary_report_history_by_uid)——歷史版本這兩支是「常保留後來被刪掉的稽核發現」的出口,優先補;同檔 :47 revert_...(第 57-58 行已有 assert_project_participant)是可照抄的樣本api/project_summary_report/routes/project_summary_report_route.py:74/:94/:111/:153、api/project_summary_report/routes/project_summary_report_history_route.py:28/:44#57 同公司裡不是這個專案的人,在網址上換成別人的專案編號,就看到那個專案全部稽核輪次的名稱、狀態、起訖日與人員(🟡 中)
api/project/routes/audit_round_route.py:74(AuditRoundsListRoute.post,收 project_uid,第 76 行直接 list_rounds(project_uid))→ jedi-compliance-audit/jedi_compliance_audit/app/service/audit_round_app_service.py:211(list_rounds)。單筆 audit_round_route.py:85(AuditRoundRoute.get,by round_uid)一起補。✅ 已補查(2026-09-21):「同檔的統計功能」= AuditRoundFindingsRoute.get(:258,AO 全量判定矩陣,服務層 docstring 自己寫「+ stats 分母」)。但整份檔案的唯讀 GET 共 8 支、全部是同一個形狀、全部零參與者檢查,建議首腦把範圍訂成「這 8 支一起補」而不是挑其中一兩支。全 8 支唯讀 GET 逐支核對結果(route 檔 api/project/routes/audit_round_route.py 共 32 個 route method、27 個 class;每一支的 decorator 都只有 @doc + @jwt_required() + @require_license("audit") + @inject,零參與者檢查):
| # | route(file:line) | class.method | 服務層落點 | 服務層有沒有守門 |
|---|---|---|---|---|
| 1 | :74 |
AuditRoundsListRoute.post(列表,收 project_uid) |
jedi-compliance-audit/.../audit_round_app_service.py:211 list_rounds |
❌ 只有 _resolve_project_id + 撈列表(:212-215),無 _check_role(同檔 create_round「:230」有) |
| 2 | :85 |
AuditRoundRoute.get(單筆,by round_uid) |
同檔 :218 get_round |
❌ 只有 get_by_uid + 404(:219-222) |
| 3 | :163 |
AuditRoundApRoute.get(AP 內容) |
BE 主專案 app/flow_control/service/assessment_plan_app_service.py:511 get_ap_for_round |
❌ 只有 round/AP 的 404 檢查(:512-519) |
| 4 | :234 |
ApPartiesRoute.get(稽核人員名單) |
BE 主專案 同檔 :704 list_ap_parties |
❌ docstring 明文寫「讀取不卡角色(AP 頁參與者皆可見)」——注意這句話的前提是「AP 頁參與者」,但程式沒有驗他是不是參與者 |
| 5 | :258 |
AuditRoundFindingsRoute.get(判定矩陣=本件所指的「統計」) |
jedi-compliance-audit/.../assessment_result_app_service.py:392 get_findings |
❌ _require_round_ar_result(round_uid, writable=False)(:394),該 helper(:120-129)只判 round 存在 + ar_result 存在 + 階段可寫,不判人 |
| 6 | :333 |
AuditRoundRisksRoute.get(風險清單) |
同檔 :695 list_risks |
❌ 同上,writable=False(:696) |
| 7 | :419 |
AuditRoundPoamItemsRoute.get(POA&M 清單) |
jedi-compliance-audit/.../poam_app_service.py:246 list_items |
❌ _require_round_poam(round_uid, writable=False)(:247),該 helper(:59-70)同樣只判 round/poam 存在 + 階段,不判人 |
| 8 | :429 |
AuditRoundPoamItemRoute.get(POA&M 單筆詳情) |
同檔 :271 get_item |
❌ 同上(:277) |
三個要一起寫進卡片的要點:
jedi-compliance-audit,#3/#4 兩支在 BE 主專案 app/flow_control/service/assessment_plan_app_service.py。開卡時 worktree 要開兩個。writable=False 是刻意的、不要動——那個參數管的是「結案後仍可檢視」(_require_round_ar_result 與 _require_round_poam 的 docstring 都寫明),與「是不是參與者」是兩件事。補守門要另加一行參與者檢查,不是把 writable 改回 True(改了會讓結案輪次的 POA&M 頁整個打不開)。jedi-compliance-audit/jedi_compliance_audit/common/guard.py:48 assert_project_role(精確查詢,簽名 (participant_domain_service, project_id, user_id, allowed_roles, error_code))或 :58 assert_project_role_fallback(含 control→group→project 繼承,簽名 不同:(role_service, project_id, allowed_roles, group_id, control_id, error_code))。⚠️ guard.py:57-60 的 docstring 明文警告兩支簽名不同、照另一支的形狀改會靜默傳錯位置參數——這一句要原樣寫進卡片。讀取類要放行任一角色(含 viewer),所以 allowed_roles 不能沿用寫入路徑的 ("manager",) 預設值。功能不能壞:稽核輪次選單是專案頁的高頻元件,補完要確認參與者切輪次照樣切得動。
這張卡的手測總清單:
jedi-ai-dashboard(主體)+ BE di_containers/dashboard_apis/(26 支申報)+ BE app/flow_control/service/project_service.py + 套件 jedi-common(序列化白名單)。worktree 建議 wt-fix-b2-ai-dashboard。涵蓋 #60、#61、#62、#114。#60 AI 儀表板呼叫的那 26 支查詢完全不問「你能不能看這筆資料」,基層員工因此拿得到整份組織架構、誰有什麼權限、公司有哪些客戶(🟡 中)
jedi-ai-dashboard/jedi_ai_dashboard/app/registry.py:37(DashboardApi dataclass——目前欄位只有 api_key/module/parameters/required_params/optional_params 等,沒有任何權限欄位,新欄位加在這裡;:71 as_metadata 與 :122 metadata 一併要處理)jedi-ai-dashboard/jedi_ai_dashboard/app/service/data_api_service.py:83(call_api——檢查就插在這裡:第 108-112 行是「①驗證 API 是否存在」、第 115-120 行是「②驗證必要參數」、第 123 行才「③執行」,新的權限檢查放在①之後、②之前或之後皆可,但一定要在③之前)di_containers/dashboard_apis/auth.py(4 支)、participant.py(7 支)、flow_engine.py(3 支)、oscal.py(2 支)、survey.py(2 支)、device.py、feedback.py、bulletin.py、system_menu.py、system_config.py、module_frame.py、project.py、user_auth_provider.py(各 1 支)#61 AI 儀表板查專案清單時把呼叫者當成管理員處理,不是專案成員的人也能列出該客戶底下全部稽核專案(🟡 中)
app/flow_control/service/project_service.py:212(get_projects_for_dashboard)——後門就是第 224 行的 is_admin=True,函式 docstring 第 217-219 行自己寫明「對齊 v1 dashboard『看本 tenant 全部專案』…is_admin=True 繞 grc user 參與可見性,tenant 隔離由 RLS 負責」。申報處:di_containers/dashboard_apis/project.py(project.get_projects,⚠️ 該檔 module="grc" 與 api_key 前綴 project. 不一致是既有事實,照抄不動以免動到 AI 的選擇行為)#62 任何登入帳號叫出 AI 儀表板說一句「列出所有公告」,就拿到整個客戶底下全部公告(含別部門的、停用草稿、未到發布時間的),前三筆還原文送到外部 AI(🟡 中)
di_containers/dashboard_apis/bulletin.py(bulletin.get_bulletins 的申報;optional_params 已有 org_unit_id 但儀表板不傳)→ jedi-bulletin/jedi_bulletin/app/service/bulletin_service.py:239(get_bulletins(user, <strong>kwargs))。⚠️ 該申報的註解已說明 parameters 第一個 user 現在收的是純 dict**({uid, login_name, org_unit_id})不是 UserContextDTO——改的時候別把它當成身分物件用#114 共用的「資料轉成回覆內容」工具資料上有什麼就吐什麼,呼叫的人忘了先篩選,畫面就會拿到不該看到的內部項目(⚪ 低)
jedi-common/jedi_common/utils/serialization_util.py:12(_ORM_SKIP_KEYS = frozenset({'metadata', 'registry', 'sa_instance_state'})——就是這三個「系統內部項目」,鹽值與超管旗標加在這裡)→ 同檔 :15 to_serializable(使用處)。⚠️ 此檔在 jedi-common,是全站共用工具,改它要確認其他 consumer 不受影響(加白名單只會少送欄位,風險低,但仍要在卡上點名這是跨套件改動)這張卡的手測總清單:
di_containers/dashboard_apis/ 挑一支把權限欄位留空,確認它被拒絕而不是放行(這是 #60 那條「不能省」的驗證,測完改回來)。api/flow_control/routes/job_route.py、app/flow_control/service/job_service.py。worktree 建議 wt-fix-b2-job-detail。涵蓋 #63。#63 任何登入的人在任務列表上看到一筆別人專案的任務編號,就點得開它的完整內容——入口雖然要填專案編號,但填真的、全零、亂打一串字三種都查得到同一筆(🟡 中,⚠️ 部分修)
api/flow_control/routes/job_route.py:82(ProjectJobDetailResource.get——第 88 行 job_service.get_job(job_uid) 收了 project_uid 但整支沒用它,這是主要缺口)api/flow_control/routes/job_route.py:104(同類別 put,第 118-127 行有傳 project_uid 進 update_job)api/flow_control/routes/job_route.py:147(同類別 delete)app/flow_control/service/job_service.py:140(get_job(uid)——簽名裡根本沒有 project_uid,要加參數並核對)app/flow_control/service/job_service.py:266(update_job,第 287 行 self._require_manager(project_uid)——只驗人、沒驗物)app/flow_control/service/job_service.py:211(delete_job(uid, project_uid=None),第 212 行同病;⚠️ project_uid 預設 None 代表不傳就完全不檢查)app/flow_control/service/job_service.py:126(_require_manager 本體,第 132-137 行;⚠️ 第 132 行 if self._participant_role_service is None 時的行為要確認是不是靜默放行——若是,那是另一個缺口,回寫時點名):217 delete_job_from_diagram(uid)(完全沒有 project_uid,但已實查:唯一呼叫端是 app/flow_control/service/workflow_xml_sync_service.py:124,該處的 to_delete 是由流程圖存檔的三方 diff 算出來的、不是使用者直接指定任務編號,守門走存檔端點那道(該檔第 122-123 行的註解已說明)——本卡不動它,但回寫時要點名「存檔端點那道守門本身是否核對專案」由 FR-112 那條線負責)ProjectJobListResource.post:52 與 MyJobListResource.post:231 列出來的任務,點進去每一筆都要能開。這張卡的手測總清單:
app/oscal/service/、api/oscal/routes/、app/module_frame/service/。worktree 建議 wt-fix-b2-resource-library。涵蓋 #46、#65。序列組 C(2-8 先做,2-9 接著在同一個 worktree)。api/oscal/routes/resource_library_route.py 與 app/oscal/service/resource_library_app_service.py 這一組檔上,而且 #65 的「三個入口要一起補」與 #46 的「三條路打同一塊地」指的是同一組入口(手動編輯、Excel 匯入、Word 匯入)。分兩張卡會互撞。#46 子公司的管理員在「資源庫」改範本時,可以把母公司分享下來的範本的適用控制項清單整個改掉或清空,底下實作記錄一併刪除——之後每個用這份範本開專案的人,以為在稽核範圍內的項目其實沒被評估(涵蓋掃描總表 #120 + #67 + M10-11)
common/authz/sharing.py 既有的 assert_scope_writable——🔵 那支警衛本來就是專門擋這件事的、寫得對,只是站錯位置。_init_ao_workflows(處理新增控制項那半邊)要補同一道檢查——只補一半等於沒補。oscal.profile_imports/oscal.ssp_implemented_requirements/oscal.catalog_control_parts)根本沒設客戶隔離、連客戶欄位都沒有,而且本來就設計成「跟著範本走」;補隔離是第二道保險、不是這條的解法(要補歸 FR-094「要先改結構」那一級)。compliance.module_frames,而這次的請求只送控制項清單、沒動到那張表任何欄位,程式根本沒對它發出更新指令——沒發指令,規則就沒機會觸發(#67 當初標「可信度中」的那個未實測疑點,O2 已經回答:不是拋錯也不是靜默 0 列,是連觸發機會都沒有)。②程式那層其實已經有一支對的警衛,但它只站在「使用者要改範本的分享身分」那個 if 裡面,只送控制項清單、不送分享身分就完全繞過。白話講就是「讀得到」被當成了「改得動」的通行證。app/oscal/service/resource_library_app_service.py:364(update_applicable_controls(module_frame_uid, include_controls, user)——第 371 行 @transaction 之後第 372 行直接 get_session()、第 373 行下 raw SQL 撈 compliance.module_frames,從頭到尾沒問一句「這份範本是不是你家的」)app/oscal/service/resource_library_app_service.py:412(上面那支的呼叫入口)common/authz/sharing.py 的 assert_scope_writable(現成的正確警衛,照抄它的判斷)app/module_frame/service/module_frame_service.py:110-118(繞過點——警衛就站在這個 if 裡面)app/oscal/service/resource_library_app_service.py:220(_init_ao_workflows,新增控制項那半邊;被 :419 呼叫——就在 update_applicable_controls 這條路徑上,另一個呼叫點 :497 是建立路徑)api/oscal/routes/ssp/ssp_excel_import_route.py:103(SspExcelImportConfirmRoute.post,掛 /ssp-excel-import/<parse_uid>/confirm,見 api/oscal/__init__.py:91)→ app/oscal/service/ssp_excel_import_app_service.py:441(_confirm_update_module_frame,Model A 既有資源庫、replace 語意——第 449 行 clear_ssp_body 先清舊 body 再整批重建,這就是「覆寫範本」那條路)app/oscal/service/ssp_docx_import_app_service.py:304(confirm_import,第 339 行 module_frame_uid = effective_source_uid 是 Model A 既有資源庫那條、第 347-352 行是 Model B 新建那條);route 在 api/oscal/routes/ssp/ssp_docx_import_route.py:96(SspDocxImportConfirmRoute.post,掛 /ssp-docx-import/<parse_uid>/confirm,見 api/oscal/__init__.py:99)#65 建立合規資源庫的入口完全沒有權限檢查、只驗有沒有登入,而隔壁做同樣事情的入口是有要求權限的(中等)
ModuleFrameRoute.post 的寫法)+用量上限。兩件事都要,因為每建一次就複製一整套框架控制項目錄、每個查核項目各產一張流程範本——這不是一筆小寫入,而系統沒裝任何流量限制,重複呼叫就是用很小的代價把資料庫灌大。api/oscal/routes/resource_library_route.py:43(ResourceLibraryCreateRoute.post,第 40-42 行只有 @doc + @jwt_required() + @inject;第 44-47 行的註解正是那段「說對了一半」的說明——它解釋了為什麼不會碰別人家資料,但沒回答「你有沒有資格建」與「能建幾次」)。⚠️ 同檔 :28(ResourceLibrariesRoute.post,列表查詢)同樣只有 @jwt_required(),開卡前確認它是不是也該守api/module_frame/routes/module_frame_route.py:58(ModuleFrameRoute.post,正確範本——第 56 行 @require_capability("module-frame.create") 就是要照抄的那一行)api/oscal/routes/ssp/ssp_excel_import_route.py:103(SspExcelImportConfirmRoute.post)→ app/oscal/service/ssp_excel_import_app_service.py:544(_confirm_create_resource_library,第 556 行呼叫 create_resource_library;由 :344 分派)app/oscal/service/ssp_docx_import_app_service.py:347(confirm_import 內的 Model B preselect-new 那條,直接呼叫 create_resource_library)app/oscal/service/resource_library_app_service.py:427(create_resource_library)——⚠️ 權限要補在三個入口、不是這裡(這支被匯入流程內部呼叫,補在這裡會擋掉正常匯入;但可在這裡加用量計數)這張卡的手測總清單:
app/oscal/service/。worktree 建議沿用 wt-fix-b2-resource-library(同組 C,2-8 做完接著做)。涵蓋 #64(掃描總表 #118 + #119)。#64 刪除控制項底下的佐證文件、刪除文件庫文件時,系統檢查的是「你管不管網址上那份計畫」,刪掉的卻是「你另外給的那份文件編號」,兩者從不核對(中等;後者還會連帶刪關聯、可能把實體檔一起永久銷毀)
app/module_frame/service/module_frame_reference_document_service.py:147-151——它先 get_by_uid(doc_uid) 撈出來,再比對 context_type 與 context_id,不符就 raise NotFound。照抄這個形狀。app/oscal/service/ssp_control_implementation_service.py:888-890(delete_reference_document(ssp_uid, doc_uid)——整個方法只有兩行:第 889 行 self._perm.require_manager(ssp_uid)(連回傳值都沒接)、第 890 行 self._ref_doc_ds.delete_by_uid(doc_uid) 直接刪)add_reference_documents() 寫對了(有接 ctx = ... 並用 ctx.ssp.id)——同一個檔案裡兩支方法一嚴一鬆,是漏掉不是設計;另外 :895 附近的 list_ao_reference_documents 也有 ctx = self._perm.require_participant(ssp_uid) 可參考app/oscal/service/ssp_document_pool_service.py:123-129(delete_from_pool——第 118 行 ctx = self._perm.require_manager(ssp_uid) 有守門但守錯對象;第 123 行 list_pool(ctx.ssp.id) 看起來像在核對歸屬,但那次查找的產出只有檔案編號(判斷要不要刪實體檔),找不到就填 None 繼續往下跑、不會擋;第 125 行 self._ref_doc_ds.get_by_uid(doc_uid) 已經把真正那筆紀錄抓在手上了,卻沒問一句「它屬於哪份計畫」;第 129 行照樣 delete_by_uid(doc_uid)。整段從頭到尾沒有一個 if 擋在刪除前面。)app/module_frame/service/module_frame_reference_document_service.py:147-151(照抄的正確樣本)file_uid 從 list_pool 取、file_id 從 get_by_uid 取,兩條路。)count_by_file_id 算在受害者那個檔案上,算完判定沒人在用實體檔案就被永久刪除——所以手測要確認正常刪除的連帶行為完全沒變:自己的文件刪掉後,關聯確實清掉、還有別版在用的實體檔沒有被誤刪。這張卡的手測總清單:
jedi-detection。worktree 建議 wt-fix-b2-detection。涵蓋 #4、#47、#113。#4 弱點檢測設定頁的「測試連線」按鈕沒檢查權限,任何登入者按一下,系統就把整家公司的維運帳密解密、送到按的人自己指定的那台主機(🟠 高)
jedi-detection/jedi_detection/api/routes/detection_tool_route.py:126(TenantDetectionToolConfigTestConnectionRoute.post——第 125 行只有 @require_license("plugin"),缺 @capability_required;第 133 行 test_connection(...) 就把帳密解密送出去了)。同檔三處現成寫法::67(TenantDetectionToolConfigsRoute.post,@capability_required(lambda rt: rt.config.plugin_update_capability))、:88(TenantDetectionToolConfigDetailRoute.put)、:105(TenantDetectionToolConfigResetRoute.post)。⚠️ 不要動 :143(TenantDetectionToolConfigRefCountRoute.get,查引用次數,沒掛是合理的)host/credential_group/agent_uid 三種參數形式)。#47 同一家客戶裡任何成員只要知道任務編號,就讀得到別人專案的掃描執行紀錄(🟡 中)
jedi-detection/jedi_detection/app/service/detection_orchestration_service.py:1649(list_executions(job_uid, tenant_id)——第 1668 行直接 list_by_job_execution_ordered(job_uid, ...),沒有 assert_project_participant)。八處現成寫法可照抄:同檔 :196、:871、:925、:1007、:1032、:1097、:1127、:1470(每處都是 self._wf_svc.assert_project_participant(job.workflow_execution_id);第 1085 行的註解還寫明了這道守門的設計意圖)group_uid 為 NULL、以「單筆自成一組」形狀回傳的紀錄)。#113 租戶管理員在權限矩陣把某個權限取消勾選,前端選單確實藏起來了,但後端八支讀取功能照樣回全部資料——那一格勾選其實不生效,管理員被騙了(⚪ 低)
jedi-detection/jedi_detection/api/routes/detection_profile_route.py:290「GET 不設 capability,與其他讀取端一致」、同檔 :540「讀狀態的 GET 不設 capability,與其他讀取端一致」;另 api/routes/detection_profile_taxonomy_route.py:7 有一句「限制讀取等於讓分類功能對租戶失效」——⚠️ 那一句講的是分類下拉、屬另一回事,判斷後再決定要不要動。此條與 CLAUDE.md「檔案內註解只寫為什麼與陷阱」那條鐵則方向一致:過期脈絡會帶偏下一個人。)jedi-detection/jedi_detection/api/routes/detection_profile_route.py):
:135(DetectionProfileListRoute.post,基準清單):163(DetectionProfileMenuRoute.get,下拉選單):211(DetectionProfileDetailRoute.get,單筆詳情):274(DetectionProfileVersionsRoute.get,版本歷史):303(DetectionProfileUsageRoute.get,主檔使用狀況):328(DetectionProfileVersionUsageRoute.get,版本使用狀況):507(DetectionProfileVersionControlsRoute.get,規則清單):527(DetectionProfileVersionExtractionRoute.get,抽取狀態):54-56 的三個 decorator(_profile_create/_profile_delete/_profile_update,🔴 是 lambda 延遲求值——route class 在 import 期定義、那時還讀不到 config,新增讀取用的 decorator 要照同樣形狀寫),掛在 :184 post/:230 put/:258 delete 等處這張卡的手測總清單:
detection-profile-taxonomies)要照樣列得出來(那支刻意不守是對的)。D-b2-1:孤兒檔要不要一併擋掉?(卡 2-1) 卡 2-1 的修法規定「三張下游表都認不出來的檔一律拒絕」,但全庫 2,821 筆裡一定有一批認不出來的(歷史遺留、系統產生的轉檔 PDF)。我的建議:runner 先在 DEV 用唯讀 SQL 數出「認不出來的有幾筆、長什麼樣」再回報,數量少就照原則直接擋(fail-closed 才安全),數量可觀就先擋、同時給一個「平台管理員可下載」的例外口,避免客服接到「舊檔下載不了」。
D-b2-2:刪除改成可復原,範圍到哪裡?(卡 2-1) 報告要求刪除改成「可復原的刪除」,但沒說復原期多長、誰能復原、要不要做畫面。我的建議:本卡只做到「資料層軟刪、實體檔保留」+平台管理員可用 SQL 撈回,復原畫面另開需求——否則這張卡會從補守門變成做新功能。
D-b2-3:流程範本到底要不要權限?(卡 2-3 的 #115) 報告給了兩條路:補權限(與前端選單一致),或產品決定「人人可看」就把權限與選單綁定一起拿掉。我的建議:補權限。流程範本含階段設計、角色指派、判斷條件,是稽核流程的骨架,不該人人可讀;而且「前端藏、後端開」本身就是要消除的不一致。
D-b2-4:IProjectRoleGuard 要加「參與者」那一支嗎?(卡 2-4) jedi-task-platform 的 port 目前只宣告 assert_manager,但本批讀取門檻是「任一參與者」。我的建議:加一支 assert_participant 到 port,宿主接 common/authz/project.py:101 的 assert_project_participant。這是套件契約異動,照 CLAUDE.md 外部套件規範要決策者點頭。
D-b2-5:資源庫該開放給哪些角色建?(卡 2-8 的 #65) 報告點明「公司內部任何角色(含唯讀帳號、一般稽核員)都能建範本庫,這件事該不該開放從沒被決定過」。我的建議:比照隔壁 ModuleFrameRoute.post 的權限點(範本編輯權),亦即租戶管理員與有範本編輯權的角色可建、唯讀角色不可建。
D-b2-6:資源庫建立的用量上限訂多少?(卡 2-8 的 #65) 每建一次就複製一整套控制項目錄與流程範本,需要上限;但原則十一說安全措施不可以擋住正常客戶。我的建議:上限訂在「一小時 10 次/每租戶」這種寬到正常使用碰不到的量級,超過回明確訊息(不是 500),並記一筆稽核事件。具體數字請決策者定。
D-b2-7:弱點檢測的權限矩陣與設備清冊要一起裁嗎?(卡 2-10 的 #113) 報告標「與設備清冊那塊同形,要一起裁」。我的建議:兩處一起裁、方向一致(都是「當初刻意放寬、現在重新確認」,比照 #61 的處理),但分兩張卡做——設備清冊那條不在第 2 批,本卡只做弱點檢測側,回寫時點名兩處方向已對齊。
D-b2-8:掃描總表 #123/#125(Excel/Word 匯入覆寫範本)歸哪批?(卡 2-8) O3a/O4 已判定它們與 #46 是「同一塊地被三條路打穿,必須三條一起修」,但 #123/#125 本身在 SUMMARY 的批次歸屬要確認。我的建議:若不在第 2 批,把它們併進卡 2-8 一起做——不然補完 #46 另外兩條照樣進得去,等於白做一輪。