Add TSCM report generation feature

- Add Generate Report button to TSCM sidebar (appears after sweep)
- Implement generateTscmReport() function that creates professional HTML report
- Report includes: executive summary, device tables by risk level,
  indicators, recommendations, and disclaimers
- Track sweep start/end times for duration calculation
- Fix script tag escaping in template literal to prevent parsing issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-14 20:21:55 +00:00
parent f326be77cd
commit 9f39f1cc2f
2 changed files with 523 additions and 0 deletions

View File

@@ -8700,6 +8700,8 @@
let tscmWifiDevices = [];
let tscmBtDevices = [];
let isRecordingBaseline = false;
let tscmSweepStartTime = null;
let tscmSweepEndTime = null;
async function refreshTscmDevices() {
// Fetch available interfaces for TSCM scanning
@@ -8836,9 +8838,12 @@
const data = await response.json();
if (data.status === 'success') {
isTscmRunning = true;
tscmSweepStartTime = new Date();
tscmSweepEndTime = null;
document.getElementById('startTscmBtn').style.display = 'none';
document.getElementById('stopTscmBtn').style.display = 'block';
document.getElementById('tscmProgress').style.display = 'flex';
document.getElementById('tscmReportBtn').style.display = 'none';
// Show warnings if any devices unavailable
if (data.warnings && data.warnings.length > 0) {
@@ -8903,6 +8908,7 @@
}
isTscmRunning = false;
tscmSweepEndTime = new Date();
if (tscmEventSource) {
tscmEventSource.close();
tscmEventSource = null;
@@ -8911,6 +8917,520 @@
document.getElementById('startTscmBtn').style.display = 'block';
document.getElementById('stopTscmBtn').style.display = 'none';
document.getElementById('tscmProgress').style.display = 'none';
// Show report button if we have any data
const hasData = tscmWifiDevices.length > 0 || tscmBtDevices.length > 0 || tscmRfSignals.length > 0;
document.getElementById('tscmReportBtn').style.display = hasData ? 'block' : 'none';
}
function generateTscmReport() {
// Calculate sweep duration
const startTime = tscmSweepStartTime || new Date();
const endTime = tscmSweepEndTime || new Date();
const durationMs = endTime - startTime;
const durationMin = Math.floor(durationMs / 60000);
const durationSec = Math.floor((durationMs % 60000) / 1000);
// Categorize devices by classification
const allDevices = [
...tscmWifiDevices.map(d => ({...d, protocol: 'WiFi'})),
...tscmBtDevices.map(d => ({...d, protocol: 'Bluetooth'})),
...tscmRfSignals.map(d => ({...d, protocol: 'RF'}))
];
const highInterest = allDevices.filter(d => d.classification === 'high_interest' || d.score >= 6);
const needsReview = allDevices.filter(d => d.classification === 'review' || (d.score >= 3 && d.score < 6));
const informational = allDevices.filter(d => d.classification === 'informational' || d.score < 3);
// Determine overall assessment
let assessment = 'LOW CONCERN';
let assessmentClass = 'informational';
if (highInterest.length > 0) {
assessment = `ELEVATED CONCERN: ${highInterest.length} high-interest item(s) detected requiring immediate attention`;
assessmentClass = 'high-interest';
} else if (needsReview.length > 0) {
assessment = `MODERATE CONCERN: ${needsReview.length} item(s) requiring further review`;
assessmentClass = 'needs-review';
} else {
assessment = 'LOW CONCERN: No significant threats detected. Environment appears normal.';
}
// Helper function to render device row
const renderDevice = (device) => {
const scoreClass = device.score >= 6 ? 'high' : (device.score >= 3 ? 'medium' : 'low');
const indicators = (device.indicators || []).map(i =>
`<span class="indicator">${i.type}: ${i.desc}</span>`
).join('');
const reasons = (device.reasons || []).map(r => `<li>${r}</li>`).join('');
let identifier = device.bssid || device.mac || (device.frequency ? `${device.frequency} MHz` : 'Unknown');
let name = device.essid || device.name || device.band || 'Unknown';
return `
<tr class="device-row ${device.classification || ''}">
<td><span class="protocol-badge ${device.protocol.toLowerCase()}">${device.protocol}</span></td>
<td><strong>${name}</strong><br><small class="identifier">${identifier}</small></td>
<td><span class="score-badge ${scoreClass}">${device.score || 0}</span></td>
<td>${device.classification || 'unknown'}</td>
<td>${device.signal || device.rssi || device.power || 'N/A'} dBm</td>
<td>
${indicators ? `<div class="indicators">${indicators}</div>` : ''}
${reasons ? `<ul class="reasons">${reasons}</ul>` : ''}
</td>
<td>${device.recommended_action || 'monitor'}</td>
</tr>
`;
};
// Generate HTML report
const reportHtml = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TSCM Sweep Report - ${startTime.toLocaleDateString()}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
background: #1a1a2e;
color: #e8eaed;
padding: 40px;
line-height: 1.6;
}
.report-container {
max-width: 1200px;
margin: 0 auto;
background: #0f1218;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.report-header {
background: linear-gradient(135deg, #1a1a2e 0%, #0f1218 100%);
padding: 40px;
border-bottom: 1px solid #2a2a4a;
}
.report-title {
font-size: 28px;
font-weight: 700;
color: #4a9eff;
margin-bottom: 8px;
}
.report-subtitle {
font-size: 14px;
color: #9ca3af;
letter-spacing: 1px;
text-transform: uppercase;
}
.report-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 30px;
padding: 20px;
background: rgba(0,0,0,0.3);
border-radius: 8px;
}
.meta-item {
text-align: center;
}
.meta-value {
font-size: 24px;
font-weight: 700;
color: #fff;
}
.meta-label {
font-size: 11px;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 1px;
}
.section {
padding: 30px 40px;
border-bottom: 1px solid #2a2a4a;
}
.section:last-child {
border-bottom: none;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #4a9eff;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.assessment {
padding: 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
}
.assessment.high-interest {
background: rgba(255, 51, 51, 0.15);
border: 1px solid #ff3333;
color: #ff6666;
}
.assessment.needs-review {
background: rgba(255, 204, 0, 0.15);
border: 1px solid #ffcc00;
color: #ffdd44;
}
.assessment.informational {
background: rgba(0, 204, 0, 0.15);
border: 1px solid #00cc00;
color: #44dd44;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.summary-card {
background: rgba(0,0,0,0.3);
padding: 20px;
border-radius: 8px;
text-align: center;
border: 1px solid #2a2a4a;
}
.summary-card.high-interest { border-color: #ff3333; }
.summary-card.needs-review { border-color: #ffcc00; }
.summary-card.informational { border-color: #00cc00; }
.summary-card .count {
font-size: 32px;
font-weight: 700;
}
.summary-card.high-interest .count { color: #ff3333; }
.summary-card.needs-review .count { color: #ffcc00; }
.summary-card.informational .count { color: #00cc00; }
.summary-card .label {
font-size: 11px;
color: #6b7280;
text-transform: uppercase;
margin-top: 4px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
text-align: left;
padding: 12px;
background: rgba(0,0,0,0.4);
color: #9ca3af;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid #2a2a4a;
}
td {
padding: 12px;
border-bottom: 1px solid #2a2a4a;
vertical-align: top;
}
.device-row.high_interest { background: rgba(255, 51, 51, 0.08); }
.device-row.review { background: rgba(255, 204, 0, 0.08); }
.protocol-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.protocol-badge.wifi { background: #4a9eff; color: #000; }
.protocol-badge.bluetooth { background: #8b5cf6; color: #fff; }
.protocol-badge.rf { background: #f59e0b; color: #000; }
.score-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-weight: 600;
font-size: 12px;
}
.score-badge.high { background: rgba(255,51,51,0.2); color: #ff3333; }
.score-badge.medium { background: rgba(255,204,0,0.2); color: #ffcc00; }
.score-badge.low { background: rgba(0,204,0,0.2); color: #00cc00; }
.identifier {
color: #6b7280;
font-family: monospace;
font-size: 11px;
}
.indicators {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 6px;
}
.indicator {
display: inline-block;
padding: 2px 6px;
background: rgba(255,153,51,0.2);
color: #ff9933;
border-radius: 3px;
font-size: 10px;
}
.reasons {
margin: 0;
padding-left: 16px;
font-size: 11px;
color: #9ca3af;
}
.reasons li {
margin-bottom: 2px;
}
.category-section {
margin-bottom: 30px;
}
.category-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 6px;
}
.category-title.high-interest { background: rgba(255,51,51,0.15); color: #ff6666; }
.category-title.needs-review { background: rgba(255,204,0,0.15); color: #ffdd44; }
.category-title.informational { background: rgba(0,204,0,0.15); color: #44dd44; }
.empty-state {
text-align: center;
padding: 40px;
color: #6b7280;
}
.disclaimer {
padding: 20px;
background: rgba(74, 158, 255, 0.1);
border-radius: 8px;
font-size: 12px;
color: #9ca3af;
}
.disclaimer h4 {
color: #4a9eff;
margin-bottom: 10px;
font-size: 13px;
}
.recommendations {
margin-top: 20px;
}
.recommendations ul {
padding-left: 20px;
}
.recommendations li {
margin-bottom: 8px;
}
.print-btn {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: #4a9eff;
color: #000;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
z-index: 1000;
}
.print-btn:hover {
background: #6bb3ff;
}
@media print {
body { background: #fff; color: #000; padding: 20px; }
.report-container { box-shadow: none; }
.report-header { background: #f8f9fa; }
.report-title { color: #1a1a2e; }
.section { border-color: #ddd; }
.section-title { color: #1a1a2e; }
th { background: #f0f0f0; color: #333; }
td { border-color: #ddd; }
.print-btn { display: none; }
.device-row.high_interest { background: rgba(255, 51, 51, 0.1); }
.device-row.review { background: rgba(255, 204, 0, 0.1); }
}
</style>
</head>
<body>
<button class="print-btn" onclick="window.print()">🖨️ Print Report</button>
<div class="report-container">
<div class="report-header">
<div class="report-title">TSCM Sweep Report</div>
<div class="report-subtitle">Technical Surveillance Counter-Measures Analysis</div>
<div class="report-meta">
<div class="meta-item">
<div class="meta-value">${startTime.toLocaleDateString()}</div>
<div class="meta-label">Date</div>
</div>
<div class="meta-item">
<div class="meta-value">${startTime.toLocaleTimeString()} - ${endTime.toLocaleTimeString()}</div>
<div class="meta-label">Time Range</div>
</div>
<div class="meta-item">
<div class="meta-value">${durationMin}m ${durationSec}s</div>
<div class="meta-label">Duration</div>
</div>
<div class="meta-item">
<div class="meta-value">${allDevices.length}</div>
<div class="meta-label">Total Devices</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">📊 Executive Summary</div>
<div class="summary-grid">
<div class="summary-card high-interest">
<div class="count">${highInterest.length}</div>
<div class="label">High Interest</div>
</div>
<div class="summary-card needs-review">
<div class="count">${needsReview.length}</div>
<div class="label">Needs Review</div>
</div>
<div class="summary-card informational">
<div class="count">${informational.length}</div>
<div class="label">Informational</div>
</div>
<div class="summary-card">
<div class="count" style="color: #4a9eff;">${tscmWifiDevices.length}/${tscmBtDevices.length}/${tscmRfSignals.length}</div>
<div class="label">WiFi/BT/RF</div>
</div>
</div>
<div class="assessment ${assessmentClass}">
<strong>Assessment:</strong> ${assessment}
</div>
</div>
${highInterest.length > 0 ? `
<div class="section">
<div class="section-title">🔴 High Interest Items</div>
<div class="category-section">
<table>
<thead>
<tr>
<th>Type</th>
<th>Device</th>
<th>Score</th>
<th>Class</th>
<th>Signal</th>
<th>Indicators / Reasons</th>
<th>Action</th>
</tr>
</thead>
<tbody>
${highInterest.map(renderDevice).join('')}
</tbody>
</table>
</div>
</div>
` : ''}
${needsReview.length > 0 ? `
<div class="section">
<div class="section-title">🟡 Items Requiring Review</div>
<div class="category-section">
<table>
<thead>
<tr>
<th>Type</th>
<th>Device</th>
<th>Score</th>
<th>Class</th>
<th>Signal</th>
<th>Indicators / Reasons</th>
<th>Action</th>
</tr>
</thead>
<tbody>
${needsReview.map(renderDevice).join('')}
</tbody>
</table>
</div>
</div>
` : ''}
${informational.length > 0 ? `
<div class="section">
<div class="section-title">🟢 Informational Items</div>
<div class="category-section">
<table>
<thead>
<tr>
<th>Type</th>
<th>Device</th>
<th>Score</th>
<th>Class</th>
<th>Signal</th>
<th>Indicators / Reasons</th>
<th>Action</th>
</tr>
</thead>
<tbody>
${informational.map(renderDevice).join('')}
</tbody>
</table>
</div>
</div>
` : ''}
${allDevices.length === 0 ? `
<div class="section">
<div class="empty-state">
<p>No devices were detected during this sweep.</p>
</div>
</div>
` : ''}
<div class="section">
<div class="section-title">📋 Recommendations</div>
<div class="recommendations">
<ul>
${highInterest.length > 0 ? `
<li><strong>Immediate Action Required:</strong> ${highInterest.length} high-interest item(s) detected. These devices exhibit characteristics commonly associated with surveillance equipment and should be physically located and investigated.</li>
` : ''}
${needsReview.length > 0 ? `
<li><strong>Further Investigation Recommended:</strong> ${needsReview.length} item(s) require additional review to determine their purpose and legitimacy.</li>
` : ''}
${allDevices.filter(d => d.is_new).length > 0 ? `
<li><strong>New Devices Detected:</strong> ${allDevices.filter(d => d.is_new).length} device(s) were not present in the baseline. Verify these are authorized.</li>
` : ''}
<li><strong>Regular Monitoring:</strong> Consider establishing a baseline of normal wireless activity and conducting periodic sweeps to detect changes.</li>
<li><strong>Physical Inspection:</strong> For any high-interest items, conduct a thorough physical inspection of the area to locate potential surveillance devices.</li>
</ul>
</div>
</div>
<div class="section">
<div class="section-title">⚠️ Disclaimer</div>
<div class="disclaimer">
<h4>Important Notice</h4>
<p>This report is generated by automated wireless spectrum analysis software. The findings presented are <strong>indicators only</strong> and do not constitute confirmation of surveillance activity. Many legitimate devices may trigger alerts due to their wireless characteristics.</p>
<p style="margin-top: 10px;">Professional TSCM services involve specialized equipment and expertise beyond wireless spectrum analysis, including: non-linear junction detection, thermal imaging, physical inspection, and RF spectrum analysis with calibrated equipment.</p>
<p style="margin-top: 10px;"><strong>No content was intercepted or decoded during this analysis.</strong> This tool only detects the presence and characteristics of wireless transmissions.</p>
</div>
</div>
</div>
<scr` + `ipt>
// Auto-focus print on load (optional)
// window.onload = () => window.print();
</scr` + `ipt>
</body>
</html>
`;
// Open report in new window
const reportWindow = window.open('', '_blank');
reportWindow.document.write(reportHtml);
reportWindow.document.close();
}
function startTscmStream() {

View File

@@ -75,6 +75,9 @@
<button class="stop-btn" id="stopTscmBtn" onclick="stopTscmSweep()" style="display: none;">
Stop Sweep
</button>
<button class="preset-btn" id="tscmReportBtn" onclick="generateTscmReport()" style="display: none; width: 100%; margin-top: 8px; background: var(--accent-cyan); color: #000; font-weight: 600;">
📄 Generate Report
</button>
<!-- Futuristic Scanner Progress -->
<div id="tscmProgress" class="tscm-scanner-progress" style="display: none;">