Compare commits
11 Commits
new_functi
...
32ef62e4ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 32ef62e4ac | |||
|
|
04acb4ac21 | ||
|
|
9949e34d68 | ||
|
|
0a027bbebd | ||
|
|
3e7861f489 | ||
|
|
da1af612ef | ||
|
|
370c7099f5 | ||
|
|
27f9984574 | ||
|
|
34c84f1115 | ||
| 71b0b39a0f | |||
| 5591772e76 |
113
app.py
113
app.py
@@ -1,10 +1,8 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
from flask import Flask, render_template, render_template_string
|
from flask import Flask, render_template, render_template_string, request, jsonify
|
||||||
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
|
||||||
@@ -59,7 +57,6 @@ except Exception as e:
|
|||||||
|
|
||||||
app.register_blueprint(main_bp)
|
app.register_blueprint(main_bp)
|
||||||
app.register_blueprint(edit_bp)
|
app.register_blueprint(edit_bp)
|
||||||
|
|
||||||
setup_auth(app)
|
setup_auth(app)
|
||||||
|
|
||||||
certificate_path = None
|
certificate_path = None
|
||||||
@@ -69,71 +66,131 @@ ssl_context = None
|
|||||||
try:
|
try:
|
||||||
config2 = configparser.ConfigParser()
|
config2 = configparser.ConfigParser()
|
||||||
config2.read(SSL_INI)
|
config2.read(SSL_INI)
|
||||||
|
|
||||||
if config2.has_section('ssl'):
|
if config2.has_section('ssl'):
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/statistics')
|
@app.route('/statistics')
|
||||||
def display_haproxy_stats():
|
def display_haproxy_stats():
|
||||||
haproxy_stats = fetch_haproxy_stats()
|
haproxy_stats = fetch_haproxy_stats()
|
||||||
parsed_stats = parse_haproxy_stats(haproxy_stats)
|
parsed_stats = parse_haproxy_stats(haproxy_stats)
|
||||||
return render_template('statistics.html', stats=parsed_stats)
|
return render_template('statistics.html', stats=parsed_stats)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/logs', endpoint='display_logs')
|
@app.route('/logs', endpoint='display_logs')
|
||||||
#@requires_auth
|
|
||||||
def display_haproxy_logs():
|
def display_haproxy_logs():
|
||||||
log_file_path = '/var/log/haproxy.log'
|
log_file_path = '/var/log/haproxy.log'
|
||||||
|
|
||||||
if not os.path.exists(log_file_path):
|
if not os.path.exists(log_file_path):
|
||||||
return render_template('logs.html',
|
return render_template('logs.html',
|
||||||
logs=[],
|
logs=[],
|
||||||
|
total_logs=0,
|
||||||
error_message=f"Log file not found: {log_file_path}")
|
error_message=f"Log file not found: {log_file_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logs = parse_log_file(log_file_path)
|
logs = parse_log_file(log_file_path)
|
||||||
if not logs:
|
total_logs = len(logs)
|
||||||
return render_template('logs.html',
|
# Załaduj ostatnie 200 logów
|
||||||
logs=[],
|
initial_logs = logs[-200:] if len(logs) > 200 else logs
|
||||||
error_message="Log file is empty or unreadable")
|
|
||||||
return render_template('logs.html', logs=logs)
|
|
||||||
except Exception as e:
|
|
||||||
return render_template('logs.html',
|
return render_template('logs.html',
|
||||||
logs=[],
|
logs=initial_logs,
|
||||||
|
total_logs=total_logs,
|
||||||
|
loaded_count=len(initial_logs))
|
||||||
|
except Exception as e:
|
||||||
|
return render_template('logs.html',
|
||||||
|
logs=[],
|
||||||
|
total_logs=0,
|
||||||
error_message=f"Error parsing logs: {str(e)}")
|
error_message=f"Error parsing logs: {str(e)}")
|
||||||
|
|
||||||
|
@app.route('/api/logs', methods=['POST'])
|
||||||
|
def api_get_logs():
|
||||||
|
"""API endpoint for paginated and filtered logs"""
|
||||||
|
try:
|
||||||
|
log_file_path = '/var/log/haproxy.log'
|
||||||
|
|
||||||
|
if not os.path.exists(log_file_path):
|
||||||
|
return jsonify({'error': 'Log file not found', 'success': False}), 404
|
||||||
|
|
||||||
|
page = request.json.get('page', 1)
|
||||||
|
per_page = request.json.get('per_page', 50)
|
||||||
|
search_query = request.json.get('search', '').lower()
|
||||||
|
exclude_phrases = request.json.get('exclude', [])
|
||||||
|
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if per_page < 1 or per_page > 500:
|
||||||
|
per_page = 50
|
||||||
|
|
||||||
|
print(f"[API] page={page}, per_page={per_page}, search={search_query}, exclude={len(exclude_phrases)}", flush=True)
|
||||||
|
|
||||||
|
# Parse all logs
|
||||||
|
all_logs = parse_log_file(log_file_path)
|
||||||
|
total_logs = len(all_logs)
|
||||||
|
|
||||||
|
# Reverse to show newest first
|
||||||
|
all_logs = all_logs[::-1]
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
filtered_logs = all_logs
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
filtered_logs = [log for log in filtered_logs if search_query in
|
||||||
|
f"{log.get('timestamp', '')} {log.get('ip_address', '')} {log.get('http_method', '')} {log.get('requested_url', '')}".lower()]
|
||||||
|
|
||||||
|
if exclude_phrases:
|
||||||
|
filtered_logs = [log for log in filtered_logs if not any(
|
||||||
|
phrase in f"{log.get('message', '')}" for phrase in exclude_phrases
|
||||||
|
)]
|
||||||
|
|
||||||
|
total_filtered = len(filtered_logs)
|
||||||
|
|
||||||
|
# Paginate
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
paginated_logs = filtered_logs[offset:offset + per_page]
|
||||||
|
|
||||||
|
print(f"[API] total={total_logs}, filtered={total_filtered}, returned={len(paginated_logs)}", flush=True)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'logs': paginated_logs,
|
||||||
|
'page': page,
|
||||||
|
'per_page': per_page,
|
||||||
|
'total': total_logs,
|
||||||
|
'total_filtered': total_filtered,
|
||||||
|
'loaded_count': len(paginated_logs),
|
||||||
|
'has_more': offset + per_page < total_filtered
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Error: {e}", flush=True)
|
||||||
|
return jsonify({'error': str(e), 'success': False}), 500
|
||||||
|
|
||||||
|
|
||||||
@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()
|
||||||
return render_template('home.html',
|
return render_template('home.html',
|
||||||
frontend_count=frontend_count,
|
frontend_count=frontend_count,
|
||||||
backend_count=backend_count,
|
backend_count=backend_count,
|
||||||
acl_count=acl_count,
|
acl_count=acl_count,
|
||||||
layer7_count=layer7_count,
|
layer7_count=layer7_count,
|
||||||
layer4_count=layer4_count)
|
layer4_count=layer4_count)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='::', port=5000, ssl_context=ssl_context, debug=True)
|
app.run(host='::', port=5000, ssl_context=ssl_context, debug=True)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
def parse_log_file(log_file_path):
|
def parse_log_file(log_file_path):
|
||||||
"""
|
"""
|
||||||
Parse HAProxy syslog format and identify security threats.
|
Parse HAProxy syslog format and identify security threats.
|
||||||
@@ -78,7 +79,7 @@ def parse_log_file(log_file_path):
|
|||||||
|
|
||||||
ip_address = ip_match.group(1)
|
ip_address = ip_match.group(1)
|
||||||
|
|
||||||
# Extract date/time in brackets
|
# Extract date/time in brackets (preferred format)
|
||||||
datetime_match = re.search(r'\[(\d{2}/\w+/\d{4}:\d{2}:\d{2}:\d{2})', line)
|
datetime_match = re.search(r'\[(\d{2}/\w+/\d{4}:\d{2}:\d{2}:\d{2})', line)
|
||||||
if datetime_match:
|
if datetime_match:
|
||||||
timestamp = datetime_match.group(1)
|
timestamp = datetime_match.group(1)
|
||||||
@@ -95,10 +96,17 @@ def parse_log_file(log_file_path):
|
|||||||
# Extract HTTP method and URL
|
# Extract HTTP method and URL
|
||||||
http_match = re.search(r'"(\w+)\s+([^\s]+)\s+HTTP', line)
|
http_match = re.search(r'"(\w+)\s+([^\s]+)\s+HTTP', line)
|
||||||
if not http_match:
|
if not http_match:
|
||||||
continue
|
# Fallback: extract entire request line
|
||||||
|
request_match = re.search(r'"([^"]*)"', line)
|
||||||
http_method = http_match.group(1)
|
if request_match:
|
||||||
requested_url = http_match.group(2)
|
request_line = request_match.group(1).split()
|
||||||
|
http_method = request_line[0] if len(request_line) > 0 else 'UNKNOWN'
|
||||||
|
requested_url = request_line[1] if len(request_line) > 1 else '/'
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
http_method = http_match.group(1)
|
||||||
|
requested_url = http_match.group(2)
|
||||||
|
|
||||||
# Detect threats
|
# Detect threats
|
||||||
xss_alert = bool(xss_pattern.search(line))
|
xss_alert = bool(xss_pattern.search(line))
|
||||||
@@ -107,6 +115,24 @@ def parse_log_file(log_file_path):
|
|||||||
put_method = http_method == 'PUT'
|
put_method = http_method == 'PUT'
|
||||||
illegal_resource = status_code == '403'
|
illegal_resource = status_code == '403'
|
||||||
|
|
||||||
|
# Determine status class for UI coloring
|
||||||
|
status_class = 'secondary'
|
||||||
|
if status_code.startswith('2'):
|
||||||
|
status_class = 'success'
|
||||||
|
elif status_code.startswith('3'):
|
||||||
|
status_class = 'info'
|
||||||
|
elif status_code.startswith('4'):
|
||||||
|
status_class = 'warning'
|
||||||
|
if illegal_resource:
|
||||||
|
status_class = 'warning'
|
||||||
|
elif status_code.startswith('5'):
|
||||||
|
status_class = 'danger'
|
||||||
|
|
||||||
|
# Add threat flag if any security issue detected
|
||||||
|
has_threat = xss_alert or sql_alert or webshell_alert or put_method or illegal_resource
|
||||||
|
if has_threat:
|
||||||
|
status_class = 'danger'
|
||||||
|
|
||||||
parsed_entries.append({
|
parsed_entries.append({
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'ip_address': ip_address,
|
'ip_address': ip_address,
|
||||||
@@ -120,16 +146,20 @@ def parse_log_file(log_file_path):
|
|||||||
'put_method': put_method,
|
'put_method': put_method,
|
||||||
'illegal_resource': illegal_resource,
|
'illegal_resource': illegal_resource,
|
||||||
'webshell_alert': webshell_alert,
|
'webshell_alert': webshell_alert,
|
||||||
|
'status_class': status_class,
|
||||||
|
'has_threat': has_threat,
|
||||||
|
'message': f"{frontend}~ {backend} [{status_code}] {http_method} {requested_url}"
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error parsing line: {e}")
|
print(f"[LOG_PARSER] Error parsing line: {e}", flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"Log file not found: {log_file_path}")
|
print(f"[LOG_PARSER] Log file not found: {log_file_path}", flush=True)
|
||||||
return []
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error reading log file: {e}")
|
print(f"[LOG_PARSER] Error reading log file: {e}", flush=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
print(f"[LOG_PARSER] Parsed {len(parsed_entries)} log entries", flush=True)
|
||||||
return parsed_entries
|
return parsed_entries
|
||||||
|
|||||||
@@ -60,10 +60,9 @@ def index():
|
|||||||
# Server header removal
|
# Server header removal
|
||||||
del_server_header = 'del_server_header' in request.form
|
del_server_header = 'del_server_header' in request.form
|
||||||
|
|
||||||
# Backend SSL redirect
|
|
||||||
backend_ssl_redirect = 'backend_ssl_redirect' in request.form
|
backend_ssl_redirect = 'backend_ssl_redirect' in request.form
|
||||||
ssl_redirect_backend_name = request.form.get('ssl_redirect_backend_name', '').strip() if backend_ssl_redirect else ''
|
ssl_redirect_backend_name = request.form.get('ssl_redirect_backend_name', '').strip() if backend_ssl_redirect else ''
|
||||||
ssl_redirect_port = request.form.get('ssl_redirect_port', '80')
|
ssl_redirect_port = request.form.get('ssl_redirect_port', '80') # ✅ POBIERA PORT Z FORMU
|
||||||
|
|
||||||
# Backend servers
|
# Backend servers
|
||||||
backend_server_names = request.form.getlist('backend_server_names[]')
|
backend_server_names = request.form.getlist('backend_server_names[]')
|
||||||
|
|||||||
@@ -1,103 +1,269 @@
|
|||||||
|
/**
|
||||||
|
* HAProxy Logs Management with Security Alerts
|
||||||
|
* Fixed pagination
|
||||||
|
*/
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const filterIp = document.getElementById('filter_ip');
|
let currentPage = 1;
|
||||||
const filterStatus = document.getElementById('filter_status');
|
let perPage = 50;
|
||||||
const filterMethod = document.getElementById('filter_method');
|
let totalLogs = parseInt(document.getElementById('total_count').textContent);
|
||||||
const filterThreats = document.getElementById('filter_threats');
|
let allLoadedLogs = [];
|
||||||
const filterHideStats = document.getElementById('filter_hide_stats');
|
let excludePhrases = [];
|
||||||
const resetBtn = document.getElementById('reset_filters');
|
|
||||||
|
|
||||||
const logsTable = document.getElementById('logs_table');
|
const logsContainer = document.getElementById('logs_container');
|
||||||
if (!logsTable) return; // Exit if no logs
|
const searchFilter = document.getElementById('search_filter');
|
||||||
|
const excludeFilter = document.getElementById('exclude_filter');
|
||||||
|
const excludeBtn = document.getElementById('exclude_btn');
|
||||||
|
const perPageSelect = document.getElementById('logs_per_page');
|
||||||
|
const refreshBtn = document.getElementById('refresh_logs_btn');
|
||||||
|
const prevBtn = document.getElementById('prev_btn');
|
||||||
|
const nextBtn = document.getElementById('next_btn');
|
||||||
|
const loadAllBtn = document.getElementById('load_all_btn');
|
||||||
|
const clearFilterBtn = document.getElementById('clear_filter_btn');
|
||||||
|
const loadedSpan = document.getElementById('loaded_count');
|
||||||
|
const matchSpan = document.getElementById('match_count');
|
||||||
|
const currentPageSpan = document.getElementById('current_page');
|
||||||
|
const totalPagesSpan = document.getElementById('total_pages');
|
||||||
|
|
||||||
const allRows = Array.from(document.querySelectorAll('.log-row'));
|
// Event Listeners
|
||||||
|
searchFilter.addEventListener('keyup', debounce(function() {
|
||||||
|
console.log('[Logs] Search changed');
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogsWithPage();
|
||||||
|
}, 300));
|
||||||
|
|
||||||
// Filter function
|
excludeBtn.addEventListener('click', function() {
|
||||||
function applyFilters() {
|
const phrase = excludeFilter.value.trim();
|
||||||
const ipValue = filterIp.value.toLowerCase();
|
if (phrase) {
|
||||||
const statusValue = filterStatus.value;
|
if (!excludePhrases.includes(phrase)) {
|
||||||
const methodValue = filterMethod.value;
|
excludePhrases.push(phrase);
|
||||||
const showThreats = filterThreats.checked;
|
updateExcludeUI();
|
||||||
const hideStats = filterHideStats.checked;
|
currentPage = 1;
|
||||||
|
loadLogsWithPage();
|
||||||
let visibleCount = 0;
|
|
||||||
let threatCount = 0;
|
|
||||||
let count2xx = 0, count4xx = 0, count5xx = 0;
|
|
||||||
const uniqueIps = new Set();
|
|
||||||
|
|
||||||
allRows.forEach(row => {
|
|
||||||
const ip = row.dataset.ip;
|
|
||||||
const status = row.dataset.status;
|
|
||||||
const method = row.dataset.method;
|
|
||||||
const hasThreat = row.dataset.threats === '1';
|
|
||||||
const url = row.querySelector('td:nth-child(4)').textContent.trim();
|
|
||||||
|
|
||||||
let show = true;
|
|
||||||
|
|
||||||
// IP filter
|
|
||||||
if (ipValue && !ip.includes(ipValue)) {
|
|
||||||
show = false;
|
|
||||||
}
|
}
|
||||||
|
excludeFilter.value = '';
|
||||||
// Status filter
|
}
|
||||||
if (statusValue) {
|
|
||||||
const statusStart = statusValue;
|
|
||||||
if (!status.startsWith(statusStart)) {
|
|
||||||
show = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method filter
|
|
||||||
if (methodValue && method !== methodValue) {
|
|
||||||
show = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Threats filter
|
|
||||||
if (!showThreats && hasThreat) {
|
|
||||||
show = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide /stats filter
|
|
||||||
if (hideStats && url.includes('/stats')) {
|
|
||||||
show = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.style.display = show ? '' : 'none';
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
visibleCount++;
|
|
||||||
if (hasThreat) threatCount++;
|
|
||||||
if (status.startsWith('2')) count2xx++;
|
|
||||||
if (status.startsWith('4')) count4xx++;
|
|
||||||
if (status.startsWith('5')) count5xx++;
|
|
||||||
uniqueIps.add(ip);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
document.getElementById('stat_total').textContent = visibleCount;
|
|
||||||
document.getElementById('stat_threats').textContent = threatCount;
|
|
||||||
document.getElementById('stat_2xx').textContent = count2xx;
|
|
||||||
document.getElementById('stat_4xx').textContent = count4xx;
|
|
||||||
document.getElementById('stat_5xx').textContent = count5xx;
|
|
||||||
document.getElementById('stat_ips').textContent = uniqueIps.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
filterIp.addEventListener('input', applyFilters);
|
|
||||||
filterStatus.addEventListener('change', applyFilters);
|
|
||||||
filterMethod.addEventListener('change', applyFilters);
|
|
||||||
filterThreats.addEventListener('change', applyFilters);
|
|
||||||
filterHideStats.addEventListener('change', applyFilters);
|
|
||||||
|
|
||||||
// Reset button
|
|
||||||
resetBtn.addEventListener('click', function() {
|
|
||||||
filterIp.value = '';
|
|
||||||
filterStatus.value = '';
|
|
||||||
filterMethod.value = '';
|
|
||||||
filterThreats.checked = true;
|
|
||||||
filterHideStats.checked = true;
|
|
||||||
applyFilters();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFilters();
|
excludeFilter.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') excludeBtn.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
clearFilterBtn.addEventListener('click', function() {
|
||||||
|
console.log('[Logs] Clear filters');
|
||||||
|
searchFilter.value = '';
|
||||||
|
excludePhrases = [];
|
||||||
|
excludeFilter.value = '';
|
||||||
|
updateExcludeUI();
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogsWithPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
perPageSelect.addEventListener('change', function() {
|
||||||
|
console.log(`[Logs] Per page changed to ${this.value}`);
|
||||||
|
perPage = parseInt(this.value);
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogsWithPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('click', function() {
|
||||||
|
console.log('[Logs] Refresh clicked');
|
||||||
|
searchFilter.value = '';
|
||||||
|
excludePhrases = [];
|
||||||
|
excludeFilter.value = '';
|
||||||
|
updateExcludeUI();
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogsWithPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
prevBtn.addEventListener('click', function() {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
console.log(`[Logs] Prev button: page ${currentPage} -> ${currentPage - 1}`);
|
||||||
|
currentPage--;
|
||||||
|
loadLogsWithPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nextBtn.addEventListener('click', function() {
|
||||||
|
const totalPages = parseInt(document.getElementById('total_pages').textContent);
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
console.log(`[Logs] Next button: page ${currentPage} -> ${currentPage + 1}`);
|
||||||
|
currentPage++;
|
||||||
|
loadLogsWithPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadAllBtn.addEventListener('click', function() {
|
||||||
|
console.log('[Logs] Load all clicked');
|
||||||
|
perPage = totalLogs > 500 ? 500 : totalLogs;
|
||||||
|
currentPage = 1;
|
||||||
|
perPageSelect.value = perPage;
|
||||||
|
loadLogsWithPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function
|
||||||
|
*/
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(func, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load logs with pagination from API
|
||||||
|
*/
|
||||||
|
function loadLogsWithPage() {
|
||||||
|
console.log(`[Logs] loadLogsWithPage: page=${currentPage}, per_page=${perPage}, search="${searchFilter.value.trim()}", exclude=${excludePhrases.length}`);
|
||||||
|
|
||||||
|
logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4">Loading logs...</td></tr>';
|
||||||
|
|
||||||
|
fetch('/api/logs', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
page: currentPage,
|
||||||
|
per_page: perPage,
|
||||||
|
search: searchFilter.value.trim(),
|
||||||
|
exclude: excludePhrases
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('[Logs] API Response:', data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
allLoadedLogs = data.logs;
|
||||||
|
loadedSpan.textContent = data.loaded_count;
|
||||||
|
totalLogs = data.total;
|
||||||
|
document.getElementById('total_count').textContent = data.total;
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(data.total_filtered / perPage) || 1;
|
||||||
|
totalPagesSpan.textContent = totalPages;
|
||||||
|
matchSpan.textContent = data.total_filtered;
|
||||||
|
currentPageSpan.textContent = data.page;
|
||||||
|
|
||||||
|
renderLogs(data.logs);
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
prevBtn.disabled = currentPage === 1;
|
||||||
|
nextBtn.disabled = !data.has_more;
|
||||||
|
|
||||||
|
console.log(`[Logs] Updated: page ${data.page}/${totalPages}, has_more=${data.has_more}, prev_disabled=${prevBtn.disabled}, next_disabled=${nextBtn.disabled}`);
|
||||||
|
} else {
|
||||||
|
showError(data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error('[Logs] Error:', e);
|
||||||
|
showError('Failed to load logs: ' + e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render logs as table rows
|
||||||
|
*/
|
||||||
|
function renderLogs(logs) {
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4">No logs found</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logsContainer.innerHTML = logs.map((entry) => {
|
||||||
|
const threat_badges = [];
|
||||||
|
if (entry.xss_alert) threat_badges.push('<span class="badge bg-danger me-1">XSS</span>');
|
||||||
|
if (entry.sql_alert) threat_badges.push('<span class="badge bg-danger me-1">SQL</span>');
|
||||||
|
if (entry.webshell_alert) threat_badges.push('<span class="badge bg-danger me-1">SHELL</span>');
|
||||||
|
if (entry.put_method) threat_badges.push('<span class="badge bg-danger me-1">PUT</span>');
|
||||||
|
if (entry.illegal_resource) threat_badges.push('<span class="badge bg-warning me-1">403</span>');
|
||||||
|
|
||||||
|
const threat_html = threat_badges.length > 0 ? `<div class="mb-2">${threat_badges.join('')}</div>` : '';
|
||||||
|
|
||||||
|
let row_class = '';
|
||||||
|
if (entry.has_threat) {
|
||||||
|
row_class = 'table-danger';
|
||||||
|
} else if (entry.status_code.startsWith('5')) {
|
||||||
|
row_class = 'table-danger';
|
||||||
|
} else if (entry.status_code.startsWith('4')) {
|
||||||
|
row_class = 'table-warning';
|
||||||
|
} else if (entry.status_code.startsWith('2')) {
|
||||||
|
row_class = 'table-light';
|
||||||
|
} else {
|
||||||
|
row_class = 'table-light';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="${row_class}" style="font-family: monospace; font-size: 11px;">
|
||||||
|
<td>
|
||||||
|
${threat_html}
|
||||||
|
<small style="color: #0066cc;">${escapeHtml(entry.timestamp)}</small><br>
|
||||||
|
<small style="color: #666;">${escapeHtml(entry.ip_address)}</small>
|
||||||
|
<strong style="color: #333;">${escapeHtml(entry.http_method)}</strong>
|
||||||
|
<code style="color: #333;">${escapeHtml(entry.requested_url)}</code>
|
||||||
|
<span class="badge bg-dark" style="color: white; margin-left: 5px;">${escapeHtml(entry.status_code)}</span>
|
||||||
|
<br>
|
||||||
|
<small style="color: #666;">${escapeHtml(entry.frontend)}~ ${escapeHtml(entry.backend)}</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update exclude UI
|
||||||
|
*/
|
||||||
|
function updateExcludeUI() {
|
||||||
|
if (excludePhrases.length > 0) {
|
||||||
|
const tags = excludePhrases.map((phrase, idx) => `
|
||||||
|
<span class="badge bg-warning text-dark me-2" style="cursor: pointer;" onclick="window.removeExcludePhrase(${idx})">
|
||||||
|
${escapeHtml(phrase)} <i class="bi bi-x"></i>
|
||||||
|
</span>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'small mt-2';
|
||||||
|
container.innerHTML = `<strong>Hiding:</strong> ${tags}`;
|
||||||
|
|
||||||
|
const existing = document.getElementById('exclude_ui');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
container.id = 'exclude_ui';
|
||||||
|
excludeFilter.parentElement.parentElement.after(container);
|
||||||
|
} else {
|
||||||
|
const existing = document.getElementById('exclude_ui');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove exclude phrase
|
||||||
|
*/
|
||||||
|
window.removeExcludePhrase = function(idx) {
|
||||||
|
console.log(`[Logs] Remove exclude phrase at index ${idx}`);
|
||||||
|
excludePhrases.splice(idx, 1);
|
||||||
|
updateExcludeUI();
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogsWithPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show error
|
||||||
|
*/
|
||||||
|
function showError(msg) {
|
||||||
|
logsContainer.innerHTML = `<tr><td class="alert alert-danger mb-0">${escapeHtml(msg)}</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML
|
||||||
|
*/
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''};
|
||||||
|
return (text || '').replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
console.log('[Logs] Initial load');
|
||||||
|
loadLogsWithPage();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
|
||||||
<i class="bi bi-{% if message_type == 'success' %}check-circle{% elif message_type == 'danger' %}exclamation-circle{% else %}info-circle{% endif %} me-2"></i>
|
<i class="bi bi-{% if message_type == 'success' %}check-circle{% elif message_type == 'danger' %}exclamation-circle{% elif message_type == 'warning' %}exclamation-triangle{% else %}info-circle{% endif %} me-2"></i>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,12 +98,13 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="lb_method" class="form-label">Load Balancing Method</label>
|
<label for="lb_method" class="form-label">Load Balancing Method</label>
|
||||||
<select class="form-select" id="lb_method" name="lb_method" required>
|
<select class="form-select" id="lb_method" name="lb_method" required>
|
||||||
|
<option value="no-lb">No Load Balancing (single host)</option>
|
||||||
<option value="roundrobin">Round Robin</option>
|
<option value="roundrobin">Round Robin</option>
|
||||||
<option value="leastconn">Least Connections</option>
|
<option value="leastconn">Least Connections</option>
|
||||||
<option value="source">Source IP Hash</option>
|
<option value="source">Source IP Hash</option>
|
||||||
<option value="uri">URI Hash</option>
|
<option value="uri">URI Hash</option>
|
||||||
<option value="static-rr">Static Round Robin (WRR)</option>
|
<option value="static-rr">Static Round Robin (WRR)</option>
|
||||||
<option value="no-lb">No Load Balancing (single host)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Backend SSL Redirect -->
|
<!-- HTTP to HTTPS Redirect -->
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -147,16 +148,23 @@
|
|||||||
<label class="form-check-label" for="backend_ssl_redirect">
|
<label class="form-check-label" for="backend_ssl_redirect">
|
||||||
<i class="bi bi-arrow-repeat me-1"></i>Add HTTP Redirect to HTTPS
|
<i class="bi bi-arrow-repeat me-1"></i>Add HTTP Redirect to HTTPS
|
||||||
</label>
|
</label>
|
||||||
<small class="text-muted d-block">Creates additional frontend on port 80</small>
|
<small class="text-muted d-block">Creates additional frontend to redirect HTTP traffic to HTTPS</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 mb-3 d-none" id="backend_ssl_fields">
|
<div class="row g-3 mb-3 d-none" id="backend_ssl_fields">
|
||||||
<div class="col-md-12">
|
<div class="col-md-6">
|
||||||
<label for="ssl_redirect_backend_name" class="form-label">Redirect Backend Name</label>
|
<label for="ssl_redirect_backend_name" class="form-label">Redirect Backend Name</label>
|
||||||
<input type="text" class="form-control" id="ssl_redirect_backend_name"
|
<input type="text" class="form-control" id="ssl_redirect_backend_name"
|
||||||
name="ssl_redirect_backend_name" placeholder="e.g. redirect">
|
name="ssl_redirect_backend_name" placeholder="e.g. redirect">
|
||||||
|
<small class="text-muted">Name for the redirect backend</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="ssl_redirect_port" class="form-label">HTTP Redirect Port</label>
|
||||||
|
<input type="number" class="form-control" id="ssl_redirect_port"
|
||||||
|
name="ssl_redirect_port" value="80" min="1" max="65535">
|
||||||
|
<small class="text-muted">Default: 80 (leave empty for standard)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<nav aria-label="breadcrumb" class="mb-3">
|
<nav aria-label="breadcrumb" class="mb-3">
|
||||||
<ol class="breadcrumb mb-0">
|
<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"><a href="{{ url_for('main.index') }}"><i class="bi bi-house"></i></a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Access Logs</li>
|
<li class="breadcrumb-item active" aria-current="page">Logs</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -17,192 +17,85 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header bg-primary text-white">
|
<div class="card-header bg-primary text-white">
|
||||||
<h5 class="mb-0"><i class="bi bi-file-text me-2"></i>HAProxy Access Logs</h5>
|
<h5 class="mb-0"><i class="bi bi-file-earmark-text me-2"></i>HAProxy Logs</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
{% if error_message %}
|
{% if error_message %}
|
||||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
<div class="alert alert-warning">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>{{ error_message }}
|
||||||
<strong>Warning:</strong> {{ error_message }}
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if logs and logs|length > 0 %}
|
<!-- Controls Row -->
|
||||||
<div class="row mb-3 g-2">
|
<div class="row g-2 mb-3">
|
||||||
<div class="col-auto">
|
<div class="col-md-3">
|
||||||
<input type="text" class="form-control form-control-sm" id="filter_ip" placeholder="Filter by IP">
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" class="form-control" id="search_filter" placeholder="Search logs...">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-md-2">
|
||||||
<select class="form-select form-select-sm" id="filter_status" style="width: auto;">
|
<button class="btn btn-sm btn-outline-secondary w-100" id="clear_filter_btn" title="Clear search">
|
||||||
<option value="">All Status</option>
|
<i class="bi bi-x-circle me-1"></i>Clear
|
||||||
<option value="2">2xx (Success)</option>
|
</button>
|
||||||
<option value="3">3xx (Redirect)</option>
|
</div>
|
||||||
<option value="4">4xx (Client Error)</option>
|
<div class="col-md-2">
|
||||||
<option value="5">5xx (Server Error)</option>
|
<select class="form-select form-select-sm" id="logs_per_page">
|
||||||
|
<option value="25" selected>25 per page</option>
|
||||||
|
<option value="50">50 per page</option>
|
||||||
|
<option value="100">100 per page</option>
|
||||||
|
<option value="200">200 per page</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
|
||||||
<select class="form-select form-select-sm" id="filter_method" style="width: auto;">
|
|
||||||
<option value="">All Methods</option>
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
<option value="DELETE">DELETE</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="filter_threats" checked>
|
|
||||||
<label class="form-check-label" for="filter_threats" style="margin-top: 5px;">
|
|
||||||
Show Threats
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="filter_hide_stats" checked>
|
|
||||||
<label class="form-check-label" for="filter_hide_stats" style="margin-top: 5px;">
|
|
||||||
Hide /stats
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto ms-auto">
|
|
||||||
<button class="btn btn-sm btn-secondary" id="reset_filters">Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3 g-2">
|
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<div class="card text-center" style="font-size: 0.9rem;">
|
<button class="btn btn-sm btn-primary w-100" id="refresh_logs_btn">
|
||||||
<div class="card-body p-2">
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
<div class="text-muted small">Total</div>
|
</button>
|
||||||
<strong id="stat_total">{{ logs|length }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-3">
|
||||||
<div class="card text-center text-danger" style="font-size: 0.9rem;">
|
<div class="input-group input-group-sm">
|
||||||
<div class="card-body p-2">
|
<span class="input-group-text"><i class="bi bi-funnel"></i></span>
|
||||||
<div class="text-muted small">Threats</div>
|
<input type="text" class="form-control" id="exclude_filter" placeholder="Hide phrase (e.g. /stats)">
|
||||||
<strong id="stat_threats">0</strong>
|
<button class="btn btn-outline-warning btn-sm" id="exclude_btn" type="button">Hide</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<div class="card text-center text-success" style="font-size: 0.9rem;">
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<div class="text-muted small">2xx</div>
|
|
||||||
<strong id="stat_2xx">0</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<div class="card text-center text-warning" style="font-size: 0.9rem;">
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<div class="text-muted small">4xx</div>
|
|
||||||
<strong id="stat_4xx">0</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<div class="card text-center text-danger" style="font-size: 0.9rem;">
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<div class="text-muted small">5xx</div>
|
|
||||||
<strong id="stat_5xx">0</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<div class="card text-center" style="font-size: 0.9rem;">
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<div class="text-muted small">Unique IPs</div>
|
|
||||||
<strong id="stat_ips">0</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<!-- Statistics -->
|
||||||
|
<div class="alert alert-info small mb-3">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
<strong>Total:</strong> <span id="total_count">{{ total_logs|default(0) }}</span> logs |
|
||||||
|
<strong>Loaded:</strong> <span id="loaded_count">{{ loaded_count|default(0) }}</span> |
|
||||||
|
<strong>Displayed:</strong> <span id="match_count">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<!-- Logs Container (Dark Theme) -->
|
||||||
<table class="table table-striped table-hover">
|
<div id="logs_container_wrapper" style="max-height: 650px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; background: #0d1117;">
|
||||||
<thead class="table-dark">
|
<table class="table table-sm table-dark mb-0" id="logs_table">
|
||||||
<tr>
|
<tbody id="logs_container">
|
||||||
<th>Timestamp</th>
|
<tr><td class="text-center text-muted py-4">Loading logs...</td></tr>
|
||||||
<th>IP Address</th>
|
|
||||||
<th>HTTP Method</th>
|
|
||||||
<th>Requested URL</th>
|
|
||||||
<th>Status Code</th>
|
|
||||||
<th>Alerts</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="logs_table">
|
|
||||||
{% for entry in logs %}
|
|
||||||
<tr class="log-row"
|
|
||||||
data-ip="{{ entry['ip_address'] }}"
|
|
||||||
data-status="{{ entry['status_code'] }}"
|
|
||||||
data-method="{{ entry['http_method'] }}"
|
|
||||||
data-threats="{% if entry['xss_alert'] or entry['sql_alert'] or entry['put_method'] or entry['webshell_alert'] or entry['illegal_resource'] %}1{% else %}0{% endif %}">
|
|
||||||
<td>{{ entry['timestamp'] }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-secondary">{{ entry['ip_address'] }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-primary">{{ entry['http_method'] }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-truncate" style="max-width: 300px;" title="{{ entry['requested_url'] }}">
|
|
||||||
{{ entry['requested_url'] }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge {% if entry['status_code']|int >= 200 and entry['status_code']|int < 300 %}bg-success{% elif entry['status_code']|int >= 300 and entry['status_code']|int < 400 %}bg-secondary{% elif entry['status_code']|int >= 400 and entry['status_code']|int < 500 %}bg-warning{% else %}bg-danger{% endif %}">
|
|
||||||
{{ entry['status_code'] }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if entry['xss_alert'] %}
|
|
||||||
<span class="badge bg-danger">XSS</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry['sql_alert'] %}
|
|
||||||
<span class="badge bg-danger">SQL</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry['put_method'] %}
|
|
||||||
<span class="badge bg-warning">PUT</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry['webshell_alert'] %}
|
|
||||||
<span class="badge bg-danger">Webshell</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry['illegal_resource'] %}
|
|
||||||
<span class="badge bg-warning">403</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% elif logs %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>No log entries match your filters.
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
<h4 class="alert-heading"><i class="bi bi-exclamation-circle me-2"></i>No logs available</h4>
|
|
||||||
<hr>
|
|
||||||
<p class="mb-2"><strong>Possible reasons:</strong></p>
|
|
||||||
<ul class="mb-0">
|
|
||||||
<li>Log file does not exist or is not readable</li>
|
|
||||||
<li>HAProxy is not configured to log requests</li>
|
|
||||||
<li>Log file path is incorrect in configuration</li>
|
|
||||||
<li>No requests have been processed yet</li>
|
|
||||||
</ul>
|
|
||||||
<hr class="my-2">
|
|
||||||
<p class="small text-muted mb-0">Check HAProxy configuration and log file permissions.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="mt-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<small class="text-muted">
|
||||||
|
Page <span id="current_page">1</span> / <span id="total_pages">1</span>
|
||||||
|
</small>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button class="btn btn-outline-primary" id="prev_btn" disabled>
|
||||||
|
<i class="bi bi-chevron-left"></i> Prev
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-primary" id="next_btn">
|
||||||
|
Next <i class="bi bi-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" id="load_all_btn">
|
||||||
|
<i class="bi bi-download"></i> All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ def frontend_exists_at_port(frontend_ip, frontend_port):
|
|||||||
|
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip().startswith('frontend'):
|
if line.strip().startswith('frontend'):
|
||||||
# Szukaj bind line
|
|
||||||
for j in range(i+1, min(i+10, len(lines))):
|
for j in range(i+1, min(i+10, len(lines))):
|
||||||
if lines[j].strip().startswith('bind'):
|
if lines[j].strip().startswith('bind'):
|
||||||
bind_info = lines[j].strip().split(' ', 1)[1]
|
bind_info = lines[j].strip().split(' ', 1)[1]
|
||||||
if f"{frontend_ip}:{frontend_port}" in bind_info:
|
bind_part = bind_info.split(' ssl ')[0].strip()
|
||||||
|
if f"{frontend_ip}:{frontend_port}" in bind_part:
|
||||||
return line.strip().split(' ', 1)[1] # Zwróć nazwę frontendu
|
return line.strip().split(' ', 1)[1] # Zwróć nazwę frontendu
|
||||||
elif lines[j].strip().startswith('frontend') or lines[j].strip().startswith('backend'):
|
elif lines[j].strip().startswith('frontend') or lines[j].strip().startswith('backend'):
|
||||||
break
|
break
|
||||||
@@ -32,7 +32,6 @@ def frontend_exists_at_port(frontend_ip, frontend_port):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name):
|
def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name):
|
||||||
"""Dodaj ACL i use_backend do istniejącego frontendu"""
|
|
||||||
if not os.path.exists(HAPROXY_CFG):
|
if not os.path.exists(HAPROXY_CFG):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -40,7 +39,6 @@ def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name):
|
|||||||
with open(HAPROXY_CFG, 'r') as f:
|
with open(HAPROXY_CFG, 'r') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
# Znajdź frontend
|
|
||||||
frontend_idx = -1
|
frontend_idx = -1
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if 'frontend' in line and frontend_name in line:
|
if 'frontend' in line and frontend_name in line:
|
||||||
@@ -48,19 +46,19 @@ def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if frontend_idx == -1:
|
if frontend_idx == -1:
|
||||||
|
print(f"[HAPROXY_CONFIG] Frontend '{frontend_name}' not found", flush=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Sprawdź czy ACL już istnieje
|
|
||||||
for line in lines[frontend_idx:]:
|
for line in lines[frontend_idx:]:
|
||||||
if acl_name in line and 'acl' in line:
|
if acl_name in line and 'acl' in line:
|
||||||
return True # Już istnieje
|
print(f"[HAPROXY_CONFIG] ACL '{acl_name}' already exists", flush=True)
|
||||||
|
return True
|
||||||
if line.strip().startswith('backend'):
|
if line.strip().startswith('backend'):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Znajdź ostatnią linię ACL/use_backend w tym frontendzie
|
|
||||||
insert_idx = frontend_idx + 1
|
insert_idx = frontend_idx + 1
|
||||||
for i in range(frontend_idx + 1, len(lines)):
|
for i in range(frontend_idx + 1, len(lines)):
|
||||||
if lines[i].strip().startswith('backend'):
|
if lines[i].strip().startswith('backend') or lines[i].strip().startswith('frontend'):
|
||||||
insert_idx = i
|
insert_idx = i
|
||||||
break
|
break
|
||||||
if 'use_backend' in lines[i] or 'default_backend' in lines[i]:
|
if 'use_backend' in lines[i] or 'default_backend' in lines[i]:
|
||||||
@@ -76,6 +74,7 @@ def add_acl_to_frontend(frontend_name, acl_name, hostname, backend_name):
|
|||||||
with open(HAPROXY_CFG, 'w') as f:
|
with open(HAPROXY_CFG, 'w') as f:
|
||||||
f.writelines(lines)
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print(f"[HAPROXY_CONFIG] ACL '{acl_name}' added to frontend '{frontend_name}'", flush=True)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[HAPROXY_CONFIG] Error adding ACL: {e}", flush=True)
|
print(f"[HAPROXY_CONFIG] Error adding ACL: {e}", flush=True)
|
||||||
@@ -158,7 +157,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
|
|||||||
existing_frontend = frontend_exists_at_port(frontend_ip, frontend_port)
|
existing_frontend = frontend_exists_at_port(frontend_ip, frontend_port)
|
||||||
|
|
||||||
if existing_frontend:
|
if existing_frontend:
|
||||||
# Frontend już istnieje - dodaj tylko backend + ACL
|
|
||||||
print(f"[HAPROXY] Found existing frontend '{existing_frontend}' at {frontend_ip}:{frontend_port}", flush=True)
|
print(f"[HAPROXY] Found existing frontend '{existing_frontend}' at {frontend_ip}:{frontend_port}", flush=True)
|
||||||
|
|
||||||
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
|
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
|
||||||
@@ -198,16 +196,53 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
|
|||||||
else:
|
else:
|
||||||
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
|
haproxy_cfg.write(f" server {server_name} {server_ip}:{server_port}{maxconn_str}\n")
|
||||||
|
|
||||||
# Dodaj ACL do istniejącego frontendu
|
|
||||||
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"is_{unique_backend_name}"
|
acl_name_sanitized = f"is_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"is_{unique_backend_name}"
|
||||||
add_acl_to_frontend(existing_frontend, acl_name_sanitized, frontend_hostname or 'localhost', unique_backend_name)
|
add_acl_to_frontend(existing_frontend, acl_name_sanitized, frontend_hostname or 'localhost', unique_backend_name)
|
||||||
|
|
||||||
|
# ===== REDIRECT HTTP→HTTPS (jeśli zaznaczony) =====
|
||||||
|
if backend_ssl_redirect and ssl_redirect_backend_name:
|
||||||
|
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"{ssl_redirect_backend_name}_redirect"
|
||||||
|
|
||||||
|
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
|
||||||
|
|
||||||
|
if existing_http_frontend:
|
||||||
|
print(f"[HAPROXY] Adding redirect ACL to existing HTTP frontend '{existing_http_frontend}'", flush=True)
|
||||||
|
|
||||||
|
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
|
||||||
|
haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n")
|
||||||
|
haproxy_cfg.write(f" mode http\n")
|
||||||
|
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
|
||||||
|
|
||||||
|
if frontend_hostname:
|
||||||
|
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
|
||||||
|
add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name)
|
||||||
|
else:
|
||||||
|
print(f"[HAPROXY] Creating new HTTP redirect frontend at {frontend_ip}:{ssl_redirect_port}", flush=True)
|
||||||
|
|
||||||
|
with open(HAPROXY_CFG, 'a') as haproxy_cfg:
|
||||||
|
generic_http_redirect_name = f"http_redirect_frontend"
|
||||||
|
|
||||||
|
haproxy_cfg.write(f"\nfrontend {generic_http_redirect_name}\n")
|
||||||
|
haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n")
|
||||||
|
haproxy_cfg.write(f" mode http\n")
|
||||||
|
|
||||||
|
if frontend_hostname:
|
||||||
|
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
|
||||||
|
haproxy_cfg.write(f" acl {acl_name_redirect} hdr(host) -i {frontend_hostname}\n")
|
||||||
|
haproxy_cfg.write(f" use_backend {unique_redirect_backend_name} if {acl_name_redirect}\n")
|
||||||
|
else:
|
||||||
|
haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n")
|
||||||
|
|
||||||
|
# Redirect backend
|
||||||
|
haproxy_cfg.write(f"\nbackend {unique_redirect_backend_name}\n")
|
||||||
|
haproxy_cfg.write(f" mode http\n")
|
||||||
|
haproxy_cfg.write(f" redirect scheme https code 301 if !{{ ssl_fc }}\n")
|
||||||
|
|
||||||
return f"Backend added to existing frontend"
|
return f"Backend added to existing frontend"
|
||||||
|
|
||||||
# ===== TWORZENIE NOWEGO FRONTENDU (GENERYCZNE NAZWY) =====
|
# ===== TWORZENIE NOWEGO FRONTENDU (GENERYCZNE NAZWY) =====
|
||||||
# Generuj generyczną nazwę frontendu
|
# Generuj generyczną nazwę frontendu
|
||||||
generic_frontend_name = f"https_frontend" if use_ssl else f"http_frontend"
|
generic_frontend_name = f"https_frontend" if use_ssl else f"http_frontend"
|
||||||
generic_http_redirect_name = f"http_redirect_frontend"
|
|
||||||
|
|
||||||
print(f"[HAPROXY] Creating new frontend '{generic_frontend_name}' at {frontend_ip}:{frontend_port}", flush=True)
|
print(f"[HAPROXY] Creating new frontend '{generic_frontend_name}' at {frontend_ip}:{frontend_port}", flush=True)
|
||||||
|
|
||||||
@@ -314,13 +349,14 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
|
|||||||
|
|
||||||
# ===== REDIRECT HTTP -> HTTPS (GENERIC NAME) =====
|
# ===== REDIRECT HTTP -> HTTPS (GENERIC NAME) =====
|
||||||
if backend_ssl_redirect and ssl_redirect_backend_name:
|
if backend_ssl_redirect and ssl_redirect_backend_name:
|
||||||
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else ssl_redirect_backend_name
|
unique_redirect_backend_name = f"{ssl_redirect_backend_name}_redirect_{sanitize_name(frontend_hostname)}" if frontend_hostname else f"{ssl_redirect_backend_name}_redirect"
|
||||||
|
|
||||||
# Check if HTTP redirect frontend exists
|
# Check if HTTP frontend exists
|
||||||
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
|
existing_http_frontend = frontend_exists_at_port(frontend_ip, ssl_redirect_port)
|
||||||
|
|
||||||
if not existing_http_frontend:
|
if not existing_http_frontend:
|
||||||
# Utwórz nowy HTTP redirect frontend (generic name)
|
generic_http_redirect_name = f"http_redirect_frontend"
|
||||||
|
|
||||||
haproxy_cfg.write(f"\nfrontend {generic_http_redirect_name}\n")
|
haproxy_cfg.write(f"\nfrontend {generic_http_redirect_name}\n")
|
||||||
haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n")
|
haproxy_cfg.write(f" bind {frontend_ip}:{ssl_redirect_port}\n")
|
||||||
haproxy_cfg.write(f" mode http\n")
|
haproxy_cfg.write(f" mode http\n")
|
||||||
@@ -332,7 +368,6 @@ def update_haproxy_config(frontend_name, frontend_ip, frontend_port, lb_method,
|
|||||||
else:
|
else:
|
||||||
haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n")
|
haproxy_cfg.write(f" default_backend {unique_redirect_backend_name}\n")
|
||||||
else:
|
else:
|
||||||
# Dodaj ACL do istniejącego HTTP frontendu
|
|
||||||
if frontend_hostname:
|
if frontend_hostname:
|
||||||
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
|
acl_name_redirect = f"is_{sanitize_name(frontend_hostname)}_redirect"
|
||||||
add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name)
|
add_acl_to_frontend(existing_http_frontend, acl_name_redirect, frontend_hostname, unique_redirect_backend_name)
|
||||||
|
|||||||
Reference in New Issue
Block a user