FR-032 系統資產盤點(資訊系統作為資產納入 SSP inventory)— 設計文件

項目 內容
FR 編號 FR-032
提出日期 2026-06-04
arc 關聯 relates to FR-018(MF 範本預設)/ 概念上對齊 jedi_information_system 受評標的
狀態 Design(brainstorm 完成,待 spec review + 寫 implementation plan)
本期範圍 A 範本層系統資產表 + picker、B 啟動專案 clone、C 專案內 SSP 編輯、D OSCAL 匯出
出 scope(後續分階段) E 匯入回灌、F device 也加 picker、G jedi_information_system 拆套件

1. 問題與動機

1.1 白話需求

合規資源庫範本(Module Frame)與專案 SSP 目前的「資產清冊」只盛裝設備(device 性質的資產:IPv4 / MAC / 主機名稱 / 作業系統…)。實務上做資產盤點時,資訊系統本身也是一種資產,需要跟設備並列在盤點清冊裡。

使用者希望在 /module-frame/<uid>/template-edit 的「系統元件、外部授權與資產」tab,於既有「資產清冊」下方再多一張「系統資產」表,可以:

  1. 從現有資訊系統清單挑選,自動帶入欄位
  2. 系統清單沒有對應資料時,允許直接在表單自由輸入
  3. 同時保存鉤稽 id,方便溯源

1.2 受評標的維持獨立(不動)

information_system 在系統中目前對應 OSCAL SSP 的 system-characteristics(受評標的):一份 SSP 只有一個受評主體。本期不改變受評標的的角色與資料。系統資產是「同一筆 information_system 在別的範本 / SSP 中以資產身分被引用」的新視角,與受評標的並存。

1.3 三者 OSCAL 對照(釐清「不是同一個東西」)

系統概念 對應 OSCAL OSCAL 層 角色
合規資源庫(Module Frame) Profile / Baseline Control Layer 評分標準(控制項基線)
受評標的 SSP → system-characteristics Implementation Layer 被評的主體系統(一份 SSP 一個)
資訊系統(information_system) system-characteristics(受評標的視角)/ inventory-item(資產視角,本期新增) Implementation Layer 雙重身分

合規資源庫(Profile)與受評標的(SSP)是不同層;受評標的與資訊系統現況是同一概念的兩個名字。本期讓資訊系統多一個資產身分


2. 設計原則

2.1 雙重身分(dual-identity)

同一筆 compliance.information_systems

  • 它自己的受評案裡 → 受評標的(system-characteristics,主體)
  • 別的範本 / SSP 裡被引用 → inventory-item(資產),純引用

2.2 Hybrid soft-ref(複製欄位 + 選填鉤稽 id)

採用專案既有的 soft-ref + 快照 + 自由輸入模式(對齊 OSCAL party / 參與人員 matching:matched_user_id soft ref + enrich 顯示快照 + 配不到維持自由輸入):

  • 一筆系統資產列存複製下來的欄位(一定有值,文件自包含)+選填的 refref_type + ref_id
  • ref 只負責「自動帶入」與「溯源」,不負責即時同步
  • 同步方向單向:master → 表單(挑選時 / 未來重新帶入時);永不回寫 master

選此模式而非純 reference / 純 copy 的理由:

做法 系統無資料時 匯出路徑 文件自包含 稽核快照
純 Copy(現況 device) 可自由輸入 不改 凍結
純 Reference 卡住(沒得選不能填) 要新寫 join 會被事後改
**Hybrid(採用) 可自由輸入 不改**(props 本來就有值) 凍結 + 可溯源

2.3 與設備並存、共用管線

設備維持 copy 行為不動(jedi-device 未接入,本就無 master 可鉤稽);系統因有 master(information_systems)才走 hybrid。兩者同桌(共用 inventory 表),靠 asset_type 鑑別,UI 渲染成兩張表。

2.4 雙重身分安全保證

在 inventory 表(範本 default 或 SSP copy)的任何編輯只動本地列,永不回寫 compliance.information_systems master。改 master 只能走資訊系統管理頁。


