forked from wangziqi/gongxue-base
feat: add content asset security scanning
This commit is contained in:
@@ -28,7 +28,7 @@ POST /api/tenant-content/assets/sign-upload
|
||||
PUT /api/tenant-content/assets
|
||||
```
|
||||
|
||||
后台确认上传并发布:
|
||||
后台确认上传:
|
||||
|
||||
```text
|
||||
POST /api/tenant-content/assets/confirm-upload
|
||||
@@ -58,6 +58,12 @@ POST /api/videos/play
|
||||
GET /api/tenant-content/assets/access-events?assetId=...
|
||||
```
|
||||
|
||||
后台查看资源安全扫描事件:
|
||||
|
||||
```text
|
||||
GET /api/tenant-content/assets/security-scan-events?assetId=...
|
||||
```
|
||||
|
||||
后台管理员下载:
|
||||
|
||||
```text
|
||||
@@ -90,11 +96,13 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
2. 前端使用返回的 `upload.url` 和 `upload.headers` 直传对象存储。
|
||||
3. 调用 `PUT /api/tenant-content/assets` 登记资源台账。托管对象默认进入 `status=draft`、`uploadStatus=pending`。
|
||||
4. 调用 `POST /api/tenant-content/assets/confirm-upload`,由后端读取对象元数据并比对大小、MIME、SHA-256。
|
||||
5. 校验通过且 `publish=true` 时,后端将资源置为 `status=active`、`uploadStatus=verified`。
|
||||
6. 学生端只能下载或预览 `active + verified` 的托管对象资源。
|
||||
7. 学生端和后台管理员下载/预览都会写入 `content_asset_access_events`,包含 `assetId/userId/accessType/result/expiresInSec/signatureMode/ip/userAgent` 等审计字段。
|
||||
8. 生产环境定时运行 assets worker,复检 `pending/verified` 托管对象的大小、MIME、SHA-256 等元数据。
|
||||
9. 如果复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致,worker 会把资源置为 `uploadStatus=failed`,并将 `active` 资源退回 `draft`,同时写入 `security_flags.assetRecheckFailed=true` 和 `audit_logs`。
|
||||
5. 校验通过后,后端只会置为 `uploadStatus=verified`、`securityScanStatus=pending`,并继续保持 `status=draft`。即使请求里传 `publish=true`,也不会绕过安全扫描直接发布。
|
||||
6. 运行 assets worker。worker 会先复检对象元数据,再执行内置 `metadata_rules` 安全扫描,检查 object key、MIME allowlist、文件大小、扩展名/MIME 是否匹配等规则。
|
||||
7. 扫描通过后,资源变为 `securityScanStatus=passed`,后台再调用 `PUT /api/tenant-content/assets` 将 `status=active` 发布。
|
||||
8. 学生端只能下载或预览 `active + uploadStatus=verified + securityScanStatus=passed` 的托管对象资源;后台管理员下载/预览也执行同一安全扫描门禁。
|
||||
9. 学生端和后台管理员下载/预览都会写入 `content_asset_access_events`,包含 `assetId/userId/accessType/result/expiresInSec/signatureMode/ip/userAgent` 等审计字段。
|
||||
10. 生产环境定时运行 assets worker,复检 `pending/verified` 托管对象的大小、MIME、SHA-256 等元数据。
|
||||
11. 如果复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致,worker 会把资源置为 `uploadStatus=failed`、`securityScanStatus=skipped`,并将 `active` 资源退回 `draft`,同时写入 `security_flags.assetRecheckFailed=true`、`content_asset_security_scan_events` 和 `audit_logs`。
|
||||
|
||||
## 题库导出生成资源
|
||||
|
||||
@@ -104,7 +112,7 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
2. API 校验租户内容权限、导出范围和答案/解析开关,创建 `content_export_jobs.status=pending`。
|
||||
3. `apps/worker --job exports` 抢占 pending job,复用后端导出 payload 规则渲染 PDF/Word,或为每日一练生成 ZIP 素材包;PDF/Word 会应用 `options.watermarkText` 水印,ZIP 包会包含 PNG/SVG 卡片、拼图、manifest 和脱敏 payload。
|
||||
4. worker 将二进制文件写入配置的对象存储。`local_dev` 会写入 `EXPORT_LOCAL_STORAGE_ROOT`,生产建议使用 `supabase_storage`、`aliyun_oss` 或 `tencent_cos`。
|
||||
5. worker 创建 `content_assets`,设置 `uploadStatus=verified`,并把 `assetId`、文件名、大小和 SHA-256 回填到 `content_export_jobs.output_metadata`。
|
||||
5. worker 创建 `content_assets`,设置 `uploadStatus=verified`、`securityScanStatus=passed`、`securityScanProvider=trusted_export_worker`,并把 `assetId`、文件名、大小和 SHA-256 回填到 `content_export_jobs.output_metadata`。
|
||||
6. 前端轮询 `GET /api/tenant-content/exports/jobs`,拿到 `assetId` 后通过 `POST /api/tenant-content/assets/sign-download` 或 `sign-preview` 下载/预览。
|
||||
|
||||
注意:PDF/Word/ZIP 文件本身不直接存入数据库,数据库只保存 job、资源台账和校验 metadata。`daily_practice_zip` 资源类型为 `package`,通常只做签名下载,不做 inline 预览。
|
||||
@@ -129,6 +137,8 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
- `supabase_storage` 使用 Storage list metadata 做最小存在性/大小校验。
|
||||
- 如果 provider 无法返回 SHA-256,后端会记录 `checksumUnavailable=true`,生产建议上传时写入对象自定义元数据,例如 `x-oss-meta-sha256` 或 `x-cos-meta-sha256`。
|
||||
- worker 的复检证据写入 `verification_details.assetWorker`,包含 `lastCheckedAt`、`lastResult`、`expected`、`observed`、`issues`,便于租户后台定位资源异常。
|
||||
- `publish=true` 只作为迁移期兼容参数保留。确认上传成功后仍然会保持草稿,必须等 `securityScanStatus=passed` 后再由后台显式发布。
|
||||
- 新上传托管对象常见状态流为:`draft/pending/pending` -> `draft/verified/pending` -> `draft/verified/passed` -> `active/verified/passed`。
|
||||
|
||||
## 安全规则
|
||||
|
||||
@@ -136,7 +146,8 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
- `objectKey` 默认必须以当前 `tenantId/` 开头,防止跨租户覆盖或读取。
|
||||
- 禁止 `..`、反斜杠、编码斜杠等危险 object key。
|
||||
- 上传会校验 MIME 类型和文件大小。
|
||||
- 托管对象资源未确认前不能发布为 `active`,学生端不可下载。
|
||||
- 托管对象资源未确认或安全扫描未通过前不能发布为 `active`,学生端不可下载。
|
||||
- 托管对象安全扫描失败会返回 `ASSET_SECURITY_SCAN_FAILED`;仍在等待扫描或扫描被跳过会返回 `ASSET_SECURITY_SCAN_REQUIRED`。
|
||||
- PDF/图片预览使用 `inline` 签名,不等同于长期公开 URL。
|
||||
- 下载、预览和视频播放必须先经过 API 权限判断,再下发短期签名 URL。
|
||||
- 学生端 `preview`、锁定资料下载、视频和资料包默认使用更短 TTL。当前策略是学生 inline 预览、`members/svip/private` 资料、`video/package` 资源最多 300 秒;租户后台预览最多 3600 秒,后台下载最多 86400 秒。
|
||||
@@ -144,7 +155,43 @@ npm --workspace @tiku-saas/worker run exports:once
|
||||
- 每次上传签名、上传确认、学生下载/预览、后台下载/预览都会写入 `content_asset_access_events`。授权失败也会记录 `result=denied` 和 `denyCode`,用于租户后台排查资源访问问题。
|
||||
- 云厂商 AccessKey、SecretKey、Service Role Key 只存在服务端环境变量,不返回前端。
|
||||
- `content_assets` 是资源唯一台账,前端不得绕过台账直接访问私有 bucket。
|
||||
- 前端不能把 `uploadStatus=failed` 或 `status=draft` 的资源继续展示为可下载;列表仍返回时应展示“资料处理中”或“资源异常已下架”,真正下载/预览会被后端拒绝。
|
||||
- 前端不能把 `uploadStatus=failed`、`securityScanStatus!=passed` 或 `status=draft` 的资源继续展示为可下载;列表仍返回时应展示“资料处理中”“安全扫描中”或“资源异常已下架”,真正下载/预览会被后端拒绝。
|
||||
|
||||
## 安全扫描字段
|
||||
|
||||
资源台账 `content_assets` 新增安全扫描状态:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `security_scan_status` | `not_required/pending/scanning/passed/failed/skipped` |
|
||||
| `security_scanned_at` | 最近一次扫描完成时间 |
|
||||
| `security_scan_provider` | 扫描来源,例如 `metadata_rules`、`trusted_export_worker` |
|
||||
| `security_scan_summary` | 风险等级、问题码、扫描证据和失败原因 |
|
||||
|
||||
当前内置 provider 是 `metadata_rules`。它不是完整杀毒引擎,但能阻断明显危险或不合规对象:跨租户 key、非法 object key、超限文件、MIME allowlist 外文件、扩展名/MIME 不匹配,以及测试/运营标记的强制失败。商用生产仍需继续接入真实 AV/内容安全 provider,并将结果写入同一张事件表。
|
||||
|
||||
扫描事件表:
|
||||
|
||||
```text
|
||||
public.content_asset_security_scan_events
|
||||
```
|
||||
|
||||
后台接口:
|
||||
|
||||
```text
|
||||
GET /api/tenant-content/assets/security-scan-events?assetId=<assetId>&limit=100
|
||||
```
|
||||
|
||||
事件字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `asset_id` | 资源 ID |
|
||||
| `provider` | 扫描 provider |
|
||||
| `scan_status` | `pending/scanning/passed/failed/skipped` |
|
||||
| `risk_level` | `none/low/medium/high/critical` |
|
||||
| `issue_codes` | 问题码数组,例如 `file_extension_mime_mismatch` |
|
||||
| `details` | observed/declared metadata、扩展名、错误信息等证据 |
|
||||
|
||||
## 访问事件字段
|
||||
|
||||
|
||||
Reference in New Issue
Block a user