Files
aurora-admin b59247231a
Regression / regression (push) Canceled after 0s
feat(安全+品牌): 对外产物发布脱敏(3注入点/2465文件0泄露) + Kole Cup 定稿
安全:CHANGELOG.md 是仓库内部变更流水(含服务器目录、镜像回滚标签、内网网段、
部署时序、AI 工作流用语),此前被 build-site.ps1 / precompute.mjs / build-mobile.mjs
原样注入站点,而站点是公网可下载的静态文件 —— 抓一次 /site/m/changelog.html
即可拿到内网地址段与服务器目录布局。

- 新增 tools/lib/redact-publish.mjs(零依赖,构建期过滤,不改 CHANGELOG.md,
  内部可追溯性完整保留)
- 接入 precompute.mjs(PC data.json/changelog.json)与 build-mobile.mjs
  (移动端更新日志页);build-site.ps1 不碰(ASCII-only 铁律)
- 只处理 changelog 字段:components[].sources 是规范实现源码,逐字保真
  (详情页代码区主动高亮注释,剥注释会破坏该功能)
- 实测消除:/opt/aurora-admin.prev-*、kole-ui-showcase:pre-*、docker compose、
  192.168.5.7、16 位产物指纹、1531/1531、并发会话/本会话/派子 agent
- settings.html 演示占位 IP 192.168.5.0/24(= 真实网段)改为 RFC 5737 的 192.0.2.0/24

品牌:Kole Cup 饮料杯标记定稿(几何 K → 圆角杯盖 + 杯身负空间 K,无吸管),
brand-mark.json 升 schemaVersion 3(paths 支持 { d, evenodd }),
verify:brand 增至 25 条(新增 B9b:负空间必须带 fill-rule)。

验证:发布集 2465 文件全量扫描 0 泄露;PC 回归 100%(1464/1464) ·
移动端 100%(807/807);门禁品牌 25 / 隔离 31 / 移动文档 12 / 示例 9 / 版本 40 / i18n 17 全绿;
已按 AGENTS §九 发布公网,2461/2461 逐字节一致,五项验收全过。

已知未处理(既有缺口,ROADMAP S7-P28 已记录):data.mobile.json 的
meta.generated 为墙上时钟,会让 CI 的「生成物可复现」断言在重跑构建后永远非空;
该 CI 流水线本身亦从未通过(无 runner)。
2026-09-23 07:31:06 +08:00

