feat: Add security mechanisms list and GeoIP stats to admin dashboard

- New 'Mechanisms' tab listing all security features with star ratings (5★=critical)
- New 'GeoIP' tab with blocking statistics (daily/monthly/yearly/total)
- Country breakdown with flags for blocked connections
- Status indicators for each security mechanism

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-14 22:16:33 +01:00
parent 341ce29aa9
commit e9e37796c7
2 changed files with 334 additions and 3 deletions

61
app.py
View File

@ -10615,12 +10615,71 @@ def admin_security():
'alert_breakdown': {a.alert_type: a.count for a in alert_breakdown}
}
# GeoIP stats
from security_service import _get_geoip_enabled
geoip_enabled = _get_geoip_enabled()
geoip_stats = {'today': 0, 'this_month': 0, 'this_year': 0, 'total': 0, 'by_country': []}
if geoip_enabled:
today = datetime.now().date()
first_of_month = today.replace(day=1)
first_of_year = today.replace(month=1, day=1)
# Count geo_blocked alerts
geoip_stats['today'] = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geo_blocked',
func.date(SecurityAlert.created_at) == today
).count()
geoip_stats['this_month'] = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geo_blocked',
func.date(SecurityAlert.created_at) >= first_of_month
).count()
geoip_stats['this_year'] = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geo_blocked',
func.date(SecurityAlert.created_at) >= first_of_year
).count()
geoip_stats['total'] = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geo_blocked'
).count()
# Country breakdown (from details JSON)
country_flags = {
'RU': ('🇷🇺', 'Rosja'), 'CN': ('🇨🇳', 'Chiny'), 'KP': ('🇰🇵', 'Korea Płn.'),
'IR': ('🇮🇷', 'Iran'), 'BY': ('🇧🇾', 'Białoruś'), 'SY': ('🇸🇾', 'Syria'),
'VE': ('🇻🇪', 'Wenezuela'), 'CU': ('🇨🇺', 'Kuba')
}
geo_alerts = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geo_blocked'
).all()
country_counts = {}
for alert in geo_alerts:
if alert.details and 'country' in alert.details:
country = alert.details['country']
if country:
country_counts[country] = country_counts.get(country, 0) + 1
# Sort by count descending
sorted_countries = sorted(country_counts.items(), key=lambda x: x[1], reverse=True)
for code, count in sorted_countries:
flag, name = country_flags.get(code, ('🏴', code))
geoip_stats['by_country'].append({
'code': code, 'flag': flag, 'name': name, 'count': count
})
return render_template(
'admin/security_dashboard.html',
audit_logs=audit_logs,
alerts=alerts,
locked_accounts=locked_accounts,
stats=stats
stats=stats,
geoip_enabled=geoip_enabled,
geoip_stats=geoip_stats
)
finally:
db.close()

View File

