V2 掃描報告 — 稽核計畫、稽核結果、改善計畫:服務與 repo(CM-2125)

範圍:套件側 jedi-oscal-v2 26 檔/1,880 行(AP 稽核計畫、AR 稽核結果、POA&M 改善計畫三種文件的服務層與資料存取層)。 掃描工具:Claude Code 官方 claude-security plugin,effort low,scoped 掃描(指定檔案清單,不是掃整個目錄)。 基準 commit:eafc7ae511d5(monorepo 主 checkout,工作區有平行 session 的未提交改動,標 dirty)。 驗證章:verified,0 findings(工具自己的正式清單是空的)。只掃不修。


1. 一句話結論

工具這次沒有找到任何問題,但這不代表這批程式碼是乾淨的。 卡片原本就點名了六個可疑的地方,我把這六個地方的程式碼逐一開檔核對過,其中有四個確認存在——程式碼寫法就是卡片講的那樣,只是工具的自動掃描沒有抓到。這四個問題的共同點是:「接東西進來的時候,沒有檢查這東西是不是真的屬於同一份文件」。舉個例子:稽核結果裡有很多「判定」(這條規定符不符合),也有「風險」(這條不符合會有什麼後果),把判定接到風險上的那支程式,完全沒有檢查這個判定跟這個風險是不是屬於同一次稽核——理論上可以把 A 公司稽核出來的問題,接到 B 公司的風險紀錄上。

不過這四個問題能不能真的被濫用,關鍵不在這支套件本身,而在呼叫它的另一支程式(jedi-compliance-audit,另一棒 FR-116 C3 正在掃)。這支套件本身「不知道使用者是誰」是設計上的常態(它沒有網址入口),所以它不做身分或歸屬檢查是正常的;真正該檢查的是「呼叫它的程式,在把編號交給它之前,有沒有先確認這個編號屬於正確的文件」。這件事本棒查不到,要等 FR-116 C3 的報告出來對照。


2. 這一棒在檢查什麼

稽核結果(AR)記錄「這條控制項有沒有做到」,做不到的地方會產生「判定」(finding)與對應的「風險」(risk);改善計畫(POA&M)則是針對這些風險,排定要怎麼修正、什麼時候修好。三種文件彼此有連結:判定接風險、風險接矯正措施、矯正措施接改善計畫條目。

這一棒要回答的問題是:接的時候有沒有驗證「兩邊屬於同一份文件」——例如把 B 客戶稽核結果裡的判定,接到 A 客戶的風險上。

範圍涵蓋 26 支檔案:3 支稽核計畫(AP)相關服務、3 支稽核結果(AR)相關服務、2 支改善計畫(POA&M)相關服務,以及底下 18 支資料存取層(repo)。這批文件共用的 19 張資料表,全部沒有客戶欄位、沒有資料庫隔離(盤點檔第 5.1 節已確認),所以「程式層有沒有檢查歸屬」是這批文件唯一的防線。


3. 掃到什麼:總覽

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 該補檢查的位置 嚴重度 來源
V2-1 link_findings 接判定到風險時不驗歸屬 呼叫端如果沒擋,可以把別份稽核結果的判定接到這份風險上,造成判定與風險錯配 呼叫端(compliance-audit)沒有先驗證判定屬於本輪稽核結果 assessment_risk_service.py:109-125(link_findings)、ar_finding_risk_repo_impl.py:42-55(link) 待定(要等 FR-116 C3 確認呼叫端有沒有擋) runner 開檔核對,未經三人面板投票
V2-2 get_risk_findings 讀判定時不驗歸屬 如果 V2-1 真的被塞入別份結果的判定,這支會把它原樣讀出來、顯示給使用者看 同 V2-1(要先有錯配的資料存在) assessment_risk_service.py:132-143(get_risk_findings) 待定,併入 V2-1 一起看 runner 開檔核對,未經三人面板投票
V2-3 update_poam_item 只收編號、欄位照單全收 若使用者能操作到這支方法,可以改到 poam_id 這種歸屬欄位,把改善計畫條目搬到別份計畫底下 目前查無任何呼叫者(主專案與 compliance-audit 都沒人叫) poam_service.py:126-144(update_poam_item) 低(零呼叫者,目前打不到,但方法本身存在且對外公開) runner 開檔核對,未經三人面板投票
V2-4 get_by_assignee 查里程碑時沒有任何文件範圍 查詢只給使用者編號,理論上會回「這個人在所有客戶、所有改善計畫裡被指派的里程碑」——跨客戶洩漏 目前查無任何呼叫者 poam_milestone_repo_impl.py:31-39(get_by_assignee) 低(零呼叫者,目前打不到) runner 開檔核對,未經三人面板投票

