🛡️ 天眼合并膜v2 — 同账号沙箱隔离 + Copilot Agent身份检测 + 物理层阻塞
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/fbf443ce-2f3e-4a23-a3e4-515cc2dda5a7
This commit is contained in:
parent
aac467ffb7
commit
76f1277dcd
|
|
@ -1220,13 +1220,13 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "AG-ZY-095",
|
"id": "AG-ZY-095",
|
||||||
"name": "PR合并风险检查引擎",
|
"name": "天眼合并膜引擎",
|
||||||
"type": "workflow",
|
"type": "workflow",
|
||||||
"file": ".github/workflows/skyeye-pr-risk-check.yml",
|
"file": ".github/workflows/skyeye-pr-risk-check.yml",
|
||||||
"parent_sys": "SYS-GLW-0001",
|
"parent_sys": "SYS-GLW-0001",
|
||||||
"owner": "ICE-0002∞",
|
"owner": "ICE-0002∞",
|
||||||
"registered": "2026-03-25",
|
"registered": "2026-03-25",
|
||||||
"description": "天眼系统PR级合并前风险评估 — R1关键文件覆盖/R2受保护路径入侵/R3构建产物/R4权限/R5大规模删除"
|
"description": "天眼系统PR级合并膜 — 同账号沙箱隔离 + 全系统审核。R1关键文件/R2天眼区域入侵/R3构建产物/R4核心配置篡改/R5大规模删除/R6工作流篡改。物理层阻塞:workflow失败=merge按钮禁用"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配",
|
"note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配",
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,33 @@
|
||||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||||
# ═══════════════════════════════════════════════
|
# ═══════════════════════════════════════════════
|
||||||
#
|
#
|
||||||
# 天眼 · PR 合并风险检查
|
# 天眼 · 合并膜 (SkyEye Merge Membrane)
|
||||||
# SkyEye PR Merge Risk Check
|
|
||||||
#
|
#
|
||||||
# 弥补天眼系统在 PR 合并环节的安全缺口:
|
# 架构背景:
|
||||||
# gate-guard v1/v2 保护 push 事件
|
# 所有开发者共用 qinfendebingshuo 账号(企业权限单人仓库)
|
||||||
# 本工作流保护 pull_request 事件(合并前风险评估)
|
# 代理(Copilot Agent)以 copilot/* 分支开发(沙箱隔离)
|
||||||
|
# 合并到 main 时 → 天眼合并膜自动启动,全系统审核
|
||||||
|
# 审核不通过 → 物理层拒绝合并(workflow 失败 = merge 按钮禁用)
|
||||||
|
#
|
||||||
|
# 沙箱隔离原则:
|
||||||
|
# 开发者自由开发(不在天眼包裹内)→ 天眼不干预
|
||||||
|
# 触及天眼包裹区域(.github/, scripts/, skyeye/, core/, docs/ 等)→ 必须通过审核
|
||||||
|
# 天眼核心配置(security-protocol, gate-guard-config 等)→ 仅限主权者修改
|
||||||
#
|
#
|
||||||
# 风险检测维度:
|
# 风险检测维度:
|
||||||
# R1 · 关键文件覆盖检测(docs/index.html 等)
|
# R1 · 关键文件覆盖检测
|
||||||
# R2 · 受保护路径入侵检测
|
# R2 · 天眼包裹区域入侵检测
|
||||||
# R3 · 构建产物误入检测
|
# R3 · 构建产物误入检测
|
||||||
# R4 · 开发者路径权限检测
|
# R4 · 天眼核心配置篡改检测
|
||||||
# R5 · 大规模删除检测
|
# R5 · 大规模删除检测
|
||||||
|
# R6 · 工作流篡改检测
|
||||||
#
|
#
|
||||||
name: 天眼 · PR风险检查
|
# 物理层阻塞:
|
||||||
|
# 此工作流必须配置为 Required Status Check
|
||||||
|
# Settings → Branches → Branch protection rules → main → Require status checks
|
||||||
|
# 添加: "🛡️ 天眼合并膜" 为必需检查
|
||||||
|
#
|
||||||
|
name: 天眼 · 合并膜
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
@ -29,8 +41,8 @@ permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
risk-check:
|
merge-membrane:
|
||||||
name: 🛡️ PR合并风险评估
|
name: 🛡️ 天眼合并膜
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -44,73 +56,103 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Collect PR changed files
|
- name: Collect PR metadata
|
||||||
|
id: collect
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
# Get the list of files changed in this PR
|
echo "📂 收集PR变更文件列表..."
|
||||||
gh pr diff ${{ github.event.pull_request.number }} --name-only > /tmp/pr_files.txt || true
|
gh pr diff ${{ github.event.pull_request.number }} --name-only > /tmp/pr_files.txt
|
||||||
|
FILE_COUNT=$(wc -l < /tmp/pr_files.txt)
|
||||||
|
echo " 文件数: $FILE_COUNT"
|
||||||
|
|
||||||
# Get diff stats
|
|
||||||
gh pr diff ${{ github.event.pull_request.number }} --stat > /tmp/pr_stats_raw.txt || true
|
|
||||||
|
|
||||||
# Parse stats into numstat-like format for the script
|
|
||||||
# Extract additions/deletions from the stat output
|
|
||||||
git diff --numstat origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > /tmp/pr_stats.txt 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "📂 Changed files:"
|
|
||||||
cat /tmp/pr_files.txt
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "📊 Stats:"
|
echo "📊 收集变更统计..."
|
||||||
cat /tmp/pr_stats.txt
|
git diff --numstat origin/${{ github.base_ref }}...HEAD > /tmp/pr_stats.txt 2>/dev/null || echo "" > /tmp/pr_stats.txt
|
||||||
|
|
||||||
- name: 🔍 天眼 PR 风险检查
|
echo ""
|
||||||
id: risk_check
|
echo "📝 收集Commit元数据..."
|
||||||
|
git log origin/${{ github.base_ref }}..HEAD --format="%B---COMMIT_SEP---" > /tmp/pr_commits.txt 2>/dev/null || echo "" > /tmp/pr_commits.txt
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📂 PR变更文件:"
|
||||||
|
cat /tmp/pr_files.txt
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "branch=${{ github.head_ref }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: 🛡️ 天眼合并膜审核
|
||||||
|
id: membrane
|
||||||
env:
|
env:
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
|
PR_BRANCH: ${{ github.head_ref }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
PR_FILES: /tmp/pr_files.txt
|
PR_FILES: /tmp/pr_files.txt
|
||||||
PR_STATS: /tmp/pr_stats.txt
|
PR_STATS: /tmp/pr_stats.txt
|
||||||
|
PR_COMMITS: /tmp/pr_commits.txt
|
||||||
run: node scripts/skyeye/pr-risk-check.js
|
run: node scripts/skyeye/pr-risk-check.js
|
||||||
|
|
||||||
- name: 📝 Write risk report comment
|
- name: 🧠 天眼全系统审计
|
||||||
|
if: failure()
|
||||||
|
id: skyeye_audit
|
||||||
|
run: |
|
||||||
|
echo "━━━ 天眼全系统审计(合并被阻止后触发)━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run SkyEye scan dimensions that can be checked locally
|
||||||
|
echo "📡 D1 · 目录结构检查..."
|
||||||
|
node scripts/skyeye/scan-structure.js 2>/dev/null || echo "⚠️ scan-structure 执行失败"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🧠 D2 · 大脑健康检查..."
|
||||||
|
node scripts/skyeye/scan-brain-health.js 2>/dev/null || echo "⚠️ scan-brain-health 执行失败"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔐 D3 · 安全协议完整性检查..."
|
||||||
|
node scripts/skyeye/scan-security-protocol.js 2>/dev/null || echo "⚠️ scan-security-protocol 执行失败"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━ 天眼审计完成 ━━━"
|
||||||
|
|
||||||
|
- name: 📝 写入天眼审核报告
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const decision = '${{ steps.membrane.outcome }}';
|
||||||
const decision = '${{ steps.risk_check.outcome }}';
|
const branch = '${{ github.head_ref }}';
|
||||||
const riskLevel = '${{ steps.risk_check.outputs.risk_level }}' || 'unknown';
|
|
||||||
const riskSummary = '${{ steps.risk_check.outputs.risk_summary }}' || '';
|
|
||||||
const author = '${{ github.event.pull_request.user.login }}';
|
const author = '${{ github.event.pull_request.user.login }}';
|
||||||
|
const prTitle = context.payload.pull_request.title;
|
||||||
|
const isAgentBranch = branch.startsWith('copilot/') || branch.startsWith('agent/') || branch.startsWith('bot/');
|
||||||
|
|
||||||
let body = '## 🛡️ 天眼 · PR合并风险检查报告\n\n';
|
let body = '## 🛡️ 天眼 · 合并膜审核报告\n\n';
|
||||||
body += `**PR作者:** ${author}\n`;
|
body += `| 项目 | 值 |\n`;
|
||||||
body += `**检查时间:** ${new Date().toISOString()}\n\n`;
|
body += `|------|-----|\n`;
|
||||||
|
body += `| **PR作者** | ${author} |\n`;
|
||||||
|
body += `| **分支** | \`${branch}\` |\n`;
|
||||||
|
body += `| **代理PR** | ${isAgentBranch ? '是 🤖' : '否 👤'} |\n`;
|
||||||
|
body += `| **检查时间** | ${new Date().toISOString()} |\n\n`;
|
||||||
|
|
||||||
if (decision === 'success') {
|
if (decision === 'success') {
|
||||||
body += '### ✅ 风险检查通过\n\n';
|
body += '### ✅ 天眼合并膜: 通过\n\n';
|
||||||
body += `风险等级: **${riskLevel}**\n\n`;
|
body += '此 PR 的变更已通过天眼系统审核,可以安全合并。\n\n';
|
||||||
if (riskSummary && riskSummary !== '仓库主人 PR,直接放行' && riskSummary !== '白名单用户 PR,直接放行') {
|
if (!isAgentBranch) {
|
||||||
body += '> ⚠️ 注意事项:\n';
|
body += '> 💡 非代理分支(主权者手动操作),天眼合并膜自动放行\n';
|
||||||
body += `> ${riskSummary}\n\n`;
|
|
||||||
}
|
}
|
||||||
body += '此 PR 可以安全合并。\n';
|
|
||||||
} else {
|
} else {
|
||||||
body += '### ❌ 风险检查未通过\n\n';
|
body += '### ❌ 天眼合并膜: 物理层拒绝合并\n\n';
|
||||||
body += `风险等级: **${riskLevel}**\n\n`;
|
body += '此 PR 的变更触及了天眼系统包裹区域且未通过审核。\n\n';
|
||||||
body += '**检测到的风险:**\n';
|
body += '**处理方式:**\n';
|
||||||
if (riskSummary) {
|
body += '1. 🔍 检查上方日志中标记的风险项\n';
|
||||||
const items = riskSummary.split(' | ');
|
body += '2. 📦 确保代理PR的变更限制在沙箱区域内\n';
|
||||||
for (const item of items) {
|
body += '3. 👑 如需修改天眼包裹区域,请联系主权者 TCS-0002∞ 审核\n\n';
|
||||||
body += `- 🔴 ${item}\n`;
|
body += '> ⛔ 天眼合并膜已阻止此PR合并。\n';
|
||||||
}
|
body += '> 主权者可手动批准后合并(需在 Branch Protection 中 bypass)。\n';
|
||||||
}
|
|
||||||
body += '\n⛔ **此 PR 已被天眼系统阻止合并。**\n';
|
|
||||||
body += '请联系仓库管理员 (TCS-0002∞) 审核。\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body += '\n---\n';
|
body += '\n---\n';
|
||||||
body += '> 天眼 · PR合并风险检查引擎 · SkyEye PR Risk Check';
|
body += '> 天眼 · 合并膜 (SkyEye Merge Membrane) · AG-ZY-095\n';
|
||||||
|
body += '> 「地球是圆的,没有缺口 — 语言系统包裹所有入口」';
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,47 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// scripts/skyeye/pr-risk-check.js
|
// scripts/skyeye/pr-risk-check.js
|
||||||
// 天眼·PR合并风险检查引擎
|
// 天眼·合并膜 — PR合并前风险检查引擎 (SkyEye Merge Membrane)
|
||||||
//
|
//
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
//
|
//
|
||||||
// 目的:弥补天眼系统在PR合并环节的安全缺口
|
// 架构背景:
|
||||||
// gate-guard v1/v2 保护 push 事件
|
// 所有开发者共用 qinfendebingshuo 账号(企业权限单人仓库)
|
||||||
// 本脚本保护 pull_request 事件(合并前风险评估)
|
// 代理(Copilot Agent)以 copilot/* 分支开发,通过 PR 合并
|
||||||
|
// 无法通过 GitHub username 区分身份 → 改用分支名/commit元数据识别
|
||||||
//
|
//
|
||||||
// 核心原则(与 gate-guard 一致):
|
// 沙箱隔离原则:
|
||||||
// 仓库主人(repo_owner)的 PR → 永远放行
|
// 开发者在各自分支自由开发(沙箱)→ 天眼不干预
|
||||||
// 系统 bot PR → 永远放行
|
// 合并到 main 时 → 天眼合并膜启动,全系统审核
|
||||||
// 其他 PR → 按风险等级评估
|
// 天眼审核不通过 → 物理层拒绝合并(exit 1 = GitHub Status Check 失败)
|
||||||
|
//
|
||||||
|
// 身份识别(不依赖 GitHub username):
|
||||||
|
// I1 · 分支名模式匹配(copilot/* = 代理PR)
|
||||||
|
// I2 · Commit 签名提取(Co-authored-by, Agent-Logs-Url)
|
||||||
|
// I3 · PR 标题/描述中的开发者编号(DEV-XXX, TCS-GL-XX)
|
||||||
//
|
//
|
||||||
// 风险检测维度:
|
// 风险检测维度:
|
||||||
// R1 · 关键文件覆盖检测(docs/index.html 等)
|
// R1 · 关键文件覆盖检测(docs/index.html 等核心文件)
|
||||||
// R2 · 受保护路径入侵检测
|
// R2 · 天眼系统包裹区域入侵检测(.github/, scripts/, skyeye/ 等)
|
||||||
// R3 · 构建产物误入检测
|
// R3 · 构建产物误入检测(Vite/Webpack hash文件)
|
||||||
// R4 · 开发者路径权限检测
|
// R4 · 天眼核心配置篡改检测(security-protocol, gate-guard-config 等)
|
||||||
// R5 · 大规模删除检测
|
// R5 · 大规模删除检测(>500行删除)
|
||||||
|
// R6 · 工作流篡改检测(.github/workflows/ 修改)
|
||||||
//
|
//
|
||||||
// 输入环境变量:
|
// 输入环境变量:
|
||||||
// PR_AUTHOR — PR 作者 GitHub username
|
// PR_AUTHOR — PR 作者 GitHub username
|
||||||
// PR_FILES — 变更文件列表路径(默认 /tmp/pr_files.txt)
|
// PR_BRANCH — PR 源分支名
|
||||||
// PR_STATS — 变更统计路径(默认 /tmp/pr_stats.txt)
|
// PR_TITLE — PR 标题
|
||||||
|
// PR_FILES — 变更文件列表路径(默认 /tmp/pr_files.txt)
|
||||||
|
// PR_STATS — 变更统计路径(默认 /tmp/pr_stats.txt)
|
||||||
|
// PR_COMMITS — PR commit 信息路径(默认 /tmp/pr_commits.txt)
|
||||||
//
|
//
|
||||||
// 输出:
|
// 输出:
|
||||||
// exit 0 → pass(允许合并)
|
// exit 0 → pass(允许合并)
|
||||||
// exit 1 → block(阻止合并)
|
// exit 1 → block(物理层拒绝合并)
|
||||||
// GITHUB_OUTPUT → risk_level, risk_summary, decision
|
// GITHUB_OUTPUT → risk_level, risk_summary, decision, identity_source
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -42,39 +52,71 @@ const path = require('path');
|
||||||
const ROOT = path.resolve(__dirname, '../..');
|
const ROOT = path.resolve(__dirname, '../..');
|
||||||
const BRAIN_CONFIG_PATH = path.join(ROOT, '.github/persona-brain/gate-guard-config.json');
|
const BRAIN_CONFIG_PATH = path.join(ROOT, '.github/persona-brain/gate-guard-config.json');
|
||||||
const OWNER_CONFIG_PATH = path.join(ROOT, '.github/gate-guard-config.json');
|
const OWNER_CONFIG_PATH = path.join(ROOT, '.github/gate-guard-config.json');
|
||||||
|
const SECURITY_PROTOCOL_PATH = path.join(ROOT, '.github/persona-brain/security-protocol.json');
|
||||||
const PR_FILES_PATH = process.env.PR_FILES || '/tmp/pr_files.txt';
|
const PR_FILES_PATH = process.env.PR_FILES || '/tmp/pr_files.txt';
|
||||||
const PR_STATS_PATH = process.env.PR_STATS || '/tmp/pr_stats.txt';
|
const PR_STATS_PATH = process.env.PR_STATS || '/tmp/pr_stats.txt';
|
||||||
|
const PR_COMMITS_PATH = process.env.PR_COMMITS || '/tmp/pr_commits.txt';
|
||||||
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null';
|
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null';
|
||||||
|
|
||||||
// ━━━ 仓库主人 ━━━
|
// ━━━ 仓库主人 ━━━
|
||||||
const REPO_OWNER = 'qinfendebingshuo';
|
const REPO_OWNER = 'qinfendebingshuo';
|
||||||
|
|
||||||
// ━━━ 关键文件 — 这些文件被覆盖意味着高风险 ━━━
|
// ━━━ Copilot Agent 分支模式 ━━━
|
||||||
const CRITICAL_FILES = [
|
const AGENT_BRANCH_PATTERNS = [
|
||||||
|
/^copilot\//, // GitHub Copilot agent branches
|
||||||
|
/^agent\//, // Generic agent branches
|
||||||
|
/^bot\// // Bot branches
|
||||||
|
];
|
||||||
|
|
||||||
|
// ━━━ 天眼系统包裹区域 — 这些路径构成天眼保护的核心系统 ━━━
|
||||||
|
const SKYEYE_WRAPPED_PATHS = [
|
||||||
|
'.github/workflows/',
|
||||||
|
'.github/persona-brain/',
|
||||||
|
'.github/brain/',
|
||||||
|
'.github/gate-guard-config.json',
|
||||||
|
'scripts/',
|
||||||
|
'skyeye/',
|
||||||
|
'core/',
|
||||||
|
'connectors/',
|
||||||
|
'backend/api-server/',
|
||||||
'docs/index.html',
|
'docs/index.html',
|
||||||
'docs/CNAME',
|
'docs/CNAME',
|
||||||
'docs/.nojekyll',
|
'docs/.nojekyll',
|
||||||
|
'docs/js/',
|
||||||
|
'docs/css/',
|
||||||
|
'docs/dashboard/',
|
||||||
|
'data/system-health.json',
|
||||||
|
'data/bulletin-board.json',
|
||||||
'README.md',
|
'README.md',
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json'
|
||||||
'.github/persona-brain/security-protocol.json',
|
|
||||||
'.github/persona-brain/gate-guard-config.json',
|
|
||||||
'.github/gate-guard-config.json'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ━━━ 构建产物特征 — 这些模式不应出现在仓库中 ━━━
|
// ━━━ 天眼核心配置 — 最高保护等级,任何修改都触发 block ━━━
|
||||||
|
const SKYEYE_CORE_FILES = [
|
||||||
|
'.github/persona-brain/security-protocol.json',
|
||||||
|
'.github/persona-brain/gate-guard-config.json',
|
||||||
|
'.github/persona-brain/agent-registry.json',
|
||||||
|
'.github/persona-brain/ontology.json',
|
||||||
|
'.github/gate-guard-config.json',
|
||||||
|
'scripts/gate-guard.js',
|
||||||
|
'scripts/gate-guard-v2.js',
|
||||||
|
'scripts/skyeye/pr-risk-check.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// ━━━ 构建产物特征 ━━━
|
||||||
const BUILD_ARTIFACT_PATTERNS = [
|
const BUILD_ARTIFACT_PATTERNS = [
|
||||||
/^docs\/assets\/index-[A-Za-z0-9_-]+\.(js|css)$/, // Vite build hashes
|
/^docs\/assets\/index-[A-Za-z0-9_-]+\.(js|css)$/,
|
||||||
/^dist\//, // dist directory
|
/^dist\//,
|
||||||
/^build\//, // build directory
|
/^build\//,
|
||||||
/^\.next\//, // Next.js build
|
/^\.next\//,
|
||||||
/^node_modules\//, // node_modules
|
/^node_modules\//,
|
||||||
/\.chunk\.(js|css)$/, // Webpack chunks
|
/\.chunk\.(js|css)$/,
|
||||||
/\.bundle\.(js|css)$/ // Bundle files
|
/\.bundle\.(js|css)$/
|
||||||
];
|
];
|
||||||
|
|
||||||
// ━━━ 大规模删除阈值 ━━━
|
// ━━━ 大规模删除阈值 ━━━
|
||||||
const MASS_DELETE_THRESHOLD = 500; // lines
|
const MASS_DELETE_THRESHOLD = 500;
|
||||||
|
|
||||||
// ━━━ 安全读取 JSON ━━━
|
// ━━━ 安全读取 JSON ━━━
|
||||||
function readJSON(filePath) {
|
function readJSON(filePath) {
|
||||||
|
|
@ -96,6 +138,76 @@ function setOutput(key, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ━━━ I1 · 检测是否为代理(Agent)PR ━━━
|
||||||
|
function isAgentBranch(branchName) {
|
||||||
|
if (!branchName) return false;
|
||||||
|
return AGENT_BRANCH_PATTERNS.some(p => p.test(branchName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ━━━ I2 · 从 commit 元数据提取代理身份 ━━━
|
||||||
|
function extractAgentIdentity(commitsPath, prTitle) {
|
||||||
|
const identity = {
|
||||||
|
isAgent: false,
|
||||||
|
agentName: null,
|
||||||
|
devId: null,
|
||||||
|
devName: null,
|
||||||
|
source: 'unknown'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check PR title for dev identifiers
|
||||||
|
if (prTitle) {
|
||||||
|
const devMatch = prTitle.match(/\b(DEV-\d{3})\b/i);
|
||||||
|
const tcsMatch = prTitle.match(/\b(TCS-GL-\d{2})\b/i);
|
||||||
|
if (devMatch) {
|
||||||
|
identity.devId = devMatch[1].toUpperCase();
|
||||||
|
identity.source = 'pr_title';
|
||||||
|
}
|
||||||
|
if (tcsMatch) {
|
||||||
|
identity.devId = tcsMatch[1].toUpperCase();
|
||||||
|
identity.source = 'pr_title';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check commit messages
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(commitsPath)) {
|
||||||
|
const content = fs.readFileSync(commitsPath, 'utf8');
|
||||||
|
|
||||||
|
// Agent-Logs-Url indicates Copilot agent
|
||||||
|
if (content.includes('Agent-Logs-Url:')) {
|
||||||
|
identity.isAgent = true;
|
||||||
|
identity.source = 'agent_logs_url';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Co-authored-by pattern
|
||||||
|
const coAuthorMatch = content.match(/Co-authored-by:\s*(.+?)\s*</);
|
||||||
|
if (coAuthorMatch) {
|
||||||
|
identity.agentName = coAuthorMatch[1].trim();
|
||||||
|
identity.isAgent = true;
|
||||||
|
if (identity.source === 'unknown') identity.source = 'co_authored_by';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persona signature in commit messages [PER-XXX] [TCS-XXX]
|
||||||
|
const sigMatch = content.match(/^\[([A-Z]+-[A-Z0-9\-∞]+)\]/m);
|
||||||
|
if (sigMatch) {
|
||||||
|
identity.devId = identity.devId || sigMatch[1];
|
||||||
|
if (identity.source === 'unknown') identity.source = 'commit_signature';
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEV-XXX in commit messages
|
||||||
|
const devInCommit = content.match(/\b(DEV-\d{3})\b/i);
|
||||||
|
if (devInCommit && !identity.devId) {
|
||||||
|
identity.devId = devInCommit[1].toUpperCase();
|
||||||
|
if (identity.source === 'unknown') identity.source = 'commit_message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Non-fatal - continue without commit analysis
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
// ━━━ 加载门禁配置 ━━━
|
// ━━━ 加载门禁配置 ━━━
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
const brainConfig = readJSON(BRAIN_CONFIG_PATH);
|
const brainConfig = readJSON(BRAIN_CONFIG_PATH);
|
||||||
|
|
@ -104,14 +216,15 @@ function loadConfig() {
|
||||||
if (!brainConfig && !ownerConfig) {
|
if (!brainConfig && !ownerConfig) {
|
||||||
console.error('⚠️ 门禁配置缺失,使用最小安全配置');
|
console.error('⚠️ 门禁配置缺失,使用最小安全配置');
|
||||||
return {
|
return {
|
||||||
whitelist: [REPO_OWNER, 'github-actions[bot]', 'zhuyuan-bot'],
|
whitelist: ['github-actions[bot]', 'zhuyuan-bot'],
|
||||||
system_protected_paths: ['.github/', 'scripts/', 'docs/', 'data/', 'core/', 'connectors/'],
|
system_protected_paths: ['.github/', 'scripts/', 'docs/', 'data/', 'core/', 'connectors/'],
|
||||||
developers: {}
|
developers: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
whitelist: brainConfig?.whitelist_actors || ownerConfig?.whitelist || [],
|
whitelist: (brainConfig?.whitelist_actors || ownerConfig?.whitelist || [])
|
||||||
|
.filter(u => u !== REPO_OWNER), // Do NOT whitelist owner for agent PRs
|
||||||
system_protected_paths: brainConfig?.system_protected_paths || ownerConfig?.protected_paths || [],
|
system_protected_paths: brainConfig?.system_protected_paths || ownerConfig?.protected_paths || [],
|
||||||
developers_brain: brainConfig?.developer_permissions || {},
|
developers_brain: brainConfig?.developer_permissions || {},
|
||||||
developers_owner: ownerConfig?.developers || {}
|
developers_owner: ownerConfig?.developers || {}
|
||||||
|
|
@ -140,7 +253,6 @@ function loadPRStats() {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(PR_STATS_PATH)) return null;
|
if (!fs.existsSync(PR_STATS_PATH)) return null;
|
||||||
const content = fs.readFileSync(PR_STATS_PATH, 'utf8').trim();
|
const content = fs.readFileSync(PR_STATS_PATH, 'utf8').trim();
|
||||||
// Format: "additions\tdeletions\tfilename" per line
|
|
||||||
let totalDeletions = 0;
|
let totalDeletions = 0;
|
||||||
for (const line of content.split('\n')) {
|
for (const line of content.split('\n')) {
|
||||||
const parts = line.split('\t');
|
const parts = line.split('\t');
|
||||||
|
|
@ -155,31 +267,43 @@ function loadPRStats() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━ 识别开发者身份 ━━━
|
// ━━━ 根据 devId 查找注册开发者 ━━━
|
||||||
function identifyDeveloper(author, config) {
|
function findDeveloperByDevId(devId, config) {
|
||||||
// Check brain config
|
if (!devId) return null;
|
||||||
|
|
||||||
if (config.developers_brain) {
|
if (config.developers_brain) {
|
||||||
for (const [devId, dev] of Object.entries(config.developers_brain)) {
|
for (const [id, dev] of Object.entries(config.developers_brain)) {
|
||||||
if (dev.github_usernames && dev.github_usernames.includes(author)) {
|
if (id === devId) {
|
||||||
return { devId, name: dev.name, allowed_paths: dev.allowed_paths || [] };
|
return { devId: id, name: dev.name, allowed_paths: dev.allowed_paths || [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check owner config
|
if (config.developers_owner) {
|
||||||
if (config.developers_owner && config.developers_owner[author]) {
|
for (const [, dev] of Object.entries(config.developers_owner)) {
|
||||||
const dev = config.developers_owner[author];
|
if (dev.dev_id === devId) {
|
||||||
return { devId: dev.dev_id, name: dev.name, allowed_paths: dev.allowed_paths || [] };
|
return { devId: dev.dev_id, name: dev.name, allowed_paths: dev.allowed_paths || [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ━━━ 检查文件是否在天眼包裹区域内 ━━━
|
||||||
|
function isInSkyeyeZone(file) {
|
||||||
|
return SKYEYE_WRAPPED_PATHS.some(p => file === p || file.startsWith(p));
|
||||||
|
}
|
||||||
|
|
||||||
// ━━━ R1 · 关键文件覆盖检测 ━━━
|
// ━━━ R1 · 关键文件覆盖检测 ━━━
|
||||||
function checkCriticalFiles(files) {
|
function checkCriticalFiles(files) {
|
||||||
const risks = [];
|
const risks = [];
|
||||||
|
const criticalFiles = [
|
||||||
|
'docs/index.html', 'docs/CNAME', 'docs/.nojekyll',
|
||||||
|
'README.md', 'package.json', 'package-lock.json'
|
||||||
|
];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (CRITICAL_FILES.includes(file)) {
|
if (criticalFiles.includes(file)) {
|
||||||
risks.push({
|
risks.push({
|
||||||
dimension: 'R1',
|
dimension: 'R1',
|
||||||
severity: 'high',
|
severity: 'high',
|
||||||
|
|
@ -191,38 +315,32 @@ function checkCriticalFiles(files) {
|
||||||
return risks;
|
return risks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━ R2 · 受保护路径入侵检测 ━━━
|
// ━━━ R2 · 天眼包裹区域入侵检测 ━━━
|
||||||
function checkProtectedPaths(files, config, developer) {
|
function checkSkyeyeZoneIntrusion(files, developer) {
|
||||||
const risks = [];
|
const risks = [];
|
||||||
|
|
||||||
// If developer is registered, check if they're modifying protected paths outside their allowed_paths
|
for (const file of files) {
|
||||||
if (developer) {
|
if (!isInSkyeyeZone(file)) continue;
|
||||||
for (const file of files) {
|
|
||||||
const isProtected = config.system_protected_paths.some(p => file.startsWith(p));
|
|
||||||
if (!isProtected) continue;
|
|
||||||
|
|
||||||
|
// If developer is identified, check allowed_paths
|
||||||
|
if (developer) {
|
||||||
const isAllowed = developer.allowed_paths.some(p => file.startsWith(p));
|
const isAllowed = developer.allowed_paths.some(p => file.startsWith(p));
|
||||||
if (!isAllowed) {
|
if (!isAllowed) {
|
||||||
risks.push({
|
risks.push({
|
||||||
dimension: 'R2',
|
dimension: 'R2',
|
||||||
severity: 'high',
|
severity: 'high',
|
||||||
file,
|
file,
|
||||||
detail: `${developer.name}(${developer.devId}) 修改了授权路径之外的受保护文件: ${file}`
|
detail: `${developer.name}(${developer.devId}) 修改了天眼包裹区域: ${file}`
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Unknown developer touching protected paths
|
|
||||||
for (const file of files) {
|
|
||||||
const isProtected = config.system_protected_paths.some(p => file.startsWith(p));
|
|
||||||
if (isProtected) {
|
|
||||||
risks.push({
|
|
||||||
dimension: 'R2',
|
|
||||||
severity: 'critical',
|
|
||||||
file,
|
|
||||||
detail: `未注册开发者修改受保护路径: ${file}`
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown identity touching SkyEye zone
|
||||||
|
risks.push({
|
||||||
|
dimension: 'R2',
|
||||||
|
severity: 'high',
|
||||||
|
file,
|
||||||
|
detail: `代理PR修改天眼包裹区域: ${file}`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,20 +366,16 @@ function checkBuildArtifacts(files) {
|
||||||
return risks;
|
return risks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━ R4 · 开发者路径权限检测 ━━━
|
// ━━━ R4 · 天眼核心配置篡改检测 ━━━
|
||||||
function checkDeveloperPaths(files, developer) {
|
function checkCoreConfigTampering(files) {
|
||||||
if (!developer) return []; // Handled by R2
|
|
||||||
|
|
||||||
const risks = [];
|
const risks = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const isAllowed = developer.allowed_paths.some(p => file.startsWith(p));
|
if (SKYEYE_CORE_FILES.includes(file)) {
|
||||||
if (!isAllowed) {
|
|
||||||
// Not in allowed paths — but might be in a non-protected area (info level)
|
|
||||||
risks.push({
|
risks.push({
|
||||||
dimension: 'R4',
|
dimension: 'R4',
|
||||||
severity: 'medium',
|
severity: 'critical',
|
||||||
file,
|
file,
|
||||||
detail: `${developer.name}(${developer.devId}) 修改了非授权路径: ${file}`
|
detail: `天眼核心配置被修改: ${file} — 此文件仅限 TCS-0002∞ 主权者修改`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -282,90 +396,192 @@ function checkMassDeletion(stats) {
|
||||||
return risks;
|
return risks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ━━━ R6 · 工作流篡改检测 ━━━
|
||||||
|
function checkWorkflowTampering(files) {
|
||||||
|
const risks = [];
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith('.github/workflows/') && (file.endsWith('.yml') || file.endsWith('.yaml'))) {
|
||||||
|
risks.push({
|
||||||
|
dimension: 'R6',
|
||||||
|
severity: 'critical',
|
||||||
|
file,
|
||||||
|
detail: `工作流文件被修改: ${file} — 代理PR不应修改自动化管线`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return risks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ━━━ 天眼系统完整性验证 ━━━
|
||||||
|
function verifySkyeyeIntegrity() {
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
// Check security protocol
|
||||||
|
const protocol = readJSON(SECURITY_PROTOCOL_PATH);
|
||||||
|
if (!protocol) {
|
||||||
|
issues.push('security-protocol.json 不存在或无法读取');
|
||||||
|
} else {
|
||||||
|
if (!protocol.permanent) issues.push('security-protocol.json: permanent 标记缺失');
|
||||||
|
if (!protocol.root_rules || protocol.root_rules.length < 3) {
|
||||||
|
issues.push('security-protocol.json: root_rules 不完整');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check gate-guard configs
|
||||||
|
if (!readJSON(BRAIN_CONFIG_PATH) && !readJSON(OWNER_CONFIG_PATH)) {
|
||||||
|
issues.push('门禁配置文件全部缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
// ━━━ 风险评估决策 ━━━
|
// ━━━ 风险评估决策 ━━━
|
||||||
function makeDecision(risks) {
|
function makeDecision(risks, touchesSkyeyeZone) {
|
||||||
const hasCritical = risks.some(r => r.severity === 'critical');
|
const hasCritical = risks.some(r => r.severity === 'critical');
|
||||||
const highCount = risks.filter(r => r.severity === 'high').length;
|
const highCount = risks.filter(r => r.severity === 'high').length;
|
||||||
const mediumCount = risks.filter(r => r.severity === 'medium').length;
|
|
||||||
|
|
||||||
|
// Any critical risk = immediate block
|
||||||
if (hasCritical) {
|
if (hasCritical) {
|
||||||
return { decision: 'block', risk_level: 'critical' };
|
return { decision: 'block', risk_level: 'critical' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multiple high risks = block
|
||||||
if (highCount >= 2) {
|
if (highCount >= 2) {
|
||||||
return { decision: 'block', risk_level: 'high' };
|
return { decision: 'block', risk_level: 'high' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single high risk = warn (allow merge but notify)
|
||||||
if (highCount === 1) {
|
if (highCount === 1) {
|
||||||
return { decision: 'warn', risk_level: 'high' };
|
return { decision: 'warn', risk_level: 'high' };
|
||||||
}
|
}
|
||||||
if (mediumCount >= 3) {
|
|
||||||
return { decision: 'warn', risk_level: 'medium' };
|
// No SkyEye zone touched = safe pass
|
||||||
|
if (!touchesSkyeyeZone) {
|
||||||
|
return { decision: 'pass', risk_level: 'low' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { decision: 'pass', risk_level: 'low' };
|
return { decision: 'pass', risk_level: 'low' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ━━━ 主逻辑 ━━━
|
// ━━━ 主逻辑 ━━━
|
||||||
function run() {
|
function run() {
|
||||||
const author = process.env.PR_AUTHOR || '';
|
const author = process.env.PR_AUTHOR || '';
|
||||||
|
const branch = process.env.PR_BRANCH || '';
|
||||||
|
const prTitle = process.env.PR_TITLE || '';
|
||||||
|
|
||||||
console.log('═══════════════════════════════════════════════');
|
console.log('═══════════════════════════════════════════════');
|
||||||
console.log('🔺 天眼 · PR合并风险检查引擎');
|
console.log('🛡️ 天眼 · 合并膜 (SkyEye Merge Membrane)');
|
||||||
console.log('═══════════════════════════════════════════════');
|
console.log('═══════════════════════════════════════════════');
|
||||||
console.log(`PR作者: ${author || '(unknown)'}`);
|
console.log(`PR作者(GitHub): ${author || '(unknown)'}`);
|
||||||
|
console.log(`分支: ${branch || '(unknown)'}`);
|
||||||
|
console.log(`标题: ${prTitle || '(unknown)'}`);
|
||||||
console.log(`时间: ${new Date().toISOString()}`);
|
console.log(`时间: ${new Date().toISOString()}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// ─── 白名单检查 ───
|
// ─── 检测 PR 来源类型 ───
|
||||||
if (author === REPO_OWNER) {
|
const fromAgent = isAgentBranch(branch);
|
||||||
console.log('✅ 仓库主人 PR — 直接放行');
|
const identity = extractAgentIdentity(PR_COMMITS_PATH, prTitle);
|
||||||
|
const isAgentPR = fromAgent || identity.isAgent;
|
||||||
|
|
||||||
|
if (isAgentPR) {
|
||||||
|
console.log('🤖 代理PR检测: 是');
|
||||||
|
console.log(` 分支匹配: ${fromAgent ? '是' : '否'}`);
|
||||||
|
console.log(` Agent标记: ${identity.isAgent ? '是' : '否'}`);
|
||||||
|
if (identity.agentName) console.log(` 代理名称: ${identity.agentName}`);
|
||||||
|
if (identity.devId) console.log(` 开发者编号: ${identity.devId}`);
|
||||||
|
console.log(` 身份来源: ${identity.source}`);
|
||||||
|
} else {
|
||||||
|
// Non-agent PR from repo owner — this is the owner's own manual work
|
||||||
|
console.log('👤 主权者手动PR — 天眼合并膜放行');
|
||||||
|
console.log(' (非代理分支,无Agent标记)');
|
||||||
setOutput('decision', 'pass');
|
setOutput('decision', 'pass');
|
||||||
setOutput('risk_level', 'none');
|
setOutput('risk_level', 'none');
|
||||||
setOutput('risk_summary', '仓库主人 PR,直接放行');
|
setOutput('identity_source', 'owner_manual');
|
||||||
|
setOutput('risk_summary', '主权者手动PR,天眼合并膜放行');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ─── 天眼系统完整性预检 ───
|
||||||
|
console.log('━━━ 天眼系统完整性预检 ━━━');
|
||||||
|
const integrityIssues = verifySkyeyeIntegrity();
|
||||||
|
if (integrityIssues.length > 0) {
|
||||||
|
console.log('⚠️ 天眼系统完整性问题:');
|
||||||
|
integrityIssues.forEach(i => console.log(` · ${i}`));
|
||||||
|
} else {
|
||||||
|
console.log('✅ 天眼系统完整性: 正常');
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ─── 加载配置和文件 ───
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
if (config.whitelist.includes(author)) {
|
|
||||||
console.log(`✅ 白名单用户 (${author}) — 直接放行`);
|
|
||||||
setOutput('decision', 'pass');
|
|
||||||
setOutput('risk_level', 'none');
|
|
||||||
setOutput('risk_summary', '白名单用户 PR,直接放行');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 加载变更文件 ───
|
|
||||||
const files = loadPRFiles();
|
const files = loadPRFiles();
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
console.log('⚠️ 无变更文件或文件列表不可用 — 放行');
|
console.log('⚠️ 无变更文件或文件列表不可用');
|
||||||
setOutput('decision', 'pass');
|
// For agent PRs with no file info, we block to be safe
|
||||||
|
console.log('❌ 代理PR无法获取变更文件列表 — 安全起见阻止合并');
|
||||||
|
setOutput('decision', 'block');
|
||||||
setOutput('risk_level', 'unknown');
|
setOutput('risk_level', 'unknown');
|
||||||
setOutput('risk_summary', '无变更文件信息');
|
setOutput('identity_source', identity.source);
|
||||||
process.exit(0);
|
setOutput('risk_summary', '代理PR变更文件列表不可用');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📂 变更文件数: ${files.length}`);
|
console.log(`📂 变更文件数: ${files.length}`);
|
||||||
files.forEach(f => console.log(` · ${f}`));
|
files.forEach(f => console.log(` · ${f}`));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// ─── 识别开发者 ───
|
// ─── 查找注册开发者(通过 devId) ───
|
||||||
const developer = identifyDeveloper(author, config);
|
const developer = identity.devId ? findDeveloperByDevId(identity.devId, config) : null;
|
||||||
if (developer) {
|
if (developer) {
|
||||||
console.log(`👤 识别开发者: ${developer.name} (${developer.devId})`);
|
console.log(`👤 已识别开发者: ${developer.name} (${developer.devId})`);
|
||||||
|
} else if (identity.devId) {
|
||||||
|
console.log(`⚠️ 开发者编号 ${identity.devId} 未在门禁系统注册`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`⚠️ 未注册开发者: ${author}`);
|
console.log('⚠️ 无法识别开发者身份');
|
||||||
}
|
}
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ─── 分析天眼区域接触情况 ───
|
||||||
|
const skyeyeZoneFiles = files.filter(f => isInSkyeyeZone(f));
|
||||||
|
const nonSkyeyeFiles = files.filter(f => !isInSkyeyeZone(f));
|
||||||
|
const touchesSkyeyeZone = skyeyeZoneFiles.length > 0;
|
||||||
|
|
||||||
|
console.log(`🛡️ 天眼包裹区域文件: ${skyeyeZoneFiles.length}/${files.length}`);
|
||||||
|
if (touchesSkyeyeZone) {
|
||||||
|
console.log(' ⚠️ 此PR修改了天眼系统包裹区域 — 启动完整审核');
|
||||||
|
skyeyeZoneFiles.forEach(f => console.log(` 🔒 ${f}`));
|
||||||
|
} else {
|
||||||
|
console.log(' ✅ 此PR未触及天眼包裹区域 — 沙箱内操作');
|
||||||
|
}
|
||||||
|
if (nonSkyeyeFiles.length > 0) {
|
||||||
|
console.log(` 📦 沙箱区域文件: ${nonSkyeyeFiles.length}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ─── 如果完全在沙箱内且有注册身份 → 放行 ───
|
||||||
|
if (!touchesSkyeyeZone && developer) {
|
||||||
|
console.log('✅ 沙箱内操作 + 已注册开发者 — 天眼合并膜放行');
|
||||||
|
setOutput('decision', 'pass');
|
||||||
|
setOutput('risk_level', 'low');
|
||||||
|
setOutput('identity_source', identity.source);
|
||||||
|
setOutput('risk_summary', `${developer.name}(${developer.devId}) 沙箱内操作,未触及天眼区域`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 加载统计 ───
|
// ─── 加载统计 ───
|
||||||
const stats = loadPRStats();
|
const stats = loadPRStats();
|
||||||
|
|
||||||
// ─── 风险检测 ───
|
// ─── 风险检测 ───
|
||||||
console.log('━━━ 风险检测开始 ━━━');
|
console.log('━━━ 天眼风险检测启动 ━━━');
|
||||||
const allRisks = [
|
const allRisks = [
|
||||||
...checkCriticalFiles(files),
|
...checkCriticalFiles(files),
|
||||||
...checkProtectedPaths(files, config, developer),
|
...checkSkyeyeZoneIntrusion(files, developer),
|
||||||
...checkBuildArtifacts(files),
|
...checkBuildArtifacts(files),
|
||||||
...checkDeveloperPaths(files, developer),
|
...checkCoreConfigTampering(files),
|
||||||
...checkMassDeletion(stats)
|
...checkMassDeletion(stats),
|
||||||
|
...checkWorkflowTampering(files)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Deduplicate by file + dimension
|
// Deduplicate by file + dimension
|
||||||
|
|
@ -377,13 +593,27 @@ function run() {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (risks.length === 0) {
|
if (risks.length === 0 && !touchesSkyeyeZone) {
|
||||||
console.log('✅ 未检测到风险');
|
console.log('✅ 未检测到风险(沙箱内操作)');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('═══ 决策: PASS ═══');
|
console.log('═══ 决策: PASS ═══');
|
||||||
setOutput('decision', 'pass');
|
setOutput('decision', 'pass');
|
||||||
setOutput('risk_level', 'low');
|
setOutput('risk_level', 'low');
|
||||||
setOutput('risk_summary', '未检测到风险,允许合并');
|
setOutput('identity_source', identity.source);
|
||||||
|
setOutput('risk_summary', '沙箱内操作,未检测到风险');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (risks.length === 0 && touchesSkyeyeZone) {
|
||||||
|
// Touches SkyEye zone but no specific risks detected
|
||||||
|
// This could happen if a registered developer touches their allowed paths
|
||||||
|
console.log('✅ 天眼区域变更已验证 — 在授权范围内');
|
||||||
|
console.log('');
|
||||||
|
console.log('═══ 决策: PASS ═══');
|
||||||
|
setOutput('decision', 'pass');
|
||||||
|
setOutput('risk_level', 'low');
|
||||||
|
setOutput('identity_source', identity.source);
|
||||||
|
setOutput('risk_summary', '天眼区域变更已验证,在授权范围内');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,21 +627,23 @@ function run() {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// ─── 决策 ───
|
// ─── 决策 ───
|
||||||
const { decision, risk_level } = makeDecision(risks);
|
const { decision, risk_level } = makeDecision(risks, touchesSkyeyeZone);
|
||||||
const summary = risks.map(r => `[${r.dimension}] ${r.detail}`).join(' | ');
|
const summary = risks.map(r => `[${r.dimension}] ${r.detail}`).join(' | ');
|
||||||
|
|
||||||
setOutput('decision', decision);
|
setOutput('decision', decision);
|
||||||
setOutput('risk_level', risk_level);
|
setOutput('risk_level', risk_level);
|
||||||
|
setOutput('identity_source', identity.source);
|
||||||
setOutput('risk_summary', summary.slice(0, 500));
|
setOutput('risk_summary', summary.slice(0, 500));
|
||||||
|
|
||||||
if (decision === 'block') {
|
if (decision === 'block') {
|
||||||
console.log(`═══ 决策: BLOCK (风险等级: ${risk_level}) ═══`);
|
console.log(`═══ 决策: BLOCK (风险等级: ${risk_level}) ═══`);
|
||||||
console.log('❌ PR被阻止合并 — 请联系仓库管理员审核');
|
console.log('❌ 天眼合并膜: 物理层拒绝合并');
|
||||||
|
console.log(' 此PR的变更触及天眼系统包裹区域且未通过审核');
|
||||||
|
console.log(' 请联系主权者 TCS-0002∞ 审核后手动合并');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else if (decision === 'warn') {
|
} else if (decision === 'warn') {
|
||||||
console.log(`═══ 决策: WARN (风险等级: ${risk_level}) ═══`);
|
console.log(`═══ 决策: WARN (风险等级: ${risk_level}) ═══`);
|
||||||
console.log('⚠️ PR存在风险 — 建议管理员审核后合并');
|
console.log('⚠️ 天眼合并膜: 存在风险 — 建议主权者审核');
|
||||||
// Warn exits with 0 to not block, but provides information
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
console.log(`═══ 决策: PASS (风险等级: ${risk_level}) ═══`);
|
console.log(`═══ 决策: PASS (风险等级: ${risk_level}) ═══`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue