feat: add IP insight feature with detailed view and actions
- Updated various tables to include "Actions" column with inspect buttons for IP insights. - Created a new IP insight template for displaying detailed information about an IP address. - Implemented JavaScript functions to handle opening the IP insight view and loading data via HTMX. - Enhanced map markers to include inspect buttons for quick access to IP insights. - Added styles for the new IP insight page and buttons to maintain UI consistency.
This commit is contained in:
@@ -35,6 +35,9 @@
|
||||
<div class="tabs-container">
|
||||
<a class="tab-button" :class="{ active: tab === 'overview' }" @click.prevent="switchToOverview()" href="#overview">Overview</a>
|
||||
<a class="tab-button" :class="{ active: tab === 'attacks' }" @click.prevent="switchToAttacks()" href="#ip-stats">Attacks</a>
|
||||
<a class="tab-button" :class="{ active: tab === 'ip-insight', disabled: !insightIp }" @click.prevent="insightIp && switchToIpInsight()" href="#ip-insight">
|
||||
IP Insight<span x-show="insightIp" x-text="' (' + insightIp + ')'"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# ==================== OVERVIEW TAB ==================== #}
|
||||
@@ -147,6 +150,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ==================== IP INSIGHT TAB ==================== #}
|
||||
<div x-show="tab === 'ip-insight'" x-cloak>
|
||||
{# IP Insight content - loaded via HTMX when IP is selected #}
|
||||
<div id="ip-insight-container">
|
||||
<template x-if="!insightIp">
|
||||
<div class="table-container" style="text-align: center; padding: 60px 20px;">
|
||||
<p style="color: #8b949e; font-size: 16px;">Select an IP address from any table to view detailed insights.</p>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="insightIp" id="ip-insight-htmx-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Raw request modal - Alpine.js #}
|
||||
{% include "dashboard/partials/raw_request_modal.html" %}
|
||||
|
||||
|
||||
@@ -9,22 +9,302 @@
|
||||
<span class="github-logo-text">Krawl</span>
|
||||
</a>
|
||||
|
||||
<h1>Krawl {{ ip_address }} analysis </h1>
|
||||
{# Back to dashboard link #}
|
||||
<div style="position: absolute; top: 0; right: 0;">
|
||||
<a href="{{ dashboard_path }}/" class="download-btn" style="text-decoration: none;">
|
||||
← Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Page header #}
|
||||
<div class="ip-page-header">
|
||||
<h1>
|
||||
<span class="ip-address-title">{{ ip_address }}</span>
|
||||
{% if stats.category %}
|
||||
<span class="category-badge category-{{ stats.category | lower | replace('_', '-') }}">
|
||||
{{ stats.category | replace('_', ' ') | title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if stats.city or stats.country %}
|
||||
<p class="ip-location-subtitle">
|
||||
{{ stats.city | default('') }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include "dashboard/partials/map_section.html" %}
|
||||
{% include "dashboard/partials/ip_detail.html" %}
|
||||
{# Main content grid #}
|
||||
<div class="ip-page-grid">
|
||||
{# Left column: IP Info + Map #}
|
||||
<div class="ip-page-left">
|
||||
{# IP Information Card #}
|
||||
<div class="table-container ip-info-card">
|
||||
<h2>IP Information</h2>
|
||||
<div class="ip-info-grid">
|
||||
<div class="ip-info-section">
|
||||
<h3>Activity</h3>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Total Requests:</span>
|
||||
<span class="stat-value-sm">{{ stats.total_requests | default('N/A') }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">First Seen:</span>
|
||||
<span class="stat-value-sm">{{ stats.first_seen | format_ts }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Last Seen:</span>
|
||||
<span class="stat-value-sm">{{ stats.last_seen | format_ts }}</span>
|
||||
</div>
|
||||
{% if stats.last_analysis %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Last Analysis:</span>
|
||||
<span class="stat-value-sm">{{ stats.last_analysis | format_ts }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Attack Types table #}
|
||||
<div class="table-container alert-section">
|
||||
<h2>{{ip_address}} Access History</h2>
|
||||
<div class="ip-info-section">
|
||||
<h3>Location</h3>
|
||||
{% if stats.city %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">City:</span>
|
||||
<span class="stat-value-sm">{{ stats.city | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.region_name %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Region:</span>
|
||||
<span class="stat-value-sm">{{ stats.region_name | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.country %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Country:</span>
|
||||
<span class="stat-value-sm">{{ stats.country | e }} ({{ stats.country_code | default('') | e }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.timezone %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Timezone:</span>
|
||||
<span class="stat-value-sm">{{ stats.timezone | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="ip-info-section">
|
||||
<h3>Network</h3>
|
||||
{% if stats.isp %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">ISP:</span>
|
||||
<span class="stat-value-sm">{{ stats.isp | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.asn_org %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Organization:</span>
|
||||
<span class="stat-value-sm">{{ stats.asn_org | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.asn %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">ASN:</span>
|
||||
<span class="stat-value-sm">AS{{ stats.asn }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.reverse_dns %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Reverse DNS:</span>
|
||||
<span class="stat-value-sm" style="word-break: break-all;">{{ stats.reverse_dns | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="ip-info-section">
|
||||
<h3>Flags & Reputation</h3>
|
||||
{% set flags = [] %}
|
||||
{% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %}
|
||||
{% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %}
|
||||
{% if flags %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Flags:</span>
|
||||
<span class="stat-value-sm">
|
||||
{% for flag in flags %}
|
||||
<span class="ip-flag">{{ flag }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.reputation_score is not none %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Reputation:</span>
|
||||
<span class="stat-value-sm reputation-score {% if stats.reputation_score <= 30 %}bad{% elif stats.reputation_score <= 60 %}medium{% else %}good{% endif %}">
|
||||
{{ stats.reputation_score }}/100
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.blocklist_memberships %}
|
||||
<div class="stat-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||
<span class="stat-label-sm">Listed On:</span>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
{% for bl in stats.blocklist_memberships %}
|
||||
<span class="reputation-badge">{{ bl | e }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Blocklists:</span>
|
||||
<span class="reputation-clean">Clean</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Single IP Map #}
|
||||
<div class="table-container">
|
||||
<h2>Location</h2>
|
||||
<div id="single-ip-map" style="height: 300px; border-radius: 6px; border: 1px solid #30363d;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Right column: Radar Chart + Timeline #}
|
||||
<div class="ip-page-right">
|
||||
{# Category Analysis Card #}
|
||||
{% if stats.category_scores %}
|
||||
<div class="table-container">
|
||||
<h2>Category Analysis</h2>
|
||||
<div class="radar-chart-container">
|
||||
<div class="radar-chart" id="ip-radar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Behavior Timeline #}
|
||||
{% if stats.category_history %}
|
||||
<div class="table-container">
|
||||
<h2>Behavior Timeline</h2>
|
||||
<div class="timeline">
|
||||
{% for entry in stats.category_history %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker {{ entry.new_category | default('unknown') | replace('_', '-') }}"></div>
|
||||
<div>
|
||||
<strong style="color: #e6edf3;">{{ entry.new_category | default('unknown') | replace('_', ' ') | title }}</strong>
|
||||
{% if entry.old_category %}
|
||||
<span style="color: #8b949e;"> from {{ entry.old_category | replace('_', ' ') | title }}</span>
|
||||
{% endif %}
|
||||
<br><span style="color: #8b949e; font-size: 11px;">{{ entry.timestamp | format_ts }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Access History table #}
|
||||
<div class="table-container alert-section" style="margin-top: 20px;">
|
||||
<h2>Access History</h2>
|
||||
<div class="htmx-container"
|
||||
hx-get="{{ dashboard_path }}/htmx/access-logs?page=1&ip_filter={{ip_address}}"
|
||||
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>
|
||||
|
||||
{# Raw Request Modal #}
|
||||
<div class="raw-request-modal" x-show="rawModal.show" @click.self="closeRawModal()" x-cloak>
|
||||
<div class="raw-request-modal-content">
|
||||
<div class="raw-request-modal-header">
|
||||
<h3>Raw Request</h3>
|
||||
<span class="raw-request-modal-close" @click="closeRawModal()">×</span>
|
||||
</div>
|
||||
<div class="raw-request-modal-body">
|
||||
<pre class="raw-request-content" x-text="rawModal.content"></pre>
|
||||
</div>
|
||||
<div class="raw-request-modal-footer">
|
||||
<button class="raw-request-download-btn" @click="downloadRawRequest()">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function() {
|
||||
// Initialize radar chart
|
||||
{% if stats.category_scores %}
|
||||
const scores = {{ stats.category_scores | tojson }};
|
||||
const container = document.getElementById('ip-radar-chart');
|
||||
if (container && typeof generateRadarChart === 'function') {
|
||||
container.innerHTML = generateRadarChart(scores, 220, true);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Initialize single IP map
|
||||
{% if stats.latitude and stats.longitude %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(function() {
|
||||
const mapContainer = document.getElementById('single-ip-map');
|
||||
if (!mapContainer || typeof L === 'undefined') return;
|
||||
|
||||
const lat = {{ stats.latitude }};
|
||||
const lng = {{ stats.longitude }};
|
||||
const category = '{{ stats.category | default("unknown") | lower }}';
|
||||
|
||||
const categoryColors = {
|
||||
attacker: '#f85149',
|
||||
bad_crawler: '#f0883e',
|
||||
good_crawler: '#3fb950',
|
||||
regular_user: '#58a6ff',
|
||||
unknown: '#8b949e'
|
||||
};
|
||||
|
||||
const map = L.map('single-ip-map', {
|
||||
center: [lat, lng],
|
||||
zoom: 6,
|
||||
zoomControl: true,
|
||||
scrollWheelZoom: true
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© CartoDB | © OpenStreetMap contributors',
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd'
|
||||
}).addTo(map);
|
||||
|
||||
const color = categoryColors[category] || '#8b949e';
|
||||
const markerHtml = `
|
||||
<div style="
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: ${color};
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 12px ${color}, 0 0 24px ${color}80;
|
||||
"></div>
|
||||
`;
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: markerHtml,
|
||||
iconSize: [24, 24],
|
||||
className: 'single-ip-marker'
|
||||
});
|
||||
|
||||
L.marker([lat, lng], { icon: icon }).addTo(map);
|
||||
}, 100);
|
||||
});
|
||||
{% else %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mapContainer = document.getElementById('single-ip-map');
|
||||
if (mapContainer) {
|
||||
mapContainer.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #8b949e;">Location data not available</div>';
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
<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>
|
||||
{% if log.id %}
|
||||
<button class="view-btn" @click="viewRawRequest({{ log.id }})">View Request</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -60,10 +60,13 @@
|
||||
</td>
|
||||
<td>{{ (attack.user_agent | default(''))[:50] | e }}</td>
|
||||
<td>{{ attack.timestamp | format_ts }}</td>
|
||||
<td>
|
||||
<td style="display: flex; gap: 6px; flex-wrap: wrap;">
|
||||
{% if attack.log_id %}
|
||||
<button class="view-btn" @click="viewRawRequest({{ attack.log_id }})">View Request</button>
|
||||
{% endif %}
|
||||
<button class="inspect-btn" @click="openIpInsight('{{ attack.ip | e }}')" title="Inspect IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<th>First Seen</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Location</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -45,16 +46,21 @@
|
||||
<td>{{ ip.first_seen | format_ts }}</td>
|
||||
<td>{{ ip.last_seen | format_ts }}</td>
|
||||
<td>{{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }}</td>
|
||||
<td>
|
||||
<button class="inspect-btn" @click="openIpInsight('{{ ip.ip | e }}')" title="Inspect IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
<td colspan="6" class="ip-stats-cell">
|
||||
<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="6" style="text-align: center;">No attackers found</td></tr>
|
||||
<tr><td colspan="7" style="text-align: center;">No attackers found</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
hx-swap="innerHTML">
|
||||
Time
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -45,16 +46,21 @@
|
||||
<td>{{ cred.password | default('N/A') | e }}</td>
|
||||
<td>{{ cred.path | default('') | e }}</td>
|
||||
<td>{{ cred.timestamp | format_ts }}</td>
|
||||
<td>
|
||||
<button class="inspect-btn" @click="openIpInsight('{{ cred.ip | e }}')" title="Inspect IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
<td colspan="6" class="ip-stats-cell">
|
||||
<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="6" style="text-align: center;">No credentials captured</td></tr>
|
||||
<tr><td colspan="7" style="text-align: center;">No credentials captured</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
hx-swap="innerHTML">
|
||||
Honeypot Triggers
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -39,16 +40,21 @@
|
||||
{{ item.ip | e }}
|
||||
</td>
|
||||
<td>{{ item.count }}</td>
|
||||
<td>
|
||||
<button class="inspect-btn" @click="openIpInsight('{{ item.ip | e }}')" title="Inspect IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
<td colspan="3" class="ip-stats-cell">
|
||||
<td colspan="4" class="ip-stats-cell">
|
||||
<div class="ip-stats-dropdown">
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" style="text-align: center;">No data</td></tr>
|
||||
<tr><td colspan="4" style="text-align: center;">No data</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
279
src/templates/jinja2/dashboard/partials/ip_insight.html
Normal file
279
src/templates/jinja2/dashboard/partials/ip_insight.html
Normal file
@@ -0,0 +1,279 @@
|
||||
{# HTMX fragment: IP Insight - full IP detail view for inline display #}
|
||||
<div class="ip-insight-content" id="ip-insight-content">
|
||||
{# Page header #}
|
||||
<div class="ip-page-header">
|
||||
<h1>
|
||||
<span class="ip-address-title">{{ ip_address }}</span>
|
||||
{% if stats.category %}
|
||||
<span class="category-badge category-{{ stats.category | lower | replace('_', '-') }}">
|
||||
{{ stats.category | replace('_', ' ') | title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if stats.city or stats.country %}
|
||||
<p class="ip-location-subtitle">
|
||||
{{ stats.city | default('') }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Main content grid #}
|
||||
<div class="ip-page-grid">
|
||||
{# Left column: IP Info + Map #}
|
||||
<div class="ip-page-left">
|
||||
{# IP Information Card #}
|
||||
<div class="table-container ip-info-card">
|
||||
<h2>IP Information</h2>
|
||||
<div class="ip-info-grid">
|
||||
<div class="ip-info-section">
|
||||
<h3>Activity</h3>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Total Requests:</span>
|
||||
<span class="stat-value-sm">{{ stats.total_requests | default('N/A') }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">First Seen:</span>
|
||||
<span class="stat-value-sm">{{ stats.first_seen | format_ts }}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Last Seen:</span>
|
||||
<span class="stat-value-sm">{{ stats.last_seen | format_ts }}</span>
|
||||
</div>
|
||||
{% if stats.last_analysis %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Last Analysis:</span>
|
||||
<span class="stat-value-sm">{{ stats.last_analysis | format_ts }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="ip-info-section">
|
||||
<h3>Location</h3>
|
||||
{% if stats.city %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">City:</span>
|
||||
<span class="stat-value-sm">{{ stats.city | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.region_name %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Region:</span>
|
||||
<span class="stat-value-sm">{{ stats.region_name | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.country %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Country:</span>
|
||||
<span class="stat-value-sm">{{ stats.country | e }} ({{ stats.country_code | default('') | e }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.timezone %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Timezone:</span>
|
||||
<span class="stat-value-sm">{{ stats.timezone | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="ip-info-section">
|
||||
<h3>Network</h3>
|
||||
{% if stats.isp %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">ISP:</span>
|
||||
<span class="stat-value-sm">{{ stats.isp | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.asn_org %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Organization:</span>
|
||||
<span class="stat-value-sm">{{ stats.asn_org | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.asn %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">ASN:</span>
|
||||
<span class="stat-value-sm">AS{{ stats.asn }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.reverse_dns %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Reverse DNS:</span>
|
||||
<span class="stat-value-sm" style="word-break: break-all;">{{ stats.reverse_dns | e }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="ip-info-section">
|
||||
<h3>Flags & Reputation</h3>
|
||||
{% set flags = [] %}
|
||||
{% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %}
|
||||
{% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %}
|
||||
{% if flags %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Flags:</span>
|
||||
<span class="stat-value-sm">
|
||||
{% for flag in flags %}
|
||||
<span class="ip-flag">{{ flag }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.reputation_score is not none %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Reputation:</span>
|
||||
<span class="stat-value-sm reputation-score {% if stats.reputation_score <= 30 %}bad{% elif stats.reputation_score <= 60 %}medium{% else %}good{% endif %}">
|
||||
{{ stats.reputation_score }}/100
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.blocklist_memberships %}
|
||||
<div class="stat-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||
<span class="stat-label-sm">Listed On:</span>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
{% for bl in stats.blocklist_memberships %}
|
||||
<span class="reputation-badge">{{ bl | e }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="stat-row">
|
||||
<span class="stat-label-sm">Blocklists:</span>
|
||||
<span class="reputation-clean">Clean</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# Right column: Radar Chart + Timeline #}
|
||||
<div class="ip-page-right">
|
||||
{# Category Analysis Card #}
|
||||
{% if stats.category_scores %}
|
||||
<div class="table-container">
|
||||
<h2>Category Analysis</h2>
|
||||
<div class="radar-chart-container">
|
||||
<div class="radar-chart" id="insight-radar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Behavior Timeline #}
|
||||
{% if stats.category_history %}
|
||||
<div class="table-container">
|
||||
<h2>Behavior Timeline</h2>
|
||||
<div class="timeline">
|
||||
{% for entry in stats.category_history %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker {{ entry.new_category | default('unknown') | replace('_', '-') }}"></div>
|
||||
<div>
|
||||
<strong style="color: #e6edf3;">{{ entry.new_category | default('unknown') | replace('_', ' ') | title }}</strong>
|
||||
{% if entry.old_category %}
|
||||
<span style="color: #8b949e;"> from {{ entry.old_category | replace('_', ' ') | title }}</span>
|
||||
{% endif %}
|
||||
<br><span style="color: #8b949e; font-size: 11px;">{{ entry.timestamp | format_ts }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Single IP Map - full width #}
|
||||
<div class="table-container" style="margin-top: 20px;">
|
||||
<h2>Location</h2>
|
||||
<div id="insight-ip-map" style="height: 300px; border-radius: 6px; border: 1px solid #30363d;"></div>
|
||||
</div>
|
||||
|
||||
{# Access History table #}
|
||||
<div class="table-container alert-section" style="margin-top: 20px;">
|
||||
<h2>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>
|
||||
|
||||
{# Inline script for initializing map and chart after HTMX swap #}
|
||||
<script>
|
||||
(function() {
|
||||
// Initialize radar chart
|
||||
{% if stats.category_scores %}
|
||||
const scores = {{ stats.category_scores | tojson }};
|
||||
const container = document.getElementById('insight-radar-chart');
|
||||
if (container && typeof generateRadarChart === 'function') {
|
||||
container.innerHTML = generateRadarChart(scores, 220, true);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Initialize single IP map
|
||||
{% if stats.latitude and stats.longitude %}
|
||||
setTimeout(function() {
|
||||
const mapContainer = document.getElementById('insight-ip-map');
|
||||
if (!mapContainer || typeof L === 'undefined') return;
|
||||
|
||||
// Clean up any existing map instance
|
||||
if (mapContainer._leaflet_id) {
|
||||
mapContainer._leaflet_id = null;
|
||||
}
|
||||
mapContainer.innerHTML = '';
|
||||
|
||||
const lat = {{ stats.latitude }};
|
||||
const lng = {{ stats.longitude }};
|
||||
const category = '{{ stats.category | default("unknown") | lower }}';
|
||||
|
||||
const categoryColors = {
|
||||
attacker: '#f85149',
|
||||
bad_crawler: '#f0883e',
|
||||
good_crawler: '#3fb950',
|
||||
regular_user: '#58a6ff',
|
||||
unknown: '#8b949e'
|
||||
};
|
||||
|
||||
const map = L.map('insight-ip-map', {
|
||||
center: [lat, lng],
|
||||
zoom: 6,
|
||||
zoomControl: true,
|
||||
scrollWheelZoom: true
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© CartoDB | © OpenStreetMap contributors',
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd'
|
||||
}).addTo(map);
|
||||
|
||||
const color = categoryColors[category] || '#8b949e';
|
||||
const markerHtml = `
|
||||
<div style="
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: ${color};
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 12px ${color}, 0 0 24px ${color}80;
|
||||
"></div>
|
||||
`;
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: markerHtml,
|
||||
iconSize: [24, 24],
|
||||
className: 'single-ip-marker'
|
||||
});
|
||||
|
||||
L.marker([lat, lng], { icon: icon }).addTo(map);
|
||||
}, 100);
|
||||
{% else %}
|
||||
const mapContainer = document.getElementById('insight-ip-map');
|
||||
if (mapContainer) {
|
||||
mapContainer.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #8b949e;">Location data not available</div>';
|
||||
}
|
||||
{% endif %}
|
||||
})();
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@
|
||||
<th>Path</th>
|
||||
<th>User-Agent</th>
|
||||
<th>Time</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -23,16 +24,21 @@
|
||||
<td>{{ activity.path | e }}</td>
|
||||
<td style="word-break: break-all;">{{ (activity.user_agent | default(''))[:80] | e }}</td>
|
||||
<td>{{ activity.timestamp | format_ts(time_only=True) }}</td>
|
||||
<td>
|
||||
<button class="inspect-btn" @click="openIpInsight('{{ activity.ip | e }}')" title="Inspect IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
<td colspan="4" class="ip-stats-cell">
|
||||
<td colspan="5" class="ip-stats-cell">
|
||||
<div class="ip-stats-dropdown">
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>
|
||||
<tr><td colspan="5" style="text-align:center;">No suspicious activity detected</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
hx-swap="innerHTML">
|
||||
Access Count
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -39,16 +40,21 @@
|
||||
{{ item.ip | e }}
|
||||
</td>
|
||||
<td>{{ item.count }}</td>
|
||||
<td>
|
||||
<button class="inspect-btn" @click="openIpInsight('{{ item.ip | e }}')" title="Inspect IP">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="ip-stats-row" style="display: none;">
|
||||
<td colspan="3" class="ip-stats-cell">
|
||||
<td colspan="4" class="ip-stats-cell">
|
||||
<div class="ip-stats-dropdown">
|
||||
<div class="loading">Loading stats...</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" style="text-align: center;">No data</td></tr>
|
||||
<tr><td colspan="4" style="text-align: center;">No data</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user