modified dashboard, added ip-api data fetch

This commit is contained in:
BlessedRebuS
2026-02-01 22:43:12 +01:00
parent c8d0ef3da1
commit 863fac251d
8 changed files with 601 additions and 130 deletions

View File

@@ -181,6 +181,7 @@ location / {
## API ## API
Krawl uses the following APIs Krawl uses the following APIs
- http://ip-api.com (IP Data)
- https://iprep.lcrawl.com (IP Reputation) - https://iprep.lcrawl.com (IP Reputation)
- https://nominatim.openstreetmap.org/reverse (Reverse IP Lookup) - https://nominatim.openstreetmap.org/reverse (Reverse IP Lookup)
- https://api.ipify.org (Public IP discovery) - https://api.ipify.org (Public IP discovery)

View File

@@ -3,7 +3,7 @@ name: krawl-chart
description: A Helm chart for Krawl honeypot server description: A Helm chart for Krawl honeypot server
type: application type: application
version: 1.0.0 version: 1.0.0
appVersion: 1.0.0 appVersion: 1.0.1
keywords: keywords:
- honeypot - honeypot
- security - security

View File

@@ -137,6 +137,39 @@ class DatabaseManager:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN longitude REAL") cursor.execute("ALTER TABLE ip_stats ADD COLUMN longitude REAL")
migrations_run.append("longitude") migrations_run.append("longitude")
# Add new geolocation columns
if "country" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN country VARCHAR(100)")
migrations_run.append("country")
if "region" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN region VARCHAR(2)")
migrations_run.append("region")
if "region_name" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN region_name VARCHAR(100)")
migrations_run.append("region_name")
if "timezone" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN timezone VARCHAR(50)")
migrations_run.append("timezone")
if "isp" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN isp VARCHAR(100)")
migrations_run.append("isp")
if "is_proxy" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN is_proxy BOOLEAN")
migrations_run.append("is_proxy")
if "is_hosting" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN is_hosting BOOLEAN")
migrations_run.append("is_hosting")
if "reverse" not in columns:
cursor.execute("ALTER TABLE ip_stats ADD COLUMN reverse VARCHAR(255)")
migrations_run.append("reverse")
if migrations_run: if migrations_run:
conn.commit() conn.commit()
applogger.info( applogger.info(
@@ -377,7 +410,7 @@ class DatabaseManager:
) -> None: ) -> None:
""" """
Internal method to record category changes in history. Internal method to record category changes in history.
Only records if there's an actual change from a previous category. Records all category changes including initial categorization.
Args: Args:
ip: IP address ip: IP address
@@ -385,11 +418,6 @@ class DatabaseManager:
new_category: New category new_category: New category
timestamp: When the change occurred timestamp: When the change occurred
""" """
# Don't record initial categorization (when old_category is None)
# Only record actual category changes
if old_category is None:
return
session = self.session session = self.session
try: try:
history_entry = CategoryHistory( history_entry = CategoryHistory(
@@ -445,6 +473,14 @@ class DatabaseManager:
city: Optional[str] = None, city: Optional[str] = None,
latitude: Optional[float] = None, latitude: Optional[float] = None,
longitude: Optional[float] = None, longitude: Optional[float] = None,
country: Optional[str] = None,
region: Optional[str] = None,
region_name: Optional[str] = None,
timezone: Optional[str] = None,
isp: Optional[str] = None,
reverse: Optional[str] = None,
is_proxy: Optional[bool] = None,
is_hosting: Optional[bool] = None,
) -> None: ) -> None:
""" """
Update IP rep stats Update IP rep stats
@@ -458,6 +494,14 @@ class DatabaseManager:
city: City name (optional) city: City name (optional)
latitude: Latitude coordinate (optional) latitude: Latitude coordinate (optional)
longitude: Longitude coordinate (optional) longitude: Longitude coordinate (optional)
country: Full country name (optional)
region: Region code (optional)
region_name: Region name (optional)
timezone: Timezone (optional)
isp: Internet Service Provider (optional)
reverse: Reverse DNS lookup (optional)
is_proxy: Whether IP is a proxy (optional)
is_hosting: Whether IP is a hosting provider (optional)
""" """
session = self.session session = self.session
@@ -475,6 +519,22 @@ class DatabaseManager:
ip_stats.latitude = latitude ip_stats.latitude = latitude
if longitude is not None: if longitude is not None:
ip_stats.longitude = longitude ip_stats.longitude = longitude
if country:
ip_stats.country = country
if region:
ip_stats.region = region
if region_name:
ip_stats.region_name = region_name
if timezone:
ip_stats.timezone = timezone
if isp:
ip_stats.isp = isp
if reverse:
ip_stats.reverse = reverse
if is_proxy is not None:
ip_stats.is_proxy = is_proxy
if is_hosting is not None:
ip_stats.is_hosting = is_hosting
session.commit() session.commit()
except Exception as e: except Exception as e:
session.rollback() session.rollback()
@@ -714,8 +774,16 @@ class DatabaseManager:
"last_seen": stat.last_seen.isoformat() if stat.last_seen else None, "last_seen": stat.last_seen.isoformat() if stat.last_seen else None,
"country_code": stat.country_code, "country_code": stat.country_code,
"city": stat.city, "city": stat.city,
"country": stat.country,
"region": stat.region,
"region_name": stat.region_name,
"timezone": stat.timezone,
"isp": stat.isp,
"reverse": stat.reverse,
"asn": stat.asn, "asn": stat.asn,
"asn_org": stat.asn_org, "asn_org": stat.asn_org,
"is_proxy": stat.is_proxy,
"is_hosting": stat.is_hosting,
"list_on": stat.list_on or {}, "list_on": stat.list_on or {},
"reputation_score": stat.reputation_score, "reputation_score": stat.reputation_score,
"reputation_source": stat.reputation_source, "reputation_source": stat.reputation_source,

View File

@@ -1,113 +1,124 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Geolocation utilities for reverse geocoding and city lookups. Geolocation utilities for IP lookups using ip-api.com.
""" """
import requests import requests
from typing import Optional, Tuple from typing import Optional, Dict, Any
from logger import get_app_logger from logger import get_app_logger
app_logger = get_app_logger() app_logger = get_app_logger()
# Simple city name cache to avoid repeated API calls # Cache for IP geolocation data to avoid repeated API calls
_city_cache = {} _geoloc_cache = {}
def reverse_geocode_city(latitude: float, longitude: float) -> Optional[str]: def fetch_ip_geolocation(ip_address: str) -> Optional[Dict[str, Any]]:
""" """
Reverse geocode coordinates to get city name using Nominatim (OpenStreetMap). Fetch geolocation data for an IP address using ip-api.com.
Args: Args:
latitude: Latitude coordinate ip_address: IP address to lookup
longitude: Longitude coordinate
Returns: Returns:
City name or None if not found Dictionary containing geolocation data or None if lookup fails
""" """
# Check cache first # Check cache first
cache_key = f"{latitude},{longitude}" if ip_address in _geoloc_cache:
if cache_key in _city_cache: return _geoloc_cache[ip_address]
return _city_cache[cache_key] # This is now replacing lcrawl to fetch IP data like latitude/longitude, city, etc...
try: try:
# Use Nominatim reverse geocoding API (free, no API key required) # Use ip-api.com API for geolocation
url = "https://nominatim.openstreetmap.org/reverse" url = f"http://ip-api.com/json/{ip_address}"
params = { params = {
"lat": latitude, "fields": "status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,reverse,mobile,proxy,hosting,query"
"lon": longitude,
"format": "json",
"zoom": 10, # City level
"addressdetails": 1,
} }
headers = {"User-Agent": "Krawl-Honeypot/1.0"} # Required by Nominatim ToS
response = requests.get(url, params=params, headers=headers, timeout=5) response = requests.get(url, params=params, timeout=5)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
address = data.get("address", {})
# Try to get city from various possible fields # Check if the API call was successful
city = ( if data.get("status") != "success":
address.get("city") app_logger.warning(f"IP lookup failed for {ip_address}: {data.get('message')}")
or address.get("town") return None
or address.get("village")
or address.get("municipality")
or address.get("county")
)
# Cache the result # Cache the result
_city_cache[cache_key] = city _geoloc_cache[ip_address] = data
if city: app_logger.debug(f"Fetched geolocation for {ip_address}")
app_logger.debug(f"Reverse geocoded {latitude},{longitude} to {city}") return data
return city
except requests.RequestException as e: except requests.RequestException as e:
app_logger.warning(f"Reverse geocoding failed for {latitude},{longitude}: {e}") app_logger.warning(f"Geolocation API call failed for {ip_address}: {e}")
return None return None
except Exception as e: except Exception as e:
app_logger.error(f"Error in reverse geocoding: {e}") app_logger.error(f"Error fetching geolocation for {ip_address}: {e}")
return None return None
def get_most_recent_geoip_data(results: list) -> Optional[dict]: def extract_geolocation_from_ip(ip_address: str) -> Optional[Dict[str, Any]]:
""" """
Extract the most recent geoip_data from API results. Extract geolocation data for an IP address.
Results are assumed to be sorted by record_added (most recent first).
Args: Args:
results: List of result dictionaries from IP reputation API ip_address: IP address to lookup
Returns: Returns:
Most recent geoip_data dict or None Dictionary with city, country, lat, lon, and other geolocation data or None
""" """
if not results: geoloc_data = fetch_ip_geolocation(ip_address)
if not geoloc_data:
return None return None
# The first result is the most recent (sorted by record_added) return {
most_recent = results[0] "city": geoloc_data.get("city"),
return most_recent.get("geoip_data") "country": geoloc_data.get("country"),
"country_code": geoloc_data.get("countryCode"),
"region": geoloc_data.get("region"),
"region_name": geoloc_data.get("regionName"),
"latitude": geoloc_data.get("lat"),
"longitude": geoloc_data.get("lon"),
"timezone": geoloc_data.get("timezone"),
"isp": geoloc_data.get("isp"),
"org": geoloc_data.get("org"),
"reverse": geoloc_data.get("reverse"),
"is_proxy": geoloc_data.get("proxy"),
"is_hosting": geoloc_data.get("hosting"),
}
def extract_city_from_coordinates(geoip_data: dict) -> Optional[str]: def fetch_blocklist_data(ip_address: str) -> Optional[Dict[str, Any]]:
""" """
Extract city name from geoip_data using reverse geocoding. Fetch blocklist data for an IP address using lcrawl API.
Args: Args:
geoip_data: Dictionary containing location_latitude and location_longitude ip_address: IP address to lookup
Returns: Returns:
City name or None Dictionary containing blocklist information or None if lookup fails
""" """
if not geoip_data: # This is now used only for ip reputation
return None try:
api_url = "https://iprep.lcrawl.com/api/iprep/"
params = {"cidr": ip_address}
headers = {"Content-Type": "application/json"}
response = requests.get(api_url, headers=headers, params=params, timeout=10)
latitude = geoip_data.get("location_latitude") if response.status_code == 200:
longitude = geoip_data.get("location_longitude") payload = response.json()
if payload.get("results"):
results = payload["results"]
# Get the most recent result (first in list, sorted by record_added)
most_recent = results[0]
list_on = most_recent.get("list_on", {})
app_logger.debug(f"Fetched blocklist data for {ip_address}")
return list_on
except requests.RequestException as e:
app_logger.warning(f"Failed to fetch blocklist data for {ip_address}: {e}")
except Exception as e:
app_logger.error(f"Error processing blocklist data for {ip_address}: {e}")
if latitude is None or longitude is None: return None
return None
return reverse_geocode_city(latitude, longitude)

