
简介面向Web安全测试与Git运维人员的工具包《Git_Extract.zip》基于Python3开发用于检测并提取网站公开目录中意外暴露的.git目录帮助安全工程师快速评估源码泄露、账号密码或数据库连接信息外泄的风险。压缩包体积仅13KB包含10个文件以5个Python脚本为核心覆盖git_pack、git_index、utils等功能模块另附对应pyc编译文件和Markdown说明文档方便直接运行、扩展或阅读实现思路。目前已有942人学习下载。借助该工具可掌握Git目录泄露的检测流程理解如何从目标站点重建提交历史、分支与文件内容并据此开展删除暴露目录、收紧访问控制等应急响应。适合具备基础Python能力、关注Web攻防与代码资产安全的读者作为轻量级实战工具。1. Git_Extract.zip 到底解决什么问题先想清楚要不要拆这个包做项目交付或代码归档时你大概率撞过这样的场景仓库里堆了几百个commit合作方只要某个稳定版本的核心模块做审计时要拉出某段时间内变更过的全部文件上线前要导出一份没有测试目录、没有密钥、没有构建产物的干净代码包。每次都用git checkout、git archive加手工复制拼凑第一次能跑通第二次就漏文件第三次把.env一起带了出去。名为 Git_Extract.zip 的工具包解决的就是这件事它把“从 Git 仓库里按规则提取内容”做成可重复的流程。解压后通常是脚本加配置文件输入仓库路径与提取规则输出一份带清单的干净目录或压缩包适合做模块化交付、仓库拆分、代码归档和变更审计的开发者。但先泼盆冷水它不是什么官方发布的正式软件拿回来先检查依赖、读清楚规则语法再往自己的仓库上套否则大概率第一轮就翻车。2. 从“git 自带的能力”到“为什么还要工具包”拆包前先弄懂边界2.1 git 自带的三种提取姿势与能力边界在拆任何压缩包之前值得先把 Git 原生的提取手段过一遍因为这决定了你拿到 Git_Extract.zip 后是改配置就能用还是得动脚本。第一种是git archive它能把某个分支或提交直接打成 tar/zip干净利落但它不支持按目录做复杂的过滤规则而且遇到 Git LFS 时打出来的通常是几百字节的指针文件不是真实内容。第二种是git show commit:path取单个文件非常好用但一次只处理一个文件批量导出时得套循环。第三种是git checkout commit -- path能一次捞出多个文件但它会直接写进当前工作区容易覆盖你正在编辑的内容。# 归档整个分支tar 格式 git archive --formattar --outputproj.tar main # 取单个文件的历史版本 git show a1b2c3d:src/main.py /tmp/main.py # 取多个文件到工作区 git checkout a1b2c3d -- src/app/controller.py这三条命令覆盖了大概 80% 的临时提取需求剩下的 20% 才是工具包存在的理由。比如“从提交 A 到提交 B 之间变更了哪些文件”“导出时要把仓库内的src/api映射到交付包的api-server/目录下”“导出的每个文件最好带一个 manifest 标明它来自哪个 commit”。用原生命令做这些能做到但每条都是一串容易记错的参数组合换个仓库就要重新调试。Git_Extract.zip 这类工具的核心价值就是把上面这些操作固化成一份规则文件让同样的事情跑第二遍时零思考。2.2 一个提取工具包的典型构成入口脚本、规则配置、输出模板常见的 Git_Extract.zip 解压后不是一个大而全的软件而是三块东西一个入口脚本、一份规则配置、一个输出目录模板。入口脚本多数用 Python 写少数用纯 Shell原因很直接Python 跨平台处理路径比 Shell 稳调subprocess跑 git 命令又很方便而且处理 Windows 风格路径时踩坑少。规则配置常见是 YAML 或 JSON里面声明提取哪个分支、包含哪些路径、排除哪些路径、输出到哪个目录、要不要生成 manifest。输出目录模板则是给最终交付包预设好的骨架比如docs/、src/、config/预先建好提取脚本只往里面填文件。git_extract/ ├── extract.py # 入口脚本负责解析参数、调用 git、写文件 ├── rules.yaml # 提取规则分支、包含/排除路径、输出配置 ├── output/ # 输出目录模板脚本运行后在这里生成交付包 └── README.md # 使用说明与依赖清单把这几个文件拆开看你会发现入口脚本通常只有三个职责读配置、调 git 命令拿文件列表、把文件复制到输出目录并生成清单。那些声称“智能化”的包多数也只是在规则语法上加了一些通配符支持核心骨架没有本质区别。所以拿到这类工具包后第一步是打开rules.yaml看它的规则字段而不是急着跑命令规则语法决定了它能覆盖你的场景还是需要你自己改脚本。2.3 先想清楚三个问题再改规则快照还是历史、过滤还是映射、单包还是分片动手配置之前有三个决策直接决定工具包的适用场景建议先想清楚再改配置文件。第一是快照模式还是历史模式。快照模式只导出某个提交时刻的文件内容不带任何历史过程适合模块交付和归档历史模式则要额外产出变更文件清单、起始和结束提交号适合审计和 Code Review 场景。这决定了脚本里是调ls-tree拉全量文件还是调git diff --name-only拉变更列表。第二是简单过滤还是路径映射。简单过滤就是“不要 test 目录、不要.env文件”规则配置里声明排除项即可路径映射要把仓库内的src/api搬到交付包的api-server/目录下这时候输出目录模板就派上用场了。注意很多工具包的过滤规则只支持前缀匹配或子串匹配不支持完整的 glob——这意味着**/test/**这种写法不一定有效拿到包后先用小目录试一遍。第三是单包还是分片。只要一个模块就一个输出目录要按模块拆多个压缩包就得在配置里写多个提取任务。这个决策影响 manifest 的设计单包只要一个文件清单分片时清单里必须带包名和源路径对应关系否则接收方拿到一堆压缩包根本不知道哪个是哪个。把这三个问题想清楚再看手头的 Git_Extract.zip 是改配置能用还是需要动手改脚本判断成本很低最多花十分钟。3. 本地跑通 Git_Extract.zip最小提取命令与目录校验3.1 拆包后的依赖检查三件事拿到 zip 解压后先别急着往正式仓库上跑我一般会在一个测试仓库上先把依赖和环境查一遍。第一个要查的是 Git 版本很多提取逻辑依赖git ls-tree和git diff --name-only的输出格式旧版 Git 的差异比较大建议至少 2.23 以上。第二个是 Python 版本工具包入口脚本如果是 Python 写的通常要求 3.8低于这个版本容易碰到路径库行为不一致的问题。第三个也是最少有人查的是core.autocrlf的配置——Windows 上如果开着自动转换导出的文件会被改成 CRLF 换行后面做审计比对时整片文件全是红色。# 三项依赖检查 git --version python3 --version git config --get core.autocrlf # 输出 true 时提取命令建议显式关掉检查完这三项再去处理规则配置。注意core.autocrlf这条很多人在仓库上直接改全局配置我不建议动用户的全局设置更稳妥的做法是在提取命令里加-c core.autocrlffalse覆盖一次只影响当前命令不污染全局。3.2 最小提取命令把指定分支的某个目录导出成可发布版本环境没问题之后跑一次最小提取。假设要把main分支的src/app目录导出到./release排除其中的test子目录。工具包入口脚本通用一点的调用方式是参数覆盖配置命令像下面这样python extract.py \ --repo /path/to/project \ --branch main \ --include src/app \ --exclude test \ --out ./release--repo指向仓库根目录--branch支持分支名也支持提交号--include做前缀匹配--exclude则对文件路径做子串过滤。注意这里的关键点--exclude test会把所有路径里含test的文件都排除掉包括src/app/controller/test_util.py和src/app/test/下的所有文件这是合理的但如果你的代码里有src/contest这样的目录也会被误伤。子串过滤就是这么粗暴想精确匹配路径边界得看工具包是否支持--exclude /test/带斜杠的写法。import argparse import subprocess import pathlib import shutil def list_files(repo: str, branch: str, include: str, exclude: list[str]): 从 git 对象中直接列出文件不碰当前工作区 git_args [git, -C, repo, ls-tree, -r, --name-only, branch] raw subprocess.check_output(git_args, textTrue) for name in raw.splitlines(): if include in name and all(e not in name for e in exclude): yield name def main(): ap argparse.ArgumentParser() ap.add_argument(--repo, requiredTrue) ap.add_argument(--branch, defaultHEAD) ap.add_argument(--include, requiredTrue) ap.add_argument(--exclude, nargs*, default[]) ap.add_argument(--out, requiredTrue) args ap.parse_args() out_root pathlib.Path(args.out).resolve() for rel in list_files(args.repo, args.branch, args.include, args.exclude): source pathlib.Path(args.repo, rel) target out_root / rel target.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(source, target) if __name__ __main__: main()上面这段是我见过这类工具包里最常见的一种实现思路剥离了业务逻辑后的核心大概就是这么多。用ls-tree而不是git checkout去拿文件列表好处是全程不污染当前工作区不会出现“提取完发现本地改动被覆盖”的惨剧。shutil.copy2保留文件权限和时间戳交付包拷贝到别的机器时行为更可预期。但它也有局限性不处理 LFS 指针不处理符号链接的指向关系这些在避坑章节会展开。3.3 校验输出三行命令确认“包没漏”跑完最小提取后别只看目录里“好像有文件”。我习惯做一次最朴素的文件列表对账用find和git ls-tree分别生成文件列表再 difffind release -type f | sort out_files.txt git -C /path/to/project ls-tree -r --name-only main | grep ^src/app | sort expect_files.txt diff out_files.txt expect_files.txt echo OK这条命令组合能核对文件是否漏导或多导但它的前提是grep ^src/app与工具包的--include src/app前缀匹配规则一致。如果工具包用的是目录名包含匹配这里对不上会产生假阳性差异需要根据实际情况调整 grep 表达式。文件数量对上了只代表“没漏文件”不代表“文件内容准确”内容级的校验放到最后一章讲。4. 三种高频提取场景单文件回溯、按提交范围导出、干净快照4.1 找回被删掉的单文件并导出指定历史版本模块交付时经常遇到“这个工具类以前写过后来被删了现在客户想要”的请求。在仓库里用git log加完整历史搜索可以定位它最后一次存在的提交然后直接导出那份内容。这类提取的关键是--all --full-history默认的git log -- path在文件被删除后可能查不到历史记录必须加这两个参数确保所有分支和所有历史路径都被扫描。# 找到文件最后一次出现的提交 git -C repo log --all --full-history --oneline -- src/legacy/util.py # 用找到的提交号导出文件内容 git -C repo show a1b2c3d:src/legacy/util.py /tmp/util.py这里有个容易忽略的点文件路径要用相对于仓库根目录的路径而且如果文件在历史上被重命名过git log定位到的提交可能还在旧路径上这时候要先跑一次带--follow的日志确认改名链再去git show。不然你会对着一份空输出怀疑人生。4.2 按提交范围导出变更文件diff 出清单再批量复制审计场景下经常要回答“v1.0 到 v2.0 之间到底改了哪些文件”。这个过程可以先让 git 列出变更文件再逐个取出结束提交时刻的文件内容。注意这里一定用git show而不是直接去工作区复制因为工作区可能已经有后续改动直接复制拿到的不是 v2.0 时刻的真实内容。starttag_v1.0 endtag_v2.0 git -C repo diff --name-only $start $end -- src/ | while read -r rel; do if git -C repo cat-file -e $end:$rel 2/dev/null; then mkdir -p changes/$(dirname $rel) git -C repo show $end:$rel changes/$rel fi donecat-file -e的作用是检查该文件在结束提交中是否存在因为 diff 出来的清单里包含被删除的文件这些文件无法从$end中取内容不加判断脚本会在中途报错退出。这个场景下如果还想带上“每个文件改了几行”的统计可以再加一段git diff --numstat $start $end -- src/生成一个 CSV 清单跟提取目录放一起打包交付。4.3 干净快照模式过滤敏感文件与生成 manifest第三种场景是上线前导出一份“干净代码包”不要测试目录、不要.env、不要本地编译产物并且最好每个文件都能追溯来源提交。工具包一般通过规则配置来实现YAML 风格大致是这样branch: main include: - src/ - docs/ exclude: - **/test/** - .env output: format: dir with_manifest: true注意**/test/**能不能生效取决于工具包是否支持 glob 语义不是所有包都支持。高效的做法是先在测试仓库上验证规则语法再上正式项目。我遇到过不少次规则里写了通配符但实际只做子串匹配的情况出来的包多了一大堆目录。manifest 一般会在提取完成后生成内容形如“文件路径、来源提交号、来源分支、提取时间”四个字段用空格或制表符分隔src/main.py a1b2c3d main docs/api.md a1b2c3d main config/nginx.conf a1b2c3d main拿到 manifest 后接收方不需要接触 Git 就能知道每个文件的来源这比给一个裸目录强得多。三种模式的适用情况可以归纳成下面这张表提取模式输出内容历史信息典型适用场景单文件回溯单个文件内容来源提交号找回被删文件、补交历史版本提交范围导出变更文件清单与内容起止提交号审计、Code Review 交付、版本差异分析干净快照分支全量文件过滤后单一提交号模块交付、上线归档、仓库拆分5. Git_Extract 避坑指南五个容易翻车的提取边界5.1 导出路径穿越规则里的../把文件写到了输出目录外现象提取脚本跑完./release里文件没几个反而在仓库上一级目录多了一堆目录。原因规则或参数里出现了../outside之类的相对路径脚本在拼接输出路径时没有做边界校验release/../outside实际写到了输出目录外面。解决在脚本的复制逻辑里加一个最终路径校验用resolve()算出绝对路径后确认它位于输出根目录之内。target (out_root / rel).resolve() if os.path.commonpath([str(out_root), str(target)]) ! str(out_root): raise SystemExit(fillegal path: {rel})这条边界在很多工具包里是缺失的因为正常使用不会有人故意写../但仓库里一旦出现文件名以../开头的异常对象或者规则配置手滑写错层级翻车概率很高。自己动手给脚本加上这条防御是最稳的。5.2 Git LFS 文件被导出成几百字节的指针文件现象导出的包从体积看就不对劲某个应该是几十 MB 的二进制文件只有几百字节打开一看内容是version https://git-lfs.github.com/spec/v1开头的指针文本。原因仓库启用了 Git LFSgit ls-tree读出来的是指针对象而不是真实文件内容普通复制不会触发 LFS 的 smudge 转换。解决提取前确认 LFS 内容已经在本地缓存中先执行git lfs fetch --all再用git lfs checkout确保工作区是真实文件。如果工具包本身走的是git archive路线在archive命令前临时设置 filter 指向 LFS 也能处理但条件是该文件的 LFS 对象在本地存在。最好的做法是提取前先跑一遍 LFS fetch把必要对象都拉下来否则导出包会在接收方手里以“坏文件”的形式暴露问题后面排查成本会更高。5.3 换行符被自动转换审计比对整片飘红现象在 Windows 上跑完提取用 diff 工具对比两个版本的同一文件逻辑没变的地方全都标红逐行看只是换行符不同。原因全局core.autocrlftrue在 checkout 时把 LF 转成了 CRLF 写入工作区而提取脚本如果依赖工作区复制而不是直接读 git 对象拿到手的就是经过转换的版本。解决提取命令统一加-c core.autocrlffalse让当前进程不做自动转换。校验时用 git 的忽略行尾差异参数git -C repo diff --ignore-space-at-eol a1b2c3d main -- src/controller.py注意autocrlf只影响工作区文件如果工具包是用ls-tree读对象再解码通常不受影响而用checkout落盘再复制的实现就一定会中招。拿到工具包先看它是哪种机制再决定要不要处理这个问题。5.4 增量导出漏掉了未跟踪但必需的配置文件现象导出的包在本地能跑换一台机器就起不来缺config/local.properties或者.run/start.sh。原因提取脚本基于git ls-tree或git diff拿到的都是 Git 跟踪文件列表仓库里那些没提交但运行必需的本地配置不会被包含。解决在规则配置里增加一个附加文件清单把这类文件显式列进去。additional_files: - config/local.properties - .run/start.sh这类文件往往体积小但作用致命而且因为它们“不入库”所以经常被忽略。导出前最好花几分钟问一句项目运行除了代码还需要哪些本地文件列不全这个包就算不完整。5.5 部分克隆与子模块导致目录为空现象提取结果里vendor/目录是空的或者子模块目录只有一个.git占位文件。原因仓库启用了 submodule 或者 clone 时用了--filterblob:none、sparse checkout 这类部分克隆机制本地压根没有完整对象。解决提取前先检查仓库状态。test -f .gitmodules echo has submodule git config --get remote.origin.promisor /dev/null echo partial clone有.gitmodules就先执行git submodule update --init --recursive检测到 promisor 仓库部分克隆先跑git fetch --unshallow把完整历史拉全了再提取。这两步不做完后面所有提取结果都不可信这也是我踩过的最大的坑——表面上是工具包的问题实际上仓库本身就不完整。6. 给导出包“验明正身”三重校验与一次交付教训工具包跑通只是第一步交付前最好用三重校验给包做个体检。第一重是文件数量核对把上一章那个 diff 对账做成习惯动作这是性价比最高的检查。第二重是内容级对比做法是在临时目录重新克隆一份仓库用同一个工具包再提取一次然后对两份输出目录做递归 difftmp$(mktemp -d) git clone --depth1 /path/to/project $tmp/proj python extract.py --repo $tmp/proj --branch main --include src/app --exclude test --out $tmp/extract2 diff -r ./release $tmp/extract2diff -r能同时发现缺失文件和多出文件只要两边用的是同一份规则结果应当完全一致。实际运行时注意大仓库的克隆耗时可以先做浅克隆提出来的是单分支快照浅克隆不影响提取结果。第三重是关键词抽查不抽随机文件专挑.env、Dockerfile、Makefile这类容易出问题的文件打开确认内容里没有占位符、没有本地绝对路径、没有密钥残留。讲个教训早年我做交付只对文件数量结果忘了一件事——仓库里有个软链接文件指向构建脚本。提取工具按普通文件复制把软链接解开成了实体文件接收方解压后找不到入口来回沟通了一整天才定位。从那以后我每次导出包里必带 manifest内容至少包含文件路径、来源提交号和提取时间而且无论多急上面三重校验一项不落。因为“导出能在本地跑起来”不算交付完成“接收方按文档能复现”才算。如果你手头的 Git_Extract.zip 只有脚本没有配套校验流程建议按这三步补上十分钟的检查能省下后面一整天的排查。希望帮到你。本文还有配套的精品资源点击获取