From 7356113f8915981ef77fbf00016500966e20bbcf Mon Sep 17 00:00:00 2001 From: nap0o Date: Tue, 10 Mar 2026 11:22:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=89=AB=E6=8F=8F=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 - .github/scripts/security.js | 467 +++++++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 2 +- .github/workflows/security.yml | 371 +++++++------------------- .gitignore | 1 - wrangler.demo.toml | 1 - wrangler.dev.toml | 1 - wrangler.toml | 1 - 8 files changed, 556 insertions(+), 289 deletions(-) create mode 100644 .github/scripts/security.js diff --git a/.dockerignore b/.dockerignore index 23c3188..1211a2c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,7 +23,6 @@ data/ .agents/ .act/ .actrc -.security.md # Environment & Secrets .secrets diff --git a/.github/scripts/security.js b/.github/scripts/security.js new file mode 100644 index 0000000..f403f3b --- /dev/null +++ b/.github/scripts/security.js @@ -0,0 +1,467 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Security Report Generator (Node.js) + * Better, faster, and more maintainable than Bash. + */ + +class SecurityReport { + constructor() { + this.results = { + codeql: { status: 'PASS', findings: [], alertCount: 0, rulesCount: 0 }, + snyk: { status: 'PASS', findings: [], vulnCount: 0 }, + gitleaks: { status: 'PASS', findings: [], leaksCount: 0 }, + trivy: { status: 'PASS', findings: [], misconfigCount: 0 }, + coverage: { actions: 0, js: 0, ts: 0 }, + artifactUris: [] + }; + this.auditTime = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; + this.runId = process.env.GITHUB_RUN_ID || '0'; + this.repository = process.env.GITHUB_REPOSITORY || 'unknown/repo'; + this.runUrl = `https://github.com/${this.repository}/actions/runs/${this.runId}`; + + this.locales = { + zh: { + filename: 'README_CN.md', + switcher: '[English](README.md) | 中文', + title: '🛡️ 安全审计与透明度报告', + grade: '安全评级', + important: '> [!IMPORTANT]\n> 本报告由 **GitHub Actions** 自动生成。为确保数据主权的绝对透明度,所有核心模块的安全扫描结果均实时公开。', + auditTime: '📅 审计时间', + runId: '📝 运行 ID', + env: '🛠️ 环境', + dashboard: '📉 实时安全仪表盘', + tool: '工具', + status: '状态', + findings: '发现项', + leaks: '泄露', + vulns: '漏洞', + alerts: '告警', + coverageTitle: '🔍 扫描覆盖范围', + module: '模块', + auditedFiles: '已审计文件', + coverage: '覆盖率', + detailedFindings: '🔍 详细发现项', + gitleaksTitle: '🔑 凭据泄露检查 (Gitleaks)', + gitleaksDesc: '`检测代码历史记录中硬编码的 API 密钥、密码或其他敏感令牌。`', + gitleaksSafe: '✅ **安全**:未发现硬编码的敏感凭据。', + gitleaksScope: '`扫描范围:所有代码更改和 Git 历史记录 (Gitleaks 全量扫描)`', + snykTitle: '📦 第三方依赖', + snykSafe: '✅ **安全**:在依赖项中未发现已知漏洞。', + package: '软件包', + severity: '严重程度', + description: '描述', + fixPlan: '修复方案', + codeqlTitle: '💻 代码质量与安全 (CodeQL)', + codeqlSummary: '#### 摘要', + rulesChecked: '已检查规则', + totalAlerts: '告警总数', + codeqlSafe: '✅ **安全**:CodeQL 扫描清洁,未检测到问题。', + ruleId: '规则 ID', + level: '级别', + location: '位置', + auditedList: '📂 已审计文件列表', + guideTitle: '⚠️ 操作指南', + guideDesc: '如果您看到 **FAIL** 状态或严重的代码问题:', + guideStep1: '1. **开发人员**:使用上方表格中的 **位置** 列找到确切的文件和行号。', + guideStep2: '2. **纠正**:遵循为每个规则提供的文档链接以提交修复。', + guideStep3: '3. **可追溯性**:完整的原始 `.sarif` 数据已附加到此分支。下载并将其导入您的 IDE(例如 VS Code SARIF 查看器)进行本地分析。', + footer: '💡 *由 Antigravity AI 安全引擎生成。透明度是我们的承诺。*', + auditedIcon: '✅ **已审计**', + noFiles: '未检索到文件。', + trivyTitle: '🛡️ 容器配置安全 (Trivy)', + trivyDesc: '`检测 Dockerfile 和容器配置中的安全风险与最佳实践。`', + trivySafe: '✅ **安全**:未发现容器配置缺陷。' + }, + en: { + filename: 'README.md', + switcher: 'English | [中文](README_CN.md)', + title: '🛡️ Security Audit & Transparency Report', + grade: 'Security Grade', + important: '> [!IMPORTANT]\n> This report is automatically generated by **GitHub Actions**. To ensure absolute transparency of data sovereignty, all core module security scan results are made public in real-time.', + auditTime: '📅 Audit Time', + runId: '📝 Run ID', + env: '🛠️ Environment', + dashboard: '📉 Real-time Security Dashboard', + tool: 'Tool', + status: 'Status', + findings: 'Findings', + leaks: 'Leaks', + vulns: 'Vulns', + alerts: 'Alerts', + coverageTitle: '🔍 Scan Coverage', + module: 'Module', + auditedFiles: 'Audited Files', + coverage: 'Coverage', + detailedFindings: '🔍 Detailed Findings', + gitleaksTitle: '🔑 Credential Leak Check (Gitleaks)', + gitleaksDesc: '`This section detects hardcoded API Keys, passwords, or other sensitive tokens in the code history.`', + gitleaksSafe: '✅ **SAFE**: No hardcoded sensitive credentials found.', + gitleaksScope: '`Scan Scope: All code changes and Git history (Gitleaks Full Scan)`', + snykTitle: '📦 Third-party Dependencies', + snykSafe: '✅ **SAFE**: No known vulnerabilities found in dependencies.', + package: 'Package', + severity: 'Severity', + description: 'Description', + fixPlan: 'Fix Plan', + codeqlTitle: '💻 Code Quality & Safety (CodeQL)', + codeqlSummary: '#### Summary', + rulesChecked: 'Rules Checked', + totalAlerts: 'Total Alerts', + codeqlSafe: '✅ **SAFE**: CodeQL clean. No issues detected.', + ruleId: 'Rule ID', + level: 'Level', + location: 'Location', + auditedList: '📂 Audited File List', + guideTitle: '⚠️ Action Guide', + guideDesc: 'If you see a **FAIL** status or serious code issues:', + guideStep1: '1. **Developers**: Use the **Location** column in the tables above to find the exact file and line number.', + guideStep2: '2. **Remediate**: Follow the documentation links provided for each rule to submit a fix.', + guideStep3: '3. **Traceability**: Full raw `.sarif` data is attached to this branch. Download and import it into your IDE (e.g., VS Code SARIF Viewer) for local analysis.', + footer: '💡 *Generated by Antigravity AI Security Engine. Transparency is our commitment.*', + auditedIcon: '✅ **Audited**', + noFiles: 'No files found.', + trivyTitle: '🛡️ Container Config Security (Trivy)', + trivyDesc: '`This section detects security risks and best practices in Dockerfile and container configurations.`', + trivySafe: '✅ **SAFE**: No container configuration defects found.' + } + }; + } + + // --- Data Parsers --- + + async parseCodeQL() { + const sarifPath = 'sarif-results'; + if (!fs.existsSync(sarifPath)) return; + + const files = this.globFiles(sarifPath, '.sarif'); + let totalAlerts = 0; + let rulesSet = new Set(); + let findings = []; + let artifactUris = new Set(); + + for (const file of files) { + const data = JSON.parse(fs.readFileSync(file, 'utf8')); + for (const run of data.runs || []) { + // Collect Rules + (run.tool.driver.rules || []).forEach(r => rulesSet.add(r.id)); + (run.tool.extensions || []).forEach(ext => { + (ext.rules || []).forEach(r => rulesSet.add(r.id)); + }); + + // Collect Results + for (const res of run.results || []) { + totalAlerts++; + const loc = (res.locations && res.locations[0]?.physicalLocation) || {}; + findings.push({ + id: res.ruleId, + level: res.level || 'warning', + path: loc.artifactLocation?.uri || 'Global', + line: loc.region?.startLine || '-', + message: res.message?.text || 'No description' + }); + } + + // Track Coverage (Deduplicated) + (run.artifacts || []).forEach(art => { + const uri = art.location?.uri || ''; + if (uri) artifactUris.add(uri); + }); + } + } + + this.results.artifactUris = Array.from(artifactUris).sort(); + this.results.coverage.actions = this.results.artifactUris.filter(u => u.startsWith('.github/workflows/')).length; + this.results.coverage.js = this.results.artifactUris.filter(u => u.endsWith('.js')).length; + this.results.coverage.ts = this.results.artifactUris.filter(u => u.endsWith('.ts')).length; + + this.results.codeql.alertCount = totalAlerts; + this.results.codeql.rulesCount = rulesSet.size; + this.results.codeql.findings = findings; + if (totalAlerts > 0) this.results.codeql.status = 'INFO'; + } + + async parseSnyk() { + const jsonPath = 'snyk_result.json'; + if (!fs.existsSync(jsonPath)) return; + + try { + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + const projects = Array.isArray(data) ? data : [data]; + let vulnTotal = 0; + let findings = []; + + for (const proj of projects) { + const vulns = proj.vulnerabilities || []; + vulnTotal += vulns.length; + vulns.forEach(v => { + findings.push({ + pkg: `${v.packageName}@${v.version}`, + severity: v.severity, + title: v.title, + url: v.url, + fixedIn: Array.isArray(v.fixedIn) ? v.fixedIn.join(', ') : (v.fixedIn || 'N/A') + }); + }); + } + + this.results.snyk.vulnCount = vulnTotal; + this.results.snyk.findings = findings; + if (vulnTotal > 0) this.results.snyk.status = 'WARN'; + } catch (e) { + console.error('Error parsing Snyk JSON:', e.message); + } + } + + async parseGitleaks() { + const files = this.globFiles('.', 'results.sarif'); + if (files.length === 0) return; + + try { + const data = JSON.parse(fs.readFileSync(files[0], 'utf8')); + let leaks = 0; + let findings = []; + for (const run of data.runs || []) { + for (const res of run.results || []) { + leaks++; + findings.push({ + id: res.ruleId, + message: res.message.text, + path: res.locations[0]?.physicalLocation?.artifactLocation?.uri || 'Unknown' + }); + } + } + + this.results.gitleaks.leaksCount = leaks; + this.results.gitleaks.findings = findings; + if (leaks > 0) this.results.gitleaks.status = 'FAIL'; + } catch (e) { + console.error('Error parsing Gitleaks SARIF:', e.message); + } + } + + async parseTrivy() { + const jsonPath = 'trivy_result.json'; + if (!fs.existsSync(jsonPath)) return; + + try { + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + let misconfigs = 0; + let findings = []; + + (data.Results || []).forEach(res => { + (res.Misconfigurations || []).forEach(m => { + misconfigs++; + findings.push({ + id: m.ID, + severity: m.Severity, + title: m.Title, + message: m.Message, + status: m.Status, + target: res.Target + }); + }); + }); + + this.results.trivy.misconfigCount = misconfigs; + this.results.trivy.findings = findings; + if (misconfigs > 0) this.results.trivy.status = 'WARN'; + } catch (e) { + console.error('Error parsing Trivy JSON:', e.message); + } + } + + generateTable(type, t) { + let files = []; + if (type === 'actions') files = this.results.artifactUris.filter(u => u.startsWith('.github/workflows/')); + else if (type === 'js') files = this.results.artifactUris.filter(u => u.endsWith('.js')); + else if (type === 'ts') files = this.results.artifactUris.filter(u => u.endsWith('.ts')); + + if (files.length === 0) return `> ${t.noFiles}\n`; + + let table = `| ${t.module} | ${t.location} | ${t.status} |\n| :--- | :--- | :--- |\n`; + files.forEach(f => { + const filename = path.basename(f); + table += `| \`${filename}\` | \`${f}\` | ${t.auditedIcon} |\n`; + }); + return table; + } + + // --- Renderers --- + + generateMarkdown(localeKey) { + const { codeql, snyk, gitleaks, coverage } = this.results; + const t = this.locales[localeKey]; + + // Calculate Grade + let grade = 'A+'; + let gradeColor = 'success'; + if (gitleaks.status === 'FAIL') { grade = 'D'; gradeColor = 'red'; } + else if (snyk.vulnCount > 10 || this.results.trivy.misconfigCount > 5) { grade = 'C'; gradeColor = 'orange'; } + else if (snyk.vulnCount > 0 || codeql.alertCount > 0 || this.results.trivy.misconfigCount > 0) { grade = 'B'; gradeColor = 'blue'; } + + const badge = (label, value, color) => `![${label}](https://img.shields.io/badge/${label.replace(/ /g, '_')}-${value}-${color}?style=for-the-badge)`; + + let md = `# ${t.title}\n\n`; + md += `${t.switcher}\n\n`; + md += `${badge(t.grade.replace(/ /g, '_'), grade, gradeColor)}\n\n`; + md += `${t.important}\n\n`; + + md += `| ${t.auditTime} | ${t.runId} | ${t.env} |\n`; + md += `| :--- | :--- | :--- |\n`; + md += `| \`${this.auditTime}\` | [#${this.runId}](${this.runUrl}) | \`GitHub CI/CD\` |\n\n`; + + md += `---\n\n## ${t.dashboard}\n\n`; + md += `| ${t.tool} | ${t.status} | ${t.findings} |\n`; + md += `| :--- | :--- | :--- |\n`; + md += `| **Credential Leak (Gitleaks)** | ${this.getBadge(gitleaks.status)} | \`${gitleaks.leaksCount}\` ${t.leaks} |\n`; + md += `| **Dependency Scan (Snyk)** | ${this.getBadge(snyk.status)} | \`${snyk.vulnCount}\` ${t.vulns} |\n`; + md += `| **Static Analysis (CodeQL)** | ${this.getBadge(codeql.status)} | \`${codeql.alertCount}\` ${t.alerts} |\n`; + md += `| **Container Scan (Trivy)** | ${this.getBadge(this.results.trivy.status)} | \`${this.results.trivy.misconfigCount}\` ${t.findings} |\n\n`; + + md += `---\n\n## ${t.coverageTitle}\n\n`; + md += `| ${t.module} | ${t.auditedFiles} | ${t.coverage} |\n`; + md += `| :--- | :---: | :---: |\n`; + md += `| **GitHub Actions** | \`${coverage.actions}\` | ✨ **100%** |\n`; + md += `| **JavaScript (Frontend)** | \`${coverage.js}\` | ✨ **100%** |\n`; + md += `| **TypeScript (Backend)** | \`${coverage.ts}\` | ✨ **100%** |\n\n`; + + md += `---\n\n## ${t.detailedFindings}\n\n`; + + // Gitleaks Section + md += `### ${t.gitleaksTitle}\n`; + md += `${t.gitleaksDesc} ${t.gitleaksScope}\n\n`; + if (gitleaks.findings.length > 0) { + md += `| ${t.ruleId} | ${t.location} | ${t.description} |\n`; + md += `| :--- | :--- | :--- |\n`; + gitleaks.findings.forEach(f => { + md += `| \`${f.id}\` | \`${f.path}\` | ${f.message} |\n`; + }); + } else { + md += `${t.gitleaksSafe}\n`; + } + + // Trivy Section + md += `\n### ${t.trivyTitle}\n`; + md += `${t.trivyDesc}\n\n`; + if (this.results.trivy.findings.length > 0) { + md += `| ${t.ruleId} | ${t.severity} | ${t.location} | ${t.description} |\n`; + md += `| :--- | :---: | :--- | :--- |\n`; + this.results.trivy.findings.forEach(f => { + const icon = f.severity === 'CRITICAL' ? '🔴' : (f.severity === 'HIGH' ? '🟠' : '🟡'); + md += `| \`${f.id}\` | ${icon} ${f.severity} | \`${f.target}\` | ${f.title}: ${f.message} |\n`; + }); + } else { + md += `${t.trivySafe}\n`; + } + + // Snyk Section + md += `\n### ${t.snykTitle}\n`; + if (snyk.findings.length > 0) { + md += `| ${t.package} | ${t.severity} | ${t.description} | ${t.fixPlan} |\n`; + md += `| :--- | :---: | :--- | :--- |\n`; + snyk.findings.forEach(f => { + const icon = f.severity === 'critical' ? '🔴' : (f.severity === 'high' ? '🟠' : '🟡'); + md += `| \`${f.pkg}\` | ${icon} ${f.severity} | [${f.title}](${f.url}) | ${f.fixedIn === 'N/A' ? 'No fix' : `Upgrade to \`${f.fixedIn}\``} |\n`; + }); + } else { + md += `${t.snykSafe}\n`; + } + + // CodeQL Section + md += `\n### ${t.codeqlTitle}\n`; + if (codeql.findings.length > 0) { + md += `${t.codeqlSummary}\n- **${t.rulesChecked}**: \`${codeql.rulesCount}\`\n- **${t.totalAlerts}**: \`${codeql.alertCount}\`\n\n`; + md += `| ${t.ruleId} | ${t.level} | ${t.location} | ${t.description} |\n`; + md += `| :--- | :---: | :--- | :--- |\n`; + codeql.findings.forEach(f => { + const icon = f.level === 'error' ? '🔴' : (f.level === 'warning' ? '🟠' : '🔵'); + const prefix = f.id.split('/')[0]; + const langMap = { + 'js': 'javascript', + 'actions': 'github-actions', + 'cpp': 'cpp', + 'cs': 'csharp', + 'go': 'go', + 'java': 'java', + 'py': 'python', + 'rb': 'ruby', + 'swift': 'swift' + }; + const langPath = langMap[prefix] || 'javascript'; + md += `| [${f.id}](https://codeql.github.com/codeql-query-help/${langPath}/${f.id.replace(/\//g, '-')}/) | ${icon} ${f.level} | \`${f.path}:${f.line}\` | ${f.message} |\n`; + }); + } else { + md += `${t.codeqlSafe}\n`; + } + + // Audited Files List + md += `\n### ${t.auditedList}\n`; + md += `
\nGitHub Actions (${this.results.coverage.actions})\n\n`; + md += this.generateTable('actions', t); + md += `\n
\n\n`; + + md += `
\nJavaScript (${this.results.coverage.js})\n\n`; + md += this.generateTable('js', t); + md += `\n
\n\n`; + + md += `
\nTypeScript (${this.results.coverage.ts})\n\n`; + md += this.generateTable('ts', t); + md += `\n
\n\n`; + + // Action Guide + md += `--- \n\n## ${t.guideTitle}\n\n`; + md += `${t.guideDesc}\n`; + md += `${t.guideStep1}\n`; + md += `${t.guideStep2}\n`; + md += `${t.guideStep3}\n\n`; + + md += `--- \n\n${t.footer}`; + + return md; + } + + // --- Helpers --- + + getBadge(status) { + if (status === 'PASS') return '![Pass](https://img.shields.io/badge/Status-PASS-success?style=for-the-badge)'; + if (status === 'WARN' || status === 'INFO') return '![Warning](https://img.shields.io/badge/Status-NOTICE-orange?style=for-the-badge)'; + return '![Fail](https://img.shields.io/badge/Status-FAIL-red?style=for-the-badge)'; + } + + globFiles(dir, ext) { + let results = []; + const list = fs.readdirSync(dir); + for (const file of list) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + if (stat && stat.isDirectory()) { + results = results.concat(this.globFiles(fullPath, ext)); + } else if (file.endsWith(ext)) { + results.push(fullPath); + } + } + return results; + } + + async run() { + console.log('--- Security Report Generation Started ---'); + await this.parseCodeQL(); + await this.parseSnyk(); + await this.parseGitleaks(); + await this.parseTrivy(); + + for (const localeKey of Object.keys(this.locales)) { + const locale = this.locales[localeKey]; + const markdown = this.generateMarkdown(localeKey); + fs.writeFileSync(locale.filename, markdown); + console.log(`Report generated successfully at ${locale.filename}`); + } + } +} + +new SecurityReport().run().catch(err => { + console.error('Report generation failed:', err); + process.exit(1); +}); diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8e1cb95..5792003 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -116,7 +116,7 @@ jobs: # 部署到 Cloudflare Workers - name: Deploy to Cloudflare Workers - uses: cloudflare/wrangler-action@v3 + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 1e75d9b..3c66b79 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -16,331 +16,136 @@ jobs: contents: write security-events: write actions: read - + env: + SECURITY_SNYK_TOKEN: ${{ secrets.SECURITY_SNYK_TOKEN }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - # 1. 检查代码中的敏感信息 (Gitleaks) - - name: Secret Detection - id: gitleaks - uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2 - continue-on-error: true - - # 2. 检查依赖包漏洞 (Snyk) - - name: Dependency Scan - id: snyk - continue-on-error: true - run: | - npm install -g snyk - echo "Installing backend dependencies..." - cd backend && npm install --package-lock-only && cd .. - echo "Installing frontend dependencies..." - cd frontend && npm install --package-lock-only && cd .. - snyk auth ${{ secrets.SNYK_TOKEN }} - # 🚀 UX优化:同时输出人类可读的 TXT 日志与供程序解析的 JSON 数据文件 - snyk test --all-projects --json-file-output=snyk_result.json > snyk_result.txt || true - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - # 3. 静态代码安全分析 (CodeQL) - name: Initialize CodeQL if: env.ACT != 'true' + continue-on-error: true uses: github/codeql-action/init@v4 with: languages: javascript-typescript, actions + build-mode: none queries: security-extended,security-and-quality - - name: Autobuild - if: env.ACT != 'true' - uses: github/codeql-action/autobuild@v4 - - name: Perform CodeQL Analysis if: env.ACT != 'true' + continue-on-error: true uses: github/codeql-action/analyze@v4 with: upload: true output: sarif-results - # 4. 生成 README.md 报告 (🚀 UX 深度优化版) - - name: Generate Security Report + - name: Secret Detection + id: gitleaks + uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + format: sarif + report_path: results.sarif + + - name: Install Project Dependencies + if: env.SECURITY_SNYK_TOKEN != '' + env: + SECURITY_PACKAGE: ${{ vars.SECURITY_PACKAGE || '' }} run: | - REPORT_FILE="README.md" - - CODEQL_SARIFS=$(find sarif-results -name "*.sarif" 2>/dev/null) - REAL_CSV=$(find . -name "code-scanning-files-extracted.csv" | head -n 1) - - if [ -n "$REAL_CSV" ]; then - # Fallback csv 模式 - WF_FILES=$(grep ",GitHub Actions," "$REAL_CSV" | grep "\.github/workflows/" | sort -u | wc -l) - JS_FILES=$(grep ",JavaScript," "$REAL_CSV" | sort -u | wc -l) - TS_FILES=$(grep ",TypeScript," "$REAL_CSV" | sort -u | wc -l) - - gen_csv_table() { - echo "| 文件路径 | 状态 |" - echo "| :--- | :--- |" - if [ "$1" == "GitHub Actions" ]; then - grep ",$1," "$REAL_CSV" | grep "\.github/workflows/" | awk -F',' '{print "| `"$3"` | ✅ 已审计 |"}' | sort -u + echo "Preparing dependency lock files for security scanning..." + if [ -z "$SECURITY_PACKAGE" ]; then + echo "SECURITY_PACKAGE is empty, installing in root..." + npm install --package-lock-only + else + echo "SECURITY_PACKAGE is set to: $SECURITY_PACKAGE" + # Split by comma and install + IFS=',' read -ra PACKAGES <<< "$SECURITY_PACKAGE" + for pkg in "${PACKAGES[@]}"; do + if [ -d "$pkg" ]; then + echo "Installing in "$pkg"..." + npm install --prefix "$pkg" --package-lock-only else - grep ",$1," "$REAL_CSV" | awk -F',' '{print "| `"$3"` | ✅ 已审计 |"}' | sort -u + echo "Warning: Directory $pkg not found, skipping." fi - } - WF_TABLE=$(gen_csv_table "GitHub Actions") - JS_TABLE=$(gen_csv_table "JavaScript") - TS_TABLE=$(gen_csv_table "TypeScript") - - elif [ -n "$CODEQL_SARIFS" ]; then - WF_FILES=$(jq -s '[.[].runs[].artifacts[]? | select(.location.uri | startswith(".github/workflows/") and test("\\.ya?ml$"))] | unique_by(.location.uri) | length' $CODEQL_SARIFS || echo "0") - JS_FILES=$(jq -s '[.[].runs[].artifacts[]? | select(.location.uri | endswith(".js"))] | unique_by(.location.uri) | length' $CODEQL_SARIFS || echo "0") - TS_FILES=$(jq -s '[.[].runs[].artifacts[]? | select(.location.uri | endswith(".ts"))] | unique_by(.location.uri) | length' $CODEQL_SARIFS || echo "0") - - gen_sarif_table() { - echo "| 文件路径 | 状态 |" - echo "| :--- | :--- |" - if [ "$1" == "workflow" ]; then - jq -s -r '.[].runs[].artifacts[]? | select(.location.uri | startswith(".github/workflows/") and test("\\.ya?ml$")) | .location.uri' $CODEQL_SARIFS | sort -u | awk '{print "| `"$1"` | ✅ 已审计 |"}' - elif [ "$1" == "js" ]; then - jq -s -r '.[].runs[].artifacts[]? | select(.location.uri | endswith(".js")) | .location.uri' $CODEQL_SARIFS | sort -u | awk '{print "| `"$1"` | ✅ 已审计 |"}' - elif [ "$1" == "ts" ]; then - jq -s -r '.[].runs[].artifacts[]? | select(.location.uri | endswith(".ts")) | .location.uri' $CODEQL_SARIFS | sort -u | awk '{print "| `"$1"` | ✅ 已审计 |"}' - fi - } - WF_TABLE=$(gen_sarif_table "workflow") - JS_TABLE=$(gen_sarif_table "js") - TS_TABLE=$(gen_sarif_table "ts") + done + fi + + - name: Dependency Scan + id: snyk + if: env.SECURITY_SNYK_TOKEN != '' + continue-on-error: true + run: | + npm install -g snyk + snyk auth ${{ secrets.SECURITY_SNYK_TOKEN }} + snyk test --all-projects --json-file-output=snyk_result.json > snyk_result.txt || true + env: + SECURITY_SNYK_TOKEN: ${{ secrets.SECURITY_SNYK_TOKEN }} + + - name: Check for Dockerfile + id: check_docker + run: | + if [ -f "Dockerfile" ]; then + echo "exists=true" >> $GITHUB_OUTPUT else - WF_FILES="0"; JS_FILES="0"; TS_FILES="0" - WF_TABLE="| No files found | - |" - JS_TABLE="| No files found | - |" - TS_TABLE="| No files found | - |" + echo "exists=false" >> $GITHUB_OUTPUT fi - AUDIT_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + - name: Container Security Scan (Trivy) + if: steps.check_docker.outputs.exists == 'true' + continue-on-error: true + run: | + VERSION="0.56.1" + echo "Installing Trivy $VERSION..." + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin "v$VERSION" + trivy config . --format json --output trivy_result.json --severity CRITICAL,HIGH || true - BADGE_PASS="![Pass](https://img.shields.io/badge/Status-PASS-success?style=for-the-badge)" - BADGE_WARN="![Warning](https://img.shields.io/badge/Status-NOTICE-orange?style=for-the-badge)" - BADGE_FAIL="![Fail](https://img.shields.io/badge/Status-FAIL-red?style=for-the-badge)" + - name: Generate Security Report + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Gitleaks typically produces results.sarif if configured or by default in some versions + # We'll ensure it exists for our reporter + node .github/scripts/security.js - # 提取 Gitleaks 结果 - GITLEAKS_RES="PASS" - [ "${{ steps.gitleaks.outcome }}" != "success" ] && GITLEAKS_RES="FAIL" - - # 🚀 UX优化:解析 Snyk JSON 计算实际漏洞数量 - SNYK_VULNS_COUNT=0 - if [ -f "snyk_result.json" ]; then - SNYK_VULNS_COUNT=$(jq 'if type=="array" then [.[].vulnerabilities[]?] | length else (.vulnerabilities // []) | length end' snyk_result.json || echo "0") - fi - - SNYK_RES="PASS" - [ "$SNYK_VULNS_COUNT" -gt 0 ] && SNYK_RES="WARN" - [ "$SNYK_VULNS_COUNT" -eq 0 ] && grep -i -q "vulnerabilities found" snyk_result.txt && SNYK_RES="WARN" + # Also append to step summary for immediate visibility in GHA UI + cat README.md >> $GITHUB_STEP_SUMMARY + echo -e "\n---\n" >> $GITHUB_STEP_SUMMARY + cat README_CN.md >> $GITHUB_STEP_SUMMARY - OVERALL_GRADE="![Grade A+](https://img.shields.io/badge/Security_Grade-A%2B-success?style=for-the-badge)" - if [ "$GITLEAKS_RES" == "FAIL" ]; then - OVERALL_GRADE="![Grade D](https://img.shields.io/badge/Security_Grade-D-red?style=for-the-badge)" - elif [ "$SNYK_RES" == "WARN" ]; then - OVERALL_GRADE="![Grade B](https://img.shields.io/badge/Security_Grade-B-orange?style=for-the-badge)" - fi + - name: Upload Gitleaks Results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: results.sarif + category: gitleaks - { - echo "# 🛡️ 安全审计与透明度报告" - echo "" - echo " $OVERALL_GRADE" - echo "" - echo "> [!NOTE]" - echo "> 本报告由 **GitHub Actions** 自动生成。为了确保数据主权的绝对透明,我们将所有核心模块的安全扫描结果实时公开。" - echo "" - echo "| 📅 审计时间 | 📝 运行编号 | 🛠️ 环境 |" - echo "| :--- | :--- | :--- |" - echo "| \`$AUDIT_TIME\` | [#${{ github.run_id }}]($RUN_URL) | \`GitHub CI/CD\` |" - echo "" - echo "---" - echo "" - echo "## 📉 实时安全看板" - echo "" - echo "| 项目 | 状态 | 风险等级 |" - echo "| :--- | :--- | :--- |" - echo "| **敏感泄露检查 (Gitleaks)** | $([ "$GITLEAKS_RES" == "PASS" ] && echo "$BADGE_PASS" || echo "$BADGE_FAIL") | \`NONE\` |" - echo "| **依赖项漏洞扫描 (Snyk)** | $([ "$SNYK_RES" == "PASS" ] && echo "$BADGE_PASS" || echo "$BADGE_WARN") | \`LOW/MED\` |" - echo "| **静态代码分析 (CodeQL)** | $BADGE_PASS | \`SAFE\` |" - echo "" - echo "---" - echo "" - echo "## 🔍 扫描覆盖范围" - echo "" - echo "| 模块 | 已审计文件数 | 提取状态 |" - echo "| :--- | :---: | :---: |" - echo "| **GitHub Actions** | \`$WF_FILES\` | ✨ **100%** |" - echo "| **JavaScript** | \`$JS_FILES\` | ✨ **100%** |" - echo "| **TypeScript** | \`$TS_FILES\` | ✨ **100%** |" - echo "" - echo "---" - echo "" - echo "## 🔍 详细发现" - echo "" - - # 🚀 UX优化:全新的 Snyk 漏洞表格化展示 - echo "### 📦 依赖安全性" - echo "
" - echo "点击查看依赖漏洞详情" - echo "" - - if [ "$SNYK_VULNS_COUNT" -gt 0 ]; then - echo "> ⚠️ 共发现 **$SNYK_VULNS_COUNT** 个第三方依赖漏洞。" - echo "" - echo "| 依赖包 | 严重程度 | 漏洞描述 | 修复方案 |" - echo "| :--- | :---: | :--- | :--- |" - - jq -r ' - def levelIcon: if . == "critical" then "🔴" elif . == "high" then "🟠" elif . == "medium" then "🟡" else "🔵" end; - (if type=="array" then .[] else . end) | .vulnerabilities[]? | - "| `\(.packageName)@\(.version)` | \(levelIcon) \(.severity) | [\(.title)](\(.url)) | \(.fixedIn // ["无 / N/A"] | join(", ") | if . == "无 / N/A" then . else "Upgrade to `\(.)`" end) |" - ' snyk_result.json | sort -u >> $REPORT_FILE - - elif [ "$SNYK_RES" == "WARN" ]; then - # Fallback 显示 txt 结果 - echo "\`\`\`text" - grep -E -i "Tested|found|severity|Vulnerabilit|Upgrade|Path|Issue" snyk_result.txt || echo "Please check snyk_result.txt for details." - echo "\`\`\`" - else - echo "> ✅ **SAFE**: 未发现第三方依赖包包含已知安全漏洞。" - fi - - echo "
" - echo "" - - echo "### 🔑 凭证泄露检查" - echo "该项探测代码历史记录中是否存在硬编码的 API Key、密码或其他敏感令牌。" - echo "
" - echo "扫描结论" - echo "" - echo "> ✅ **SAFE**: 未发现代码中包含硬编码的敏感凭证。" - echo "" - echo "---" - echo "🔍 *审计范围:全量代码变更与 Git 历史 (Gitleaks Full Scan)*" - echo "
" - echo "" - echo "### 📂 审计文件列表" - echo "
" - echo "GitHub Actions ($WF_FILES 份流水线文件)" - echo "" - echo "$WF_TABLE" - echo "" - echo "
" - echo "" - echo "
" - echo "JavaScript ($JS_FILES 个前端文件)" - echo "" - echo "$JS_TABLE" - echo "" - echo "
" - echo "" - echo "
" - echo "TypeScript ($TS_FILES 个后端文件)" - echo "" - echo "$TS_TABLE" - echo "" - echo "
" - echo "" - echo "### 🔍 CodeQL静态代码安全分析细节" - echo "
" - echo "点击查看发现的代码问题" - echo "" - } > $REPORT_FILE - - if [ -n "$CODEQL_SARIFS" ]; then - ALERTS_COUNT=$(jq -s '[.[].runs[].results[]?] | length' $CODEQL_SARIFS) - ERRORS=$(jq -s '[.[].runs[].results[]? | select((.level // "warning") == "error")] | length' $CODEQL_SARIFS) - WARNINGS=$(jq -s '[.[].runs[].results[]? | select((.level // "warning") == "warning")] | length' $CODEQL_SARIFS) - NOTES=$(jq -s '[.[].runs[].results[]? | select((.level // "warning") == "note")] | length' $CODEQL_SARIFS) - RULES_COUNT=$(jq -s '[.[].runs[].tool.driver.rules[]?, .[].runs[].tool.extensions[].rules[]?] | unique_by(.id) | length' $CODEQL_SARIFS) - - { - echo "#### 审计摘要" - echo "- **检查规则总数**: \`$RULES_COUNT\` 项" - echo "- **发现的问题总数**: \`$ALERTS_COUNT\` 处" - echo "- 🔴 高危 (Error): \`$ERRORS\` | 🟠 中危 (Warning): \`$WARNINGS\` | 🔵 建议 (Note): \`$NOTES\` " - echo "" - echo "| 规则 ID | 严重程度 | 触发位置 | 问题描述 |" - echo "| :--- | :---: | :--- | :--- |" - } >> $REPORT_FILE - - if [ "$ALERTS_COUNT" -gt 0 ]; then - jq -s -r ' - def lang: .ruleId | split("/")[0] | if . == "js" then "javascript" elif . == "ts" then "typescript" else . end; - def levelIcon: (.level // "warning") | if . == "error" then "🔴" elif . == "warning" then "🟠" else "🔵" end; - .[].runs[].results[]? | - "| [\(.ruleId)](https://codeql.github.com/codeql-query-help/\(lang)/\(.ruleId | gsub("/";"-"))/) | \(levelIcon) \(.level // "warning") | `\(.locations[0].physicalLocation.artifactLocation.uri // "Project-level"):\(.locations[0].physicalLocation.region.startLine // "-")` | \(.message.text // "No description") |" - ' $CODEQL_SARIFS >> $REPORT_FILE - else - echo "| - | ✅ 完美 | - | 未检测到任何代码安全与质量问题 |" >> $REPORT_FILE - fi - else - echo "> ⚠️ CodeQL SARIF report not found." >> $REPORT_FILE - fi - - { - echo "
" - echo "" - echo "---" - echo "" - echo "## ⚠️ 行动指引 | Steps to Take" - echo "" - echo "如果您看到状态显示为 **FAIL** 或发现严重代码问题:" - echo "1. **开发者**:请立即通过上方表格的 **触发位置** 定位到具体文件和行号。" - echo "2. **修复**:参考静态分析细节中的官方文档链接了解漏洞详情并提交修复。" - echo "3. **追溯**:完整的 \`.sarif\` 原始数据已附在本分支,可下载后导入本地 IDE 插件进行分析。" - echo "" - echo "---" - echo "💡 *Powered by Gitleaks, Snyk & CodeQL. Transparency is our commitment.*" - } >> $REPORT_FILE - - cat $REPORT_FILE >> $GITHUB_STEP_SUMMARY - - # 5. 将报告、重命名的 SARIF 文件及 Snyk 结果推送到孤儿分支 - name: Push to Audit Branch if: github.event_name != 'pull_request' run: | mkdir audit_temp - cp README.md audit_temp/ + cp README_CN.md audit_temp/ + [ -f "snyk_result.txt" ] && cp snyk_result.txt audit_temp/ + [ -f "snyk_result.json" ] && cp snyk_result.json audit_temp/ - if [ -f "snyk_result.txt" ]; then - cp snyk_result.txt audit_temp/ + # Collect all SARIF files with descriptive names + [ -f "results.sarif" ] && cp results.sarif audit_temp/Gitleaks_results.sarif + if [ -d "sarif-results" ]; then + for f in sarif-results/*.sarif; do + [ -f "$f" ] && cp "$f" "audit_temp/CodeQL_$(basename "$f")" + done fi - # 🚀 UX优化:同步保存 Snyk JSON 文件到审计分支 - if [ -f "snyk_result.json" ]; then - cp snyk_result.json audit_temp/ - fi - - GITLEAKS_FILE=$(find . -name "results.sarif" | head -n 1) - if [ -n "$GITLEAKS_FILE" ]; then - cp "$GITLEAKS_FILE" audit_temp/Gitleaks_results.sarif - fi - - for f in sarif-results/*.sarif; do - if [ -f "$f" ]; then - fname=$(basename "$f") - cp "$f" "audit_temp/CodeQL_${fname}" - fi - done - cd audit_temp git init git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git checkout --orphan security-audit - git add README - [ -f "snyk_result.txt" ] && git add snyk_result.txt - [ -f "snyk_result.json" ] && git add snyk_result.json - ls *.sarif >/dev/null 2>&1 && git add *.sarif - - git commit -m "chore: archive security report, snyk logs and sarif data [skip ci]" - + git add . + git commit -m "chore: archive security report and raw data [skip ci]" git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} git push -f origin security-audit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3da016d..9767479 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ yarn-error.log* # Act (local GitHub Actions) configuration and state .actrc .event -SECURITY.md # Test related .test/ tests/ diff --git a/wrangler.demo.toml b/wrangler.demo.toml index 81449a6..250e8d6 100644 --- a/wrangler.demo.toml +++ b/wrangler.demo.toml @@ -7,7 +7,6 @@ compatibility_flags = ["nodejs_compat"] binding = "ASSETS" directory = "frontend/dist" not_found_handling = "single-page-application" -run_worker_first = true [build] command = "npm run build --prefix frontend" diff --git a/wrangler.dev.toml b/wrangler.dev.toml index 57d8c7a..2e591c2 100644 --- a/wrangler.dev.toml +++ b/wrangler.dev.toml @@ -11,7 +11,6 @@ port = 8787 binding = "ASSETS" directory = "frontend/dist" not_found_handling = "single-page-application" -run_worker_first = true [build] command = "npm run build --prefix ../frontend" diff --git a/wrangler.toml b/wrangler.toml index 207d694..1683be2 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -7,7 +7,6 @@ compatibility_flags = ["nodejs_compat"] binding = "ASSETS" directory = "frontend/dist" not_found_handling = "single-page-application" -run_worker_first = true [build] command = "npm run build --prefix frontend"