面向文件共享的凭据与敏感数据暴露分类排查工具。
当出现一个开放共享时,问题从来不是“这个仓库里有没有泄露的密钥”,而是 “刚刚暴露了什么,我必须在今天下班前轮换哪些密钥。” sift 正是为这个问题而生:高召回率、快速的审查队列,以及一个反馈循环,让你肉眼发现的任何内容都能成为一条规则,进而找到另外两百份副本。

Python 3.11+,仅使用标准库。无需 pip 安装,无需联网,无需构建步骤。它运行在一台锁定加固的事件响应笔记本电脑上,而这正是你需要它的地方。```bash sift survey \fileserver\openshare # how big is this thing sift copy \fileserver\openshare C:\IR\case-4471 # take a throttled copy sift scan C:\IR\case-4471 # scan it, opens the triage UI
就地扫描也同样有效。先在一批虚构的凭据上试试:```bash
sift demo C:\temp\demoshare
sift.cmd 是一个启动器,可以从任何目录运行。要输入 sift 而不是
完整路径,请将 C:\Dev\sift 添加到 PATH:```bash
setx PATH "%PATH%;C:\Dev\sift"
对于完全没有 Python 的机器,`python build_portable.py` 会构建出
`dist/sift-secrets-<version>-portable-win64.zip`:内含官方的 python.org
可嵌入运行时及本源码树,通过附带的 `sift.cmd` 解压即用。约 11 MB,
无需安装,无需管理员权限,且没有任何内容被编译或重新打包——参见
`build_portable.py` 中的 docstring,了解为何这比在严格受限的 IR 笔记本上
使用冻结的 .exe 更胜一筹。
---
## 为什么不是直接用 gitleaks 或 trufflehog
两者都是好工具,但解决的是不同的问题。
它们是面向 CI 构建的**精确**工具,一次误报会耗费
开发者整个下午,因此它们主要针对形似已知供应商 API 密钥
的内容触发。trufflehog 更进一步,倾向于选择自己可以通过
调用供应商 API 来*验证*的机密,这是正则表达式无法
复现的真正出色的信号。
共享目录分类逆转了这种成本逻辑。人工已经在阅读每一条命中,因此
一次误报只耗费三秒钟。真正让你付出代价的是**漏报**。
供应商 API 密钥确实会通过共享泄露——例如 web 根目录备份、部署
脚本、被复制到部门驱动器上的某人的项目文件夹,或者其中
有一个包含有效 Stripe 密钥的 `.env` 文件。这些值得捕获,sift
也能捕获它们。但它们也是 gitleaks 和 trufflehog 已经处理得很好的部分。
真正的空白在于其他一切,而在文件共享上,这些空白占了大多数:
| What the CI scanners miss | Why they walk past it |
|---|---|
| `web.config` with a SQL connection string | Not a known key format, no vendor to verify against |
| `Map-Drives.ps1` with `net use ... /user:` | Just a shell command with a word after it |
| `New Hire Setup Guide.docx` | Office file, read as binary, skipped entirely |
| `unattend.xml`, GPP `Groups.xml` | Windows deployment artefacts nobody wrote a detector for |
| `confCons.xml`, `.rdg`, WinSCP.ini | Reversible stored passwords, but not a "secret format" |
| `passwords.xlsx` | It is a ZIP. Plain-text scanners see binary and move on |
| `.kdbx`, `.pfx`, `id_rsa` | Opaque bytes - the *filename* is the finding |
| A `.bak` with a connection string inside | Binary, so never read |
sift 覆盖这些内容,自带自己的供应商密钥规则,并**导入其他工具的
规则包和发现结果**——包括 gitleaks TOML、Kingfisher/Titus YAML 和
trufflehog JSON——因此你无需在工具之间做选择。
最接近的既有成果是 [Snaffler](https://github.com/SnaffCon/Snaffler),它
在文件名与分类这一半上表现出色,也是文件名规则的直接灵感来源。
它所没有的——以及一旦你有 400 条命中就会成为真正瓶颈的——
是一个审查循环。
---
## 循环
1. **扫描**共享目录。
2. **处理队列。**每条发现都会显示其周围行,并高亮匹配部分。
方向键可扩大上下文;单击即可在 VS Code 中打开该行的整个文件,
或在记事本中打开。
3. **发现漏报。**你一定会。在预览中将其高亮,然后按 `r`。
4. **sift 会提出模式**,并实时告诉你每个模式在已读取的所有内容中
会命中多少次。
5. **保存它。**缓存的重新扫描大约需要一秒钟,新命中会出现在队列中,
而你已有的分类决策保持不变。
第 5 步是让其余步骤值得去做的关键。发现结果以
`(path, rule, line, value-hash)` 为键,因此重新扫描会重新插入相同的行,而你的
状态、备注和负责人也会随之保留。否则你会在每次迭代中重新审查同样的
300 条命中,并在第三次时放弃。
---
## 命令```bash
# size it up first: file count, total bytes, biggest folders, transfer estimates
sift survey \\fileserver\share
# take a rate-limited local copy (resumable; gentle = 5 MB/s by default)
sift copy \\fileserver\share C:\IR\case-4471 --speed gentle
# scan a share and open the triage UI
sift scan \\fileserver\share
# maximum recall: more noise, but a human is reading anyway
sift scan D:\dfs\dept --tier 3
# re-open the UI over the most recent scan
sift ui
# build a share of fabricated credentials, scan it, open the UI
sift demo C:\temp\demoshare
# inherit other tools' vendor-key rules, then use them in the live rescan loop
sift import-rules gitleaks.toml # gitleaks TOML
sift import-rules path/to/kingfisher/data/rules # a directory of YAML
# pull in what the other scanners found, into the same queue
trufflehog filesystem \\fileserver\share --json > th.json
sift import-findings th.json
# hand off to the incident record (redacted unless you say otherwise)
sift export --fmt pdf --status confirmed --out ir-4471.pdf
sift export --fmt csv --out ir-4471.csv
sift export --fmt pdf --no-redact # plaintext; handle as evidence
sift rules # what is loaded
sift selftest # detection tests against a synthetic share
# ask vendors whether confirmed findings are still live. NETWORK. Opt-in.
sift validate --status confirmed
在任何出现 sift 的地方,你都可以改用 python -m sift,从 C:\Dev\sift 目录运行。
发现结果存放在 %LOCALAPPDATA%\sift\ 下的按目标划分的文件夹中,绝不会存放在工作目录——数据库保存的是明文凭据,从你的主目录运行工具不应悄悄把一个凭据文件丢在那里。每个共享都有自己的存储,因此两次调查任务永远不会共用一个分类队列。不带参数运行 sift ui 会重新打开最近的一个;--data DIR 可覆盖此行为。
自定义规则是全局的,位于 %LOCALAPPDATA%\sift\user-rules.json,因此你在一次调查任务中编写的模式会在你接下来查看的共享上发挥作用。
两者都位于 UI 的头部、路径框旁边,也出现在 CLI 中。
检查大小是一次仅 stat 的遍历。不会打开任何文件,因此即使通过 SMB 也很轻量,它会告诉你文件数量、总字节数、最大的文件夹、按扩展名的分布、sift 实际会读取多少数据,以及在不同速度下复制需要多长时间。把扫描器指向一个未知的 DFS 根目录然后干等,一个下午就这么没了。
本地复制先将共享拉取到本地文件夹。这样做值得,因为:
传输是限速的,默认 5 MB/s。下午两点把到生产文件服务器的链路打满,会把你的调查变成第二起事件。当你确定路径空闲时再放开速度。
| 速度 | 速率 |
|---|---|
| gentle(默认) | 5 MB/s |
| normal | 25 MB/s |
| fast | 100 MB/s |
| unlimited | 链路能给多少就用多少 |
传输可断点续传:目标中具有相同大小和 mtime 的文件会被跳过,因此一个在 80% 处中断的采集会从停止处继续。被锁定或被拒绝访问的文件会被记录并跳过,而不会中止整个运行。
PDF、CSV、MD 和 JSON,可通过 UI 头部或 sift export --fmt 导出。
PDF 是交给事件记录的那一份:封面页包含目标、按严重程度和分类状态统计的总数、最常触发的规则、跨文件复用的值,然后是按严重程度分组的结果。它是直接生成的,不依赖任何 PDF 库,因此即使机器上从未安装过 pip 也能工作。
导出默认经过脱敏处理。 值会被遮蔽,每一页都带有横幅标记,并且端点只有在显式传入 redact=0 时才会禁用遮蔽——截断或格式错误的请求无法导致泄漏。在 UI 中关闭脱敏需要确认一个警告对话框,生成的文件每一页都会带有横幅标记:UNREDACTED - CONTAINS PLAINTEXT CREDENTIALS。
发现结果的 数据库 仍保留真实值,因为分析人员需要知道哪个密码泄漏了,才能决定轮换哪个。边界在于离开工具的数据。
所有内容都可点击。结果列表顶部的排序控件可按严重程度、文件路径、规则、分类状态或最近时间重新排序,并带有反向按钮。侧边栏筛选器可按严重程度、类别、规则和复用值过滤。分类、上下文扩展、打开文件和创建规则都是按钮操作。
下面的键盘快捷键是处理长队列时的加速器,并非唯一的操作方式。
快照按钮将高亮的片段渲染为 PNG,便于粘贴到事件工单中;复制片段则将其作为 markdown 复制。
UI 会在浏览器窗口中实时渲染明文凭据,因此:
127.0.0.1,除非使用 --unsafe-bind,否则拒绝其他任何地址;SameSite=Strict cookie 中;Host 头,因此恶意页面无法对其发起 DNS 重新绑定;/api/context 不是任意文件读取,/api/open 也不是任意进程启动。发现数据库按设计包含明文凭据——事件响应分析人员需要知道哪个密码泄漏了,才能决定轮换哪个。请将 %LOCALAPPDATA%\sift\<target>\findings.db 视为证据:与共享本身同等对待,并在调查任务结束时删除它。如果它会离开事件边界,请使用 --redact。
还有显而易见的一点:只对你被授权访问的系统运行 sift。
sift/rules_builtin.py——内容规则。sift/rules_filename.py——文件名规则。两者都是使用原始字符串模式的纯 Python,因此可读且可 diff;用户规则以 JSON 形式存放在 .sift/user-rules.json。
三个层级让你可以在精确率和召回率之间取舍:
cpassword、NTLM 转储、LDAP 绑定密码。仅凭形状即可确认。一条规则还可以携带 min_digits、min_lowercase、min_uppercase 和 min_special,这样可以在不动其他规则的情况下收紧某条噪声较大的规则;还有 examples——它必须仍然匹配的字符串。
examples 是其中有用的一半。sift selftest 会对照每条规则为其编写的字符串运行,走完整条链路:匹配、提取,然后是抑制过滤器。最后一步才是关键,因为实际发生的回归并不是某个模式不再匹配,而是某个噪声过滤器——在别处出于合理原因被收紧后,悄悄在真实发现结果输出前将其吞掉。
它立刻证明了自身价值。为现有规则添加示例时发现了一个真实存在的漏洞:下划线是单词字符,因此通用赋值规则中的前导 \b 无法匹配 DB_PASSWORD、MYSQL_PASSWORD 或 REDIS_PASSWORD 内部——这是现存最常见的三个凭据变量名,却被静默漏掉了。那个示例看起来明显正确,却没有匹配,这正是它存在的意义。
你在 UI 中编写的规则自动享有这一好处:你选中的那行会被保存为该规则的示例,因此六个月后当你编辑一条规则时,它会告诉你它已经不再匹配当初促使你编写它的那个东西。
sift import-rules 接受 gitleaks 的 .toml、Kingfisher/Titus 风格的 .yml,或包含这些文件的目录。针对 Kingfisher 的规则包,1,082 条规则中有 1,073 条被导入,连同它们的熵下限、数字和大小写要求以及示例一起带入。YAML 由 sift/yamlmini.py 读取,这是一个面向这些规则包所用子集的读取器——零依赖,并且遇到锚点和标签会直接报错,而不是假装理解它们。
在导入过程中有两类内容会被有意丢弃:
validation: 块,它为每条规则指定一个 URL。 sift 只会联系 validate.py 中硬编码的主机。一个能够指定端点的规则包将决定你的发现结果被发送到哪里,而规则文件是数据,不是决策。[[:alnum:]] 是 POSIX 字符类;Python 会将其解读为一组字面字符,愉快地编译,然后匹配到错误的内容。转换会对照每条规则的示例进行检查,因此一个通过了编译但含义已改变的模式会被拒绝,而不是悄悄永不触发。Kingfisher 的规则中有十二条未通过该检查,因此不会被导入。sift 自身的抑制是否会丢弃某个示例,并不会使该规则失效——这些规则包有意附带伪造的示例(keyXXXXXXXX、...EXAMPLE),因此占位符过滤器对示例的判断是对的,但对模式本身说明不了任何问题。在区分这一点之前,过于严格曾导致丢弃了 121 条可工作的规则。
通用秘密规则常常因为噪声而被弃用,因此噪声过滤器的调优与模式本身一样仔细。以一个真实的 6,000 文件目录树为基准,下面的抑制规则将发现结果从 1,088 条削减到 257 条,且在测试语料上毫无召回率损失:
password: process.env.DB_PASS 是一个变量,而不是值。这一条抑制就能消除源码树中大部分通用规则的噪声。def login(user: str, password: str) 是一个函数签名。.wrangler、.next、site-packages、node_modules,…)以及压缩后的 bundle 和 source map 都会被跳过。机器生成的文本只会产生机器生成的误报。这里刻意没有带宽松尾部的 https://user:pass@host 内容规则。那个显而易见的版本在一个开发树上触发了 476 次,因为压缩后的 JSON 没有空白字符,模式会从一个 URL 一直跨过引号和逗号,直到在数百个字符之后找到一个无关的 @。
python tests/run_all.py
十二个测试套件:针对合成共享的检测(分为“CI 扫描器已能捕获的内容”、“本工具针对的缺口”以及“必须保持安静的诱饵”)、命令行、规则建议排序、调查与限速采集(包括对照墙钟时间测量速率限制)、PDF 写入器(按读者阅读的方式解析回来,以证明编辑确实作用于页面内容)、gitleaks/trufflehog 导入器、包含所有安全守卫的 HTTP API、UI 的静态分析,以及超大文件的块读取器(断言最后一块中的机密仍能报告其在整份文件中的真实行号)。
要获得一个可供试用的共享:```bash
sift demo C:\temp\demoshare
该语料库中的每个凭据都是虚构的。
两个层级,因为它们承载的风险截然不同。
校验和免费且始终开启。 ghp_、npm_、Atlassian ATATT,
Bitbucket ATCTT 和 GitLab 可路由的 glpat- 令牌都对其
自身主体携带 CRC32。重新计算它可以在离线状态下回答过去
需要互联网才能回答的问题:这是真实令牌,还是某人粘贴到
README 中的示例?发现结果会在队列中被标记为 checksum ok 或
malformed。校验和证明的是形式,而非活性——格式正确的
令牌可能早在一年前就被吊销了。
实时验证默认关闭,直到你运行它。 sift validate 会向
供应商询问凭据是否仍然有效。这是一个单独的命令,不是
scan 上的标志,它会让你在一个提示符下输入 validate,
该提示符首先列明它将联系的每个端点。
安全规则:
sift/validate.py 中是硬编码的。 任何规则——
内置的、用户编写的,或从他人规则包导入的——
都无法提供 URL。没有这一点,导入一个规则包
就足以将共享上的每个凭据发送到
规则包作者选择的地址。--redact 存储运行:如果由于数据库
即将离开事件边界而将值掩码处理,那么
传输明文正是需要防范的行为。validation-log.json
验证尝试会记录在凭据所有者的审计日志中, 并在那一刻归属于你的地址。这有时正是你想要的, 有时却会惊动正在观望的对手。运行之前先决定好; 这正是它要询问的原因。
目前支持 GitHub、npm、Slack 和 Stripe。trufflehog 验证的范围
仍然广得多——运行它并执行 import-findings 即可两者兼得。
import-findings;
已验证的命中结果会排在顶部。.kdbx 或 .pfx
会按名称报告;sift 不会尝试打开它。name=value;
否则秘密词及其值会位于不同列中,
任何规则都无法看到这对组合。.7z、.rar 和嵌套压缩包会被标记但不会解压。
只有基于 ZIP 的格式和 .eml 消息
会在内部读取。--max-size 的文件按块读取,不做缓存。
它们仍会被扫描(4 GB .bak 中的连接字符串
会被找到,并定位到真实行号),但文本缓存
只容纳放得下的内容,因此一秒缓存重扫
不会覆盖它们——新规则会在下一次
完整扫描时触及大文件。.doc/.xls/.pdf(2007 年之前及 PDF)会经过
字符串扫描,而非真正的解析器,因此
这些格式的召回率低于 OOXML 格式。Apache-2.0。参见 LICENSE。
检测测试语料库(sift/selftest.py、tests/)特意包含
格式正确但虚构的凭据;这些路径的密钥扫描告警
已通过 .github/secret_scanning.yml 抑制。
| Flag | Effect |
|---|
--tier 1|2|3 | 召回旋钮。1 = 高信号,2 = 默认,3 = 什么都不漏 |
--redact | 在存储和导出中遮蔽值。如果数据库将离开事件边界,请使用此选项 |
--no-ui | 填充存储后退出,用于脚本化运行 |
--no-browser | 启动 UI 服务器但不打开浏览器(在 RDP 下很有用) |
--include/--exclude GLOB | 缩小遍历范围 |
--no-archives | 不打开 docx/xlsx/zip 容器 |
--no-strings | 不对二进制文件运行 strings 扫描 |
--no-large | 跳过超过 --max-size 的文件,而不是分块读取 |
--jobs N | 工作进程数(默认:自动) |
--max-size MB | 跳过大于此值的文件(默认 25) |
--port N | UI 端口(默认 8973) |
| Key | Action |
|---|
j / k | 下一条 / 上一条发现 |
↑ / ↓ | 向上 / 向下扩展上下文 |
c / f | 确认 / 标记为误报 |
x | 切换选择以进行批量分类 |
o / n | 在 VS Code 中打开到该行 / 用记事本打开文件 |
r | 从高亮文本构建规则 |
y | 复制该值 |
/ | 搜索 |