掃描日期:2026-09-08 掃描版本:jedi-python-package
feature/FR-075@8aa6f0609c44(乾淨,無未 commit 變更) 工具:Claude Codeclaude-securityplugin(claude-security:scanworkflow) 範圍:jedi-bulletin/jedi_bulletin/+harness/,共 29 個受版控.py(工具實際計入 31 檔,多的兩個是harness/docker-compose.yml與非.py檔;其中 13 個是空的__init__.py,實質內容 16 檔、847 行) effort:low,focus:attack-surface模型:主 session Opus 5 (1M context),研究員繼承 狀態:✅ 驗證面板完整跑完,stamp 為verification.status: verified——工具零發現(2 條候選全被面板 0/3 否決) 對應卡片:CM-1611(母卡 CM-1609)
工具在這個套件裡沒找到任何成立的資安問題,這個結果與預期一致——這半邊確實只有沒有守門的 CRUD,安全決策全外包給宿主。 但「沒有資安問題」不等於「沒有問題」:人工逐項核對卡片十個重點時,找到 一個會讓程式當場崩潰的真 bug(update() 檢查錯對象)與 兩個健壯性缺口,都不是權限問題、也都因為「這支 service 現階段沒有任何呼叫者」而目前打不到。最重要的一條是重點 ⑧ 的答案:公告 uid 用的是 uuid.uuid4(),密碼學等級隨機、不可預測——這對 B2 那條無守門的單筆讀取是好消息,把它的嚴重度往下拉。
工具面板存活發現:0 條。 下表是人工核對十個重點時自己找到的,全部未經驗證面板(面板只驗研究員提出的候選,這些不在其中),可信度標示見表後。
| # | 嚴重度 | 這是什麼問題(白話) | 出事會怎樣 | 要先有什麼才打得到 | 位置行號 | 修正卡 |
|---|---|---|---|---|---|---|
| M1 | 🔵 LOW | 更新公告時,程式想確認「這筆公告存在嗎」,卻檢查了傳進來的參數、而不是查詢的結果。查無此筆時下一行直接對 None 設值 |
該次請求 500 崩潰(AttributeError),不是資料外洩也不是權限繞過。純錯誤處理缺陷 |
要有人呼叫套件版 update_bulletin() 並帶一個不存在的 uid——而現在沒有任何呼叫者(主專案走自己那支),所以目前打不到 |
infra/repository/bulletin_repo_impl.py:56(if not bulletin 應為 if not bulletin_model) |
✅ 已修(SUMMARY #145) |
| M2 | 🔵 LOW | 建立/更新公告時直接取登入者的 uid,但取不到登入者時回的是 None,對 None 取 .uid 會崩 |
背景執行緒/排程若呼叫此路徑會 500。好消息是它「炸」而不是「靜默寫進 NULL 建立者」——後者才是稽核惡夢 | 同上,需要有呼叫者,且在無 user context 的情境下呼叫 | app/service/bulletin_service.py:50、:62 |
✅ 已修(SUMMARY #145) |
| M3 | ⚪ INFO | update() 拿到 uid 就改,不看這筆公告屬於誰、也不看是否已軟刪除 |
套件層本身無守門是刻意設計(安全決策外包給宿主)。真正要回答「能不能改別人的公告」的是 B2 那一棒 | — | infra/repository/bulletin_repo_impl.py:51-69 |
屬 B2 範疇 |
要分三層講,混在一起會誤導。
第一層——「工具報的 0 條」可信度高。 兩條候選(一條 dead code、一條 harness 密碼)各拿 0/3 全否,六票全數投出,unreviewed_candidate_sites: 0,stamp 記 verification.status: verified。面板不是敷衍否決:三個 lens 各自去讀了 pyproject.toml 的打包設定、plugin.py 的空 blueprint、主專案那支真正被呼叫的 service,才判定「無可達路徑」。這是本 arc 至今第二次拿到面板完整跑完(前一次是 FR-078 N1)。
第二層——上表 M1/M2/M3 是我人工開檔核對的,未經面板。 它們的「存在性」可信(打開那幾行就是那樣寫的,是事實陳述),但「嚴重度判定」只有我一個人的判斷。三條都因「無呼叫者」而目前無法觸發,所以我沒有為它們開修正卡(建議見末段)。現況:M1、M2 後來已修(SUMMARY #145)。
第三層——「只有這些嗎」不可信。 這是 low effort 單次快篩,沒有 inventory、沒有威脅模型、沒有廣度掃蕩,工具自己把 completenessCheckOutcome 標成 not-applicable。範圍很小(實質 16 檔),但一次 low 掃過不等於逐行讀完。安靜不等於乾淨。
另外:掃描過程沒有執行任何被掃的程式碼——沒跑測試、沒發攻擊、沒驗 PoC。所有結論都是讀源碼推導出來的。
| 項目 | 數字 |
|---|---|
| 研究員派出 / 回報 | 2 / 2(一支掃源碼、一支密鑰專項) |
| 候選發現 | 2 條 → 去重後 2 條 |
| 面板票數 | 6 / 6(2 條 × 3 票,全數投出) |
| 面板存活 | 0 條(兩條皆 0/3 否決) |
| 未投票候選 | 0(unreviewed_candidate_sites: 0) |
| 驗證輪次 | 1 |
| 總耗時 | 約 42 分鐘(2,554 秒) |
| Agent 數 | 8 個(含面板),全部完成、0 錯誤 |
工具的完整性欄位全部乾淨:skippedComponents、droppedComponents、prunedBuckets、lostCandidates、severityLowered 皆為空,dispatchRefusals: 0,沒有候選被交棒到下一輪。
報告產物落點:~/Projects/Jedicogy/module/jedi-python-package/jedi-bulletin/CLAUDE-SECURITY-20260908-125155/(含 .jsonl / .sarif / stamp;該目錄自帶 .gitignore,不入版控)。
零發現的報告若不寫下「差點成為發現的是什麼」,接手者無從判斷掃描到底看了什麼。
_gen_filters() 裡有兩個永遠不生效的過濾條件研究員指出 infra/repository/bulletin_repo_impl.py:23 的動態過濾器,其 hasattr(self.model, field) 對 release_time__gt_now / expire_time__le_now 這兩個欄位永遠是 False——Bulletin model(infra/models/bulletin.py:22-71)根本沒有這兩個欄位,BaseModel 也沒有 __getattr__。所以第 27-30 行那段「對齊主專案修過的正確版」的時間窗過濾一行都不會執行。
事實部分成立(我獨立核對過:model 只有 uid/title/content/category/release_time/expire_time/enable/is_delete,確實沒有那兩個帶 __ 的名字)。面板三個 lens 全判 FALSE_POSITIVE,理由一致且我認同:套件掛出來的 blueprint 沒有任何 resource(plugin.py:224,且被 tests/unittest/test_plugin_contract.py:129 焊死),沒有任何外部入口;主專案那唯一的 consumer 只 import model/DTO/entity,過濾器是它自己重寫的一份。這是正確性缺陷不是資安漏洞,而且落在一段沒人執行的程式碼上。
⚠️ 但這條對 B2 有交叉價值:主專案自己那支
_gen_filters()是另一份實作,B2 那一棒要獨立確認它的hasattr白名單有沒有同樣的空轉問題——套件這半邊空轉無害,主專案那半邊空轉就是「時間窗過濾沒生效」。
harness/docker-compose.yml:12 有一組明文資料庫密碼密鑰專項掃到 POSTGRES_PASSWORD: bulletin_harness_local(harness/dev_app.py:44 的連線字串裡有同一份鏡像)。
字面屬實,但面板三票全否,我認同:pyproject.toml:38-41 的 packages = [{ include = "jedi_bulletin" }] 只打包 jedi_bulletin/,harness/ 不進發行包;而且這組密碼保護的容器就是同一個 compose 檔當場創造出來的——庫裡只有 harness 自己寫的假資料(dev_app.py:111-128 的「harness 公告」、name='harness' 的假 tenant/org_unit),用完 down -v 銷毀。它不是任何既存系統的憑證。 檔案第 12 行原本就有註解「僅本機 harness 用,非任何環境憑證」——這次的獨立核對證實該註解是真的,不是自我辯護。
唯一值得記一筆的小事:ports: - "5491:5432" 是綁在所有介面上的(Docker 預設行為),harness 執行期間同網段可連。考量到裡面只有假資料、且是開發者手動起停的臨時容器,不值得開卡,但寫進 README「常見坑」是合理的。
卡片列了十個「思考起點」,以下逐條給結論。每條都標明是「工具報的」「我人工核對的」還是「本棒未觸及」——這個區分比結論本身更重要。
有結論:沒有可繞過的入口,因為根本沒有入口。(人工核對 plugin.py 全 226 行)
四個插槽逐個看過:
mount_api=True 掛出的 blueprint 是空的(plugin.py:191-193 只建了 Blueprint 並 record_once 寫 app.extensions,沒有 add_url_rule、沒有 resource)。mount_api=False 與 True 的對外行為完全相同,差別只在 app.extensions 有沒有那個 key——檔頭這句自陳屬實。adapters/config 都是純資料 dataclass,沒有可執行的預設值,也沒有任何地方把它們當 callable 呼叫。schema_extensions 是唯一「會被呼叫」的插槽(build_context / enrich 呼叫 consumer 傳進來的 callable)。這是宿主自己傳進來的函式,不是外部可影響的輸入——而且現階段沒有任何產品在用。要出事得先有宿主傳進一個惡意 callable,那時攻擊者已經在宿主的程式碼裡了。create_blueprint 的 record_once 用 lambda 捕獲 context,每個 app 各自一份,多 app 掛載不互相污染——這一點檔頭寫的與實作相符。風險反而在未來:BulletinAdapters 的 docstring 自己標了紅字「日後加認證 decorator 欄位時不可給預設值」(plugin.py:121-123)。這條警告是對的,但目前只是註解、沒有任何機制強制。真的長出 route 的那一棒若忘記,就會是一整組無聲的公開端點。建議把它變成測試(照 jedi-iam 的 _assert_api_wiring() 形狀),而不是留在註解裡。
有結論:不違反,而且這個單例沒有人在用。(人工核對,跨 repo 追到 jedi-common)
bulletin_service.py:74 確實在 import 時就建了 BulletinService(BulletinDomainService(BulletinRepoImpl()))。但追下去:
BulletinRepoImpl → BaseRepositoryImpl → BaseRepository(轉發) → SessionMixin
└ @property session: return get_session()
jedi_common/session/database/session_mixin.py:49-51 是標準的 lazy @property,BaseRepositoryImpl.__init__(base_repository_impl.py:24-35)只設 self.model / self.mapper / 幾個翻譯屬性,沒有任何 get_session() 呼叫。所以 import 時實例化不會炸——CLAUDE.md 那條鐵則描述的正是這個陷阱,而 jedi-common 的底座已經避開了(該檔 docstring 第 44-46 行明寫這個理由)。
跨請求共用狀態的疑慮也不成立:BulletinRepoImpl 的實例狀態只有 model/mapper 這些無狀態的類別引用,session 每次存取都重新向 context 取。
但這個單例是死的:全 BE repo grep jedi_bulletin,只有三處 import——infra/bulletin/models/bulletin.py(拿 model)、app/bulletin/dto/bulletin.py(拿 DTO)、domain/bulletin/entities/bulletin_entity.py(拿 entity),沒有任何一處 import 套件的 service 或那個單例。它存在但無人使用。
有結論:會拋 TypeError,是 fail-closed 的好行為。(人工核對)
BulletinQueryEntity.__init__(domain/entities/bulletin_query_entity.py:2)只收五個具名參數 (auth, is_delete, release_time__gt_now, expire_time__le_now, enable),沒有 </strong>kwargs。所以 bulletin_service.py:20 與 :36 的 BulletinQueryEntity(</strong>kwargs) 收到未預期的鍵時,Python 直接 TypeError: unexpected keyword argument——請求失敗,不會靜默把任意鍵變成 SQL 條件。
這是套件端比主專案端安全的地方(卡片的推測正確)。B2 那一棒要確認主專案那支收不收 </strong>kwargs**——收了就是另一回事。
還有一個附帶觀察:domain/service/bulletin_domain_service.py:16-19 的 if _filter.auth: 分支會強制覆寫 enable=1 與兩個時間旗標,而 auth 的預設值是 False。也就是說**「只顯示當前生效的公告」這個限制是呼叫端自願開啟的,不是伺服器強制的**——套件層這樣設計合理(它不知道業務規則),但這正是 B2 重點 ③ 講的那件事在套件端的源頭。
有結論:沒有跳過任何處理,但這是真的重複、且讓注入形同虛設。(人工核對)
get_bulletins(:35)、get_bulletin(:44)、add_bulletin(:51)、update_bulletin(:61)、delete_bulletin(:71)各自 BulletinDomainService(BulletinRepoImpl()) 就地新建,只有 get_bulletins_and_pager(:21)用了 self.bulletin_domain_service。
新建的那份與注入的那份走的是完全相同的類別與路徑,沒有任何一層被跳過,所以沒有資安後果。但後果有兩個:①建構子注入形同虛設——想換一份 repo 實作(測試用 fake、加了審計的 wrapper)對六分之五的方法無效;②BulletinService 的建構子參數看起來是契約,實際上不是。屬於設計債,不是漏洞。
add/update 的手寫欄位搬運——漏了什麼?有結論:update() 有一個會當場崩潰的真 bug,且不覆寫 is_delete、無歸屬檢查。(人工核對,見上表 M1/M3)
Impact. 對套件版 update_bulletin() 傳一個不存在的 uid,該次請求 500 崩潰。不是資料外洩、不是權限繞過,純粹是錯誤處理寫錯。
Where. jedi_bulletin/infra/repository/bulletin_repo_impl.py:56
What. 第 55 行查出 bulletin_model,第 56 行卻寫 if not bulletin:——bulletin 是傳進來的參數(前一行第 52 行才剛檢查過它的 uid),不是查詢結果。查無此筆時 bulletin_model 是 None,而 bulletin 是個有效物件,所以檢查通過、繼續往下,第 59 行 bulletin_model.title = ... 直接 AttributeError: 'NoneType' object has no attribute 'title'。
Preconditions. 需要有人呼叫套件版的 update_bulletin() 並帶不存在的 uid。現階段沒有任何呼叫者——主專案走自己那支 app/bulletin/service/bulletin_service.py。所以目前無法觸發,這是它嚴重度只有 LOW 的唯一理由。
Fix. if not bulletin: → if not bulletin_model:。一個字的修正。
update() 不覆寫 is_delete、也不做任何歸屬檢查update()(:59-65)覆寫 title/content/category/enable/release_time/expire_time/updated_user 七個欄位,不動 is_delete——這其實是對的(軟刪除旗標不該被一般更新順手改掉,否則更新一次就會把已刪的公告復活)。而 filter_by(uid=...) 拿到就改、不看歸屬,是套件層刻意不做安全決策的設計,真正要回答「能不能改別人的公告」的是宿主,屬 B2 範疇。
另外 add()(:36-46)沒有設定 tenant_id / org_unit_id——這兩個欄位靠 jedi-common 的 before_flush 事件(db_mw.py:63-85)從 user context 自動填。沒有 user context 時不會填,而 DB 的 tenant_id 是 nullable=False,結果是 NotNullViolation——同樣是「炸」而不是「寫進錯誤的租戶」,行為上安全。
add_bulletin 用 get_user_context().uid——無 user context 時會怎樣?有結論:會 AttributeError 崩潰,不會寫進 NULL。(人工核對,見上表 M2)
jedi_common/session/auth/auth_context.py:17-22:get_user_context() 在 context 為空時回 None(那行 raise RuntimeError 被註解掉了)。所以 bulletin_service.py:50 的 get_user_context().uid 在背景執行緒/排程情境下是 AttributeError: 'NoneType' object has no attribute 'uid',:62 的 update_bulletin 同樣。
這是好消息不是壞消息:崩潰比「靜默寫入 NULL 建立者」好得多——後者會讓稽核軌跡出現無主資料且無人察覺。建議修法是明確拋業務例外(帶 error code),而不是讓 AttributeError 冒到 500。
語意差異確認屬實:套件版存的是 user_context.uid,主專案版存的是 login_name。而主專案的 ExtendedBulletin(infra/bulletin/models/bulletin.py:47-57)用 created_user == User.login_name 做 relationship join——若哪天真有人改用套件版的 service 寫入,存進去的 uid 會讓那個 join 永遠對不上,created_user_name 全部變 None。現階段無害(沒人用套件版),但這是一顆埋著的地雷,值得在 README 註記。
有結論:RLS 四條 policy 都在、欄位對得上,疊加不影響。但有一個 import 時序的隱性前提。(人工核對 + DEV DB 唯讀查證)
DEV 庫(guidant_ai_dev)實查(唯讀 SELECT,未做任何異動):
bulletins | rowsecurity = t
bulletins_select | r | is_super_admin OR app_tenant_allowed_for_session(tenant_id)
bulletins_update | w | is_super_admin OR app_tenant_allowed_for_session(tenant_id)
bulletins_delete | d | is_super_admin OR app_tenant_allowed_for_session(tenant_id)
bulletins_insert | a | WITH CHECK: app_tenant_allowed_for_session(tenant_id)
AND (org_unit_id IS NULL OR app_org_allowed_for_session(org_unit_id))
四條齊全。INSERT 那條的「不對稱」是設計不是缺陷——INSERT 沒有 USING 子句(沒有既存列可檢查),只有 WITH CHECK,而且它比其餘三條更嚴格(多驗了 org_unit)。FR-079 README 引用的 migration 註解說它「與其餘三支不對稱」,實查結果是這個不對稱方向是安全的。
兩個 model 疊同一張表沒有問題:ExtendedBulletin(主專案)用 extend_existing=True 加的是 relationship 與 hybrid_property,沒有動任何實體欄位;tenant 欄位由 TenantScopedMixinModel 提供,兩邊都掛了同一個 mixin,欄位定義一致。
但有一個隱性前提要記下來:TenantScopedMixinModel 的 tenant_id / org_unit_id 是 @declared_attr,在 class 定義的當下讀 ENABLE_MULTI_TENANT 環境變數決定要不要產生欄位(jedi_common/session/database/model/tenant_mixin_model.py:11-22)。也就是說:任何 import 這個 model 的程式,若在 import 之前沒設好該環境變數,ORM 端就沒有 tenant 欄位,而 DB 端的 NOT NULL 與 RLS 依然在——症狀是寫入時 NotNullViolation,非常不直觀。BE 的 .env 有 ENABLE_MULTI_TENANT=true,harness 也在 import 前 setdefault(dev_app.py:40),現況都對。這是「會靜默失效的設定相依」,值得寫進 README 而不只留在 harness 的 docstring 裡。
generate_uuid 是不是密碼學等級隨機?(卡片指定必答)有結論:是。用的是 uuid.uuid4(),不可預測。(人工核對,卡片要求即使工具沒報也要看)
# jedi_bulletin/common/utils/common_util.py:1-5
import uuid
def generate_uuid():
return str(uuid.uuid4())uuid4 在 CPython 的實作是 os.urandom(16) 取 122 bits 隨機(其餘 6 bits 是版本與變體標記),走作業系統的 CSPRNG。不是 uuid1(含 MAC 位址+時間戳,可預測)、不是時間戳、不是序號。
infra/models/bulletin.py:22-28 確認 uid 欄位的 default=generate_uuid,也就是每一則公告的 uid 都是這樣產的。
這條的跨棒意義:B2 的重點 ② 指出「單筆讀取只靠 uid、不做任何範圍檢查」。若 uid 可預測,那條路徑等於「任何人都能列舉全部公告」,會是 HIGH;現在確認 uid 是 122-bit 的密碼學隨機,列舉不可行,那條路徑退化成「必須先取得某個 uid 才打得到那一則」——B2 該條的嚴重度應據此下調,但不歸零:uid 會出現在列表 API 的回應裡、也會出現在網址上,「合法看得到列表的人事後仍能讀到已被移出可見範圍的那則公告」這個問題依然存在。
common/enum/code.py 有沒有把內部資訊帶進對外訊息?有結論:沒有。但這整個檔案是與公告無關的殘留複製品。(人工核對)
全檔 38 行,六個常數類別:UserStatus、LogTypeCode、RoleStatusCode、EnableCode、DeleteCode、ChangePasswordTypeCode、ChangePasswordStatusCode。全是整數常數,沒有任何字串訊息、沒有表名、沒有 SQL、沒有路徑——不可能洩漏內部資訊。
但值得記一筆:這個套件叫 jedi-bulletin,而這個檔案裡有 ChangePasswordTypeCode(改密碼流程)、UserStatus(使用者狀態)、RoleStatusCode(角色狀態)——全部與公告無關,明顯是從別的套件複製過來的殘留。實際被用到的只有 EnableCode / DeleteCode 的語意(而且程式裡是直接寫 1 / 0 字面值,連這兩個都沒真的 import)。清掉它是衛生問題不是資安問題,但殘留的複製品是「這個套件抽出來時抽得不乾淨」的訊號。
harness/dev_app.py 有沒有寫死憑證/debug 開關/會被誤帶進正式環境的設定?有結論:有寫死憑證,但打不進發行包,面板判定不成立(C2),我認同。(工具報的 + 人工核對)
完整分析見上方 C2。補充三點人工核對的結果:
harness/ 確實不進發行包——pyproject.toml:38-41 只 include jedi_bulletin。裝了這個套件的產品拿不到這個檔案。dev_app.py:40 的 os.environ.setdefault("ENABLE_MULTI_TENANT", "true") 是唯一的環境變數寫入,用 setdefault(不覆蓋既有值),且這是 harness 專用進入點,不會被套件本體 import。app.run(debug=True)、沒有 FLASK_DEBUG。它只建 schema、跑一輪 CRUD assert、印結果,跑完就結束。順帶查證了 plugin.py 檔頭的兩個自陳(卡片特別警告要獨立驗證,不可複述檔頭):
| 檔頭宣稱 | 我的獨立查證 | 結果 |
|---|---|---|
| 跨 jedi 套件 import:0 處 | grep -rn "^from jedi_|^import jedi_" jedi_bulletin/ | grep -v "jedi_common|jedi_bulletin" → 無輸出 |
✅ 屬實 |
os.getenv / os.environ:0 處 |
grep -rn "os\.getenv|os\.environ" jedi_bulletin/ → 只命中 plugin.py:50 那行註解本身 |
✅ 屬實 |
這次的自陳是真的。 但要說清楚方法論:我是自己跑 grep 驗證得到這個結論,不是因為檔頭這樣寫就採信。FR-075 S2 的教訓(docstring 把真洞寫成「刻意設計」,研究員讀了就接受)在這裡不適用——不是因為檔頭可信,而是因為這次獨立查證的結果剛好與它一致。
三條人工發現都因「套件版 service 沒有任何呼叫者」而目前打不到。為打不到的程式碼開修正卡,會佔用修正佇列、也會讓 arc 的修正卡清單失真。
但反悔條件很明確:哪一天有人開始使用套件版的 BulletinService(不論是主專案改接、還是第二個產品接走),M1 就從「打不到的 LOW」變成「一個字造成的 500」,M2 變成背景任務的雷。 建議把這三條寫進套件 README 的「已知問題」段,讓下一個接手的人在動它之前就看到。
bulletin_repo_impl.py:56 的 if not bulletin → if not bulletin_model。一個字,無行為風險,順手修掉比留著等它變成問題好。plugin.py:121-123 那條紅字警告變成測試(「adapters 的認證欄位不可有預設值」),照 jedi-iam 的 _assert_api_wiring() 形狀。註解攔不住人,測試可以。_gen_filters() 要獨立查——套件這份有兩個永遠不生效的過濾條件(C1)。套件端空轉無害,主專案端空轉就是「時間窗過濾沒生效」。BulletinQueryEntity 收不收 </strong>kwargs 要實查**——套件端不收(fail-closed),卡片說主專案端收。這是兩邊安全性的關鍵差異。零發現的報告必須說清楚「讀過但沒報」與「根本沒讀到」的界線。
jedi_bulletin/tests/ 與 repo 根的 tests/(含四支架構測試):focus: attack-surface 明確把測試樹當背景而非稽核目標。架構測試本身沒有被當作安全目標審查過——test_plugin_contract.py 焊死「blueprint 無 resource」這件事,我讀了那一行來確認結論(重點 ①),但沒有審查那些測試是否可被規避。publish.sh:不在 scope(jedi_bulletin/ + harness/ 之外)。發版腳本是密鑰常見藏身處,本棒未讀。README.md、pyproject.toml、poetry.toml、testarch_report/:同上,範圍外。pyproject.toml 我因為要判斷 C2 的打包範圍而讀了打包段落,但沒有當作稽核目標通讀(例如 Nexus source 設定的 http:// 明文連線就沒有進一步分析)。session_mixin.py、auth_context.py、db_mw.py、tenant_mixin_model.py 的相關段落,但那是為了判定本棒的問題,不是對 jedi-common 的稽核。jedi-common 是否該獨立掃描是另一個決策。