基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
编程提效2026-08-04 00:32

本地 LLM 双层密钥检测 Pre-commit Hook

dev.to · AI#安全#自动化#Git
Editor brief · 编辑速览

用 Ollama 在 pre-commit 阶段作为第二层验证,解决 regex 假阳性问题,同时确保密钥数据不离开本机。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

我的正则 secret scanner 曾经阻止过一次 commit,原因是某个测试文件中包含字符串 sk_test_EXAMPLE_KEY_DO_NOT_USE。同一周,另一个项目的一位同事把真实的 Etherscan API key 写进了一个硬编码 URL 并提交,而 scanner 没有发现,因为它不符合任何已知的 key 格式。这两次失败恰好概括了基于正则的 secret scanning:不重要的地方警报不断,真正重要的地方却悄无声息。

标准解决方案是维护一个永远只增不减的 allowlist 文件,再加上一群已经形成肌肉记忆、随手就会输入 git commit --no-verify 的开发者。一旦大家习惯性绕过 hook,scanner 就只剩装饰作用了。

于是我尝试了另一种方式:保留 regex scanner,但加入一个小型本地 LLM,让它提供第二意见。regex 阶段负责判断哪些内容值得检查,模型负责判断它究竟是不是真正的 secret。只有被标记的文件才会交给模型,因此 hook 依然很快;而且模型是运行在我自己机器上的 Ollama,所以 staged diff 永远不会离开我的笔记本电脑。最后这一点对我来说没有商量余地——把可能包含 secret 的 diff 发送给云端 API,让它帮你检查其中有没有 secret,这简直是一个自己就能写完的笑话。

用一段话说明架构

Stage 1 会对 staged changes 执行一次刻意偏谨慎的正则扫描:高熵字符串、已知的 key 前缀、PRIVATE KEY 区块以及可疑的变量名。Stage 2 会把每个被标记的 hunk 连同前后几行上下文发送给通过 Ollama 运行的 qwen2.5-coder,并附上一段分类 prompt。判定为 SECRET 就阻止 commit,判定为 FALSE_POSITIVE 就允许通过。绝大多数 commit 根本不会触发 Stage 1,因此完全没有额外延迟。

.git/hooks/pre-commit(或者通过你选择的 hook manager 接入):

#!/usr/bin/env bash
set -euo pipefail

MODEL="${SECRET_HOOK_MODEL:-qwen2.5-coder:7b}"

# Stage 1: cheap and paranoid. Wide patterns, we WANT false positives here.
PATTERNS=(
  'AKIA[0-9A-Z]{16}'                      # AWS access key
  '-----BEGIN( RSA| EC| OPENSSH)? PRIVATE KEY-----'
  '(api[_-]?key|secret|token|passwd|password)["'"'"']?\s*[:=]\s*["'"'"'][^"'"'"']{16,}'
  '0x[a-fA-F0-9]{64}'                     # possible EVM private key
  '[A-Za-z0-9+/]{40,}={0,2}'              # high-entropy base64-ish
)

flagged=()
while IFS= read -r file; do
  [[ -f "$file" ]] || continue
  for p in "${PATTERNS[@]}"; do
    if git show ":$file" | grep -qE "$p"; then
      flagged+=("$file")
      break
    fi
  done
done < <(git diff --cached --name-only --diff-filter=ACM)

