<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Gitee on Zhang's Notes</title><link>https://zhangsnotes.com/tags/gitee/</link><description>Recent content in Gitee on Zhang's Notes</description><generator>Hugo</generator><language>zh-CN</language><copyright>Zhang's Notes</copyright><lastBuildDate>Sun, 04 Oct 2026 22:14:06 +0800</lastBuildDate><atom:link href="https://zhangsnotes.com/tags/gitee/index.xml" rel="self" type="application/rss+xml"/><item><title>GiteePushWatcher：给私有 Gitee 仓库装一道代码安检门——推送即拉取、提交即扫描</title><link>https://zhangsnotes.com/posts/gitee-push-watcher-malware-scan/</link><pubDate>Sun, 04 Oct 2026 09:30:00 +0800</pubDate><guid>https://zhangsnotes.com/posts/gitee-push-watcher-malware-scan/</guid><description>用一套 Python 脚本监控私有 Gitee 仓库的推送，收到推送就拉取对应分支、对新增提交做静态恶意代码扫描，并落出可复核的报告。覆盖轮询与 Webhook 两种触发方式、token 鉴权、diff 扫描、force-push 历史改写防护与优缺点。</description><content:encoded><![CDATA[<p><img src="/posts/gitee-push-watcher-malware-scan/cover.avif" alt="cover"></p>
<blockquote>
<p>封面由 AI 生成，主题为「代码安检门」：一面盾牌正在扫描一条由代码构成的 git 分支。</p>
</blockquote>
<h2 id="tldr--read-here">TLDR : <a href="#%e5%8d%81%e4%ba%8c%e3%80%81%e5%a6%82%e6%9e%9c%e5%8f%aa%e8%ae%b0%e4%bd%8f%e4%b8%80%e5%8f%a5%e8%af%9d">Read here</a>
</h2>
<hr>
<p>最近在折腾一个挺实际的需求：</p>
<blockquote>
<p>怎么在有人往私有 Gitee 仓库推代码之后，自动把对应分支拉下来，再检查一下最近的提交里有没有混进恶意代码？</p>
</blockquote>
<p>这件事听起来简单，真做起来有三个坑：</p>
<ul>
<li>Gitee 的私有仓库，API 和网页在你没登录时一律返回 404，不是真 404，是「你没权限」的 404；</li>
<li>「监控推送」到底是轮询还是 Webhook，选错了后面一堆事都白干；</li>
<li>静态扫恶意代码这件事，光扫 diff 不够，还得防有人 <code>git push --force</code> 把历史改写了。</li>
</ul>
<p>我最后做出来的是一套大概 400 行的 Python 脚本，叫它 <code>GiteePushWatcher</code>。它干三件事：监控、拉取、扫描。本文把这个方案怎么实现、怎么用、有什么优缺点都讲清楚，代码我放在文章同级的 <code>git_watcher.py</code> / <code>config.json</code> / <code>patterns.json</code> 里了。</p>
<p>先说结论，后面慢慢展开。</p>
<hr>
<h2 id="一为什么会有这个需求">一、为什么会有这个需求</h2>
<p>先讲清楚场景。</p>
<p>如果你的仓库是开源的、你自己一个人写、而且你对自己的键盘绝对信任，那这事没必要做。</p>
<p>但现实往往不是这样：</p>
<ul>
<li>仓库是团队私有的，成员不止一个，谁哪天手滑或者账号被盗，推一段 <code>eval($_GET['x'])</code> 上来你不一定立刻发现；</li>
<li>CI 跑得再全，也未必会对「新提交里是否藏了 webshell / 反弹 shell / 危险函数调用」做专门告警；</li>
<li>等你在生产环境炸了才回溯，成本远高于「推送那一秒就扫一遍」。</li>
</ul>
<p>所以这个工具的价值，不是替代安全审计，而是把「最便宜的第一道防线」放在推送发生的地方：</p>
<blockquote>
<p>在恶意代码进入你的构建链路之前，先在提交这一关拦一下。</p>
</blockquote>
<p>它的定位很明确——<strong>轻量级、低门槛、可复核的第一道告警</strong>，不是 WAF，不是 SAST 平台，不是代码评审的替代品。</p>
<hr>
<h2 id="二方案全景监控拉取扫描三段式">二、方案全景：监控、拉取、扫描三段式</h2>
<p>整条链路就三段，画出来是这样：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌────────────┐   push    ┌─────────────────┐   git fetch    ┌────────────┐
</span></span><span class="line"><span class="cl">│  Gitee 仓库 │ ───────▶ │  监控触发         │ ────────────▶ │  本地仓库   │
</span></span><span class="line"><span class="cl">│ (私有)      │          │  (轮询/Webhook)  │               │  (对应分支) │
</span></span><span class="line"><span class="cl">└────────────┘          └────────┬────────┘               └─────┬──────┘
</span></span><span class="line"><span class="cl">                                 │ diff before..after            │
</span></span><span class="line"><span class="cl">                                 ▼                               ▼
</span></span><span class="line"><span class="cl">                        ┌─────────────────┐            ┌─────────────────┐
</span></span><span class="line"><span class="cl">                        │  静态规则扫描     │ ◀────────── │  新提交区间/整树 │
</span></span><span class="line"><span class="cl">                        │  patterns.json  │            │  (防 force-push) │
</span></span><span class="line"><span class="cl">                        └────────┬────────┘            └─────────────────┘
</span></span><span class="line"><span class="cl">                                 ▼
</span></span><span class="line"><span class="cl">                        ┌─────────────────┐
</span></span><span class="line"><span class="cl">                        │  报告 + 高危送审   │
</span></span><span class="line"><span class="cl">                        │  reports/*.json  │
</span></span><span class="line"><span class="cl">                        └─────────────────┘</span></span></code></pre></div>
</div>
<p>拆开看每一段负责什么：</p>
<ul>
<li><strong>监控</strong>：发现「哪个分支、SHA 从 A 变成了 B」，这是触发点；</li>
<li><strong>拉取</strong>：把变化实际落到本地，<code>fetch</code> + <code>checkout</code> 到对应分支；</li>
<li><strong>扫描</strong>：算 <code>before..after</code> 的 diff，只对<strong>新增的代码行</strong>跑规则，命中了就进报告。</li>
</ul>
<p>三个环节解耦，任何一个挂了都不影响另外两个的独立性——比如监控用 Webhook，扫描逻辑完全不变。</p>
<hr>
<h2 id="三两种触发方式轮询-vs-webhook">三、两种触发方式：轮询 vs Webhook</h2>
<p>监控这一步，本质上只有两种选法。我先给一张表，再分别讲。</p>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>轮询（Polling）</th>
					<th>Webhook</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>触发时机</td>
					<td>每隔 N 秒查一次 API</td>
					<td>Gitee 一推送就 POST 过来</td>
			</tr>
			<tr>
					<td>实时性</td>
					<td>有延迟，取决于轮询间隔</td>
					<td>秒级，推送即触发</td>
			</tr>
			<tr>
					<td>需要的权限</td>
					<td>能读仓库即可</td>
					<td>能读仓库 + 需要公网回调地址</td>
			</tr>
			<tr>
					<td>部署难度</td>
					<td>低，本机就能跑</td>
					<td>中，服务要能被 Gitee 访问到</td>
			</tr>
			<tr>
					<td>API 限流</td>
					<td>会吃 Gitee API 配额</td>
					<td>基本不吃</td>
			</tr>
			<tr>
					<td>漏推风险</td>
					<td>间隔内发生又回滚可能漏</td>
					<td>几乎不漏</td>
			</tr>
	</tbody>
</table>
<p>我的建议很直接：</p>
<blockquote>
<p>如果只是为了「别让我漏掉可疑提交」，轮询完全够用，而且本机就能跑，零部署成本。<br>
如果你要秒级响应、或者仓库很活跃、不想被 Gitee API 限流烦，那就上 Webhook，代价是要解决公网可达。</p>
</blockquote>
<p><code>GiteePushWatcher</code> 两种都支持，而且可以共存：平时 Webhook 当主力，轮询当兜底。</p>
<hr>
<h2 id="四先把仓库读出来私有仓库的-token-问题">四、先把仓库「读」出来：私有仓库的 token 问题</h2>
<p>这是第一个、也是最容易被卡住的坑。</p>
<p>我一开始直接拿 Gitee 的公开 API 去查 <code>miao-team/miaoyuyin-php</code>，结果返回 404。我以为路径写错了，换了几个接口还是 404。最后用浏览器打开网页，才发现页面提示「您的访问受限（需登录）」。</p>
<p>真相是：</p>
<blockquote>
<p>Gitee 对私有仓库不会返回 401「未授权」，而是直接返回 404「不存在」。<br>
所以「API 404」在这里不等于「仓库不存在」，而等于「你没带着能看的凭证」。</p>
</blockquote>
<p>解决办法只有一个：<strong>去 Gitee 申请一个个人访问令牌（Access Token），勾上 <code>projects</code> 的读权限</strong>。</p>
<p>拿到 token 之后有两处会用到，而且它们是<strong>两个完全不同的东西</strong>，千万别混：</p>
<table>
	<thead>
			<tr>
					<th>密钥</th>
					<th>方向</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>gitee_token</code></td>
					<td>你的脚本 → Gitee</td>
					<td>clone / fetch 私有仓库时做授权</td>
			</tr>
			<tr>
					<td><code>webhook_secret</code></td>
					<td>Gitee → 你的脚本</td>
					<td>校验推送请求是不是 Gitee 真发的</td>
			</tr>
	</tbody>
</table>
<p><code>gitee_token</code> 是必选项（仓库私有，没它连 clone 都做不到）；<code>webhook_secret</code> 是强烈建议项（不设的话，谁都能往你的 <code>/webhook</code> 发请求骗你扫描）。</p>
<p>token 我推荐走环境变量，别写进 <code>config.json</code> 落盘：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GIT_WATCHER_TOKEN</span><span class="o">=</span>你的_gitee_token
</span></span><span class="line"><span class="cl">python git_watcher.py --check   <span class="c1"># 先验证 token 能不能列分支</span></span></span></code></pre></div>
</div>
<p><code>--check</code> 模式只干一件事：列出所有分支和各自的最新 SHA。能列出来，就说明 token 有效、网络通、仓库路径对；列不出来，问题就在这三样里。</p>
<hr>
<h2 id="五轮询模式怎么实现">五、轮询模式怎么实现</h2>
<p>轮询模式的骨架非常朴素：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">loop every poll_interval_seconds:
</span></span><span class="line"><span class="cl">    对每个分支:
</span></span><span class="line"><span class="cl">        sha = 调 Gitee API 取最新 commit
</span></span><span class="line"><span class="cl">        if sha != state[branch]:
</span></span><span class="line"><span class="cl">            本地 git fetch
</span></span><span class="line"><span class="cl">            checkout 到该分支
</span></span><span class="line"><span class="cl">            算旧 sha .. 新 sha 的 diff
</span></span><span class="line"><span class="cl">            扫 diff 新增行
</span></span><span class="line"><span class="cl">            更新 state[branch] = sha
</span></span><span class="line"><span class="cl">            落报告</span></span></code></pre></div>
</div>
<p><code>state.json</code> 是核心——它记住「每个分支上一次看到的 SHA 是什么」。下一轮比对时，发现 SHA 变了，就知道有新的提交要处理。</p>
<p>配置里几个关键项：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">json</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;repo&#34;</span><span class="p">:</span> <span class="s2">&#34;miao-team/miaoyuyin-php&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;gitee_token_env&#34;</span><span class="p">:</span> <span class="s2">&#34;GIT_WATCHER_TOKEN&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;poll_interval_seconds&#34;</span><span class="p">:</span> <span class="mi">900</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;branches&#34;</span><span class="p">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;scan_extensions&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;php&#34;</span><span class="p">,</span> <span class="s2">&#34;html&#34;</span><span class="p">,</span> <span class="s2">&#34;js&#34;</span><span class="p">,</span> <span class="s2">&#34;sh&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;baseline_full_scan&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;reports_dir&#34;</span><span class="p">:</span> <span class="s2">&#34;reports&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p><code>branches</code> 留空表示监控所有分支；<code>scan_extensions</code> 控制只扫哪些后缀，PHP 项目自然把 <code>php</code> 放最前面；<code>baseline_full_scan</code> 为 <code>true</code> 时，首次运行会对整棵树做一次基线扫描，而不是只看增量。</p>
<p>跑起来就两条命令：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python git_watcher.py            <span class="c1"># 常驻守护，按间隔循环</span>
</span></span><span class="line"><span class="cl">python git_watcher.py --once     <span class="c1"># 单次检查后退出（适合被调度器调用）</span></span></span></code></pre></div>
</div>
<p><code>--once</code> 这个模式特别适合丢给系统定时任务或者自动化平台，跑完就退，不占进程。</p>
<hr>
<h2 id="六webhook-模式怎么实现">六、Webhook 模式怎么实现</h2>
<p>Webhook 模式是一个常驻的小 HTTP 服务，监听一个路径（默认 <code>/webhook</code>）。</p>
<p>Gitee 后台配置（仓库 → 管理 → WebHooks → 添加）：</p>
<ol>
<li><strong>URL</strong>：<code>https://你的公网地址/webhook</code>（端口默认 9000，路径可在 <code>config.json</code> 改 <code>webhook_path</code>）</li>
<li><strong>密码</strong>：填和 <code>webhook_secret</code> 一样的字符串</li>
<li><strong>触发事件</strong>：勾选「推送事件 / Push」</li>
<li>保存后 Gitee 会发一次测试，看是不是返回 200</li>
</ol>
<p>服务收到推送后，从请求体里取出 <code>ref</code>（分支）和 <code>before</code> / <code>after</code>（新旧 SHA），校验密码通过后就立刻走「拉取 + 扫描」流程。</p>
<p>我本地实测过路由和鉴权：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">错误 token     → HTTP 403
</span></span><span class="line"><span class="cl">缺少 token     → HTTP 403
</span></span><span class="line"><span class="cl">正确 token     → HTTP 200
</span></span><span class="line"><span class="cl">错误路径       → HTTP 404</span></span></code></pre></div>
</div>
<p>也就是说，没带正确 <code>webhook_secret</code> 的请求一律被挡在门外，不会触发任何克隆或扫描。</p>
<p>启动命令：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python git_watcher.py --serve</span></span></code></pre></div>
</div>
<p><strong>关于公网可达——这是 Webhook 唯一的硬门槛。</strong> Gitee 必须能访问到你的服务，<code>127.0.0.1</code> 不行。三种办法按成本排序：</p>
<ul>
<li>把 <code>--serve</code> 部署到一台有公网 IP 的服务器（systemd / nohup 常驻）；</li>
<li>本机用隧道暴露：<code>ngrok http 9000</code> 或 <code>cloudflared tunnel --url http://localhost:9000</code>，把生成的 HTTPS 地址填到 Gitee；</li>
<li>用 Nginx / Caddy 做反代，终结 HTTPS 后转发到本地 9000。</li>
</ul>
<p>我的观点是：</p>
<blockquote>
<p>如果你只是想先验证整条链路能不能跑通，ngrok 一把梭最快；真要长期用，还是丢到一台小服务器上常驻更稳，毕竟 ngrok 免费版地址会变。</p>
</blockquote>
<hr>
<h2 id="七扫描逻辑不只扫-diff还要防-force-push">七、扫描逻辑：不只扫 diff，还要防 force-push</h2>
<p>扫描这一段的坑，比前面都深。</p>
<p>最 naive 的做法是「扫 <code>before..after</code> 的 diff 新增行」。但有个问题：如果有人 <code>git push --force</code>，把历史改写了，旧的 <code>before</code> SHA 可能根本不再是新提交的祖先。这时候再用 <code>before..after</code> 算区间，要么算错、要么直接报错。</p>
<p>所以脚本里加了一道判断：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">拿到旧 sha 和新 ref 之后:
</span></span><span class="line"><span class="cl">    先跑 git merge-base --is-ancestor 旧sha 新ref
</span></span><span class="line"><span class="cl">    如果返回非 0:
</span></span><span class="line"><span class="cl">        → 历史被改写, 降级为「整树扫描」
</span></span><span class="line"><span class="cl">    否则:
</span></span><span class="line"><span class="cl">        → 正常算 旧sha..新ref 的增量 diff</span></span></code></pre></div>
</div>
<p>还有一个细节：基线（<code>baseline_full_scan</code>）那一轮，本地还没有任何旧 SHA，所以第一次必然是整树扫描，把当前所有文件的命中项都记下来，避免以后每次都把「历史存量」当成「新增恶意」误报。</p>
<blockquote>
<p>这层防护的意义在于：恶意提交不一定只藏在「最新一次 push」里，force-push 完全可以抹掉中间某次提交、再把带毒的代码塞进去。宁可多扫一遍整树，也不能让改写历史成了扫描的盲区。</p>
</blockquote>
<hr>
<h2 id="八patternsjson恶意特征规则长什么样">八、patterns.json：恶意特征规则长什么样</h2>
<p>规则库是一份 JSON，目前放了 32 条 PHP 方向的静态特征，大致分几类：</p>
<ul>
<li><strong>危险函数直接调用</strong>：<code>eval</code>、<code>system</code>、<code>exec</code>、<code>shell_exec</code>、<code>passthru</code>、<code>proc_open</code></li>
<li><strong>动态包含 / 变量函数</strong>：<code>include $x</code>、<code>$$var</code>、<code>create_function</code></li>
<li><strong>已知 webshell 特征</strong>：某些写在注释里或变量里的后门标记</li>
<li><strong>可疑的网络外联</strong>：<code>fsockopen</code> 反弹、<code>curl</code> 把数据往外发且地址来自用户输入</li>
<li><strong>编码混淆</strong>：<code>base64_decode</code> 套 <code>eval</code>、<code>gzinflate</code> 套 <code>base64</code> 这种经典一句话变形</li>
</ul>
<p>每条规则大概长这样：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">json</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;PHP-EVAL-DIRECT&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;severity&#34;</span><span class="p">:</span> <span class="s2">&#34;high&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;pattern&#34;</span><span class="p">:</span> <span class="s2">&#34;\\beval\\s*\\(&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;直接调用 eval 执行动态代码&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>扫描时只对 diff 的<strong>新增行</strong>逐条匹配（整树模式下扫全文件），命中后记录：文件名、行号、匹配到的那一行、命中的规则 id 和严重级别。</p>
<p>需要说清楚的是，这是「特征匹配」不是「语义分析」：</p>
<blockquote>
<p>它能非常可靠地抓住「明显写死的恶意写法」，但抓不住「逻辑上巧妙构造的恶意」。<br>
比如它看到 <code>eval(</code> 就会报，但它分不清这是你故意留的后门，还是某个老框架里确实在用的一句话。</p>
</blockquote>
<p>所以报告里我特意留了一个字段 <code>llm_review_context</code>，把命中的上下文（前后若干行 diff）打包好，直接贴给大模型就能做一轮人工语义复核——这也是为什么标题里我说「提交即扫描」，但真正的「判断是否恶意」可以交给人和模型一起看。</p>
<hr>
<h2 id="九报告长什么样以及怎么送审">九、报告长什么样，以及怎么送审</h2>
<p>每次扫描结束，会在 <code>reports/</code> 下产出两份：</p>
<ul>
<li><code>report_YYYYMMDD_HHMMSS.json</code> —— 机器可读，含分支、提交列表、命中明细、是否整树扫描、是否历史改写</li>
<li><code>report_YYYYMMDD_HHMMSS.md</code> —— 人读版，直接打开就能看</li>
</ul>
<p>报告里高危项的样子大概是这样：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[high] PHP-EVAL-DIRECT @ src/runtime/loader.php:42
</span></span><span class="line"><span class="cl">  命中行: eval(base64_decode($_POST[&#39;c&#39;]));
</span></span><span class="line"><span class="cl">  上下文已写入 llm_review_context，可直接送审</span></span></code></pre></div>
</div>
<p>「送审」这一步现在是半自动的：你看到高危，把 <code>llm_review_context</code> 贴给我（或任意大模型），让它判断「这是真后门，还是正常业务代码」。</p>
<p>如果想全自动，也可以把 <code>llm_review</code> 打开、配一个模型端点，命中高危就自动把上下文发过去拿结论。但这块我建议先跑一段时间人工看，确认误报率你能接受，再考虑全自动化——不然容易被误报淹没了真告警。</p>
<hr>
<h2 id="十这套东西的优缺点老实说">十、这套东西的优缺点，老实说</h2>
<p>我不太喜欢把自制的工具吹成银弹，所以把优缺点摊开讲。</p>
<p><strong>优点</strong></p>
<ul>
<li><strong>门槛极低</strong>：一个 Python 标准库脚本 + 一份 JSON 配置，不需要 Docker、不需要数据库、不需要接第三方 SaaS；</li>
<li><strong>私有仓库可用</strong>：靠 token 解决了 Gitee 私有仓库的读取问题；</li>
<li><strong>双触发</strong>：轮询兜底、Webhook 实时，部署灵活；</li>
<li><strong>防 force-push 盲区</strong>：历史改写时自动降级整树扫描；</li>
<li><strong>可复核</strong>：报告结构化，高危项天然适合丢给大模型做第二轮判断；</li>
<li><strong>零侵入</strong>：不修改目标仓库，只在本地克隆副本上操作。</li>
</ul>
<p><strong>缺点（同样重要）</strong></p>
<ul>
<li><strong>特征匹配有盲区</strong>：换种写法、用白名单函数拼出来、或者逻辑型后门，它大概率扫不到；</li>
<li><strong>误报不可避免</strong>：<code>eval</code> / <code>exec</code> 在不少框架里是正常用法，需要人工或模型二次判断；</li>
<li><strong>Webhook 的公网门槛</strong>：没有公网地址就只能用轮询，而轮询有延迟、吃 API 配额；</li>
<li><strong>token 落盘风险</strong>：用 token 克隆后，token 会写进本地 <code>repo/.git/config</code>，这台机器得只你自己用；</li>
<li><strong>不是实时防护</strong>：它是在「推送之后」告警，不是「推送之前」拦截，真要拦在 CI 合并前，得接到 PR 流水线里；</li>
<li><strong>只扫代码文本</strong>：不跑、不执行，所以「运行时才暴露」的恶意它看不到。</li>
</ul>
<p>一张表收一下：</p>
<table>
	<thead>
			<tr>
					<th>它能做到</th>
					<th>它做不到</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>推送后秒级/分钟级告警</td>
					<td>推送前拦截合并</td>
			</tr>
			<tr>
					<td>抓住明显写死的恶意写法</td>
					<td>抓住逻辑型、变形型后门</td>
			</tr>
			<tr>
					<td>私有仓库自动拉取扫描</td>
					<td>替代专业 SAST / 人工审计</td>
			</tr>
			<tr>
					<td>生成可复核的结构化报告</td>
					<td>零误报</td>
			</tr>
	</tbody>
</table>
<blockquote>
<p>一句话定位：它是「便宜的第一道防线」，不是「全部防线」。把它放在推送这一关，专门干「先响一声、再让人来看」的活，性价比最高。</p>
</blockquote>
<hr>
<h2 id="十一如果你想自己搭">十一、如果你想自己搭</h2>
<p>最小步骤：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. 申请 Gitee Access Token（projects 读权限）</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GIT_WATCHER_TOKEN</span><span class="o">=</span>你的token
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. 改 config.json：repo、branches、scan_extensions</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 3. 验证连通性</span>
</span></span><span class="line"><span class="cl">python git_watcher.py --check
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. 跑</span>
</span></span><span class="line"><span class="cl">python git_watcher.py --serve     <span class="c1"># 有公网就 Webhook</span>
</span></span><span class="line"><span class="cl">python git_watcher.py             <span class="c1"># 没公网就轮询守护</span></span></span></code></pre></div>
</div>
<p><code>config.json</code> 管「监控谁、怎么触发、扫哪些后缀」，<code>patterns.json</code> 管「拿什么规则扫」——这两个文件就是你能调的全部旋钮，按自己项目增删规则即可。</p>
<hr>
<h2 id="十二如果只记住一句话">十二、如果只记住一句话</h2>
<p><code>GiteePushWatcher</code> 可以简单理解成：</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Gitee 推送
</span></span><span class="line"><span class="cl">    = 触发信号
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git fetch + checkout
</span></span><span class="line"><span class="cl">    = 把变化拉到本地
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">patterns.json 扫 diff 新增行
</span></span><span class="line"><span class="cl">    = 第一道告警
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">report + llm_review_context
</span></span><span class="line"><span class="cl">    = 让人/模型来判是不是真恶意</span></span></code></pre></div>
</div>
<blockquote>
<p>它解决的核心问题只有一个：<strong>在恶意代码溜进你的构建链路之前，先在你的提交这一关，响一声。</strong></p>
</blockquote>
<p>如果你也想给自己的私有仓库加这么一道门，照着上面的步骤把 <code>git_watcher.py</code> 跑起来就行——它不完美，但比「完全没人看」强太多。</p>
]]></content:encoded></item></channel></rss>