mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
merge: adopt simplified security scan workflow from pr-70
This commit is contained in:
@@ -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: '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: '💡 *由 Antigravity AI 安全引擎生成。透明度是我们的承诺。*',
|
||||||
|
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 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 += `<details>\n<summary><b>GitHub Actions (${this.results.coverage.actions})</b></summary>\n\n`;
|
||||||
|
md += this.generateTable('actions', t);
|
||||||
|
md += `\n</details>\n\n`;
|
||||||
|
|
||||||
|
md += `<details>\n<summary><b>JavaScript (${this.results.coverage.js})</b></summary>\n\n`;
|
||||||
|
md += this.generateTable('js', t);
|
||||||
|
md += `\n</details>\n\n`;
|
||||||
|
|
||||||
|
md += `<details>\n<summary><b>TypeScript (${this.results.coverage.ts})</b></summary>\n\n`;
|
||||||
|
md += this.generateTable('ts', t);
|
||||||
|
md += `\n</details>\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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
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@v4
|
||||||
|
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: 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: |
|
||||||
|
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@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
|
category: gitleaks
|
||||||
|
|
||||||
|
- name: Upload Security Report Artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user