VicroCode
让代码创造价值
VicroCode是一个轻量级代码在线发布、交易平台,开箱即用,免部署、免服务器、免备案,支持接入智能体、通用管理系统、游戏等多种项目
稍候片刻,您可先学习AI编程手册
正在加载中...

AI MARKET GUIDE

AI编程助手把API Key写进代码:发布前我用一个Python脚本做密钥体检

V2EX上有人被coding agent把1Password里的密码顺手写进了开源项目。我把自己项目翻了一遍,发现.env里的key也可能被agent写进文件和日志。这篇复盘讲清楚:怎么用一个轻量Python脚本扫可疑密钥、把结果记进SQLite逐条核对,为什么发布前这道体检必须自己做一次。

先说那个让我后背发凉的帖子

刷V2EX看到一个分享创造帖,标题挺直白:密码放在1Password,最后被DS写进了GitHub。楼主的朋友从一个agent迁到另一个,让LM帮忙干活,结果它从1Password里把密码读出来,顺手写进了项目文件。

后来项目开源,谁也没注意,密码就跟着代码一起推上去了。

我看完第一反应是笑,第二反应是赶紧去翻自己的项目。因为楼主后面那句话说的就是我:我一般.env会给agent放一些key,它拿到以后会不会写文件、打日志,我也没法每一步都盯着。

对,这才是真正扎心的地方。不是agent坏,是我们默认它只读不写,可它干活的时候,随手把一段配置贴进README、把报错连同环境变量打进日志、生成一个示例文件里带上真实key,这些都太正常了。

你审代码的时候盯着业务逻辑,谁会一行去看有没有夹带私货。

那个帖子底下有人甩了gitleaks、sops这些工具,也有人质疑楼主是不是在推广自己的项目。工具好不好用先不论,我更在意的是:发布前这道检查,到底该谁来做。

我的结论是,别指望agent自己交代,也别全押在某个供应商身上,自己在推送之前跑一遍才踏实。

我不打算装一堆工具,先写个能跑的脚本

说真的,一开始我想直接上现成的扫描工具。但独立开发者的项目往就几个仓库,装CI、配规则、调误报,投入产出不划算。

我想要的是:随手能跑、结果我能看懂、扫完还能留个账本方便逐条核对。

所以我干脆自己写了个小脚本。逻辑不复杂:遍历项目文件,用几组正则去匹配常见的密钥形态,把命中的文件路径、行号、匹配到的片段记下来。

为了不把脚本环境和我本机搞混,我是直接放到Python在线运行里先验证逻辑的,跑通了再挪回项目根录。

核心的匹配规则大概长这样:

import re
import os

# 几组常见密钥形态,按需增删
PATTERNS = {
    "OpenAI": re.compile(r"sk-[A-Za-z0-9]{20,}"),
    "通用API_KEY赋值": re.compile(r"(?i)(api[_-]?key|secret|token|password)\s*[=:]\s*['\"]?[A-Za-z0-9_\-]{16,}"),
    "AWS": re.compile(r"AKIA[0-9A-Z]{16}"),
    "私钥头": re.compile(r"-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----"),
}

# 不用扫的目录和文件
SKIP_DIRS = {".git", "node_modules", "venv", "__pycache__", "dist"}
SCAN_EXT = {".py", ".js", ".ts", ".json", ".yaml", ".yml", ".md", ".env", ".txt", ".log"}
def scan_file(path):
    hits = []
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            for lineno, line in enumerate(f, 1):
                for name, pat in PATTERNS.items():
                    if pat.search(line):
                        hits.append((name, path, lineno, line.strip()[:120]))
    except Exception:
        pass
    return hits

def walk(root):
    all_hits = []
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fn in filenames:
            ext = os.path.splitext(fn)[1]
            if ext in SCAN_EXT or fn == ".env":
                all_hits.extend(scan_file(os.path.join(dirpath, fn)))
    return all_hits

if __name__ == "__main__":
    for h in walk("."):
        print(h)

第一次跑完,屏幕哗滚过几十条。我当时心里咯噔一下,以为漏得这么厉害。

结果一条看下去才发现,绝大多数是误报。这就引出下一个问题。

误报才是真正花时间的地方

本以为难点是写正则,结果真正磨人的是处理误报。