3. 資料模型

3.1 共用 inventory 表加欄位(純 additive,既有設備列零影響)

compliance.module_frame_inventory_item_defaults(範本層)oscal.ssp_inventory_items(SSP 層) 各加:

新欄位 型別 說明
asset_type VARCHAR 鑑別欄:hardware(既有設備,backfill 預設)/ information_system(系統資產)
ref_type VARCHAR NULL soft-ref 類型:information_system(本期);未來 device。自由輸入為 null
ref_id VARCHAR NULL 鉤稽 master uid(information_systems.uid)。自由輸入為 null

Migration

  • 既有列 backfill asset_type='hardware'ref_type/ref_id=NULL → 設備行為完全不變
  • 只加欄位、不新建表,既有 GRANT 沿用(無需新授權)
  • 兩張表的 migration 各加日期註解,檔頭加 -- Date: YYYY-MM-DD(per SQL migration 規範)
  • migration 用 cmmgr 帳號跑(cm_app 受 RLS)

3.2 系統資產列的資料落點

落點 內容
description 系統描述(OSCAL inventory-item description 必填;空則 fallback 系統名稱)
props(JSONB) { name, abbreviation, owner_id, owner_name, deployment_model, system_status }
asset_type information_system
ref_type / ref_id 從清單挑:information_system / 系統 uid;自由輸入:null / null

不放 CIA(security_objective_*)與 authorization_boundary — 那是受評標的層級,避免兩邊重疊混淆。

審計欄位:owner_idowner_name(nickname)。注意:本設計的 owner_name 是 snapshot 當下凍結進 props 的值(符合 §2.2「凍結」原則),讀取時不再 re-enrich。這與一般「審計欄位 read 時批次轉 nickname」慣例不同 — 系統資產是時間點快照,刻意保留挑選當下的 owner nickname。snapshot 寫入時(BE 抓 master 那一刻)查 User.login_name → nickname 一次即可。implementation plan 要明確標示此點,避免被誤改成 read 時 re-enrich。


4. 資料流

A. 範本層(template-edit 頁)
   「系統資產」表 →「新增系統資產」
     ├─ 從清單挑:picker 撈 /information-systems/menu → 選定
     │    → BE 用 ref_id 回 master 撈 → snapshot 欄位進 props + 存 ref_type/ref_id
     │      (snapshot 由 BE 抓 master,不信 FE 傳值)
     └─ 自由輸入:直接填 → ref 為 null
   存入 compliance.module_frame_inventory_item_defaults(asset_type='information_system')
        │
        │ B. 啟動專案 clone(沿用既有 INSERT...SELECT,補上 3 個新欄位)
        ▼
   oscal.ssp_inventory_items:props 快照逐字複製、ref 帶過來 → SSP 凍結評估當下狀態
        │
        │ C. 專案內 SSP 編輯(同表、同元件、同 picker;編輯只動該份 SSP copy,與 master 分岔可接受)
        ▼
   D. 匯出:mapper 對 asset_type='information_system' 列吐 inventory-item + asset-type prop

clone 的最小改動:現有 clone step 已逐字複製 props,只需把 asset_type / ref_type / ref_id 加進 INSERT...SELECT 欄位清單;系統資產列同桌故自動 clone,不另寫管線。


5. API 設計

5.1 範本層(沿用既有 inventory 端點,加 asset_type 分支)

沿用 /module-frame/<uid>/inventory 的 POST/PUT/DELETE,payload 多帶 asset_type + 選填 ref_type/ref_id

  • service 分支:
    • ref_id 有值 → 注入 information_system_domain_service 回 master 撈、組 props 快照
    • ref_id → 收 FE 自由輸入的 props
  • 不另開新資源,維持 API 面最小
  • marshmallow schema 必須加 asset_type / ref_type / ref_id 欄位:既有 inventory route 若用嚴格 use_kwargs,未宣告的新 payload key 會在進 service 前被 silently drop(plan pre-flight 要驗 schema 是否嚴格、補欄位)

5.2 SSP 層(C)

