diff --git a/README.md b/README.md
index 70f21d9..acb1055 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,8 @@ threats.
```bash
./run-analysis.sh
```
+The JSON results are written under `output/analysis` and can be viewed from the
+"Analysis" tab in the generated dashboard.
## Serving Reports with Nginx
To expose the generated HTML dashboards and JSON files over HTTP you can use a
diff --git a/scripts/analyze.py b/scripts/analyze.py
index 219ceeb..8ac7c30 100644
--- a/scripts/analyze.py
+++ b/scripts/analyze.py
@@ -138,6 +138,10 @@ def check_missing_domains(json_output: bool = typer.Option(False, "--json", help
missing = sorted(db_domains - config_domains)
+ ANALYSIS_DIR.mkdir(parents=True, exist_ok=True)
+ out_path = ANALYSIS_DIR / "missing_domains.json"
+ out_path.write_text(json.dumps(missing, indent=2))
+
if json_output:
typer.echo(json.dumps(missing))
else:
@@ -189,14 +193,19 @@ def suggest_cache(
rows = [r for r in cur.fetchall() if r[0] in no_cache]
conn.close()
+ result = [
+ {"host": host, "path": path, "misses": count} for host, path, count in rows
+ ]
+
+ ANALYSIS_DIR.mkdir(parents=True, exist_ok=True)
+ out_path = ANALYSIS_DIR / "cache_suggestions.json"
+ out_path.write_text(json.dumps(result, indent=2))
+
if json_output:
- result = [
- {"host": host, "path": path, "misses": count} for host, path, count in rows
- ]
typer.echo(json.dumps(result))
else:
- for host, path, count in rows:
- typer.echo(f"{host} {path} {count}")
+ for item in result:
+ typer.echo(f"{item['host']} {item['path']} {item['misses']}")
@app.command("detect-threats")
diff --git a/templates/index.html b/templates/index.html
index 91482f7..7b0b98f 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -15,6 +15,7 @@
Overview
All Domains
Per Domain
+ Analysis
@@ -56,9 +57,15 @@
-
+
+
+
@@ -73,13 +80,19 @@
const sections = {
overview: document.getElementById('overview-section'),
all: document.getElementById('all-section'),
- domain: document.getElementById('domain-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');
@@ -169,19 +182,99 @@
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);
- });
+ 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);
+ });
+ });
+ feather.replace();
});
- feather.replace();
+ }
+
+ function loadAnalysis() {
+ analysisElems.missing.innerHTML = 'Missing Domains
';
+ analysisElems.cache.innerHTML = 'Cache Suggestions
';
+ analysisElems.threats.innerHTML = 'Threat Report
';
+
+ fetch('analysis/missing_domains.json')
+ .then(r => r.json())
+ .then(list => {
+ if (list.length === 0) {
+ analysisElems.missing.insertAdjacentHTML('beforeend', 'None
');
+ return;
+ }
+ const items = list.map(d => `${d}`).join('');
+ analysisElems.missing.insertAdjacentHTML('beforeend', ``);
+ });
+
+ fetch('analysis/cache_suggestions.json')
+ .then(r => r.json())
+ .then(data => {
+ if (data.length === 0) {
+ analysisElems.cache.insertAdjacentHTML('beforeend', 'No suggestions
');
+ return;
+ }
+ analysisElems.cache.insertAdjacentHTML('beforeend', '');
+ 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', 'No threats detected
');
+ return;
+ }
+ if (rep.error_spikes && rep.error_spikes.length) {
+ analysisElems.threats.insertAdjacentHTML('beforeend', 'Error Spikes
');
+ 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', 'Suspicious User Agents
');
+ 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', 'High IP Requests
');
+ const rows = rep.high_ip_requests.map(x => [x.ip, x.requests]);
+ new DataTable('#table-ips', {
+ data: rows,
+ columns: [
+ { title: 'IP' },
+ { title: 'Requests' }
+ ]
+ });
+ }
});
}
@@ -198,7 +291,11 @@
if (name === 'overview') {
loadStats();
}
- loadReports();
+ if (name === 'analysis') {
+ loadAnalysis();
+ } else {
+ loadReports();
+ }
}
intervalSelect.addEventListener('change', () => {