View File

@@ -162,12 +162,20 @@ class IpStats(Base):
# GeoIP fields (populated by future enrichment) # GeoIP fields (populated by future enrichment)
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
city: Mapped[Optional[str]] = mapped_column(String(MAX_CITY_LENGTH), nullable=True) city: Mapped[Optional[str]] = mapped_column(String(MAX_CITY_LENGTH), nullable=True)
country: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
region: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
region_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
timezone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
isp: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
reverse: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
latitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True) latitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
longitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True) longitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
asn: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) asn: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
asn_org: Mapped[Optional[str]] = mapped_column( asn_org: Mapped[Optional[str]] = mapped_column(
String(MAX_ASN_ORG_LENGTH), nullable=True String(MAX_ASN_ORG_LENGTH), nullable=True
) )
is_proxy: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
is_hosting: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
list_on: Mapped[Optional[Dict[str, str]]] = mapped_column(JSON, nullable=True) list_on: Mapped[Optional[Dict[str, str]]] = mapped_column(JSON, nullable=True)
# Reputation fields (populated by future enrichment) # Reputation fields (populated by future enrichment)

View File

@@ -2,7 +2,7 @@ from database import get_database
from logger import get_app_logger from logger import get_app_logger
import requests import requests
from sanitizer import sanitize_for_storage, sanitize_dict from sanitizer import sanitize_for_storage, sanitize_dict
from geo_utils import get_most_recent_geoip_data, extract_city_from_coordinates from geo_utils import extract_geolocation_from_ip, fetch_blocklist_data
# ---------------------- # ----------------------
# TASK CONFIG # TASK CONFIG
@@ -27,34 +27,49 @@ def main():
) )
for ip in unenriched_ips: for ip in unenriched_ips:
try: try:
api_url = "https://iprep.lcrawl.com/api/iprep/" # Fetch geolocation data using ip-api.com
params = {"cidr": ip} geoloc_data = extract_geolocation_from_ip(ip)
headers = {"Content-Type": "application/json"}
response = requests.get(api_url, headers=headers, params=params, timeout=10) # Fetch blocklist data from lcrawl API
payload = response.json() blocklist_data = fetch_blocklist_data(ip)
if payload.get("results"): if geoloc_data:
results = payload["results"] # Extract fields from the new API response
country_iso_code = geoloc_data.get("country_code")
country = geoloc_data.get("country")
region = geoloc_data.get("region")
region_name = geoloc_data.get("region_name")
city = geoloc_data.get("city")
timezone = geoloc_data.get("timezone")
isp = geoloc_data.get("isp")
reverse = geoloc_data.get("reverse")
asn = geoloc_data.get("asn")
asn_org = geoloc_data.get("org")
latitude = geoloc_data.get("latitude")
longitude = geoloc_data.get("longitude")
is_proxy = geoloc_data.get("is_proxy", False)
is_hosting = geoloc_data.get("is_hosting", False)
# Get the most recent result (first in list, sorted by record_added) # Use blocklist data if available, otherwise create default with flags
most_recent = results[0] if blocklist_data:
geoip_data = most_recent.get("geoip_data", {}) list_on = blocklist_data
list_on = most_recent.get("list_on", {}) else:
list_on = {}
# Extract standard fields
country_iso_code = geoip_data.get("country_iso_code") # Add flags to list_on
asn = geoip_data.get("asn_autonomous_system_number") list_on["is_proxy"] = is_proxy
asn_org = geoip_data.get("asn_autonomous_system_organization") list_on["is_hosting"] = is_hosting
latitude = geoip_data.get("location_latitude")
longitude = geoip_data.get("location_longitude")
# Extract city from coordinates using reverse geocoding
city = extract_city_from_coordinates(geoip_data)
sanitized_country_iso_code = sanitize_for_storage(country_iso_code, 3) sanitized_country_iso_code = sanitize_for_storage(country_iso_code, 3)
sanitized_country = sanitize_for_storage(country, 100)
sanitized_region = sanitize_for_storage(region, 2)
sanitized_region_name = sanitize_for_storage(region_name, 100)
sanitized_asn = sanitize_for_storage(asn, 100) sanitized_asn = sanitize_for_storage(asn, 100)
sanitized_asn_org = sanitize_for_storage(asn_org, 100) sanitized_asn_org = sanitize_for_storage(asn_org, 100)
sanitized_city = sanitize_for_storage(city, 100) if city else None sanitized_city = sanitize_for_storage(city, 100) if city else None
sanitized_timezone = sanitize_for_storage(timezone, 50)
sanitized_isp = sanitize_for_storage(isp, 100)
sanitized_reverse = sanitize_for_storage(reverse, 255) if reverse else None
sanitized_list_on = sanitize_dict(list_on, 100000) sanitized_list_on = sanitize_dict(list_on, 100000)
db_manager.update_ip_rep_infos( db_manager.update_ip_rep_infos(
@@ -63,11 +78,19 @@ def main():
sanitized_asn, sanitized_asn,
sanitized_asn_org, sanitized_asn_org,
sanitized_list_on, sanitized_list_on,
sanitized_city, city=sanitized_city,
latitude, latitude=latitude,
longitude, longitude=longitude,
country=sanitized_country,
region=sanitized_region,
region_name=sanitized_region_name,
timezone=sanitized_timezone,
isp=sanitized_isp,
reverse=sanitized_reverse,
is_proxy=is_proxy,
is_hosting=is_hosting,
) )
except requests.RequestException as e: except requests.RequestException as e:
app_logger.warning(f"Failed to fetch IP rep for {ip}: {e}") app_logger.warning(f"Failed to fetch geolocation for {ip}: {e}")
except Exception as e: except Exception as e:
app_logger.error(f"Error processing IP {ip}: {e}") app_logger.error(f"Error processing IP {ip}: {e}")

