Compare commits

9 Commits

Author SHA1 Message Date
gru
32ef62e4ac Merge pull request 'new_functions' (#2) from new_functions into master
Reviewed-on: #2
2025-11-04 09:04:37 +01:00
Mateusz Gruszczyński
04acb4ac21 fixes 2025-11-04 08:59:16 +01:00
Mateusz Gruszczyński
9949e34d68 fixes 2025-11-04 08:54:29 +01:00
Mateusz Gruszczyński
0a027bbebd fixes 2025-11-04 08:51:11 +01:00
Mateusz Gruszczyński
3e7861f489 fixes 2025-11-04 08:47:17 +01:00
Mateusz Gruszczyński
da1af612ef fixes 2025-11-04 08:43:42 +01:00
Mateusz Gruszczyński
370c7099f5 fixes 2025-11-04 08:40:06 +01:00
Mateusz Gruszczyński
27f9984574 fixes 2025-11-04 08:26:41 +01:00
Mateusz Gruszczyński
34c84f1115 fixes 2025-11-04 08:20:39 +01:00
7 changed files with 509 additions and 321 deletions

89
app.py
View File

@@ -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,7 +66,6 @@ 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')
@@ -88,41 +84,103 @@ try:
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) return render_template('logs.html',
logs=initial_logs,
total_logs=total_logs,
loaded_count=len(initial_logs))
except Exception as e: except Exception as e:
return render_template('logs.html', return render_template('logs.html',
logs=[], 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():
@@ -134,6 +192,5 @@ def home():
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)

View File

@@ -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

View File

@@ -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[]')

View File

@@ -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 = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return (text || '').replace(/[&<>"']/g, m => map[m]);
}
// Initial load
console.log('[Logs] Initial load');
loadLogsWithPage();
}); });

View File

@@ -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>

View File

@@ -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 %} <!-- Pagination -->
<div class="alert alert-info"> <div class="mt-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<i class="bi bi-info-circle me-2"></i>No log entries match your filters. <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>
{% else %} </div>
<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 %}
</div> </div>
</div> </div>

View File

@@ -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)