|
893 | 893 | <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)"> |
894 | 894 | <div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"> |
895 | 895 | <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> |
897 | 897 | </div> |
898 | 898 | <div id="recon-quick-chips" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"></div> |
899 | 899 | <div style="margin-left:auto;display:flex;align-items:center;gap:6px;flex-wrap:wrap"> |
|
1279 | 1279 | exportJsonBtn.addEventListener('click', async () => { |
1280 | 1280 | const exportedRows = allRows.filter(r => rowMatch(r) && !HIDDEN_KINDS.has(r.kind)); |
1281 | 1281 | 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 | + |
1282 | 1358 | 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`); |
1285 | 1361 | } catch (e) { |
1286 | 1362 | showToast('error', 'Export failed', e.message || String(e)); |
1287 | 1363 | } |
|
0 commit comments