h.
loading
← 返回博客

我用零依赖写了一个 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 里加了个不可信字段)。这恰恰是安全审计工具最该做的:不是只有”真漏洞”才报,把风险信号标记出来让人复核。

四、收获

  1. 熵是很好的”像不像密钥”的信号,零成本(不调 API)就能过滤掉大量误报、抓出无格式密钥。
  2. 静态工具的价值在于”提醒 + 让人复核”,不在于零误报。aiscan 扫出 ledger-app 那个 SQL 拼接时,我的第一反应是”这个写法确实该改”。
  3. 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 / 贡献规则。