工具本身的正式清單是空的(見第 4 節),上面四條全部是本棒依卡片指引人工開檔核對出來的,沒有經過三人審查小組投票。


4. 工具報的:一條都沒有

三人審查小組的投票紀錄是空白的——不是候選被否決,是研究員一開始就沒有提出任何候選。這跟「掃了、確認沒問題」不一樣:研究員讀完這 26 支檔案後,沒有把卡片點名的任何一個疑點寫成候選,所以審查小組完全沒有東西可以投票。

推測原因:這幾支方法本身邏輯很單純(沒有字串拼接、沒有直接執行外部輸入這類明顯的資安關鍵字),套件「不驗身分」又是這批程式碼裡到處都是的正常寫法,研究員可能把它們都歸類成「符合套件本身的設計」而沒有進一步追問「呼叫端有沒有把該做的檢查做好」——這正是這一棒卡片本來就寫明「要跟 FR-116 C3 對看」的原因:光看這支套件,看不出呼叫端做了什麼。


5. 卡片點名的疑點逐條回答(runner 自行開檔核對,未經三人面板投票)

現況:已修(M11-29,FR-114 CM-2184,套件 commit ae5ffff5,1.21.0 出貨)

assessment_risk_service.py:109-125。這支方法收到一個風險編號(risk_id)和一串判定編號(finding_ids),逐一把每個判定接到這個風險上。中間沒有任何一步檢查「這個判定編號是不是屬於同一份稽核結果」——只要編號存在,就會被接上去。

底下實際執行「接」這個動作的 ar_finding_risk_repo_impl.py:42-55 的 link() 方法也一樣,查詢條件只有「判定編號」加「風險編號」這一組,沒有再多帶一層「而且要屬於同一份稽核結果」的限制。

唯一的呼叫端是 jedi-compliance-audit 套件的 assessment_result_app_service.py:792,它傳進來的判定編號清單(desired)是從使用者輸入的 uid 轉換出來的。這個轉換過程有沒有限定在本輪稽核結果底下,是 FR-116 C3 那一棒的檢查範圍,本棒沒有讀 compliance-audit 的程式碼,也沒有被授權去追。

5.2 get_risk_findings 讀判定不驗歸屬:成立

現況:已修(M11-29,同上)

assessment_risk_service.py:132-143。用 get_by_id(link.finding_id) 直接依編號撈判定資料,同樣沒有檢查這筆判定跟目前這份風險是不是屬於同一份稽核結果。如果 5.1 的 link_findings 真的被塞進了別份結果的判定編號,這支方法就會原樣把它讀出來。

這支方法目前查無其他呼叫者,是跟 link_findings 綁在一起的一組(寫入沒驗證,讀取也沒驗證),一起記錄。

5.3 update_poam_item 只收編號、欄位照單全收:成立,但零呼叫者

poam_service.py:126-144。這支方法收一個條目編號(item_id)和一包任意欄位(**fields),逐一用 setattr 直接寫進物件(第 141-142 行)。程式碼裡沒有任何白名單限制「這包欄位裡可以有哪些鍵」——如果呼叫端把使用者的原始輸入整包傳進來,使用者理論上可以改到 poam_id 這種決定「這個條目屬於哪份改善計畫」的欄位,把它搬到別份計畫底下。

確認結果:主專案與 compliance-audit 目前都沒有任何地方呼叫這支方法——目前打不到。但方法本身確實存在,而且沒有任何內建的保護,屬於「零呼叫者但對外公開」的狀態,一旦將來有人接上呼叫端而沒做好白名單,這個洞就會被打開。

5.4 upsert_remediation 用 risk_id 限定範圍,但欄位仍照單全收:形狀對,部分成立

remediation_service.py:49-94。跟前面兩支不一樣,這支有先用 risk_id 限定範圍、再比對 uuid 找出要更新的物件,範圍檢查的形狀是對的。但一樣有 **fields 逐欄照寫的問題(第 77-78 行),沒有白名單。

已知的兩個呼叫端是 jedi-compliance-audit 的 poam_app_service.py:317 與 assessment_result_app_service.py:661——這兩處傳進來的欄位是不是白名單(而不是使用者原始輸入整包丟進來),要靠讀 compliance-audit 的程式碼才能確認,本棒沒有讀那邊的程式碼,無法下定論,列為「形狀待確認」。

5.5 assessment_plan_service.py 三支 set_*:形狀對,跟卡片判斷一致

