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) => `}-${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 '';
+ if (status === 'WARN' || status === 'INFO') return '';
+ return '';
+ }
+
+ 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=""
- BADGE_WARN=""
- BADGE_FAIL=""
+ - 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=""
- if [ "$GITLEAKS_RES" == "FAIL" ]; then
- OVERALL_GRADE=""
- elif [ "$SNYK_RES" == "WARN" ]; then
- OVERALL_GRADE=""
- 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"