我扫出来的东西里,有一大半根本不是密钥。比如示例文档里写的`api_key = "your_api_key_here"`,比如测试用的`token = "1234567890abcdef"`,还有一些哈希值、UID、静态资源的长字符串,全被通用规则抓了进来。

如果你不管这些,扫描结果就变成一堆噪音,看两遍就不想看了,最后等于没扫。

我的处理办法是加一层白名单和熵值判断。占位符类的固定词直接跳过,太规整的、明显是示例的也降权。

给每条命中打个初步的可信度标记,真正需要人眼确认的那几条就浮上来了。

PLACEHOLDER = re.compile(r"(?i)(your|example|placeholder|xx|test|dummy|changeme|here)")

def looks_fake(snippet):
    # 命中占位符词,基本可以判定为示例
    return bool(PLACEHOLDER.search(snippet))

加上这层过滤之后,几十条噪音收敛到个位数。剩下的那几条,才是我真正要一条核对的。

这里我要多说一句:宁可误报多一点,也别为了干净把规则收得太紧。漏一个真key的代价,比多看几条假的大太多了。

把扫描结果记进SQLite,当成一个核对清单

光打印到屏幕不够。项目一大,命中几十条,你看着就串行了,哪条核过哪条没核,全靠脑子记,很容易漏。

所以我给脚本加了个落库的动作,把每条命中写进一张SQLite表,带上状态字段。

import sqlite3

def init_db(db="secret_audit.db"):
    conn = sqlite3.connect(db)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS findings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            rule TEXT, file TEXT, line INTEGER,
            snippet TEXT, status TEXT DEFAULT 'pending'
        )
    """)
    return conn

def save(conn, hits):
    con.executemany(
        "INSERT INTO findings(rule,file,line,snippet) VALUES(?,?,?,?)",
        hits
    )
    conn.commit()

这么做的好处很实在:每条记录有个status,我核过一条就把它标成confirmed或者ignored,下次接着核不用从头来。真发现是密钥的,就去对应文件行号处理,处理完回来更新状态。

整个过程像在勾一张checklist,心里有底。

如果不想全用命令行看表,直接用SQLite编辑器打开这个库,表结构和每一行目了然,筛选pending状态、按文件排序都很顺手。我个人习惯扫完先在编辑器里过一遍全局,再回到脚本里批量更新状态,比纯命令行舒服。

扫描、过滤、落库、核对,这一套下来,一个中小项目大概十几分钟能走完。比起真泄露之后一个个换key、盯供应商账单,这点时间不值一提。

为什么这道体检必须你自己做

聊到这我想把观点摆明:发布前的密钥体检,别外包给agent,也别只信一道供应商侧的防护。

第一,agent不会主动告诉你它把key写哪了。它的目标是完成任务,不是替你做安全审计。

那个V2EX帖子里,密码被写进文件、跟着开源推出去,全程没有任何一步提醒。你不查,就是不知道。

第二,泄露的成本是不对称的。写脚本扫一遍是十几分钟的事,key一旦进了公开仓库,轻则挨个换、重置服务,重则被人跑额度、上账单。

前面另一个帖子里有人一天烧掉2.7亿token,虽然是模型涨价的锅,但你想,要是这token是被泄露的key跑掉的,那才叫冤。

第三,这件事没法一劳永逸。每次要对外发布、开源、分享链接之前,项目状态都变了,该重扫就得重扫。

我现在的习惯是把这个脚本当成发布流程的一道固定关卡,扫干净、SQLite里没有pending的真命中,才走下一步托管发布。

顺带说下,我这个扫描脚本本身如果想做成一个能随时点开跑的小工具,而不是每次翻本地文件,可以把它挂成一个简单的Web小页面走Web应用托管,输入项目路径或者贴文件进去就出结果,团队里别的人也能用。当然这属于加分项,核心还是那个脚本和那张核对表。

今天就能做的一个小动作

别等到下次开源前才想起来。现在就打开你手头那个最常用的项目,把上面那几组正则跑一遍,重点看`.env`、日志文件、README和各种示例文件。

哪怕只扫出一条`your_api_key_here`这样的占位符,也说明你的扫描逻辑跑通了。真扫出一条sk开头的活key,那这十几分钟就算赚回来了。

扫完记得把结果留个账本,别扫完就忘。密钥这东西,你不盯着它,agent可不会替你盯。