本卡屬 FR-169(母卡 (建卡後補 Case No)),卡 B1,批 2。只改 BE(主 repo,Repo 欄空著)。依賴:P1。
主系統這邊把套件接起來,並做「看得到效果」的最小一段:①一支共用 helper send_protected,所有匯出端點之後都呼叫它——讀前端送來的密碼 header、查安全政策開關、呼叫套件加密、寫一筆稽核、回檔並加不可快取的 header;②安全政策多一個布林鍵「檔案下載保護」(預設開)——型別與預設寫在 jedi-iam 套件裡(它管所有安全政策鍵),所以這張卡會動到 jedi-iam;③心跳回應多帶這個開關值給 Agent;④新稽核事件碼 FILE_DOWNLOADED;⑤先把「稽核計畫匯出」與「結案報告匯出」兩個端點改走 helper,讓決策者在 DEV 看到第一個加密檔。三支既有端點各自抄了一份 _send_attachment,本卡抽成一份。
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
repo:~/Projects/Billows/Audit-Manager/compliance-manager-be
改既有: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
jedi_iam/domain/security_policy.py:BOOL_KEYS 加 FILE_DOWNLOAD_PROTECTION;_STATIC_DEFAULTS 加 "FILE_DOWNLOAD_PROTECTION": True。主專案 pyproject.toml jedi-iam 與 jedi-file-protect 開發期走 path dependency([tool.poetry.dependencies] 註解區有先例),path 改動不 commit。common/util/file_protect.py:send_protected(buf, filename, mimetype, *, actor: UserContext, source: str)。順序:讀 request.headers.get("X-File-Password") → read_root_config_value(RUNTIME_CONFIG_GROUP, "FILE_DOWNLOAD_PROTECTION")(預設 True)→ 開啟且無 header 拋 BadRequestError(GrcErrorCode.GRC_FILE_PASSWORD_REQUIRED)(新碼,grc_error_code.py 400 序號接最大+1;FE error-code.json 同步由 F1 補,回寫註明)→ jedi_file_protect.protect(...) → audit(logger, "FILE_DOWNLOADED", EventCode.FILE_DOWNLOADED, file_name=, fmt=, encrypted=, source=) → send_file + Cache-Control: no-store、X-Filename(URL 編碼,沿用 ap_export_route._send_attachment 的寫法)、X-File-Protected、X-File-Needs-7zip;Access-Control-Expose-Headers 補三個。helper 內不得 log header、不得把密碼放進任何例外訊息。common/enum/event_code.py:FILE_DOWNLOADED = 6229(開卡已查最大 6228)。core/plugins/_agent_file_protect.py:file_protect_heartbeat_extra(agent) -> {"file_protect": {"enabled": bool}},照 _agent_vulndb.py 形狀;core/plugins/remote_agent.py heartbeat_extras tuple 加進去。app/system_config/service/security_policy_app_service.py 既有 audit(... SECURITY_POLICY_UPDATED ...) 的 kwargs 加 file_download_protection=changed.get("FILE_DOWNLOAD_PROTECTION")(照 mfa_required 那行)。api/project/routes/ap_export_route.py 的 _send_attachment 刪掉改呼叫 helper;api/closing_report/routes/closing_report_route.py 匯出端點同。zip 格式的呼叫端改傳 files=[...] 走 7z。X-File-Password(若名單在套件,回寫註明要發版;若宿主可注入就注入)。test/test_file_protect_helper.py:開關開+無 header → 400;開關關 → 明文且 encrypted=False;logger patch 照 memory feedback_test_logger_patch_db_handler。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 已改、發版前要確認
~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-169-2610-download-file-encryption/design.md §3 決策、§5 詳細設計、§6 拆分表本卡那列