fase(15): reporting module with pdf generation
This commit is contained in:
138
dist/modules/reporting/infrastructure/generators/HTMLReportGenerator.js
vendored
Normal file
138
dist/modules/reporting/infrastructure/generators/HTMLReportGenerator.js
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.HTMLReportGenerator = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
class HTMLReportGenerator {
|
||||
async generate(report, findings) {
|
||||
const outputDir = path.join(process.cwd(), 'reports', report.id.toString());
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
||||
for (const f of findings) {
|
||||
const sev = f.severity.value;
|
||||
severityCounts[sev] = (severityCounts[sev] ?? 0) + 1;
|
||||
}
|
||||
const findingsHtml = findings.map(f => `
|
||||
<div class="finding severity-${f.severity.value}">
|
||||
<div class="finding-header">
|
||||
<span class="badge badge-${f.severity.value}">${f.severity.value.toUpperCase()}</span>
|
||||
<span class="finding-type">${f.type.value}</span>
|
||||
<span class="finding-status">${f.status.value}</span>
|
||||
</div>
|
||||
<p class="finding-desc">${escapeHtml(f.description)}</p>
|
||||
<small class="finding-meta">
|
||||
Session: ${f.sessionId} ·
|
||||
${new Date(f.createdAt).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
`).join('\n');
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${escapeHtml(report.title)}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 0 auto; padding: 2rem; color: #1a1a1a; }
|
||||
h1 { font-size: 1.75rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 0.5rem; }
|
||||
.meta { color: #64748b; font-size: 0.875rem; margin-bottom: 2rem; }
|
||||
.stats { display: flex; gap: 1rem; margin: 1.5rem 0; }
|
||||
.stat-card { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem 1.5rem; min-width: 100px; text-align: center; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: 700; }
|
||||
.stat-card .label { font-size: 0.75rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.finding { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; margin-bottom: 0.75rem; }
|
||||
.finding-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.badge { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; font-weight: 700; }
|
||||
.badge-critical { background: #fee2e2; color: #dc2626; }
|
||||
.badge-high { background: #ffedd5; color: #ea580c; }
|
||||
.badge-medium { background: #fef9c3; color: #ca8a04; }
|
||||
.badge-low { background: #dbeafe; color: #2563eb; }
|
||||
.finding-type { font-family: monospace; font-size: 0.8rem; color: #475569; }
|
||||
.finding-status { margin-left: auto; font-size: 0.75rem; color: #64748b; }
|
||||
.finding-desc { margin: 0.25rem 0; font-size: 0.9rem; }
|
||||
.finding-meta { color: #94a3b8; font-size: 0.75rem; }
|
||||
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 0.75rem; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeHtml(report.title)}</h1>
|
||||
<div class="meta">
|
||||
Generated by ABE · ${new Date().toLocaleString()}
|
||||
${report.filters.sessionId ? ` · Session: ${report.filters.sessionId}` : ''}
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="value">${findings.length}</div>
|
||||
<div class="label">Total</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" style="color:#dc2626">${severityCounts['critical'] ?? 0}</div>
|
||||
<div class="label">Critical</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" style="color:#ea580c">${severityCounts['high'] ?? 0}</div>
|
||||
<div class="label">High</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" style="color:#ca8a04">${severityCounts['medium'] ?? 0}</div>
|
||||
<div class="label">Medium</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="value" style="color:#2563eb">${severityCounts['low'] ?? 0}</div>
|
||||
<div class="label">Low</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Findings (${findings.length})</h2>
|
||||
${findings.length === 0 ? '<p style="color:#64748b">No findings match the selected filters.</p>' : findingsHtml}
|
||||
|
||||
<footer>Generated by ABE — Autonomous Bug Explorer</footer>
|
||||
</body>
|
||||
</html>`;
|
||||
const filePath = path.join(outputDir, `report.html`);
|
||||
fs.writeFileSync(filePath, html, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
exports.HTMLReportGenerator = HTMLReportGenerator;
|
||||
function escapeHtml(s) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
88
dist/modules/reporting/infrastructure/generators/JSONReportGenerator.js
vendored
Normal file
88
dist/modules/reporting/infrastructure/generators/JSONReportGenerator.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.JSONReportGenerator = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
class JSONReportGenerator {
|
||||
async generate(report, findings) {
|
||||
const outputDir = path.join(process.cwd(), 'reports', report.id.toString());
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const data = {
|
||||
reportId: report.id.toString(),
|
||||
title: report.title,
|
||||
generatedAt: new Date().toISOString(),
|
||||
filters: report.filters,
|
||||
summary: {
|
||||
total: findings.length,
|
||||
bySeverity: buildSeverityCount(findings),
|
||||
byStatus: buildStatusCount(findings),
|
||||
},
|
||||
findings: findings.map(f => ({
|
||||
id: f.id.toString(),
|
||||
sessionId: f.sessionId,
|
||||
type: f.type.value,
|
||||
severity: f.severity.value,
|
||||
description: f.description,
|
||||
status: f.status.value,
|
||||
browser: f.browser,
|
||||
createdAt: f.createdAt.toISOString(),
|
||||
resolvedAt: f.resolvedAt?.toISOString() ?? null,
|
||||
evidence: f.evidence.toJSON(),
|
||||
actionTraceLength: f.actionTrace.length,
|
||||
})),
|
||||
};
|
||||
const filePath = path.join(outputDir, 'report.json');
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
exports.JSONReportGenerator = JSONReportGenerator;
|
||||
function buildSeverityCount(findings) {
|
||||
const counts = {};
|
||||
for (const f of findings) {
|
||||
const s = f.severity.value;
|
||||
counts[s] = (counts[s] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
function buildStatusCount(findings) {
|
||||
const counts = {};
|
||||
for (const f of findings) {
|
||||
const s = f.status.value;
|
||||
counts[s] = (counts[s] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
81
dist/modules/reporting/infrastructure/generators/PDFReportGenerator.js
vendored
Normal file
81
dist/modules/reporting/infrastructure/generators/PDFReportGenerator.js
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PDFReportGenerator = void 0;
|
||||
const path = __importStar(require("path"));
|
||||
const fs = __importStar(require("fs"));
|
||||
const HTMLReportGenerator_1 = require("./HTMLReportGenerator");
|
||||
/**
|
||||
* PDF report generator — uses Playwright to render the HTML report to PDF.
|
||||
* Requires Playwright + Chromium to be installed.
|
||||
*/
|
||||
class PDFReportGenerator {
|
||||
constructor() {
|
||||
this.htmlGenerator = new HTMLReportGenerator_1.HTMLReportGenerator();
|
||||
}
|
||||
async generate(report, findings) {
|
||||
// First generate the HTML version
|
||||
const htmlPath = await this.htmlGenerator.generate(report, findings);
|
||||
const outputDir = path.dirname(htmlPath);
|
||||
const pdfPath = path.join(outputDir, 'report.pdf');
|
||||
// Use Playwright to convert HTML to PDF
|
||||
let chromium;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const pw = require('playwright');
|
||||
chromium = pw.chromium;
|
||||
}
|
||||
catch {
|
||||
throw new Error('Playwright not available — install playwright to generate PDF reports');
|
||||
}
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
const htmlContent = fs.readFileSync(htmlPath, 'utf8');
|
||||
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
|
||||
await page.pdf({
|
||||
path: pdfPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
|
||||
});
|
||||
}
|
||||
finally {
|
||||
await browser.close();
|
||||
}
|
||||
return pdfPath;
|
||||
}
|
||||
}
|
||||
exports.PDFReportGenerator = PDFReportGenerator;
|
||||
Reference in New Issue
Block a user