/**
* Compliance Report Templates
*
* Generates markdown reports from git-steer state data.
* Templates: executive-summary, change-records, vulnerability-report, full-audit
*/
import type { RfcEntry, QualityEntry, SecurityMetrics } from '../state/manager.js';
export interface ReportData {
metrics: SecurityMetrics;
rfcs: RfcEntry[];
quality: QualityEntry[];
dateRange?: { start: string; end: string };
}
export function generateReport(
template: string,
data: ReportData
): string {
switch (template) {
case 'executive-summary':
return generateExecutiveSummary(data);
case 'change-records':
return generateChangeRecords(data);
case 'vulnerability-report':
return generateVulnerabilityReport(data);
case 'full-audit':
return generateFullAudit(data);
default:
return generateExecutiveSummary(data);
}
}
function formatDateRange(dateRange?: { start: string; end: string }): string {
if (!dateRange) return 'All time';
return `${dateRange.start} to ${dateRange.end}`;
}
function generateExecutiveSummary(data: ReportData): string {
const { metrics } = data;
const fixRatePct = Math.round(metrics.fixRate * 100);
const avgMttrHours = Math.round(metrics.avgMttr);
// Top risk repos (most open CVEs)
const topRiskRepos = Object.entries(metrics.byRepo)
.map(([repo, counts]) => ({ repo, open: counts.total - counts.fixed, total: counts.total }))
.filter((r) => r.open > 0)
.sort((a, b) => b.open - a.open)
.slice(0, 10);
const severityTable = Object.entries(metrics.bySeverity)
.map(([sev, counts]) => `| ${sev.toUpperCase()} | ${counts.total} | ${counts.fixed} | ${counts.total - counts.fixed} |`)
.join('\n');
return `# Executive Security Summary
**Period:** ${formatDateRange(data.dateRange)}
**Generated:** ${new Date().toISOString().split('T')[0]}
## Key Metrics
| Metric | Value |
|--------|-------|
| Total CVEs Tracked | ${metrics.totalCves} |
| CVEs Fixed | ${metrics.fixedCves} |
| Fix Rate | ${fixRatePct}% |
| Avg MTTR | ${avgMttrHours} hours |
| Active RFCs | ${data.rfcs.filter((r) => r.status !== 'fixed' && r.status !== 'closed').length} |
## Severity Breakdown
| Severity | Total | Fixed | Open |
|----------|-------|-------|------|
${severityTable}
## Top Risk Repositories
| Repository | Open CVEs | Total CVEs |
|------------|-----------|------------|
${topRiskRepos.map((r) => `| ${r.repo} | ${r.open} | ${r.total} |`).join('\n')}
## Trend
${metrics.timeline.length > 0
? metrics.timeline.slice(-7).map((t) => `- **${t.date}:** ${t.opened} opened, ${t.fixed} fixed`).join('\n')
: 'No timeline data available.'}
---
*Generated by git-steer v0.2.0*
`;
}
function generateChangeRecords(data: ReportData): string {
const { rfcs } = data;
const rfcRows = rfcs
.sort((a, b) => b.ts.localeCompare(a.ts))
.map((rfc) => {
const mttr = rfc.mttr != null ? `${Math.round(rfc.mttr)}h` : '-';
const pr = rfc.prNumber ? `#${rfc.prNumber}` : '-';
return `| ${rfc.repo} | #${rfc.issueNumber} | ${rfc.severity.toUpperCase()} | ${rfc.status} | ${pr} | ${mttr} | ${rfc.ts.split('T')[0]} |`;
})
.join('\n');
return `# Change Records (RFC Log)
**Period:** ${formatDateRange(data.dateRange)}
**Generated:** ${new Date().toISOString().split('T')[0]}
**Total RFCs:** ${rfcs.length}
## Summary
| Status | Count |
|--------|-------|
| Open | ${rfcs.filter((r) => r.status === 'open').length} |
| In Progress | ${rfcs.filter((r) => r.status === 'in_progress').length} |
| Fixed | ${rfcs.filter((r) => r.status === 'fixed').length} |
| Closed | ${rfcs.filter((r) => r.status === 'closed').length} |
## RFC Details
| Repository | RFC | Severity | Status | PR | MTTR | Date |
|------------|-----|----------|--------|----|------|------|
${rfcRows || '| - | - | - | - | - | - | - |'}
---
*Generated by git-steer v0.2.0*
`;
}
function generateVulnerabilityReport(data: ReportData): string {
const { rfcs } = data;
// Flatten all vulnerabilities from all RFCs
const allVulns = rfcs.flatMap((rfc) =>
rfc.vulnerabilities.map((v) => ({
...v,
repo: rfc.repo,
status: rfc.status,
issueNumber: rfc.issueNumber,
prNumber: rfc.prNumber,
}))
);
const vulnRows = allVulns
.sort((a, b) => {
const order = ['critical', 'high', 'medium', 'low'];
return order.indexOf(a.severity) - order.indexOf(b.severity);
})
.map((v) => {
const status = v.status === 'fixed' ? 'Fixed' : 'Open';
const pr = v.prNumber ? `#${v.prNumber}` : '-';
return `| ${v.cve || 'N/A'} | ${v.package} | ${v.severity.toUpperCase()} | ${v.fixVersion || 'N/A'} | ${status} | ${v.repo} | ${pr} |`;
})
.join('\n');
return `# Vulnerability Report
**Period:** ${formatDateRange(data.dateRange)}
**Generated:** ${new Date().toISOString().split('T')[0]}
**Total Vulnerabilities:** ${allVulns.length}
## Summary by Severity
| Severity | Total | Fixed | Open |
|----------|-------|-------|------|
| CRITICAL | ${allVulns.filter((v) => v.severity === 'critical').length} | ${allVulns.filter((v) => v.severity === 'critical' && v.status === 'fixed').length} | ${allVulns.filter((v) => v.severity === 'critical' && v.status !== 'fixed').length} |
| HIGH | ${allVulns.filter((v) => v.severity === 'high').length} | ${allVulns.filter((v) => v.severity === 'high' && v.status === 'fixed').length} | ${allVulns.filter((v) => v.severity === 'high' && v.status !== 'fixed').length} |
| MEDIUM | ${allVulns.filter((v) => v.severity === 'medium').length} | ${allVulns.filter((v) => v.severity === 'medium' && v.status === 'fixed').length} | ${allVulns.filter((v) => v.severity === 'medium' && v.status !== 'fixed').length} |
| LOW | ${allVulns.filter((v) => v.severity === 'low').length} | ${allVulns.filter((v) => v.severity === 'low' && v.status === 'fixed').length} | ${allVulns.filter((v) => v.severity === 'low' && v.status !== 'fixed').length} |
## Vulnerability Details
| CVE | Package | Severity | Fix Version | Status | Repository | PR |
|-----|---------|----------|-------------|--------|------------|----|
${vulnRows || '| - | - | - | - | - | - | - |'}
---
*Generated by git-steer v0.2.0*
`;
}
function generateFullAudit(data: ReportData): string {
const execSummary = generateExecutiveSummary(data);
const changeRecords = generateChangeRecords(data);
const vulnReport = generateVulnerabilityReport(data);
// Quality summary
const qualityByTool: Record<string, { runs: number; errors: number; warnings: number }> = {};
for (const q of data.quality) {
if (!qualityByTool[q.tool]) qualityByTool[q.tool] = { runs: 0, errors: 0, warnings: 0 };
qualityByTool[q.tool].runs++;
qualityByTool[q.tool].errors += q.errors;
qualityByTool[q.tool].warnings += q.warnings;
}
const qualityRows = Object.entries(qualityByTool)
.map(([tool, counts]) => `| ${tool} | ${counts.runs} | ${counts.errors} | ${counts.warnings} |`)
.join('\n');
return `# Full Audit Report
**Period:** ${formatDateRange(data.dateRange)}
**Generated:** ${new Date().toISOString().split('T')[0]}
---
${execSummary}
---
${changeRecords}
---
${vulnReport}
---
# Code Quality Summary
| Tool | Runs | Errors | Warnings |
|------|------|--------|----------|
${qualityRows || '| - | - | - | - |'}
---
*Full audit report generated by git-steer v0.2.0*
`;
}