added first version of single ip page breakdiwn
This commit is contained in:
committed by
Lorenzo Venerandi
parent
486d02fbd4
commit
75722051d6
@@ -850,6 +850,69 @@ class DatabaseManager:
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
def get_access_logs_paginated(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 25,
|
||||
ip_filter: Optional[str] = None,
|
||||
suspicious_only: bool = False,
|
||||
since_minutes: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve access logs with pagination and optional filtering.
|
||||
|
||||
Args:
|
||||
page: Page to retrieve
|
||||
page_size: Number of records for page
|
||||
ip_filter: Filter by IP address
|
||||
suspicious_only: Only return suspicious requests
|
||||
since_minutes: Only return logs from the last N minutes
|
||||
|
||||
Returns:
|
||||
List of access log dictionaries
|
||||
"""
|
||||
session = self.session
|
||||
try:
|
||||
offset = (page - 1) * page_size
|
||||
query = session.query(AccessLog).order_by(AccessLog.timestamp.desc())
|
||||
|
||||
if ip_filter:
|
||||
query = query.filter(AccessLog.ip == sanitize_ip(ip_filter))
|
||||
if suspicious_only:
|
||||
query = query.filter(AccessLog.is_suspicious == True)
|
||||
if since_minutes is not None:
|
||||
cutoff_time = datetime.now() - timedelta(minutes=since_minutes)
|
||||
query = query.filter(AccessLog.timestamp >= cutoff_time)
|
||||
|
||||
logs = query.offset(offset).limit(page_size).all()
|
||||
# Get total count of attackers
|
||||
total_access_logs = (
|
||||
session.query(AccessLog).filter(AccessLog.ip == sanitize_ip(ip_filter)).count()
|
||||
)
|
||||
total_pages = (total_access_logs + page_size - 1) // page_size
|
||||
|
||||
return {
|
||||
"access_logs": [
|
||||
{
|
||||
"id": log.id,
|
||||
"ip": log.ip,
|
||||
"path": log.path,
|
||||
"user_agent": log.user_agent,
|
||||
"method": log.method,
|
||||
"is_suspicious": log.is_suspicious,
|
||||
"is_honeypot_trigger": log.is_honeypot_trigger,
|
||||
"timestamp": log.timestamp.isoformat(),
|
||||
"attack_types": [d.attack_type for d in log.attack_detections],
|
||||
}
|
||||
for log in logs ],
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_logs": total_access_logs,
|
||||
"total_pages": total_pages,
|
||||
},
|
||||
}
|
||||
finally:
|
||||
self.close_session()
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ All endpoints are prefixed with the secret dashboard path.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Request, Response, Query
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
|
||||
@@ -6,6 +6,8 @@ Renders the main dashboard page with server-side data for initial load.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from logger import get_app_logger
|
||||
|
||||
from dependencies import get_db, get_templates
|
||||
|
||||
@@ -37,3 +39,32 @@ async def dashboard_page(request: Request):
|
||||
"suspicious_activities": suspicious,
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/ip/{ip_address:path}")
|
||||
async def ip_page(ip_address: str, request: Request):
|
||||
db = get_db()
|
||||
try:
|
||||
stats = db.get_ip_stats_by_ip(ip_address)
|
||||
config = request.app.state.config
|
||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
||||
|
||||
if stats:
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/ip.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": dashboard_path,
|
||||
"stats": stats,
|
||||
"ip_address": ip_address
|
||||
},
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
content={"error": "IP not found"},
|
||||
)
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching IP stats: {e}")
|
||||
return JSONResponse(content={"error": str(e)})
|
||||
|
||||
|
||||
@@ -167,6 +167,42 @@ async def htmx_attackers(
|
||||
)
|
||||
|
||||
|
||||
# ── Access logs by ip ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/access-logs")
|
||||
async def htmx_access_logs_by_ip(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("total_requests"),
|
||||
sort_order: str = Query("desc"),
|
||||
ip_filter: str = Query("ip_filter"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_access_logs_paginated(
|
||||
page=max(1, page),page_size=25, ip_filter=ip_filter
|
||||
)
|
||||
|
||||
# Normalize pagination key (DB returns total_attackers, template expects total)
|
||||
pagination = result["pagination"]
|
||||
if "total_access_logs" in pagination and "total" not in pagination:
|
||||
pagination["total"] = pagination["total_access_logs"]
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/access_by_ip_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["access_logs"],
|
||||
"pagination": pagination,
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
"ip_filter": ip_filter,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Credentials ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
30
src/templates/jinja2/dashboard/ip.html
Normal file
30
src/templates/jinja2/dashboard/ip.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container" x-data="dashboardApp()" x-init="init()">
|
||||
|
||||
{# GitHub logo #}
|
||||
<a href="https://github.com/BlessedRebuS/Krawl" target="_blank" class="github-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
||||
<span class="github-logo-text">Krawl</span>
|
||||
</a>
|
||||
|
||||
<h1>Krawl {{ ip_address }} analysis </h1>
|
||||
|
||||
|
||||
{% include "dashboard/partials/map_section.html" %}
|
||||
{% include "dashboard/partials/ip_detail.html" %}
|
||||
|
||||
{# Attack Types table #}
|
||||
<div class="table-container alert-section">
|
||||
<h2>{{ip_address}} Access History</h2>
|
||||
<div class="htmx-container"
|
||||
hx-get="{{ dashboard_path }}/htmx/access-logs?page=1&ip_filter={{ip_address}}"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="innerHTML">
|
||||
<div class="htmx-indicator">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,63 @@
|
||||
{# HTMX fragment: Detected Access logs by ip table #}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<span class="pagination-info">Page {{ pagination.page }}/{{ pagination.total_pages }} — {{ pagination.total }} total</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="pagination-btn"
|
||||
hx-get="{{ dashboard_path }}/htmx/access-logs?page={{ pagination.page - 1 }}&sort_by={{ sort_by }}&sort_order={{ sort_order }}&ip_filter={{ ip_filter }}"
|
||||
hx-target="closest .htmx-container"
|
||||
hx-swap="innerHTML"
|
||||
{% if pagination.page <= 1 %}disabled{% endif %}>Prev</button>
|
||||
<button class="pagination-btn"
|
||||
hx-get="{{ dashboard_path }}/htmx/access-logs?page={{ pagination.page + 1 }}&sort_by={{ sort_by }}&sort_order={{ sort_order }}&ip_filter={{ ip_filter }}"
|
||||
hx-target="closest .htmx-container"
|
||||
hx-swap="innerHTML"
|
||||
{% if pagination.page >= pagination.total_pages %}disabled{% endif %}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Path</th>
|
||||
<th>User-Agent</th>
|
||||
<th class="sortable {% if sort_by == 'timestamp' %}{{ sort_order }}{% endif %}"
|
||||
hx-get="{{ dashboard_path }}/htmx/access-logs?page=1&sort_by=timestamp&sort_order={% if sort_by == 'timestamp' and sort_order == 'desc' %}asc{% else %}desc{% endif %}&ip_filter={{ ip_filter }}"
|
||||
hx-target="closest .htmx-container"
|
||||
hx-swap="innerHTML">
|
||||
Time
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in items %}
|
||||
<tr class="ip-row" data-ip="{{ log.ip | e }}">
|
||||
<td class="rank">{{ loop.index + (pagination.page - 1) * pagination.page_size }}</td>
|
||||
<td>
|
||||
<div class="path-cell-container">
|
||||
<span class="path-truncated">{{ log.path | e }}</span>
|
||||
{% if log.path | length > 30 %}
|
||||
<div class="path-tooltip">{{ log.path | e }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ (log.user_agent | default(''))[:50] | e }}</td>
|
||||
<td>{{ log.timestamp | format_ts }}</td>
|
||||
<td>
|
||||
{% if log.log_id %}
|
||||
<button class="view-btn" @click="viewRawRequest({{ log.log_id }})">View Request</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
<td colspan="7" class="ip-stats-cell">
|
||||
<div class="ip-stats-dropdown">
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" style="text-align: center;">No logs detected</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
Reference in New Issue
Block a user