View File

@@ -643,6 +643,330 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
max-height: 400px; max-height: 400px;
}} }}
/* Mobile Optimization - Tablets (768px and down) */
@media (max-width: 768px) {{
body {{
padding: 12px;
}}
.container {{
max-width: 100%;
}}
h1 {{
font-size: 24px;
margin-bottom: 20px;
}}
.github-logo {{
position: relative;
top: auto;
left: auto;
margin-bottom: 15px;
}}
.download-section {{
position: relative;
top: auto;
right: auto;
margin-bottom: 20px;
}}
.stats-grid {{
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 20px;
}}
.stat-value {{
font-size: 28px;
}}
.stat-card {{
padding: 15px;
}}
.table-container {{
padding: 12px;
margin-bottom: 15px;
overflow-x: auto;
}}
table {{
font-size: 13px;
}}
th, td {{
padding: 10px 6px;
}}
h2 {{
font-size: 18px;
}}
.tabs-container {{
gap: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}}
.tab-button {{
padding: 10px 16px;
font-size: 12px;
}}
.ip-stats-dropdown {{
flex-direction: column;
gap: 15px;
}}
.stats-right {{
flex: 0 0 auto;
width: 100%;
}}
.radar-chart {{
width: 160px;
height: 160px;
}}
.timeline-container {{
flex-direction: column;
gap: 15px;
min-height: auto;
}}
.timeline-column {{
flex: 1 !important;
max-height: 300px;
}}
#attacker-map {{
height: 350px !important;
}}
.leaflet-popup-content {{
min-width: 200px !important;
}}
.ip-marker {{
font-size: 8px;
}}
.ip-detail-content {{
padding: 20px;
max-width: 95%;
max-height: 85vh;
}}
.download-btn {{
padding: 6px 12px;
font-size: 12px;
}}
}}
/* Mobile Optimization - Small phones (480px and down) */
@media (max-width: 480px) {{
body {{
padding: 8px;
}}
h1 {{
font-size: 20px;
margin-bottom: 15px;
}}
.stats-grid {{
grid-template-columns: 1fr;
gap: 10px;
margin-bottom: 15px;
}}
.stat-value {{
font-size: 24px;
}}
.stat-card {{
padding: 12px;
}}
.stat-label {{
font-size: 12px;
}}
.table-container {{
padding: 10px;
margin-bottom: 12px;
border-radius: 4px;
}}
table {{
font-size: 12px;
}}
th, td {{
padding: 8px 4px;
}}
th {{
position: relative;
}}
th.sortable::after {{
right: 4px;
font-size: 10px;
}}
h2 {{
font-size: 16px;
margin-bottom: 12px;
}}
.tabs-container {{
gap: 0;
}}
.tab-button {{
padding: 10px 12px;
font-size: 11px;
flex: 1;
}}
.ip-row {{
display: block;
margin-bottom: 10px;
background: #1c2128;
padding: 10px;
border-radius: 4px;
}}
.ip-row td {{
display: block;
padding: 4px 0;
border: none;
}}
.ip-row td::before {{
content: attr(data-label);
font-weight: bold;
color: #8b949e;
margin-right: 8px;
}}
.ip-clickable {{
display: inline-block;
}}
.ip-stats-dropdown {{
flex-direction: column;
gap: 12px;
font-size: 12px;
}}
.stats-left {{
flex: 1;
}}
.stats-right {{
flex: 0 0 auto;
width: 100%;
}}
.radar-chart {{
width: 140px;
height: 140px;
}}
.radar-legend {{
margin-top: 8px;
font-size: 10px;
}}
.stat-row {{
padding: 4px 0;
}}
.stat-label-sm {{
font-size: 12px;
}}
.stat-value-sm {{
font-size: 13px;
}}
.category-badge {{
padding: 3px 6px;
font-size: 10px;
}}
.timeline-container {{
flex-direction: column;
gap: 12px;
min-height: auto;
}}
.timeline-column {{
flex: 1 !important;
max-height: 250px;
font-size: 11px;
}}
.timeline-header {{
font-size: 12px;
margin-bottom: 8px;
}}
.timeline-item {{
padding-bottom: 10px;
font-size: 11px;
}}
.timeline-marker {{
left: -19px;
width: 12px;
height: 12px;
}}
.reputation-badge {{
display: block;
margin-bottom: 6px;
margin-right: 0;
font-size: 10px;
}}
#attacker-map {{
height: 300px !important;
}}
.leaflet-popup-content {{
min-width: 150px !important;
}}
.ip-marker {{
font-size: 7px;
}}
.ip-detail-modal {{
justify-content: flex-end;
align-items: flex-end;
}}
.ip-detail-content {{
padding: 15px;
max-width: 100%;
max-height: 90vh;
border-radius: 8px 8px 0 0;
width: 100%;
}}
.download-btn {{
padding: 6px 10px;
font-size: 11px;
}}
.github-logo {{
font-size: 12px;
}}
.github-logo svg {{
width: 24px;
height: 24px;
}}
}}
/* Landscape mode optimization */
@media (max-height: 600px) and (orientation: landscape) {{
body {{
padding: 8px;
}}
h1 {{
margin-bottom: 10px;
font-size: 18px;
}}
.stats-grid {{
margin-bottom: 10px;
gap: 8px;
}}
.stat-value {{
font-size: 20px;
}}
.stat-card {{
padding: 8px;
}}
#attacker-map {{
height: 250px !important;
}}
.ip-stats-dropdown {{
gap: 10px;
}}
.radar-chart {{
width: 120px;
height: 120px;
}}
}}
/* Touch-friendly optimizations */
@media (hover: none) and (pointer: coarse) {{
.ip-clickable {{
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(88, 166, 255, 0.2);
}}
.tab-button {{
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(88, 166, 255, 0.2);
padding: 14px 18px;
}}
.download-btn {{
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(36, 134, 54, 0.3);
}}
input[type="checkbox"] {{
width: 18px;
height: 18px;
cursor: pointer;
}}
}}
</style> </style>
</head> </head>
<body> <body>
@@ -1081,6 +1405,20 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
html += '</div>'; html += '</div>';
}} }}
if (stats.country) {{
html += '<div class="stat-row">';
html += '<span class="stat-label-sm">Country:</span>';
html += `<span class="stat-value-sm">${{stats.country}}</span>`;
html += '</div>';
}}
if (stats.reverse) {{
html += '<div class="stat-row">';
html += '<span class="stat-label-sm">Reverse DNS:</span>';
html += `<span class="stat-value-sm">${{stats.reverse}}</span>`;
html += '</div>';
}}
if (stats.asn_org) {{ if (stats.asn_org) {{
html += '<div class="stat-row">'; html += '<div class="stat-row">';
html += '<span class="stat-label-sm">ASN Org:</span>'; html += '<span class="stat-label-sm">ASN Org:</span>';
@@ -1088,6 +1426,19 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
html += '</div>'; html += '</div>';
}} }}
if (stats.is_proxy !== undefined || stats.is_hosting !== undefined) {{
const flags = [];
if (stats.is_proxy) flags.push('Proxy');
if (stats.is_hosting) flags.push('Hosting');
if (flags.length > 0) {{
html += '<div class="stat-row">';
html += '<span class="stat-label-sm">Flags: <span title="Proxy: IP is using a proxy service. Hosting: IP is from a hosting/cloud provider" style="cursor: help; color: #58a6ff; font-weight: bold;">ⓘ</span></span>';
html += `<span class="stat-value-sm">${{flags.join(', ')}}</span>`;
html += '</div>';
}}
}}
if (stats.reputation_score !== null && stats.reputation_score !== undefined) {{ if (stats.reputation_score !== null && stats.reputation_score !== undefined) {{
html += '<div class="stat-row">'; html += '<div class="stat-row">';
html += '<span class="stat-label-sm">Reputation Score:</span>'; html += '<span class="stat-label-sm">Reputation Score:</span>';
@@ -1125,8 +1476,6 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
if (change.old_category) {{ if (change.old_category) {{
html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`; html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`;
html += '<span style="color: #8b949e; margin: 0 4px;">→</span>'; html += '<span style="color: #8b949e; margin: 0 4px;">→</span>';
}} else {{
html += '<span style="color: #8b949e;">Initial:</span>';
}} }}
html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`; html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`;
@@ -1142,16 +1491,35 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
html += '<div class="timeline-column">'; html += '<div class="timeline-column">';
if (stats.list_on && Object.keys(stats.list_on).length > 0) {{ if (stats.list_on && Object.keys(stats.list_on).length > 0) {{
html += '<div class="timeline-header">Listed On</div>'; // Filter out is_hosting and is_proxy from the displayed list
const sortedSources = Object.entries(stats.list_on).sort((a, b) => a[0].localeCompare(b[0])); const filteredList = Object.entries(stats.list_on).filter(([source, data]) =>
source !== 'is_hosting' && source !== 'is_proxy'
);
sortedSources.forEach(([source, url]) => {{ if (filteredList.length > 0) {{
if (url && url !== 'N/A') {{ html += '<div class="timeline-header">Listed On</div>';
html += `<a href="${{url}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`; const sortedSources = filteredList.sort((a, b) => a[0].localeCompare(b[0]));
}} else {{
html += `<span class="reputation-badge">${{source}}</span>`; sortedSources.forEach(([source, data]) => {{
}} // Handle both string URLs and nested object data
}}); if (typeof data === 'string' && data !== 'N/A') {{
html += `<a href="${{data}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`;
}} else if (typeof data === 'object' && data !== null) {{
// For nested blocklist data, extract source_link if available
const sourceLink = data['__source_link'] || data.source_link;
if (sourceLink) {{
html += `<a href="${{sourceLink}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`;
}} else {{
html += `<span class="reputation-badge">${{source}}</span>`;
}}
}} else {{
html += `<span class="reputation-badge">${{source}}</span>`;
}}
}});
}} else if (stats.country_code || stats.asn) {{
html += '<div class="timeline-header">Reputation</div>';
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';
}}
}} else if (stats.country_code || stats.asn) {{ }} else if (stats.country_code || stats.asn) {{
html += '<div class="timeline-header">Reputation</div>'; html += '<div class="timeline-header">Reputation</div>';
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>'; html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';

View File

@@ -35,7 +35,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from database import get_database from database import get_database
from logger import get_app_logger from logger import get_app_logger
from geo_utils import extract_city_from_coordinates from geo_utils import extract_geolocation_from_ip
# ---------------------- # ----------------------
# TEST DATA GENERATORS # TEST DATA GENERATORS
@@ -44,6 +44,12 @@ from geo_utils import extract_city_from_coordinates
# Fake IPs for testing - geolocation data will be fetched from API # Fake IPs for testing - geolocation data will be fetched from API
# These are real public IPs from various locations around the world # These are real public IPs from various locations around the world
FAKE_IPS = [ FAKE_IPS = [
# VPN
"31.13.189.236",
"37.120.215.246",
"37.120.215.247",
"37.120.215.248",
"37.120.215.249",
# United States # United States
"45.142.120.10", "45.142.120.10",
"107.189.10.143", "107.189.10.143",
@@ -226,8 +232,7 @@ def cleanup_database(db_manager, app_logger):
def fetch_geolocation_from_api(ip: str, app_logger) -> tuple: def fetch_geolocation_from_api(ip: str, app_logger) -> tuple:
""" """
Fetch geolocation data from the IP reputation API. Fetch geolocation data using ip-api.com.
Uses the most recent result and extracts city from coordinates.
Args: Args:
ip: IP address to lookup ip: IP address to lookup
@@ -237,28 +242,15 @@ def fetch_geolocation_from_api(ip: str, app_logger) -> tuple:
Tuple of (country_code, city, asn, asn_org) or None if failed Tuple of (country_code, city, asn, asn_org) or None if failed
""" """
try: try:
api_url = "https://iprep.lcrawl.com/api/iprep/" geoloc_data = extract_geolocation_from_ip(ip)
params = {"cidr": ip}
headers = {"Content-Type": "application/json"} if geoloc_data:
response = requests.get(api_url, headers=headers, params=params, timeout=10) country_code = geoloc_data.get("country_code")
city = geoloc_data.get("city")
if response.status_code == 200: asn = geoloc_data.get("asn")
payload = response.json() asn_org = geoloc_data.get("org")
if payload.get("results"):
results = payload["results"] return (country_code, city, asn, asn_org)
# Get the most recent result (first in list, sorted by record_added)
most_recent = results[0]
geoip_data = most_recent.get("geoip_data", {})
country_code = geoip_data.get("country_iso_code")
asn = geoip_data.get("asn_autonomous_system_number")
asn_org = geoip_data.get("asn_autonomous_system_organization")
# Extract city from coordinates using reverse geocoding
city = extract_city_from_coordinates(geoip_data)
return (country_code, city, asn, asn_org)
except requests.RequestException as e: except requests.RequestException as e:
app_logger.warning(f"Failed to fetch geolocation for {ip}: {e}") app_logger.warning(f"Failed to fetch geolocation for {ip}: {e}")
except Exception as e: except Exception as e:
@@ -313,9 +305,9 @@ def generate_fake_data(
request_counts = [] request_counts = []
for i in range(len(selected_ips)): for i in range(len(selected_ips)):
if i < len(selected_ips) // 5: # 20% high-traffic IPs if i < len(selected_ips) // 5: # 20% high-traffic IPs
count = random.randint(1000, 10000)
elif i < len(selected_ips) // 2: # 30% medium-traffic IPs
count = random.randint(100, 1000) count = random.randint(100, 1000)
elif i < len(selected_ips) // 2: # 30% medium-traffic IPs
count = random.randint(10, 100)
else: # 50% low-traffic IPs else: # 50% low-traffic IPs
count = random.randint(5, 100) count = random.randint(5, 100)
request_counts.append(count) request_counts.append(count)