summary history files

commit:386f3961a1ce96cbe3efd3da0410555d36ab3296
date:Thu Mar 19 20:43:45 2026 +1100
parents:d21692a794ad7b996aae0b153dfae7c02a30d7e3
added linux firewall log analyser tool
diff --git a/linux-firewall-log-analyser/index.html b/linux-firewall-log-analyser/index.html
line changes: +1024/-0
index 0000000..53c3cbb
--- /dev/null
+++ b/linux-firewall-log-analyser/index.html
@@ -0,0 +1,1024 @@
+<!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...&#10;[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>

diff --git a/linux-firewall-log-analyser/plan.md b/linux-firewall-log-analyser/plan.md
line changes: +41/-0
index 0000000..35e2a22
--- /dev/null
+++ b/linux-firewall-log-analyser/plan.md
@@ -0,0 +1,41 @@
+# Linux Firewall Log Analyser
+
+## Goal
+
+Build a single-page web application that parses Linux netfilter (iptables/nftables) kernel logs and generates comprehensive security reports. The application will accept log data via file upload or direct paste, parse netfilter entries containing kernel uptime timestamps, actions (IN, DROP, FWD, OUT), network interfaces, MAC addresses, source/destination IPs (IPv4 and IPv6), ports, protocols, and TCP flags.
+
+The tool will aggregate netfilter data to identify patterns such as port scanning attempts, top attacking IPs, most targeted ports, temporal distribution of blocked traffic, and protocol distributions. Output will be presented through summary statistics, sortable data tables, and visual charts to help system administrators quickly assess security events and network anomalies without requiring command-line tools.
+
+## Requirements
+
+### MUST Implement
+
+- Parse standard netfilter kernel log format produced by iptables/nftables LOG target
+- Handle kernel uptime timestamps `[seconds.microseconds]` format, not wall-clock time
+- Parse netfilter actions: `IN` (incoming accepted), `DROP` (blocked by rule), `FWD` (forwarded through), `OUT` (outgoing), with optional `INVALID` state prefix
+- Parse all netfilter key-value pairs: `IN=`, `OUT=`, `MAC=`, `SRC=`, `DST=`, `LEN=`, `TOS=`, `PREC=`, `TTL=`/`HOPLIMIT=`, `ID=`/`FLOWLBL=`, `DF` (Don't Fragment flag), `PROTO=`, `SPT=`, `DPT=`, `WINDOW=`, `RES=`, and TCP flags (SYN, ACK, FIN, PSH, RST, URG)
+- Handle both IPv4 and IPv6 netfilter formats (IPv6 uses `TC`, `HOPLIMIT`, `FLOWLBL` instead of `TOS`, `TTL`, `ID`)
+- Accept log input via text area paste and file upload (.log, .txt files)
+- Generate summary statistics: total events, event counts by netfilter action, unique source IPs, unique destination ports, temporal range
+- Identify and flag potential port scans (multiple destination ports from single source in short time window)
+- Display top 10 source IPs by event count
+- Display top 10 destination ports targeted
+- Show chronological event timeline with filtering by netfilter action type
+- Export generated report as static HTML file containing all analysis results
+- Single-file application: all HTML, CSS, and JavaScript contained in one .html file
+- No external dependencies (pure vanilla JavaScript, SVG/CSS for charts)
+
+### SHOULD Implement
+
+- Real-time filtering of displayed results by date range (kernel uptime range), IP subnet, or port number
+- Severity scoring heuristic (SYN floods, multiple DROP events from single source)
+- Search functionality across raw netfilter log lines
+- Dark/light mode toggle for usability
+
+## Research
+
+### Netfilter Log Format Confirmation
+
+The provided logs are confirmed netfilter messages from the Linux kernel's packet filtering framework. Netfilter is the kernel framework that enables packet filtering, network address translation (NAT), and packet logging. Iptables and nftables are userspace tools that configure netfilter rules; they do not perform logging themselves. When the LOG target is used in iptables/nftables rules, netfilter generates these kernel messages via the klogd/syslog interface.
+
+Format pattern: