|
| 1 | +const { context } = require('@actions/github') |
| 2 | +const { getRepo, createComment, closeIssue, addLabel } = require('./github-utils') |
| 3 | +const { verifyCertificateOwnership, isOrgAdmin } = require('./cert-manager') |
| 4 | + |
| 5 | +/** |
| 6 | + * 从 issue body 中提取序列号 |
| 7 | + * @param {string} issueBody - Issue body 内容 |
| 8 | + * @returns {string|null} |
| 9 | + */ |
| 10 | +function extractSerialNumber (issueBody) { |
| 11 | + if (!issueBody) return null |
| 12 | + |
| 13 | + // 尝试匹配不同格式的序列号 |
| 14 | + // 格式1: Serial Number: `xxxxx` |
| 15 | + const match1 = issueBody.match(/Serial.*?Number.*?`([0-9a-fA-F]+)`/i) |
| 16 | + if (match1) return match1[1] |
| 17 | + |
| 18 | + // 格式2: serial_number: xxxxx (来自 GitHub issue template) |
| 19 | + const match2 = issueBody.match(/serial[_\s]*number[::]\s*([0-9a-fA-F]+)/i) |
| 20 | + if (match2) return match2[1] |
| 21 | + |
| 22 | + // 格式3: 直接的序列号(32位或更长的十六进制) |
| 23 | + const match3 = issueBody.match(/\b([0-9a-fA-F]{32,})\b/) |
| 24 | + if (match3) return match3[1] |
| 25 | + |
| 26 | + return null |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * 处理证书吊销请求 |
| 31 | + */ |
| 32 | +async function handleRevokeIssue () { |
| 33 | + try { |
| 34 | + const token = process.env.REPO_TOKEN |
| 35 | + const { owner, repo } = getRepo() |
| 36 | + const issue = context.payload.issue |
| 37 | + |
| 38 | + if (!issue) { |
| 39 | + console.log('Not an issue event') |
| 40 | + return |
| 41 | + } |
| 42 | + |
| 43 | + const issueNumber = issue.number |
| 44 | + const issueTitle = issue.title |
| 45 | + const issueBody = issue.body || '' |
| 46 | + const requester = issue.user.login |
| 47 | + |
| 48 | + // 检查是否是 revoke issue |
| 49 | + if (!issueTitle.toLowerCase().includes('[revoke]')) { |
| 50 | + console.log('Not a revoke issue') |
| 51 | + return |
| 52 | + } |
| 53 | + |
| 54 | + console.log(`Processing revoke request from @${requester}`) |
| 55 | + |
| 56 | + // 提取序列号 |
| 57 | + const serialNumber = extractSerialNumber(issueBody) |
| 58 | + |
| 59 | + if (!serialNumber) { |
| 60 | + console.log('No serial number found in issue body') |
| 61 | + await createComment( |
| 62 | + token, |
| 63 | + owner, |
| 64 | + repo, |
| 65 | + issueNumber, |
| 66 | + `❌ **吊销请求失败 / Revocation Request Failed**\n\n` + |
| 67 | + `无法从 Issue 中提取证书序列号。\n` + |
| 68 | + `Unable to extract certificate serial number from issue.\n\n` + |
| 69 | + `**请确保 Issue 包含以下信息 / Please ensure the issue contains:**\n` + |
| 70 | + `- \`Serial Number\`: \`1a2b3c4d5e6f7890...\`\n` + |
| 71 | + `- 或使用开发者门户自动填充 / Or use the Developer Portal to auto-fill\n\n` + |
| 72 | + `**提示 / Tip**: 序列号可以在您的证书签发评论中找到。\n` + |
| 73 | + `The serial number can be found in your certificate issuance comment.` |
| 74 | + ) |
| 75 | + await closeIssue(token, owner, repo, issueNumber, false) |
| 76 | + return |
| 77 | + } |
| 78 | + |
| 79 | + console.log(`Serial number extracted: ${serialNumber}`) |
| 80 | + |
| 81 | + // 验证证书所有权 |
| 82 | + console.log('Verifying certificate ownership...') |
| 83 | + const ownership = await verifyCertificateOwnership( |
| 84 | + serialNumber, |
| 85 | + requester, |
| 86 | + token, |
| 87 | + owner, |
| 88 | + repo |
| 89 | + ) |
| 90 | + |
| 91 | + if (!ownership.isOwner) { |
| 92 | + // 检查是否是组织管理员 |
| 93 | + console.log('User is not the owner, checking admin status...') |
| 94 | + const isAdmin = await isOrgAdmin(requester, token, owner) |
| 95 | + |
| 96 | + if (!isAdmin) { |
| 97 | + console.log('Permission denied: not owner and not admin') |
| 98 | + await createComment( |
| 99 | + token, |
| 100 | + owner, |
| 101 | + repo, |
| 102 | + issueNumber, |
| 103 | + `❌ **吊销请求被拒绝 / Revocation Request Denied**\n\n` + |
| 104 | + `@${requester},您无权吊销此证书。\n` + |
| 105 | + `You do not have permission to revoke this certificate.\n\n` + |
| 106 | + `**原因 / Reason**: 此证书不属于您 / This certificate does not belong to you\n\n` + |
| 107 | + (ownership.actualOwner |
| 108 | + ? `- **证书所有者 / Certificate Owner**: @${ownership.actualOwner}\n` + |
| 109 | + `- **原始 Issue / Original Issue**: #${ownership.issueNumber}\n\n` |
| 110 | + : `- **状态 / Status**: 证书未找到或已被吊销 / Certificate not found or already revoked\n\n`) + |
| 111 | + `**说明 / Note**:\n` + |
| 112 | + `- 只有证书所有者可以吊销自己的证书\n` + |
| 113 | + `- Only the certificate owner can revoke their own certificate\n` + |
| 114 | + `- 组织管理员拥有吊销任何证书的特殊权限\n` + |
| 115 | + `- Organization admins have special permission to revoke any certificate` |
| 116 | + ) |
| 117 | + await closeIssue(token, owner, repo, issueNumber, false) |
| 118 | + return |
| 119 | + } |
| 120 | + |
| 121 | + console.log(`Admin override: @${requester} is an organization admin`) |
| 122 | + } |
| 123 | + |
| 124 | + // 权限验证通过,执行吊销 |
| 125 | + console.log('Permission granted, proceeding with revocation...') |
| 126 | + |
| 127 | + // TODO: 在这里添加实际的 CRL 更新逻辑 |
| 128 | + // await addToCRL(serialNumber, 'keyCompromise') |
| 129 | + |
| 130 | + await createComment( |
| 131 | + token, |
| 132 | + owner, |
| 133 | + repo, |
| 134 | + issueNumber, |
| 135 | + `✅ **证书吊销成功 / Certificate Revoked Successfully**\n\n` + |
| 136 | + `- **序列号 / Serial Number**: \`${serialNumber}\`\n` + |
| 137 | + `- **吊销时间 / Revoked At**: ${new Date().toISOString()}\n` + |
| 138 | + `- **请求者 / Requested By**: @${requester}${ownership.isOwner ? '' : ' (Organization Admin)'}\n` + |
| 139 | + (ownership.actualOwner && !ownership.isOwner |
| 140 | + ? `- **证书所有者 / Certificate Owner**: @${ownership.actualOwner}\n` |
| 141 | + : '') + |
| 142 | + (ownership.issueNumber ? `- **原始 Issue / Original Issue**: #${ownership.issueNumber}\n` : '') + |
| 143 | + `\n---\n\n` + |
| 144 | + `**重要提示 / Important Notes**:\n\n` + |
| 145 | + `1. ✅ 该证书已被添加到吊销列表 / The certificate has been added to the revocation list\n` + |
| 146 | + `2. ⚠️ 使用此证书签名的模块将不再被信任 / Modules signed with this certificate will no longer be trusted\n` + |
| 147 | + `3. 🔄 如需新证书,请创建新的 \`[keyring]\` issue / To get a new certificate, create a new \`[keyring]\` issue\n` + |
| 148 | + `4. 📋 吊销信息将在下次 CRL 更新时生效 / Revocation will take effect on the next CRL update\n\n` + |
| 149 | + `**安全建议 / Security Recommendations**:\n` + |
| 150 | + `- 如果私钥泄露,请立即停止使用该证书签名 / If the private key was compromised, stop using it immediately\n` + |
| 151 | + `- 使用新证书重新签名所有模块 / Re-sign all modules with your new certificate\n` + |
| 152 | + `- 保护好新的私钥,不要与任何人分享 / Protect your new private key, never share it with anyone` |
| 153 | + ) |
| 154 | + |
| 155 | + await addLabel(token, owner, repo, issueNumber, 'revoked') |
| 156 | + await closeIssue(token, owner, repo, issueNumber, true) |
| 157 | + |
| 158 | + console.log(`Certificate ${serialNumber} revoked successfully`) |
| 159 | + } catch (error) { |
| 160 | + console.error('Error handling revoke issue:', error) |
| 161 | + console.error('Error stack:', error.stack) |
| 162 | + |
| 163 | + try { |
| 164 | + const token = process.env.REPO_TOKEN |
| 165 | + const { owner, repo } = getRepo() |
| 166 | + const issue = context.payload.issue |
| 167 | + |
| 168 | + if (issue) { |
| 169 | + await createComment( |
| 170 | + token, |
| 171 | + owner, |
| 172 | + repo, |
| 173 | + issue.number, |
| 174 | + `❌ **系统错误 / System Error**\n\n` + |
| 175 | + `处理吊销请求时发生错误。\n` + |
| 176 | + `An error occurred while processing the revocation request.\n\n` + |
| 177 | + `**错误信息 / Error**: ${error.message}\n\n` + |
| 178 | + `请联系管理员或在仓库中报告此问题。\n` + |
| 179 | + `Please contact an administrator or report this issue in the repository.` |
| 180 | + ) |
| 181 | + } |
| 182 | + } catch (commentError) { |
| 183 | + console.error('Failed to post error comment:', commentError) |
| 184 | + } |
| 185 | + |
| 186 | + throw error |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +module.exports = { |
| 191 | + handleRevokeIssue, |
| 192 | + extractSerialNumber |
| 193 | +} |
0 commit comments