沿用 FR-011.3「專案內直接編輯 SSP」既有的 SSP inventory 編輯端點,同樣加 asset_type 分支。 實作期 pre-flight 必驗:FR-011.3 既有端點的實際路徑與 service 方法(plan vs reality)。

5.3 picker

直接重用既有 GET /information-systems/menu,不新增端點。

5.4 權限與 error code

  • 寫入走 service 層 manager/auditor 角色檢查(per 開發規範:寫入 API 必須角色權限檢查)
  • 新 error code(per GRC_<HTTP><序號> 命名):information_system ref_id 無效時拋 NotFound
    • 實作期到 common/code/grc_error_code.py 查最大序號 +1,避免序號被佔(per plan vs reality)

5.5 DDD 規範

  • Route 不查 DB、不 import ORM model;權限檢查在 service 層透過 domain service
  • app service public method 加 @transaction;snapshot 抓 master 走 information_system_domain_service
  • 不直接 import infra ORM model

6. 匯出(D1-A:repoint docx/pdf loader 到 live inventory 表)

6.1 Pre-flight 驗證後的 live 現實(取代原假設)

驗證確認「資產清冊」live path = module_frame_inventory_item_defaults(範本,經 /module-frame/:uid/inventory)→ clone → oscal.ssp_inventory_items(SSP)。但匯出 loader 沒跟著 §11.29 遷移 repoint

  • MfSspContentLoader(範本匯出)讀 resources(= 廢棄的 ssp_components devices/info_systems),不是 module_frame_inventory_item_defaults → 連 rich 設備列現在都匯不出(既有遺漏)
  • SspVersionContentLoader(專案 SSP 匯出)讀永遠空的 ssp.system_implementations → inventory/components/leveraged 全空
  • OSCAL-native(json/yaml/xml,ssp_yaml_mapper)同讀空的 system_implementations

6.2 D1-A 決策

docx/pdf/odt 匯出的 inventory builder repoint 到 live 表,同時吐 hardware + information_system

  • MfSspContentLoader._build_inventory_items → 改讀 module_frame_inventory_item_defaults(按 asset_type 分流)
  • SspVersionContentLoader._build_inventory_items → 改讀 ssp_inventory_items(按 asset_type 分流)
  • 系統資產列在 docx 內以 inventory 列呈現(SspInventoryItemExportItem.device_type='information-system' 區分;或視 generator 渲染另立 sub-section)
  • 副作用(已知並接受):設備匯出輸出改為讀正確的新表(一併修好既有遺漏)

6.3 本期不碰(follow-up)

  • OSCAL-native json/yaml/xml:讀空 system_implementations跟本功能無關的更深層既有缺陷(影響整個 system-implementation 區塊)→ 列 follow-up,不在 FR-032 修
  • 因此 §原「asset-type prop on OSCAL inventory-item」shape(含 system-name/deployment-model 等 props)屬 OSCAL-native path,遞延到該 path 修復時再落地;本期 docx 走 rendered 列
  • ref_id 本期不寫進任何匯出(round-trip 是 E 期)

7. 前端設計

  • 新「系統資產」表元件,置於「系統元件、外部授權與資產」tab 的資產清冊下方,比照既有 inventory 表樣式(PrimeVue / BaseService / src/config/api/api.js 常數,per FE CLAUDE.md)
  • 「新增系統資產」dialog:
    • 上方 picker(下拉選系統,選了自動帶入欄位)
    • 下方可編輯欄位(支援自由輸入 / 微調帶入值)
  • 同一元件於 C 的專案內 SSP 編輯畫面重用
  • 跨 repo 作業:動工前先讀 FE CLAUDE.md(per 跨 repo 規範)

8. 預設決策(YAGNI)

決策 取捨
不做「重新帶入 / re-sync」按鈕 要更新就重選一次系統即可,省一個維護面
snapshot 由 BE 從 master 抓 不信 FE 傳的鉤稽快照值,確保資料正確
device 不在本期加 picker jedi-device 未接入,留 F 期