@ -236,6 +236,116 @@
.tab-content.active {
display: block;
}
/* Security mechanisms styles */
.mechanisms-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.mechanism-item {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--background);
border-radius: var(--radius);
border-left: 4px solid transparent;
}
.mechanism-item.stars-5 { border-left-color: #22c55e; }
.mechanism-item.stars-4 { border-left-color: #3b82f6; }
.mechanism-item.stars-3 { border-left-color: #f59e0b; }
.mechanism-stars {
font-size: 1rem;
color: #fbbf24;
min-width: 100px;
letter-spacing: 2px;
}
.mechanism-name {
flex: 1;
font-weight: 500;
color: var(--text-primary);
}
.mechanism-desc {
flex: 2;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.mechanism-status {
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius);
font-weight: 500;
}
.mechanism-status.active {
background: #dcfce7;
color: #166534;
}
.mechanism-status.inactive {
background: #fee2e2;
color: #991b1b;
}
/* GeoIP stats */
.geoip-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.geoip-stat {
background: var(--background);
padding: var(--spacing-lg);
border-radius: var(--radius);
text-align: center;
}
.geoip-stat-value {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-primary);
}
.geoip-stat-label {
font-size: var(--font-size-xs);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.country-breakdown {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-top: var(--spacing-lg);
}
.country-badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-md);
background: var(--background);
border-radius: var(--radius);
font-size: var(--font-size-sm);
}
.country-flag {
font-size: 1.2em;
}
.country-count {
font-weight: 600;
color: var(--text-primary);
}
</style>
{% endblock %}
@ -267,13 +377,175 @@
<!-- Tabs -->
<div class="tabs">
<button class="tab active" onclick="showTab('alerts')">Alerty bezpieczeństwa</button>
<button class="tab active" onclick="showTab('mechanisms')">Mechanizmy</button>
<button class="tab" onclick="showTab('geoip')">GeoIP</button>
<button class="tab" onclick="showTab('alerts')">Alerty</button>
<button class="tab" onclick="showTab('audit')">Audit log</button>
<button class="tab" onclick="showTab('locked')">Zablokowane konta</button>
</div>
<!-- Mechanisms Tab -->
<div id="tab-mechanisms" class="tab-content active">
<div class="section">
<div class="section-header">
<h2>🔐 Mechanizmy bezpieczeństwa</h2>
</div>
<p style="color: var(--text-secondary); margin-bottom: var(--spacing-lg);">
Lista wszystkich aktywnych mechanizmów ochrony systemu, posortowana według ważności (5★ = krytyczne).
</p>
<div class="mechanisms-list">
<!-- 5 stars - CRITICAL -->
<div class="mechanism-item stars-5">
<span class="mechanism-stars">★★★★★</span>
<span class="mechanism-name">Uwierzytelnianie dwuskładnikowe (2FA)</span>
<span class="mechanism-desc">TOTP przez aplikacje mobilne (Google/Microsoft Authenticator)</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-5">
<span class="mechanism-stars">★★★★★</span>
<span class="mechanism-name">Ochrona przed CSRF</span>
<span class="mechanism-desc">Tokeny CSRF w formularzach (Flask-WTF)</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-5">
<span class="mechanism-stars">★★★★★</span>
<span class="mechanism-name">Szyfrowanie połączeń (HTTPS/TLS)</span>
<span class="mechanism-desc">Let's Encrypt SSL z automatycznym odnowieniem</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-5">
<span class="mechanism-stars">★★★★★</span>
<span class="mechanism-name">Hashowanie haseł</span>
<span class="mechanism-desc">Bezpieczne przechowywanie z Werkzeug (bcrypt)</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-5">
<span class="mechanism-stars">★★★★★</span>
<span class="mechanism-name">Ochrona przed SQL Injection</span>
<span class="mechanism-desc">Parametryzowane zapytania przez SQLAlchemy ORM</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-5">
<span class="mechanism-stars">★★★★★</span>
<span class="mechanism-name">Ochrona przed XSS</span>
<span class="mechanism-desc">Automatyczne escapowanie w szablonach Jinja2</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<!-- 4 stars - IMPORTANT -->
<div class="mechanism-item stars-4">
<span class="mechanism-stars">★★★★☆</span>
<span class="mechanism-name">Blokowanie geograficzne (GeoIP)</span>
<span class="mechanism-desc">Blokada krajów wysokiego ryzyka: RU, CN, KP, IR, BY, SY, VE, CU</span>
<span class="mechanism-status {{ 'active' if geoip_enabled else 'inactive' }}">{{ 'Aktywne' if geoip_enabled else 'Nieaktywne' }}</span>
</div>
<div class="mechanism-item stars-4">
<span class="mechanism-stars">★★★★☆</span>
<span class="mechanism-name">Rate Limiting</span>
<span class="mechanism-desc">Ograniczenie żądań przez Flask-Limiter z Redis</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-4">
<span class="mechanism-stars">★★★★☆</span>
<span class="mechanism-name">Blokada konta</span>
<span class="mechanism-desc">Automatyczna blokada po 5 nieudanych logowaniach</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-4">
<span class="mechanism-stars">★★★★☆</span>
<span class="mechanism-name">Audit Log</span>
<span class="mechanism-desc">Śledzenie wszystkich działań administracyjnych</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-4">
<span class="mechanism-stars">★★★★☆</span>
<span class="mechanism-name">Bezpieczne sesje</span>
<span class="mechanism-desc">Zarządzanie sesjami przez Flask-Login</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<!-- 3 stars - ADDITIONAL -->
<div class="mechanism-item stars-3">
<span class="mechanism-stars">★★★☆☆</span>
<span class="mechanism-name">Honeypot endpoints</span>
<span class="mechanism-desc">Wykrywanie skanerów i botów (/.env, /wp-admin, /phpmyadmin)</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-3">
<span class="mechanism-stars">★★★☆☆</span>
<span class="mechanism-name">Security Alerting</span>
<span class="mechanism-desc">Powiadomienia email o krytycznych zdarzeniach</span>
<span class="mechanism-status active">Aktywne</span>
</div>
<div class="mechanism-item stars-3">
<span class="mechanism-stars">★★★☆☆</span>
<span class="mechanism-name">Security Logging</span>
<span class="mechanism-desc">Centralne logowanie zdarzeń bezpieczeństwa</span>
<span class="mechanism-status active">Aktywne</span>
</div>
</div>
</div>
</div>
<!-- GeoIP Tab -->
<div id="tab-geoip" class="tab-content">
<div class="section">
<div class="section-header">
<h2>🌍 Statystyki GeoIP Blocking</h2>
</div>
{% if geoip_enabled %}
<div class="geoip-stats">
<div class="geoip-stat">
<div class="geoip-stat-value">{{ geoip_stats.today }}</div>
<div class="geoip-stat-label">Zablokowanych dziś</div>
</div>
<div class="geoip-stat">
<div class="geoip-stat-value">{{ geoip_stats.this_month }}</div>
<div class="geoip-stat-label">W tym miesiącu</div>
</div>
<div class="geoip-stat">
<div class="geoip-stat-value">{{ geoip_stats.this_year }}</div>
<div class="geoip-stat-label">W tym roku</div>
</div>
<div class="geoip-stat">
<div class="geoip-stat-value">{{ geoip_stats.total }}</div>
<div class="geoip-stat-label">Od początku</div>
</div>
</div>
<h3 style="margin-bottom: var(--spacing-md);">Zablokowane kraje</h3>
<p style="color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-md);">
Blokowane: 🇷🇺 Rosja, 🇨🇳 Chiny, 🇰🇵 Korea Północna, 🇮🇷 Iran, 🇧🇾 Białoruś, 🇸🇾 Syria, 🇻🇪 Wenezuela, 🇨🇺 Kuba
</p>
{% if geoip_stats.by_country %}
<div class="country-breakdown">
{% for country in geoip_stats.by_country %}
<div class="country-badge">
<span class="country-flag">{{ country.flag }}</span>
<span>{{ country.name }}</span>
<span class="country-count">{{ country.count }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p>Brak zablokowanych połączeń</p>
</div>
{% endif %}
{% else %}
<div class="empty-state" style="background: #fef3c7; border-radius: var(--radius-lg); padding: var(--spacing-xl);">
<p style="color: #92400e;">⚠️ GeoIP Blocking jest wyłączone</p>
<p style="color: #78716c; font-size: var(--font-size-sm);">Ustaw GEOIP_ENABLED=true w .env aby włączyć</p>
</div>
{% endif %}
</div>
</div>
<!-- Alerts Tab -->
<div id="tab-alerts" class="tab-content active">
<div id="tab-alerts" class="tab-content">
<div class="section">
<div class="section-header">
<h2>🚨 Alerty bezpieczeństwa</h2>