FR-157 · 設計文件 v1 · 2026-10-22(D1~D11 全數定案)
從主系統一鍵跳進 Agent 機上的主控台:填檢測工具帳密、瀏覽這台存的證據檔;兩個能力點決定看得到哪些分頁
管理員在主系統 Agent 清單按一下,新分頁直接進到 Agent 機上的主控台(免再登入)。檢測工具的連線帳密在那裡填、加密存 Agent 本機,主系統從此不存;派工時 Agent 用自己機上那份。同一個主控台有「檔案」分頁,列這台 Agent 實際存在 SeaweedFS 或磁碟上的證據檔,可查可下載。權限拆成「Agent 設定」與「Agent 檔案」兩個能力點,稽核人員只勾檔案就能進去看檔、看不到帳密分頁。本文件把 11 項決策展開到「照做就能交」的程度,拆成五個子需求、10 張實作卡。
狀態:設計定案(D1~D11),已開卡待派|日期:2026-10-22|母卡 CM-3026、子卡 CM-3027~CM-3036|前作:FR-156(入場券+8444 門)、FR-056 D9(被推翻)、FR-039/066.6(Agent 儲存) 討論稿(PM 意見、選項推演):
discussion.html|雛形:prototype/index.html(六幕,右上可切「IT 管理員/稽核人員」身分看權限差異) 開發 branch:四個 repo 2026-10-22 查皆在feature/FR-139;runner 開工git branch --show-current再確認,不對就停下問,不切
🔴 這份是設計決策,不是現況。 內文的行號與座標停在 2026-10-22 定稿當時,之後實作改動的結果不回頭改寫這裡——上線後的現況看 SPEC 頁。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-22 | 初版設計定案(D1~D11),拆 5 子需求 10 張實作卡 | 母卡 CM-3026,子卡 CM-3027~CM-3036 |
PM 判定檢測工具的連線帳密是客戶機敏資料,不該存在我們主系統。現況(FR-056 D9)是主系統 config.tenant_detection_tool_configs.credentials_encrypted 存 Fernet 密文,派工時解密塞進派工單下發,Agent 用完即丟。落地版客戶的立場是「能打進我內網主機的密碼,為什麼在你們資料庫裡」。
決策者再加一項:客戶部署時會把 Agent 當儲存設備,證據檔本體落在 Agent 機的 SeaweedFS(主系統只有 storage_type=remote_agent 的參照列;DEV 實查 186 列),但 Agent 那邊沒有任何畫面,客戶想知道「這台存了什麼」只能問我們。
本案兩件事:帳密搬 Agent 本機、Agent 開一個主控台讓人看檔。兩件事共用同一道門:FR-156 E1 做好的五分鐘入場券+Agent 8444 瀏覽器門。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
participant U as 使用者瀏覽器
participant M as 主系統
participant AG as Agent 8444(瀏覽器門)
U->>M: Agent 管理頁按「開啟設定頁」或「瀏覽檔案」
M->>M: 查他有哪些能力點:Agent 設定?Agent 檔案?
M-->>U: 五分鐘入場券(誰、租戶、哪台、scope=[configure, files])
U->>AG: 新分頁開 https://agent:8444/console#票
AG->>AG: 用雲端公鑰驗票:這台嗎、過期嗎、jti 用過嗎
AG-->>U: 發 30 分鐘 HttpOnly session(記 scope),抹掉網址上的票
U->>AG: 總覽/檔案(files)、檢測工具(configure)
AG->>AG: 每個 API 先看 session 的 scope,不在範圍回 403
AG->>AG: 帳密 Fernet 加密存本機;操作寫本機稽核
AG->>M: 心跳:configured_tools、storage、console_events
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
A1[Agent 主控台<br/>檢測工具分頁] -->|填寫| A2[(Agent 本機<br/>tool-credentials.enc)]
A3[主系統派工單<br/>不帶 _credentials] --> A4[Agent task_executor<br/>依工具代號讀本機]
A2 --> A4
A2 -.心跳只報已設定/最後測試.-> A5[主系統<br/>remote_agents.configured_tools]
A5 --> A6[工具設定頁<br/>各 Agent 狀態表]
A5 --> A7[派工前檢查<br/>沒設定立即失敗]
整案一句話:使用者在主系統 Agent 清單按一下,新分頁直接進到 Agent 機上的主控台(免再登入);有「Agent 設定」權限的人在那裡填檢測工具帳密,有「Agent 檔案」權限的人在那裡看這台存了哪些檔;主系統從此不存帳密,派工時 Agent 用自己機上的那份。
| 子需求 | 做什麼 | 產出 | 完成怎麼判定(決策者檢查法,對照雛形第幾幕) |
|---|---|---|---|
| A 跳轉與主控台殼 | 兩個能力點;主系統簽帶範圍的票;Agent 8444 掛主控台頁、驗票換 session、指路頁;主控台三分頁的殼 | 新分頁開出主控台總覽 | 幕 1→2:按「開啟設定頁」新分頁出現主控台、不輸帳密;切「稽核人員」身分只看到「瀏覽檔案」、進去沒有「檢測工具」分頁;幕 6:直接打 https://<agent>:8444/console 是指路頁;三十分鐘不動再點回指路頁 |
| B 檔案瀏覽 | 主控台列 Agent 索引裡的證據檔,可依檔名/日期/上傳者查、可下載;容量統計 | 主控台「檔案」分頁 | 幕 4:在主系統上傳一份證據到走這台 Agent 的專案,到主控台檔案頁搜檔名找得到、大小與 SHA-256 一致、按下載拿到同一份;源碼暫存與弱點資料只顯示 GB 數 |
| C 工具帳密搬 Agent | 表單定義從主系統工具目錄經心跳下發;主控台填寫;Agent 加密存本機;測試連線在 Agent 本機跑;派工不再下發帳密,Agent 讀本機;心跳回報設定狀態 | 主控台「檢測工具」分頁+Agent 加密儲存+派工改道 | 幕 3:填 OpenVAS 帳密按「測試連線」綠勾;DEV DB credentials_encrypted 該租戶該工具為空;主系統派一支 OpenVAS 檢測正常跑完;派到沒設定的 Agent 立即失敗且訊息寫「Agent X 尚未設定 OpenVAS 連線」 |
| D 主系統端配合 | 工具設定頁拿掉連線欄位、改各 Agent 設定狀態表;Agent 清單加「已設定工具」「儲存」欄;升級時既有帳密自動搬家;主控台操作回傳稽核 | 主系統三頁改版+搬家+稽核 | 幕 5:工具設定頁沒有帳密欄、有每台 Agent 的狀態表;幕 1:清單看得到每台設了哪些工具、存了多少檔;升級舊客戶 DB 後原本能跑的 OpenVAS 檢測不重填也能跑、密文欄清空;稽核報表看得到「誰在 Agent 改了帳密、下載了哪個檔」 |
| E 文件與出貨 | SPEC、手冊、安裝文件、出貨基線 | 文件改成現況 | 手冊「檢測工具設定」一節的截圖是 Agent 主控台;安裝需求寫 8444 埠與憑證警告處理;188 基線庫含本案 migration |
依賴:A → B 與 C 平行 → C 的派工改道 → D 的三頁改版與搬家 → E。FR-156 E1 已 Done,本案直接接上。決策者功課:無。
| # | 決策 | 定案 | 理由 | 被排除的方案 |
|---|---|---|---|---|
| D1 | 怎麼進主控台 | 從主系統跳轉:簽五分鐘票、Agent 驗票換 30 分鐘 session;Agent 不開登入表單 | 使用者本來就在主系統做事做到一半才會想進 Agent;不用記網址、不用多登一次、帳密不經 Agent;Agent 不存帳號、驗票不回頭問主系統,FR-156 E1 機制現成 | 用主系統同一組帳密登入 Agent(Agent 要連主系統驗、或存一份帳號;多一個登入表單多一個攻擊面) |
| D2 | 連線設定搬多少 | 整組搬(網址、帳號、埠、密碼全在主控台填) | 填一半在 A、一半在 B 是最差的使用體驗;PM 說的「連線設定」本來就是整組;主系統連客戶 OpenVAS 在哪個 IP 都不知道,機敏範圍寬一點也涵蓋 | 只搬機敏欄位 |
| D3 | 表單定義從哪來 | 主系統工具目錄經心跳下發、Agent 快取 | 目錄 config.detection_tools.config_field_schema 已有且有 i18n;新增工具或改欄位不用改 Agent 程式 |
Agent 每支 connector 自帶(兩份定義改一邊漏一邊) |
| D4 | 既有帳密怎麼搬家 | 升級後經既有 mTLS 派工通道自動交接一次、所有 Agent 回執後主系統清除 | 這條通道本來就天天送帳密,多送最後一次再關掉;客戶升級後什麼都不用做;搬完才清,中途斷線重跑不丟 | 升級後請客戶重填(升級當天所有檢測跑不動) |
| D5 | 主控台前端放哪 | FE repo 第二個 Vite 入口 console/,build 產物複製進 Agent 安裝包 |
設計系統、PrimeVue、i18n 都在 FE repo,手抄就是第二套真相;build 鏈多一步用腳本固定 | evidence-agent 內手寫靜態 HTML+JS |
| D6 | 檔案瀏覽動作範圍 | 只查只下載,不刪 | 刪除是主系統證據生命週期的事;從 Agent 端刪會讓主系統參照變孤兒 | 可刪(要再做反向通知) |
| D7 | 檔案瀏覽顯示業務脈絡 | 第一版不做 | 檔案屬於哪個專案只有主系統知道,要經雲端給對照表;先列檔案本身 | 第一版就做對照快取 |
| D8 | 主控台操作回傳主系統稽核 | 要,走心跳 | 稽核報表一處看完;客戶問「誰下載過這份證據」答得出;事件只有誰、何時、做了什麼,沒有帳密內容 | 只留 Agent 本機 log(主系統稽核缺一塊) |
| D9 | 多台 Agent 設定同步 | 不做,各設一次 | 會經過雲端,與動機矛盾;決策者裁一個網段通常一台 Agent、或資料台與掃描台分開 | 雲端設一次同步各台 |
| D10 | 誰能開主控台 | 兩個能力點「Agent 設定」「Agent 檔案」,簽票時把範圍寫進票,主控台照票開分頁與 API | 檔案頁的對象(稽核、顧問、專案成員)比 Agent 管理寬,綁在 Agent 管理權限上會把他們鎖在外面;走 FR-155 做好的 RBAC,不另發明守門 | 與 Agent 管理頁同一權限、主控台內不分讀寫(本稿原建議,決策者不採) |
| D11 | Agent 端加密金鑰放哪 | 獨立金鑰檔 secrets.key 0600 |
換 mTLS 憑證(重新註冊)不影響帳密;「重新註冊就要重填所有帳密」會是客服電話來源 | 用 mTLS 私鑰派生 |
D10 的限制要寫進手冊
第一版檔案頁的權限是租戶層:有「Agent 檔案」就看得到這台 Agent 的全部檔案,做不到「只看自己專案的證據」。Agent 不知道檔案屬於哪個專案,那是 D7 第二期的事。對客戶 IT 或稽核通常可接受;若要讓一般專案成員也能看,D7 要提前做。
UI 設計約束(沿用 FR-134/FR-156)
版面照 Guidant 設計系統;沒做的功能不在畫面預告(檔案頁不出現刪除、不出現專案欄);grep border-left 為零;下拉選項若有一律進 system_menus。
| 元件 | 現況座標 | 現況 | 本案動作 | 卡 |
|---|---|---|---|---|
| Agent 管理能力點 | DEV public.capabilities:detection.remote-agent.{read,create,update,delete},license_module='remote-agent-manage';路由 remote-agent-manage 綁 read=ALL、其餘 ANY |
四顆 CRUD | 加 detection.remote-agent.configure(Agent 設定)、detection.remote-agent.browse-files(Agent 檔案);路由綁 browse-files 為 ANY(只有檔案權限也看得到清單頁);出廠範本角色「稽核員」補 browse-files |
A1 |
| 主系統 Agent 管理端點守門 | BE core/plugins/remote_agent.py:174 admin_required,_ADMIN_CAPABILITIES 依 __qualname__ 查能力點 |
清單 RemoteAgentManageRoute.get 守 read |
清單改守 read 或 browse-files(require_capability(*names) 多值=任一);新端點 RemoteAgentConsoleTicketRoute.post 守 configure 或 browse-files |
A1 |
| 入場券簽發 | jedi-remote-agent app/service/agent_ticket_service.py:68 issue(purpose, agent_uid, subject, tenant_id, scan_uid, max_bytes, origin, ttl);PURPOSE_CONSOLE_LOGIN="console_login" 已預留;claims aud/sub/tenant_id/purpose/jti/origin |
只有上傳票在用(jedi-detection DetectionToolJobUploadTicketRoute) |
加 scope: list[str] 參數,console_login 必填且非空;claims 多 scope、display_name(右上角「以王小明登入」用) |
A1 |
| 主系統取使用者能力點 | common/authz require_capability;FE $can(CAP.X) |
守門用 | RemoteAgentAdapters 加 capability_checker: Callable[[str], bool](宿主注入),簽票 service 用它算 scope |
A1 |
| Agent 8444 瀏覽器門 | evidence-agent core/browser_plane_server.py:111 create_browser_app:只有 PUT/OPTIONS /scan-input/<uid>,_method_not_allowed 把其他路徑當不存在;:181 start_browser_plane_tls |
單一用途 | 加 /console(靜態)、POST /console/session、/console/api/*、POST /console/logout;/scan-input 驗票、/console/api 驗 session,互不認對方憑據 |
A2 |
| Agent 驗票 | evidence-agent core/agent_auth.py verify_upload_ticket(雲端公鑰、claims 檢查) |
認 scan_upload |
加 verify_console_ticket:purpose=console_login、aud=agent:<自己>、scope 非空;jti 一次性記憶體集合(五分鐘淘汰) |
A2 |
| Agent 心跳 payload | evidence-agent core/enroll.py:260 _heartbeat_once(capabilities、browser_port、vulndb_sync.heartbeat_fields());observers :326 _observe_error_tracking、:333 _observe_allowed_targets |
自報能力與狀態 | payload 加 credential_store: true、configured_tools、storage、console_events;observer 加 _observe_tool_schemas(版本不同就拉) |
C1、C2、D3 |
| 雲端心跳處理 | jedi-remote-agent app/service/agent_enrollment_service.py:464 _heartbeat;_capabilities_from_payload、_browser_port_from_payload、vulndb_values_from_payload;回應 pending_tasks/allowed_targets/vulndb |
欄位逐一從 payload 取 | 收 configured_tools→remote_agents.configured_tools JSONB、storage→storage_stats JSONB、credential_store→credential_store BOOLEAN;回應加 tool_schemas_version;console_events 交宿主 hook 寫稽核 |
C2、D3 |
| 心跳宿主擴充先例 | BE core/plugins/_agent_vulndb.py vulndb_heartbeat_extra(FR-156 C1) |
宿主往心跳回應塞 vulndb |
照抄一支 core/plugins/_agent_tool_schemas.py:回 tool_schemas_version;新端點 GET /agents/tool-schemas(AUTH_AGENT)回工具目錄 |
C1 |
| 工具目錄 | DEV config.detection_tools:9 支,config_field_schema 全有、credential_group_schema 只有 gcb/inspec、requires_credentials 除 sbom-scan 外皆 true、requires_target_host gcb/inspec/nmap/openscap |
主系統工具設定頁表單來源 | 原樣下發給 Agent(code、名稱、i18n 標籤、欄位、群組、requires_target_host) |
C1 |
| 主系統工具設定表 | config.tenant_detection_tool_configs:credentials_encrypted、field_values、status、last_tested_at、last_test_result |
每租戶每工具一列 | 加 handover JSONB(每台 Agent 回執);搬家完成後 credentials_encrypted=NULL、field_values 連線欄位清空、status='on_agent';表與欄位留著給「啟用開關」 |
D2 |
| 派工解密下發 | jedi-detection app/service/detection_orchestration_service.py:240/:812 _resolve_credentials、:1237 _tool_requires_credentials |
解密塞 _credentials |
目標 Agent credential_store=true 時不帶 _credentials,改查該台 configured_tools 含此工具,沒有→立即 failed 新錯誤碼 DETECTION_AGENT_TOOL_NOT_CONFIGURED;credential_store=false(舊版 Agent)照舊下發(搬家過渡) |
C2 |
| 主系統測試連線 | jedi-detection api/routing.py:76 /detection-tools/configs/<uid>/test-connection→DetectionToolService.test_connection 解密後打 Agent POST /detection/probe(evidence-agent api/detection/routes/detection_route.py:51) |
雲端帶帳密去 probe | 改成選一台 Agent、不帶帳密;Agent probe 改讀本機儲存 | C2 |
| Agent 執行器拿帳密 | evidence-agent core/task_executor.py(_credentials 夾帶慣例,:100 註解)、task_executor_connectors/base.py:51 DetectionConnector(params, credentials)、:106 probe() |
從派工單取 | 派工單沒帶就從本機 credential_store.get(tool_code) 取;兩者都沒有→failed「尚未設定」 |
C2 |
| Agent 檔案索引 | Agent SQLite /data/agent.db 表 upload_files(jedi-file-upload infra/models/upload_file.py:uid/file_name/file_ext/size/mimetype/storage_type/sha256/created_at/created_user…);core/app_factory.py:37-46 建表 |
只有程式在讀 | 主控台檔案 API 讀它;下載沿用 FileUploadService 既有串流(8443 GET /blob/<uid> 同一支 service) |
B1 |
| Agent 儲存設定 | evidence-agent config/config.py:23 STORAGE_TYPE(local/seaweedfs/minio)、:45 data_dir()、core/disk_space.py |
啟動時組 DTO | 總覽與檔案頁的「儲存後端/健康/磁碟剩餘」從這裡算;SeaweedFS 健康=S3 head_bucket |
A2、B1 |
| Agent 資料目錄 | /data(AGENT_DATA_DIR):agent.db、憑證、scan-input/、vulndb/ |
升級不動的那層 | 加 secrets/secrets.key(0600)、secrets/tool-credentials.enc、tool-schemas.json、console-audit.jsonl |
C1、D3 |
| Agent 安裝包 | evidence-agent scripts/build/build_agent_bundle.sh、build_compose_bundle.sh、assets/ |
不含前端 | FE console build 產物進 assets/console/;browser plane 以靜態檔服務 |
A2 |
| FE Agent 清單 | src/views/remote-agent/RemoteAgentManage.vue(v-can="CAP.DETECTION_REMOTE_AGENT_*" 控新增/啟停/撤銷);src/config/capabilities.js:43-46 |
名稱/位址/狀態/能力/版本/弱點資料 | 加兩顆 CAP 常數;加「已設定工具」「儲存」欄與「開啟設定頁/瀏覽檔案」按鈕;只有 browse-files 的人看到唯讀清單 | A1、D1 |
| FE 工具設定頁 | src/views/plugin/ToolPluginManage.vue(958 行,超標)、src/utils/detectionCredentialFields.js |
對話框填連線欄位、has_credentials 不回顯 |
連線欄位整段拿掉改「各 Agent 設定狀態」表;超標檔只准拆不准塞:狀態表另開子元件 | D1 |
| FE build | vite.config.js 單入口、package.json build:ONPREM 等 |
一個站 | 加 vite.console.config.js(root console/、outDir dist-console/)與 build:CONSOLE |
A2 |
| 稽核事件碼 | BE common/enum/event_code.py:77 AGENT_TICKET_ISSUED=6084,6085~6089 空;jedi-remote-agent common/code.py:64 AgentEventCode 鏡像 |
一顆票事件 | 加 AGENT_CONSOLE_LOGIN=6085、AGENT_CREDENTIAL_SET=6086、AGENT_CREDENTIAL_CLEARED=6087、AGENT_FILE_DOWNLOADED=6088(兩邊同值) |
D3 |
能力點(主線 phase=active migration,照 scripts/sql/2026-10-10-fr134-source-scan-capabilities.sql 形狀)
| name | module | resource_type | action | description | license_module | 路由綁定 |
|---|---|---|---|---|---|---|
detection.remote-agent.configure |
detection | remote-agent | configure | Agent 設定(檢測工具帳密) | remote-agent-manage | remote-agent-manage ANY |
detection.remote-agent.browse-files |
detection | remote-agent | browse-files | Agent 檔案(瀏覽、下載) | remote-agent-manage | remote-agent-manage ANY |
capabilities.action 若有 CHECK 約束要先查(\d public.capabilities),新值不在清單就一併放寬。browse-files;「負責人」「審核者」「瀏覽者」不補(決策者要加去角色管理勾)。role_capability_patterns 既有 detection.*.* 的範本會自動涵蓋。CAPABILITY_CATALOG/VERSION 加一。簽票端點(jedi-remote-agent,ROUTE_TABLE 加 ("/remote-agents/<string:uid>/console-ticket", RemoteAgentConsoleTicketRoute, AUTH_ADMIN);BE _ADMIN_CAPABILITIES["RemoteAgentConsoleTicketRoute.post"] = (configure, browse-files)):
POST /api/1.0/remote-agents/{uid}/console-ticket
→ scope = [s for s in ("configure","files") if capability_checker(對應能力點)]
→ agent_ticket_service.issue(purpose="console_login", agent_uid, subject=login_name,
tenant_id, origin=request Origin, scope=scope, display_name=nickname)
← {url: "<agent_url>/console#<ticket>", expires_in: 300, scope: [...]}
票 claims(凍結):aud=agent:<uid>、sub=<login_name>、tenant_id、purpose=console_login、jti、origin、scope: ["configure","files"](子集)、display_name、exp(5 分鐘)。scope 空就不簽(403)。
Agent 8444 主控台路由(browser_plane_server.py 已 200 餘行,新路由放 core/console/ 套件,create_browser_app 只掛 blueprint):
| 路由 | 驗什麼 | 做什麼 |
|---|---|---|
GET /console、GET /console/assets/* |
無 | 靜態檔(assets/console/);沒有 session cookie 時前端自己顯示指路頁(靜態頁內含兩種狀態,不另開端點) |
POST /console/session body {ticket} |
verify_console_ticket + jti 未用過 |
發 session:Set-Cookie: agent_console=<隨機 32 bytes>; HttpOnly; Secure; SameSite=Strict; Path=/console; Max-Age=1800;記憶體 {sid: {sub, display_name, tenant_id, scope, expires_at}};每次 API 成功呼叫把 expires_at 往後推 30 分鐘;回 {display_name, scope, expires_at};寫本機稽核 login |
GET /console/api/me |
session | 回同上,前端刷新時用 |
GET /console/api/overview |
session | 名稱、租戶、角色(儲存/檢測)、版本、儲存後端與健康、主系統網址、最後心跳、容量四數字、各工具設定狀態(只有 configure 才給這段) |
POST /console/logout |
session | 刪 session、清 cookie |
/console/api/tools/* |
session + configure in scope |
§5.3 |
/console/api/files/* |
session + files in scope |
§5.2 |
# 後:前端載入後讀 location.hash,POST 換 session,history.replaceState 抹掉。jti 一次性:記憶體 set,五分鐘後淘汰;Agent 重啟後視窗可接受(票只五分鐘),不持久化。{error_code: "AGENT_CONSOLE_SCOPE_DENIED"};前端照 me.scope 不渲染該分頁。/scan-input 的 CORS 處理不動。FE console 入口(D5):compliance-manager-fe/console/(index.html、main.js、App.vue、views/{Overview,Tools,Files}.vue、api.js);vite.console.config.js:root: 'console'、base: '/console/'、build.outDir: '../dist-console',alias @ 同主站以共用 src/assets 的設計 token、src/config/locales/i18n(只載 remote-agent.json、plugin_manage 與新 agent-console.json)、PrimeVue 元件按需 import;不載 Pinia 使用者 store、不載主站 router。package.json 加 build:CONSOLE。evidence-agent build_agent_bundle.sh 加一步:FE_CONSOLE_DIST(預設 ../compliance-manager-fe/dist-console)存在才複製進 assets/console/,不存在 build 失敗(不是略過)。體積上限 2 MB(gzip 前),量過寫進回寫。
GET /console/api/files?q=<檔名包含>&from=<date>&to=<date>&uploader=<login>&sort=created_at|size&order=desc&page=1&size=50
← {total, items: [{uid, file_name, size, mimetype, storage_type, sha256, created_at, created_user}], stats: {file_count, bytes_used, backend, backend_healthy, disk_free_bytes, scan_input_bytes, vulndb_bytes}}
GET /console/api/files/<uid>/download → Content-Disposition attachment,串流自後端(FileUploadService 既有 adapter)
GET /console/api/files/uploaders → 上傳者清單(distinct created_user)給篩選用
upload_files,只列 storage_scope='customer' 的證據檔;源碼暫存(scan-input/)與弱點資料(vulndb/)只算目錄大小進 stats,不列。file_downloaded {uid, file_name, sha256}。created_user(主系統 login name);暱稱 Agent 沒有,第一版就顯示 login name。表單定義下發(D3):BE core/plugins/_agent_tool_schemas.py 提供 tool_schemas_heartbeat_extra()→心跳回應多 tool_schemas_version(=config.detection_tools 最大 updated_at 的 epoch 或 CAPABILITY_CATALOG 式計數);新端點 GET /api/1.0/agents/tool-schemas(AUTH_AGENT,掛在 jedi-remote-agent ROUTE_TABLE,資料由宿主 payload_provider 風格的 tool_schemas_provider 注入)回:
{version, tools: [{code, name, name_i18n: {zh_Hant_TW, en}, requires_credentials, requires_target_host,
config_field_schema: [...原樣...], credential_group_schema: {...原樣或 null}}]}
Agent _observe_tool_schemas:版本與 /data/tool-schemas.json 不同就拉一次、原子寫入。主控台工具頁從這份畫表單;拉不到時工具頁顯示「尚未取得工具目錄,請稍候(Agent 需先與主系統完成一次心跳)」。
Agent 本機儲存(D11):core/credential_store.py
secrets/secrets.key:第一次寫入時 Fernet.generate_key(),os.open(..., 0o600);讀取時檢查 mode 不是 0600 就警告(不拒絕)。secrets/tool-credentials.enc:Fernet 密文,解開是 {<tool_code>: {fields: {...所有連線欄位含非機敏...}, configured_at, configured_by, last_test_at, last_test_result, last_test_message}};整檔讀寫、寫入走 temp+os.replace。get(code) -> dict|None、put(code, fields, actor)、clear(code, actor)、status() -> {code: {configured_at, last_test_at, last_test_result}}(不含欄位值)、mark_test(code, ok, message)。status() 是心跳 configured_tools 的來源。主控台工具 API:
GET /console/api/tools → 目錄(tool-schemas.json)+ status()
GET /console/api/tools/<code> → 非機敏欄位回顯、機敏欄位只回 {set: true}
PUT /console/api/tools/<code> body {fields} → 機敏欄位留空=不改(與主系統 has_credentials 慣例同);寫稽核 credential_set
DELETE /console/api/tools/<code> → clear;寫稽核 credential_cleared
POST /console/api/tools/<code>/test body {target_host?, credential_group?} → 用本機帳密呼叫 connector.probe()(與 api/detection/routes/detection_route.py 同一條路,差別是帳密來源),回 {ok, message, duration_ms},mark_test
欄位驗證照 config_field_schema(required、type)在 Agent 做一次;gcb/inspec 的 credential_group_schema 兩組(SSH/WinRM)各自存在 fields 下的群組 key,形狀與主系統 field_values 一致,派工時 connector 不用改讀法。
派工改道(C2,jedi-detection detection_orchestration_service):
if agent.credential_store: # 新版 Agent(心跳自報)
if tool_code not in agent.configured_tools: # 心跳自報的 status()
→ execution failed, error DETECTION_AGENT_TOOL_NOT_CONFIGURED,
message「Agent {name} 尚未設定 {tool} 連線,請至該台設定頁填寫」
params 不帶 _credentials
else: # 舊版 Agent,搬家過渡
照 FR-056 D9 解密下發(直到 D2 搬家完成清掉密文)
Agent task_executor:credentials = params.pop("_credentials", None) or credential_store.get(code);兩者皆無→failed 同一句話。probe 路由(8443 /detection/probe)同樣:body 沒帶 credentials 就讀本機。主系統「測試連線」改:選一台 Agent→POST /detection-tools/configs/<uid>/test-connection {agent_uid, target_host?, credential_group?} 不帶帳密→Agent 本機帳密 probe→結果寫 tenant_detection_tool_configs.last_test_*(與主控台本機測試各自記錄,兩邊都能測)。
心跳回報:payload configured_tools: {<code>: {configured_at, last_test_at, last_test_result}}、credential_store: true;雲端 remote_agents.configured_tools JSONB、credential_store BOOLEAN DEFAULT false(套件 migrations/010-remote-agent-console.sql)。
三頁改版(D1 卡)
RemoteAgentManage.vue:加「已設定工具」欄(configured_tools 的 key 綠標、目錄內其餘工具灰標、tooltip 最後測試)、「儲存」欄(storage_stats:{file_count} 檔 · {bytes_used} · {backend});按鈕:$can(configure)→「開啟設定頁」、否則 $can(browse-files)→「瀏覽檔案」;只有 browse-files 的人:隱藏新增/啟停/撤銷/註冊 token 區塊與「已設定工具」欄;離線的 Agent 按鈕 disabled。按下→POST console-ticket→window.open(url, '_blank', 'noopener')。按鈕旁提示「需在可連到 Agent 的網路」。ToolPluginManage.vue(超標):對話框連線欄位整段移除,改掛新子元件 ToolAgentStatusTable.vue(每台 Agent 一列:名稱、已設定/未設定、最後測試、「前往設定」=同一支簽票);「測試連線」改先選 Agent(只有一台自動選);detectionCredentialFields.js 不再被工具頁 import(主控台 console 入口改用它,搬到 src/utils/ 共用位置不動)。RemoteAgentManageRoute)serializer 加 configured_tools、storage_stats、credential_store。搬家(D2 卡)
升級後 Agent 第一次心跳 credential_store=true 且 remote_agents.credential_store 由 false→true
→ 宿主 lifecycle hook:對該租戶每支 credentials_encrypted 非空的工具,建 agent_tasks
type="credential_handover", params={tools: {<code>: {fields: field_values ∪ 解密後機敏欄位}}}(既有 mTLS pending_tasks 通道)
→ Agent 執行器收到:credential_store.put 每支(已設定的不覆蓋,回執 skipped)→ result {stored:[...], skipped:[...]}
→ 雲端 result handler:tenant_detection_tool_configs.handover[<agent_uid>] = {at, stored, skipped}
→ 每次回執後檢查:該租戶所有 enabled 且 credential_store=true 的 Agent 都回執了
→ credentials_encrypted=NULL、field_values 只留非連線欄位(啟用等)、status='on_agent'、稽核 CREDENTIALS_HANDED_OVER
→ Agent 清單「已設定工具」欄在過渡期顯示「帳密搬家中:2/3 台」(有 handover 未齊)
稽核回傳(D3 卡)
console-audit.jsonl(append,每列 {at, actor, action, tool|file_uid, file_name, sha256, ok}),心跳帶 console_events: [...上次心跳後新增的...],雲端回 console_events_ack: <最後一筆 at> 後 Agent 才前進游標(沒 ack 下次重送,雲端以 (agent_uid, at, action) 去重)。audit():事件碼 6085~6088,actor=<sub>、agent_uid、tool/file_uid;FE 稽核報表的事件碼 i18n 補四條。SPEC 三頁(Agent 管理、檢測工具、新頁「Agent 主控台」);手冊「檢測工具設定」改成主控台流程、加「誰能開」(兩個能力點)、「檔案頁看得到這台全部檔案」限制、憑證警告處理;安裝文件 8444 埠與 FE console 體積;docs/claude/domain-capabilities.md 補 credential_store、console session、tool_schemas_provider 三列;出貨基線重產(本案主線 migration:能力點 1 支、tenant_detection_tool_configs.handover 1 支)。
| 規則 | 本案怎麼守 |
|---|---|
| 票在 URL | # fragment 不進日誌;一次性 jti;5 分鐘;綁 aud |
| session | HttpOnly/Secure/SameSite=Strict/Path=/console;30 分鐘滑動;登出即刪 |
| scope | API 層檢查,不是只藏分頁;403 有錯誤碼 |
| 帳密 | Fernet;金鑰 0600;日誌/稽核/心跳永不含欄位值;回顯只回 {set: true} |
| 機器擁有者 | 與 FR-064 D6 邊界一致:不宣稱擋機器擁有者;手冊寫明 |
| 下載 | 走 session,稽核留 sha256;不開匿名下載 |
| CSP | console 入口沿用主站 nonce 作法(cspNonce),browser plane 回 Content-Security-Policy 與主站同形 |
排序以「決策者何時看得到畫面」為準:先讓主控台開得出來、檔案頁有真資料(Agent 索引本來就有東西),再做帳密。同批依卡號串行。
| 批 | 卡 | 白話 | 改哪些檔(repo) | 驗收(對照雛形) | 依賴 |
|---|---|---|---|---|---|
| 1 | A1 兩個能力點+簽票端點+清單按鈕 | 主系統這邊:種兩個權限、簽一張帶範圍的票、清單加按鈕 | BE scripts/sql/2026-10-xx-fr157-agent-console-capabilities.sql、core/plugins/remote_agent.py(_ADMIN_CAPABILITIES、adapters 加 capability_checker);jedi-remote-agent agent_ticket_service.py(scope/display_name)、api/routes/remote_agent_route.py+routing.py(console-ticket)、plugin.py(adapters 欄位);FE capabilities.js、RemoteAgentManage.vue(按鈕與依權限顯示)、i18n |
幕 1 兩種身分;curl 打 console-ticket 回 url 以 # 帶票、jwt decode 看到 scope;沒有兩顆能力點的人 403;只有 browse-files 的人清單頁看得到但無管理按鈕 |
— |
| 1 | A2 Agent 主控台殼:8444 掛頁、驗票換 session、指路頁、總覽;FE console 入口 | Agent 這邊:開門、換 session、三分頁的殼與總覽;前端骨架進 Agent 包 | evidence-agent core/console/{__init__,session,routes,overview}.py(新)、core/agent_auth.py(verify_console_ticket)、core/browser_plane_server.py(掛 blueprint 一行)、scripts/build/build_agent_bundle.sh、assets/console/;FE console/(新)、vite.console.config.js、package.json |
幕 2、幕 6;用 A1 的票新分頁開出總覽;# 票被抹掉;同票二用 409;直接打網址是指路頁;30 分鐘不動回指路頁;切稽核身分沒有「檢測工具」分頁且 GET /console/api/tools 403 |
A1(claims 形狀已凍結,可先用手簽票開發) |
| 2 | B1 檔案瀏覽 | 列這台存的證據檔,搜、篩、下載、容量 | evidence-agent core/console/files.py(新)、core/disk_space.py(加統計);FE console/views/Files.vue |
幕 4:主系統上傳一份證據→主控台搜到、SHA-256 一致、下載同一份;暫存與弱點資料只有 GB 數;沒有刪除按鈕 | A2 |
| 2 | C1 工具表單下發+Agent 加密存+本機測試連線 | 主控台能填帳密、存 Agent、按測試 | BE core/plugins/_agent_tool_schemas.py(新)、core/plugins/remote_agent.py(接 provider);jedi-remote-agent 心跳回應 tool_schemas_version、GET /agents/tool-schemas;evidence-agent core/credential_store.py(新)、core/console/tools.py(新)、core/enroll.py(observer);FE console/views/Tools.vue |
幕 3:填 OpenVAS 帳密測試綠勾;cat /data/secrets/tool-credentials.enc 是密文;ls -l secrets.key 0600;機敏欄位重開只顯示「已設定」;稽核 jsonl 無欄位值 |
A2 |
| 3 | C2 派工改讀本機+心跳回報設定狀態+主系統測試連線改派 Agent | 主系統派工不再帶帳密;Agent 用自己那份;沒設定立即失敗 | jedi-detection detection_orchestration_service.py、detection_tool_service.py(test_connection)、錯誤碼;jedi-remote-agent 心跳收 configured_tools/credential_store、migrations/010;evidence-agent core/task_executor.py、api/detection/routes/detection_route.py、core/enroll.py(payload) |
主系統派 OpenVAS 檢測跑完(派工單 JSON 無 _credentials);派到沒設定的 Agent 立即 failed 訊息指名哪台哪支;主系統測試連線選 Agent 成功;舊版 Agent(credential_store=false)照舊能跑 |
C1 |
| 3 | D1 主系統三頁改版 | 工具設定頁拿掉帳密欄改各台狀態表;清單加兩欄 | FE ToolPluginManage.vue(只拆不塞)、components/plugin/ToolAgentStatusTable.vue(新)、RemoteAgentManage.vue(兩欄);jedi-remote-agent serializer 加三欄 |
幕 5、幕 1 兩欄;工具對話框無任何帳密欄;狀態表每台一列、「前往設定」開得出主控台對應工具 | C2 |
| 4 | D2 既有帳密搬家 | 升級後自動把雲端的帳密交給每台 Agent,收齊回執才清 | jedi-detection(handover task 建立、result handler、tenant_detection_tool_configs.handover 套件 migration、清除邏輯);jedi-remote-agent lifecycle hook;evidence-agent task_executor_connectors/credential_handover.py(新);FE 清單「搬家中」標籤 |
用 FR-056 時代的 DEV 資料(credentials_encrypted 非空)起新版 Agent→心跳後 handover 有回執→密文清空、status='on_agent'→原本能跑的檢測不重填能跑;兩台只升一台→清單「搬家中 1/2」、密文未清 |
C2 |
| 4 | D3 主控台操作回傳稽核 | 誰在 Agent 改了帳密、下載了什麼,主系統稽核看得到 | evidence-agent core/console/audit.py(新)、core/enroll.py(console_events+ack 游標);jedi-remote-agent 心跳收事件交宿主;BE common/enum/event_code.py 四碼、core/plugins/remote_agent.py hook;FE 稽核報表 i18n |
主控台登入、改帳密、下載各一次→主系統稽核報表三列,事件碼 6085/6086/6088,actor 是主系統帳號;心跳斷一次再連不重複 |
A2、C1、B1 |
| 5 | E1 SPEC、手冊、安裝文件 | 文件改現況 | BE docs/spec-site/current/(Agent 管理、檢測工具、新頁 Agent 主控台)、docs/user-manual/、docs/claude/domain-capabilities.md;evidence-agent deploy/README.md |
手冊「檢測工具設定」截圖是主控台;「誰能開」寫兩個能力點與租戶層限制;安裝文件 8444 與 console 體積;grep "原本|改判" 零命中 |
全部 |
| 5 | E2 出貨基線重產+收尾紀錄 | 新 migration 進基線;STATE/LOG | BE scripts/init/、handoff/ |
188 基線庫 schema_migrations 含本案 2 支主線 migration;STATE 只描述此刻 |
E1 |
決策者功課(開工前):無。角色管理裡要不要給哪些角色「Agent 檔案」,上線後由客戶管理員自己勾;出廠只補「稽核員」。
| # | 步驟 | 期望 |
|---|---|---|
| 1 | 用有 configure+browse-files 的帳號開 Agent 管理頁 | 看得到「開啟設定頁」、新增/啟停/撤銷按鈕、「已設定工具」「儲存」欄 |
| 2 | 按「開啟設定頁」 | 新分頁主控台總覽;網址 # 後沒有票;右上角「以 王小明 登入 · 設定+檔案」 |
| 3 | 用只有 browse-files 的帳號(出廠「稽核員」)開 Agent 管理頁 | 唯讀清單、按鈕「瀏覽檔案」、沒有管理按鈕與「已設定工具」欄 |
| 4 | 按「瀏覽檔案」 | 主控台只有「總覽」「檔案」分頁;curl -b cookie GET /console/api/tools 403 |
| 5 | 直接打 https://<agent>:8444/console |
指路頁,不顯示任何這台的資訊 |
| 6 | 主控台檢測工具填 OpenVAS 帳密、測試連線 | 綠勾;/data/secrets/ 兩檔存在、key 0600;重開表單密碼欄顯示「已設定」 |
| 7 | 主系統建 OpenVAS 檢測任務派到這台 | 跑完;agent_tasks.params 無 _credentials;DEV DB credentials_encrypted 為空 |
| 8 | 派到另一台沒設 OpenVAS 的 Agent | 立即 failed,訊息「Agent 鼎泰研發區 Agent-02 尚未設定 OpenVAS 連線」 |
| 9 | 主系統上傳一份證據到走這台 Agent 的專案 | 主控台檔案頁搜檔名找到;大小、SHA-256 一致;下載同一份;源碼暫存與弱點資料只有 GB |
| 10 | 主系統稽核報表 | 2、4、6、9 各有對應列(登入、登入、帳密設定、下載),actor 是主系統帳號 |
| 11 | 還原一份 FR-056 時代的 DEV dump(密文非空)、起新版 Agent | 心跳後密文清空、status='on_agent';原本能跑的檢測不重填能跑;兩台只升一台時清單顯示「搬家中 1/2」且密文未清 |
| 12 | 主控台放 30 分鐘不動再點 | 回指路頁 |
| 風險 | 怎麼擋 |
|---|---|
| 瀏覽器連不到 Agent(使用者在外網) | 設計上的必然;按鈕旁提示、開不出來時指路頁寫原因;手冊寫明只能在客戶內網操作 |
| 自簽憑證警告 | 同 FR-156 D9:手冊寫匯入 CA;第二期選配客戶自備憑證 |
| 票被攔截重放 | 5 分鐘、一次性 jti、綁 aud、放 # 不進日誌 |
| 搬家中途有 Agent 沒升級 | 等所有 enabled 且新版的 Agent 回執才清;舊版照舊收帶帳密的單;清單顯示進度 |
| 同租戶多台只設一台 | 清單「已設定工具」欄一眼看到;派工前檢查、失敗訊息指名哪台 |
| 檔案頁租戶層權限被誤解為專案層 | 手冊與角色管理的能力點說明寫明;D7 第二期 |
/scan-input 票與主控台 session 共用 8444 |
路由分開、各驗各的憑據,互不承認 |
| Agent 機器擁有者讀得到金鑰與密文 | 與 FR-064 D6 邊界一致,手冊寫明 |
| FE console 體積進 Agent 包 | PrimeVue 按需 import;上限 2 MB,量過寫回寫 |
ToolPluginManage.vue 958 行超標 |
D1 只拆不塞:狀態表新子元件,連線欄位段刪除 |
capabilities.action 有 CHECK 約束擋新值 |
A1 開工先 \d,有就同支 migration 放寬 |
| 工具目錄沒下發到 Agent 時工具頁空白 | 顯示「尚未取得工具目錄」而非空白;心跳每 N 秒會補 |
| 你想知道 | 看 |
|---|---|
| PM 說什麼、選項怎麼推、D10 為什麼改 | 討論稿 §6.4 |
| 畫面長什麼樣、兩種身分差在哪 | 雛形(右上切身分) |
| 每個決策為什麼 | 本文件 §3 |
| 開工要改哪些檔 | 本文件 §4、§6 |
| 入場券與 8444 門怎麼做的 | FR-156 design E1 |
| 被推翻的帳密存雲端設計 | FR-056 design D9 |
| 做到哪了 | README 總進度、handoff/(開工後) |