feat: security panel - recent blocks table + top attacked paths
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Added to GeoIP tab:
- Last 20 blocked requests with IP, country, path, timestamp
- Top 10 most targeted URL paths with hit counts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-10 10:45:29 +02:00
parent 5030b71beb
commit 7073a56dc3
2 changed files with 70 additions and 0 deletions

View File

@ -140,6 +140,33 @@ def admin_security():
'code': code, 'flag': flag, 'name': name, 'count': count 'code': code, 'flag': flag, 'name': name, 'count': count
}) })
# Recent geo blocks (last 20) with details
recent_geo_blocks = []
top_paths = []
if geoip_enabled:
recent_blocks = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geo_blocked'
).order_by(desc(SecurityAlert.created_at)).limit(20).all()
for b in recent_blocks:
country_code = b.details.get('country', '??') if b.details else '??'
flag, name = country_flags.get(country_code, ('🏴', country_code))
recent_geo_blocks.append({
'ip': b.ip_address,
'country_flag': flag,
'country_name': name,
'path': b.details.get('path', '/') if b.details else '/',
'created_at': b.created_at
})
# Top attacked paths
path_counts = {}
for alert in geo_alerts:
if alert.details and 'path' in alert.details:
path = alert.details['path']
path_counts[path] = path_counts.get(path, 0) + 1
top_paths = sorted(path_counts.items(), key=lambda x: x[1], reverse=True)[:10]
return render_template( return render_template(
'admin/security_dashboard.html', 'admin/security_dashboard.html',
audit_logs=audit_logs, audit_logs=audit_logs,
@ -148,6 +175,8 @@ def admin_security():
stats=stats, stats=stats,
geoip_enabled=geoip_enabled, geoip_enabled=geoip_enabled,
geoip_stats=geoip_stats, geoip_stats=geoip_stats,
recent_geo_blocks=recent_geo_blocks,
top_paths=top_paths,
generated_at=datetime.now() generated_at=datetime.now()
) )
finally: finally:

View File

@ -542,6 +542,47 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Top attacked paths -->
{% if top_paths %}
<h3 style="margin-top: var(--spacing-xl); margin-bottom: var(--spacing-md);">🎯 Najczęściej atakowane ścieżki</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--spacing-sm);">
{% for path, count in top_paths %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: var(--background); border-radius: var(--radius); border: 1px solid var(--border);">
<code style="font-size: var(--font-size-sm); color: var(--text-primary); word-break: break-all;">{{ path }}</code>
<span style="flex-shrink: 0; margin-left: 10px; background: var(--primary); color: white; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px;">{{ count }}</span>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Recent geo blocks table -->
{% if recent_geo_blocks %}
<h3 style="margin-top: var(--spacing-xl); margin-bottom: var(--spacing-md);">📋 Ostatnie zablokowane próby</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-sm);">
<thead>
<tr style="border-bottom: 2px solid var(--border); text-align: left;">
<th style="padding: 10px 12px; color: var(--text-secondary); font-weight: 600;">Data i godzina</th>
<th style="padding: 10px 12px; color: var(--text-secondary); font-weight: 600;">IP</th>
<th style="padding: 10px 12px; color: var(--text-secondary); font-weight: 600;">Kraj</th>
<th style="padding: 10px 12px; color: var(--text-secondary); font-weight: 600;">Ścieżka</th>
</tr>
</thead>
<tbody>
{% for block in recent_geo_blocks %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 8px 12px; white-space: nowrap;">{{ block.created_at.strftime('%d.%m.%Y %H:%M:%S') }}</td>
<td style="padding: 8px 12px; font-family: monospace; font-size: 12px;">{{ block.ip }}</td>
<td style="padding: 8px 12px;">{{ block.country_flag }} {{ block.country_name }}</td>
<td style="padding: 8px 12px;"><code style="background: var(--background); padding: 2px 6px; border-radius: 3px; font-size: 12px;">{{ block.path }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% else %} {% else %}
<div class="empty-state" style="background: #fef3c7; border-radius: var(--radius-lg); padding: var(--spacing-xl);"> <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: #92400e;">⚠️ GeoIP Blocking jest wyłączone</p>