9. 測試策略

  • BE app service 測試:
    • 範本層 add/update 系統資產(有 ref / 無 ref 兩路徑)
    • ref_id 無效 → 拋 NotFound
    • snapshot 由 master 取值(不採 FE 傳值)
    • clone 帶過 asset_type / ref_type / ref_id
    • docx 匯出(repoint 後)含系統資產列 + 設備列(D1-A)
    • 加 logger patch autouse fixture(per DBLogHandler 雷,抄 test_ssp_excel_import_app_service.py:46)
  • E2E(後續於 compliance-manager-test repo):template-edit 新增系統資產 → 啟動專案 → SSP 內可見可編輯 → docx 匯出含系統列

10. Pre-flight 驗證結果(2026-06-04,開工前已驗)

# 待驗項 結果
1 匯出路徑聚合關係 已釐清:docx/pdf 走 MfSspContentLoader(讀廢棄 ssp_components)/ SspVersionContentLoader(讀空 system_implementations);OSCAL-native 走 ssp_yaml_mapper(讀空 system_implementations)。決策 D1-A:repoint docx/pdf loader 到 live 表;OSCAL-native 列 follow-up(§6)
2 SSP inventory 編輯端點 已確認api/oscal/routes/ssp/ssp_inventory_items_route.pyGET/POST/PUT/DELETE /ssp/<ssp_uid>/inventory-items)→ SspInventoryItemsAppService;payload 走 raw request.get_json(),無嚴格 schema
3 clone INSERT...SELECT 已確認module_frame_template_copy_service.py:270-294 Step 6(10 INSERT 欄、需補 asset_type/ref_type/ref_id)
4 error code 序號 已確認:最大 GRC_404034 → 用 GRC_404035
5 inventory route / schema 嚴格度 已確認/module-frame/:uid/inventorymodule_frame_inventory_service)走 raw request.get_json(),新 key 不會被 drop;ORM ModuleFrameInventoryItemDefault / entity 目前無 asset_type/ref 欄位,需補

已廢棄但可借用的成熟邏輯SspResourcesContextService(§11.29 前的 info_systems 機制)已實作 information_system soft-ref + matched_* enrich + information_system_domain_service 注入 —— 搬這套邏輯到新 inventory 機制,不重新發明。


11. Implementation Reality / Reconciliation

A/B/C/D/F 收尾(2026-06-04)逐條列出與原設計的偏差與原因。E-a 換 session 接手(見 handoff)。

# 偏差 / 落地實況 原因
11.1 error code 沿用既有 GRC_INFORMATION_SYSTEM_NOT_FOUND(404020),未用原規劃的 GRC_404035 pre-flight 只比序號漏看名稱,發現已存在 → 沿用;404035 改給新 GRC_DEVICE_NOT_FOUND(F)
11.2 jedi-oscal 不需 poetry update,BE 重啟即生效 venv 內 jedi_oscal 是 symlink editable(指向 source);原 handoff 假設要 poetry update
11.3 系統資產的 hybrid soft-ref 邏輯抽成 common/util/system_asset_snapshot.py 共用 範本層 + SSP 層兩入口共用(DRY),非各自 inline
11.4 匯出(D)實況比 spec 複雜:原設計假設「加 asset-type prop 到 inventory-item」,實際匯出層是壞的 驗證發現 docx loader 讀廢棄 ssp_components / 空的 system_implementations;採 D1-A:repoint docx loader 到 live 表,OSCAL-native 列 follow-up
11.5 docx 系統資產獨立成 System Assets 表(非混入 inventory-item 帶 asset-type prop) user 回報混表 + IP/OS 欄位不適用系統 → 拆獨立表(Name/Abbreviation/Owner/Deployment/Status);新增 SspSystemAssetExportItem + partition_inventory_rows
11.6 ssp_version docx 匯出原本就壞(ssp.system_implementations AttributeError),順手修 getattr 防呆 該路徑以前沒人跑過,repoint inventory 時引爆;components/leveraged 仍空(follow-up)
11.7 F(device picker)採 merge(device 填 hostname/ipv4/os,其餘 hardware 欄位保留 FE 輸入),非系統資產的 full-replace DeviceEntity 只有 hostname/ip/os 三欄,full-replace 會丟失 mac/asset_id 等
11.8 F 為了「挑選當下表單即帶 hostname/os」擴充 jedi-device device menu(加 hostname/os) menu 原本只有 uid/name/ip;jedi-device 是 pin → dev 改 path dependency + user poetry update
11.9 F 順手把「資產清冊」文案改「設備資產」、「描述」改「名稱」(col_device_name 獨立 key) user 要求;不共用 col_inventory_description 以免動到系統資產表的描述欄
11.10 廢棄機制(SspResourcesContextService,§11.29 前的 info_systems/devices)保留未動 走新 inventory 機制,廢棄機制不在本期 scope(避免 scope creep)

