dboudreau00/Sluice
GitHub: dboudreau00/Sluice
一个零依赖的 Go 威胁情报源接入库,专注于拉取免费情报源数据并严格追踪每条指标的再分发许可边界。
Stars: 0 | Forks: 0
# sluice
一个用于拉取免费威胁情报源的 Go 库——并且让你清楚如何处理获取到的数据。
```
reg := sluice.NewRegistry(sluice.Credentials{AbuseCh: key},
sluice.WithUserAgent("acme-soc/1.0 (+https://acme.example; soc@acme.example)"))
feed, _ := reg.Feed("threatfox")
obs, _ := feed.Fetch(ctx, http.DefaultClient)
// Every observation carries the terms it arrived under.
cleared, blocked := sluice.NewGate(sluice.UseRedistribute).Filter(obs)
```
零依赖。`go.mod` 没有 `require` 块,如果出现依赖,CI 会导致构建失败。
```
go get github.com/yourorg/sluice
```
需要 Go 1.23 或更高版本。
## 为什么会有这个项目
有两个问题,它们都足够小,以至于每个人都在绕过它们,却没人去修复它们。
**情报源适配器在悄无声息地腐烂。** 2025 年,abuse.ch 将所有社区 API 都置于强制的 `Auth-Key` 请求头之后。多年来一直正常运行的工具开始返回 HTTP 200,但在响应体中却包含错误信息,解析器获取不到任何结果,而这种失败看起来就像是一个平静的正常周期。目前没有标准的 Go 模块来保持这些适配器的正确性,因此每个人都在维护着一份私人副本,然后在某个不同的星期二崩溃。
**没人追踪他们被允许再发布什么内容。** 情报源的条款差异很大。OpenPhish 禁止二次分发。abuse.ch 在合理使用下是免费的,但表示商业使用*可能*需要付费订阅。PhishTank 允许商业使用,但要求注明出处。一旦你将它们聚合在一起,一个由四个情报源交叉印证的指标就会受到这四套条款交集的约束——而对此的通常应对方式要么是一张电子表格,要么干脆不管。
MISP 的分发级别回答的是另一个问题:你的共享社区中有谁能看到这些信息。它们并不回答你的上游提供商是否允许你将其发布出去。
sluice 只做这两件事,仅此而已。它不是一个平台。它没有数据库,没有调度器,也没有 UI。这些都是需要你自己做的决定。
## 许可证模型
每个 `Observation` 都带有其获取时所遵循的 `Licence`,因此记录在离开库之后依然是自描述的。
```
type Licence struct {
Class Class // coarse label for humans scanning a list
Commercial Permission // Allowed | Ask | Prohibited | PermissionUnknown
Redistribute Permission
Attribution string // required credit line, empty if none
Note string
TermsURL string
}
```
### Ask 并不代表 yes
之所以存在 `Ask`,是因为大多数情报源的条款不是二元的。abuse.ch 在合理使用下免费提供其社区 API,并声明商业使用*可能*需要订阅。这既不是许可也不是拒绝,而将其以任何方式扁平化正是导致人们违规的原因。
该网关默认将 `Ask` 视为拒绝。`PermitAsk()` 会放宽这一限制,但只有在你实际询问并得到肯定答复后才应该调用它。
```
gate := sluice.NewGate(sluice.UseCommercial).PermitAsk()
```
`PermitAsk` 永远不会覆盖明确的禁止条款。
### Unknown 是最严格的状态
未填充的许可证具有 `PermissionUnknown` 属性,网关会拒绝该状态。如果没人阅读过条款,答案就是不行。这正是让安全添加情报源适配器成为可能的原因,而无需审计每一个调用点:未声明的许可证会失败关闭,并且测试会断言每一个注册的情报源都声明了许可证。
### 交叉印证取条款交集
这是聚合应用会产生、而其他模型无法处理的情况。一旦四个情报源在某个地址上达成一致,约束该指标的条款就是这四个情报源条款的交集,而不是最友好的那一个。
```
groups := sluice.GroupByKey(observations) // dedup across feeds
cleared, blocked := gate.FilterMerged(groups) // strictest terms win per indicator
```
`Merge` 会累积归属信息,而不是仅选择其中一个,因为发布一份只注明部分来源的衍生列表本身就会带来问题。
`Filter` 和 `FilterMerged` 都会返回被拒绝的对象及其原因。静默丢弃记录正是导致人们搞不清为什么导出数据变少的原因。
## 标准化
核心函数是 `Normalise`。如果两个情报源以不同的格式报告相同的基础设施,且这些格式没有合并为一个键,那么交叉印证的计数就会偏低,建立在它之上的每个置信度评分都会以无人察觉的方式出错。
它处理了情报源数据中实际出现的情况:
| 输入 | 标准化结果 |
|---|---|
| `192.0.2.1:443` | `192.0.2.1` — C2 情报源会附加端口 |
| `192[.]0[.]2[.]1` | `192.0.2.1` — 情报源会回显消除危险性的报告 |
| `hxxp://evil[.]invalid/a` | `http://evil.invalid/a` |
| `www.evil.invalid` | `evil.invalid` |
| `http://evil.invalid:80/a` | `http://evil.invalid/a` |
| `192.0.2.55/24` | `192.0.2.0/24` |
| `2001:DB8::1` | `2001:db8::1` |
它*故意不*合并 `evil.invalid` 和 `mail.evil.invalid`,或者 `http://` 和 `https://` 变体——过度合并会制造虚假的交叉印证,这比漏掉一些更糟糕。`TestNormaliseKeepsDistinct` 保护了这一原则。
`FamilyLabel` 会折叠供应商的命名方式,以便将 `RedLine`、`redline_stealer` 和 `win.redline` 聚合在一起。它是启发式的,偶尔会出错;原始标签保留在 `Raw` 中。
## 情报源
| Slug | 内容 | Key | 商业用途 | 二次分发 |
|---|---|---|---|---|
| `urlhaus` | 恶意软件分发 URL | abuse.ch | 询问 | 否 |
| `threatfox` | 带有家族归属的混合 IOC | abuse.ch | 询问 | 否 |
| `malwarebazaar` | 样本哈希和家族标签 | abuse.ch | 询问 | 否 |
| `feodotracker` | 活跃的僵尸网络 C2 地址 | 无 | 询问 | 否 |
| `openphish` | 社区钓鱼 URL | 无 | 否 | 否 |
| `phishtank` | 社区验证的钓鱼网站 | 免费 | 是 | 否 |
| `blocklist_de` | 被 fail2ban 传感器报告的主机 | 无 | 是 | **是** |
| `cins_army` | 信誉不良的地址 | 无 | 是 | 否 |
| `spamhaus_drop` | 被劫持的网络块 | 无 | 询问 | 否 |
| `tor_exits` | Tor 出口中继(上下文背景,非威胁) | 无 | 是 | **是** |
| `anyrun` | 沙箱 STIX bundle | 付费 | 否 | 否 |
一个来自 [auth.abuse.ch](https://auth.abuse.ch/) 的免费 Key 可同时解锁 URLhaus、ThreatFox 和 MalwareBazaar。缺少凭证的情报源根本不会被注册,因此遍历 `reg.All()` 永远不会产生注定会失败的结果。
### 在制定计划前值得了解的三件事
**abuse.ch 需要身份验证。** 每一个社区 API 都是如此。没有任何值得去实现的匿名回退方案。
**VirusTotal 没有免费的批量情报源。** 免费层级仅支持查询,大约每分钟 4 次请求,每天 500 次,仅限非商业用途,结果不可再分发。它属于你的富化路径,而不是你的接入路径,这就是为什么这里没有 VT 适配器的原因。批量地址信誉来自于 Feodo Tracker、Spamhaus DROP、CINS 和 blocklist.de。
**ANY.RUN 的实时 TI 情报源是付费产品。** 只有演示 bundle 是开放的,并且大约有六个月的延迟。对于追溯狩猎和测试 STIX 解析器很有用;对于实时检测毫无用处。该适配器仅在你提供 Key 时才会注册,并且其许可证类别为 `restricted`。
### 遵循提供商的 TTL
ThreatFox 在六个月后会废弃旧指标,因为云地址会更换租户。复活它们会对下一个租用该地址的人造成误报。`Observation.ExpiresAt` 会在提供商声明保留期的地方映射上游设置,而 `Expired()` 供你在采取任何行动之前调用。
### 添加你自己的情报源
任何 STIX 2.x 或 TAXII 集合无需编写新代码即可运行:
```
feed := sluice.NewSTIXFeed(sluice.Descriptor{
Slug: "internal", Name: "Our sandbox", Vendor: "us",
MinInterval: time.Hour,
Licence: sluice.Licence{
Class: sluice.ClassPublicDomain,
Commercial: sluice.Allowed, Redistribute: sluice.Allowed,
},
}, "https://ti.internal.example/stix", authHeader, 90*24*time.Hour)
```
对于其他任何内容,请实现 `Feed` 接口。只需两个方法。
## 网络礼仪
这些都是由他人在其预算下运行的免费服务。该库会帮助你避免成为麻烦,但它不会强制执行任何操作。
- **设置带有联系地址的 User-Agent。** PhishTank 会对空白和通用的 Agent 进行限流或验证。abuse.ch 宁愿给你发邮件也不愿封禁你的 Key。在这里,`WithUserAgent` 是最有价值的选项。
- **遵守 `MinInterval`。** 每个描述符都会根据提供商实际发布的频率,声明最合理的最短间隔。OpenPhish 大约每六小时刷新一次;每分钟轮询一次只会返回 360 次相同的文件。
- **在启动时加入抖动。** 重启后,十个情报源在同一秒内发起请求看起来就像是滥用,并且必然会导致被限流。
- `HTTPError.Retryable()` 会报告退避是否有帮助——对于 429 和 5xx 是肯定的,对于 401 和 403 是否定的。
## 测试
该测试套件针对 `testdata/` 中记录的固定数据运行。没有网络调用,不需要凭证,不会出现不稳定的情况。
```
go test ./...
```
测试数据使用了 RFC 5737 文档地址和 `.invalid` 域名。数据结构是真实的;数值则不是。任何人都不应该将测试数据误认为是实时情报,也不应该有测试数据因为写错了位置而最终出现在某人的黑名单中。
每个适配器都接受 `WithBaseURL`,这正是使它们能够针对本地测试数据服务器进行测试的原因。
### 金丝雀
情报源工具中代价高昂的失败是静默的失败。提供商重命名了一个字段,适配器继续返回 HTTP 200,解析器得不到任何结果,直到某次事件审查询问为什么某个情报源在三周前变得悄无声息时,才会有人注意到。
`internal/canary` 会轮询真实的端点,并将响应结构同测试数据进行对比:
```
go run ./internal/canary # human readable
go run ./internal/canary -only=urlhaus
go run ./internal/canary -json # machine readable
```
对于 JSON 情报源,它会比较字段路径的集合。**被移除的**字段意味着发生了偏移并会导致运行失败;**新增的**字段仅供参考,因为提供商经常会添加列。对于纯文本列表,它会测量仍然能被归类为指标的行数比例,这可以捕获提供商向 CSV 格式的转换。
它还会运行实际的适配器,如果实时响应获取不到任何观测数据就会失败,这是最需要警惕的失败模式。
`.github/workflows/canary.yml` 会每天运行一次,并维护一个带有 `feed-drift` 标签的滚动 issue,它会在该 issue 下发表评论,而不是每天早上都新开一个。每天零星出现的重复 issue 会被静音,而静音的警报无异于没有警报。当一切重新恢复正常时,它会自动关闭该 issue。
要启用它,请添加仓库密钥 `ABUSECH_AUTH_KEY` 和 `PHISHTANK_APP_KEY`,以及一个仓库变量 `SLUICE_USER_AGENT`。没有凭证的情报源会报告为跳过而不是失败,因此即使在完全没有密钥的 fork 中,金丝雀也很有用。
## 示例
**`examples/export`** — 收集所有被允许的情报源,按交叉印证分组,评分,并写入带有归属信息块的 JSON 或 CSV 文件:
```
go run ./examples/export -use=internal > everything.json
go run ./examples/export -use=redistribute > safe-to-publish.json
```
运行两者并对它们进行 diff。其中的差距正是大多数工具在无意中发布错误数据的地方。
那里的评分机制 —— `sources × 28 × e^(−age/7d)`,上限为 100 —— 是一个示例,而不是建议。它存在于示例中而不是库中,因为你的权重分配取决于你的环境,而一个替你评分的库只会是一个让你头疼的库。
**`examples/console`** — 一个用于渲染导出数据的单个 HTML 文件。打开它,选择一个 JSON 文件。无需构建步骤,无需服务器。
## 本项目不是什么
- 不是一个 TIP。没有存储,没有调度,没有关联引擎,没有 UI。
- 不是法律建议。许可证元数据反映的是在特定时间点对公开条款的解读。条款会发生变化,本仓库的更新会落后于它们。每个许可证上都有 `TermsURL` 是有原因的——如果答案在商业上很重要,请阅读它,并在我们出错时告诉我们。
- 不是 VirusTotal 客户端。请参阅上文。
- 不是富化层。被动 DNS、WHOIS、地理位置和信誉查询都属于上层应用。
如果你想要完整的平台,[MISP](https://www.misp-project.org/) 和 [OpenCTI](https://www.filigran.io/en/products/opencti/) 都很成熟,并且两者都能接入这里的每一个情报源。当你只想要适配器和许可证模型,而不想继承其中任何一个平台时,请使用 sluice。
## 许可证
MIT。请参阅 `LICENSE`。
这涵盖的是代码。它不涉及代码获取的数据,这些数据受各提供商自己的条款约束——这正是 `Licence` 类型的核心意义所在。
标签:EVTX分析, Go库, 威胁情报, 开发者工具, 日志审计, 适配器