diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c71e250..91a17c8 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Project Wiki/ 项目文档 - url: https://github.com/shuaiplus/nodewarden/wiki + url: https://nodewarden.app about: | Please check the documentation for common questions and troubleshooting steps. 请先查看文档,常见问题和排查步骤可能已经覆盖了你的问题。 diff --git a/.github/scripts/security.cjs b/.github/scripts/security.cjs deleted file mode 100644 index bead320..0000000 --- a/.github/scripts/security.cjs +++ /dev/null @@ -1,467 +0,0 @@ -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: 'security-report-cn.md', - switcher: '[English](security-report.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: '💡 *由 NodeWarden 安全工作流生成。透明度是我们的承诺。*', - auditedIcon: '✅ **已审计**', - noFiles: '未检索到文件。', - trivyTitle: '🛡️ 容器配置安全 (Trivy)', - trivyDesc: '`检测 Dockerfile 和容器配置中的安全风险与最佳实践。`', - trivySafe: '✅ **安全**:未发现容器配置缺陷。' - }, - en: { - filename: 'security-report.md', - switcher: 'English | [中文](security-report-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 the NodeWarden security workflow. 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/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 644582b..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: Security Scan - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - scan: - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - actions: read - env: - SECURITY_SNYK_TOKEN: ${{ secrets.SECURITY_SNYK_TOKEN }} - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - 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: Perform CodeQL Analysis - if: env.ACT != 'true' - continue-on-error: true - uses: github/codeql-action/analyze@v4 - with: - upload: true - output: sarif-results - - - name: Install Gitleaks - if: env.ACT != 'true' - continue-on-error: true - run: | - GITLEAKS_VERSION="8.28.0" - curl -sSL -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - tar -xzf gitleaks.tar.gz gitleaks - chmod +x gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - - - name: Secret Detection - if: env.ACT != 'true' - continue-on-error: true - run: | - gitleaks git . --report-format sarif --report-path results.sarif --no-banner || true - - - name: Install Project Dependencies - if: env.SECURITY_SNYK_TOKEN != '' - env: - SECURITY_PACKAGE: ${{ vars.SECURITY_PACKAGE || '' }} - run: | - 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 - echo "Warning: Directory $pkg not found, skipping." - fi - 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 - echo "exists=false" >> $GITHUB_OUTPUT - fi - - - 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 - - - name: Generate Security Report - 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.cjs - - # Also append to step summary for immediate visibility in GHA UI - cat security-report.md >> $GITHUB_STEP_SUMMARY - echo -e "\n---\n" >> $GITHUB_STEP_SUMMARY - cat security-report-cn.md >> $GITHUB_STEP_SUMMARY - - - name: Upload Gitleaks Results to GitHub Security - uses: github/codeql-action/upload-sarif@v4 - if: always() - with: - sarif_file: results.sarif - category: gitleaks - - - name: Upload Security Report Artifacts - if: always() - uses: actions/upload-artifact@v6 - with: - name: security-report - if-no-files-found: ignore - path: | - security-report.md - security-report-cn.md - snyk_result.txt - snyk_result.json - trivy_result.json - results.sarif - sarif-results/*.sarif