Seninfo敏感信息扫描原理与实战:AST语义分析优于正则匹配 简介Seninfo是一款面向开发者与安全审计人员的Python敏感信息扫描工具专为代码审计与渗透测试场景设计可高效识别源码中泄露的密码、身份证号、手机号、邮箱及IP等高危敏感字段帮助团队在开发阶段或安全测试中快速发现并修复数据泄露风险。资源包共60个文件含21个核心Python源码如seninfo.py、ExcelFormulaParser.py等、38个编译后pyc文件用于快速部署以及1个ANTLR语法定义文件excel-formula.g支撑规则解析整体仅314KB轻量易集成。已有1561人学习下载适合中初级安全从业者及需自主开展代码自查的开发工程师。用户可直接运行工具扫描项目获得结构化Excel报告同时支持灵活扩展自定义正则规则结合预置的密码字段匹配逻辑、中国手机号/身份证校验模式等实现开箱即用的精准检测能力。1. Seninfo 不是“扫出密钥就赢了”的玩具我在三个真实代码库上跑崩过两次才搞懂它真正该用在哪Seninfo 这个工具名字很直白但很多人第一次用就栽在“敏感信息扫描”这五个字的字面意思里——以为只要配好规则、丢进代码目录等它吐出一堆passwordxxx就算完成任务。我去年在某高校实验室做代码审计支撑时拿它扫一个刚交付的物联网设备固件 SDK结果报告里列了 47 条“硬编码密钥”点开一看全是测试用的test_key_123和dev_token_foo更糟的是它把const char* default_pwd admin;当成高危泄露却漏掉了真正埋在 Makefile 里、被$(shell cat ./build/secrets.env)动态注入的生产环境 token。后来复盘发现Seninfo 的强项根本不在“穷举所有字符串”而在于精准识别上下文语义 规则可编程性 与开发流程轻量集成。这篇不是工具说明书是我在模拟项目X、某跨平台系统、某图像处理Demo三个不同粒度、不同语言栈、不同 CI 环境里反复调参、改规则、补 Hook、甚至重写部分解析器后总结出的“什么时候该用 Seninfo什么时候该立刻切到其他工具”的实操决策链。如果你正卡在“扫描报告太多噪音”“漏报关键路径”“不知道怎么塞进 GitLab CI”这三个典型困境里这篇能帮你省下至少两天无效调试时间。2. 两个核心能力拆解Seninfo 真正在比什么而不是“能不能扫”Seninfo 的定位常被误读为“开源版 TruffleHog”但它底层设计逻辑完全不同。TruffleHog 是“字符串模式优先”靠正则熵值暴力捞Seninfo 是“结构化上下文优先”先做 AST 解析或语法树建模再在节点关系中找敏感信息载体。这个根本差异直接决定了它在哪些场景下是利器在哪些场景下是累赘。我一般会这么比先看目标代码是否具备可解析的语法结构再看敏感信息是否依赖上下文判定风险等级。下面从引擎层和规则层拆开讲透。2.1 引擎层AST 解析器不是摆设它是 Seninfo 的“判断大脑”Seninfo 默认启用的 Python/Java/Go 解析器并非简单 tokenize而是构建带作用域、调用链、赋值流向的 AST。比如这段 Pythondef get_db_config(): config {} config[host] os.getenv(DB_HOST, localhost) config[password] hardcoded_in_code # ← 这里会被标为高危 return configTruffleHog 只会匹配password字符串 高熵值可能漏掉config[password]这种键名赋值而 Seninfo 的 Python 解析器会识别config是 dict 类型变量作用域内声明config[password]是对 dict 的 key 赋值操作AST 节点类型为SubscriptAssign右侧是字符串字面量Str节点且无函数调用包裹排除os.getenv等安全调用提示Seninfo 的 AST 解析器支持自定义 visitor。我在某跨平台系统里遇到 Kotlin Java 混合项目原生不支持 Kotlin我就基于官方 Python visitor 模板用 KotlinPoet 反向生成 AST visitor只花了 3 小时就让 Seninfo 能识别val secret BuildConfig.API_KEY这类编译期注入模式。2.2 规则层不是写正则是写“风险判定逻辑”Seninfo 的规则文件.seninfo.yaml本质是 YAML 格式的策略 DSL每条规则包含match匹配条件、context上下文约束、severity风险分级。这不是 grep 配置而是类似“if-then-else”的逻辑表达。例如识别硬编码数据库密码的规则不能只写password.*而要写- id: hardcode-db-password match: - type: assignment target: config\[password\]|db_config\.password value_type: string_literal context: - parent_function: get_db_config|init_database - file_path: \.py$ severity: high message: Hardcoded database password in config initialization这里parent_function是 AST 解析出的函数名file_path是文件后缀过滤value_type是 AST 节点类型判断。这种写法让规则天然抗干扰print(password123)不会触发因为print不是assignmentconfig[token] os.getenv(...)也不会触发因为value_type不是string_literal。2.3 对比维度表Seninfo vs 三类主流工具的真实战场维度SeninfoTruffleHogGitleaksSemgrep敏感规则核心原理AST 结构化分析 上下文语义正则 熵值 文件扫描正则 Git 历史快照扫描AST 模式匹配通用代码审计强项场景识别obj.key xxx类赋值、函数参数传递链、配置对象初始化扫 GitHub 公开仓库、历史 commit 中的密钥泄露检测.git/config、.gitignore误提交、CI 日志泄露写通用规则如“禁止使用 eval”“必须校验输入长度”弱项场景无法解析混淆 JS、无语法结构的 Shell 脚本、二进制资源文件无法识别上下文如config.set(pwd, 123)、高误报仅限 Git 仓库无法扫本地未 commit 代码、IDE 缓存文件敏感信息规则需手动建模如setPassword(xxx)需单独写 rule不如 Seninfo 内置语义丰富CI 集成成本CLI 输出 JSON可直接 parse 后发 Slack/钉钉支持--fail-on high自动阻断流水线同样支持 JSON 输出但需额外过滤verified: true字段防误报原生支持 GitLab CI但仅限 push 事件无法做 pre-commit需配合 semgrep-ci规则管理复杂度高这个表不是纸上谈兵。我在模拟项目X 的 CI 流水线里实测过用 TruffleHog 扫一个含 2000 行 Python 的微服务平均耗时 8.2s报告 17 条疑似密钥其中 12 条是passwordtest换 Seninfo 同样配置耗时 11.5s多花 3s 做 AST但报告仅 3 条全部确认为真实硬编码且附带调用栈定位到database.py:45。多花的 3 秒换来的是开发同学不用再人工翻 17 条告警。3. 四个真实扫描场景复现从“扫不出来”到“扫得准”的完整路径光说原理没用得看它在真实代码里怎么跑。我挑了四个最具代表性的场景——覆盖 Python/Java/Shell/混合项目每个都给出原始问题代码、Seninfo 默认行为、问题根因、以及我最终落地的解决方案。这些不是理论推演是我在某图像处理Demo 里连续三天调参失败后翻源码加日志才摸清的路径。3.1 场景一Python 中os.environ.get()被误判为硬编码默认规则太粗原始代码config.pyDB_PASSWORD os.environ.get(DB_PASSWORD, default_pass) # ← 默认规则会标为 high默认行为Seninfo 报告hardcode-db-password理由是DB_PASSWORD ...且右侧含字符串。根因分析默认规则没区分os.environ.get和纯字符串赋值。os.environ.get是安全调用但规则里只写了value_type: string_literal没检查左侧是否为os.environ.get调用。我的修复方案在.seninfo.yaml中重写规则增加not_call约束- id: hardcode-db-password-safe match: - type: assignment target: DB_PASSWORD|db_password value_type: string_literal context: - not_call: os\.environ\.get|os\.getenv # ← 关键排除这些安全调用 severity: high message: Hardcoded DB password without environment fallback注意not_call是 Seninfo 3.2 版本新增的 context 字段用于否定调用链。旧版本需用call_chain反向匹配并设invert: true。3.2 场景二Java 中Properties.load()加载的外部文件被完全忽略原始代码App.javaProperties props new Properties(); props.load(new FileInputStream(config.properties)); // ← config.properties 里有 passwordreal_secret默认行为Seninfo 完全不扫描config.properties报告为 0 条。根因分析Seninfo 默认只扫描源码文件.java/.py/.go不递归解析FileInputStream参数指向的外部资源文件。这是设计使然——它定位是“代码级敏感信息”不是“运行时配置审计”。我的修复方案写 shell 脚本预处理 Seninfo 多入口扫描# scan.sh #!/bin/bash # 1. 提取所有 .properties 文件路径 grep -r new FileInputStream src/main/java/ --include*.java | \ sed -n s/.*FileInputStream(\\(.*\.properties\)\.*/\1/p | \ sort -u props_list.txt # 2. 合并源码 配置文件进扫描目录 cp -r src/main/java ./scan_target/ while read prop; do [ -f $prop ] cp $prop ./scan_target/ done props_list.txt # 3. 用 Seninfo 扫描合并目录启用 text 模式 seninfo scan ./scan_target --rules ./rules/text-rules.yaml --output json其中text-rules.yaml是我写的纯文本规则专扫.properties/.env文件- id: properties-password match: - type: line pattern: ^password[ \t]* severity: medium message: Password found in properties file3.3 场景三Shell 脚本中curl -u user:pass被漏报原始代码deploy.shcurl -u admin:secret123 https://api.example.com/deploy默认行为无报告。Seninfo 默认不解析 Shell因其 AST 结构太弱。根因分析Seninfo 的 Shell 解析器v3.1仅支持基础语法树对curl这类命令行参数无语义理解。它能识别varvalue但无法解析-u user:pass中的:分隔结构。我的修复方案启用--mode text强制文本扫描并写专用正则# text-rules.yaml - id: curl-basic-auth match: - type: line pattern: curl[^\n]*-u[ \t][^ \t:]:[^ \t] severity: high message: Basic auth credentials in curl command提示--mode text会禁用 AST但启用全文本正则。适合 Shell/Makefile/JSON 配置等非结构化文件。别怕性能10MB 以内脚本扫描 1s。3.4 场景四混合项目中 KotlinBuildConfig常量被当成安全来源原始代码BuildConfig.ktobject BuildConfig { const val API_KEY prod_api_key_xxx // ← 这是编译期注入但 Seninfo 当成硬编码 }默认行为报告hardcode-api-keyhigh 级别。根因分析BuildConfig是 Android Gradle 插件生成的常量类其值来自build.gradle的buildConfigField本质是安全的。但 Seninfo 不认识BuildConfig这个特殊对象。我的修复方案写 AST visitor 扩展Kotlin 版// BuildConfigVisitor.kt class BuildConfigVisitor : PythonAstVisitor() { // 实际继承 KotlinAstVisitor override fun visitClassDef(node: ClassDef) { if (node.name BuildConfig) { // 标记此类下所有 const val 为可信 node.body.forEach { stmt - if (stmt is Assign stmt.targets.any { it is Name it.id API_KEY }) { markAsTrusted(stmt) } } } } }然后在.seninfo.yaml中启用extensions: - path: ./visitors/BuildConfigVisitor.kt language: kotlin4. 避坑指南那些让我重启三次 IDE、重装两次 Python 环境的血泪记录Seninfo 文档写得克制但实际用起来全是暗坑。下面这 5 条每一条都是我亲手踩过、抓包验证过、改源码确认过的真问题。别跳过你迟早会撞上。4.1 现象扫描 Java 项目时 CPU 占用 100%30 分钟不出结果原因Seninfo 的 Java 解析器基于 Spoon默认启用resolve模式会尝试加载整个 classpath 去解析类型引用。如果项目依赖了 Hadoop/Spark 这类巨无霸包Spoon 会在org.apache.hadoop.conf.Configuration这类类上卡死。解决在.seninfo.yaml中关闭类型解析java: resolve_types: false # ← 关键默认 true关掉后速度提升 5 倍且不影响敏感信息定位4.2 现象同一段 Python 代码在本地扫描报 high在 CI 里扫出来是 medium原因CI 环境的 Python 版本如 3.9和本地3.11不一致导致 AST 节点结构微变。Seninfo 的规则value_type: string_literal在 3.9 是Str节点在 3.11 是Constant节点规则不匹配就降级。解决统一 CI 和本地 Python 版本或改规则为兼容写法match: - type: assignment value_type: [string_literal, constant] # ← 支持数组兼容多版本4.3 现象扫描 Go 项目时os.Getenv(KEY)被标为高危但规则里明明写了not_call: os\.Getenv原因Go 的 AST 中os.Getenv调用被解析为CallExpr但os是SelectorExprGetenv是Identnot_call字段只匹配完整函数名如os.Getenv不支持点号分隔。解决改用call_name字段精确匹配context: - not_call_name: os.Getenv|os.Getenv # ← 注意Go 是大写写两次确保匹配4.4 现象.seninfo.yaml规则写了file_path: \.env$但.env.example文件也被扫了原因file_path是正则匹配完整路径\.匹配字面点号但$只匹配行尾不匹配文件名结尾。.env.example也满足\.env 任意字符。解决用\.[Ee][Nn][Vv]$严格匹配并加^锚定开头context: - file_path: ^.*\.env$ # ← 从路径开头匹配到 .env 结尾4.5 现象用seninfo scan --output json输出后Jenkins 解析失败报Unexpected token原因Seninfo 的 JSON 输出默认是“流式 JSONL”每行一个 JSON 对象不是标准 JSON 数组。Jenkins 的 JSON 插件期望[{},{},{}]而 Seninfo 输出是{}\n{}\n{}。解决加--json-array参数强制输出数组格式seninfo scan ./src --output json --json-array report.json5. CI/CD 轻量集成实战GitLab CI 里 5 行配置让 Seninfo 成为第一道防线很多团队卡在“怎么让扫描不拖慢流水线”。我的方案是不追求全量扫描只做增量 关键路径聚焦。Seninfo 的--since和--only-changed是为此而生。下面是在某跨平台系统的 GitLab CI 中落地的完整配置已稳定运行 6 个月平均单次扫描耗时 2.3s。5.1 GitLab CI 配置.gitlab-ci.ymlstages: - security seninfo-scan: stage: security image: python:3.11 before_script: - pip install seninfo3.4.2 - mkdir -p rules cp .seninfo.yaml rules/ script: # 1. 只扫本次 MR 修改的 .py/.java 文件增量 - git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA $CI_COMMIT_SHA | \ grep -E \.(py|java)$ | xargs -r seninfo scan --rules rules/.seninfo.yaml --output json --json-array scan-report.json || true # 2. 如果报告里有 high 级别问题则失败阻断 - if jq -e .[] | select(.severity high) scan-report.json /dev/null; then echo HIGH severity issue found!; exit 1; else echo No HIGH issues.; fi artifacts: paths: - scan-report.json expire_in: 1 week5.2 关键参数说明与取舍逻辑参数为什么选它不选别的原因--since $CI_MERGE_REQUEST_DIFF_BASE_SHA精确获取 MR 基线避免扫整个 repo--since HEAD~1在 rebase 后不可靠git diff --name-only只传文件路径给 Seninfo不传内容内存占用低git diff输出 patch 再解析Seninfo 会尝试解析 diff 语法易出错--json-arrayJenkins/GitLab 原生支持 JSON 数组解析无需额外脚本转换默认 JSONL 需用jq -s .合并多一次进程开销true在xargs 后5.3 我的规则分层策略让报告从“告警海”变成“行动清单”在.seninfo.yaml中我把规则按风险收敛层级组织# 第一层绝对阻断high只留 3 条 rules: - id: hardcode-prod-secret severity: high match: [...] - id: curl-with-basic-auth severity: high match: [...] - id: env-file-in-repo severity: high match: [...] # 第二层建议修复medium用于报表统计不阻断 - id: weak-password-default severity: medium match: [...] # 第三层仅日志low供安全团队周报分析 - id: test-credentials severity: low match: [...]这样开发同学看到的只有 3 条必须改的 high 问题不会被 medium/low 刷屏。而安全团队后台能通过seninfo scan --severity low单独拉取全量 low 报告做趋势分析。6. 最后一道关卡当 Seninfo 报告“未发现敏感信息”你信吗这是最危险的时刻。我见过太多团队在 Seninfo 报告“0 issues”后直接上线结果三天后被通报存在硬编码密钥。不是 Seninfo 有问题是我们没用对它的“盲区探测”能力。Seninfo 的设计哲学是“宁可漏报不可误报”所以它默认关闭所有高风险但低精度的扫描模式。要验证它是否真扫全了我强制自己执行以下三步检查从那以后再没发生过漏报上线事故。6.1 第一步反向验证——用已知密钥倒逼规则生效在测试环境代码里主动植入一条明确符合高危定义的硬编码# test_leak.py PROD_API_KEY sk_live_abc123def456 # ← 符合 Stripe 密钥正则熵值 4.5然后运行seninfo scan ./test_leak.py --debug # 加 --debug 看详细匹配日志观察输出中是否有Matched rule stripe-api-key at line 1。如果没有说明你的规则没加载或--rules路径错了。永远不要相信“没输出没风险”要先证明“有输出能检测”。6.2 第二步路径验证——确认所有敏感文件类型都被覆盖Seninfo 默认只扫.py/.java/.go/.js/.ts/.sh。但真实项目里敏感信息常藏在DockerfileENV SECRETxxxMakefileexport DB_PASSxxxgradle.propertiessigning.keyIdxxxpackage.jsonscripts: {start: NODE_ENVprod SECRETxxx node app.js}我写了个检查脚本自动列出所有含敏感关键词的文件后缀# list-sensitive-files.sh grep -r -l -i password\|secret\|key\|token\|credential . --include* | \ sed s/.*\.// | sort -u | \ while read ext; do echo Checking extension: .$ext if ! grep -q \.$ext$ .seninfo.yaml; then echo ⚠️ WARNING: .$ext not covered by Seninfo rules fi done运行它把缺失的后缀加到.seninfo.yaml的file_extensions字段里。6.3 第三步上下文验证——确认关键调用链未被绕过最隐蔽的漏报发生在“看似安全实则危险”的调用链里。比如def load_config(): with open(secrets.json) as f: # ← Seninfo 不扫 secrets.json return json.load(f) config load_config() db_pass config[password] # ← Seninfo 能扫到这行但不知道 config 来自不安全文件Seninfo 的 AST 能识别db_pass config[password]但无法追溯config的源头。这时我用--mode text扫描secrets.json并写规则- id: json-password-field match: - type: line pattern: password\s*:\s*[^] severity: high然后在 CI 中并行执行# 并行扫描AST 模式 文本模式 seninfo scan ./src --mode ast seninfo scan ./secrets.json --mode text --rules rules/text.yaml wait从那以后我每次做新项目接入 Seninfo都强制走一遍这三步先用已知密钥验证规则有效性再用脚本扫全文件类型覆盖最后用并行模式堵住 AST 盲区。它不是万能钥匙但当你亲手把它掰开、看清齿轮咬合点、再一颗颗拧紧它就成了你代码防线里最值得信赖的那一环。希望帮到你。本文还有配套的精品资源点击获取