316 lines
11 KiB
HTML
316 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>ngxstat Reports</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
|
|
</head>
|
|
<body class="section">
|
|
<div class="container">
|
|
<h1 class="title">ngxstat Reports</h1>
|
|
|
|
<div class="tabs is-toggle" id="report-tabs">
|
|
<ul>
|
|
<li class="is-active" data-tab="overview"><a>Overview</a></li>
|
|
<li data-tab="all"><a>All Domains</a></li>
|
|
<li data-tab="domain"><a>Per Domain</a></li>
|
|
<li data-tab="analysis"><a>Analysis</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div id="controls" class="field is-grouped mb-4">
|
|
<div id="interval-control" class="control has-icons-left is-hidden">
|
|
<div class="select is-small">
|
|
<select id="interval-select">
|
|
{% for interval in intervals %}
|
|
<option value="{{ interval }}">{{ interval.title() }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<span class="icon is-small is-left"><img src="icons/clock.svg" alt="Interval"></span>
|
|
</div>
|
|
<div id="domain-control" class="control has-icons-left is-hidden">
|
|
<div class="select is-small">
|
|
<select id="domain-select">
|
|
<option value="">All Domains</option>
|
|
{% for domain in domains %}
|
|
<option value="{{ domain }}">{{ domain }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<span class="icon is-small is-left"><img src="icons/server.svg" alt="Domain"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="overview-section">
|
|
<div id="overview" class="box mb-5">
|
|
<h2 class="subtitle">Overview</h2>
|
|
<p>Total logs: <span id="stat-total">-</span></p>
|
|
<p>Date range: <span id="stat-start">-</span> to <span id="stat-end">-</span></p>
|
|
<p>Unique domains: <span id="stat-domains">-</span></p>
|
|
</div>
|
|
<div id="overview-reports"></div>
|
|
</div>
|
|
|
|
<div id="all-section" class="is-hidden">
|
|
<div id="reports-all"></div>
|
|
</div>
|
|
|
|
<div id="domain-section" class="is-hidden">
|
|
<div id="reports-domain"></div>
|
|
</div>
|
|
|
|
<div id="analysis-section" class="is-hidden">
|
|
<div id="analysis-missing" class="box"></div>
|
|
<div id="analysis-cache" class="box mt-5"></div>
|
|
<div id="analysis-threats" class="box mt-5"></div>
|
|
</div>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
|
<script>
|
|
const intervalSelect = document.getElementById('interval-select');
|
|
const domainSelect = document.getElementById('domain-select');
|
|
const intervalControl = document.getElementById('interval-control');
|
|
const domainControl = document.getElementById('domain-control');
|
|
const tabs = document.querySelectorAll('#report-tabs li');
|
|
const sections = {
|
|
overview: document.getElementById('overview-section'),
|
|
all: document.getElementById('all-section'),
|
|
domain: document.getElementById('domain-section'),
|
|
analysis: document.getElementById('analysis-section')
|
|
};
|
|
const containers = {
|
|
overview: document.getElementById('overview-reports'),
|
|
all: document.getElementById('reports-all'),
|
|
domain: document.getElementById('reports-domain')
|
|
};
|
|
const analysisElems = {
|
|
missing: document.getElementById('analysis-missing'),
|
|
cache: document.getElementById('analysis-cache'),
|
|
threats: document.getElementById('analysis-threats')
|
|
};
|
|
const totalElem = document.getElementById('stat-total');
|
|
const startElem = document.getElementById('stat-start');
|
|
const endElem = document.getElementById('stat-end');
|
|
const domainsElem = document.getElementById('stat-domains');
|
|
|
|
let currentInterval = intervalSelect.value;
|
|
let currentDomain = domainSelect.value;
|
|
let currentTab = 'overview';
|
|
|
|
function initReport(rep, base) {
|
|
fetch(base + '/' + rep.json)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (rep.chart === 'table') {
|
|
const rows = data.map(x => [x.bucket, x.value]);
|
|
new DataTable('#table-' + rep.name, {
|
|
data: rows,
|
|
columns: [
|
|
{ title: 'Bucket' },
|
|
{ title: 'Value' }
|
|
]
|
|
});
|
|
return;
|
|
}
|
|
|
|
const labels = data.map(x => x.bucket);
|
|
const values = data.map(x => x.value);
|
|
const chartType = rep.chart === 'stackedBar' ? 'bar' : rep.chart;
|
|
const options = { scales: { y: { beginAtZero: true } } };
|
|
if (rep.chart === 'stackedBar') {
|
|
options.scales.x = { stacked: true };
|
|
options.scales.y.stacked = true;
|
|
}
|
|
const dataset = {
|
|
label: rep.label,
|
|
data: values,
|
|
borderWidth: 1,
|
|
fill: rep.chart !== 'bar' && rep.chart !== 'stackedBar'
|
|
};
|
|
if (rep.colors) {
|
|
dataset.backgroundColor = rep.colors;
|
|
dataset.borderColor = rep.colors;
|
|
} else if (rep.color) {
|
|
dataset.backgroundColor = rep.color;
|
|
dataset.borderColor = rep.color;
|
|
} else {
|
|
dataset.backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
|
dataset.borderColor = 'rgba(54, 162, 235, 1)';
|
|
}
|
|
new Chart(document.getElementById('chart-' + rep.name), {
|
|
type: chartType,
|
|
data: {
|
|
labels: labels,
|
|
datasets: [dataset]
|
|
},
|
|
options: options
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadStats() {
|
|
fetch('global/stats.json')
|
|
.then(r => r.json())
|
|
.then(stats => {
|
|
totalElem.textContent = stats.total_logs;
|
|
startElem.textContent = stats.start_date;
|
|
endElem.textContent = stats.end_date;
|
|
domainsElem.textContent = stats.unique_domains;
|
|
});
|
|
}
|
|
|
|
function loadReports() {
|
|
let path;
|
|
let container;
|
|
if (currentTab === 'overview') {
|
|
path = 'global';
|
|
container = containers.overview;
|
|
} else if (currentTab === 'all') {
|
|
path = currentInterval;
|
|
container = containers.all;
|
|
} else {
|
|
container = containers.domain;
|
|
if (!currentDomain) {
|
|
container.innerHTML = '<p>Select a domain</p>';
|
|
return;
|
|
}
|
|
path = 'domains/' + encodeURIComponent(currentDomain) + '/' + currentInterval;
|
|
}
|
|
|
|
fetch(path + '/reports.json')
|
|
.then(r => r.json())
|
|
.then(reports => {
|
|
container.innerHTML = '';
|
|
reports.forEach(rep => {
|
|
fetch(path + '/' + rep.html)
|
|
.then(r => r.text())
|
|
.then(html => {
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
initReport(rep, path);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function loadAnalysis() {
|
|
analysisElems.missing.innerHTML = '<h2 class="subtitle">Missing Domains</h2>';
|
|
analysisElems.cache.innerHTML = '<h2 class="subtitle">Cache Suggestions</h2>';
|
|
analysisElems.threats.innerHTML = '<h2 class="subtitle">Threat Report</h2>';
|
|
|
|
fetch('analysis/missing_domains.json')
|
|
.then(r => r.json())
|
|
.then(list => {
|
|
if (list.length === 0) {
|
|
analysisElems.missing.insertAdjacentHTML('beforeend', '<p>None</p>');
|
|
return;
|
|
}
|
|
const items = list.map(d => `<li>${d}</li>`).join('');
|
|
analysisElems.missing.insertAdjacentHTML('beforeend', `<ul>${items}</ul>`);
|
|
});
|
|
|
|
fetch('analysis/cache_suggestions.json')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.length === 0) {
|
|
analysisElems.cache.insertAdjacentHTML('beforeend', '<p>No suggestions</p>');
|
|
return;
|
|
}
|
|
analysisElems.cache.insertAdjacentHTML('beforeend', '<table id="table-cache" class="table is-striped"></table>');
|
|
const rows = data.map(x => [x.host, x.path, x.misses]);
|
|
new DataTable('#table-cache', {
|
|
data: rows,
|
|
columns: [
|
|
{ title: 'Domain' },
|
|
{ title: 'Path' },
|
|
{ title: 'Misses' }
|
|
]
|
|
});
|
|
});
|
|
|
|
fetch('analysis/threat_report.json')
|
|
.then(r => r.json())
|
|
.then(rep => {
|
|
const hasData = rep.error_spikes?.length || rep.suspicious_agents?.length || rep.high_ip_requests?.length;
|
|
if (!hasData) {
|
|
analysisElems.threats.insertAdjacentHTML('beforeend', '<p>No threats detected</p>');
|
|
return;
|
|
}
|
|
if (rep.error_spikes && rep.error_spikes.length) {
|
|
analysisElems.threats.insertAdjacentHTML('beforeend', '<h3 class="subtitle is-6 mt-4">Error Spikes</h3><table id="table-errors" class="table is-striped"></table>');
|
|
const rows = rep.error_spikes.map(x => [x.host, x.recent_error_rate, x.previous_error_rate]);
|
|
new DataTable('#table-errors', {
|
|
data: rows,
|
|
columns: [
|
|
{ title: 'Domain' },
|
|
{ title: 'Recent %' },
|
|
{ title: 'Previous %' }
|
|
]
|
|
});
|
|
}
|
|
if (rep.suspicious_agents && rep.suspicious_agents.length) {
|
|
analysisElems.threats.insertAdjacentHTML('beforeend', '<h3 class="subtitle is-6 mt-4">Suspicious User Agents</h3><table id="table-agents" class="table is-striped"></table>');
|
|
const rows = rep.suspicious_agents.map(x => [x.user_agent, x.requests]);
|
|
new DataTable('#table-agents', {
|
|
data: rows,
|
|
columns: [
|
|
{ title: 'User Agent' },
|
|
{ title: 'Requests' }
|
|
]
|
|
});
|
|
}
|
|
if (rep.high_ip_requests && rep.high_ip_requests.length) {
|
|
analysisElems.threats.insertAdjacentHTML('beforeend', '<h3 class="subtitle is-6 mt-4">High IP Requests</h3><table id="table-ips" class="table is-striped"></table>');
|
|
const rows = rep.high_ip_requests.map(x => [x.ip, x.requests]);
|
|
new DataTable('#table-ips', {
|
|
data: rows,
|
|
columns: [
|
|
{ title: 'IP' },
|
|
{ title: 'Requests' }
|
|
]
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function switchTab(name) {
|
|
currentTab = name;
|
|
tabs.forEach(tab => {
|
|
tab.classList.toggle('is-active', tab.dataset.tab === name);
|
|
});
|
|
Object.entries(sections).forEach(([key, section]) => {
|
|
section.classList.toggle('is-hidden', key !== name);
|
|
});
|
|
intervalControl.classList.toggle('is-hidden', name === 'overview' || name === 'analysis');
|
|
domainControl.classList.toggle('is-hidden', name !== 'domain');
|
|
if (name === 'overview') {
|
|
loadStats();
|
|
}
|
|
if (name === 'analysis') {
|
|
loadAnalysis();
|
|
} else {
|
|
loadReports();
|
|
}
|
|
}
|
|
|
|
intervalSelect.addEventListener('change', () => {
|
|
currentInterval = intervalSelect.value;
|
|
loadReports();
|
|
});
|
|
|
|
domainSelect.addEventListener('change', () => {
|
|
currentDomain = domainSelect.value;
|
|
loadReports();
|
|
});
|
|
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
|
|
});
|
|
|
|
switchTab('overview');
|
|
</script>
|
|
</body>
|
|
</html>
|