Skip to content

Commit 7f37e5b

Browse files
committed
feat: change findings export format from JSON to Markdown table
1 parent 0a5849d commit 7f37e5b

1 file changed

Lines changed: 79 additions & 3 deletions

File tree

internal/api/ui/pages/scan-detail.js

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@
893893
<div id="recon-quick-tools" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-bottom:1px solid var(--border);background:rgba(2,6,23,.38)">
894894
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
895895
<button type="button" id="recon-copy-selected-tsv" title="Copy checked rows from the current page" style="padding:6px 10px;background:rgba(34,211,238,.1);border:1px solid rgba(34,211,238,.35);border-radius:6px;color:var(--accent-cyan);font-size:11px;cursor:pointer;white-space:nowrap">📋 Copy selected</button>
896-
<button type="button" id="recon-export-all-json" title="Export all findings in the current view" style="padding:6px 10px;background:rgba(167,139,250,.08);border:1px solid rgba(167,139,250,.35);border-radius:6px;color:#c4b5fd;font-size:11px;cursor:pointer;white-space:nowrap">📥 Export Findings</button>
896+
<button type="button" id="recon-export-all-json" title="Export all findings in the current view as Markdown" style="padding:6px 10px;background:rgba(167,139,250,.08);border:1px solid rgba(167,139,250,.35);border-radius:6px;color:#c4b5fd;font-size:11px;cursor:pointer;white-space:nowrap">📥 Export Markdown</button>
897897
</div>
898898
<div id="recon-quick-chips" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"></div>
899899
<div style="margin-left:auto;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
@@ -1279,9 +1279,85 @@
12791279
exportJsonBtn.addEventListener('click', async () => {
12801280
const exportedRows = allRows.filter(r => rowMatch(r) && !HIDDEN_KINDS.has(r.kind));
12811281
if (!exportedRows.length) { showToast('info', 'No findings', 'There are no findings in the current view to export.'); return; }
1282+
1283+
const dynamicRawMode = exportedRows.some((r) => r && r.raw && typeof r.raw === 'object' && Object.keys(r.raw).length > 0) &&
1284+
isGitHubTableKind(activeKind) &&
1285+
!/nuclei/.test(String(activeKind || '').toLowerCase());
1286+
const dynamicCols = dynamicRawMode ? collectDynamicColumns(exportedRows) : [];
1287+
1288+
let markdown = '';
1289+
if (dynamicRawMode) {
1290+
const headers = ['DETECTORNAME', 'SEV', ...dynamicCols];
1291+
markdown += `| ${headers.join(' | ')} |\n`;
1292+
markdown += `| ${headers.map(() => '---').join(' | ')} |\n`;
1293+
exportedRows.forEach(r => {
1294+
const info = (r.raw && r.raw.info) || {};
1295+
const cols = [
1296+
r.title || info.detector_name || '—',
1297+
r.severity || 'info',
1298+
...dynamicCols.map(k => {
1299+
const v = r.raw[k];
1300+
return v !== undefined && v !== null ? String(v).replace(/\|/g, '\\|').replace(/\n/g, ' ') : '—';
1301+
})
1302+
];
1303+
markdown += `| ${cols.map(c => String(c).replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |\n`;
1304+
});
1305+
} else if (isGitHubTableKind(activeKind)) {
1306+
const headers = ['DETECTORNAME', 'SEV', 'VERIFIED', 'REDACTED', 'SOURCE FILE', 'LINE', 'SOURCE LINK'];
1307+
markdown += `| ${headers.join(' | ')} |\n`;
1308+
markdown += `| ${headers.map(() => '---').join(' | ')} |\n`;
1309+
exportedRows.forEach(r => {
1310+
const info = (r.raw && r.raw.info) || {};
1311+
const cols = [
1312+
r.title || info.detector_name || '—',
1313+
r.severity || 'info',
1314+
info.verified ? 'Yes' : 'No',
1315+
info.redacted ? 'Yes' : 'No',
1316+
r.file || r.module || '—',
1317+
info.line || '—',
1318+
info.url ? `[Link](${info.url})` : '—'
1319+
];
1320+
markdown += `| ${cols.map(c => String(c).replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |\n`;
1321+
});
1322+
} else {
1323+
const schema = window.ModuleRegistry?.get ? window.ModuleRegistry.get(activeKind) : null;
1324+
if (presetMode !== 'raw' && schema && schema.columns) {
1325+
const schemaCols = schema.columns;
1326+
markdown += `| ${schemaCols.map(c => c.label).join(' | ')} |\n`;
1327+
markdown += `| ${schemaCols.map(() => '---').join(' | ')} |\n`;
1328+
1329+
exportedRows.forEach(r => {
1330+
const extracted = schema.extract ? schema.extract(r, window.getModuleDisplayInfo(r.module)) : r;
1331+
const rowStr = schemaCols.map(c => {
1332+
let val = extracted[c.id];
1333+
if (val && typeof val === 'object') {
1334+
if (val.href && val.label) return `[${val.label}](${val.href})`;
1335+
if (val.label) return val.label;
1336+
return JSON.stringify(val);
1337+
}
1338+
return val !== undefined && val !== null ? String(val) : '—';
1339+
});
1340+
markdown += `| ${rowStr.map(c => String(c).replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |\n`;
1341+
});
1342+
} else {
1343+
const headers = ['TARGET', 'SEV', 'FINDING', 'MODULE'];
1344+
markdown += `| ${headers.join(' | ')} |\n`;
1345+
markdown += `| ${headers.map(() => '---').join(' | ')} |\n`;
1346+
exportedRows.forEach(r => {
1347+
const cols = [
1348+
r.target || r.host || '—',
1349+
r.severity || '',
1350+
r.finding || r.title || '—',
1351+
r.module || ''
1352+
];
1353+
markdown += `| ${cols.map(c => String(c).replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |\n`;
1354+
});
1355+
}
1356+
}
1357+
12821358
try {
1283-
await copyToClipboard(JSON.stringify(exportedRows, null, 2));
1284-
showToast('success', 'Exported', `Copied ${exportedRows.length} finding(s) as JSON`);
1359+
await copyToClipboard(markdown);
1360+
showToast('success', 'Exported', `Copied ${exportedRows.length} finding(s) as Markdown`);
12851361
} catch (e) {
12861362
showToast('error', 'Export failed', e.message || String(e));
12871363
}

0 commit comments

Comments
 (0)