☰
敏感信息扫描工具Seninfo:从工作区到Git历史阻断密钥泄露
2026/10/13 4:53:52 网站建设 项目流程

简介:Seninfo是一款基于Python的敏感信息扫描工具,主要面向开发人员、代码审计与渗透测试人员,用于在源码中快速识别密码字段、身份证号、手机号、邮箱和IP地址等敏感信息,能在代码提交前或安全评估中帮助发现潜在泄露风险。该工具通过预设与可扩展的正则规则逐行扫描,命中结果自动导出到Excel文件,既可作为独立自查工具,也能嵌入测试流程提升效率。资源包内共60个文件,以38个pyc编译模块和21个py源码为主,含主程序seninfo.py及xlwt Excel导出相关实现,另附excel-formula.g语法文件,整体仅314KB,轻量易用。目前已有1557人学习下载。获取后可直接运行扫描,也可参考其清晰结构学习Python工具开发、正则匹配及Excel报表生成思路,并按需添加自定义规则,适配特定业务场景。

1. 敏感信息扫描工具Seninfo:代码泄露的第一道闸门,到底在扫什么

某公司一个内部系统被拖库后复盘,发现攻击者用的口令就是仓库里硬编码的那串数据库密码,而这行配置已经在三个项目里被复制过。人工审计不可能盯着每次提交逐个看,“敏感信息扫描工具Seninfo”这类工具的定位就是把这个环节自动化:在代码合入前、发版前、或者做安全审计时,对文件系统与Git历史做批量扫描,把疑似AK、口令、私钥、身份证号按“文件 + 行号 + 匹配片段”报出来,让开发与安全的人看一眼就能决定要不要处理。

这类工具适合谁:研发负责人想低成本做自检,安全工程师要批量审计多个仓库,做等保或客户合规验收的人想在自查报告里多一条可执行的记录。一个反直觉的结论先放在这里:敏感信息扫描最难的不是“规则不够多”,而是规则太宽、报告太多,最后没人看。能扫出来只完成了一半,让人愿意看、看得完、处理得掉,才是这个方向真正要解决的问题。

2. 判断“什么是敏感”?先想清楚这三层检测逻辑

一份代码里可以被称为“敏感信息”的东西差别很大:AK/SK这类有固定前缀的,一眼就能认出;而一段随机字符串是不是Token,不结合上下文几乎无法判断。如果只靠正则一条路走到黑,要么漏报,要么误报刷屏。把检测拆成三层会更可控:强指纹优先、正则加熵兜底、上下文验证收尾。这一章先把前两层怎么设计说清楚,上下文验证放到最后一章展开。

2.1 强指纹优先:没有歧义的命中先报出来

强指纹规则的特征是“命中即事实”,比如云厂商的AccessKey、各类私钥头、JWT的固定头。这类内容有明确前缀或固定结构,几乎没有正常代码会误长成这样。扫描器应该把它们放在规则集的最前面,一旦命中直接进高危列表,不再做任何二次判断。

