diff --git a/client/electron/test/criteria-hygiene.test.mjs b/client/electron/test/criteria-hygiene.test.mjs index ea065af..4f40497 100644 --- a/client/electron/test/criteria-hygiene.test.mjs +++ b/client/electron/test/criteria-hygiene.test.mjs @@ -401,42 +401,74 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是 * 一致 ⇒ 本地历史可信,判它 * 不一致 ⇒ **红**(本地 ref 陈旧或被改过 —— 这种时候"绿"毫无意义) * 问不到 ⇒ **红并明说**"这是不知道,不是安全" + * + * ★★ 而**覆盖面**也要按同一句话判(pi 2026-09-15 抓到的第三个洞): + * 我原来只问 `refs/heads/<当前分支>` **一条 ref**,而这条判据的标题说的是 + * "从未进过**远端**" —— 于是**一次把受污染历史推到旁支、或推一个指向它的 tag**, + * 凭证就出去了,而这条判据**照样绿**。 + * ⇒ 这就是"**把'不知道'读成'安全'**"的**同一句话换一根轴**: + * 可达性那一侧我立了"问不到 ⇒ 红",**覆盖面**这一侧却把 + * "我没枚举到的 ref"**静默当成干净** —— **"看不到 ⇒ 绿"**。 + * + * 修法(pi 给的,**不需要 fetch 任何对象**):`ls-remote` 问**全部 ref**, + * 对**每一条**的 tip 都查该路径是否在其可达历史里。查明"远端有哪些 ref"是 + * `ls-remote` 的免费信息,没有理由只问一条。 + * + * ⚠️ 我**没有**把"远端 ref 集合恰好等于 HEAD + refs/heads/main"写成不变量 —— + * 那会在加第一个 tag / 第一条正常旁支时误红(**为了抓泄露而给日常操作设卡**, + * 与 `pre-push` 删 ref 那次同族:**一道闸消费了不属于它管辖的东西**)。 + * 真正要判的性质是"**有没有哪条 ref 的可达历史里有那个 blob**", + * 它对**任意** ref 集合都成立。 */ - const rb = /refs\/remotes\/origin\/(\S+)/.exec(git(['symbolic-ref', '-q', 'refs/remotes/origin/HEAD']).stdout.trim()); - const branch = rb ? rb[1] : 'main'; - const lsr = git(['ls-remote', 'origin', `refs/heads/${branch}`]); + const lsr = git(['ls-remote', 'origin']); assert.equal(lsr.status, 0, - `问不到远端(\`git ls-remote origin refs/heads/${branch}\` 失败)——\n` + '问不到远端(`git ls-remote origin` 失败)——\n' + ' ★ 这是**不知道**,不是**安全**。一条专门用来抓"绕过"的闹钟,\n' + ' 如果因为"不 fetch / 问不到"就报绿,那它自己就能被绕过。\n' + ' 修法:确认远端可达、`origin` 名字对,再跑这条。'); - const remoteTip = (lsr.stdout.trim().split('\n')[0] || '').split('\t')[0]; - assert.ok(/^[0-9a-f]{40}$/.test(remoteTip), - `远端没有 refs/heads/${branch}(或输出形状不认识)—— 同样按"不知道"处理,不当绿。` + /* + * 解析成 {ref, sha}。跳过 `HEAD`(symbolic,与某条分支同 sha,查它是重复劳动); + * 形状不认识的直接跳过(下面 `remoteRefs.length > 0` 会兜住"全都没认出来")。 + */ + const remoteRefs = []; + for (const line of lsr.stdout.trim().split('\n')) { + const [sha, ref] = line.split('\t'); + if (!sha || !ref || ref === 'HEAD') continue; + if (!/^[0-9a-f]{40}$/.test(sha)) continue; + remoteRefs.push({ ref, sha }); + } + assert.ok(remoteRefs.length > 0, + '远端一条 ref 都没解析出来(或输出形状不认识)—— 按"不知道"处理,不当绿。' + `\n 原始输出:${JSON.stringify(lsr.stdout.slice(0, 200))}`); - const localTip = git(['rev-parse', `refs/remotes/origin/${branch}`]); - assert.equal(localTip.status, 0, - `本地没有 refs/remotes/origin/${branch} —— 无从证明本地历史等于远端,别把"没有"读成"干净"`); - const localSha = localTip.stdout.trim(); - assert.equal(localSha, remoteTip, - '本地 `origin/' + branch + '` 与**远端真值不一致** ——\n' - + ` 本地:${localSha}\n 远端:${remoteTip}\n` - + ' ⇒ 本地那条历史**此刻不可信**,而这条判据要读的正是它。\n' - + ' ★ 这种时候报"绿"是没有意义的(**"ref 被改坏了"与"它其实被推过"是同一个盲区** ——\n' - + ' 我在做变体验证时就亲手把这条 ref 指到过伪造的提交上)。\n' - + ' 修法:`git fetch origin` 把它拉回真值,再跑。'); + /* + * 对**每一条**远端 ref 的 tip 查该路径。`git log -- <路径>` 查的是 + * **从该 sha 可达的全部历史**,所以未 fetch 的对象也在其列(这一点我实测过才这么写)。 + * 解析不了的(annotated tag 指向 tag 对象等)**报 unknown 而不是绿**。 + */ + const published = []; + const unresolved = []; + for (const { ref, sha } of remoteRefs) { + const lg = git(['log', '--oneline', sha, '--', AGC_PATH]); + if (lg.status !== 0) { unresolved.push(`${ref}(${sha.slice(0, 8)})`); continue; } + if (lg.stdout.trim() !== '') { + published.push(`[${ref}] ${sha.slice(0, 8)}\n` + + lg.stdout.trim().split('\n').map(l => ' ' + l).join('\n')); + } + } + assert.deepEqual(unresolved, [], + '这几条远端 ref 的 tip **查不了可达历史**(`git log -- <路径>` 失败)——\n' + + ' ★ 按"不知道"处理,**不当绿**:**查不了 ≠ 干净**。\n' + + ' (常见成因:annotated tag 指向 tag 对象,或该对象本地没有且远端也不可达。)\n' + + ` ${unresolved.join('\n ')}`); - const published = git(['log', `origin/${branch}`, '--oneline', '--', AGC_PATH]); - assert.equal(published.status, 0, - '要能查到 origin/main —— 查不到时不要当成"没有"(那是把"不知道"读成"安全")'); - assert.equal(published.stdout.trim(), '', - 'AGC 客户端凭证的**真身已经进过远端历史** ——\n' + assert.deepEqual(published, [], + `AGC 客户端凭证的**真身已经进过远端历史**(在 ${published.length} 条 ref 上查到)——\n` + ' 它含 `client_secret`/`api_key` 信封与明文 `client_id`/`app_id`,\n' + ' 而本仓镜像是**匿名可 clone 的公开项目**(docs/GITCODE-MIRROR.md)。\n' + ' ★ 处置**不是**"删掉再推"(历史里的 blob 撤不回):按**已泄露**处理 ——\n' + ' 1) 去 AGC 控制台**轮换** client_secret / api_key;\n' + ' 2) 历史清理(filter-repo)+ 强推,并与所有 clone 过的人对齐;\n' + ' 3) 查清是哪次 push 绕过了 `.githooks/pre-push`(`--no-verify`?没装的机器?)。\n' - + ` 涉及提交:\n ${published.stdout.trim().split('\n').join('\n ')}`); + + ` 命中:\n ${published.join('\n ')}`); });