FR-169 · 設計文件 v1 · 2026-10-29(D1~D8 定案;10 張卡 6 批;母卡 CM-3223)
下載下來的檔案自己帶鎖;密碼系統產、畫面顯示一次;獨立小套件兩邊接;管理員開關預設開
報表、證據包下載到電腦後是明文,別人走到這台電腦雙擊就能看。本案讓每份下載出去的檔自己帶鎖:Word、Excel、PDF 用檔案格式內建的開啟密碼,雙擊會先問密碼;多檔、圖片、log 打成 AES-256 加密 zip。密碼由瀏覽器端產生、按下載前只顯示一次,不存、不寄信、不進 log。加密寫成獨立小套件 jedi-file-protect,主系統 16 支匯出端點與 Agent 主控台各自一行接上,Agent 離線照常。使用者永遠不能選「不加密」;系統管理員在安全政策有一個開關、出貨預設開、關閉時寫稽核並常駐警示。不做撤銷、到期、IRM/DRM。
狀態:已收尾(母卡與十二張子卡 Done;STG/POC 未上、Agent image 未出包)|日期:2026-10-29|母卡 CM-3223、子卡 CM-3224~CM-3231、補卡 CM-3280~CM-3281|前作 FR-166/166.1(Agent 主控台、分模組下載/匯出能力點、心跳下發模型、Agent 稽核 logging 管線)、FR-133(錯誤追蹤遮罩)、FR-145.3(結案報告草稿浮水印) 開發 branch:四個 repo(BE、FE、evidence-agent、jedi-python-package)2026-10-29 皆在
feature/FR-139;runner 開工git branch --show-current再確認,不對就停下問,不切
🔴 這份是設計決策,不是現況。 上線後的畫面與 API 現況看 SPEC 頁(各匯出頁的「下載檔案會加密」段、安全政策設定);使用者怎麼開加密檔看使用手冊
docs/user-manual/encrypted-download-guide.md;套件用法看 jedi-file-protect README。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-29 | 初版:討論稿 v3 D1~D8 全照建議定案;8 張卡 5 批;同日開卡 | 母卡 CM-3223,子卡 CM-3224~CM-3231 |
| 2026-10-29 | 母卡層總驗通過;補批 6 兩張卡(D5 的全站常駐橫幅、Agent 稽核 encrypted 欄進主系統);三支套件發版 | CM-3280、CM-3281;jedi-file-protect 0.1.0、jedi-iam 1.10.0、jedi-masking 0.1.1 |
| 2026-10-29 | D5 第三道「畫面上標未加密」裁示不做(系統無稽核事件頁);批 6 驗收通過、四 repo 合回 | CM-3281 復核、決策者裁示 |
| 2026-10-29 | 決策者實測證據附件下載沒加密:範圍漏了套件自帶的上傳檔下載 route。首腦直修 jedi-file-upload 1.5.0 download_responder+BE 注入+FE 12 處;三支套件匯出端點另開批 7~8 |
首腦直修;CM-3286、CM-3287 |
| 2026-10-29 | 決策者指定不加密的三類匯出:責任分工設定、控制項對照(4 個出口)、任務批次——要離線改再匯回,匯入端讀不了加密檔;後端 send_unprotected 照樣記稽核 encrypted=false。「哪些出口要加密」的設定功能先不做 |
首腦直修 |
| 2026-10-29 | 第三方元件掃描頁 SBOM 檢視器改走 inline(畫面解析 JSON,不經加密),下載鈕照舊加密 | 首腦直修 |
| 2026-10-29 | 批 7~8 完成:jedi-log/jedi-issue/jedi-task-platform 匯出加 download_responder 掛點並發版,主系統以 responder_for(source) 注入(操作日誌是自產 zip,unzip=True 重包成一個 AES zip) |
CM-3286、CM-3287;jedi-log 1.4.0、jedi-issue 1.5.0、jedi-task-platform 1.5.0 |
| 2026-10-29 | D3 補:包 zip 的單檔外層檔名去掉原副檔名(截圖.zip,裡面仍是 截圖.png);jedi-file-protect 0.2.1 |
首腦直修 |
| 2026-10-29 | D7 補:預覽本身不改造,但 PDF 預覽藏掉瀏覽器內建檢視器的下載/列印工具列、列印印空白,免得有預覽權的人從預覽拿到沒密碼的檔;順帶修 FR-166 CM-3198 的 iframe sandbox 回歸(Chrome 全部 PDF 預覽哭臉) |
首腦直修,FE 72ed484c、03e6acfb |
| 2026-10-29 | D3 改判:7z → AES-256 zip(決策者實測 .7z 內建解壓打不開);jedi-file-protect 0.2.0;needs_7zip 欄位名沿用、語意改「要裝第三方解壓工具」。檔名清單明文一事決策者裁示接受(畫面上本來就看得到檔名),zip 內外都用原檔名,不做中性檔名+對照表 |
首腦直修 |
要擋的只有一件事:檔案下載到電腦後,別人走到這台電腦雙擊就能開。不擋檔案離開系統後的收回、到期、轉傳(決策者裁示先不做撤銷),不擋截圖與拍照。
端到端(一次下載):使用者按匯出 → 前端問開關(快取)→ 開啟則對話框在瀏覽器端產 16 碼密碼、只顯示一次、複製後「下載」才亮 → 請求帶 X-File-Password header(不進 URL)→ 後端照常產檔 → 呼叫 jedi-file-protect 依副檔名加密(pdf/docx/xlsx 原生密碼;其他與多檔 AES zip)並蓋浮水印 → 寫 FILE_DOWNLOADED 稽核(不含密碼)→ 回檔帶 Cache-Control: no-store、X-Filename、X-File-Protected、X-File-Needs-7zip → 對話框關閉、密碼清掉。
Agent 主控台同一條,差別是開關值來自最後一次心跳、稽核事件先寫本機 system_logs 再隨心跳回主系統。
.zip,要 7-Zip/WinZip/Keka 輸入密碼開。jedi-file-protect,之後診斷包、資料庫備份下載、授權檔匯出都能直接用。| 批 | 做什麼 | 產出 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|
| 1 套件 | jedi-file-protect:產密碼、三種加密、浮水印 |
一支能 pip install 的套件 |
套件 tests 全綠,含兩個突變測試(改 R5 紅、zip 拿掉 WZ_AES 紅) |
| 2 看到效果 | BE helper+開關+心跳+事件碼;FE 對話框元件+composable+安全政策段;兩邊先接稽核計畫與結案報告 | 按匯出跳對話框、下載到的檔要密碼 | DEV 稽核計畫按匯出 Word:對話框顯示密碼、複製後下載鈕才亮、Word 開要密碼;安全政策關開關後不跳直接給檔、頁頂有警示 |
| 3 全部接上 | BE 其餘端點;FE 其餘 24 處;Agent 下載與心跳 | 任何下載都加密,Agent 離線照常 | 翻每個有下載鈕的頁面都跳對話框;拔 Agent 網路後主控台下載照常要密碼 |
| 4 主控台前端 | FE console/ 三頁接同一個對話框 |
主控台與主站長一樣 | 主控台檔案頁下載跳同一個對話框 |
| 5 文件 | SPEC、使用手冊「如何開啟加密檔」、套件 README | 客戶看得懂怎麼開 | 手冊頁四種格式各一段 |
依賴:1 → 2(B1、F1 平行)→ 3(B2、F2、A1 平行)→ 4 → 5。決策者功課:發版前確認 jedi-iam(加一個政策鍵)與新套件 jedi-file-protect 兩支套件發版。
| # | 題目 | 定案 | 理由 | 排除 |
|---|---|---|---|---|
| D1(CM-3231) | 套件名 | jedi-file-protect |
做的不只加密,浮水印也在;對到 Office「保護文件」用語 | jedi-file-crypto(只講加密)、jedi-export-guard |
| D2 | 密碼形態 | 16 碼隨機英數分四段(Kq7m-Xw2p-Rt9v-Bn4z),去 0/O/1/l/I,secrets 產 |
檔案密碼可離線無限次試,六碼數字幾分鐘破;分段好對照、純英數不被密碼框吃符號 | 五單字密語(要自備單字表、強度較低)、20 碼含符號(難抄) |
| D3 | 多檔與非文件容器 | AES-256 zip(pyzipper,WinZip AE-2;內容加密、檔名清單明文;禁 ZipCrypto) |
決策者實測 .7z 用內建解壓打不開、使用者不知道怎麼辦,裁示用大家認得的 zip;代價是檔名外露,且 Windows/macOS 內建解壓一樣開不了 AES zip、仍要裝工具 | 7z(header 加密連檔名一起藏,但副檔名陌生)、ZipCrypto zip(內建能開但已被已知明文攻破,等於沒鎖)、單張圖片轉加密 PDF |
| D4 | 浮水印 | PDF 與 docx 頁尾小字「下載者 暱稱(帳號) · 時間 · Guidant AI」;xlsx 與 zip 不蓋 | 擋「自己傳出去」的追責;滿版斜字影響可讀;Excel 沒有頁 | 滿版斜字、不做 |
| D5 | 誰能選不加密 | 使用者永遠不能;管理員一個開關、預設開;關閉寫稽核、系統管理員全站頂部常駐橫幅(每筆下載的稽核紀錄記 encrypted=false;「畫面上標未加密」因系統沒有稽核事件頁,決策者裁示不做) | e2e/教育訓練、客戶既有 DLP、匯出餵別系統三種情境要有正規出口;預設開+三道警示壓掉「被關掉」風險。反悔條件:客戶普遍一裝就關則改不可關 | 三選一含「使用者可選」(會習慣性點掉)、寫死強制(沒有正規出口使用者會繞) |
| D6 | 開關值到 Agent | 掛 heartbeat_extras(照 FR-166 D23),Agent 存本機 data/file_protect.json,離線沿用;沒帶視為開啟 |
一份真相、掛點現成、離線語意 FR-166 已接受 | Agent 自己一份設定、Agent 一律加密不吃開關 |
| D7 | 預設線上看 | 本案不做預覽器改造;既有 PDF 預覽藏掉瀏覽器內建檢視器的工具列(#toolbar=0)、列印印空白 |
每個匯出頁加預覽器範圍是本案好幾倍;但預覽框那顆原生下載鈕給的是沒密碼的 PDF,等於繞過加密,藏掉是一行的事。擋不住右鍵與 Ctrl+S,要全擋得 pdf.js 自繪 | pdf.js 自繪(另開卡)、後端轉圖片(截圖本來就擋不住,為擋開發者工具付的成本不划算) |
| D8 | 密碼在哪產 | 瀏覽器端 crypto.getRandomValues,放 header X-File-Password 送後端;後端收不到回 400 不靜默吐明文 |
密碼只在瀏覽器與那一次請求出現;URL 會進 access log 與歷史。getRandomValues 不要求 secure context(與 randomUUID 不同),F1 驗收仍要在 http+IP 實測 |
後端產回給前端(多一處會留痕) |
| 元件 | 現況 | 本案動作 | 卡 |
|---|---|---|---|
| BE 匯出端點 | 16 支 route 各自 send_file;ap_export、ssp_export、mf_ssp_export 各有一份 _send_attachment 複本 |
抽成 api/common/file_protect.py::send_protected,各端點一行呼叫;三份複本刪掉 |
B1、B2 |
上傳檔下載 GET /file/download/<uid>(jedi-file-upload 自帶 route:證據附件、程序書、前置資料、問卷與意見回饋附件) |
套件 send_file 直出;FE 用簽票 ?st= 開新分頁(帶不了密碼 header) |
套件 1.5.0 加選填 download_responder 掛點,主系統注入 send_protected;FE 改走 useProtectedDownload().downloadFile、簽票明文路徑刪除 |
首腦直修 |
| 其他套件自帶的人用匯出(jedi-api-log 操作日誌、jedi-issue 意見回饋、jedi-task-platform 任務批次) | 各自 send_file |
同一形狀開掛點、主系統注入 | P2、B4 |
| 加密能力 | 主系統有 cryptography(Fernet 欄位加密)、pypdf、LibreOffice;無 Office 加密、無 AES zip |
新套件帶 msoffcrypto-tool、pyzipper,重用 pypdf |
P1 |
| 浮水印 | 結案報告「草稿」浮水印是 docx 範本變數 | 套件提供通用頁尾浮水印;結案報告定稿版也走套件 | P1 |
| 安全政策 | system_configs 群 RUNTIME_CONFIG;型別與預設在 jedi-iam domain/security_policy.py(BOOL_KEYS 只有 MFA_REQUIRED);FE 頁四段 |
jedi-iam 加布林鍵 FILE_DOWNLOAD_PROTECTION=True;FE 登入段後加 InputSwitch 與關閉警示 |
B1、F1 |
| 心跳下發 | core/plugins/remote_agent.py heartbeat_extras 已掛 vulndb、tool_schemas、ldap_config;Agent 側 ldap_source.py 是 observer 範本 |
多掛 _agent_file_protect.py;Agent 新 file_protect_sync.py |
B1、A1 |
| 稽核事件 | audit(logger, name, EventCode, **fields);事件碼最大 6228;Agent 側 FR-166.1 D6 接 logging 管線 |
新 FILE_DOWNLOADED(6229);Agent AGENT_FILE_DOWNLOADED 既有、加 encrypted 欄 |
B1、A1 |
| FE 下載 | 26 處 responseType: 'blob' 散在 composable、service、view;檔名用 filenameFromHeaders、下載用 downloadBlob |
抽 useProtectedDownload 一支,全部改呼叫;對話框元件一個 |
F1、F2 |
| Agent 主控台 | 後端 core/console/files.py、ops_routes.py 下載;前端在 FE repo console/(Files/Runs/Ops) |
後端接套件與心跳;前端 reuse 主站元件 | A1、F3 |
| 下載權限 | FR-166.1 D2/D3:下載分模組看各自 download 能力點(7 顆)+匯出 export 顆,出貨零角色,上傳者本人免;Agent 主控台看 download-files 範圍 |
不動;加密對話框掛在「已通過下載守門」之後,誰看得到下載鈕由 FR-166.1 決定 | — |
generate_password() -> str # 19 字元:4 段 × 4 碼英數,'-' 相連,去 0O1lI
protect(data: bytes, filename: str, password: str, *, watermark: WatermarkInfo | None = None) -> ProtectedFile
ProtectedFile(data: bytes, filename: str, needs_7zip: bool, encrypted: bool)
分派:.pdf → pypdf encrypt(user_password, algorithm="AES-256")(R6,禁 AES-256-R5/RC4);.docx/.xlsx/.pptx → msoffcrypto agile;其他副檔名與多檔(傳 list[(name, bytes)])→ py7zr SevenZipFile(password=…, header_encryption=True),回 .7z。浮水印只對 pdf/docx 生效,內容「{nickname}({login}) · {YYYY-MM-DD HH:mm} · Guidant AI」頁尾小字。零框架相依,照 jedi-masking 形狀;publish.sh 推 Nexus 等令。
send_protected(buf, filename, mimetype, *, actor):① 讀 request.headers["X-File-Password"];② read_root_config_value("RUNTIME_CONFIG","FILE_DOWNLOAD_PROTECTION"),關閉→跳過加密;③ 開啟且無 header → BadRequestError(GRC_400xxx 缺少檔案密碼);④ 呼叫套件;⑤ audit(FILE_DOWNLOADED, filename, format, encrypted, source);⑥ send_file + headers:Cache-Control: no-store、X-Filename(URL 編碼)、X-File-Protected: 0|1、X-File-Needs-7zip: 0|1,Access-Control-Expose-Headers 補這三個。密碼不得出現在任何 log 字串:helper 內不 log header,錯誤訊息不帶值;FR-133 遮罩規則補 X-File-Password header 名。
useProtectedDownload().download(url, params, fallbackName):讀 securityPolicyStore.fileDownloadProtection(登入後載一次,安全政策頁存檔後刷新)→ 開啟則開 ProtectedDownloadDialog(props:needs7zip 由呼叫端依格式預判,zip/多檔為 true)→ 使用者按複製(navigator.clipboard,失敗退回 execCommand)→ 下載鈕亮 → axios blob 帶 header → downloadBlob → 對話框關、密碼變數清空。密碼不進 Pinia、不進 localStorage。
後端 core/console/file_protect.py 與主系統 helper 同介面、獨立實作(主系統那支綁 Flask-RESTful/EventCode/read_root_config_value)。心跳 observer file_protect_sync.py 照 ldap_source.py:收 data.file_protect.enabled 寫 data/file_protect.json;key 不存在(舊主系統)不動、檔案不存在視為 true。前端 console/ 直接 import 主站 ProtectedDownloadDialog 與 composable。
| 規則 | 怎麼滿足 |
|---|---|
| 祕密不進 log/錯誤追蹤 | helper 不 log header;FR-133 遮罩加 X-File-Password;驗收 grep X-File-Password log/app.log 為 0 |
| 輸出不被快取 | Cache-Control: no-store |
| 密碼學不自創 | 全走格式標準:PDF AES-256 R6、Office agile、zip AES-256(WinZip AE-2);secrets 產亂數 |
| 新端點守門 | 不開新端點;既有端點守門不動 |
| 暫存檔 | 套件全程 bytes 在記憶體;LibreOffice 轉檔的暫存由既有路徑負責刪 |
批(Stage)=工作坊同時開幾個小弟一起跑的那一組。底表在 handoff/cards.py,兩個斷言已過(依賴在更早的批;同批同 repo 不改同一檔)。
登記型檔只讓一張卡碰:BE event_code.py、remote_agent.py adapters、jedi-iam security_policy.py 都在 B1;FE 主站 i18n 由 F1 一次佔好 key(含 F2 用的);api.js 不動(端點不變)。
套件異動(發版前統一確認):新 jedi-file-protect、jedi-iam(加一鍵);開發期 path dependency,不 commit path 改動。
| 批 | 卡 | repo | reuse/extend/new+理由 | 改哪些既有檔 | 新開哪些檔 | 依賴 | 驗收 |
|---|---|---|---|---|---|---|---|
| 1 | P1(CM-3224) 新套件 jedi-file-protect:產密碼、PDF/Office/7z 三種加密、PDF 與 docx 頁尾浮水印(零框架相依) | jedi 套件 | new:照 jedi-masking 形狀開一支「一件事、零框架相依」的小套件(只相依 pypdf、msoffcrypto-tool、py7zr、python-docx、cryptography)。既有 cryptography Fernet 是欄位加密不是檔案加密,不適用;既有結案報告「草稿」浮水印是 docx 範本變數,只適用該範本,故套件另提供通用頁尾浮水印。政策、稽核、密碼顯示畫面都不進套件(宿主的事) |
— | jedi-file-protect/pyproject.toml;jedi-file-protect/jedi_file_protect/init.py;jedi-file-protect/jedi_file_protect/password.py(generate_password:16 碼英數四段、去 0O1lI、secrets);jedi-file-protect/jedi_file_protect/protect.py(protect(data, filename, password, *, watermark=None) → ProtectedFile(data, filename, needs_7zip: bool);依副檔名分派);jedi-file-protect/jedi_file_protect/pdf.py(pypdf AES-256 R6;禁 RC4/R5);jedi-file-protect/jedi_file_protect/office.py(msoffcrypto agile 加密 docx/xlsx/pptx);jedi-file-protect/jedi_file_protect/archive.py(py7zr AES-256、header 加密,多檔與其他副檔名);jedi-file-protect/jedi_file_protect/watermark.py(PDF 頁尾小字、docx 頁尾小字);jedi-file-protect/tests/;jedi-file-protect/README.md;jedi-file-protect/publish.sh |
— | 套件 tests:pdf 加密後 pypdf 不帶密碼開啟 raise、帶密碼開啟成功且 /R 為 6;docx/xlsx 加密後 msoffcrypto 回 is_encrypted() True 且用密碼解回原 bytes;7z 不帶密碼 py7zr 列檔名 raise(header 加密);password 1 萬次無 0O1lI、長度 19、四段;突變:把 pdf 改 R5 測試紅、把 7z header 加密拿掉測試紅;pip show 相依不含 Flask/SQLAlchemy;浮水印:PDF 每頁最後一行含下載者字串、docx footer 含同字串 |
| 2 | B1(CM-3225) 主系統:出檔前保護 helper、安全政策開關 FILE_DOWNLOAD_PROTECTION、心跳下發、FILE_DOWNLOADED 事件碼、先接稽核計畫與結案報告兩個端點 | BE | new:common/util/file_protect.py 的 send_protected(buf, filename, mimetype, *, request, watermark_actor)——讀 header X-File-Password、查開關、呼叫套件、寫稽核、回 send_file+Cache-Control: no-store+X-Filename+X-File-Protected;取代各端點手寫的 _send_attachment(ap_export、ssp_export、mf_ssp_export 三支已各有一份,本卡抽成一份)。extend:jedi-iam security_policy.py BOOL_KEYS/_STATIC_DEFAULTS 加 FILE_DOWNLOAD_PROTECTION=True(套件改動,開發期 path dependency);core/plugins/remote_agent.py heartbeat_extras 加 file_protect_heartbeat_extra;event_code.py 加 FILE_DOWNLOADED;ap_export_route.py、closing_report_route.py 改呼叫 helper |
jedi-iam/jedi_iam/domain/security_policy.py(BOOL_KEYS、_STATIC_DEFAULTS);pyproject.toml(jedi-file-protect pin、jedi-iam path 開發期);common/enum/event_code.py;core/plugins/remote_agent.py(heartbeat_extras);api/project/routes/ap_export_route.py;api/closing_report/routes/closing_report_route.py;app/system_config/service/security_policy_app_service.py(開關關閉寫稽核 SECURITY_POLICY_UPDATED 帶 file_download_protection=False) | common/util/file_protect.py;core/plugins/_agent_file_protect.py(心跳 extra:{"file_protect": {"enabled": bool}});test/test_file_protect_helper.py |
P1 | DEV 重啟後:GET /ap/<uid>/export?format=docx 不帶 header 回 400(GRC_400xxx 新碼「缺少檔案密碼」),帶 X-File-Password 回 docx 且 Word 要密碼、回應 header 有 Cache-Control: no-store 與 X-File-Protected: 1;format=zip 回 .7z 且 7-Zip 要密碼、X-File-Needs-7zip: 1;結案報告 pdf 同;稽核事件頁出現 FILE_DOWNLOADED(使用者、檔名、encrypted=true)且 payload 無密碼;grep -rn X-File-Password log/app.log 為 0;安全政策 PUT FILE_DOWNLOAD_PROTECTION=false 後同端點不帶 header 直接給明文、事件 encrypted=false;心跳回應 data.file_protect.enabled 跟著變;守衛測試綠;回寫明講 jedi-iam 已改、發版前要確認 |
| 2 | F1(CM-3226) 主站共用 composable useProtectedDownload +「密碼只顯示一次」對話框元件 + 安全政策開關段 + 先接稽核計畫與結案報告 | FE | new:src/composables/useProtectedDownload.js(先問 /security-policy 開關快取;開啟→開 ProtectedDownloadDialog 產密碼(crypto.getRandomValues,16 碼四段去 0O1lI)→確認後 axios blob 帶 X-File-Password;關閉→直接下載;檔名用既有 filenameFromHeaders、下載用既有 downloadBlob),取代各 composable 內重複的 axios blob 段。new:src/components/common/ProtectedDownloadDialog.vue(照雛形①:提示列、密碼框、複製後下載鈕才亮、7z 提醒依 needs7zip 顯示)。extend:SecurityPolicyForm.vue 登入段後加「檔案下載保護」InputSwitch(照 MFA 那段形狀)+關閉時頁頂 Message 警示;useApExportDownload.js、useClosingReport.js 改走新 composable。i18n 兩語言 key 本卡一次佔好(含 F2 會用的) |
src/composables/useApExportDownload.js;src/composables/useClosingReport.js;src/views/security-policy/SecurityPolicyForm.vue;src/config/locales/i18n/zh-tw/security-policy.json;src/config/locales/i18n/en/security-policy.json;src/config/locales/i18n/zh-tw/common.json;src/config/locales/i18n/en/common.json | src/composables/useProtectedDownload.js;src/components/common/ProtectedDownloadDialog.vue | P1 | 稽核計畫頁按匯出 Word:對話框顯示 19 字元密碼、「下載」鈕灰到按「複製」才亮、下載後對話框關閉;DevTools Network 看請求 header 有 X-File-Password、URL 無密碼;localStorage/Pinia 無密碼;再按一次密碼不同;匯出 ZIP 對話框多 7-Zip 提醒;安全政策頁多一個開關、關掉存檔後頁頂出現警示、再按匯出不跳對話框;grep -rn border-left src/components/common/ProtectedDownloadDialog.vue 為 0;http://<IP>:5173 非 localhost 開頁實測 crypto.getRandomValues 可用 |
| 3 | B2(CM-3227) 主系統其餘匯出端點全部改走 send_protected(SSP、控制項對照、RACI、風險登錄、前置資料、診斷包、AI 呼叫紀錄、SSP 控制實作、範本下載、來源掃描、PoA&M、稽核結果、通過報告、Agent 報告與檔案包、弱點庫包除外) | BE | extend:每支端點把自己的 send_file/_send_attachment 換成 B1 的 send_protected 一行;刪掉 ssp_export、mf_ssp_export 各自的 _send_attachment 複本。不接:agent_vulndb_route(Agent 機器對機器拉的包,不是人下載)、oscal_framework_version_route OSCAL json 下載(框架公開內容,非機敏;回寫列出)、poam_objective_route json 若為機器匯出同上 |
api/oscal/routes/ssp/ssp_export_route.py;api/module_frame/routes/mf_ssp_export_route.py;api/oscal/routes/control_mapping_route.py;api/governance/routes/raci_defaults_io_route.py;api/governance/routes/risk_register_route.py;api/pre_engagement/routes/file_route.py;api/support/routes/diagnostic_bundle_route.py;api/ai_call_log/routes/ai_call_log_route.py;api/oscal/routes/ssp/ssp_control_impl_import_route.py;api/module_frame/routes/ssp_import_template_route.py;api/module_frame/routes/module_frame_template_import_route.py;api/source_scan/routes/scan_route.py;api/project/routes/ar_workbook_route.py;api/project/routes/pass_report_route.py;api/project/routes/poam_objective_route.py;api/project_summary_report/routes/project_summary_report_route.py;api/remote_agent/routes/agent_local_run_route.py;api/remote_agent/routes/agent_file_route.py | — | B1 | `grep -rn 'send_file|as_attachment' api/ |
| 3 | F2(CM-3228) 主站其餘 24 處下載全部改走 useProtectedDownload(service 層 blob 與頁面內 axios blob 都收進 composable) | FE | extend:src/service/*Service.js 內回 blob 的方法改回 {url, params} 讓 composable 打,或直接在頁面改呼叫 useProtectedDownload().download(url, params, fallbackName);頁面內自己寫 axios blob 的段落刪掉改呼叫。不接:裝置匯入範本、使用者匯入範本這類「空白範本」(非機敏,回寫列出);確認每個下載按鈕只剩一條路徑 |
src/composables/useAgentLocalRuns.js;src/composables/useArExportDownload.js;src/composables/useClosingReportBundle.js;src/composables/useCraLegalDocsExport.js;src/composables/usePassReportDownload.js;src/service/ControlMappingService.js;src/service/GovernanceService.js;src/service/ModuleFrameTemplateService.js;src/service/PreEngagementService.js;src/service/SourceScanService.js;src/service/SspService.js;src/service/SupportService.js;src/views/action-log/UserLogs.vue;src/views/ai-call-log/AiCallLogList.vue;src/views/ai-call-log/AiUsageSummary.vue;src/views/device/DeviceExportMenu.vue;src/views/feedback/system/SystemFeedbackManage.vue;src/views/project-summary-report/ProjectSummaryReportForm.vue;src/views/project/ProjectPlanningView.vue;src/views/project/RoundPoamPlanView.vue | — | F1 | `grep -rln "responseType: 'blob'" src |
| 3 | A1(CM-3229) Agent 主控台:檔案與報告下載走 jedi-file-protect、心跳收 file_protect 開關離線沿用、本機稽核 AGENT_FILE_DOWNLOADED 帶 encrypted | evidence-agent | extend:core/console/files.py 兩條下載與 ops_routes.py 診斷包下載改呼叫一支 Agent 側 helper core/console/file_protect.py(讀 header、查本機快取的開關、呼叫套件、稽核;與主系統 helper 同介面但不共用程式——主系統 helper 綁 Flask-RESTful 與 EventCode);心跳 observer 照 ldap_source.py 形狀收 data.file_protect,寫 data/file_protect.json,沒帶(舊主系統)視為開啟;pyproject.toml 加 jedi-file-protect。前端在 FE repo console/,由 F3 做 |
core/console/files.py;core/console/ops_routes.py;core/console/heartbeat.py(掛 observer);pyproject.toml | core/console/file_protect.py;core/file_protect_sync.py(心跳 observer+本機快取);tests/test_file_protect.py | P1 | Agent 本機:下載不帶 header 400、帶 header 回加密檔;拔主系統後重啟 Agent 仍加密(讀快取);主系統關開關→下一跳後不加密、data/file_protect.json 為 false;開回來下一跳恢復;從未連過主系統的新 Agent 預設加密;system_logs 有 AGENT_FILE_DOWNLOADED 帶 encrypted 欄且無密碼;套件 tests 綠 |
| 4 | F3(CM-3230) Agent 主控台前端:檔案頁、執行紀錄、診斷包下載接 ProtectedDownloadDialog(reuse F1 元件,不另刻) | FE | reuse:console/ 直接 import src/components/common/ProtectedDownloadDialog.vue 與 useProtectedDownload(主控台與主站同一個 Vite 專案,可共用;若 console 有獨立 i18n 命名空間則把 key 複製一份);開關值從 Agent 既有主控台狀態端點讀(A1 若沒暴露就加一個欄位,回寫註明);不新刻第二個對話框 |
console/views/Files.vue;console/views/Runs.vue(或執行紀錄對話框);console/views/Ops.vue(診斷包);console/i18n(若獨立) | — | F1、A1 | 主控台檔案頁下載跳同一個對話框(含 7z 提醒);離線狀態下仍跳、仍能下載加密檔;主系統關開關後下一跳對話框不跳;grep -rn 'ProtectedDownloadDialog' console/ 命中且 console/ 內無第二份對話框元件 |
| 5 | D1 文件:SPEC 頁(匯出類頁面各加一段「下載會加密」、安全政策頁)、使用手冊「如何開啟加密檔」、套件 README、design.md 開頭交棒句 | BE | extend:docs/spec-site/current/ 對應頁加段落與檔頭變更紀錄一行;使用手冊新增一頁「下載的檔案要密碼?」(Word/Excel/PDF/7z 四種怎麼開、7-Zip/Keka 下載連結、忘記密碼重新匯出) |
docs/spec-site/current/system-admin/security-policy.md;docs/spec-site/current/(匯出類頁面,runner grep '匯出' 列出);docs/user-manual/ | docs/user-manual/<開啟加密檔>.md | B2、F2、F3 | MkDocs build 無警告;手冊頁有四種格式各一段與截圖位置;grep -n "原本|改判|第 [0-9] 棒" <改到的 spec 頁> 為 0 |
| 6 | B3(CM-3280) Agent 下載稽核的 encrypted 欄位進主系統(_FIELDS 白名單加一欄、bool 不被 _clean 丟掉)+ SPEC/手冊補總覽警示與未加密標記 | BE | extend:core/plugins/_agent_console_audit.py _FIELDS 加 encrypted;_clean 目前把 bool 一律丟成 None(git log -L 查原因),encrypted 要能進 message(轉 true/false 字串或 encrypted 專用處理),不要另寫第二支 sink。文件:F4 做完後 security-policy.md 與手冊兩處補回「總覽常駐警示、稽核頁標未加密」(D1 第 2 輪為照實寫先拿掉) |
core/plugins/_agent_console_audit.py(_FIELDS、_clean 對 bool);test/test_agent_console_audit_sink.py;docs/spec-site/current/system-admin/security-policy.md;docs/user-manual/encrypted-download-guide.md;docs/spec-site/current/user-manual/encrypted-download-guide.md | — | A1、D1 | Agent 主控台下載一次後,主系統稽核事件頁 AGENT_FILE_DOWNLOADED 那筆 message 含 encrypted=true(開關關閉時 encrypted=false);既有 sink 測試全綠且新增一條「encrypted 進 message、沒帶時不出現」;兩份手冊 diff 一致 |
| 6 | F4(CM-3281) D5 另兩道警示:系統管理員看得到的「檔案下載保護已關閉」頂部橫幅(總覽與全站常駐)+ 稽核事件頁每筆下載標「未加密」 | FE | reuse:securityPolicyStore.protectionOn(F2 已改讀 /system/file-download-protection)、LicenseExpiryBanner 掛在 AppLayout.vue 的位置與寫法(照它新開一支 FileProtectionOffBanner.vue,不塞進 LicenseExpiryBanner)、i18n 已預留 protected_download.unprotected_badge 與 security_policy.file_download_protection_off_warning。extend:UserLogs.vue request/message 欄對 FILE_DOWNLOADED 與 AGENT_FILE_DOWNLOADED 且 encrypted=false 的列加一顆 Tag(severity warning)。誰看得到橫幅:用側欄對「安全政策」頁的同一個能力點判斷,不自己發明角色判斷 |
src/layout/AppLayout.vue(掛一行);src/views/action-log/UserLogs.vue | src/components/security/FileProtectionOffBanner.vue | F1、F2 | 安全政策關掉存檔後:總覽與任何頁頂部出現常駐橫幅(系統管理員帳號看得到、一般使用者看不到),開回來橫幅消失;稽核事件頁 FILE_DOWNLOADED/AGENT_FILE_DOWNLOADED 且 encrypted=false 的列有「未加密(保護已關閉)」Tag,encrypted=true 的列沒有;grep -rn border-left 新檔為 0;vitest 既有綠 |
| 7 | P2(CM-3286) 三支套件的人用匯出端點加 download_responder 形狀的掛點:jedi-api-log 操作日誌匯出、jedi-issue 意見回饋匯出、jedi-task-platform 任務批次匯出(主系統注入 send_protected) | jedi 套件 | extend:照 jedi-file-upload 1.5.0 FileUploadAdapters.download_responder 的形狀,三支套件各在 adapters 加一個選填 download_responder(file_obj, file_name, mimetype),route 的 send_file 改成「有注入交給它、沒注入照舊」;不在套件裡認得加密政策。不接:jedi-iam 使用者匯入範本與 TOTP QR 圖(非機敏)、jedi-asset 裝置匯入範本、jedi-evidence-classification 預覽(inline 不落磁碟)、jedi-detection 報告(主系統 Agent 報告端點已接) |
jedi-api-log/.../api_log_route.py ExportApiLogsRoute;jedi-issue/.../feedback_route.py FeedbackExportRoute;jedi-task-platform/.../job_import_route.py JobExportRoute;三支套件 plugin/contract.py(adapters 欄位)、README adapters 表、pyproject version、tests | — | 無 | 三支套件 contract 測試各加一條「注入 responder 時交給它、未注入 send_file」;版號各進中版號(功能);回寫列出每支套件 adapters 欄位名與未接清單;不發版、不推 Nexus(發版由首腦統一做) |
| 8 | B4(CM-3287) 主系統注入三支套件的 download_responder(操作日誌、意見回饋、任務批次匯出改走 send_protected)+FE 三處下載改走 useProtectedDownload | BE | reuse:core/plugins/_file_upload_download.protected_download_responder 直接給三支套件用(同一支函式、source 改成各自代號可加參數);三支 core/plugins/useProtectedDownload().download(...)(FE 改動另開一張卡或由首腦決定併卡,本卡只列 BE) |
core/plugins/api_log.py;core/plugins/issue.py;core/plugins/task.py;core/plugins/_file_upload_download.py(source 參數化);pyproject.toml(三支套件釘版,發版後) | — | P2 | DEV 重啟後三個匯出端點不帶密碼 400、帶密碼回加密檔且 X-File-Protected: 1;稽核 source 各自代號;`grep -rn send_file( .venv/lib/python3.11/site-packages/jedi_/ --include=.py |
.zip,Windows 檔案總管與 macOS 內建解壓開不了、7-Zip/Keka 要密碼;檔名清單看得到、內容解不開。X-File-Password、URL 無密碼、回應 Cache-Control: no-store;log/app.log grep 密碼字串為 0。FILE_DOWNLOADED 帶使用者、檔名、encrypted=true,payload 無密碼。encrypted=false → 開回來全恢復。algorithm 參數有 AES-256-R5,選錯比 RC4 還好破;P1 突變測試鎖 R6。logger.info(request.headers) 都會漏;helper 不 log header,FR-133 遮罩補 header 名。navigator.clipboard 在 http+IP 不存在(memory feedback_secure_context_only_browser_apis_break_on_http_ip):複製鈕要退回 execCommand('copy');crypto.getRandomValues 本身可用但 F1 要實測。BOOL_KEYS 加鍵是套件改動,主系統 pin 要跟著升;發版前決策者確認。