Gsilvacyber/vigilis
GitHub: Gsilvacyber/vigilis
一个拒绝将关键词匹配视为严重告警的确定性评分引擎,通过证据层级和硬性上限机制确保只有经过验证的威胁才能获得高分。
Stars: 0 | Forks: 0
# Vigilis
**一个拒绝将关键词匹配称为严重警报的检测引擎。**
向 Vigilis 发送一个标题为 `"Suspected lateral movement via mimikatz"`、描述中包含了所有红队术语的警报——但如果没有 IP、没有哈希值、没有域名,没有任何引擎可以核实的内容。它的得分将是 **22/100,标记为 "low"(低)**,并在案例页面以纯文本解释原因。
在同一个警报中添加一个出站 IP。引擎会将其与六个威胁情报提供商和本地 IOC 数据库进行核对。如果其中任何一个标记了该 IP,将触发一个 `verified`(已验证)级别的信号,分数将攀升至中高区间,该案例将成为一个真正的工单。如果都没有标记它,分数将保持在低位。
这种*证据*与*氛围*之间的不对称性——正是整个项目的核心所在。
大多数检测工具在给进程名中针对 `"mimikatz"` 的正则表达式匹配赋予权重时,几乎等同于在恶意软件数据库中确认的 SHA256 匹配。两者都会触发相同的红色横幅,都会在凌晨 2 点叫醒相同的一级分析师。Vigilis 不会。每个信号都带有一个**级别(tier)**,用于捕捉*它实际上是哪种证据*,并且该级别限制了案例允许达到的最高分数:
```
# backend/app/services/enrichment/scoring.py — 上限
if not has_verified and score > 65:
score = 65
```
第二个上限会更早触发:如果没有正面信号被触发,分数将被强制设为 ≤20。自由文本的关键词匹配不计为正面信号——它们不是结构化模式库的一部分。引擎必须找到具体的结构性或外部证据,否则它将保持在低位。
详细的细分信息——触发的每一个信号、其级别、其权重贡献——都会在案例详情响应中公开,并在 UI 中呈现。没有黑盒。
## 为什么会有这个项目
我曾目睹检测工具积累信号的方式,就像老旧代码积累 `if` 语句一样:每次添加都会让分数攀升,没有任何东西会做减法,最终警报标题中的字符串匹配变得与确认的 C2 连接一样重要。这就是“警报剧场”——看起来是高置信度的输出,但如果不手动重新进行调查,分析师根本无法信任。
Vigilis 采取了相反的默认设置。它将关键词匹配视为具有提示性,而非结论性,并强制引擎在允许案例看起来处于严重状态之前,至少找到一条经过验证的证据。这个上限很小、直接且确定——正是 SOC 分析师在高负荷下也能进行推理的那种规则。
## 评分机制
引擎中的每个信号都带有一个**级别(tier)**:
| 级别 | 乘数 | 示例 |
|---|---|---|
| `verified` (1.0×) | 数据库或外部 API 确认了该指标。 | OTX 在 50 多个 pulse 中包含该 IP。本地 IOC 数据库匹配该哈希。实体图:此用户从未登录过此主机。 |
| `observed` (0.4×) | 来自源工具的预填充结构化字段。 | 来自 Azure AD 的 `_isAdminGroupMember=true`。Sysmon EID 11 的 `TargetFilename` = 敏感路径。 |
| `inferred` (0.6×) | 警报文本上的关键词或模式匹配。 | 描述中出现 `"lateral movement"`。PowerShell 脚本块包含 `Invoke-Mimikatz`。 |
最终得分为:
```
score = base_severity + Σ(signal_weight × tier_multiplier)
+ asset_criticality + user_risk
capped at 100
```
然后上限规则开始生效:
- **没有任何已验证信号 → 分数上限为 65。**
- IR 响应(主机已被隔离)→ 分数上限为 40。
- 地理位置异常且无佐证 → 分数上限为 45。
- 零个正面信号 → 分数上限为 20。
权重存在于**一个**注册表文件中——[`backend/app/services/enrichment/weights.py`](backend/app/services/enrichment/weights.py)。在整个代码库中,一个信号名称 = 一个权重。调整权重只需修改单个文件,而不是像寻宝游戏一样到处找。
完整的分数细分——每个触发的信号、其级别、其权重贡献——都是案例详情响应的一部分。它的存在是因为,如果分析师提出“为什么这个得了 78 分?”却得不到具体的答案,他们就会退回去自己 grep SIEM。
## 它是什么,以及它不是什么
**它是:**
- 一个开源的检测评分引擎,你可以在本地 60 秒内运行它。
- 一个具有可审计输出的、具备级别感知的置信度评分的参考实现。
- 横跨评分管道、信号注册表、实体图和提供商集成的约 870 个测试。
**它不是:**
- SIEM。它不索引日志。它接收的是标准化的警报。
- SOAR。它可以调用剧本(代码库中有 CrowdStrike、Okta、ServiceNow 的存根),但执行案例操作不是它的职责。
- 创业公司的推销。没有 SaaS,没有 SSO,没有计费,没有托管服务。
如果你想要一个开箱即用的产品,你需要的是别的东西。如果你想要一个可以从头到尾读完并能适应你环境的引擎,这就是为你准备的。
## 诚实的局限性
在全新安装的情况下,30 天的目标大约是 **17% 的触发信号能达到 `verified` 级别**。这个数字取决于引擎能拉取多少外部佐证(威胁情报源覆盖率、实体图基线、资产 CMDB、身份上下文)——而不是一个评分 Bug。免费的情报源能提供约 13K 个 IOC 的覆盖率;更丰富的集成会将该比例推得更高。
**为什么上限不随已验证比率变动**:上限是单个案例的属性。要么引擎为*这个*警报找到了已验证的证据,要么没有。更高的全局平均验证率只意味着有更多的案例越过了上限,而不是为那些没有越过上限的案例降低了上限。
如果你在一个没有威胁情报的安静网络上运行它,预计大多数案例会落在 50-65 区间。这是引擎在保持诚实。这不是一个需要用更强的关键词规则来修复的功能缺失。
## 快速开始
**要求:** Python 3.11+(适用于不使用 Docker 的路径)**或者** Docker Desktop。已在 macOS、Linux 和 Windows 11 上测试。
### 使用 Docker 运行
```
git clone https://github.com/Gsilvacyber/vigilis.git
cd vigilis
cp .env.example .env # Windows: copy .env.example .env
# 可选:添加 OTX_API_KEY、ABUSEIPDB_API_KEY 以获取更丰富的 threat intel
docker compose up --build -d
```
### 使用 Python 运行(无 Docker)
**macOS / Linux (bash, zsh):**
```
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# SQLite 模式 — 零基础设施
DATABASE_URL=sqlite:///local.db SKIP_INITIAL_FEEDS=true \
uvicorn backend.app.main:app --host 127.0.0.1 --port 8000
```
**Windows (PowerShell):**
```
python -m venv .venv
.venv\Scripts\Activate.ps1
# 如果 PowerShell 提示“running scripts is disabled on this system”阻止了脚本运行,
# 运行一次此命令然后重试:
# Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
pip install -r requirements.txt
# SQLite 模式 — 零基础设施
$env:DATABASE_URL = "sqlite:///local.db"
$env:SKIP_INITIAL_FEEDS = "true"
uvicorn backend.app.main:app --host 127.0.0.1 --port 8000
```
**Windows (cmd.exe):**
```
python -m venv .venv
.venv\Scripts\activate.bat
pip install -r requirements.txt
set DATABASE_URL=sqlite:///local.db
set SKIP_INITIAL_FEEDS=true
uvicorn backend.app.main:app --host 127.0.0.1 --port 8000
```
`SKIP_INITIAL_FEEDS=true` 会将 IOC 情报源的下载推迟到后台调度程序中,这样服务器将在 100 毫秒内准备就绪,而不是等待 7 个出站 HTTP 调用。适用于演示和 CI 环境。
### 验证
```
curl http://localhost:8000/health
# {"status":"ok","providers":{...}}
curl -s "http://localhost:8000/api/v1/metrics/enrichment-quality" \
-H "X-API-Key: socai-demo-key-do-not-use-in-production"
# 在全新的 DB 上 {"totalCases":0,"qualityScore":null}
```
### 查看实际运行效果
发送两个标题和描述在 SIEM 看来完全相同的警报。区别在于警报是否包含引擎可以验证的内容。
```
# Alert A — 标题醒目地写着 "mimikatz / lateral movement / pass-the-hash"
# 但没有 IP、没有 hash、没有 domain。纯粹的 free-text 诱饵。
curl -X POST http://localhost:8000/api/v1/cases \
-H "X-API-Key: socai-demo-key-do-not-use-in-production" \
-H "Content-Type: application/json" \
-d @sample_data/keyword_only_alert.json
```
预期响应结构(节选——通过管道传递给 `jq '{caseId, confidence}'` 可仅查看这些字段):
```
{
"caseId": "258c1a6d-02ee-4974-af56-b4b68bc2a40a",
"confidence": {
"score": 22,
"label": "low",
"explanation": [
{
"signal": "_score_breakdown",
"weight": 0,
"label": "Score composition: 22pts base severity + 0pts verified + 0pts inferred + 0pts observed",
"tier": null
}
]
}
}
```
`explanation` 中的唯一条目是元信号 `_score_breakdown`——引擎没有找到任何正面级别的信号来进行评分。自由文本关键词匹配不计入其中。
```
# Alert B — 相同的文本内容,外加一个 engine 可以检查的出站 IP
curl -X POST http://localhost:8000/api/v1/cases \
-H "X-API-Key: socai-demo-key-do-not-use-in-production" \
-H "Content-Type: application/json" \
-d @sample_data/verified_ioc_alert.json
```
预期响应结构(节选——使用默认配置,没有付费的威胁情报密钥):
```
{
"caseId": "1f63ad87-4bc1-47c8-b21b-fa82b3d50619",
"confidence": {
"score": 47,
"label": "medium",
"explanation": [
{"signal": "known_proxy_vpn", "weight": 15, "tier": "verified"},
{"signal": "_multiVectorAttack", "weight": 12, "tier": "inferred"},
{"signal": "network_activity", "weight": 1, "tier": "inferred"},
{"signal": "_score_breakdown", "weight": 0, "tier": null}
]
}
}
```
`known_proxy_vpn` 是允许案例逃离“纯关键词”分数上限的 `verified` 级别信号。在 `.env` 中添加 `OTX_API_KEY` 并重新运行——Alert B 将获取额外的信号(OTX 提供商会引入 `tor_exit_node`,分数将接近 56)。
相同的文字,不同的分数,因为证据起决定作用,而“氛围”并不管用。
UI 直接在 `http://localhost:8000/cases/` 展示了这一点——从任一响应中复制 `caseId` 字段并在浏览器中打开。
## 在公开数据上的测试
上面的双警报演示是手工制作的。更公平的问题是:*当你把这个引擎指向它从未见过的数据时会发生什么?*
我针对来自公共 HuggingFace SIEM 数据集(Chronicle UDM 格式,混合了来自合成但陌生的环境的登录、进程、网络和云事件)的 200 个警报运行了它。一个 30 行的适配器——[`test_data/public_datasets/map_udm_to_csv.py`](test_data/public_datasets/map_udm_to_csv.py)——将嵌套的 UDM 模式展平为上传端点所期望的扁平列。没有针对每条记录的调优,没有更改信号权重,没有预加载 IOC。
输入 200 个警报,输出 200 个案例。我运行的分数直方图如下:
```
00-20 | 0
21-40 | 38 ######################################
41-60 | 157 ##################################################################
61-65 | 5 ##### <- cap zone
66-80 | 0
81-100| 0
```
**没有任何记录越过 65 分。**引擎无法访问可以验证其中任何警报的威胁情报(没有 OTX,没有 AbuseIPDB——只有本地 IOC 数据库和 GreyNoise 社区版),因此上限限制住了全部 200 个案例。有五个案例紧贴在上限边界——引擎在结构上本想给它们更高的分数,但上限阻止了它。
这就是上限完全按照标题所说的方式发挥作用:在没有可验证证据的情况下,拒绝将警报评级为严重,即使警报文本*看起来*很引人注目。这个上限不是单元测试的固定配置。它是引擎在真实数据上的一个属性。
要复现此结果——三个步骤,三个命令:
```
# 1. 从公共 UDM 数据集生成扁平的 CSV
python test_data/public_datasets/map_udm_to_csv.py
# 2. 上传 200 个 alerts
curl -X POST "http://localhost:8000/api/v1/demo/upload?persist=true&grouping=true" \
-H "X-API-Key: socai-demo-key-do-not-use-in-production" \
-F "file=@test_data/public_datasets/hf_siem_mapped.csv"
# 3. 打印你自己的直方图(仅限 stdlib,无额外 deps)
python tools/score_histogram.py
```
步骤 3 会根据你引擎中当前的数据生成与上述相同格式的直方图。如果你也运行了双警报演示,那些案例也会包含在总数中。可以通过 `--base http://other-host:8000` 让脚本与不同的主机通信。
数据集本身(`hf_siem_200_curated.json`,约 124KB)位于 [`test_data/public_datasets/`](test_data/public_datasets/),是更大的 HuggingFace SIEM 数据集中经过精心挑选的 200 行切片。映射器生成的 CSV 被添加到了 .gitignore——可以使用脚本重新生成它。
## 架构
```
+------------------------------------------------------------------+
| Alert sources |
| Sysmon * SIEM * EDR * IdP * Email gateway * Cloud * Custom |
+-------------------------------+----------------------------------+
| POST /api/v1/cases
v
+------------------------------------------------------------------+
| Vigilis backend (FastAPI) |
| +--------------+ +-----------------+ +------------------+ |
| | Normalize |->| Enrich |->| Entity graph | |
| | alert_mapper| | (8 phases) | | (verified-tier | |
| | | | | | signals) | |
| +--------------+ +-----------------+ +------------------+ |
| | |
| +-------------------+-------------------+ |
| v v v |
| +----------+ +--------------+ +-------------+ |
| | Threat | | Scoring | | Case | |
| | intel | | (tier-aware,| | grouping | |
| | (6 prov) | | capped) | | & incidents | |
| +----------+ +--------------+ +-------------+ |
+-----------------------------+------------------------------------+
|
+------------+------------+
v v v
+----------+ +----------+ +----------+
| Webhook | | SOAR | | Store |
| delivery | | stubs | | (PG/SQLi)|
+----------+ +----------+ +----------+
```
前端是由 FastAPI 提供服务的静态 HTML——没有 Node 工具链,没有构建步骤。如果你想了解 UI 的功能,代码位于 [`backend/app/static/`](backend/app/static/) 和 [`backend/app/api/demo_ui.py`](backend/app/api/demo_ui.py)。
## 威胁情报提供商
| 提供商 | 始终开启? | 免费层级 | 提供的内容 |
|---|---|---|---|
| **LocalDBProvider** | 是 | 无限制(本地) | 启动时从 abuse.ch 情报源(Feodo、URLhaus、ThreatFox、MalwareBazaar、OpenPhish、SSLBL)摄取的约 13K 个 IOC |
| **GreyNoiseProvider** | 是 | 社区版(无需密钥) | IP 的互联网扫描器 / 背景噪音分类 |
| **WHOISProvider** | 是 | RDAP,无需密钥 | 域名年龄和注册元数据 |
| **OTXProvider** | 如果设置了 `OTX_API_KEY` | 免费密钥 | AlienVault OTX pulse 查询 |
| **AbuseIPDBProvider** | 如果设置了 `ABUSEIPDB_API_KEY` | 1000 次/天 | IP 信誉 |
| **VirusTotalProvider** | 如果设置了 `VIRUSTOTAL_API_KEY` | 4 次/分钟,500 次/天 | 文件 / IP / 域名报告 |
所有提供商都实现了单一的协议——添加新的提供商只需一个文件并在 `lifespan()` 中注册。
## 实时端点遥测
Vigilis 附带了 PowerShell 导出器,可将真实的 Windows 端点事件输入管道。每个导出器都是独立的——一个导出器的故障不会发生级联。
| 导出器 | 来源 | 频率 | 捕获内容 |
|---|---|---|---|
| `export_sysmon.ps1` | Sysmon Operational 日志 | 每 5 分钟 | EID 1, 3, 10, 11, 12, 13, 17, 18, 19, 20, 21, 22(进程、网络、文件、注册表、DNS、LSASS 访问、命名管道、WMI 持久化) |
| `export_secevt.ps1` | Windows 安全事件日志 | 每 5 分钟 | 登录 (4624/4625)、权限 (4672)、进程 (4688)、服务安装 (4697)、计划任务 (4698)、帐户创建 (4720)、组添加 (4728/4732)、日志清除 (1102) |
| `export_psbl.ps1` | PowerShell Operational 日志 | 每 5 分钟 | 脚本块日志记录 (4104)——解码后的源与 MITRE 模式进行匹配 |
| `export_state.ps1` | 主机快照 | 每小时 | 服务、计划任务、本地用户、自启动项、已安装程序——跨快照进行对比,仅发出变动事件 |
设置说明位于 [`scripts/`](scripts/)。
## 项目结构
```
vigilis/
├── backend/
│ ├── app/
│ │ ├── api/ # FastAPI routes
│ │ ├── core/ # Config, auth, metrics, DB session
│ │ ├── db/ # SQLModel models
│ │ ├── schemas/ # Pydantic request/response schemas
│ │ ├── services/
│ │ │ ├── enrichment/
│ │ │ │ ├── mappers/ # Per-domain extractors
│ │ │ │ ├── providers/ # Threat intel providers
│ │ │ │ ├── entity_graph.py # Cross-case entity relationships
│ │ │ │ ├── scoring.py # Tier-aware scoring + cap
│ │ │ │ └── weights.py # Single signal-weight registry
│ │ │ ├── case_service.py
│ │ │ ├── incident_service.py
│ │ │ ├── calibration.py # Analyst-disposition feedback loop
│ │ │ └── integrations/soar.py
│ │ └── static/ # UI (vanilla HTML/JS, no build)
│ └── tests/ # ~870 tests
├── sample_data/ # Example alerts per type / format
├── scripts/ # PowerShell endpoint exporters
├── docs/ # Reference docs
└── docker-compose.yml
```
## 运行测试
```
python -m pytest backend/tests/ -q
# 867 个通过,19 个跳过
```
跳过的测试是需要实时外部数据源的集成测试。
## 包含的范围与不包含的范围
此代码库包含:
- 评分引擎和信号注册表。
- 实体图(跨案例关系跟踪)。
- 威胁情报提供商协议和 6 个默认提供商。
- `export_psbl.ps1` 使用的 MITRE ATT&CK 模式库。
- 显示带有分数细分的案例详情的静态 UI。
- PowerShell 端点导出器。
明确不包含的范围:
- SSO / SAML / OIDC。只有一个 API 密钥模型。如果你要部署它,应该将其放在你自己的认证网关之后。
- 多租户管理 UI。数据层存在租户概念;UI 假定只有一个操作员。
- 托管 SaaS。目前没有。
- LLM 驱动的警报分类或叙事生成。确定性引擎在没有它的情况下也能工作。将 LLM 富集作为非承载层添加进来很简单;但让它成为事实来源并不是本项目的目标。
## 许可证
Apache License 2.0。参见 [LICENSE](LICENSE)。
## 贡献
这最初是一个个人项目;欢迎提交 PR。最有用的贡献包括:
1. **新的威胁情报提供商**——实现 `ThreatIntelProvider` 并在 `lifespan()` 中注册。测试位于 `backend/tests/test_threat_intel.py`。
2. **新的提取器 / 信号**——添加到域映射器中,在 `W` 中注册权重(同一个位置,请不要硬编码),添加级别注释。测试就在旁边。
3. **校准数据**——如果你在真实的遥测数据上运行它,引擎收集的处置反馈是你能够分享回来的最有用的产物。(请先进行匿名化处理。)
风格:保持确定性,保持可审计,在编写正则表达式之前先编写测试。
标签:AI合规, IOC验证, SOAR, T1分析师辅助, 告警疲劳缓解, 告警评分, 威胁情报, 威胁情报富化, 安全告警分类, 安全运营, 开发者工具, 扫描框架, 正则匹配过滤, 测试用例, 确定性检测, 网络安全, 证据关联, 误报过滤, 请求拦截, 逆向工具, 隐私保护