linux-firewall-log-analyser/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux Firewall Log Analyser</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Helvetica, Arial, sans-serif;
line-height: 1.5;
margin: 0;
padding: 20px;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
margin: 0 0 20px 0;
font-size: 24px;
font-weight: 600;
}
h2 {
font-size: 18px;
margin: 30px 0 15px 0;
font-weight: 600;
border-bottom: 1px solid #ddd;
padding-bottom: 8px;
}
.input-section {
margin-bottom: 30px;
}
textarea {
width: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: monospace;
font-size: 16px;
resize: vertical;
}
input[type="file"] {
margin: 10px 0;
font-size: 16px;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
margin-top: 10px;
}
button:hover {
background: #0052a3;
}
button.secondary {
background: #666;
}
button.secondary:hover {
background: #555;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: #f9f9f9;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #0066cc;
}
.stat-card.drop {
border-left-color: #dc3545;
}
.stat-card.fwd {
border-left-color: #ffc107;
}
.stat-card.in {
border-left-color: #28a745;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 28px;
font-weight: 600;
margin-top: 5px;
}
.filters {
background: #f9f9f9;
padding: 15px;
border-radius: 4px;
margin-bottom: 30px;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.filters label {
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.filters input[type="checkbox"] {
margin: 0;
}
.filters input[type="text"] {
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
.charts-section {
margin-bottom: 30px;
}
.chart-container {
margin-bottom: 30px;
}
.chart-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #555;
}
svg {
width: 100%;
height: auto;
background: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.bar {
fill: #0066cc;
}
.bar.drop {
fill: #dc3545;
}
.bar.fwd {
fill: #ffc107;
}
.bar.in {
fill: #28a745;
}
.bar:hover {
opacity: 0.8;
}
.axis {
stroke: #999;
stroke-width: 1;
}
.axis-text {
font-size: 11px;
fill: #666;
}
.grid-line {
stroke: #e0e0e0;
stroke-width: 1;
}
.tables-section {
margin-bottom: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-size: 14px;
}
th, td {
text-align: left;
padding: 10px;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #f5f5f5;
font-weight: 600;
cursor: pointer;
user-select: none;
}
th:hover {
background: #eeeeee;
}
tr:hover {
background: #f9f9f9;
}
.raw-log {
background: #f5f5f5;
padding: 15px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.log-line {
padding: 2px 0;
border-bottom: 1px solid #e8e8e8;
}
.log-line.drop {
background: rgba(220, 53, 69, 0.1);
border-left: 3px solid #dc3545;
padding-left: 5px;
}
.log-line.fwd {
background: rgba(255, 193, 7, 0.1);
border-left: 3px solid #ffc107;
padding-left: 5px;
}
.log-line.in {
background: rgba(40, 167, 69, 0.1);
border-left: 3px solid #28a745;
padding-left: 5px;
}
.port-scan-alert {
background: #fff3cd;
border: 1px solid #ffc107;
color: #856404;
padding: 10px 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Linux Firewall Log Analyser</h1>
<div class="input-section">
<textarea id="logInput" placeholder="Paste firewall logs here... [3065201.841366] IN IN=eth1 OUT= MAC=... SRC=... DST=..."></textarea>
<div>
<input type="file" id="fileInput" accept=".log,.txt">
</div>
<button id="analyzeBtn">Analyse Logs</button>
<button id="exportBtn" class="secondary hidden">Export Report</button>
<button id="clearBtn" class="secondary">Clear</button>
</div>
<div id="resultsSection" class="hidden">
<div id="portScanAlerts"></div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Total Events</div>
<div class="stat-value" id="totalEvents">0</div>
</div>
<div class="stat-card drop">
<div class="stat-label">DROP</div>
<div class="stat-value" id="dropCount">0</div>
</div>
<div class="stat-card fwd">
<div class="stat-label">FWD</div>
<div class="stat-value" id="fwdCount">0</div>
</div>
<div class="stat-card in">
<div class="stat-label">IN</div>
<div class="stat-value" id="inCount">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Unique Sources</div>
<div class="stat-value" id="uniqueSources">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Time Range</div>
<div class="stat-value" id="timeRange" style="font-size: 18px;">-</div>
</div>
</div>
<div class="filters">
<label><input type="checkbox" id="filterDrop" checked> DROP</label>
<label><input type="checkbox" id="filterFwd" checked> FWD</label>
<label><input type="checkbox" id="filterIn" checked> IN</label>
<label><input type="checkbox" id="filterOut" checked> OUT</label>
<input type="text" id="ipFilter" placeholder="Filter by IP...">
<input type="text" id="portFilter" placeholder="Filter by port...">
<button id="applyFilters" style="margin-top: 0;">Apply Filters</button>
</div>
<div class="charts-section">
<div class="chart-container">
<div class="chart-title">Event Timeline (by kernel uptime)</div>
<svg id="timelineChart" height="200"></svg>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div class="chart-container">
<div class="chart-title">Top 10 Source IPs</div>
<svg id="srcChart" height="300"></svg>
</div>
<div class="chart-container">
<div class="chart-title">Top 10 Destination Ports</div>
<svg id="dstPortChart" height="300"></svg>
</div>
</div>
</div>
<div class="tables-section">
<h2>Top Source IPs</h2>
<table id="srcTable">
<thead>
<tr>
<th data-sort="ip">Source IP</th>
<th data-sort="count">Events</th>
<th data-sort="drop">DROP</th>
<th data-sort="ports">Unique Ports</th>
</tr>
</thead>
<tbody id="srcTableBody"></tbody>
</table>
<h2>Top Destination Ports</h2>
<table id="portTable">
<thead>
<tr>
<th data-sort="port">Port</th>
<th data-sort="count">Events</th>
<th data-sort="drop">DROP</th>
<th data-sort="syn">SYN</th>
</tr>
</thead>
<tbody id="portTableBody"></tbody>
</table>
<h2>Raw Log Entries</h2>
<div class="raw-log" id="rawLog"></div>
</div>
</div>
</div>
<script type="module">
let parsedData = [];
let filteredData = [];
let portScanThreshold = 10;
let timeWindow = 60;
const logInput = document.getElementById('logInput');
const fileInput = document.getElementById('fileInput');
const analyzeBtn = document.getElementById('analyzeBtn');
const exportBtn = document.getElementById('exportBtn');
const clearBtn = document.getElementById('clearBtn');
const resultsSection = document.getElementById('resultsSection');
const portScanAlerts = document.getElementById('portScanAlerts');
const filterDrop = document.getElementById('filterDrop');
const filterFwd = document.getElementById('filterFwd');
const filterIn = document.getElementById('filterIn');
const filterOut = document.getElementById('filterOut');
const ipFilter = document.getElementById('ipFilter');
const portFilter = document.getElementById('portFilter');
const applyFilters = document.getElementById('applyFilters');
function parseLogLine(line) {
if (!line.trim()) return null;
const match = line.match(/^\[([\d.]+)\]\s+(\w+)(?:\s+(INVALID))?\s+(.*)$/);
if (!match) return null;
const timestamp = parseFloat(match[1]);
const action = match[2];
const invalid = !!match[3];
const rest = match[4];
const entry = {
timestamp,
action,
invalid,
raw: line
};
const keyValueRegex = /(\w+)=([^\s]+)/g;
let kv;
while ((kv = keyValueRegex.exec(rest)) !== null) {
const key = kv[1];
const value = kv[2];
if (key === 'SRC' || key === 'DST') {
entry[key.toLowerCase()] = value;
} else if (key === 'SPT' || key === 'DPT') {
entry[key.toLowerCase()] = parseInt(value, 10);
} else if (key === 'LEN' || key === 'TTL' || key === 'HOPLIMIT' || key === 'WINDOW') {
entry[key.toLowerCase()] = parseInt(value, 10);
} else if (key === 'IN' || key === 'OUT') {
entry[`iface_${key.toLowerCase()}`] = value;
} else if (key === 'MAC') {
entry.mac = value;
} else if (key === 'PROTO') {
entry.proto = value;
} else if (key === 'ID' || key === 'FLOWLBL') {
entry.id = value;
}
}
const flagMatch = rest.match(/(SYN|ACK|FIN|PSH|RST|URG)(?:\s+(SYN|ACK|FIN|PSH|RST|URG))*(?:\s+(SYN|ACK|FIN|PSH|RST|URG))*(?:\s+(SYN|ACK|FIN|PSH|RST|URG))*(?:\s+(SYN|ACK|FIN|PSH|RST|URG))*(?:\s+(SYN|ACK|FIN|PSH|RST|URG))*/);
if (flagMatch) {
entry.flags = rest.match(/\b(SYN|ACK|FIN|PSH|RST|URG)\b/g) || [];
} else {
entry.flags = [];
}
return entry;
}
function detectPortScans(data) {
const scans = [];
const bySource = {};
data.forEach(entry => {
if (!entry.src || !entry.dpt) return;
if (!bySource[entry.src]) {
bySource[entry.src] = [];
}
bySource[entry.src].push(entry);
});
Object.keys(bySource).forEach(src => {
const entries = bySource[src].sort((a, b) => a.timestamp - b.timestamp);
const windows = [];
let currentWindow = [];
entries.forEach(entry => {
if (currentWindow.length === 0) {
currentWindow.push(entry);
} else {
const timeDiff = entry.timestamp - currentWindow[0].timestamp;
if (timeDiff <= timeWindow) {
currentWindow.push(entry);
} else {
if (currentWindow.length >= portScanThreshold) {
const uniquePorts = new Set(currentWindow.map(e => e.dpt)).size;
if (uniquePorts >= portScanThreshold) {
windows.push({
src,
count: currentWindow.length,
ports: uniquePorts,
start: currentWindow[0].timestamp,
end: currentWindow[currentWindow.length - 1].timestamp
});
}
}
currentWindow = [entry];
}
}
});
if (currentWindow.length >= portScanThreshold) {
const uniquePorts = new Set(currentWindow.map(e => e.dpt)).size;
if (uniquePorts >= portScanThreshold) {
windows.push({
src,
count: currentWindow.length,
ports: uniquePorts,
start: currentWindow[0].timestamp,
end: currentWindow[currentWindow.length - 1].timestamp
});
}
}
scans.push(...windows);
});
return scans;
}
function analyzeData() {
const text = logInput.value;
const lines = text.split('\n');
parsedData = lines.map(parseLogLine).filter(x => x !== null);
filteredData = [...parsedData];
updateStats();
detectAndShowScans();
updateCharts();
updateTables();
updateRawLog();
resultsSection.classList.remove('hidden');
exportBtn.classList.remove('hidden');
}
function updateStats() {
const total = filteredData.length;
const drops = filteredData.filter(e => e.action === 'DROP').length;
const fwds = filteredData.filter(e => e.action === 'FWD').length;
const ins = filteredData.filter(e => e.action === 'IN').length;
const outs = filteredData.filter(e => e.action === 'OUT').length;
const uniqueSrcs = new Set(filteredData.map(e => e.src).filter(Boolean)).size;
let timeRange = '-';
if (filteredData.length > 0) {
const times = filteredData.map(e => e.timestamp);
const min = Math.min(...times);
const max = Math.max(...times);
timeRange = `${(max - min).toFixed(1)}s`;
}
document.getElementById('totalEvents').textContent = total;
document.getElementById('dropCount').textContent = drops;
document.getElementById('fwdCount').textContent = fwds + outs;
document.getElementById('inCount').textContent = ins;
document.getElementById('uniqueSources').textContent = uniqueSrcs;
document.getElementById('timeRange').textContent = timeRange;
}
function detectAndShowScans() {
const scans = detectPortScans(filteredData);
portScanAlerts.innerHTML = '';
if (scans.length > 0) {
scans.forEach(scan => {
const div = document.createElement('div');
div.className = 'port-scan-alert';
div.textContent = `Potential port scan detected from ${scan.src}: ${scan.count} events targeting ${scan.ports} unique ports between [${scan.start.toFixed(3)}] and [${scan.end.toFixed(3)}]`;
portScanAlerts.appendChild(div);
});
}
}
function getActionFilteredData() {
return filteredData.filter(e => {
if (e.action === 'DROP' && !filterDrop.checked) return false;
if (e.action === 'FWD' && !filterFwd.checked) return false;
if (e.action === 'IN' && !filterIn.checked) return false;
if (e.action === 'OUT' && !filterOut.checked) return false;
const ip = ipFilter.value.trim();
if (ip && e.src && !e.src.includes(ip) && e.dst && !e.dst.includes(ip)) return false;
const port = portFilter.value.trim();
if (port && e.spt !== parseInt(port) && e.dpt !== parseInt(port)) return false;
return true;
});
}
function updateCharts() {
const data = getActionFilteredData();
drawTimeline(data);
drawTopSources(data);
drawTopPorts(data);
}
function drawTimeline(data) {
const svg = document.getElementById('timelineChart');
svg.innerHTML = '';
if (data.length === 0) return;
const width = svg.clientWidth || 800;
const height = 200;
const padding = { top: 20, right: 20, bottom: 40, left: 50 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const times = data.map(e => e.timestamp);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const timeRange = maxTime - minTime || 1;
const bucketCount = 20;
const buckets = Array(bucketCount).fill(0).map(() => ({ drop: 0, fwd: 0, in: 0, out: 0 }));
data.forEach(e => {
const bucketIdx = Math.min(Math.floor((e.timestamp - minTime) / timeRange * bucketCount), bucketCount - 1);
if (e.action === 'DROP') buckets[bucketIdx].drop++;
else if (e.action === 'FWD') buckets[bucketIdx].fwd++;
else if (e.action === 'IN') buckets[bucketIdx].in++;
else if (e.action === 'OUT') buckets[bucketIdx].out++;
});
const maxCount = Math.max(...buckets.map(b => b.drop + b.fwd + b.in + b.out)) || 1;
const barWidth = chartWidth / bucketCount;
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
for (let i = 0; i <= 5; i++) {
const y = padding.top + chartHeight - (i / 5) * chartHeight;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', padding.left);
line.setAttribute('x2', width - padding.right);
line.setAttribute('y1', y);
line.setAttribute('y2', y);
line.setAttribute('class', 'grid-line');
svg.appendChild(line);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', padding.left - 10);
text.setAttribute('y', y + 4);
text.setAttribute('text-anchor', 'end');
text.setAttribute('class', 'axis-text');
text.textContent = Math.round((i / 5) * maxCount);
svg.appendChild(text);
}
buckets.forEach((bucket, i) => {
const x = padding.left + i * barWidth;
let y = padding.top + chartHeight;
const barH = (val) => (val / maxCount) * chartHeight;
['in', 'fwd', 'drop', 'out'].forEach(type => {
const h = barH(bucket[type]);
if (h > 0) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', x + 1);
rect.setAttribute('y', y - h);
rect.setAttribute('width', barWidth - 2);
rect.setAttribute('height', h);
rect.setAttribute('class', `bar ${type}`);
svg.appendChild(rect);
y -= h;
}
});
});
const xAxis = document.createElementNS('http://www.w3.org/2000/svg', 'line');
xAxis.setAttribute('x1', padding.left);
xAxis.setAttribute('x2', width - padding.right);
xAxis.setAttribute('y1', height - padding.bottom);
xAxis.setAttribute('y2', height - padding.bottom);
xAxis.setAttribute('class', 'axis');
svg.appendChild(xAxis);
const startText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
startText.setAttribute('x', padding.left);
startText.setAttribute('y', height - 10);
startText.setAttribute('class', 'axis-text');
startText.textContent = minTime.toFixed(1);
svg.appendChild(startText);
const endText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
endText.setAttribute('x', width - padding.right);
endText.setAttribute('y', height - 10);
endText.setAttribute('text-anchor', 'end');
endText.setAttribute('class', 'axis-text');
endText.textContent = maxTime.toFixed(1);
svg.appendChild(endText);
}
function drawTopSources(data) {
const svg = document.getElementById('srcChart');
svg.innerHTML = '';
const counts = {};
data.forEach(e => {
if (!e.src) return;
counts[e.src] = (counts[e.src] || 0) + 1;
});
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
drawBarChart(svg, sorted, 'ip');
}
function drawTopPorts(data) {
const svg = document.getElementById('dstPortChart');
svg.innerHTML = '';
const counts = {};
data.forEach(e => {
if (!e.dpt) return;
counts[e.dpt] = (counts[e.dpt] || 0) + 1;
});
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
drawBarChart(svg, sorted, 'port');
}
function drawBarChart(svg, data, type) {
if (data.length === 0) return;
const width = svg.clientWidth || 400;
const height = 300;
const padding = { top: 20, right: 20, bottom: 40, left: 120 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const maxValue = data[0][1];
const barHeight = chartHeight / data.length;
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
data.forEach(([label, value], i) => {
const y = padding.top + i * barHeight;
const barWidth = (value / maxValue) * chartWidth;
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', padding.left);
rect.setAttribute('y', y + 2);
rect.setAttribute('width', barWidth);
rect.setAttribute('height', barHeight - 4);
rect.setAttribute('class', 'bar');
svg.appendChild(rect);
const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
labelText.setAttribute('x', padding.left - 10);
labelText.setAttribute('y', y + barHeight / 2 + 4);
labelText.setAttribute('text-anchor', 'end');
labelText.setAttribute('class', 'axis-text');
labelText.textContent = label;
svg.appendChild(labelText);
const valText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
valText.setAttribute('x', padding.left + barWidth + 5);
valText.setAttribute('y', y + barHeight / 2 + 4);
valText.setAttribute('class', 'axis-text');
valText.textContent = value;
svg.appendChild(valText);
});
}
function updateTables() {
const data = getActionFilteredData();
updateSrcTable(data);
updatePortTable(data);
}
function updateSrcTable(data) {
const tbody = document.getElementById('srcTableBody');
tbody.innerHTML = '';
const stats = {};
data.forEach(e => {
if (!e.src) return;
if (!stats[e.src]) {
stats[e.src] = { count: 0, drop: 0, ports: new Set() };
}
stats[e.src].count++;
if (e.action === 'DROP') stats[e.src].drop++;
if (e.dpt) stats[e.src].ports.add(e.dpt);
});
const rows = Object.entries(stats)
.map(([ip, s]) => ({ ip, count: s.count, drop: s.drop, ports: s.ports.size }))
.sort((a, b) => b.count - a.count)
.slice(0, 50);
rows.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.ip}</td>
<td>${row.count}</td>
<td>${row.drop}</td>
<td>${row.ports}</td>
`;
tbody.appendChild(tr);
});
}
function updatePortTable(data) {
const tbody = document.getElementById('portTableBody');
tbody.innerHTML = '';
const stats = {};
data.forEach(e => {
if (!e.dpt) return;
if (!stats[e.dpt]) {
stats[e.dpt] = { count: 0, drop: 0, syn: 0 };
}
stats[e.dpt].count++;
if (e.action === 'DROP') stats[e.dpt].drop++;
if (e.flags && e.flags.includes('SYN')) stats[e.dpt].syn++;
});
const rows = Object.entries(stats)
.map(([port, s]) => ({ port, count: s.count, drop: s.drop, syn: s.syn }))
.sort((a, b) => b.count - a.count)
.slice(0, 50);
rows.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.port}</td>
<td>${row.count}</td>
<td>${row.drop}</td>
<td>${row.syn}</td>
`;
tbody.appendChild(tr);
});
}
function updateRawLog() {
const container = document.getElementById('rawLog');
container.innerHTML = '';
const data = getActionFilteredData().slice(0, 500);
data.forEach(e => {
const div = document.createElement('div');
div.className = `log-line ${e.action.toLowerCase()}`;
div.textContent = e.raw;
container.appendChild(div);
});
if (filteredData.length > 500) {
const div = document.createElement('div');
div.style.padding = '10px';
div.style.color = '#666';
div.textContent = `... and ${filteredData.length - 500} more entries`;
container.appendChild(div);
}
}
function handleFile(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
logInput.value = event.target.result;
analyzeData();
};
reader.readAsText(file);
}
function applyCurrentFilters() {
updateStats();
detectAndShowScans();
updateCharts();
updateTables();
updateRawLog();
}
function exportReport() {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Linux Firewall Log Analyser Report</title>
<style>
* { box-sizing: border-box; }
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.5; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { margin: 0 0 20px 0; font-size: 24px; }
h2 { font-size: 18px; margin: 30px 0 15px 0; border-bottom: 1px solid #ddd; padding-bottom: 8px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }
.stat-card { background: #f9f9f9; padding: 15px; border-radius: 4px; border-left: 4px solid #0066cc; }
.stat-card.drop { border-left-color: #dc3545; }
.stat-card.fwd { border-left-color: #ffc107; }
.stat-card.in { border-left-color: #28a745; }
.stat-label { font-size: 12px; color: #666; text-transform: uppercase; }
.stat-value { font-size: 28px; font-weight: 600; margin-top: 5px; }
.port-scan-alert { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px 15px; border-radius: 4px; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: 14px; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #e0e0e0; }
th { background: #f5f5f5; font-weight: 600; }
.raw-log { background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 13px; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
.log-line { padding: 2px 0; border-bottom: 1px solid #e8e8e8; }
.log-line.drop { background: rgba(220, 53, 69, 0.1); border-left: 3px solid #dc3545; padding-left: 5px; }
.log-line.fwd { background: rgba(255, 193, 7, 0.1); border-left: 3px solid #ffc107; padding-left: 5px; }
.log-line.in { background: rgba(40, 167, 69, 0.1); border-left: 3px solid #28a745; padding-left: 5px; }
svg { width: 100%; height: auto; background: #fafafa; border: 1px solid #e0e0e0; border-radius: 4px; margin-bottom: 20px; }
.bar { fill: #0066cc; }
.bar.drop { fill: #dc3545; }
.bar.fwd { fill: #ffc107; }
.bar.in { fill: #28a745; }
.axis { stroke: #999; stroke-width: 1; }
.axis-text { font-size: 11px; fill: #666; }
.grid-line { stroke: #e0e0e0; stroke-width: 1; }
</style>
</head>
<body>
<div class="container">
<h1>Linux Firewall Log Analyser Report</h1>
<p>Generated on ${new Date().toLocaleString()}</p>
<div id="content"></div>
</div>
<script>
const data = ${JSON.stringify(filteredData)};
const scans = ${JSON.stringify(detectPortScans(filteredData))};
document.getElementById('content').innerHTML = \`
\${scans.map(s => \`<div class="port-scan-alert">Potential port scan detected from \${s.src}: \${s.count} events targeting \${s.ports} unique ports</div>\`).join('')}
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">Total Events</div><div class="stat-value">\${data.length}</div></div>
<div class="stat-card drop"><div class="stat-label">DROP</div><div class="stat-value">\${data.filter(e => e.action === 'DROP').length}</div></div>
<div class="stat-card fwd"><div class="stat-label">FWD</div><div class="stat-value">\${data.filter(e => e.action === 'FWD').length}</div></div>
<div class="stat-card in"><div class="stat-label">IN</div><div class="stat-value">\${data.filter(e => e.action === 'IN').length}</div></div>
</div>
<h2>Top Source IPs</h2>
<table>
<thead><tr><th>Source IP</th><th>Events</th><th>DROP</th></tr></thead>
<tbody>
\${Object.entries(data.filter(e => e.src).reduce((a, e) => { a[e.src] = (a[e.src] || 0) + 1; return a; }, {})).sort((a,b) => b[1]-a[1]).slice(0,20).map(([ip, c]) => \`<tr><td>\${ip}</td><td>\${c}</td><td>\${data.filter(e => e.src === ip && e.action === 'DROP').length}</td></tr>\`).join('')}
</tbody>
</table>
<h2>Raw Log Entries</h2>
<div class="raw-log">
\${data.slice(0, 1000).map(e => \`<div class="log-line \${e.action.toLowerCase()}">\${e.raw}</div>\`).join('')}
\${data.length > 1000 ? '<div>... and ' + (data.length - 1000) + ' more entries</div>' : ''}
</div>
\`;
</script>
</body>
</html>`;
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'firewall-report.html';
a.click();
URL.revokeObjectURL(url);
}
fileInput.addEventListener('change', handleFile);
analyzeBtn.addEventListener('click', analyzeData);
exportBtn.addEventListener('click', exportReport);
clearBtn.addEventListener('click', () => {
logInput.value = '';
parsedData = [];
filteredData = [];
resultsSection.classList.add('hidden');
exportBtn.classList.add('hidden');
portScanAlerts.innerHTML = '';
});
applyFilters.addEventListener('click', applyCurrentFilters);
document.querySelectorAll('#srcTable th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const sort = th.dataset.sort;
const tbody = document.getElementById('srcTableBody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aVal = a.cells[sort === 'ip' ? 0 : sort === 'count' ? 1 : sort === 'drop' ? 2 : 3].textContent;
const bVal = b.cells[sort === 'ip' ? 0 : sort === 'count' ? 1 : sort === 'drop' ? 2 : 3].textContent;
if (sort === 'ip') return aVal.localeCompare(bVal);
return parseInt(bVal) - parseInt(aVal);
});
rows.forEach(r => tbody.appendChild(r));
});
});
document.querySelectorAll('#portTable th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const sort = th.dataset.sort;
const tbody = document.getElementById('portTableBody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aVal = a.cells[sort === 'port' ? 0 : sort === 'count' ? 1 : sort === 'drop' ? 2 : 3].textContent;
const bVal = b.cells[sort === 'port' ? 0 : sort === 'count' ? 1 : sort === 'drop' ? 2 : 3].textContent;
if (sort === 'port') return parseInt(aVal) - parseInt(bVal);
return parseInt(bVal) - parseInt(aVal);
});
rows.forEach(r => tbody.appendChild(r));
});
});
</script>
</body>
</html>