158 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 发布脱敏(redaction)—— 构建期把「仅供仓库内部」的基础设施细节从对外产物中滤掉。
*
* 为什么需要它(实测):
* CHANGELOG.md 是仓库内部的变更流水,里面忠实记录了部署过程(服务器目录名、
* 镜像回滚标签、docker compose 命令、产物哈希、并发会话轮次)。这些内容被
* build-site.ps1 / tools/precompute.mjs / tools/build-mobile.mjs 原样注入站点,
* 而站点是**公网可下载的静态文件**(https://kole-ui.mymoyu.top)——
* 抓一次 /site/m/changelog.html 就能拿到内网地址段、服务器目录布局与部署时序。
*
* 2026-09-23 线上实证命中:/opt/aurora-admin.prev-20260920-0712、
* kole-ui-showcase:pre-20260920-0710、docker compose build、192.168.5.7。
*
* 设计原则:
* 1) **不改 CHANGELOG.md** —— 它是仓库内部真源,脱敏不能牺牲内部可追溯性。
* 过滤只发生在「写进对外产物」这一步,因此每个注入点都要过一遍 redact()。
* 2) **白名单式替换**,不做全文删除 —— 把敏感片段换成不泄露事实的等价说法,
* 保持句子可读(「按 AGENTS §九 标准流程发布」),而不是留一堆空洞。
* 3) **零依赖**,与仓库铁律 1 一致。
*
* 用法(三处注入点都必须调用):
* import { redactText, redactDeep } from './lib/redact-publish.mjs';
* writeFileSync(out, redactText(src)); // 文本产物(HTML / JSON 字符串)
* const safe = redactDeep(dataObject); // 结构化数据(data.json 的 changelog 段)
*/
'use strict';
/* 每条规则:[匹配, 替换]。顺序有意义 —— 更具体的写在前面,
避免「/opt/xxx.prev-yyyy」被更宽的规则先吃掉一半。 */
const RULES = [
/* --- 服务器目录与路径 --- */
[/\/opt\/[\w.@-]+(?:\/[\w.-]+)*/g, '部署目录'],
[/\b(?:cd|ls|mv|tar|scp|rsync)\s+\/opt\/[\w.-]+(?:\/[\w.-]+)*/g, '部署命令'],
/* --- 内网地址段(保留「有内网部署」这一事实,不保留具体网段) ---
三段式与四段式必须分开写:192.168.x.x 是 192.168 + 两段,
10.x.x.x / 172.16-31.x.x 是首段 + 三段,写在一起会多消耗一个八位组而永不匹配
(2026-09-23 自检抓到:合并写法让 192.168.5.7 漏过替换)。
CIDR 后缀 /24 一并吃掉,避免留下「内网地址/24」这种残句。 */
[/\b192\.168\.\d{1,3}\.\d{1,3}(?:\/\d{1,2})?\b/g, '内网地址'],
[/\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:\/\d{1,2})?\b/g, '内网地址'],
[/\b172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}(?:\/\d{1,2})?\b/g, '内网地址'],
/* --- 镜像 / 容器名(含回滚标签与摘要) --- */
[/\bkole-ui-showcase(?::[\w.-]+)?/g, '站点镜像'],
[/\bdocker\s+tag\s+[\w./:-]+\s+[\w./:-]+/gi, '镜像打标命令'],
[/\bdocker\s+compose\s+(?:build|up|down|ps|logs)\b[^\n]*/gi, '容器编排命令'],
[/\bdocker\s+(?:build|run|rmi|push|save|load)\b[^\n]*/gi, '容器命令'],
/* --- 部署主机与登录 --- */
[/\broot@[\w.-]+/g, '部署账号'],
[/\b(?:ssh|scp)\s+(?:-o\s+\S+\s+)*[\w.-]+@[\w.-]+/g, '部署传输命令'],
/* --- 隧道 / 私有域名(公网域名本身是公开的,保留;只滤掉隧道与私有源) --- */
[/\bfrpc?\b/gi, '隧道'],
/* --- 发布过程指纹:产物哈希与文件数(可供离线比对,属内部细节) ---
只吃「裸的 16 位十六进制」,且要求前后不是字母数字(避免切进 URL、ID、色值)。
文件数「1531/1531」按 context 替换:只在同时出现对账词时才算指纹。 */
[/\b(?<![0-9a-zA-Z])[0-9a-f]{16}(?![0-9a-zA-Z])/g, '产物指纹'],
[/(?:逐文件哈希对账|哈希(?:一致|对账)|树哈希(?:与本地一致)?)[^\n。;]{0,20}?\d+\s*\/\s*\d+/g, '逐文件哈希对账一致'],
/* 部署包规模:「(1531 文件 / 5.8 MB,1.0 MB gzip)」——同一个发布过程的产物指纹。 */
[/[((]\s*\d+\s*文件\s*\/\s*[\d.]+\s*MB[^))]*[))]/g, '(产物规模)'],
[/[((]\s*\d+\s*文件\s*\/\s*[\d.]+\s*MB\b[^\n。;]*/g, '(产物规模)'],
/* --- 会话 / 轮次用语(暴露开发方式,非技术事实) ---
注意匹配顺序:先长词后短词,否则「本会话」先被短规则吃掉一半。 */
[/(?:并发会话|并行会话|另一个会话|另一个子\s*agent|上一轮的两个子\s*agent|子\s*agent\s*并存)/g, '并行改动'],
[/(?:本会话|本次会话|这一轮|本轮(?!改动)|上一轮|前一批|本批次)/g, '本轮改动'],
[/(?:派子\s*agent|派活)/g, '批次派发'],
[/\/son\b/g, '批次派发'],
[/\/agi\b/g, '全自主模式'],
];
/* 关键词命中检测:只在这些词出现时才做替换,避免误伤正常文案
(例如组件演示里的「待办表格」不该被当成敏感信息处理)。 */
const TRIGGERS = [
/\/opt\//, /\b192\.168\./, /\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, /\b172\.(?:1[6-9]|2\d|3[01])\./,
/kole-ui-showcase/, /\bdocker\b/i, /root@/, /\bfrpc?\b/i,
/* 会话/轮次用语必须和 RULES 里的那一组保持同步 ——
漏一个就会出现「规则写好了但永远不触发」的静默漏网(2026-09-23 实测踩到:
`**本会话直接产出的一批**` 因 TRIGGERS 缺词而整行未过滤)。 */
/并发会话|并行会话|另一个会话|本会话|本次会话|这一轮|本轮|上一轮|前一批|本批次|派子\s*agent|派活/,
/\/son\b/, /\/agi\b/,
/\b(?<![0-9a-zA-Z])[0-9a-f]{16}(?![0-9a-zA-Z])/,
/* 对账句式本身也是信号:即使没带 16 位哈希,「逐文件哈希对账 1531/1531」
照样暴露了产物规模,需要过滤掉文件数。 */
/哈希对账|树哈希|哈希一致/,
/* 部署包规模:「(1531 文件 / 5.8 MB,1.0 MB gzip)」 */
/\d+\s*文件\s*\/\s*[\d.]+\s*MB/,
];
/** 文本脱敏:命中触发词才逐条替换,否则原样返回。 */
export function redactText(s) {
if (typeof s !== 'string' || !s) return s;
if (!TRIGGERS.some((re) => re.test(s))) return s;
let out = s;
for (const [re, to] of RULES) out = out.replace(re, to);
/* 清理替换后可能出现的重复与空洞(「部署目录/部署目录」「→ →」) */
out = out.replace(/(部署目录)(?:\s*→\s*\1)+/g, '$1');
out = out.replace(/([,、])\1+/g, '$1');
/* 指纹后面的产物规模一并吃掉:「产物指纹,1531 文件」→「产物指纹」
注意 markdown 反引号可能夹在中间(`f00d…`,1531 文件),要跨过它们。 */
out = out.replace(/产物指纹(`*)[,,]\s*`*\d+\s*(?:个)?\s*文件/g, '产物指纹');
/* 文件数被吃掉后可能留下空括号?(「…对账 0 差异(1531/1531)」→「…对账一致)」)
—— 去掉紧跟在替换结果后的孤立右括号。 */
out = out.replace(/(逐文件哈希对账一致)\s*[))]/g, '$1');
/* 「`pack-deploy --tar`(1531 文件 / 5.8 MB…)」整块被换掉后,
前一个 `→` 箭头会悬空成「→ (产物规模)→」,收掉重复箭头。 */
out = out.replace(/(产物规模)\s*→\s*/g, '(产物规模)→ ');
/* 替换后被 markdown 反引号包住会留下不配对的引号:「(`产物指纹)」→「(产物指纹)」
直接把「反引号+替换词」整体规整成裸词。 */
out = out.replace(/`(产物指纹|产物规模|部署目录|内网地址|站点镜像|容器编排命令|并行改动|本轮改动|批次派发|全自主模式|逐文件哈希对账一致|容器命令|镜像打标命令|部署命令|部署账号|部署传输命令)`/g, '$1');
/* 相邻替换词造成的重复:「批次派发 批次派发推进」→「批次派发推进」
(`/son` 与「派活」是同一个动作的两种写法,替换后撞在一起)。 */
const REPL = '产物指纹|产物规模|部署目录|内网地址|站点镜像|容器编排命令|并行改动|本轮改动|批次派发|全自主模式|逐文件哈希对账一致|容器命令|镜像打标命令|部署命令|部署账号|部署传输命令';
out = out.replace(new RegExp(`(${REPL})\\s+(?=\\1)`, 'g'), '');
/* 注意:这里**不能**再有「删掉行尾替换词」的规则 ——
那会把「→ 替换 → 容器编排命令」整句砍成「→ 替换 →」,属于有损删除
(2026-09-23 实测:加了该规则后第二次调用结果与第一次不同,且丢信息)。 */
/* 收尾:替换词原来被 markdown 反引号包着时,替换后可能只剩单边引号
(「(`产物指纹)」)。紧贴替换词的反引号一律去掉 —— 替换词本身已是干净词,
不需要代码样式;而正文里真正的代码跨度(如 `pack-deploy --tar`)不与替换词相邻,不受影响。 */
out = out.replace(new RegExp('`(?=' + REPL + ')', 'g'), '');
out = out.replace(new RegExp('(?<=' + REPL + ')`', 'g'), '');
/* 括号里被整块替换后留下悬空的连接符:「(目录 + 镜像 站点镜像)」→「(站点镜像)」
「(备份目录 部署目录)」→「(部署目录)」。 */
out = out.replace(new RegExp(`[((]\\s*(?:[++、]|—+|-{1,2}|→)\\s*(?:[\\w\\u4e00-\\u9fa5-]{0,8}\\s*)?(${REPL})\\s*[))]`, 'g'), '($1)');
out = out.replace(new RegExp(`[((]\\s*(${REPL})\\s*(?:[++、]|—+|-{1,2}|→)\\s*[\\w\\u4e00-\\u9fa5-]{0,8}\\s*[))]`, 'g'), '($1)');
/* 括号内只剩连接符或空白 → 整个括号删掉。 */
out = out.replace(/[((]\s*(?:[++、]|—+|-{1,2}|→|\s)+\s*[))]/g, '');
/* 「本轮改动两个并行改动时」这类撞词(两个替换词描述同一场合)收成一句。 */
out = out.replace(/(本轮改动|并行改动)(?:两个|多个)?(本轮改动|并行改动)/g, '多个改动并行');
return out;
}
/** 结构化数据脱敏:递归遍历对象/数组的所有字符串值。返回**新对象**,不改入参。 */
export function redactDeep(v) {
if (typeof v === 'string') return redactText(v);
if (Array.isArray(v)) return v.map(redactDeep);
if (v && typeof v === 'object') {
const o = {};
for (const k of Object.keys(v)) o[k] = redactDeep(v[k]);
return o;
}
return v;
}
/** 自检:用于 verify-redaction.mjs 与门禁断言。 */
export const _selfTest = [
['部署侧(192.168.5.7)的镜像已在仓库改好', '内网地址'],
['备份目录 /opt/aurora-admin.prev-20260920-0712', '部署目录'],
['镜像 kole-ui-showcase:pre-20260920-0710 保留回滚点', '站点镜像'],
['按 AGENTS §九 发布 → docker compose build && up -d', '容器编排命令'],
['暂存目录逐文件哈希对账 0 差异(1531/1531)', '产物指纹'],
['并发会话 07:36 重建了薄壳与 app.js', '并行改动'],
['继续 /son 派活推进清单覆盖', '批次派发'],
];