reservoir-sandbox/backend
GitHub: reservoir-sandbox/backend
一个用于自动化 Linux ELF 恶意软件分析的后端服务,负责样本上传、分析任务编排与 Kubernetes worker 调度,为可扩展的恶意软件分析平台提供基础。
Stars: 1 | Forks: 0
# Reservoir
用于自动化 Linux 恶意软件分析的后端服务。
Reservoir 允许用户上传 ELF 二进制文件,将样本存储在兼容 S3 的存储中,管理分析作业和任务,并提供安全的基于 JWT 的身份验证。该项目作为可扩展恶意软件分析平台的后端基础,未来的 worker 将执行静态分析、沙箱执行和基于机器学习的分类。
## 架构概述
```
User
│
▼
FastAPI Backend
│
├── Authentication (JWT + Redis)
├── Sample Upload (ELF validation)
├── Job / Task Orchestration
├── Worker Callback API (internal)
│
├── PostgreSQL
├── Redis
└── S3 Storage
│
│ (1) backend launches one one-shot Job per task
▼
Kubernetes (Flux HelmRelease → Job)
┌───────────────┬───────────────┬───────────────┐
│ static worker │ sandbox worker│ ml worker │
└───────────────┴───────────────┴───────────────┘
│ (2) pull sample from S3, analyze, push report to S3
│ (3) POST result to backend callback (X-Worker-Token)
▼
FastAPI Backend ──► updates JobTask + recomputes Job status
```
**端到端流程**
1. 用户上传样本 → 后端将其存储在 S3 中,并创建一个包含三个 `JobTask`(STATIC、SANDBOX、ML)的 `Job`。
2. 后端通过 Flux `HelmRelease` 为每个任务启动一个 Kubernetes `Job`,并传递二进制文件的 S3 指针和回调 URL。
3. 每个 worker 从 S3 拉取二进制文件并进行分析,将报告上传到 S3(或返回一个小的内联 JSON 结果),然后向 backend callback endpoint 报告。
4. backend 持久化每个任务的结果并重新计算整个 `Job` 的状态。
## 关键特性
* 基于 JWT 的身份验证,支持 refresh token 轮换
* ELF 二进制文件上传与验证
* 基于 SHA256 的样本去重
* 兼容 S3 的对象存储集成
* Job 和 task 编排系统
* 为分析 worker 启动 Kubernetes job(Flux `HelmRelease`)
* 用于接收分析结果的内部 worker 回调 API
* PostgreSQL 和 Redis 集成
* 健康检查与就绪探针
* Docker 和 Docker Compose 部署
* 包含 Black、Ruff、Mypy 和 Pytest 的 CI pipeline
* 完全基于 FastAPI 和 SQLAlchemy 2.x 的异步架构
## 已实现功能
- 带有版本化 API 前缀 `/api/v1` 的 FastAPI 应用
- 带有 access/refresh token 轮换的 JWT 身份验证:
- 响应体中的 access token(15 分钟)
- `HttpOnly` cookie 中的 refresh token(30 天)
- 通过 Redis `GETDEL` 实现具有一次性使用语义的 refresh token 轮换
- 使用 Argon2 进行密码哈希
- 异步 SQLAlchemy + asyncpg
- Alembic 迁移设置
- 集中化异常处理 + HTTP 异常映射
- 速率限制 middleware(SlowAPI,可配置)
- CORS middleware
- 健康检查:liveness (`/health/live`) 和 readiness (`/health/ready`) 探测 PostgreSQL 和 Redis
- **样本(ELF 二进制文件)上传**:
- 验证 ELF magic bytes `\x7fELF`
- 文件大小限制(10 MB)
- SHA256 去重
- 上传到兼容 S3 的存储(MinIO / AWS S3)
- 将样本元数据存储在 PostgreSQL 中(size、sha256、object_name)
- 通过 `user_samples` 关联表跟踪用户所有权
- **Job 和 Task 管理**:
- 上传时,创建一个 `Job`(状态:pending)和三个 `JobTask` 实例(STATIC、SANDBOX、ML)
- 每个任务都有自己的状态生命周期(pending → running → completed/failed)
- Job 保留 started_at/finished_at 时间戳
- 每个 `JobTask` 可以存储分析结果:`report_object_name`(报告的 S3 key)和/或 `result`(小的内联 JSON)
- **分析作业编排**:
- 通过 `JOB_LAUNCHER` 设置选择可插拔的 `JobLauncher`:
- `noop` — 仅记录启动 payload(本地开发/测试)
- `k8s` — 每个任务创建一个 Flux `HelmRelease`(chart `charts/job-to-run`),以便 Flux 启动一个一次性的 Kubernetes `Job`
- Kubernetes 配置在集群内加载,并为本地调试提供 kubeconfig 后备
- launcher 在应用生命周期内构建一次(类似于 DB/Redis/S3 客户端)
- **Worker 回调 API** (`POST /api/v1/internal/tasks/{id}/callback`):
- 通过 `X-Worker-Token` header 使用共享密钥进行身份验证(恒定时间比较)
- 更新相应的 `JobTask`(状态、时间戳、报告指针/内联结果、错误)
- 重新计算父 `Job` 的状态,并从其任务中推导出 `started_at`/`finished_at`
- 并发回调在每个 job 中通过 `SELECT ... FOR UPDATE` 进行串行化,以避免 job 卡在 `running` 状态
- 使用 `aiobotocore` 的 S3 存储抽象(异步 S3 客户端)
- 基于 Redis 的 refresh token
- GitHub Actions CI:black、ruff、mypy、pytest
- 使用伪造 CRUD 实现的 `AuthService` 和 `UserService` 单元测试
- 生产级 Dockerfile(多阶段构建、非 root 用户、健康检查)
- 用于开发和生产的 Docker Compose 文件
## 技术栈
| 组件 | 技术 |
|------------------------|--------------------------------------------------|
| 语言 | Python 3.12+ |
| 框架 | FastAPI 0.136+ |
| 数据库 ORM | SQLAlchemy 2.x (异步) + asyncpg |
| 数据库 | PostgreSQL 16 |
| 缓存 / Token 存储 | Redis 7 (redis-py 异步) |
| 对象存储 | 兼容 S3 (通过 aiobotocore 使用 MinIO / AWS S3) |
| 作业编排 | Kubernetes + Flux (`HelmRelease`),通过 `kubernetes` 客户端 |
| 身份验证 | JWT (HS256),使用 httpOnly cookie |
| 密码哈希 | Argon2 (passlib) |
| Schema 验证 | Pydantic v2 (设置、模型、验证) |
| 数据迁移 | Alembic |
| 速率限制 | SlowAPI |
| 测试 | pytest, pytest-asyncio, fakeredis, httpx |
| Linting 和格式化 | Black, Ruff, Mypy |
| CI/CD | GitHub Actions |
| 容器化 | Docker, Docker Compose |
## 项目结构
```
app/
├── api/ # HTTP layer (incl. internal worker callback)
├── services/ # Business logic (incl. JobLauncher orchestration)
├── crud/ # Data access layer
├── models/ # Database models
├── schemas/ # API schemas
├── auth/ # Authentication & authorization
├── db/ # PostgreSQL, Redis, S3
├── dependencies/ # Dependency injection (incl. worker token auth)
├── core/ # Config, security, logging
└── utils/ # Shared helpers
tests/ # Unit tests
alembic/ # Database migrations
```
## 身份验证流程
1. **注册** (`POST /api/v1/register`) — 创建一个新的用户账户(用户名、电子邮件、密码)。密码在存储前使用 Argon2 进行哈希处理。
2. **登录** (`POST /api/v1/login`) — 验证凭据,在 JSON body 中返回 `access_token`,并在 `HttpOnly` cookie 中设置 `refresh_token`。refresh token 的 JTI 存储在 Redis 中(TTL 等于其生命周期)。
3. **访问受保护的 endpoint** — 使用 `Authorization: Bearer ` header。该 token 包含 `sub`(用户 ID)、`role` 和 `type`("access")。
4. **刷新** (`POST /api/v1/refresh`) — 当 access token 过期时,使用 `refresh_token` cookie 获取一对新的 token。旧的 refresh token 通过 Redis `GETDEL` 被消耗(一次性使用),并签发一个新的 `refresh_token`。
5. **登出** (`POST /api/v1/logout`) — 撤销当前的 refresh token(从 Redis 中删除)并清除 cookie。
### Token 详情
| Token | 位置 | 生命周期 | 包含 |
|-----------------|----------------|-----------------|------------------------------------------|
| `access_token` | 响应体 | 15 分钟(默认) | `sub`, `role`, `type`, `exp`, `iat`, `jti` |
| `refresh_token` | HttpOnly cookie| 30 天(默认) | `sub`, `type`, `exp`, `iat`, `jti` |
## 样本上传与分析 Pipeline 流程
1. **用户上传文件** (`POST /api/v1/samples`,multipart,需要 Bearer token)
2. **验证** — 检查:
- 文件大小 ≤ 10 MB
- Magic bytes = `\x7fELF`(ELF 二进制文件)
3. 计算文件内容的 **SHA256 哈希**
4. **去重** — 检查具有相同 SHA256 的样本是否已存在于数据库中
- 如果 **存在**:
- 检查此用户是否已拥有指向此样本的链接(通过 `user_samples` 表)。如果没有,则创建一个新的 `UserSample` 记录。
- 如果该样本有 **最近的(pending/running/completed)job**,则返回该 job(避免重复分析)。
- 否则,为现有样本创建一个新的 `Job`。
- 如果 **不存在**:
- 将文件上传到 S3 bucket,key 为 `uploads/{sha256}`
- 在 PostgreSQL 中创建 `Sample` 记录
- 创建 `UserSample` 记录
- 创建一个新的包含三个 `JobTask` 记录(STATIC、SANDBOX、ML)的 `Job`
5. **响应** 返回 `Job` 对象及其当前状态(`pending`)和相关时间戳。
6. **作业启动** — 对于每个新创建的 `Job`,backend 通过配置的 `JobLauncher` 为每个 `JobTask` 启动一个 Kubernetes job(见下文)。此启动发生在事务提交之后,以便 worker 可以立即进行回调。
### 分析作业编排
backend 本身不运行分析;它将每个 `JobTask` 委托给 Kubernetes 中的一个短期 worker container。
- 启动在 `JobLauncher` 之后进行抽象,并由 `JOB_LAUNCHER` 环境变量选择:
- `noop`(默认) — 仅记录启动 payload;非常适合本地开发和测试(不需要集群)。
- `k8s` — 每个任务创建一个 Flux `HelmRelease`(chart `charts/job-to-run`);然后 Flux 将其协调为一个一次性的 Kubernetes `Job`。
- 对于每个任务,backend 会将这些值发送到 chart 中:
| 值 | 来源 | 描述 |
|-------|--------|-------------|
| `taskId` | `JobTask.id` | 标识任务;在回调 URL 中返回 |
| `taskType` | `static` / `sandbox` / `ml` | 让 chart 选择 worker 镜像 |
| `backendCallbackUrl` | `BACKEND_CALLBACK_URL` | 回调的基础 URL |
| `s3EndpointUrl` | `S3_ENDPOINT_URL` | S3 endpoint |
| `s3BucketName` | `S3_BUCKET_NAME` | S3 bucket |
| `objectKey` | `Sample.object_name` (`uploads/{sha256}`) | 二进制文件的 S3 key |
| `sha256` | `Sample.sha256` | 样本哈希 |
### Worker 回调协议
当一个 worker 完成时,它必须报告回来:
```
curl -X POST "$BACKEND_CALLBACK_URL/api/v1/internal/tasks/$TASK_ID/callback" \
-H "X-Worker-Token: " \
-H "Content-Type: application/json" \
-d '{"status":"completed","report_object_name":"reports//static.json"}'
```
body 可以携带报告指针(`report_object_name`)、一个小的内联 `result` 对象,或一个 `error`(带有 `"status":"failed"`);`started_at`/`finished_at` 是可选的 ISO-8601 时间戳。
### Job / Task 模型
- **Job** — 表示样本的一次分析运行。状态生命周期:`pending` → `running` → `completed` / `failed`。`started_at`/`finished_at` 派生自其任务。
- **JobTask** — 作业内的单个分析步骤。类型:`STATIC`、`SANDBOX`、`ML`。每个任务都有自己的状态、开始/结束时间、可选的错误消息以及结果字段(`report_object_name`、`result`)。
- Task 和 job 的状态通过 worker 向内部回调 endpoint 报告来推进。同一个 job 的并发回调通过行锁进行串行化。
## API Endpoints
### 概述
| 方法 | 路径 | 认证 | 描述 |
|--------|-------------------------------|------------------|----------------------------------------------------|
| GET | `/health/live` | 无 | Liveness 探针 |
| GET | `/health/ready` | 无 | Readiness 探针(检查 PostgreSQL + Redis) |
| POST | `/api/v1/register` | 无 | 注册新用户 |
| POST | `/api/v1/login` | 无 | 登录(用户名/密码表单) |
| POST | `/api/v1/refresh` | 无 | 轮换 refresh token,签发新的 access token |
| POST | `/api/v1/logout` | 无 | 撤销 refresh token,清除 cookie |
| GET | `/api/v1/about_me` | Bearer | 获取当前用户的个人资料 |
| GET | `/api/v1/samples` | Bearer | 列出当前用户拥有的所有样本 |
| POST | `/api/v1/samples` | Bearer | 上传 ELF 二进制文件以进行分析 |
| DELETE | `/api/v1/samples/{id}` | Bearer | 删除用户-样本链接(移除) |
| GET | `/api/v1/jobs/{id}` | Bearer | 通过 job ID 获取 job 详情(包含任务) |
| POST | `/api/v1/internal/tasks/{id}/callback` | Worker token | 报告任务的分析结果(内部) |
### Endpoint 详情
#### `GET /health/live`
简单的 liveness 检查。无需身份验证。
**示例请求:**
```
curl -X GET "http://127.0.0.1:8000/health/live"
```
**示例响应(200 OK):**
```
{
"status": "ok"
}
```
#### `GET /health/ready`
Readiness 探针。验证与 PostgreSQL 和 Redis 的连接。
**示例请求:**
```
curl -X GET "http://127.0.0.1:8000/health/ready"
```
**示例响应(200 OK):**
```
{
"status": "ok",
"services": {
"postgres": "ok",
"redis": "ok"
}
}
```
**当服务不可用时(503 Service Unavailable)的示例响应:**
```
{
"status": "error",
"services": {
"postgres": "error",
"redis": "ok"
}
}
```
#### `POST /api/v1/register`
创建一个新的用户账户。
**请求体** — JSON (`UserRegister` schema):
| 字段 | 类型 | 约束 | 描述 |
|----------|----------|---------------|-----------------|
| username | string | 4–24 个字符 | 自动转为小写 |
| email | string | 合法邮箱 | 用户的电子邮件地址 |
| password | string | 8–24 个字符 | 明文密码(使用 Argon2 进行哈希处理) |
**示例请求:**
```
curl -X POST "http://127.0.0.1:8000/api/v1/register" \
-H "Content-Type: application/json" \
-d '{"username":"john_doe","email":"john@example.com","password":"StrongPass123"}'
```
**示例响应(201 Created):**
```
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"is_active": true,
"role": "user",
"created_at": "2025-03-28T12:00:00",
"updated_at": "2025-03-28T12:00:00"
}
```
**可能出现的错误:**
- `409 Conflict` — 用户名或电子邮件已存在。
#### `POST /api/v1/login`
使用 `application/x-www-form-urlencoded`(标准 OAuth2 密码流程)通过用户名和密码进行身份验证。
设置名为 `refresh_token` 的 `HttpOnly` cookie,并在响应体中返回 access token。
**请求体** — 表单数据:
| 字段 | 类型 | 描述 |
|----------|--------|--------------------|
| username | string | 用户名 |
| password | string | 用户的密码 |
**示例请求:**
```
curl -i -X POST "http://127.0.0.1:8000/api/v1/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-c cookies.txt \
-d "username=john_doe&password=StrongPass123"
```
**示例响应(200 OK) — Headers 将包含带有 refresh token 的 `Set-Cookie`:**
```
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer"
}
```
**可能出现的错误:**
- `401 Unauthorized` — 凭据无效。
#### `POST /api/v1/refresh`
使用有效的 refresh token(来自 cookie)换取新的 access/refresh 对。要求存在 `refresh_token` cookie。旧的 refresh token 将失效(一次性使用)。
**示例请求(使用之前登录的 cookie):**
```
curl -i -X POST "http://127.0.0.1:8000/api/v1/refresh" \
-b cookies.txt -c cookies.txt
```
**示例响应(200 OK):**
```
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer"
}
```
**可能出现的错误:**
- `401 Unauthorized` — 缺少、过期或无效的 refresh token。
#### `POST /api/v1/logout`
从 Redis 撤销当前的 refresh token 并清除 `refresh_token` cookie。
**示例请求:**
```
curl -i -X POST "http://127.0.0.1:8000/api/v1/logout" \
-b cookies.txt -c cookies.txt
```
**示例响应(200 OK):**
```
{
"message": "logout successfully!"
}
```
#### `GET /api/v1/about_me`
获取当前已认证用户的资料。
**Headers:** `Authorization: Bearer `
**示例请求:**
```
curl -X GET "http://127.0.0.1:8000/api/v1/about_me" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
```
**示例响应(200 OK):**
```
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"is_active": true,
"role": "user",
"created_at": "2025-03-28T12:00:00",
"updated_at": "2025-03-28T12:00:00"
}
```
**可能出现的错误:**
- `401 Unauthorized` — 缺少或无效的 token。
#### `GET /api/v1/samples`
列出当前用户拥有的所有样本。返回一个 `SampleListItem` 对象数组。
**Headers:** `Authorization: Bearer `
**示例请求:**
```
curl -X GET "http://127.0.0.1:8000/api/v1/samples" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
```
**示例响应(200 OK):**
```
[
{
"sample_id": 1,
"filename": "malware.elf",
"uploaded_at": "2025-03-28T12:05:00",
"latest_job_id": 1,
"latest_job_status": "pending"
},
{
"sample_id": 2,
"filename": "benign.elf",
"uploaded_at": "2025-03-28T12:10:00",
"latest_job_id": 2,
"latest_job_status": "completed"
}
]
```
**可能出现的错误:**
- `401 Unauthorized` — 缺少或无效的 token。
#### `POST /api/v1/samples`
上传 ELF 二进制文件以进行分析。文件将验证 ELF magic bytes,按 SHA256 去重,并存储在 S3 中。如果样本是新的,将创建一个带有三个任务的新 Job;否则可能返回现有的 Job。
**Headers:** `Authorization: Bearer `
**Body:** `multipart/form-data`,字段名为 `sample`
**示例请求:**
```
curl -X POST "http://127.0.0.1:8000/api/v1/samples" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-F "sample=@/path/to/malware.elf"
```
**示例响应(201 Created):**
```
{
"id": 1,
"sample_id": 1,
"status": "pending",
"created_at": "2025-03-28T12:15:00",
"started_at": null,
"finished_at": null
}
```
**可能出现的错误:**
- `400 Bad Request` — 文件不是 ELF(magic bytes 不匹配)。
- `413 Payload Too Large` — 文件超过 10 MB。
- `401 Unauthorized` — 缺少或无效的 token。
#### `DELETE /api/v1/samples/{id}`
删除当前用户与样本之间的链接(移除所有权)。样本本身(S3 对象 + 数据库记录)**不会**被删除。
**Headers:** `Authorization: Bearer `
**路径参数:** `id` — 整数,列表中的 `sample_id`。
**示例请求:**
```
curl -X DELETE "http://127.0.0.1:8000/api/v1/samples/1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
```
**示例响应(204 No Content):** — 空 body。
**可能出现的错误:**
- `404 Not Found` — 样本不存在或不属于此用户。
- `401 Unauthorized` — 缺少或无效的 token。
#### `GET /api/v1/jobs/{id}`
获取特定 job 的详细信息,包括其任务(`JobTaskRead` 列表)。
**Headers:** `Authorization: Bearer `
**路径参数:** `id` — 整数,job ID。
**示例请求:**
```
curl -X GET "http://127.0.0.1:8000/api/v1/jobs/1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
```
**示例响应(200 OK):**
```
{
"id": 1,
"sample_id": 1,
"status": "pending",
"created_at": "2025-03-28T12:15:00",
"started_at": null,
"finished_at": null,
"tasks": [
{
"id": 1,
"job_id": 1,
"task_type": "STATIC",
"status": "pending",
"report_object_name": null,
"result": null,
"created_at": "2025-03-28T12:15:00",
"started_at": null,
"finished_at": null,
"error": null
},
{
"id": 2,
"job_id": 1,
"task_type": "SANDBOX",
"status": "pending",
"report_object_name": null,
"result": null,
"created_at": "2025-03-28T12:15:00",
"started_at": null,
"finished_at": null,
"error": null
},
{
"id": 3,
"job_id": 1,
"task_type": "ML",
"status": "pending",
"report_object_name": null,
"result": null,
"created_at": "2025-03-28T12:15:00",
"started_at": null,
"finished_at": null,
"error": null
}
]
}
```
**可能出现的错误:**
- `404 Not Found` — job 不存在或不属于此用户。
- `401 Unauthorized` — 缺少或无效的 token。
#### `POST /api/v1/internal/tasks/{id}/callback`
供分析 worker 报告任务结果的内部 endpoint。它**不**属于面向用户的 API,并使用共享密钥而非 JWT 进行身份验证。
**Headers:** `X-Worker-Token: `(必须等于 `WORKER_CALLBACK_SECRET`)
**路径参数:** `id` — 整数,`JobTask` ID(作为 `taskId` 传递给 worker)。
**请求体** — JSON (`TaskCallback` schema):
| 字段 | 类型 | 必需 | 描述 |
|----------------------|-----------------|----------|--------------------------------------------------|
| status | string | 是 | `completed` 或 `failed` |
| report_object_name | string \| null | 否 | 上传报告的 S3 key |
| result | object \| null | 否 | 小的内联 JSON 结果/结论 |
| error | string \| null | 否 | 错误消息(通常带有 `status=failed`) |
| started_at | datetime \| null| 否 | ISO-8601 UTC 分析开始时间 |
| finished_at | datetime \| null| 否 | ISO-8601 UTC 分析结束时间 |
**示例请求:**
```
curl -X POST "http://backend/api/v1/internal/tasks/1/callback" \
-H "X-Worker-Token: " \
-H "Content-Type: application/json" \
-d '{"status":"completed","report_object_name":"reports//static.json"}'
```
**示例响应(204 No Content):** — 空 body。
**可能出现的错误:**
- `403 Forbidden` — 缺少或无效的 `X-Worker-Token`。
- `404 Not Found` — 任务不存在。
- `422 Unprocessable Entity` — 无效的 body(例如 `status` 不是 `completed`/`failed`)。
## 错误模型
所有应用特定的错误都会返回一个包含 `{"detail": "..."}` 的 JSON body。
### 常见 HTTP 状态码
| 状态码 | 错误 | 描述 |
|--------|--------------------------------|--------------------------------------------|
| 200 | OK | 操作成功 |
| 201 | Created | 资源已创建(注册、上传样本) |
| 204 | No Content | 资源删除成功 |
| 400 | Bad Request | 无效的文件格式(非 ELF) |
| 401 | Unauthorized | 凭据无效/token 过期或无效 |
| 403 | Forbidden | 拒绝访问(角色权限不足) |
| 404 | Not Found | 未找到用户或未找到资源 |
| 409 | Conflict | 用户已存在 |
| 413 | Payload Too Large | 文件超过最大大小 (10 MB) |
| 429 | Too Many Requests | 超出速率限制 |
| 500 | Internal Server Error | 意外的服务器错误 |
## 快速开始
### 1. 前置条件
- Python 3.12+
- Poetry 2.0+
- PostgreSQL 16 正在运行
- Redis 7 正在运行
- 兼容 S3 的存储(例如 MinIO、AWS S3)
### 2. 安装 Poetry
```
pipx install poetry
```
### 3. 安装依赖
```
poetry install --with dev
```
### 4. 配置环境
```
cp .env.template .env
```
根据你的配置编辑 `.env`。最低要求的变量:
| 变量 | 必需 | 描述 |
|------------------------|----------|------------------------------------------|
| `DATABASE_URL` | 是 | `postgresql+asyncpg://user:pass@host:5432/dbname` |
| `REDIS_URL` | 是 | `redis://host:6379/0` 或 `rediss://...` |
| `ACCESS_SECRET` | 是 | 最少 32 个字符,用于签发 access token |
| `REFRESH_SECRET` | 是 | 最少 32 个字符,用于签发 refresh token |
| `S3_ACCESS_KEY` | 是 | S3 access key |
| `S3_SECRET_KEY` | 是 | S3 secret key |
| `S3_ENDPOINT_URL` | 是 | S3 endpoint(例如,用于 MinIO 的 `http://localhost:9000`) |
| `S3_BUCKET_NAME` | 是 | S3 bucket 名称 |
| `BACKEND_CALLBACK_URL` | k8s 需要 | worker 用于访问回调 API 的基础 URL |
| `WORKER_CALLBACK_SECRET`| k8s 需要 | 通过 `X-Worker-Token` 验证的共享密钥(建议最少 32 个字符) |
可选变量:`DEBUG`, `CORS_ORIGINS`, `ACCESS_TOKEN_EXPIRE_M`, `REFRESH_TOKEN_EXPIRE_M`, `COOKIE_SECURE`, `COOKIE_SAMESITE`, `ENGINE_VERSION`, `JOB_LAUNCHER` (`noop` | `k8s`,默认为 `noop`),`JOBS_NAMESPACE`(默认为 `jobs`)。
### 5. 运行数据库迁移
```
poetry run alembic upgrade head
```
### 6. 启动应用
```
poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
Swagger UI:`http://127.0.0.1:8000/docs` \
ReDoc:`http://127.0.0.1:8000/redoc`
## 快速开始
### 开发环境
1. 使用真实的密钥更新 `.env.docker`(参见上面的环境变量)。
2. 启动服务:
```
docker compose up --build
```
3. 运行迁移:
```
docker compose run --rm app alembic upgrade head
```
Makefile 快捷方式:
```
make up # docker compose up --build
make down # docker compose down
make migrate # run migrations
make makemigrations m="message" # create new migration
make logs # follow logs
```
### 生产环境
对于生产环境部署(例如在 VPS 上),请使用提供的生产环境 compose 文件:
```
docker compose -f docker-compose.prod.yml up -d --build
```
此 compose 期望从 GitHub Container Registry 获取预构建的镜像,并正确填写 `.env` 文件。
## 测试
运行所有测试:
```
poetry run pytest
```
仅运行单元测试:
```
poetry run pytest tests/unit
```
运行测试覆盖率(如果安装了 `coverage`):
```
poetry run coverage run -m pytest && poetry run coverage report
```
测试套件使用 **伪造实现**(例如 `FakeUserCRUD`)来避免外部依赖,并使用 `fakeredis` 进行 Redis 模拟。
## 开发工具
使用 Black 格式化代码:
```
poetry run black .
```
使用 Ruff lint 代码:
```
poetry run ruff check .
```
使用 Mypy 检查类型:
```
poetry run mypy app
```
## 数据迁移
创建新迁移:
```
poetry run alembic revision --autogenerate -m "describe change"
```
升级到最新版本:
```
poetry run alembic upgrade head
```
回退一个版本:
```
poetry run alembic downgrade -1
```
## 当前局限性
- **没有监控堆栈** — 当前代码库中未部署 Prometheus、Grafana、ELK。
## 许可证
MIT
## 作者
Rushan Shafeev — [GitHub](https://github.com/Lntck)
标签:AV绕过, DAST, ELF分析, FastAPI, 任务编排, 后端开发, 子域名突变, 恶意软件分析, 搜索引擎查询, 测试用例, 请求拦截, 逆向工具