mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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:
@@ -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() {
|
||||
|
||||
@@ -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;">
|
||||
|
||||
Reference in New Issue
Block a user