clwg/blacklight
GitHub: clwg/blacklight
一个自托管的安全运营平台,将 IoC 追踪、资产与漏洞管理以及事件响应整合于同一 Django 站点。
Stars: 0 | Forks: 0
# Blacklight
一个自托管的安全运营站点。追踪 indicators 并将其转化为检测内容,维护你所拥有的资产及其问题的清单,并在同一处运行 incidents。
- **Indicators** - 包含严重程度、置信度、TLP 和 ATT&CK 上下文的 IoCs,以及可将它们渲染为 Suricata、Snort 2、YARA、blocklists、hosts 文件、DNS RPZ 或 CSV 的 rulesets。
- **Assets** - 资产清单、每台主机上实际响应的服务,以及 vulnerability findings。
- **Incidents** - 具备 NIST 风格生命周期的响应案例、自动记录的 timeline、任务清单,以及指向相关 assets 和 indicators 的链接。
其底层是一个纯粹的 Django 站点:服务端渲染页面结合 htmx 实现交互、基于相同权限的 REST API,以及在站点 UI 中进行用户管理,而非使用 Django admin(默认已关闭)。没有构建步骤,没有 CDN,也没有 JavaScript 工具链 —— htmx 仅仅是一个本地化的文件。

## 技术栈
| | |
|---|---|
| Python | 3.14 (3.12+ 可用) |
| Django | 6.0.7 |
| Django REST Framework | 3.17.1 |
| drf-spectacular | 0.30.0,包含其 sidecar,因此 Swagger UI 和 Redoc 从 `/static/` 加载 |
| htmx | 2.0.10,本地化于 `static/js/htmx.min.js` |
| 数据库 | PostgreSQL 18 (也可使用 SQLite) |
## 快速开始
### 使用 Docker
```
cp .env.example .env
# 在 .env 中设置 DJANGO_DB_PASSWORD
docker compose up --build
```
打开 http://127.0.0.1:8000/。数据库启动后,应用会等待它就绪,随后运行 migrations,并启动绑定了你源代码的开发服务器,因此编辑后会自动重载。
以下每个 `manage.py` 命令都可以在容器中运行,只需在前面加上 `docker compose exec web`:
```
docker compose exec web python manage.py createsuperuser
```
### 不使用 Docker
你需要一台 PostgreSQL 服务器;在 `.env` 中将 `DJANGO_DB_*` 指向它。
```
cp .env.example .env
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
```
如果想完全跳过数据库服务器 —— 这对于初次了解或在笔记本电脑上运行测试非常方便:
```
export DJANGO_DB_ENGINE=sqlite DJANGO_DB_NAME=db.sqlite3
python manage.py migrate
python manage.py runserver
```
### 示例数据
两个 seed 命令,重复执行都是安全的:
| 命令 | 创建内容 |
|---|---|
| `seed_demo` | 一个 `admin` 超级用户以及分布在三个组中的 40 个示例用户 |
| `seed_security` | 三个安全权限组,以及示例 indicators、rulesets、assets、vulnerabilities、findings 和 incidents |
| `seed_security --groups-only` | 仅创建权限组,不包含其他内容 |
要在 Docker 下从一个空数据库变为包含数据的演示(entrypoint 已经应用了 migrations):
```
docker compose exec web python manage.py seed_demo # sign in as 'admin' / 'demo-passphrase-2024'
docker compose exec web python manage.py seed_security # sample security data
```
或者不使用 Docker:
```
python manage.py migrate
python manage.py seed_demo # sign in as 'admin' / 'demo-passphrase-2024'
python manage.py seed_security # sample security data
```
示例 indicators 使用的是 RFC 5737 文档地址和 `example.com` 主机,因此里面的任何内容都不会解析到真实地址。示例用户共用一个密码,因此这仅供开发使用 —— 一旦你有了真实数据,请删除 `core/management/commands/seed_demo.py`。
对于真实安装,请创建你自己的登录账号并使用这些组,但不要带上示例数据。使用 Docker:
```
docker compose exec web python manage.py createsuperuser
docker compose exec web python manage.py seed_security --groups-only
```
或者不使用:
```
python manage.py createsuperuser
python manage.py seed_security --groups-only
```
## 使用说明
### Indicators - `/indicators/`
追踪 observables(IPs、CIDRs、domains、URLs、hashes、JA3、user agents、mutexes、registry keys、file names),包含严重程度、置信度、TLP、ATT&CK 技术、kill chain 阶段、来源可靠性以及过期日期。
值在保存时会被规范化,因此 `EVIL.COM`、`evil.com.` 和 `evil[.]com` 会合并为一行而不是三行。一个批量粘贴界面会为你自动识别每行的类型,因此从报告中复制的一整块文本可以一次性归档。
一旦 indicators 过期、被废弃或其来源被关闭,它们就会从所有 rulesets 中剔除。`Indicator.objects.actionable()` 是对“我们是否还应该对此进行拦截”的唯一界定,并且一个 cron job 会保持状态列同步:
```
python manage.py expire_indicators
```
### Rulesets - `/indicators/rulesets/`
一个 ruleset 是一个保存的查询加上一种输出格式,而不是存储的文本。每次被请求时,它都会从当前的 indicator 集合重新渲染,因此今天添加的 indicator 会出现在选择它的每个 ruleset 中 —— 不存在会被遗忘的重新生成步骤。
| 格式 | 可表达的 Indicator 类型 |
|---|---|
| Suricata | IPs、CIDRs、domains、URLs、user agents、JA3 |
| Snort 2 | IPs、CIDRs、domains、URLs、user agents |
| YARA | MD5/SHA-1/SHA-256、file names、mutexes、registry keys、patterns |
| IP / domain / URL blocklist | 匹配的地址或名称类型 |
| Hosts 文件 | Domains |
| DNS RPZ zone | Domains、IPs、CIDRs |
| CSV | 所有类型 |
选择某种格式无法表达的类型并不是一个错误 —— 它会被直接丢弃,因此一组标准可以跨多种格式重用。一个 domain 会生成三条 Suricata 规则(DNS 查询、TLS SNI、HTTP host);Snort 2 没有解码的 DNS 缓冲区,因此那里的 domain 会作为 wire-format bytes 匹配(`|04|evil|03|com|00|`)。
Signature IDs 格式为 `sid_base + indicator_id × 10 + variant`。一个 indicator 终生保留其 SIDs,这正是让 `suppress` 和 `threshold` 调优在重新生成后依然有效的原因。
**Feed URLs。** 每个 ruleset 在 `/indicators/rulesets/
/export/` 导出。拥有 `indicators.view_ruleset` 权限的已登录会话可以获取它。对于无人值守的 sensor,从 ruleset 页面签发一个导出 token 并追加 `?token=…`。该 token 使用恒定时间比较,并且永远不会被 API 返回 —— 请将其视为凭据并通过 HTTPS 提供服务。
### Assets 和 findings - `/assets/`
包含严重程度和暴露面的资产清单、每台主机上响应的服务,以及一个 triage 得分(结合了最严重的开放 finding、业务关注程度以及该资产的可达性)。
一个 `Vulnerability` 是抽象的缺陷,只存储一次。一个 `Finding` 是特定 asset 上的该缺陷,拥有自己的状态、所有者和 remediation 计时器。手动记录一个 finding 时,会搜索 asset、vulnerability 和 port,而不是提供每个表的下拉菜单,并且会即时创建这三者中的任何一个。
### Scanner 导入
一个导入器,三个入口点,结果完全相同 —— 命令行、`/assets/findings/import/` 的上传表单,或者 `POST /api/findings/import/`。
```
python manage.py import_findings scan.csv --scanner nessus
cat scan.json | python manage.py import_findings -
```
导入是幂等的:finding 以 `(asset, vulnerability)` 作为键,因此重新运行昨天的扫描会更新现有内容,而不是堆积重复项。严重程度和证据会从扫描中刷新;triage 状态则不会,因为人类可能在此期间已经接受或驳回了该 finding。
CSV 表头和 JSON 键在匹配时不区分大小写并忽略标点符号,对照一个包含常见 Nessus、Qualys、OpenVAS 和 Trivy 拼写的别名表。每行只需满足两点:标识 asset 的内容(`asset`、`hostname` 或 `ip`)和标识缺陷的内容(`identifier`、`cve` 或 `plugin_id`)。无法理解的行会被跳过并报告;文件的其余部分仍会加载。完整的别名表会显示在导入页面上。
### Incidents - `/incidents/`
带有 `INC-YYYY-NNNN` 引用和 triage -> investigating -> contained -> eradicated -> recovering -> resolved 生命周期的响应案例。状态变更会自动写入 timeline。Assets、indicators 和 findings 是通过搜索而不是下拉菜单拉入 incident 范围的,并且 **New finding** 会记录一个针对该 incident 的 finding,并随之将其 asset 纳入范围。
### 账号和用户管理
**你自己的账号** - `/accounts/profile/`。编辑你的姓名和电子邮件(通过 htmx 保存,无需页面重载)、更改密码、查看你拥有的组和权限,以及签发或撤销你的 API token。通过电子邮件重置密码的功能已接通,并在开发环境中打印到控制台。
**管理他人** - `/users/`。可搜索的用户列表、创建、编辑、删除、设置密码、内联激活/停用,以及具有按应用分组的权限选择器的分组管理。
## 权限
用户管理需要 Django admin 同样要求的两项内容:一个活跃的 **staff** 账号以及相关的模型权限(`accounts.view_user`、`auth.add_group`、...)。
安全模块有意 **不需要 staff 权限**。一个整天处理 incidents 的分析师并不是站点的管理员,因此这些界面仅通过模型权限进行控制(`indicators.change_indicator`、`incidents.add_incident`、`assets.view_finding`、...)。这个区别体现在 `core/mixins.py` 中。
`seed_security --groups-only` 创建三个组作为起点:
| 组 | 能做什么 |
|---|---|
| 安全分析师 | 完整的 indicator、asset 和 finding 管理;incident 处理 |
| Incident 响应者 | 完整的 incident 管理,添加 indicators,读取 assets |
| 安全只读 | 查看所有内容,不能修改任何内容 |
按用户或通过组授予权限;两者都通过此 UI 生效。导航栏仅显示当前用户可用的链接,并且仪表板仅计算允许他们查看的面板。
### 防护措施
Django admin 允许任何拥有 `change_user` 权限的 staff 用户为自己授予超级用户状态。这些界面关闭了该漏洞以及其他几个相关的漏洞 —— 这也是 admin 被关闭的部分原因,因为如果挂载了它,下方的每条防护措施都有第二道后门。每一项都是几行代码,如果你想要严格的权限对等,可以安全地删除。
| 防护措施 | 位置 |
|---|---|
| 只有超级用户才能授予超级用户状态 | `usermgmt/forms.py`、`api/serializers.py` |
| 非超级用户不能编辑、删除或设置超级用户的密码 | `usermgmt/views.py` 中的 `ProtectSuperusersMixin` |
| 任何人不得移除自己的 active/staff/superuser 标志 | `AdminUserChangeForm.__init__` |
| 任何人不得删除或停用自己的账号 | `usermgmt/views.py`、`api/views.py` |
| 电子邮件地址必须唯一 | `accounts/forms.py` 中的 `UniqueEmailMixin` |
| API 读取需要 `view` 权限 | `api/permissions.py` 中的 `StrictDjangoModelPermissions` |
最后一项与 DRF 的默认设置不同,后者的 `DjangoModelPermissions` 将 GET 映射为完全不需要权限 —— 这意味着任何经过身份验证的用户都可以读取整个用户目录。
## API
两种身份验证方式,都解析为同一个 Django 用户,因此也解析为 UI 检查的相同模型权限。API 访问和 UI 访问不会产生偏差。
**Session cookie。** 站点已经使用的登录方式。只要你登录,`/api/` 的可浏览 API 和文档页面就能正常工作。
**Token。** 每个账号一个密钥,作为标头发送。没有 CSRF,因此可以在脚本中使用:
```
curl -H "Authorization: Token 5ab5718c9543a12772816ef73677e2416ee7e7ee" \
https://example.org/api/me/
```
从 **Access**(`/accounts/profile/security/`)签发一个,或者通过交换凭据获取:
```
curl -X POST https://example.org/api/token/ -d username=analyst -d password=...
# {"token": "5ab5718c9543a12772816ef73677e2416ee7e7ee"}
```
该 endpoint 返回账号现有的 token,而不是生成一个新的,因此第二个请求密钥的客户端不会使第一个客户端的密钥失效。它是此处唯一接收密码的未经身份验证的 endpoint,因此受速率限制 —— `DJANGO_API_TOKEN_RATE`,默认为每小时 10 次。
token 携带其所有者拥有的所有权限,并且仅在撤销时过期。在 Access 页面上重新生成或撤销你自己的 token;要切断另一个账号,请停用它 —— `is_active=False` 会像拒绝登录一样拒绝 token。
有意缺失的特性:过期时间、scopes、哈希存储以及每个账号多个密钥。DRF 的 token 模型不具备这些。如果你需要它们,可以将 `djangorestframework-simplejwt` 或你自己的 token 模型插入到
`REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]` 中,与现有的 session authentication 并列。
### Endpoints
对 `users`、`groups`、`indicators`、`indicator-sources`、`rulesets`、`assets`、`services`、`vulnerabilities`、`findings`、`incidents`、`incident-events`、`incident-tasks` 和 `tags` 提供完整的 CRUD,此外还有:
| Endpoint | 说明 |
|---|---|
`GET /api/permissions/` | 只读目录 |
| `GET/PATCH /api/me/` | 任何已登录的用户;权限字段为只读 |
| `POST /api/users/{id}/set-password/` | `{"password": "..."}` |
| `POST /api/indicators/bulk/` | 归档一批数据;按条目检测类型,具备幂等性 |
| `POST /api/indicators/{id}/touch/` | 记录再次观察到该 indicator |
| `GET /api/rulesets/{id}/render/` | 生成的内容及计数 |
| `GET /api/rulesets/{id}/indicators/` | 该 ruleset 当前选择的 indicators |
| `POST /api/findings/import/` | 直接将 scanner 行推送进来 |
| `GET /api/assets/{id}/findings/` | 限定在单个 asset 范围内的 findings |
| `POST /api/incidents/{id}/status/` | 推进生命周期,写入 timeline |
| `GET/POST /api/incidents/{id}/events/` | 读取或追加 timeline |
实用的过滤器:`?actionable=1`、`?min_severity=3`、`?tag=c2`、`?open=1`、`?overdue=1`、`?in_kev=1`、`?assigned_to_me=1`。
批量 endpoint 接受纯字符串或对象,带有共享的默认值:
```
{
"defaults": {"severity": 3, "confidence": 80, "tags": [1]},
"indicators": [
"evil[.]com",
"203.0.113.10",
{"type": "sha256", "value": "e3b0c442...", "severity": 4}
]
}
```
两个导入 endpoint 在所有数据成功落地时返回 `200`,在部分行被跳过时返回 `207`,并在响应体中附带原因 —— 这样夜间作业就可以区分“无事可做”和“你的一半数据源格式错误”。
密码是只写的,通过配置的验证器进行处理并哈希化。ruleset 的 `export_token` 绝不会被序列化。
### 文档
| URL | 内容 |
|---|---|
| `/api/docs/` | Swagger UI。Try-it-out 可在你的会话上使用 |
| `/api/redoc/` | 作为参考文档的相同 schema |
| `/api/schema/` | OpenAPI 3 文档 —— `?format=json` 或 `?format=yaml` |
| `/api/` | 可浏览的 API |
该 schema 由 drf-spectacular 在每次请求时根据 viewsets、serializers 和 permission classes 生成,因此它描述的是你当前正在查看的部署环境。`api/tests_schema.py` 会对其进行验证,并在声明的过滤器丢失时报错。
所有四个页面都使用站点的导航栏、样式表和浅色/深色主题。Swagger UI 和 Redoc 从 `/static/` 提供服务,因此文档在隔离网络上也能正常工作。
## 配置
设置从环境中读取,并在项目根目录下可选的 `.env` 文件下方叠加一层 —— 真实的环境变量始终优先。有关每个支持的键,请参阅 `.env.example`。Compose 在进行 `${...}` 替换时会读取同一文件并将其传递给应用,因此只有一个事实来源。
| 键 | 默认值 | 说明 |
|---|---|---|
| `DJANGO_DEBUG` | `True` | 关闭后会使 `DJANGO_SECRET_KEY` 成为必填项 |
| `DJANGO_SECRET_KEY` | - | 当 `DEBUG=False` 时必填 |
| `DJANGO_ALLOWED_HOSTS` | `localhost,127.0.0.1,[::1]` | |
| `DJANGO_SITE_NAME` | `Blacklight` | 显示在整个 UI 和 API schema 中 |
| `DJANGO_ADMIN_ENABLED` | `false` | 见下文 |
| `DJANGO_DB_ENGINE` | `postgres` | 或设为 `sqlite` 在无数据库服务器的情况下运行 |
| `DJANGO_DB_NAME` | `django_htmx` | 当引擎为 sqlite 时的文件名 |
| `DJANGO_DB_USER` | `django` | |
| `DJANGO_DB_PASSWORD` | - | 必填;compose 没有它将无法启动 |
| `DJANGO_DB_HOST` | `127.0.0.1` | Compose 会将其覆盖为 `db` |
| `DJANGO_DB_SSLMODE` | `prefer` | 在非私有网络下请使用 `require` 或更高强度 |
| `DJANGO_SECURITY_PAGE_SIZE` | `50` | 模块列表中每页的行数 |
| `DJANGO_API_TOKEN_RATE` | `10/hour` | 针对 `POST /api/token/` 的限流 |
| `DJANGO_SLA_*` | 7 / 30 / 90 / 180 / 365 | 按严重程度划分的 remediation 宽限期(天) |
当 `DEBUG=False` 时,设置会开启 `SECURE_SSL_REDIRECT`、HSTS、安全 cookies、哈希压缩的静态文件和 SMTP 电子邮件后端,并将可浏览的 API 降级为仅支持 JSON。三个文档路由仍会工作并要求已登录的账号;如果你不想完全发布它们,可以从 `api/urls.py` 中移除。
### 一个棘手之处:密钥中的 `$`
Docker Compose 会将 env 文件中的 `$` 视为变量引用,因此 `SECRET=abc$def` 会静默变成 `abc`。web 服务使用 `format: raw` 读取 `.env`,因此字面量 `$` 能完好无损地到达应用(需要 Compose v2.30+),并且 `.env.example` 使用 `secrets.token_urlsafe` 生成密钥,其字母表中不包含 `$`。
`DJANGO_DB_PASSWORD` 是个例外:它仍然会进行 `${...}` 替换,因为 Postgres 服务也需要它。请不要在其中使用 `$`,否则技术栈的两端最终会持有不同的密码。
### 开启 Django admin
admin 管理的每个模型在站点 UI 中都有一个对应界面,因此它未被安装,并且 `/admin/` 返回 404 —— 少了一个经过身份验证的攻击面。`admin.py` 模块依然全部保留在这里:
```
DJANGO_ADMIN_ENABLED=true python manage.py migrate # its log table
DJANGO_ADMIN_ENABLED=true python manage.py runserver
```
值得开启它的唯一理由是为了处理 UI 没有界面的一件事:删除另一个账号的 API token。
## Docker
你获得哪种技术栈取决于你如何调用 Compose:
| 命令 | 技术栈 |
|---|---|
| `docker compose up` | 开发环境。`runserver`,源代码绑定挂载,实时重载,数据库端口绑定在 loopback |
| `docker compose -f docker-compose.yml up` | 生产环境形态。gunicorn 带 3 个 workers,收集静态文件,无绑定挂载 |
`docker-compose.override.yml` 会被自动加载;使用 `-f` 显式指定基础文件就是为了退出此覆盖机制。
`docker/entrypoint.sh` 每次都会在服务器之前运行:它等待 Postgres 响应,应用 migrations(除非 `DJANGO_AUTO_MIGRATE=false`),并收集静态文件(除非 `DJANGO_COLLECTSTATIC=false`,开发环境的 override 设置了此项)。然后它会 `exec` 启动服务器,因此 `docker stop` 可以干净地将其关闭。
```
docker compose up --build # start (rebuild after dependency changes)
docker compose logs -f web # follow app logs
docker compose exec web python manage.py
docker compose exec db psql -U django django_htmx
docker compose run --rm web python manage.py test
docker compose down # stop, keep data
docker compose down -v # stop and delete the database volume
```
当进程已启动并且能连接到其数据库时,`GET /healthz/` 返回 `200` 及 `{"status": "ok", ...}`;如果不能,则返回 `503`。它在设计上是无身份验证的 —— Docker 的健康检查、负载均衡器和 Kubernetes probes 在任何人能够登录之前都需要它 —— 并且除了 up/down 状态外不报告任何信息。
### 迈向生产环境
基础技术栈只是具备生产环境的*形态*,并未达到*生产就绪*状态。在成为后者之前:
- 设置 `DJANGO_DEBUG=False` 和真实的 `DJANGO_SECRET_KEY`。
- 在前面放置一个 TLS 终止代理,并移除 web 服务上的 `SECURE_SSL_REDIRECT=False` 覆盖。`SECURE_PROXY_SSL_HEADER` 已经为此配置好了。
- 对于任何不在同一私有网络上的数据库,设置 `DJANGO_DB_SSLMODE=require`(或 `verify-full`)。
- 将 `DJANGO_DB_PASSWORD` 和 `DJANGO_SECRET_KEY` 移至你平台的密钥库中。
- 如果你扩展到多个副本,请设置 `DJANGO_AUTO_MIGRATE=false` 并从发布作业中执行 migrate,这样副本在每次部署时就不会产生竞争。
- 将备份指向 `pgdata` volume,或者使用托管数据库并彻底删除 `db` 服务。
然后进行验证:
```
docker compose run --rm -e DJANGO_DEBUG=False web python manage.py check --deploy
```
## 测试
```
docker compose run --rm web python manage.py test
```
或者在主机上,无需数据库服务器:
```
DJANGO_DB_ENGINE=sqlite DJANGO_DB_NAME=test.sqlite3 python manage.py test
```
该测试套件涵盖了权限控制门和上述所有防护措施、htmx 片段响应、indicator 规范化和类型检测、每个规则渲染器(包括重新生成时的 SID 稳定性和内容转义)、ruleset 选择和导出 tokens、包括幂等性和部分失败在内的 scanner 导入、incident 生命周期和 timeline、两个导入 endpoint、API 及其生成的 schema,以及健康检查 endpoint。`core/tests_pages.py` 会渲染三个模块中的每个界面,这可以捕获引用了已移动的 URL 名称或上下文变量的模板。
预计输出中会出现 `WARNING django.request Forbidden` 日志 —— 那些是访问控制测试在断言它们获得了 403。该套件中没有任何内容依赖于 PostgreSQL 独有的行为,因此在任何一个后端上都能通过。
## 项目布局
```
config/ settings, root URLconf, WSGI/ASGI
accounts/ custom User model + self-service profile pages
usermgmt/ administrator-facing user & group management
core/ landing page, dashboard, shared models, mixins, htmx helpers
indicators/ IoCs, sources, rulesets, rule renderers, value normalisation
assets/ inventory, services, vulnerabilities, findings, scanner import
incidents/ incident response: cases, timelines, tasks
api/ DRF serializers, viewsets, permission classes, schema annotations
templates/ all templates (project-level, not scattered per app)
static/ one hand-written stylesheet, two that put the API pages on it, htmx
```
### 如何使用 htmx
htmx 仅仅是一个 JavaScript 文件 —— 没有 htmx Python 包,也没有 middleware。整个服务端层面就是 [`core/htmx.py`](core/htmx.py),大约三十行代码,用于读取 `HX-Request` 标头并设置 `HX-Redirect`、`HX-Refresh` 和 `HX-Trigger`。视图检查 `is_htmx(request)` 并返回片段而不是完整页面:
```
def get_template_names(self):
if is_htmx(self.request):
return ["usermgmt/partials/user_table.html"]
return [self.template_name]
```
每个 htmx 增强的界面首先都是一个真正的表单或链接。关闭 JavaScript,列表仍然可以搜索、过滤、排序和分页 —— 它们只是会进行一次整页加载。CSRF 仅处理一次,由 `base.html` 中 `` 上的 `hx-headers` 设置,因此每个 htmx 请求都会自动携带该 token。
## 扩展
**向用户模型添加字段。** 将其添加到 `accounts/models.py`,运行 `makemigrations`,然后将其添加到 `ProfileForm`(自助服务)、`AdminUserChangeForm`(管理员)和 `UserSerializer`(API)。
**添加应用。** `startapp thing`,将其添加到 `INSTALLED_APPS`,将其模板放在 `templates/thing/` 中,并在 `config/urls.py` 中包含其 URL。对于安全类应用,请从 `core/mixins.py` 继承 `HtmxListView` 和 `ModelPermissionRequiredMixin`,并使用 `core/models.py` 中的共享 `Tag`、`Severity` 和 `TLP` —— 这就是保持跨模块使用同一个标签词汇表的原因。
**添加 indicator 类型。** 将其添加到 `IndicatorType`,在 `indicators/normalize.py` 中添加一个 normaliser,然后在 `SUPPORTED_TYPES` 中声明哪些格式可以表达它,并在支持它的渲染器中添加一个分支。任何遗漏的内容都会被丢弃,而不会输出损坏的数据。
**添加规则格式。** 在 `indicators/rules.py` 中编写一个接收 `(ruleset, indicators)` 并返回文本的渲染器,然后在 `RuleFormat`、`SUPPORTED_TYPES`、`FILE_EXTENSIONS`、`CONTENT_TYPES` 和 `RENDERERS` 中注册它。渲染器不接收任何模型导入,因此可以直接进行单元测试。
**支持另一种 scanner。** 将其列拼写添加到 `assets/importers.py` 的 `COLUMN_ALIASES` 中。所有三个导入路由都会同时识别它们。
**更改外观。** 所有内容都在 `static/css/app.css` 中,由顶部的自定义属性驱动,并通过 `prefers-color-scheme` 支持浅色和深色主题。API 页面读取相同的属性,因此重新调整站点配色也会同时重新调整可浏览的 API 和 Swagger UI。 标签:Django, htmx, incident response, 占用监测, 威胁情报, 安全运营, 开发者工具, 扫描框架, 测试用例, 网络测绘, 请求拦截, 资产管理, 逆向工具