Files
deceptifeed/internal/threatfeed/templates/live.html
Ryan Smith 7bc73f6695 threatfeed: move nav bar to dedicated template
This change moves the nav bar for the threat feed web interface to a dedicated template defined in `nav.html`. HTTP handlers and existing HTML templates are updated to utilize the new template.
2025-04-06 22:44:27 -07:00

199 lines
7.4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deceptifeed</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="full-width">
<header>
{{template "nav" .}}
</header>
<main>
<div id="ws-status"></div>
<table id="logs" class="live-logs"></table>
</main>
<script>
const maxLogs = 200;
const logs = document.getElementById('logs');
const wsStatus = document.getElementById('ws-status');
const wsURL = '/live-ws';
const initialReconnectDelay = 1000;
const maxReconnectDelay = 15000;
const maxReconnectAttempts = 100;
const timeFormat = new Intl.DateTimeFormat([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
let ws;
let reconnectAttempts = 0;
let isInitialBatchProcessed = false;
function handleWS() {
ws = new WebSocket(wsURL);
ws.onopen = () => {
reconnectAttempts = 0;
wsStatus.textContent = '';
wsStatus.className = '';
};
ws.onmessage = (event) => {
if (event.data === '---end---') {
isInitialBatchProcessed = true;
return;
}
handleMessage(event.data, isInitialBatchProcessed);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
reconnectWS();
};
}
function reconnectWS() {
if (reconnectAttempts > maxReconnectAttempts) {
wsStatus.textContent = 'Failed connecting to Deceptifeed';
wsStatus.className = 'red';
return;
}
const delay = Math.min(
maxReconnectDelay,
initialReconnectDelay * (2 ** reconnectAttempts),
);
const finalDelay = delay + (Math.random() * delay * 0.2);
setTimeout(() => {
reconnectAttempts++;
wsStatus.textContent = 'Connecting... ';
wsStatus.classList.add('gray', 'connecting');
handleWS();
}, finalDelay);
}
function handleMessage(data, shouldAnimate) {
try {
const d = JSON.parse(data);
const timeElement = document.createElement('td');
const initialTime = new Date(d.time);
timeElement.textContent = timeFormat.format(initialTime);
timeElement.className = 'timestamp';
const srcIPElement = document.createElement('td');
srcIPElement.textContent = d.source_ip;
srcIPElement.className = 'source-ip';
const eventDetails = document.createElement('td');
eventDetails.className = 'event-details';
switch (d.event_type) {
case 'http': {
const httpMethod = document.createElement('span');
httpMethod.textContent = `${d.event_details.method} `;
httpMethod.className = 'magenta';
const httpPath = document.createTextNode(d.event_details.path);
const tooltipContent = document.createElement('pre');
tooltipContent.className = 'tooltip-content';
let jsonDetails = JSON.stringify(d.event_details, null, 2);
// Remove outer braces.
jsonDetails = jsonDetails.slice(2, -1);
// Remove initial 2-space indent.
jsonDetails = jsonDetails.replace(/^ {2}/gm, '');
jsonDetails = jsonDetails.replace(/"([^"]+)":/g, '$1:');
tooltipContent.textContent = jsonDetails
eventDetails.classList.add('tooltip');
eventDetails.appendChild(httpMethod);
eventDetails.appendChild(httpPath);
eventDetails.appendChild(tooltipContent);
break;
}
case 'ssh': {
const usernameLabel = document.createElement('span');
usernameLabel.textContent = 'User: ';
usernameLabel.className = 'magenta';
const username = document.createTextNode(d.event_details.username);
const br = document.createElement('br');
const passwordLabel = document.createElement('span');
passwordLabel.textContent = 'Pass: ';
passwordLabel.className = 'magenta';
const password = document.createTextNode(d.event_details.password);
eventDetails.appendChild(usernameLabel);
eventDetails.appendChild(username);
eventDetails.appendChild(br);
eventDetails.appendChild(passwordLabel);
eventDetails.appendChild(password);
break;
}
case 'udp': {
// Remove '[unreliable]' string from IP found in UDP logs.
const spaceIndex = d.source_ip.indexOf(' ');
if (spaceIndex >= 0) {
srcIPElement.textContent = d.source_ip.slice(0, spaceIndex);
}
const udpLabel = document.createElement('span');
udpLabel.textContent = `[UDP:${d.server_port}] `;
udpLabel.className = 'magenta';
const udpData = document.createTextNode(d.event_details.data);
eventDetails.appendChild(udpLabel);
eventDetails.appendChild(udpData);
break;
}
default: {
const protoLabel = document.createElement('span');
protoLabel.textContent = `[${d.event_type.toUpperCase()}:${d.server_port}] `;
protoLabel.className = 'magenta';
const protoData = document.createTextNode(JSON.stringify(d.event_details, null, 1));
eventDetails.appendChild(protoLabel);
eventDetails.appendChild(protoData);
}
}
// Add log entry to table.
const logEntry = document.createElement('tr');
logEntry.appendChild(timeElement);
logEntry.appendChild(srcIPElement);
logEntry.appendChild(eventDetails);
if (shouldAnimate) {
logEntry.className = 'fade-in';
}
logs.insertBefore(logEntry, logs.firstChild);
if (logs.children.length > maxLogs) {
logs.removeChild(logs.lastChild);
}
}
catch (error) {
console.error('Failed to parse log data:', error);
}
}
handleWS();
</script>
</body>
</html>