[[ ${#flagged[@]} -eq 0 ]] && exit 0   # fast path: nothing suspicious

# Stage 2: ask the local model about each flagged file's staged content.
block=0
for file in "${flagged[@]}"; do
  verdict=$(git show ":$file" | ollama run "$MODEL" "$(cat <<'PROMPT'
You review a file staged for a git commit. Decide if it contains a REAL
credential that must not be committed.

REAL secrets: live API keys, private keys (including 0x-prefixed 64-hex
EVM keys), tokens, passwords, connection strings with embedded passwords.

NOT secrets: placeholders (YOUR_KEY_HERE, xxx, changeme), documented
example keys, test fixtures clearly labeled as fake, public addresses,
hashes of public data, template variables like ${API_KEY}, lockfile
integrity hashes.

First line of your answer must be exactly SECRET or FALSE_POSITIVE.
Second line: one short reason.
PROMPT
)")
  if [[ "$verdict" == SECRET* ]]; then
    echo "BLOCKED: $file"
    echo "$verdict" | sed -n '2p' | sed 's/^/  reason: /'
    block=1
  fi
done

if [[ $block -eq 1 ]]; then
  echo ""
  echo "Commit blocked. If this is wrong, re-run with SECRET_HOOK_MODEL"
  echo "set to a bigger model, or use --no-verify and accept the risk."
  exit 1
fi
exit 0

这里面有两个细节,比看上去重要得多。

扫描 staged content,而不是 working tree。git show ":$file" 读取的是 index。如果扫描磁盘上的文件,你可能会因为尚未 staged 的临时内容而阻止 commit;同时也会漏掉另一种情况:secret 已经 staged,但在 working copy 中已被删除。

exit code contract 就是完整的接口。exit 0 表示允许 commit,exit 1 表示阻止 commit,而模型输出的自由文本本身永远不能决定任何事情。我只解析第一行,并要求它必须是两个 token 之一。小型本地模型偶尔会输出一整段含糊其词的内容,强制它在第一行给出 machine-readable 结果,才是让这类模型能够用于 pipeline 的关键。

Prompt 主要列举的都是非 secret

注意,这段 prompt 用在说明什么“不是 secret”上的文字,比说明什么“是 secret”还要多。这是刻意为之。regex 阶段已经确保模型看到的所有内容都像是 secret,因此模型真正的任务,是识别 placeholder、fixture 和 template。采用这种方式描述任务之后,被误判并阻止的情况大幅减少。我在构建 spectr-ai 时发现了这个模式:相比开放式地询问“这危险吗?”,明确告诉小型模型应该排除哪些内容,它们的表现要好得多。

对于 Web3 开发者,0x 加 64 个十六进制字符的 pattern 值得单独说明。交易 hash、storage slot 和 private key 在正则眼里完全一样。模型则可以结合变量名和周围代码,区分 DEPLOYER_PRIVATE_KEY = 0x... 与 KNOWN_TX_HASH = 0x...。仅仅是这一项区分,就贡献了这个 hook 为我带来的大部分价值,因为 regex scanner 要么会把区块链代码库中的每一个 32-byte 十六进制值都标记出来——令人无法忍受;要么一个都不标记——毫无用处。

延迟方面,在我的机器上(WSL2、中端 GPU),模型完成预热后,使用 7b 模型检查一个被标记的文件大约需要两到四秒;如果 Ollama 还需要先加载模型,耗时会更长。干净的 commit 不会付出任何时间成本,因为 Stage 1 会直接 short-circuit。涉及 .env.example 或测试 fixture 的 commit 则需要多花几秒。对我来说这可以接受,但你未必这么认为。如果你的团队每小时要 commit 四十次,应该让模型阶段保持 async,或者只提供 advisory。

关于 1.5b 与 7b,我最初尝试的是 qwen2.5-coder:1.5b,因为它几乎能立刻响应。但它太热衷于迎合要求了:即使是明显虚假的 fixture,也经常被标记为 SECRET。长此以往,我迟早会开始绕过自己的 hook,这就完全违背了它的初衷。7b 在理解“这个文件位于 tests/fixtures/ 中,而且变量名是 FAKE_KEY”之类的上下文时,表现明显更好。这项任务很少需要运行模型,所以我愿意为更大的模型付出额外成本。

它仍然可能在两个方向上出错。一个本地 7b 模型并不是 security boundary。格式奇怪的真实 key 仍可能漏网,因此这个 hook 只是对平台端扫描能力(GitHub push protection 等)的补充,而不是替代。它真正修复的是人为环节:这个 hook 很少发出警报,所以当它真的报警时,我会停下来认真查看,而不是条件反射般地去输入 --no-verify。一个大家信任并愿意遵守的 scanner,胜过一个更严格、却被所有人绕过的 scanner。

关于确定性,LLM 对边界输入的判定可能会在不同运行之间发生变化。对于一个会阻止 commit 的 hook,我可以接受这一点,因为边界案例恰恰就是我希望人类重新检查的内容。

到目前为止,我已经运行这套方案几个月了。regex 阶段每周会触发几次,模型几乎每次都会推翻它的判断;而唯一一次模型判定为 SECRET 时,它确实是对的:那是一个真实的 RPC URL,其中嵌入了 API key,藏在我正准备重新启用的一个旧测试中。

你目前使用什么 secret-scanning 方案?说实话,你有多经常绕过它?

对于后续操作,你可以考虑屏蔽此人和/或举报滥用行为。

Original source

本文由 AI 翻译整理,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
44 个生产级前端 UI/UX 实战技能集
下一篇
AWS Bedrock 推出自动化规则优化能力

资讯速览

dev.to · AI · 2026-08-04 00:32

AI 评分 8.0 / 10

相关标签

#安全#自动化#Git

继续阅读

  • AWS 上从零构建 AI Agent 的完整指南
  • 44 个生产级前端 UI/UX 实战技能集
  • AWS Bedrock 推出自动化规则优化能力
  • AI 工具使用中的敏感数据自动脱敏方案
  • 调试 CLAUDE.md 失效:用 /context 排查配置加载问题