範圍:套件側
jedi-oscal-v226 檔/1,880 行(AP 稽核計畫、AR 稽核結果、POA&M 改善計畫三種文件的服務層與資料存取層)。 掃描工具:Claude Code 官方claude-securityplugin,effort low,scoped 掃描(指定檔案清單,不是掃整個目錄)。 基準 commit:eafc7ae511d5(monorepo 主 checkout,工作區有平行 session 的未提交改動,標 dirty)。 驗證章:verified,0 findings(工具自己的正式清單是空的)。只掃不修。
工具這次沒有找到任何問題,但這不代表這批程式碼是乾淨的。 卡片原本就點名了六個可疑的地方,我把這六個地方的程式碼逐一開檔核對過,其中有四個確認存在——程式碼寫法就是卡片講的那樣,只是工具的自動掃描沒有抓到。這四個問題的共同點是:「接東西進來的時候,沒有檢查這東西是不是真的屬於同一份文件」。舉個例子:稽核結果裡有很多「判定」(這條規定符不符合),也有「風險」(這條不符合會有什麼後果),把判定接到風險上的那支程式,完全沒有檢查這個判定跟這個風險是不是屬於同一次稽核——理論上可以把 A 公司稽核出來的問題,接到 B 公司的風險紀錄上。
不過這四個問題能不能真的被濫用,關鍵不在這支套件本身,而在呼叫它的另一支程式(jedi-compliance-audit,另一棒 FR-116 C3 正在掃)。這支套件本身「不知道使用者是誰」是設計上的常態(它沒有網址入口),所以它不做身分或歸屬檢查是正常的;真正該檢查的是「呼叫它的程式,在把編號交給它之前,有沒有先確認這個編號屬於正確的文件」。這件事本棒查不到,要等 FR-116 C3 的報告出來對照。
稽核結果(AR)記錄「這條控制項有沒有做到」,做不到的地方會產生「判定」(finding)與對應的「風險」(risk);改善計畫(POA&M)則是針對這些風險,排定要怎麼修正、什麼時候修好。三種文件彼此有連結:判定接風險、風險接矯正措施、矯正措施接改善計畫條目。
這一棒要回答的問題是:接的時候有沒有驗證「兩邊屬於同一份文件」——例如把 B 客戶稽核結果裡的判定,接到 A 客戶的風險上。
範圍涵蓋 26 支檔案:3 支稽核計畫(AP)相關服務、3 支稽核結果(AR)相關服務、2 支改善計畫(POA&M)相關服務,以及底下 18 支資料存取層(repo)。這批文件共用的 19 張資料表,全部沒有客戶欄位、沒有資料庫隔離(盤點檔第 5.1 節已確認),所以「程式層有沒有檢查歸屬」是這批文件唯一的防線。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度 | 來源 |
|---|---|---|---|---|---|---|
| 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 節),上面四條全部是本棒依卡片指引人工開檔核對出來的,沒有經過三人審查小組投票。
三人審查小組的投票紀錄是空白的——不是候選被否決,是研究員一開始就沒有提出任何候選。這跟「掃了、確認沒問題」不一樣:研究員讀完這 26 支檔案後,沒有把卡片點名的任何一個疑點寫成候選,所以審查小組完全沒有東西可以投票。
推測原因:這幾支方法本身邏輯很單純(沒有字串拼接、沒有直接執行外部輸入這類明顯的資安關鍵字),套件「不驗身分」又是這批程式碼裡到處都是的正常寫法,研究員可能把它們都歸類成「符合套件本身的設計」而沒有進一步追問「呼叫端有沒有把該做的檢查做好」——這正是這一棒卡片本來就寫明「要跟 FR-116 C3 對看」的原因:光看這支套件,看不出呼叫端做了什麼。
link_findings 不驗判定屬於哪份稽核結果:成立現況:已修(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 的程式碼,也沒有被授權去追。
get_risk_findings 讀判定不驗歸屬:成立現況:已修(M11-29,同上)
assessment_risk_service.py:132-143。用 get_by_id(link.finding_id) 直接依編號撈判定資料,同樣沒有檢查這筆判定跟目前這份風險是不是屬於同一份稽核結果。如果 5.1 的 link_findings 真的被塞進了別份結果的判定編號,這支方法就會原樣把它讀出來。
這支方法目前查無其他呼叫者,是跟 link_findings 綁在一起的一組(寫入沒驗證,讀取也沒驗證),一起記錄。
update_poam_item 只收編號、欄位照單全收:成立,但零呼叫者poam_service.py:126-144。這支方法收一個條目編號(item_id)和一包任意欄位(**fields),逐一用 setattr 直接寫進物件(第 141-142 行)。程式碼裡沒有任何白名單限制「這包欄位裡可以有哪些鍵」——如果呼叫端把使用者的原始輸入整包傳進來,使用者理論上可以改到 poam_id 這種決定「這個條目屬於哪份改善計畫」的欄位,把它搬到別份計畫底下。
確認結果:主專案與 compliance-audit 目前都沒有任何地方呼叫這支方法——目前打不到。但方法本身確實存在,而且沒有任何內建的保護,屬於「零呼叫者但對外公開」的狀態,一旦將來有人接上呼叫端而沒做好白名單,這個洞就會被打開。
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 的程式碼才能確認,本棒沒有讀那邊的程式碼,無法下定論,列為「形狀待確認」。
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 的範圍),本棒不重複查。
get_by_assignee 沒有任何文件範圍:成立,但零呼叫者poam_milestone_repo_impl.py:31-39。查詢條件只有使用者編號,沒有任何「屬於哪份改善計畫」之類的範圍限制。如果這支方法被呼叫,回傳的會是「這個人在所有客戶、所有改善計畫裡被指派的里程碑」——是一個會跨客戶洩漏資料的形狀。
確認結果:目前查無任何呼叫者,跟 5.3 一樣屬於「零呼叫者但存在」的狀態。本棒因紀律限制(只掃不修、不能大範圍跨 repo 追呼叫鏈),沒有另外用 grep -rn 把主專案、compliance-audit 全部翻過一次確認絕對沒有呼叫者,只查了盤點檔與卡片已經記錄的線索——這件事列在第 8 節待裁決。
分兩層看:
「這幾條存在嗎」——可信度高。 第 5 節的六條,除了 5.5(本來就判斷形狀對)之外,其餘五條本棒都親自開檔核對過程式碼的實際內容,不是照抄卡片描述。link_findings、get_risk_findings、update_poam_item、get_by_assignee 這四支方法確實如卡片所述缺少歸屬檢查或白名單;upsert_remediation 的範圍檢查確實存在。這是本棒自己讀程式碼讀出來的結論,不依賴工具。
「只有這幾條嗎」——不保證,可信度低。 原因有三個:
assessment_result_service.py、ar_finding_matrix_service.py 這幾支檔案本棒沒有另外開檔詳細看過,不排除還有卡片沒點到的問題。docs/features/FR-116-2609-compliance-audit-security-scan/scan-C3.md 還不存在)。所以「三人審查小組零投票、verified」只能解讀成「工具這次沒找到值得報告的問題」,不能解讀成「這批程式碼是乾淨的」。
| 項目 | 數值 |
|---|---|
| 範圍 | 套件側 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/(未入版控) |
link_findings/get_risk_findings)的實際風險完全取決於 compliance-audit 那邊有沒有先驗證判定歸屬。C3 報告一交,建議由首腦或下一棒直接對照兩份報告的結論,決定要不要開修正卡。update_poam_item/get_by_assignee)零呼叫者要不要預防性補強:目前打不到,但方法本身沒有任何保護。是否要在打到之前先補(加白名單、加範圍檢查),還是等真的被呼叫時再處理,請裁決。grep -rn 對主專案與 compliance-audit 做一次徹底的全庫搜尋確認。若需要更確定的答案,需另外派工執行卡片「開工前兩件事」第①項的呼叫鏈追蹤。upsert_remediation 的白名單確認:5.4 提到的兩個呼叫端傳入欄位是否為白名單,本棒沒有讀 compliance-audit 程式碼,無法下定論,需要另外查證或等 C3 報告涵蓋。