assessment_plan_service.py:113-215。三支方法都是先用 ap_uid(稽核計畫編號)找到計畫,再整批刪掉舊的子物件重建。刪除時用 delete_by_id(existing.id),而 existing 是先用 get_by_ap(ap.id) 查出來的——有範圍限定,形狀正確。ap_uid 本身是不是經過呼叫端驗證過歸屬,要看主專案 assessment_plan_app_service.py(另一棒 V8 的範圍),本棒不重複查。

5.6 get_by_assignee 沒有任何文件範圍:成立,但零呼叫者

poam_milestone_repo_impl.py:31-39。查詢條件只有使用者編號,沒有任何「屬於哪份改善計畫」之類的範圍限制。如果這支方法被呼叫,回傳的會是「這個人在所有客戶、所有改善計畫裡被指派的里程碑」——是一個會跨客戶洩漏資料的形狀。

確認結果:目前查無任何呼叫者,跟 5.3 一樣屬於「零呼叫者但存在」的狀態。本棒因紀律限制(只掃不修、不能大範圍跨 repo 追呼叫鏈),沒有另外用 grep -rn 把主專案、compliance-audit 全部翻過一次確認絕對沒有呼叫者,只查了盤點檔與卡片已經記錄的線索——這件事列在第 8 節待裁決。


6. 可信度

分兩層看:

「這幾條存在嗎」——可信度高。 第 5 節的六條,除了 5.5(本來就判斷形狀對)之外,其餘五條本棒都親自開檔核對過程式碼的實際內容,不是照抄卡片描述。link_findings、get_risk_findings、update_poam_item、get_by_assignee 這四支方法確實如卡片所述缺少歸屬檢查或白名單;upsert_remediation 的範圍檢查確實存在。這是本棒自己讀程式碼讀出來的結論,不依賴工具。

「只有這幾條嗎」——不保證,可信度低。 原因有三個:

  1. 工具這次是 effort low 的 scoped 掃描,只派了 1 位研究員,沒有跑「威脅建模」與「多輪廣度掃描」這些更全面的步驟;而且這次跑完,工具沒有留下研究員讀取範圍的紀錄(不知道它是不是真的把 26 支檔案全部讀過一遍)。
  2. 本棒只針對卡片點名的六個疑點做核對,沒有對整批 26 支檔案逐支通讀——例如 assessment_result_service.py、ar_finding_matrix_service.py 這幾支檔案本棒沒有另外開檔詳細看過,不排除還有卡片沒點到的問題。
  3. 上面四個「成立」的疑點,最終風險有多大取決於呼叫端(compliance-audit)有沒有做歸屬檢查——這部分本棒完全沒有查證能力,FR-116 C3 報告尚未交付(本棒完成時 docs/features/FR-116-2609-compliance-audit-security-scan/scan-C3.md 還不存在)。

所以「三人審查小組零投票、verified」只能解讀成「工具這次沒找到值得報告的問題」,不能解讀成「這批程式碼是乾淨的」。


7. 執行概況

項目 數值
範圍 套件側 26 檔/1,880 行,effort low,scoped 掃描
基準 commit eafc7ae511d5(dirty)
研究員 派 1、回 1
原始候選(研究員提出) 0
審查小組投票 0(無候選可投)
審查輪次 1 輪,正常完成
耗時 約 13 分鐘(1 個 agent,零失敗)
驗證章 verified,0 findings(CLAUDE-SECURITY-REVISION-eafc7ae511d5-dirty.json)
工具 run ID wf_248b85fd-18a
工具原始報告 套件 repo jedi-oscal-v2/CLAUDE-SECURITY-20260924-054501/(未入版控)

8. 待首腦裁決

  1. 等 FR-116 C3 交報告後對照:V2-1/V2-2(link_findings/get_risk_findings)的實際風險完全取決於 compliance-audit 那邊有沒有先驗證判定歸屬。C3 報告一交,建議由首腦或下一棒直接對照兩份報告的結論,決定要不要開修正卡。
  2. V2-3/V2-4(update_poam_item/get_by_assignee)零呼叫者要不要預防性補強:目前打不到,但方法本身沒有任何保護。是否要在打到之前先補(加白名單、加範圍檢查),還是等真的被呼叫時再處理,請裁決。
  3. V2-4 沒有做全庫呼叫鏈確認:本棒只依卡片與盤點檔已有的線索判斷「零呼叫者」,沒有另外用 grep -rn 對主專案與 compliance-audit 做一次徹底的全庫搜尋確認。若需要更確定的答案,需另外派工執行卡片「開工前兩件事」第①項的呼叫鏈追蹤。
  4. upsert_remediation 的白名單確認:5.4 提到的兩個呼叫端傳入欄位是否為白名單,本棒沒有讀 compliance-audit 程式碼,無法下定論,需要另外查證或等 C3 報告涵蓋。