new options
This commit is contained in:
20
app.py
20
app.py
@@ -1,14 +1,15 @@
|
|||||||
|
|
||||||
from flask import Flask, render_template, render_template_string
|
import os
|
||||||
import configparser
|
import sys
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
from flask import Flask, render_template, render_template_string
|
||||||
from routes.main_routes import main_bp
|
from routes.main_routes import main_bp
|
||||||
from routes.edit_routes import edit_bp
|
from routes.edit_routes import edit_bp
|
||||||
from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats
|
from utils.stats_utils import fetch_haproxy_stats, parse_haproxy_stats
|
||||||
from auth.auth_middleware import setup_auth
|
from auth.auth_middleware import setup_auth
|
||||||
from log_parser import parse_log_file
|
from log_parser import parse_log_file
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends
|
from utils.haproxy_config import update_haproxy_config, is_frontend_exist, count_frontends_and_backends
|
||||||
|
|
||||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
@@ -73,23 +74,23 @@ try:
|
|||||||
certificate_path = config2.get('ssl', 'certificate_path')
|
certificate_path = config2.get('ssl', 'certificate_path')
|
||||||
private_key_path = config2.get('ssl', 'private_key_path')
|
private_key_path = config2.get('ssl', 'private_key_path')
|
||||||
else:
|
else:
|
||||||
print(f"[APP] ✗ No [ssl] section in {SSL_INI}", flush=True)
|
print(f"[APP] No [ssl] section in {SSL_INI}", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not os.path.exists(certificate_path):
|
if not os.path.exists(certificate_path):
|
||||||
print(f"[APP] ✗ Certificate not found: {certificate_path}", flush=True)
|
print(f"[APP] Certificate not found: {certificate_path}", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not os.path.exists(private_key_path):
|
if not os.path.exists(private_key_path):
|
||||||
print(f"[APP] ✗ Private key not found: {private_key_path}", flush=True)
|
print(f"[APP] Private key not found: {private_key_path}", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
||||||
ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path)
|
ssl_context.load_cert_chain(certfile=certificate_path, keyfile=private_key_path)
|
||||||
print(f"[APP] ✓ SSL context loaded", flush=True)
|
print(f"[APP] SSL context loaded", flush=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[APP] ✗ SSL error: {e}", flush=True)
|
print(f"[APP] SSL error: {e}", flush=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -105,7 +106,6 @@ def display_logs():
|
|||||||
parsed_entries = parse_log_file(log_file_path)
|
parsed_entries = parse_log_file(log_file_path)
|
||||||
return render_template('logs.html', entries=parsed_entries)
|
return render_template('logs.html', entries=parsed_entries)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/home')
|
@app.route('/home')
|
||||||
def home():
|
def home():
|
||||||
frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends()
|
frontend_count, backend_count, acl_count, layer7_count, layer4_count = count_frontends_and_backends()
|
||||||
|
|||||||
232
log_parser.py
232
log_parser.py
@@ -1,7 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def parse_log_file(log_file_path):
|
def parse_log_file(log_file_path):
|
||||||
|
|
||||||
parsed_entries = []
|
parsed_entries = []
|
||||||
|
|
||||||
xss_patterns = [
|
xss_patterns = [
|
||||||
r'<\s*script\s*',
|
r'<\s*script\s*',
|
||||||
r'javascript:',
|
r'javascript:',
|
||||||
@@ -16,88 +20,170 @@ def parse_log_file(log_file_path):
|
|||||||
r'alert',
|
r'alert',
|
||||||
r'onerror',
|
r'onerror',
|
||||||
r'onload',
|
r'onload',
|
||||||
r'javascript'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sql_patterns = [
|
sql_patterns = [
|
||||||
r';',
|
r'(union|select|insert|update|delete|drop)\s+(from|into|table)',
|
||||||
r'substring',
|
r';\s*(union|select|insert|update|delete|drop)',
|
||||||
r'extract',
|
r'substring\s*\(',
|
||||||
r'union\s+all',
|
r'extract\s*\(',
|
||||||
r'order\s+by',
|
r'order\s+by\s+\d+',
|
||||||
r'--\+',
|
r'--\+',
|
||||||
r'union',
|
r'1\s*=\s*1',
|
||||||
r'select',
|
r'@@\w+',
|
||||||
r'insert',
|
|
||||||
r'update',
|
|
||||||
r'delete',
|
|
||||||
r'drop',
|
|
||||||
r'@@',
|
|
||||||
r'1=1',
|
|
||||||
r'`1',
|
r'`1',
|
||||||
r'union',
|
r'\|\|\s*chr\(',
|
||||||
r'select',
|
|
||||||
r'insert',
|
|
||||||
r'update',
|
|
||||||
r'delete',
|
|
||||||
r'drop',
|
|
||||||
r'@@',
|
|
||||||
r'1=1',
|
|
||||||
r'`1'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
webshells_patterns = [
|
webshells_patterns = [
|
||||||
r'payload',
|
r'eval\s*\(',
|
||||||
r'eval|system|passthru|shell_exec|exec|popen|proc_open|pcntl_exec|cmd|shell|backdoor|webshell|phpspy|c99|kacak|b374k|log4j|log4shell|wsos|madspot|malicious|evil.*\.php.*'
|
r'system\s*\(',
|
||||||
|
r'passthru\s*\(',
|
||||||
|
r'shell_exec\s*\(',
|
||||||
|
r'exec\s*\(',
|
||||||
|
r'popen\s*\(',
|
||||||
|
r'proc_open\s*\(',
|
||||||
|
r'pcntl_exec\s*\(',
|
||||||
|
r'\.php\?cmd=',
|
||||||
|
r'\.php\?id=',
|
||||||
|
r'backdoor|webshell|phpspy|c99|kacak|b374k|wsos|madspot|r57|c100|r57shell',
|
||||||
]
|
]
|
||||||
|
|
||||||
combined_xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE)
|
xss_pattern = re.compile('|'.join(xss_patterns), re.IGNORECASE)
|
||||||
combined_sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE)
|
sql_pattern = re.compile('|'.join(sql_patterns), re.IGNORECASE)
|
||||||
combined_webshells_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE)
|
webshell_pattern = re.compile('|'.join(webshells_patterns), re.IGNORECASE)
|
||||||
|
|
||||||
with open(log_file_path, 'r') as log_file:
|
try:
|
||||||
log_lines = log_file.readlines()
|
with open(log_file_path, 'r') as log_file:
|
||||||
|
log_lines = log_file.readlines()
|
||||||
|
|
||||||
for line in log_lines:
|
for line in log_lines:
|
||||||
if " 403 " in line: # Check if the line contains " 403 " indicating a 403 status code
|
if not line.strip():
|
||||||
match = re.search(r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"\s*(GET|POST|PUT|DELETE)\s+([^"]+)"', line)
|
continue
|
||||||
if match:
|
|
||||||
timestamp = match.group(1) # Extract the date and time
|
match = re.search(
|
||||||
ip_address = match.group(2)
|
r'(\w+\s+\d+\s\d+:\d+:\d+).*\s(\d+\.\d+\.\d+\.\d+).*"?\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^"\s]+)"?\s+(\d{3})',
|
||||||
http_method = match.group(3)
|
line
|
||||||
requested_url = match.group(4)
|
)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
timestamp = match.group(1)
|
||||||
|
ip_address = match.group(2)
|
||||||
|
http_method = match.group(3)
|
||||||
|
requested_url = match.group(4)
|
||||||
|
status_code = int(match.group(5))
|
||||||
|
|
||||||
|
threats = []
|
||||||
|
threat_level = 'info'
|
||||||
|
|
||||||
|
if xss_pattern.search(line):
|
||||||
|
threats.append('XSS Attack')
|
||||||
|
threat_level = 'danger'
|
||||||
|
|
||||||
|
if sql_pattern.search(line):
|
||||||
|
threats.append('SQL Injection')
|
||||||
|
threat_level = 'danger'
|
||||||
|
|
||||||
|
if webshell_pattern.search(line):
|
||||||
|
threats.append('Webshell')
|
||||||
|
threat_level = 'danger'
|
||||||
|
|
||||||
|
if http_method == 'PUT':
|
||||||
|
threats.append('Remote Upload')
|
||||||
|
threat_level = 'warning'
|
||||||
|
|
||||||
|
if 'admin' in requested_url.lower() or 'config' in requested_url.lower():
|
||||||
|
if status_code == 403:
|
||||||
|
threats.append('Unauthorized Access')
|
||||||
|
threat_level = 'warning'
|
||||||
|
|
||||||
|
status_category = 'info'
|
||||||
|
if 200 <= status_code < 300:
|
||||||
|
status_category = 'success'
|
||||||
|
elif 300 <= status_code < 400:
|
||||||
|
status_category = 'secondary'
|
||||||
|
elif 400 <= status_code < 500:
|
||||||
|
status_category = 'warning'
|
||||||
|
elif status_code >= 500:
|
||||||
|
status_category = 'danger'
|
||||||
|
|
||||||
|
parsed_entries.append({
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'ip_address': ip_address,
|
||||||
|
'http_method': http_method,
|
||||||
|
'requested_url': requested_url,
|
||||||
|
'status_code': status_code,
|
||||||
|
'status_category': status_category,
|
||||||
|
'threats': threats if threats else ['None'],
|
||||||
|
'threat_level': threat_level if threats else 'info',
|
||||||
|
'is_threat': bool(threats),
|
||||||
|
})
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
return [{'error': f'Log file not found: {log_file_path}'}]
|
||||||
|
except Exception as e:
|
||||||
|
return [{'error': f'Error parsing log: {str(e)}'}]
|
||||||
|
|
||||||
|
return parsed_entries
|
||||||
|
|
||||||
if combined_xss_pattern.search(line):
|
|
||||||
xss_alert = 'Possible XSS Attack Was Identified.'
|
|
||||||
else:
|
|
||||||
xss_alert = ''
|
|
||||||
if combined_sql_pattern.search(line):
|
|
||||||
sql_alert = 'Possible SQL Injection Attempt Was Made.'
|
|
||||||
else:
|
|
||||||
sql_alert = ''
|
|
||||||
if "PUT" in line:
|
|
||||||
put_method = 'Possible Remote File Upload Attempt Was Made.'
|
|
||||||
else:
|
|
||||||
put_method = ''
|
|
||||||
|
|
||||||
if "admin" in line:
|
def get_log_statistics(parsed_entries):
|
||||||
illegal_resource = 'Possible Illegal Resource Access Attempt Was Made.'
|
|
||||||
else:
|
|
||||||
illegal_resource = ''
|
|
||||||
|
|
||||||
if combined_webshells_pattern.search(line):
|
stats = {
|
||||||
webshell_alert = 'Possible WebShell Attack Attempt Was Made.'
|
'total_requests': len(parsed_entries),
|
||||||
else:
|
'threat_count': sum(1 for e in parsed_entries if e.get('is_threat')),
|
||||||
webshell_alert = ''
|
'status_codes': defaultdict(int),
|
||||||
|
'http_methods': defaultdict(int),
|
||||||
|
'top_ips': defaultdict(int),
|
||||||
|
'threat_types': defaultdict(int),
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in parsed_entries:
|
||||||
|
if 'error' in entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stats['status_codes'][entry['status_code']] += 1
|
||||||
|
stats['http_methods'][entry['http_method']] += 1
|
||||||
|
stats['top_ips'][entry['ip_address']] += 1
|
||||||
|
|
||||||
|
for threat in entry.get('threats', []):
|
||||||
|
if threat != 'None':
|
||||||
|
stats['threat_types'][threat] += 1
|
||||||
|
|
||||||
|
stats['top_ips'] = sorted(
|
||||||
|
stats['top_ips'].items(),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True
|
||||||
|
)[:5]
|
||||||
|
|
||||||
|
stats['status_codes'] = dict(stats['status_codes'])
|
||||||
|
stats['http_methods'] = dict(stats['http_methods'])
|
||||||
|
stats['threat_types'] = dict(stats['threat_types'])
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
parsed_entries.append({
|
|
||||||
'timestamp': timestamp,
|
def filter_logs(parsed_entries, filters=None):
|
||||||
'ip_address': ip_address,
|
if not filters:
|
||||||
'http_method': http_method,
|
return parsed_entries
|
||||||
'requested_url': requested_url,
|
|
||||||
'xss_alert': xss_alert,
|
filtered = parsed_entries
|
||||||
'sql_alert': sql_alert,
|
|
||||||
'put_method': put_method,
|
if 'status_code' in filters and filters['status_code']:
|
||||||
'illegal_resource': illegal_resource,
|
filtered = [e for e in filtered if e.get('status_code') == int(filters['status_code'])]
|
||||||
'webshell_alert': webshell_alert
|
|
||||||
})
|
if 'threat_level' in filters and filters['threat_level']:
|
||||||
return parsed_entries
|
filtered = [e for e in filtered if e.get('threat_level') == filters['threat_level']]
|
||||||
|
|
||||||
|
if 'http_method' in filters and filters['http_method']:
|
||||||
|
filtered = [e for e in filtered if e.get('http_method') == filters['http_method']]
|
||||||
|
|
||||||
|
if 'ip_address' in filters and filters['ip_address']:
|
||||||
|
filtered = [e for e in filtered if e.get('ip_address') == filters['ip_address']]
|
||||||
|
|
||||||
|
if 'has_threat' in filters and filters['has_threat']:
|
||||||
|
filtered = [e for e in filtered if e.get('is_threat')]
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
document.getElementById('filter_status')?.addEventListener('change', filterLogs);
|
||||||
|
document.getElementById('filter_threat')?.addEventListener('change', filterLogs);
|
||||||
|
document.getElementById('filter_method')?.addEventListener('change', filterLogs);
|
||||||
|
document.getElementById('filter_threats_only')?.addEventListener('change', filterLogs);
|
||||||
|
|
||||||
|
function filterLogs() {
|
||||||
|
const statusFilter = document.getElementById('filter_status')?.value;
|
||||||
|
const threatFilter = document.getElementById('filter_threat')?.value;
|
||||||
|
const methodFilter = document.getElementById('filter_method')?.value;
|
||||||
|
const threatsOnly = document.getElementById('filter_threats_only')?.checked;
|
||||||
|
|
||||||
|
document.querySelectorAll('.log-row').forEach(row => {
|
||||||
|
let show = true;
|
||||||
|
|
||||||
|
if (statusFilter && row.dataset.status !== statusFilter) show = false;
|
||||||
|
if (threatFilter && row.dataset.threat !== threatFilter) show = false;
|
||||||
|
if (methodFilter && row.dataset.method !== methodFilter) show = false;
|
||||||
|
if (threatsOnly && row.dataset.threatCount === '0') show = false;
|
||||||
|
|
||||||
|
row.style.display = show ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,50 +1,162 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "" %}
|
|
||||||
{% block title %}HAProxy • Logs{% endblock %}
|
{% set active_page = "logs" %}
|
||||||
{% block breadcrumb %}<nav aria-label="breadcrumb" class="mb-3"><ol class="breadcrumb mb-0"><li class="breadcrumb-item"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item active" aria-current="page">Logi</li></ol></nav>{% endblock %}
|
|
||||||
|
{% block title %}HAProxy • Access Logs{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}Access Logs{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3 class="mb-4" id="status_header">Status 403 Forbidden</h3>
|
|
||||||
{% if entries %}
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="vstack gap-3">
|
<div class="card-header bg-info text-white">
|
||||||
{% for entry in entries %}
|
<h5 class="mb-0"><i class="bi bi-file-text me-2"></i>HAProxy Access Logs & Security Analysis</h5>
|
||||||
<div class="card">
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
{% if logs %}
|
||||||
<div><strong>Czas:</strong> {{ entry['timestamp'] }}</div>
|
<!-- Statistics Cards -->
|
||||||
<div><strong>IP:</strong> {{ entry['ip_address'] }}</div>
|
<div class="row mb-4">
|
||||||
<div><strong>Metoda:</strong> {{ entry['http_method'] }}</div>
|
<div class="col-md-3">
|
||||||
<div><strong>URL:</strong> {{ entry['requested_url'] }}</div>
|
<div class="card text-center">
|
||||||
<div><strong>Status:</strong> <span class="badge bg-danger">403</span></div>
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Total Requests</h6>
|
||||||
|
<div class="fs-3 fw-bold text-primary">{{ logs|length }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Threats Detected</h6>
|
||||||
|
<div class="fs-3 fw-bold text-danger">
|
||||||
|
{{ logs|selectattr('is_threat')|list|length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Unique IPs</h6>
|
||||||
|
<div class="fs-3 fw-bold text-warning">
|
||||||
|
{{ logs|map(attribute='ip_address')|unique|list|length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Success Rate</h6>
|
||||||
|
<div class="fs-3 fw-bold text-success">
|
||||||
|
{% set success_count = logs|selectattr('status_code')|selectattr('status_code', 'ge', 200)|selectattr('status_code', 'lt', 300)|list|length %}
|
||||||
|
{{ ((success_count / logs|length * 100)|round(1)) if logs else 0 }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
{% if entry['xss_alert'] %}
|
<!-- Filters -->
|
||||||
<p class="mb-1"><button class="btn btn-sm btn-outline-danger" data-bs-toggle="collapse" data-bs-target="#xssCollapse{{ loop.index }}"><i class="bi bi-bug"></i> XSS alert</button></p>
|
<div class="card mb-4 bg-light">
|
||||||
<div id="xssCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['xss_alert'] }}</pre></div>
|
<div class="card-body">
|
||||||
{% endif %}
|
<h6 class="mb-3"><i class="bi bi-funnel me-2"></i>Filters</h6>
|
||||||
{% if entry['sql_alert'] %}
|
<div class="row g-3">
|
||||||
<p class="mb-1"><button class="btn btn-sm btn-outline-warning" data-bs-toggle="collapse" data-bs-target="#sqlCollapse{{ loop.index }}"><i class="bi bi-database-exclamation"></i> SQLi alert</button></p>
|
<div class="col-md-3">
|
||||||
<div id="sqlCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-warning">{{ entry['sql_alert'] }}</pre></div>
|
<label class="form-label small">Status Code</label>
|
||||||
{% endif %}
|
<select class="form-select form-select-sm" id="filter_status">
|
||||||
{% if entry['put_method'] %}
|
<option value="">All</option>
|
||||||
<p class="mb-1"><button class="btn btn-sm btn-outline-info" data-bs-toggle="collapse" data-bs-target="#putCollapse{{ loop.index }}"><i class="bi bi-upload"></i> PUT alert</button></p>
|
{% for log in logs %}
|
||||||
<div id="putCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-info">{{ entry['put_method'] }}</pre></div>
|
<option value="{{ log.status_code }}">{{ log.status_code }}</option>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{% if entry['illegal_resource'] %}
|
</select>
|
||||||
<p class="mb-1"><button class="btn btn-sm btn-outline-light" data-bs-toggle="collapse" data-bs-target="#illegalCollapse{{ loop.index }}"><i class="bi bi-shield-x"></i> Nielegalny zasób</button></p>
|
</div>
|
||||||
<div id="illegalCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-light">{{ entry['illegal_resource'] }}</pre></div>
|
<div class="col-md-3">
|
||||||
{% endif %}
|
<label class="form-label small">Threat Level</label>
|
||||||
{% if entry['webshell_alert'] %}
|
<select class="form-select form-select-sm" id="filter_threat">
|
||||||
<p class="mb-1"><button class="btn btn-sm btn-outline-danger" data-bs-toggle="collapse" data-bs-target="#webshellCollapse{{ loop.index }}"><i class="bi bi-file-earmark-code"></i> WebShell alert</button></p>
|
<option value="">All</option>
|
||||||
<div id="webshellCollapse{{ loop.index }}" class="collapse"><pre class="mb-0 text-danger">{{ entry['webshell_alert'] }}</pre></div>
|
<option value="danger">Danger</option>
|
||||||
{% endif %}
|
<option value="warning">Warning</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small">HTTP Method</label>
|
||||||
|
<select class="form-select form-select-sm" id="filter_method">
|
||||||
|
<option value="">All</option>
|
||||||
|
{% set methods = logs|map(attribute='http_method')|unique %}
|
||||||
|
{% for method in methods %}
|
||||||
|
<option value="{{ method }}">{{ method }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small">Show Threats Only</label>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_threats_only">
|
||||||
|
<label class="form-check-label" for="filter_threats_only">
|
||||||
|
Threats
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<!-- Logs Table -->
|
||||||
</div>
|
<div class="table-responsive">
|
||||||
{% endfor %}
|
<table class="table table-striped table-hover table-sm">
|
||||||
</div>
|
<thead class="table-dark">
|
||||||
{% else %}
|
<tr>
|
||||||
<div class="alert alert-info"><i class="bi bi-info-circle me-1"></i>No data.</div>
|
<th>Timestamp</th>
|
||||||
{% endif %}
|
<th>IP Address</th>
|
||||||
|
<th>Method</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Threats</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logs_table_body">
|
||||||
|
{% for log in logs %}
|
||||||
|
<tr class="log-row" data-status="{{ log.status_code }}" data-threat="{{ log.threat_level }}" data-method="{{ log.http_method }}" data-threat-count="{{ 1 if log.is_threat else 0 }}">
|
||||||
|
<td class="small">{{ log.timestamp }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-secondary">{{ log.ip_address }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-primary">{{ log.http_method }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-truncate" style="max-width: 300px;" title="{{ log.requested_url }}">
|
||||||
|
{{ log.requested_url }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ log.status_category }}">{{ log.status_code }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.is_threat %}
|
||||||
|
{% for threat in log.threats %}
|
||||||
|
<span class="badge bg-{{ log.threat_level }} me-1">{{ threat }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>No log entries found.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ def fetch_haproxy_stats():
|
|||||||
|
|
||||||
def parse_haproxy_stats(stats_data):
|
def parse_haproxy_stats(stats_data):
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Skip empty lines and get header
|
|
||||||
lines = [line for line in stats_data.splitlines() if line.strip()]
|
lines = [line for line in stats_data.splitlines() if line.strip()]
|
||||||
if not lines:
|
if not lines:
|
||||||
return data
|
return data
|
||||||
@@ -23,17 +22,14 @@ def parse_haproxy_stats(stats_data):
|
|||||||
|
|
||||||
# Parse CSV
|
# Parse CSV
|
||||||
reader = csv.DictReader(lines, fieldnames=header_row.split(','))
|
reader = csv.DictReader(lines, fieldnames=header_row.split(','))
|
||||||
next(reader) # Skip header
|
next(reader)
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
# Only process servers, skip BACKEND summary rows
|
|
||||||
if row.get('svname') == 'BACKEND':
|
if row.get('svname') == 'BACKEND':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Strip whitespace from values
|
|
||||||
row = {k: v.strip() if isinstance(v, str) else v for k, v in row.items()}
|
row = {k: v.strip() if isinstance(v, str) else v for k, v in row.items()}
|
||||||
|
|
||||||
# Safe conversion to int/float
|
|
||||||
try:
|
try:
|
||||||
conn_tot = int(row.get('conn_tot', 0) or 0)
|
conn_tot = int(row.get('conn_tot', 0) or 0)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@@ -51,13 +47,13 @@ def parse_haproxy_stats(stats_data):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
bin_bytes = float(row.get('bin', 0) or 0)
|
bin_bytes = float(row.get('bin', 0) or 0)
|
||||||
bytes_in_mb = bin_bytes / (1024 * 1024) # ✅ FLOAT, nie string!
|
bytes_in_mb = bin_bytes / (1024 * 1024)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
bytes_in_mb = 0.0
|
bytes_in_mb = 0.0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bout_bytes = float(row.get('bout', 0) or 0)
|
bout_bytes = float(row.get('bout', 0) or 0)
|
||||||
bytes_out_mb = bout_bytes / (1024 * 1024) # ✅ FLOAT, nie string!
|
bytes_out_mb = bout_bytes / (1024 * 1024)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
bytes_out_mb = 0.0
|
bytes_out_mb = 0.0
|
||||||
|
|
||||||
@@ -66,9 +62,9 @@ def parse_haproxy_stats(stats_data):
|
|||||||
'server_name': row.get('svname', 'Unknown'),
|
'server_name': row.get('svname', 'Unknown'),
|
||||||
'4xx_errors': hrsp_4xx,
|
'4xx_errors': hrsp_4xx,
|
||||||
'5xx_errors': hrsp_5xx,
|
'5xx_errors': hrsp_5xx,
|
||||||
'bytes_in_mb': bytes_in_mb, # ✅ Float
|
'bytes_in_mb': bytes_in_mb,
|
||||||
'bytes_out_mb': bytes_out_mb, # ✅ Float
|
'bytes_out_mb': bytes_out_mb,
|
||||||
'conn_tot': conn_tot, # ✅ Int
|
'conn_tot': conn_tot,
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
Reference in New Issue
Block a user