恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
AI 编程工具的 Git 仓库被加密传走了?用 marimo + cpolar 做一次本地可核验的代码快照审计
首页
资讯中心
/
AI 编程工具的 Git 仓库被加密传走了?用 marimo + cpolar 做一次本地可核验的代码快照审计
AI 编程工具的 Git 仓库被加密传走了?用 marimo + cpolar 做一次本地可核验的代码快照审计
发布时间:2026/9/27 7:29:03
AI 编程工具的 Git 仓库被加密传走了用 marimo cpolar 做一次本地可核验的代码快照审计上周在掘金榜上刷到一篇复盘标题很扎眼《ZCode 把整个 Git 仓库加密上传到了阿里云 OSS》。作者把客户端扒开看了看发现它把整个工作区打包加密直传对象存储其中.git目录自己就占了八成多而解密用的私钥只在云端——也就是说本机连自己都被传走了什么都解不开。我看完第一反应不是这工具真坏而是那我这台机器上到底有多少东西是值得被这么打包走的这个问题没法靠感觉回答。你得真有数字.git占多大、里面的大文件缓存有多重、reflog 里躺着多少条你以为已经删掉的提交。所以这篇不聊逆向也不聊那个客户端的对错。我做的事很朴素用 marimo 在本机搭一个只读的审计工作台把某个本地仓库里一旦被打包外传就亏大了的部分统计清楚再用 cpolar 开一条短时隧道让不在同一个局域网的安全同事也能打开这份脱敏结论页复核一遍看完立刻关隧道。先说清楚边界免得你想歪审计对象只有我自己用脚本合成的样例仓库不碰任何真实公司仓库、真实凭据、真实内网地址。我不打算、也无从解密第三方客户端上传的密文。这篇只做本地风险自查提醒你哪些内容值得打包外传。cpolar 隧道只承载脱敏后的聚合统计页体积、占比、条数这类数字短时开放用完即关。下面所有命令和返回都是我在本机真实跑出来的。图本机 marimo 只读审计页 cpolar 短时隧道供异地同事复核脱敏结论的整体链路示意1 先说清楚这篇里 marimo 到底负责哪一段marimo 是一个 reactive Python notebook。官方 README 的定位句很短A reactive Python notebook thats reproducible, git-friendly, and deployable as scripts or apps.——可复现、对 git 友好、能作为脚本或应用部署。很多人的第一反应是哦又一个 Jupyter。但这篇里我不用它画图、不用它做分析看中的是它的三个性质恰好一条一条对上审计场景第一它的笔记本存成纯.py文件。Jupyter 的.ipynb是 JSONgit diff出来是一坨转义字符你根本看不出改了哪行。marimo 的笔记本就是普通 Python 文件评审的人直接git diff就能看懂检查逻辑有没有被偷改。审计工具本身可信审计结果才可信。第二它能以只读应用模式跑起来。官方给了两条命令marimo edit用来本地编辑marimo run用来把笔记本当成只读 web app 启动而且默认绑定127.0.0.1。这个只读 只绑本机的组合正好是我要的结论页给同事看但他改不了任何东西也不从公网侧暴露服务。第三它能在无界面的服务器上执行并导出结果。README 里写明 notebook 可以execute as a Python script官方也提供marimo export系列命令。这意味着整条链路可以没有浏览器、没有账号、不依赖任何 LLM Key 就跑完验证方式是 CLI 加 HTTP是确定性的。一句话总结这一节marimo 在这篇里不是数据分析工具而是一个可信、可读、可无界面复跑的本地审计工作台。这个区分很重要因为它决定了我们敢不敢把自己的仓库交给它去数。图 1marimo 的 reactive DAG 执行模型与本文“只读审计应用”定位示意2 环境准备装 marimo、确认 cpolar 和 git这一节要装的东西很少。真正需要的是一个 Python 环境、一个 marimo、一个 git外加一个已经装好的 cpolar。先看版本确认基础工具都在python3 --version # Python 3.14.3 git --version # git version 2.39.5 (Apple Git-154) cpolar version # cpolar version 3.3.18装 marimo。官方 README 给的是最简一行pip install marimo在 macOS 上如果pip指向的是受管理的环境可以明确装到用户目录python3 -m pip install --user marimo # ... # Successfully installed marimo-0.24.2装完确认一下marimo --version # 0.24.2这里有个小提醒marimo 的 CLI 会装到用户级 bin 目录我这边是~/Library/Python/3.14/bin。如果你敲marimo --version报 command not found先把那个目录加进 PATH而不是急着重装。cpolar 这边本机是 Homebrew 装的控制台面板跑在127.0.0.1:9200curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1:9200 # 2009200 能返回 200说明 cpolar 服务是活的。如果这一步打不开先别往下走——后面隧道会失败而你会在错误的地方找半天原因。cpolar 在 macOS 上的安装方式Homebrew和面板地址官方下载页与文档里都写得很清楚这里不重复搬运。3 造一个合成样例仓库审计对象只能是它审计这件事最怕的就是顺手扫了真实仓库。所以第一步不是装工具而是先造一个干净的、可控的、可以随便折腾的假仓库。我把它写成一个可重跑的脚本放在make-synthetic-repo.sh里。它做六件事对应六种值得被审计出来的东西#!/usr/bin/env bash set -euo pipefail REPO${1:-/tmp/audit-demo-repo} rm -rf $REPO; mkdir -p $REPO; cd $REPO git init -q -b main git config user.email auditexample.invalid git config user.name audit demo git config commit.gpgsign false # 1) 少量受控源码这是我们以为仓库里只有的东西 mkdir -p src docs for i in $(seq 1 5); do printf print(module %s)\n $i src/mod_$i.py; done printf # demo project\n README.md git add -A git commit -qm init # 2) 正常提交历史写进 reflog 的轨迹 for c in 1 2 3 4 5 6; do printf print(rev %s)\n $c src/mod_1.py git commit -qam change $c done # 3) 被 reset 丢弃的提交你以为删了其实还在 reflog 里 for r in $(seq 1 3); do printf print(temp %s)\n $r src/tmp_rev.py git add -A; git commit -qm temp $r git reset -q --hard HEAD~1 done # 4) 模拟 LFS 大对象本地缓存合成占位不是真实 LFS 协议 mkdir -p .git/lfs/objects head -c 3000000 /dev/urandom .git/lfs/objects/blob_a.bin head -c 1500000 /dev/urandom .git/lfs/objects/blob_b.bin head -c 13000000 /dev/urandom .git/lfs/objects/blob_c.bin # 5) 一个像样的工作区让占比统计有意义 # 脚本里用一小段 Python 生成 src/app_*.py、src/assets/img_*.bin、docs/manual.md # 6) 两个本地专用、不该外传的文件 printf TOKENdemo-not-a-real-secret\n .env.local printf local editor state\n .idea-workspace.local跑起来看一眼./make-synthetic-repo.sh /tmp/audit-demo-repo # synthetic repo ready at /tmp/audit-demo-repo cd /tmp/audit-demo-repo git status --porcelain # ?? .env.local # ?? .idea-workspace.local这两行输出就是要的效果工作区里明晃晃躺着两个没被跟踪的本地文件。真实项目里这类文件经常就是.env、本地配置、编辑器状态——它们不在版本控制里但打整个目录的包时一定会被带上。这里是我最想让你停一下的地方。把仓库交给任何打包上传的工具之前先问自己一句这个目录里除了git ls-files列出来的东西还躺着什么这也是整篇审计脚本第一件要回答的事。图 2合成样例仓库的目录结构与 .git 内部布局标注4 写审计脚本统计 .git/objects、lfs、reflog现在开始写真正的审计逻辑。我先用普通 Python 写一版因为无界面、可断言比 notebook 更适合先跑通。脚本核心就三块受控源码有多大、.git里三块隐形仓库有多重、工作区里有没有不该跟包的本地文件。# audit_cli.py节选 def audit(repo: Path) - dict: # 1) 受控源码git 跟踪的文件 tracked [f for f in git(repo, ls-files).splitlines() if f] # 2) 未跟踪但真实存在的本地文件.env、编辑器状态之类 untracked [f for f in git(repo, ls-files, --others, --exclude-standard).splitlines() if f] # 3) .git 里的三个隐形仓库 objects_bytes sum(f.stat().st_size for f in (repo / .git / objects).rglob(*) if f.is_file()) lfs_bytes sum(f.stat().st_size for f in (repo / .git / lfs).rglob(*) if f.is_file()) reflog_lines [l for l in git(repo, reflog).splitlines() if l] # 4) 占比 git_bytes du_kb(repo / .git) * 1024 git_share round(git_bytes / (git_bytes worktree_bytes) * 100, 1) lfs_share round(lfs_bytes / git_bytes * 100, 1) ...为什么专门盯这四样东西值得说清楚.git/objects是 Git 的内容寻址对象库。它存的是所有历史版本不只是当前代码。你在工作区删掉的文件、改过的旧版本对象还在里面。.git/lfs是大文件对象的本地缓存。很多人以为我把那个大视频从仓库里删了但只要缓存还在它就会跟着.git一起被打包走。reflog是引用日志。那些被reset --hard丢掉的提交在工作区里看不见了但在 reflog 里还能捞回来——对审计方来说这就是你以为删了的东西的清单。未跟踪的本地文件是最容易被忽略的一类因为它们根本不在git ls-files里。跑一遍python3 audit_cli.py /tmp/audit-demo-repo --json输出原文照抄已格式化{ tracked_files: 37, untracked_files: 2, tracked_bytes: 3120670, worktree_bytes: 3120718, local_untracked_bytes: 48, objects_bytes: 3007188, lfs_bytes: 17500000, git_bytes: 20832256, reflog_lines: 14, unreachable_objects: 0, git_share_pct: 87.0, lfs_share_of_git_pct: 84.0, flags: [ .git 占比 87.0%超过 50% 红线, LFS 缓存占 .git 的 84.0%超过 50% 红线, 存在未跟踪的本地文件打包前必须确认里面没有凭据 ] }这组数字把问题讲透了工作区里真正受控的源码只有3.0 MB。.git目录20.8 MB占仓库总体积87.0%。.git里 LFS 缓存16.7 MB占.git的84.0%。reflog 里躺着14 条轨迹其中三条是被 reset 丢掉的临时提交。也就是说如果有个工具把整个工作区打包走体积大头根本不是你的代码而是代码的历史和大文件缓存。这跟那篇掘金复盘里.git占八成多的观察是一回事——只是我现在用的是自己可控的合成样例能反复验证。如果数字跟这里对不上优先检查两件事一是du和git命令在你系统上是否都可用二是你有没有真的跑到合成仓库上而不是误指向了别的目录。审计脚本最忌讳扫错对象还浑然不觉。图 3.git内部组成与体积占比——objects / lfs / reflog 三块隐形仓库5 把审计逻辑搬进 marimo做成只读应用命令行脚本已经能出数了但它不方便给同事看。这一节把它搬进 marimo做成一个只读、绑本机、能被服务端执行并导出结论的应用。marimo 笔记本本质上还是一个.py文件用app.cell装饰器组织单元格。我把它按读参数 → 统计 → 出结论三段来写。关键代码如下节选# audit_snapshot.py节选 import marimo app marimo.App(widthmedium, app_title本地仓库快照审计) app.cell def _(Path, os): import sys # 审计目标命令行第一个参数没传就读环境变量再没有直接报错 if len(sys.argv) 1: REPO Path(sys.argv[1]).resolve() else: REPO Path(os.environ[AUDIT_REPO]).resolve() REPO return (REPO,) app.cell def _(REPO, os): # 宁可在这里直接失败也不要默认扫到什么算什么 assert REPO.exists(), f目录不存在{REPO} assert (REPO / .git).exists(), f不是 Git 仓库找不到 .git{REPO} os.chdir(REPO) return有个细节我要专门讲审计目标必须显式传入脚本不做任何猜测默认目录的事。这是我给这个工作台定的一条硬规矩。审计工具一旦会自己挑一个目录扫你迟早会在某天发现它扫的不是你以为的那个仓库。统计部分和 CLI 脚本同源只是改用 marimo 的mo.md渲染结论。这里踩了一个小坑顺手记下来mo.md写在if/else里时服务端导出拿不到渲染结果。我一开始写成if flags: mo.md(...) else: mo.md(...)导出出来的结论是空的。改成先把结论拼成字符串、再无条件mo.md渲染导出就稳定了app.cell def _(flags, mo): verdict (\n.join(f- ⚠️ {f} for f in flags) if flags else - ✅ 未触发当前阈值) mo.md(f## 审计结论\n\n{verdict}) return如果你导出的结论页是空白的先回想一下是不是也把mo.md塞进了条件分支里。以只读应用模式启动AUDIT_REPO/tmp/audit-demo-repo \ marimo run audit_snapshot.py --port 2767 --headless--headless是不弹浏览器。启动后 marimo 只打印一行Running audit_snapshot.py ⚡ ➜ URL: http://localhost:2767注意它绑的是localhost也就是127.0.0.1这是 marimo 的默认行为恰好是我们要的。图 4marimo 只读应用的三段式单元格结构与审计目标显式传入的约束6 无界面验证curl 断言本地只读应用应用起来了怎么在不开浏览器的情况下确认它是对的靠 curl 加几条确定性断言。先确认监听lsof -iTCP:2767 -sTCP:LISTEN -P -n | grep 127.0.0.1 # → 有输出且只出现在 127.0.0.1 上抓页面和健康检查curl -s -o run-index.html -w %{http_code}\n http://127.0.0.1:2767/ # 200 grep -o title[^]*/title run-index.html # title本地仓库快照审计/title curl -s http://127.0.0.1:2767/health # {status:healthy}三条都过了说明应用本身没问题。接着验证它没有对局域网敞开——这一步很多人会漏掉# 取本机局域网地址然后从该地址直连 LAN$(ipconfig getifaddr en0) curl -s -m 3 -o /dev/null -w %{http_code}\n http://$LAN:2767/ # 000 ← 连接失败没有对外监听最后看一眼 marimo 自带的 API 入口有没有门curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1:2767/api/kernel/status # 401 ← 无凭据被拒000和401这两条是整个方案安全边界的地基服务只在本机可达且自带的接口入口不是裸奔的。后面用 cpolar 开隧道时我们只把页面这一段透出去而不是把这台机器的服务整个交出去。除了 curlmarimo 还有两种无界面复跑方式都值得一并验证# ① 导出成拓扑序的纯 Python 脚本可以直接 git diff 审阅 marimo export script audit_snapshot.py -o audit_snapshot.script.py -f # ② 服务端执行笔记本导出单元格结果快照导出结论的关键 AUDIT_REPO/tmp/audit-demo-repo \ marimo export session audit_snapshot.py --force-overwriteexport session产出的 JSON 里我确认了单元格报错数为 0并且渲染后的结论表就在这里| 指标 | 数值 | | 受控源码tracked | 3.0 MB | | 工作区总体积不含 .git | 3.0 MB | | 本地未跟踪文件体积 | 48.0 B | | .git/objects对象库 | 2.9 MB | | .git/lfs大对象缓存 | 16.7 MB | | .git 合计 | 19.9 MB | | reflog 条数 | 14 | | 不可达对象数 | 0 | ## 审计结论 - ⚠️ .git 占比 87.0%超过 50% 红线 - ⚠️ LFS 缓存占 .git 的 84.0%超过 50% 红线 - ⚠️ 存在未跟踪的本地文件48.0 B打包前必须确认里面没有凭据到这一步审计跑通、结论可信、无浏览器无账号三个目标都达成了。这一步不是为了测试而测试而是为了确认后面经隧道透出去的那份结论跟本机跑出来的是同一份。7 用 cpolar 开一条短时隧道让异地同事复核现在到了这篇文章里 cpolar 出场的地方。场景很具体我的审计结论页跑在本机127.0.0.1:2767异地一位安全同事想亲自看一眼这组数字、顺便复现一下检查命令。但我绝不会把真实仓库路径、更不会把仓库内容递出去所以只让隧道承载那一页脱敏的聚合结论。开隧道就一行cpolar http -logstdout 2767关键 stdoutlevelinfo msg[:tunnel server module] Tunnel established at https://6d49fa5e.r3.nas.cpolar.cn看到Tunnel established at https://...就是成功标志。把这条带https的地址发给同事。公网侧验证curl -s -o /dev/null -w %{http_code}\n https://6d49fa5e.r3.nas.cpolar.cn/ # 200 curl -s https://6d49fa5e.r3.nas.cpolar.cn/ | grep -o title[^]*/title # title本地仓库快照审计/title curl -s -o /dev/null -w %{http_code}\n https://6d49fa5e.r3.nas.cpolar.cn/health # 200同事在自己浏览器打开这个地址看到的就是那份统计表和三条红线结论页面是只读的——因为marimo run本来就是只读应用模式他没有编辑入口。复核完就关掉pkill -f cpolar http -logstdout 2767关闭后再打一次公网地址会拿到 404cpolar 边缘返回域名不存在说明隧道确已下线curl -s -o /dev/null -w %{http_code}\n https://6d49fa5e.r3.nas.cpolar.cn/ # 404而本地服务不受影响curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1:2767/ # 200这里有两件事必须说清楚。第一免费套餐的 cpolar 公网地址是随机临时的24 小时内会变化。所以这套做法的定位就是短时复核不是长期固定入口。每次复核重新开一条、拿一个新地址发给同事就行反而更省心。第二隧道只承载脱敏后的聚合结论不承载仓库本身。我透出去的是.git占 87%、LFS 占 84%、reflog 14 条这类数字以及公网同事能自己在本地用同样脚本复现的命令。真实仓库路径、文件内容、凭据一个都不在隧道里。如果你希望每次开会都要同一个地址那就得看固定二级子域名——它需要基础套餐或以上固定 TCP 地址则需要专业套餐或以上。那是另一个阶段的需求这篇先把临时复核这条路走干净。7.1 隧道关闭后公网地址不会立刻报错先别慌这一点是我实测时踩出来的pkill掉 cpolar 进程后马上回访那个公网地址它不会立刻返回 404 或 502而是会正常 200 一两轮。我一开始还以为没关干净多试了几次才看明白。原因不难理解公网请求先到 cpolar 的边缘节点边缘到你这台机器的长连接断开、再被边缘感知本身有个时间差。所以关隧道后的校验不要只打一次给它一点时间for i in $(seq 1 5); do sleep 12 code$(curl -s -m 15 -o /dev/null -w %{http_code} https://你的隧道地址/) echo try$i: $code [ $code ! 200 ] break done # 实测大约 12 秒后开始返回 502最终稳定为 404我这边实测大概 12 秒后先返回 502稳定下来是 404。所以判断关干净了没有的正确标准是不再返回 200而不是必须马上 404。如果你也拿这个做验收把这条写进断言里能少一次误判。8 一键复跑把整条链路压成一个脚本手工敲到这儿命令有点多了。我把整条链路压成一个verify-audit.sh从造合成仓库到关隧道全自动最后打印 PASS/FAIL 汇总。核心断言长这样#!/usr/bin/env bash set -uo pipefail PORT2767 REPO/tmp/audit-demo-repo # 1) 造合成仓库 无界面审计 ./make-synthetic-repo.sh $REPO ./audit_cli.py $REPO --json audit-result.json # 2) marimo 导出与只读应用 marimo export script audit_snapshot.py -o audit_snapshot.script.py -f AUDIT_REPO$REPO marimo export session audit_snapshot.py --force-overwrite AUDIT_REPO$REPO marimo run audit_snapshot.py --port $PORT --headless # 3) 本地 HTTP 断言 curl -s -o /dev/null -w %{http_code} http://127.0.0.1:$PORT/ # 期望 200 curl -s -o /dev/null -w %{http_code} http://127.0.0.1:$PORT/api/kernel/status # 期望 401 # 4) cpolar 短时隧道 关闭后对照 cpolar http -logstdout $PORT # 从 stdout 提取 https 地址回访 200再 kill确认不再返回 200我这台机器上跑完的汇总 结果PASS24 FAIL0 24 条断言覆盖了合成仓库生成、审计脚本退出码与统计区间、export script产物、export session报错数与结论渲染、本地监听与标题、/health、局域网不外扩、/api401、cpolar 隧道公网 200 与标题、关闭隧道后不再返回 200、关闭后本地仍 200。如果你想在自己机器上复跑跑完FAIL不为 0先看两件事一是 cpolar 是否已登录9200 是否 200二是 marimo 的 bin 目录是否在 PATH 里。这两条是最常见的失败来源跟脚本逻辑无关。9 这套工作流解决的是什么又没解决什么把话说全免得给你造成错误期待。它解决的是我这台机器上的某个本地仓库哪些内容一旦被打包外传体积和风险会超出直觉这个问题。答案是可量化、可复现、可交叉复核的.git占比、LFS 缓存重量、reflog 条数、未跟踪本地文件清单全部有数字。它没解决的也是必须说明白的它不能解密第三方客户端上传的密文也不做任何针对第三方的逆向。私钥在云端本机解不开这是客观事实不是我这套脚本能变的。它不接入、不上传任何真实仓库、真实凭据或真实内网地址。所有演示都跑在自建合成样例上。它不做长期公网暴露。cpolar 隧道是短时通道只承载脱敏聚合结论复核完即关。换句话说这是一个本地自查工具 临时复核通道不是拦截上传或破解加密的方案。它的价值在于在把仓库交给任何工具之前你先知道自己在交出去的到底是什么。10 总结到这里整条链路是干净闭环的合成的样例仓库造出来audit_cli.py给出确定性数字.git占 87%、LFS 占 84%、reflog 14 条同一套逻辑搬进 marimo 做成只读应用marimo run绑在127.0.0.1且/api无凭据返回 401export session证明结论可无界面复现再用 cpolar 开一条短时 HTTPS 隧道让异地同事只读复核核实完关隧道、公网地址立刻 404、本地服务不受影响。关键步骤浓缩成三步数python3 audit_cli.py 仓库 --json拿到.git占比、LFS 重量、reflog 条数与未跟踪本地文件清单。看marimo run audit_snapshot.py --port 2767 --headless本机只读页面 curl断言 200/401export session确认结论渲染。传cpolar http -logstdout 2767拿临时 HTTPS 地址只把脱敏结论页发给同事用完pkill回测公网不再返回 200。这套做法最舒服的地方是职责分得开marimo 管统计与结论的确定性cpolar 只管给一条临时通路隧道里不夹带任何真实仓库内容。安全边界不靠我相信同事不会乱点而是靠机制给的——应用只读、只绑本机、接口有门、隧道短时。等哪天团队真要把这套审计常态化扩展路径也很清楚把检查项从体积占比扩到敏感文件名匹配提交作者异常大对象清单导出再把 cpolar 换成固定二级子域名让同事每次开同一个地址来复核。但那都是下一步的事了。眼下先把这一版跑通——它至少能回答一个很实在的问题你正准备打包上传的那个目录里面到底有什么。