E-a 收尾(2026-06-04,Excel round-trip)

# 偏差 / 落地實況 原因
11.11 系統資產走獨立「系統資產」sheet(非塞進「資產」sheet 加 asset_type 欄) 系統欄位(name/abbreviation/owner/deployment/status)跟設備(IP/MAC/FQDN)不同;對稱 docx 獨立 System Assets 表
11.12 系統資產匯入依 name 比對 master(非 uid);deployment/status enum 直接用 OSCAL key Excel 給名稱不給 uid;exact name match → 配到設 ref + master snapshot / 配不到自由輸入。enum 用 OSCAL key 免反向 mapping(master 也存同 key)
11.13 範本 template-edit 下載填值原本「資產」+「系統資產」都空白 MF-scoped _populate_filled_data 寫死 inventory_items=[](舊缺口)且未注入 MF inventory default service → 注入 + 新增 _fetch_mf_inventory_items/_fetch_mf_system_assets 依 asset_type 分流(commit e51bd809)
11.14 「資產」改名「設備資產」+ 第一欄 header 描述→名稱 user 要求對齊 docx / FE live 表;header i18n 改值,column key 仍 description(OSCAL 必填);舊名「資產」加 legacy alias
11.15 設備資產 / 系統資產比照「參與人員」加 picker(matched_device / matched_info_system 下拉 + autofill) 復用既有 v2 device/info_system lookup + autofill 基建(DEVICES/INFO_SYSTEMS helper);picker 放欄位尾端保 backward-compat(舊樣板無此欄 parser 讀 None 不錯位)
11.16 設備鉤稽走 merge(hostname/ipv4/os 覆蓋、其餘 hardware 欄位保留),系統資產走 full-replace(snapshot) DeviceEntity 只有 hostname/ip/os 三欄,full-replace 會丟 mac/asset_id(對齊 §11.7 F 期 merge 語意)
11.17 系統資產匯入端不讀 matched_info_system(仍依 name 比對);設備匯入端讀 matched_device autofill 已把 name 填成所挑系統 → name match 等效;設備 picker 才是 device 鉤稽的 key(device 名稱識別度較低)
11.18 下載填值 round-trip 回填 picker(已鉤稽列反查 master 填 matched_device 'name (ip)' / matched_info_system 'abbr - name') 下載 → picker 顯示已選 → re-upload 可重新鉤稽,round-trip 完整
11.19 Excel VLOOKUP autofill 依賴 client 計算模式(MS Excel 手動模式不即時帶入) openpyxl 寫的公式無 cached value,靠 fullCalcOnLoad + Excel 自動重算;公式經 LibreOffice recalc 驗證正確(非 bug)。不影響匯入結果(BE 從 master 填欄位)。follow-up:可於「說明」分頁加重算提示

§11 index(對應段同步)

  • A/B/C/D/F:完工
  • E-a:✅ 完工(2026-06-04,見 FIXED SUMMARY + §11.11~11.19)
  • G(jedi_information_system 拆套件):✅ 完工(2026-06-04,解耦 jedi-auth + 抽 jedi-information-system==0.0.1 推 Nexus;見 analysis 可行性評估 + changelog 2026-06-04-tweak-fr032-g-jedi-information-system-extract.md
  • E-b / OSCAL-native 匯出 / docx components-leveraged repoint:follow-up