import re HIGH_CONF_RULES = [ {"name": "aws_ak", "pattern": r"\b(AKIA[0-9A-Z]{16})\b"}, {"name": "aliyun_ak", "pattern": r"\b(LTAI[0-9A-Za-z]{12,20})\b"}, {"name": "private_key", "pattern": r"-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----"}, ] def scan_high_conf(text, rules=HIGH_CONF_RULES): hits = [] for r in rules: for m in re.finditer(r["pattern"], text): hits.append({ "rule": r["name"], "match": m.group(0)[:20], # 只保留片段,避免完整密钥刷屏 "pos": m.start() }) return hits

注意私钥规则只匹配“头”,不要贪婪地把整段私钥内容都吐进报告。输出时默认只保留匹配片段前20个字符,既能让开发认出是什么,又不会把完整密钥复制走。\b 边界符在AK规则里很关键,不加的话,一段普通变量名里含“AKIA”开头的字符也会被命中,凭空多出一批假阳性。

强指纹规则的第二层价值在于给整个扫描器定调:只要这类命中出现,不管出现在哪个目录,都必须当天处理。这也决定了后续报告的排序策略——高危永远排最前面,因为它是确定性的泄露。

2.2 正则加熵:处理密码和Token这类“看起来随机”的内容

口令、Token、连接串就没有那么听话了,它们往往没有固定前缀,背后跟着的是一串大小写字母、数字、符号的混合体。只靠正则“password = xxx”能扫出一部分,但遇到纯随机Token、Base64编码的密钥就会漏光。常见的做法是正则负责定位“疑似片段”,再用信息熵过滤掉明显是普通单词或固定文案的命中。

import math import re def shannon_entropy(data: str) -> float: if not data: return 0.0 ent = 0.0 for ch in set(data): p = data.count(ch) / len(data) ent -= p * math.log2(p) return ent WEAK_RULES = [ {"name": "password_assignment", "pattern": r"(?i)(password|passwd|pwd)\s*[=:]\s*[\"'][^\"']{6,}[\"']"}, {"name": "jwt_like", "pattern": r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}"}, ] def scan_with_entropy(text, rules=WEAK_RULES, entropy_min=3.8): hits = [] for r in rules: for m in re.finditer(r["pattern"], text): token = m.group(0) ent = shannon_entropy(token) if ent >= entropy_min: hits.append({ "rule": r["name"], "entropy": round(ent, 2), "match": token[:20] }) return hits

熵阈值在3.5到4.2之间比较常见。云端Key的熵通常大于4,硬编码口令往往在3.5附近,JWT的熵会更分散。如果一个项目的实际误报偏高,优先把阈值往上调到4.0,而不是删规则。还要注意:对中英文混合字符串算熵没有意义,可以先判断片段里是否含中文,含中文就跳过熵校验,只保留正则命中的低危记录。

单独用正则的问题是太依赖书写习惯,换个写法“db_pwd: xxxxx”就漏了;单独用熵的问题是“hello123456”这类高熵但无害的字符串也会被报出来。所以正则加熵的组合不是完美方案,而是把两个有缺陷的办法叠在一起,用交叉约束降低噪声,这个思路在后面的上下文验证里还会再出现。

2.3 分级与灰度:规则不是越多越好

规则集需要一张分级表来约束:强指纹对应高危,正则加熵对应中危,纯正则对应低危。低危命中不能直接进报告正文,只当作“待人工复核”。这样分级的好处是,开发打开报告时第一眼看到的永远是确定性问题,而不是一堆需要猜的“疑似”。

rules: - id: R0001 name: aws_ak severity: high type: strong_fingerprint - id: R0002 name: private_key_header severity: high type: strong_fingerprint - id: R0102 name: password_assignment severity: medium type: regex_entropy entropy_min: 3.8 - id: R0201 name: id_card severity: low type: regex note: 单命中不算证据,需结合上下文验证

规则文件本身要纳入Git管理,变更走提交评审。常见做法是新规则一律先设为低危灰度一周,观察误报率后再升级。误报率 = 被开发驳回的命中数 / 总命中数,这个指标超过30%就说明规则太宽,需要加约束条件而不是继续加规则。

我踩过一次教训:上线一条看似很聪明的“URL 中带 password 参数”规则,结果两周内报了400多条低危,全是前端页面里正常的请求参数名,最后只能回滚规则版本。规则库的维护原则应该是“少而准”,不是“多而全”,每次加规则都要先想清楚:这条规则真的能指导人采取行动吗?

3. 把Seninfo跑起来:从单目录到Git历史与CI卡点

工具的价值在接入方式。先给一个能跑的本地命令,再讲批量仓库和Git历史扫描,最后落到CI卡点。最小落地路径是:先本地扫描验证规则 → 再覆盖Git全历史 → 最后挂到提交钩子,三步走完才能说真正接入了。

3.1 最小命令:一次本地扫描的完整参数

# 基本用法:指定扫描路径、规则集、输出格式 seninfo scan --path ./src \ --rules ./rules.yaml \ --ignore ./build,./vendor,./node_modules \ --output ./report.json \ --format json \ --workers 4 # 只输出高危项,适合第一次摸底 seninfo scan --path ./src --severity high --output ./report.txt

参数表:

参数作用建议值
--path扫描根目录必填
--rules规则集文件缺省用内置规则,落地后建议自维护
--ignore忽略目录,逗号分隔至少排除 build、dist、vendor
--output报告输出路径建议按日期命名
--formatjson / txt / sarifCI场景优先 json 或 sarif
--workers并发数单核2,多核可到物理核数
--severity只输出某级别以上第一次跑建议 high

第一次跑老代码时,先加一个文件大小限制,比如跳过超过1MB的文件,免得巨型日志把扫描拖死。运行完先看两样东西:扫描日志里有没有“文件被跳过”的记录,以及高危列表有多少条。如果高危直接超过10条,说明仓库里已经是“泄露重灾区”,先别急着调规则,而是先让对应负责人清理,因为再细的规则也拦不住已经躺在仓库里半年的一批密钥。

3.2 批量仓库扫描:一份报告里看多个项目

真正落地时手头往往有几十个仓库。常见做法是维护一份仓库清单,每行一个本地路径,循环执行,再把结果汇总成一份总报告。

# repos.txt 每行一个仓库绝对路径 while read -r repo; do seninfo scan --path "$repo" \ --rules ./rules.yaml \ --ignore ./build,./dist,./node_modules,./.git \ --output "reports/$(basename "$repo").json" \ --format json done < repos.txt # 合并所有json到一个汇总文件 jq -s 'add' reports/*.json > all_repos_report.json

批量扫描的核心不是循环脚本本身,而是忽略列表的统一。多个仓库可能共用同一套依赖目录,如果不统一排除,同一个第三方库里的“疑似命中”会在每个仓库里重复出现,报告直接膨胀三倍。我一般会在批扫前先对每个仓库跑一次文件类型统计,确认src目录下没有混入编译产物,再决定要不要放宽忽略规则。

汇总报告生成后,按规则名做一次分组统计很有用:如果某条规则在十几个仓库里都命中同一类文件,通常是规则本身有问题;如果命中集中在少数几个文件,才是真正的泄露点。这个统计结果也直接决定下一步是改规则还是改代码。

3.3 扫进Git全历史:当前工作区是扫不全的

只扫当前工作区会漏掉一个重要场景:已删除但还留在Git对象库里的敏感信息。密钥一旦进过仓库,即使后来删除,历史记录里仍然存在,攻击者可以翻历史翻出来。全量历史扫描和CI增量扫描是两件事,不能互相替代。

# 方式一:把所有历史blob导出后扫描,适合定期全量审计 git clone --mirror "$repo" /tmp/repo.git cd /tmp/repo.git git rev-list --all --objects \ | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize)' \ | awk '$1=="blob" && $3<1048576 {print $2}' \ | git cat-file --batch > /tmp/all_blobs.bin # 方式二:只找diff中新增的行,适合CI每次提交快速拦截 git log -p -U0 --all -- . ':(exclude)vendor' | seninfo scan --stdin --format json

方式一能扫到已删除文件里的密钥,这是权限审计和发版前最需要的。用 git clone --mirror 而不是普通 clone,因为普通 clone 可能只保留部分分支引用,历史不全。超大仓库先按 blob 大小过滤,超过1MB的对象跳过,避免把大二进制文件也送进扫描器。方式二是增量思路,只针对每次提交新增的行做判断,速度快,适合挂在CI上,但它看不到历史遗留问题,所以两条线要同时跑。

全历史扫描对资源的消耗明显更大,需要设一个“性能预算”:比如单个仓库每周六凌晨跑一次全量,目标2小时内完成;超过预算的仓库先降级为只扫差异提交。把性能预算写进落地文档,比临时出了问题再调参数要省事得多。CI侧我在本地通常用 pre-commit 钩子做第一道拦截,只对暂存区里改动过的文件跑一遍高强指纹规则,秒级返回,真正把泄露挡在提交之前。

4. Seninfo落地的5个真实坑:扫描结果不可信,大多是参数和环境问题

工具能产出报告不难,难在报告能被人相信。下面四类情况是这个方向最常见的翻车现场,我按现象、原因、解决写清楚。

4.1 只扫工作区,漏掉敏感历史

现象:报告显示“干净”,可攻击者用 git log 翻一翻就找到一个写死的数据库口令。

原因:默认扫描器如果只遍历当前工作区,已删除文件里的敏感内容不再出现在文件系统里,但内容仍留在 .git 对象库中。安全审计关心的是“历史上是否存在过泄露”,而不仅限当前代码。

解决:把全历史扫描纳入定期任务,用 git rev-list --all 导出全部 blob 后扫描,至少每周一次;对外发版或仓库权限变更前再做一次。CI 增量扫描替代不了全量历史扫描,两套任务要分开计划、分开报告。

4.2 二进制和构建产物把报告刷屏

现象:第一次在一套老代码上跑,报告一万多条,其中八成是 .min.js、.woff、.zip、.class;开发根本不想看。

原因:没有做文件类型过滤。压缩包、编译产物、字体文件里偶尔会出现类似正则命中的内容,这些命中没有处置价值,只会淹没真实问题。

解决:先建立忽略名单再扫业务代码。node_modules、build、dist、vendor 目录一律排除;对无扩展名或二进制文件,用 file 命令识别类型后跳过;限制单个文件大小超过1MB默认跳过并记入日志。忽略规则本身也要评审,避免有人打着“排除构建产物”的名义把 src 下的生成代码也排除掉。

4.3 正则回溯把扫描拖成玄学

现象:扫描进程CPU持续100%,单仓库跑了一晚上没结束,开始怀疑是规则库太大。

原因:复杂正则嵌套量词在长行上触发灾难性回溯,常见于 .* 和 (.*) 叠加。这不是机器配置问题,是正则写法问题。

解决:规则审查时禁止形如 (a+)+ 这类嵌套量词;每条正则运行时加超时限制,比如100毫秒超时就跳过并记日志;扫描前对超长行做截断,超过2000字符的切分或忽略。血泪经验:宁可写多条简单规则,也不要为省事合并成一条大正则,“能匹配”和“能跑完”是两回事。遇到扫不完的仓库,先怀疑正则回溯,再去升级机器。

4.4 报告太长没人看:先分级收敛再发出去

现象:命中列表800条,高危只有12条,但整个报告是平铺的,安全群里没人响应,安全事故最终还是发生了。

原因:报告没有按严重级别收敛。开发者的处理顺序是先看“必须处理”,再看“建议处理”,如果打开报告先看到的是几百条低危,很容易忽略真正的高危项。

解决:报告默认只输出 medium 以上;高危单独一页,附规则名称、文件、行号、匹配片段脱敏;低危只进附录,按文件聚合而不是逐条显示。发布审计报告前,先看“高危总数”这一个数字,如果高危超过10条,建议先整改再发报告,因为此时发出去只会让所有人麻木。

4.5 规则误报率反弹:灰度没做好,规则越修越乱

现象:上线一条“URL 里带 password 参数”的规则,两周内报了400多条低危,全是前端页面正常的请求参数名,最终只能回滚。

原因:新规则没有灰度直接进了正式规则集,而且只用了正则,没有叠加熵或上下文验证。

解决:规则生命周期管理。新规则先进低危灰度一周,统计误报率低于30%再升级到 medium 或 high;上线前用历史报告做回归,看新增误报主要集中在哪些文件;如果是业务固定文案导致的误报,直接加白名单文件或行模式,而不是改正则本身。规则库要像代码库一样管理,每条规则的变更都有记录,误报反弹时可以快速回滚到上一个稳定版本。

5. 进阶:用上下文验证把误报压下来,让开发愿意看报告

当规则集稳定后,下一步要解决的就是误报率。正则加熵已经过滤掉一批“普通单词”,但仍有不少命中来自业务自身的关键字,比如一个字段名正好叫 secret,但存的是授权回执。这时候再叠正则只会越叠越乱,正确的方向是加一层上下文验证:取命中行的前后几行,看它是不是真的在赋值场景里。

def validate_with_context(hit, context_lines, width=2): """ hit: {rule, match, line_no, file} context_lines: 命中行附近的前后width行文本 这里的思路:如果命中行附近出现赋值符号或引号包裹, 则认为上下文成立;否则标记为low。 """ joined = "\n".join(context_lines) strong_ctx = bool(re.search(r"[=:]|=>|SET\s|value\s*=", joined, re.I)) if not strong_ctx: hit["severity"] = "low" hit["note"] = "no_assignment_context" return hit

这个逻辑把“疑似赋值”和“只是提到敏感词”分开。实际操作时,我让扫描器按命中行上下各取2行拼成一小段,再丢给这条规则自带的校验函数。如果上下文里没有赋值符号,这条命中最多算 low,不进开发的处理列表。加了这层后,我处理过的一套老系统误报直接从每周300条降到每周30条左右。

再进一步的做法是双规则交叉验证:一条正则负责定位,另一条正则或熵算法负责确认。比如先按 JWT 结构命中一段文本,再对 header 部分做 Base64 解码,解码后能解出真实算法头才算有效命中,解不出来的直接丢弃。这样的验证逻辑比单纯堆规则更贴近“真实泄露”的场景。

修复验证也不能漏。开发改完一处硬编码口令后,如果只重扫当前工作区,等于没验证——完整做法是重扫一遍含该文件的 Git 历史,确认这条命中的对象不再存在于任何历史提交里。现在拿到任何扫描报告,我第一件事不是看命中列表,而是看忽略列表和报告分组,这个习惯是用一次误报刷屏换来的。先确认“哪些文件没被扫”,比确认“扫出了什么”更能判断报告可信度。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询