我用零依赖写了一个 AI 代码安全审计工具 aiscan
在给 ledger-app 和 purple-team-lab 配 CI 之后,我有了一个新想法:能不能用零依赖、纯 Node 内置模块,写一个 AI 辅助的代码安全审计工具? 于是有了 aiscan。
这篇文章讲三件事:为什么做、怎么实现(特别是”AI”部分到底用了什么)、以及它扫描真实项目时发现了什么。
一、为什么做
安全工具普遍太重:Semgrep 要下载二进制,SonarQube 要起服务,gitleaks 要装 Go 运行时。我想要一个任何机器都能跑、没有安装负担、node aiscan.js . 就能扫的工具。
另一个动机是 dogfooding:aiscan 的 CI 里有用 aiscan 扫描 aiscan 自己源码的步骤。自己不用自己的工具,怎么好意思让别人用。
二、实现:三根支柱
1. 正则规则引擎(14 条规则)
每条规则声明 id / severity / category / regex / message / recommendation / cwe。扫描时逐文件匹配,命中即产出发现:
🔴 [CRITICAL] SQL 注入:字符串拼接查询
demo.js:17:17
SQL 语句使用拼接方式,存在注入风险
💡 改用参数化查询 / Prepared Statement
规则覆盖密钥泄漏、SQL/命令注入、XSS、弱加密(MD5/DES)、路径穿越、关闭 TLS 校验、日志泄漏、依赖版本未锁死等 14 类。
2. “AI”部分:香农熵启发式
真正的难点是识别那些没有固定格式的密钥 —— 比如 ghp_xK9mQ2vL8nR4tW7zB1cE5hJ3fG6sD8aP2qW4eR6tY9uI 这种高熵随机串。它不像 AWS Key 有 AKIA 前缀,纯靠正则抓不到。
我用了香农熵做启发式判断。密码学随机令牌的字符分布近似均匀,熵高;普通英文/代码片段分布有偏,熵低:
// 香农熵(bit/字符)
export function shannonEntropy(str) {
const freq = new Map();
for (const ch of str) freq.set(ch, (freq.get(ch) || 0) + 1);
let entropy = 0;
for (const count of freq.values()) {
const p = count / str.length;
entropy -= p * Math.log2(p);
}
return entropy;
}
判断逻辑:熵 ≥ 4.2 + 长度 ≥ 20 + 大小写/数字/符号混合 → 疑似密钥,并输出 0~100 的置信度:
x9K5mQ2vL8nR4tW7zB1cE5hJ3fG6sD8a→ 熵 4.88,置信度 85%abcdefghijklmnopqrstuvwxyz→ 熵 4.70(字符多但无混合),置信度 65%,被过滤
这套启发式不需要调用任何大模型 API,但效果上模拟了”AI 看一长串字符判断它像不像密钥”的能力。
3. GitHub Action 一键接入
写了 action.yml,任意仓库一行 YAML 就能跑:
steps:
- uses: hedongli1/aiscan@v1
with:
path: .
fail-on: high
跑完自动把 SARIF 2.1.0 报告上传到 GitHub Security 标签页(Security → Code scanning → aiscan),无需额外配置。
三、真实运行结果
扫描自己:dogfooding
$ node bin/aiscan.js lib bin
aiscan: 0 个发现 | 安全评分 100/100(等级 A)
在加了 .aiscanignore(忽略规则库自身的安全关键词描述)之后,源码零发现。
扫描漏洞演示文件
对 fixtures/demo.js(故意埋入 11 种漏洞):
📊 汇总:11 个发现 | 🔴critical=5 🟠high=5 🟡medium=1 ⚪low=0
🏆 安全评分:0/100(等级 F)
扫描真实项目:ledger-app
最有意思的部分 —— aiscan 扫我的真实项目 ledger-app 时,在 server/src/routes.js:42 报了一个 critical SQL 注入:
.prepare( `SELECT * FROM transactions WHERE ${conds.join(' AND ')} ORDER BY date DESC, id DESC LIMIT 500` )
.all(...args);
我仔细复核了一下:这里 conds 是从白名单字段构造的,参数通过 ...args 参数化传入,实际风险较低。但它确实暴露了一个工程问题 —— 这种拼接写法很容易在将来引入真实漏洞(比如有人往 conds 里加了个不可信字段)。这恰恰是安全审计工具最该做的:不是只有”真漏洞”才报,把风险信号标记出来让人复核。
四、收获
- 熵是很好的”像不像密钥”的信号,零成本(不调 API)就能过滤掉大量误报、抓出无格式密钥。
- 静态工具的价值在于”提醒 + 让人复核”,不在于零误报。aiscan 扫出 ledger-app 那个 SQL 拼接时,我的第一反应是”这个写法确实该改”。
- dogfooding 是最诚实的宣传。aiscan 的 CI 里就有一行
node bin/aiscan.js lib bin --severity=high,它自己先扫自己。
试试看
npx aiscan . # 扫当前目录
npx aiscan src --sarif # 输出 SARIF
项目地址:github.com/hedongli1/aiscan · MIT 协议 · 欢迎 Star / Issue / 贡献规则。