first commit
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
venv
|
||||||
|
.env
|
||||||
|
README.md
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
__pycache__
|
||||||
|
venv/
|
||||||
|
env
|
||||||
|
.env
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM python:3.14-slim
|
||||||
|
WORKDIR /app
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8798
|
||||||
|
CMD ["python", "app.py"]
|
||||||
142
README.md
Normal file
142
README.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# IP WHOIS Analyzer Pro
|
||||||
|
|
||||||
|
IP WHOIS Analyzer Pro is a small Flask-based web application for bulk IP analysis.
|
||||||
|
It queries WHOIS/ASN data, lets you filter results, and generates ready-to-use firewall rules.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Paste a free-form list of IPv4 addresses (mixed separators).
|
||||||
|
- Bulk lookup via Team Cymru WHOIS, with classic WHOIS as a fallback.
|
||||||
|
- Per-IP details: ASN, owner, country, network/prefix.
|
||||||
|
- Interactive filters by:
|
||||||
|
- Country
|
||||||
|
- ASN
|
||||||
|
- Owner
|
||||||
|
- Export of selected/filtered IPs to:
|
||||||
|
- IPSet (with timeout)
|
||||||
|
- iptables
|
||||||
|
- Nginx `deny`
|
||||||
|
- Apache access rules
|
||||||
|
- Firewalld rich rules
|
||||||
|
- MikroTik RouterOS address-list + firewall filter
|
||||||
|
- CIDR network list
|
||||||
|
- CSV
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- System packages:
|
||||||
|
- `whois` (Debian/Ubuntu: `sudo apt install whois`)
|
||||||
|
- Python packages:
|
||||||
|
- `flask`
|
||||||
|
- `requests`
|
||||||
|
|
||||||
|
You can install Python dependencies with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
(Or, if you do not use the file, install manually:)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install flask requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python ip_analyzer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the app listens on:
|
||||||
|
|
||||||
|
- Web UI: <http://localhost:5000>
|
||||||
|
- API docs: <http://localhost:5000/api>
|
||||||
|
|
||||||
|
You can change host/port in `ip_analyzer.py` if needed.
|
||||||
|
|
||||||
|
## Usage (Web UI)
|
||||||
|
|
||||||
|
1. Open <http://localhost:5000> in your browser.
|
||||||
|
2. Paste IP addresses into the textarea.
|
||||||
|
- Lines, spaces, commas, semicolons and tabs are all accepted.
|
||||||
|
3. Click **"Analyze IP Addresses"**.
|
||||||
|
4. Use the filters (countries, ASNs, owners) to narrow down results.
|
||||||
|
5. Select/deselect IPs in the table if you only want a subset.
|
||||||
|
6. Choose an export format (IPSet, iptables, Nginx, etc.) and copy or download the output.
|
||||||
|
|
||||||
|
## Usage (API)
|
||||||
|
|
||||||
|
### Analyze IPs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/api/analyze \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ips": "1.1.1.1, 8.8.8.8, 9.9.9.9"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Example in Python:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
"http://localhost:5000/api/analyze",
|
||||||
|
json={"ips": "1.1.1.1, 8.8.8.8, 9.9.9.9"},
|
||||||
|
)
|
||||||
|
data = resp.json()
|
||||||
|
print("Total IPs:", data["stats"]["total"])
|
||||||
|
for row in data["results"]:
|
||||||
|
print(row["ip"], "->", row["country"], row["asn"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export IPSet Rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/api/export/ipset \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ips": ["1.1.1.1", "8.8.8.8"], "timeout": 86400}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Other export endpoints follow the same pattern:
|
||||||
|
|
||||||
|
- `/api/export/iptables`
|
||||||
|
- `/api/export/nginx`
|
||||||
|
- `/api/export/apache`
|
||||||
|
- `/api/export/firewalld`
|
||||||
|
- `/api/export/mikrotik`
|
||||||
|
- `/api/export/cidr`
|
||||||
|
- `/api/export/csv`
|
||||||
|
|
||||||
|
Refer to the web API documentation at `/api` for full examples.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ip-whois-analyzer/
|
||||||
|
├── ip_analyzer.py # Main Flask application
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── README.md # This file
|
||||||
|
├── templates/
|
||||||
|
│ ├── base.html # Base template
|
||||||
|
│ ├── index.html # Main web interface
|
||||||
|
│ └── api.html # API documentation
|
||||||
|
└── static/
|
||||||
|
├── css/
|
||||||
|
│ └── style.css # Custom styles
|
||||||
|
└── js/
|
||||||
|
├── main.js # Main app logic
|
||||||
|
└── api.js # API docs logic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- The application is designed for local/admin use.
|
||||||
|
- If you expose it externally, put it behind proper authentication and TLS.
|
||||||
|
- Generated rules should be reviewed before applying to production firewalls.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided as-is, without any warranty.
|
||||||
|
Use at your own risk.
|
||||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
ip-whois:
|
||||||
|
build: .
|
||||||
|
container_name: ip-whois
|
||||||
|
ports:
|
||||||
|
- "8799:8799"
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monitoring:
|
||||||
|
driver: bridge
|
||||||
732
ip_analyzer.py
Normal file
732
ip_analyzer.py
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
IP WHOIS Analyzer
|
||||||
|
Complete Flask application with RESTful API
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, jsonify, Response
|
||||||
|
import re
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
from collections import defaultdict, Counter
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# UTILITY FUNCTIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def parse_ip_list(text):
|
||||||
|
"""
|
||||||
|
Parse IPs from text with various separators.
|
||||||
|
Supports: comma, semicolon, space, newline, tab
|
||||||
|
"""
|
||||||
|
# Replace common separators with newlines
|
||||||
|
text = re.sub(r'[,;|\t]+', '\n', text)
|
||||||
|
lines = text.strip().split('\n')
|
||||||
|
|
||||||
|
ips = []
|
||||||
|
for line in lines:
|
||||||
|
# Extract IPs using regex
|
||||||
|
found_ips = re.findall(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b', line)
|
||||||
|
ips.extend(found_ips)
|
||||||
|
|
||||||
|
# Validate IPs
|
||||||
|
valid_ips = []
|
||||||
|
for ip in ips:
|
||||||
|
try:
|
||||||
|
ipaddress.IPv4Address(ip)
|
||||||
|
valid_ips.append(ip)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return list(set(valid_ips)) # Remove duplicates
|
||||||
|
|
||||||
|
|
||||||
|
def whois_lookup(ip):
|
||||||
|
"""Perform WHOIS lookup for single IP address"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['whois', ip],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return ""
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("WARNING: whois command not found. Install it: apt install whois")
|
||||||
|
return ""
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WHOIS error for {ip}: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_whois(whois_output):
|
||||||
|
"""Extract relevant information from WHOIS output"""
|
||||||
|
info = {
|
||||||
|
'org': 'Unknown',
|
||||||
|
'country': 'Unknown',
|
||||||
|
'netname': 'Unknown',
|
||||||
|
'asn': 'Unknown',
|
||||||
|
'cidr': 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in whois_output.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Organization
|
||||||
|
if line.startswith('Organization:') or line.startswith('org-name:'):
|
||||||
|
info['org'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# Country
|
||||||
|
elif line.startswith('Country:') or line.startswith('country:'):
|
||||||
|
info['country'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# Network name
|
||||||
|
elif line.startswith('NetName:') or line.startswith('netname:'):
|
||||||
|
info['netname'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# ASN
|
||||||
|
elif line.startswith('OriginAS:') or 'origin:' in line.lower():
|
||||||
|
asn = re.search(r'AS\d+', line)
|
||||||
|
if asn:
|
||||||
|
info['asn'] = asn.group()
|
||||||
|
|
||||||
|
# CIDR
|
||||||
|
elif line.startswith('CIDR:') or line.startswith('inetnum:'):
|
||||||
|
info['cidr'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def cymru_lookup(ips):
|
||||||
|
"""
|
||||||
|
Bulk ASN lookup using Team Cymru WHOIS service.
|
||||||
|
Much faster than individual WHOIS lookups.
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
if not ips:
|
||||||
|
return results
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build query
|
||||||
|
query = "begin\nverbose\n" + "\n".join(ips) + "\nend\n"
|
||||||
|
|
||||||
|
# Connect to Team Cymru
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(10)
|
||||||
|
sock.connect(('whois.cymru.com', 43))
|
||||||
|
sock.sendall(query.encode())
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
response = b""
|
||||||
|
while True:
|
||||||
|
data = sock.recv(4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
response += data
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
for line in response.decode('utf-8', errors='ignore').split('\n'):
|
||||||
|
if '|' in line and not line.startswith('AS'):
|
||||||
|
parts = [p.strip() for p in line.split('|')]
|
||||||
|
if len(parts) >= 5:
|
||||||
|
asn, ip, prefix, cc, owner = parts[0], parts[1], parts[2], parts[3], parts[4]
|
||||||
|
|
||||||
|
# Format ASN
|
||||||
|
if asn.isdigit():
|
||||||
|
asn = f"AS{asn}"
|
||||||
|
|
||||||
|
results[ip] = {
|
||||||
|
'asn': asn,
|
||||||
|
'prefix': prefix,
|
||||||
|
'country': cc,
|
||||||
|
'owner': owner
|
||||||
|
}
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
|
print("WARNING: Team Cymru timeout. Using fallback WHOIS.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Team Cymru lookup error: {e}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_ip(ip, cymru_data=None):
|
||||||
|
"""
|
||||||
|
Analyze single IP address.
|
||||||
|
Uses Team Cymru data if available, falls back to WHOIS.
|
||||||
|
"""
|
||||||
|
info = {
|
||||||
|
'ip': ip,
|
||||||
|
'asn': 'Unknown',
|
||||||
|
'owner': 'Unknown',
|
||||||
|
'country': 'Unknown',
|
||||||
|
'network': 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try Team Cymru data first
|
||||||
|
if cymru_data and ip in cymru_data:
|
||||||
|
data = cymru_data[ip]
|
||||||
|
info['asn'] = data.get('asn', 'Unknown')
|
||||||
|
info['owner'] = data.get('owner', 'Unknown')
|
||||||
|
info['country'] = data.get('country', 'Unknown')
|
||||||
|
info['network'] = data.get('prefix', 'Unknown')
|
||||||
|
|
||||||
|
# Fallback to WHOIS
|
||||||
|
else:
|
||||||
|
whois_output = whois_lookup(ip)
|
||||||
|
if whois_output:
|
||||||
|
parsed = parse_whois(whois_output)
|
||||||
|
info['asn'] = parsed['asn']
|
||||||
|
info['owner'] = parsed['org'] if parsed['org'] != 'Unknown' else parsed['netname']
|
||||||
|
info['country'] = parsed['country']
|
||||||
|
info['network'] = parsed['cidr']
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def apply_filters(results, filters):
|
||||||
|
"""
|
||||||
|
Apply filters to results.
|
||||||
|
Filters: countries, asns, owners
|
||||||
|
"""
|
||||||
|
countries = set(filters.get('countries', []))
|
||||||
|
asns = set(filters.get('asns', []))
|
||||||
|
owners = set(filters.get('owners', []))
|
||||||
|
|
||||||
|
# No filters = return all
|
||||||
|
if not (countries or asns or owners):
|
||||||
|
return results
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
for item in results:
|
||||||
|
# AND logic: all specified filters must match
|
||||||
|
if (not countries or item['country'] in countries) and \
|
||||||
|
(not asns or item['asn'] in asns) and \
|
||||||
|
(not owners or item['owner'] in owners):
|
||||||
|
filtered.append(item)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# EXPORT GENERATORS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def generate_ipset(ips, timeout=86400):
|
||||||
|
"""Generate IPSet rules with timeout"""
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
rules = f"""#!/bin/bash
|
||||||
|
# IPSet Rules - Generated {timestamp}
|
||||||
|
# Total IPs: {len(ips)}
|
||||||
|
# Timeout: {timeout} seconds ({timeout//3600} hours)
|
||||||
|
|
||||||
|
# Create ipset
|
||||||
|
ipset create blocked_ips hash:ip timeout {timeout} maxelem 1000000
|
||||||
|
|
||||||
|
# Add IPs to set
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f"ipset add blocked_ips {ip}\n"
|
||||||
|
|
||||||
|
rules += """
|
||||||
|
# Apply iptables rules
|
||||||
|
iptables -I INPUT -m set --match-set blocked_ips src -j DROP
|
||||||
|
iptables -I FORWARD -m set --match-set blocked_ips src -j DROP
|
||||||
|
|
||||||
|
echo "IPSet created and iptables rules applied"
|
||||||
|
echo "To remove: ipset destroy blocked_ips"
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def generate_iptables(ips):
|
||||||
|
"""Generate iptables DROP rules"""
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
rules = f"""#!/bin/bash
|
||||||
|
# iptables Rules - Generated {timestamp}
|
||||||
|
# Total IPs: {len(ips)}
|
||||||
|
|
||||||
|
# INPUT chain (incoming connections)
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f"iptables -A INPUT -s {ip} -j DROP\n"
|
||||||
|
|
||||||
|
rules += "\n# FORWARD chain (routed traffic)\n"
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f"iptables -A FORWARD -s {ip} -j DROP\n"
|
||||||
|
|
||||||
|
rules += """
|
||||||
|
# Save rules
|
||||||
|
iptables-save > /etc/iptables/rules.v4
|
||||||
|
|
||||||
|
echo "iptables rules applied and saved"
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def generate_nginx(ips):
|
||||||
|
"""Generate Nginx deny directives"""
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
rules = f"""# Nginx Deny Rules - Generated {timestamp}
|
||||||
|
# Total IPs: {len(ips)}
|
||||||
|
#
|
||||||
|
# Usage: Include in http or server block
|
||||||
|
# include /etc/nginx/conf.d/blocked_ips.conf;
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f"deny {ip};\n"
|
||||||
|
|
||||||
|
rules += "\n# After adding rules, reload nginx:\n"
|
||||||
|
rules += "# nginx -t && nginx -s reload\n"
|
||||||
|
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def generate_apache(ips):
|
||||||
|
"""Generate Apache deny directives"""
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
rules = f"""# Apache Deny Rules - Generated {timestamp}
|
||||||
|
# Total IPs: {len(ips)}
|
||||||
|
#
|
||||||
|
# Usage: Add to .htaccess or VirtualHost configuration
|
||||||
|
|
||||||
|
<RequireAll>
|
||||||
|
Require all granted
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f" Require not ip {ip}\n"
|
||||||
|
|
||||||
|
rules += """</RequireAll>
|
||||||
|
|
||||||
|
# After adding rules, restart apache:
|
||||||
|
# systemctl restart apache2
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def generate_firewalld(ips):
|
||||||
|
"""Generate Firewalld rich rules"""
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
rules = f"""#!/bin/bash
|
||||||
|
# Firewalld Rules - Generated {timestamp}
|
||||||
|
# Total IPs: {len(ips)}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f"firewall-cmd --permanent --add-rich-rule=\"rule family='ipv4' source address='{ip}' reject\"\n"
|
||||||
|
|
||||||
|
rules += """
|
||||||
|
# Reload firewall
|
||||||
|
firewall-cmd --reload
|
||||||
|
|
||||||
|
echo "Firewalld rules applied"
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mikrotik(ips):
|
||||||
|
"""Generate MikroTik RouterOS commands"""
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
rules = f"""# MikroTik RouterOS Configuration - Generated {timestamp}
|
||||||
|
# Total IPs: {len(ips)}
|
||||||
|
#
|
||||||
|
# Usage: Copy and paste into RouterOS Terminal
|
||||||
|
|
||||||
|
/ip firewall address-list
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
rules += f"add list=blocked_ips address={ip} comment=\"Auto-blocked {timestamp}\"\n"
|
||||||
|
|
||||||
|
rules += """
|
||||||
|
# Create firewall filter rules (if not exists)
|
||||||
|
/ip firewall filter
|
||||||
|
add chain=input src-address-list=blocked_ips action=drop comment="Drop blocked IPs - input"
|
||||||
|
add chain=forward src-address-list=blocked_ips action=drop comment="Drop blocked IPs - forward"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
/ip firewall address-list print where list=blocked_ips
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cidr(results):
|
||||||
|
"""Generate list of unique CIDR networks"""
|
||||||
|
networks = list(set([r['network'] for r in results if r['network'] != 'Unknown']))
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
output = f"""# CIDR Networks - Generated {timestamp}
|
||||||
|
# Total unique networks: {len(networks)}
|
||||||
|
#
|
||||||
|
# One network per line
|
||||||
|
|
||||||
|
"""
|
||||||
|
output += "\n".join(sorted(networks))
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def generate_csv(results):
|
||||||
|
"""Generate CSV export"""
|
||||||
|
csv = "IP,ASN,Owner,Country,Network\n"
|
||||||
|
|
||||||
|
for item in results:
|
||||||
|
# Escape CSV fields
|
||||||
|
ip = item['ip']
|
||||||
|
asn = item['asn'].replace('"', '""')
|
||||||
|
owner = item['owner'].replace('"', '""')
|
||||||
|
country = item['country']
|
||||||
|
network = item['network']
|
||||||
|
|
||||||
|
csv += f'"{ip}","{asn}","{owner}","{country}","{network}"\n'
|
||||||
|
|
||||||
|
return csv
|
||||||
|
|
||||||
|
def get_file_hash(filepath):
|
||||||
|
"""Generate MD5 hash for cache busting"""
|
||||||
|
with open(filepath, 'rb') as f:
|
||||||
|
return hashlib.md5(f.read()).hexdigest()[:8]
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_static_hash():
|
||||||
|
"""Inject static file hash into templates"""
|
||||||
|
def static_hash(filename):
|
||||||
|
filepath = os.path.join(app.static_folder, filename)
|
||||||
|
file_hash = get_file_hash(filepath)
|
||||||
|
return f"/static/{filename}?v={file_hash}"
|
||||||
|
return dict(static_hash=static_hash)
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def add_header(response):
|
||||||
|
if request.path.startswith('/static/'):
|
||||||
|
# Clear default cache control
|
||||||
|
response.cache_control.no_cache = None
|
||||||
|
response.cache_control.no_store = None
|
||||||
|
|
||||||
|
# Set static file cache
|
||||||
|
response.cache_control.max_age = 31536000
|
||||||
|
response.cache_control.public = True
|
||||||
|
|
||||||
|
# Remove Content-Disposition header
|
||||||
|
response.headers.pop('Content-Disposition', None)
|
||||||
|
else:
|
||||||
|
# Dynamic content: no cache
|
||||||
|
response.cache_control.no_cache = True
|
||||||
|
response.cache_control.no_store = True
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# WEB ROUTES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Main application interface"""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/favicon.ico')
|
||||||
|
def favicon():
|
||||||
|
"""Handle favicon requests - return 204 No Content instead of 404"""
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
@app.route('/api')
|
||||||
|
def api_docs():
|
||||||
|
"""API documentation page"""
|
||||||
|
return render_template('api.html')
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# API ENDPOINTS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.route('/api/analyze', methods=['POST'])
|
||||||
|
def api_analyze():
|
||||||
|
"""
|
||||||
|
Analyze IP addresses.
|
||||||
|
|
||||||
|
POST /api/analyze
|
||||||
|
{
|
||||||
|
"ips": "1.1.1.1, 8.8.8.8, 9.9.9.9"
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"results": [...],
|
||||||
|
"stats": {...}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'Invalid JSON'}), 400
|
||||||
|
|
||||||
|
ip_text = data.get('ips', '')
|
||||||
|
|
||||||
|
# Parse IPs
|
||||||
|
ips = parse_ip_list(ip_text)
|
||||||
|
|
||||||
|
if not ips:
|
||||||
|
return jsonify({'error': 'No valid IPs found'}), 400
|
||||||
|
|
||||||
|
# Bulk lookup via Team Cymru
|
||||||
|
print(f"Analyzing {len(ips)} IPs via Team Cymru...")
|
||||||
|
cymru_data = cymru_lookup(ips)
|
||||||
|
|
||||||
|
# Analyze each IP
|
||||||
|
results = []
|
||||||
|
for ip in ips:
|
||||||
|
info = analyze_ip(ip, cymru_data)
|
||||||
|
results.append(info)
|
||||||
|
|
||||||
|
# Generate statistics
|
||||||
|
stats = {
|
||||||
|
'total': len(results),
|
||||||
|
'countries': dict(Counter([r['country'] for r in results])),
|
||||||
|
'asns': dict(Counter([r['asn'] for r in results])),
|
||||||
|
'owners': dict(Counter([r['owner'] for r in results]))
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Analysis complete: {len(results)} IPs, {len(stats['countries'])} countries")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'results': results,
|
||||||
|
'stats': stats
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/filter', methods=['POST'])
|
||||||
|
def api_filter():
|
||||||
|
"""
|
||||||
|
Filter results.
|
||||||
|
|
||||||
|
POST /api/filter
|
||||||
|
{
|
||||||
|
"results": [...],
|
||||||
|
"filters": {
|
||||||
|
"countries": ["CN", "RU"],
|
||||||
|
"asns": ["AS4134"],
|
||||||
|
"owners": ["CHINANET"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"filtered": [...],
|
||||||
|
"count": 15
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'Invalid JSON'}), 400
|
||||||
|
|
||||||
|
results = data.get('results', [])
|
||||||
|
filters = data.get('filters', {})
|
||||||
|
|
||||||
|
filtered = apply_filters(results, filters)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'filtered': filtered,
|
||||||
|
'count': len(filtered)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/ipset', methods=['POST'])
|
||||||
|
def api_export_ipset():
|
||||||
|
"""
|
||||||
|
Export IPSet rules.
|
||||||
|
|
||||||
|
POST /api/export/ipset
|
||||||
|
{
|
||||||
|
"ips": ["1.1.1.1", "8.8.8.8"],
|
||||||
|
"timeout": 86400
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
ips = data.get('ips', [])
|
||||||
|
timeout = data.get('timeout', 86400)
|
||||||
|
|
||||||
|
rules = generate_ipset(ips, timeout)
|
||||||
|
|
||||||
|
return Response(rules, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/iptables', methods=['POST'])
|
||||||
|
def api_export_iptables():
|
||||||
|
"""Export iptables rules"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
ips = data.get('ips', [])
|
||||||
|
rules = generate_iptables(ips)
|
||||||
|
|
||||||
|
return Response(rules, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/nginx', methods=['POST'])
|
||||||
|
def api_export_nginx():
|
||||||
|
"""Export Nginx deny rules"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
ips = data.get('ips', [])
|
||||||
|
rules = generate_nginx(ips)
|
||||||
|
|
||||||
|
return Response(rules, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/apache', methods=['POST'])
|
||||||
|
def api_export_apache():
|
||||||
|
"""Export Apache deny rules"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
ips = data.get('ips', [])
|
||||||
|
rules = generate_apache(ips)
|
||||||
|
|
||||||
|
return Response(rules, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/firewalld', methods=['POST'])
|
||||||
|
def api_export_firewalld():
|
||||||
|
"""Export Firewalld rules"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
ips = data.get('ips', [])
|
||||||
|
rules = generate_firewalld(ips)
|
||||||
|
|
||||||
|
return Response(rules, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/mikrotik', methods=['POST'])
|
||||||
|
def api_export_mikrotik():
|
||||||
|
"""Export MikroTik rules"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
ips = data.get('ips', [])
|
||||||
|
rules = generate_mikrotik(ips)
|
||||||
|
|
||||||
|
return Response(rules, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/cidr', methods=['POST'])
|
||||||
|
def api_export_cidr():
|
||||||
|
"""Export CIDR list"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
results = data.get('results', [])
|
||||||
|
output = generate_cidr(results)
|
||||||
|
|
||||||
|
return Response(output, mimetype='text/plain')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/csv', methods=['POST'])
|
||||||
|
def api_export_csv():
|
||||||
|
"""Export CSV"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response('Invalid JSON', status=400)
|
||||||
|
|
||||||
|
results = data.get('results', [])
|
||||||
|
csv = generate_csv(results)
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
csv,
|
||||||
|
mimetype='text/csv',
|
||||||
|
headers={'Content-Disposition': f'attachment; filename=ip-analysis-{timestamp}.csv'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Legacy endpoint for backward compatibility
|
||||||
|
@app.route('/analyze', methods=['POST'])
|
||||||
|
def analyze():
|
||||||
|
"""Legacy analyze endpoint - redirects to /api/analyze"""
|
||||||
|
return api_analyze()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ERROR HANDLERS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(e):
|
||||||
|
return jsonify({'error': 'Endpoint not found'}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def server_error(e):
|
||||||
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("=" * 70)
|
||||||
|
print("IP WHOIS Analyzer - Starting")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
print("Interface: http://localhost:8799")
|
||||||
|
print("API Docs: http://localhost:8799/api")
|
||||||
|
print()
|
||||||
|
print("Press Ctrl+C to stop")
|
||||||
|
print()
|
||||||
|
|
||||||
|
app.run(
|
||||||
|
debug=True,
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=8799
|
||||||
|
)
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
requests==2.31.0
|
||||||
294
static/css/style.css
Normal file
294
static/css/style.css
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
/* IP WHOIS Analyzer Pro - Custom Styles */
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] {
|
||||||
|
--accent: #00d4ff;
|
||||||
|
--accent-rgb: 0, 212, 255;
|
||||||
|
--accent-hover: #00b8e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="light"] {
|
||||||
|
--accent: #0d6efd;
|
||||||
|
--accent-rgb: 13, 110, 253;
|
||||||
|
--accent-hover: #0b5ed7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Gradient */
|
||||||
|
.header-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--accent) 0%, #667eea 100%);
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(var(--accent-rgb), 0.2);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-gradient::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
right: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||||
|
animation: pulse 15s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||||
|
50% { transform: scale(1.1); opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-gradient h1,
|
||||||
|
.header-gradient p {
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.3s ease;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.1) 0%, rgba(102,126,234,0.1) 100%);
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-card {
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: rgba(var(--accent-rgb), 0.1);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-cell {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge-custom {
|
||||||
|
background: rgba(var(--accent-rgb), 0.2);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-badge {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-post {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-get {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter chips */
|
||||||
|
.filter-chip {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 0.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 2px 8px rgba(var(--accent-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export section */
|
||||||
|
.export-section {
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
code {
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
position: relative;
|
||||||
|
background: var(--bs-tertiary-bg);
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme toggle */
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 1050;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.spinner-border {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.2);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List group */
|
||||||
|
.list-group-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form controls */
|
||||||
|
textarea.form-control,
|
||||||
|
input.form-control {
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control:focus,
|
||||||
|
input.form-control:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(var(--accent-rgb), 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats cards */
|
||||||
|
.stat-card h3 {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h6 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bs-tertiary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-left: 4px solid;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-gradient h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
.header-gradient p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.filter-chip {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
static/js/api.js
Normal file
129
static/js/api.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Copy code to clipboard
|
||||||
|
function copyCode(btn) {
|
||||||
|
const pre = btn.closest('pre');
|
||||||
|
const code = pre.querySelector('code').textContent;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
btn.classList.remove('btn-secondary');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
alert('Failed to copy to clipboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme toggle
|
||||||
|
function toggleTheme() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const currentTheme = html.getAttribute('data-bs-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-bs-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
|
const icon = document.getElementById('themeIcon');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = newTheme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved theme on page load
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
||||||
|
const icon = document.getElementById('themeIcon');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = savedTheme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize syntax highlighting
|
||||||
|
if (typeof hljs !== 'undefined') {
|
||||||
|
hljs.highlightAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth scroll for navigation links
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
inline: 'nearest'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight target card briefly
|
||||||
|
target.classList.add('border-primary');
|
||||||
|
setTimeout(() => {
|
||||||
|
target.classList.remove('border-primary');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Active navigation highlighting on scroll
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
const sections = document.querySelectorAll('.endpoint-card');
|
||||||
|
const navLinks = document.querySelectorAll('.list-group-item');
|
||||||
|
|
||||||
|
let current = '';
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const sectionTop = section.offsetTop;
|
||||||
|
const sectionHeight = section.clientHeight;
|
||||||
|
|
||||||
|
// Check if section is in viewport (with offset for navbar)
|
||||||
|
if (window.pageYOffset >= sectionTop - 120) {
|
||||||
|
current = section.getAttribute('id');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update active link
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.classList.remove('active');
|
||||||
|
if (link.getAttribute('href') === '#' + current) {
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test API endpoint directly from documentation (optional utility)
|
||||||
|
async function testEndpoint(endpoint, method = 'POST', body = {}) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: method,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
console.log('API Response:', data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick copy all code snippets (Ctrl+Shift+C)
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.ctrlKey && e.shiftKey && e.key === 'C') {
|
||||||
|
const allCode = Array.from(document.querySelectorAll('pre code'))
|
||||||
|
.map(code => code.textContent)
|
||||||
|
.join('\n\n' + '='.repeat(50) + '\n\n');
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(allCode).then(() => {
|
||||||
|
alert('All code snippets copied to clipboard!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
421
static/js/main.js
Normal file
421
static/js/main.js
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
let allResults = [];
|
||||||
|
let filteredResults = [];
|
||||||
|
let selectedFilters = {
|
||||||
|
countries: new Set(),
|
||||||
|
asns: new Set(),
|
||||||
|
owners: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Theme Management
|
||||||
|
function toggleTheme() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const currentTheme = html.getAttribute('data-bs-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-bs-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
|
const icon = document.getElementById('themeIcon');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = newTheme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved theme
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
||||||
|
const icon = document.getElementById('themeIcon');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = savedTheme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main Analysis Function
|
||||||
|
async function analyzeIPs() {
|
||||||
|
const input = document.getElementById('ipInput').value;
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const results = document.getElementById('results');
|
||||||
|
const error = document.getElementById('error');
|
||||||
|
|
||||||
|
if (!input.trim()) {
|
||||||
|
showError('⚠️ Please paste a list of IP addresses');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.classList.add('d-none');
|
||||||
|
results.classList.add('d-none');
|
||||||
|
loading.classList.remove('d-none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/analyze', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: input })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Analysis failed - check connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
allResults = data.results;
|
||||||
|
filteredResults = [...allResults];
|
||||||
|
|
||||||
|
displayResults(data);
|
||||||
|
} catch (e) {
|
||||||
|
showError('❌ ' + e.message);
|
||||||
|
} finally {
|
||||||
|
loading.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Results
|
||||||
|
function displayResults(data) {
|
||||||
|
displayStats(data.stats);
|
||||||
|
displayFilters(data.stats);
|
||||||
|
displayTable(filteredResults);
|
||||||
|
const resultsDiv = document.getElementById('results');
|
||||||
|
resultsDiv.classList.remove('d-none');
|
||||||
|
resultsDiv.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Statistics
|
||||||
|
function displayStats(stats) {
|
||||||
|
const statsDiv = document.getElementById('stats');
|
||||||
|
statsDiv.innerHTML = `
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6><i class="fas fa-globe text-primary"></i> Countries</h6>
|
||||||
|
<h3 class="mb-0">${Object.keys(stats.countries).length}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6><i class="fas fa-server text-success"></i> ASNs</h6>
|
||||||
|
<h3 class="mb-0">${Object.keys(stats.asns).length}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6><i class="fas fa-map-marker-alt text-info"></i> IP Addresses</h6>
|
||||||
|
<h3 class="mb-0">${allResults.length}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Filters
|
||||||
|
function displayFilters(stats) {
|
||||||
|
// Countries (top 15)
|
||||||
|
const countries = Object.entries(stats.countries)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 15);
|
||||||
|
|
||||||
|
document.getElementById('countryFilters').innerHTML = countries.map(([country, count]) =>
|
||||||
|
`<span class="filter-chip" data-type="countries" data-value="${country}"
|
||||||
|
onclick="toggleFilter('countries', '${country}')">
|
||||||
|
${country} (${count})
|
||||||
|
</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// ASNs (top 15)
|
||||||
|
const asns = Object.entries(stats.asns)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 15);
|
||||||
|
|
||||||
|
document.getElementById('asnFilters').innerHTML = asns.map(([asn, count]) =>
|
||||||
|
`<span class="filter-chip" data-type="asns" data-value="${asn}"
|
||||||
|
onclick="toggleFilter('asns', '${escapeHtml(asn)}')">
|
||||||
|
${asn} (${count})
|
||||||
|
</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// Owners (top 15)
|
||||||
|
const owners = Object.entries(stats.owners)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 15);
|
||||||
|
|
||||||
|
document.getElementById('ownerFilters').innerHTML = owners.map(([owner, count]) => {
|
||||||
|
const shortOwner = owner.length > 30 ? owner.substring(0, 30) + '...' : owner;
|
||||||
|
return `<span class="filter-chip" data-type="owners" data-value="${owner}"
|
||||||
|
onclick="toggleFilter('owners', '${escapeHtml(owner)}')" title="${owner}">
|
||||||
|
${shortOwner} (${count})
|
||||||
|
</span>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Filter
|
||||||
|
function toggleFilter(type, value) {
|
||||||
|
if (selectedFilters[type].has(value)) {
|
||||||
|
selectedFilters[type].delete(value);
|
||||||
|
} else {
|
||||||
|
selectedFilters[type].add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(`[data-type="${type}"][data-value="${value}"]`).forEach(chip => {
|
||||||
|
chip.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear All Filters
|
||||||
|
function clearFilters() {
|
||||||
|
selectedFilters = { countries: new Set(), asns: new Set(), owners: new Set() };
|
||||||
|
document.querySelectorAll('.filter-chip').forEach(chip => chip.classList.remove('active'));
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Filters
|
||||||
|
function applyFilters() {
|
||||||
|
const hasFilters = selectedFilters.countries.size > 0 ||
|
||||||
|
selectedFilters.asns.size > 0 ||
|
||||||
|
selectedFilters.owners.size > 0;
|
||||||
|
|
||||||
|
if (!hasFilters) {
|
||||||
|
filteredResults = [...allResults];
|
||||||
|
} else {
|
||||||
|
filteredResults = allResults.filter(item => {
|
||||||
|
return (selectedFilters.countries.size === 0 || selectedFilters.countries.has(item.country)) &&
|
||||||
|
(selectedFilters.asns.size === 0 || selectedFilters.asns.has(item.asn)) &&
|
||||||
|
(selectedFilters.owners.size === 0 || selectedFilters.owners.has(item.owner));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displayTable(filteredResults);
|
||||||
|
updateFilterCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Filter Count
|
||||||
|
function updateFilterCount() {
|
||||||
|
const total = selectedFilters.countries.size + selectedFilters.asns.size + selectedFilters.owners.size;
|
||||||
|
const countEl = document.getElementById('filterCount');
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = total > 0
|
||||||
|
? `Active filters: ${total} | Results: ${filteredResults.length}/${allResults.length}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Table
|
||||||
|
function displayTable(results) {
|
||||||
|
const tbody = document.getElementById('tableBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = results.map((item, idx) => `
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" class="form-check-input ip-checkbox" checked></td>
|
||||||
|
<td class="ip-cell">${item.ip}</td>
|
||||||
|
<td><span class="badge-custom">${item.asn}</span></td>
|
||||||
|
<td><small>${escapeHtml(item.owner)}</small></td>
|
||||||
|
<td>${item.country}</td>
|
||||||
|
<td><code>${item.network}</code></td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
document.getElementById('tableCount').textContent = results.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle All Checkboxes
|
||||||
|
function toggleAll() {
|
||||||
|
const checked = document.getElementById('selectAll').checked;
|
||||||
|
document.querySelectorAll('.ip-checkbox').forEach(cb => cb.checked = checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Selected IPs
|
||||||
|
function getSelectedIPs() {
|
||||||
|
const checkboxes = document.querySelectorAll('#tableBody input[type="checkbox"]:checked');
|
||||||
|
return Array.from(checkboxes).map(cb =>
|
||||||
|
cb.closest('tr').querySelector('.ip-cell').textContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Export Output
|
||||||
|
function showExport(title, code) {
|
||||||
|
document.getElementById('exportTitle').textContent = title;
|
||||||
|
document.getElementById('exportCode').textContent = code;
|
||||||
|
const output = document.getElementById('exportOutput');
|
||||||
|
output.classList.remove('d-none');
|
||||||
|
output.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export Functions
|
||||||
|
async function exportIPSet() {
|
||||||
|
const ips = getSelectedIPs();
|
||||||
|
if (ips.length === 0) {
|
||||||
|
showError('Please select at least one IP address');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/export/ipset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: ips, timeout: 86400 })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('IPSet Rules', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportIPTables() {
|
||||||
|
const ips = getSelectedIPs();
|
||||||
|
if (ips.length === 0) return;
|
||||||
|
|
||||||
|
const response = await fetch('/api/export/iptables', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: ips })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('iptables Rules', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportNginx() {
|
||||||
|
const ips = getSelectedIPs();
|
||||||
|
if (ips.length === 0) return;
|
||||||
|
|
||||||
|
const response = await fetch('/api/export/nginx', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: ips })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('Nginx Configuration', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportApache() {
|
||||||
|
const ips = getSelectedIPs();
|
||||||
|
if (ips.length === 0) return;
|
||||||
|
|
||||||
|
const response = await fetch('/api/export/apache', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: ips })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('Apache Configuration', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportFirewalld() {
|
||||||
|
const ips = getSelectedIPs();
|
||||||
|
if (ips.length === 0) return;
|
||||||
|
|
||||||
|
const response = await fetch('/api/export/firewalld', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: ips })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('Firewalld Rules', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportMikrotik() {
|
||||||
|
const ips = getSelectedIPs();
|
||||||
|
if (ips.length === 0) return;
|
||||||
|
|
||||||
|
const response = await fetch('/api/export/mikrotik', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ ips: ips })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('MikroTik Configuration', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCIDR() {
|
||||||
|
const response = await fetch('/api/export/cidr', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ results: filteredResults })
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await response.text();
|
||||||
|
showExport('CIDR Network List', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCSV() {
|
||||||
|
const response = await fetch('/api/export/csv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ results: filteredResults })
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `ip-analysis-${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to Clipboard
|
||||||
|
function copyToClipboard() {
|
||||||
|
const code = document.getElementById('exportCode').textContent;
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
const btn = event.target.closest('button');
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
btn.classList.remove('btn-secondary');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
showError('Failed to copy: ' + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
function showError(message) {
|
||||||
|
const error = document.getElementById('error');
|
||||||
|
if (error) {
|
||||||
|
error.textContent = message;
|
||||||
|
error.classList.remove('d-none');
|
||||||
|
setTimeout(() => error.classList.add('d-none'), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard Shortcuts
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.ctrlKey && e.key === 'Enter' && document.getElementById('ipInput')) {
|
||||||
|
analyzeIPs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth scroll for anchor links
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
324
templates/api.html
Normal file
324
templates/api.html
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}API Documentation - IP WHOIS Analyzer Pro{% endblock %}
|
||||||
|
|
||||||
|
{% block nav_api %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- API Info -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h5 class="alert-heading"><i class="fas fa-info-circle"></i> API Information</h5>
|
||||||
|
<p class="mb-1"><strong>Base URL:</strong> <code>http://localhost:5000</code></p>
|
||||||
|
<p class="mb-1"><strong>Format:</strong> JSON</p>
|
||||||
|
<p class="mb-0"><strong>Authentication:</strong> None (local use)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sidebar Navigation -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card mb-4 position-sticky" style="top: 70px;">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="mb-0"><i class="fas fa-list"></i> Endpoints</h6>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a href="#analyze" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-search"></i> Analyze IPs
|
||||||
|
</a>
|
||||||
|
<a href="#filter" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-filter"></i> Filter Results
|
||||||
|
</a>
|
||||||
|
<a href="#export-ipset" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-list"></i> Export IPSet
|
||||||
|
</a>
|
||||||
|
<a href="#export-iptables" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-shield-alt"></i> Export iptables
|
||||||
|
</a>
|
||||||
|
<a href="#export-nginx" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-server"></i> Export Nginx
|
||||||
|
</a>
|
||||||
|
<a href="#export-apache" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-feather"></i> Export Apache
|
||||||
|
</a>
|
||||||
|
<a href="#export-firewalld" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-fire"></i> Export Firewalld
|
||||||
|
</a>
|
||||||
|
<a href="#export-mikrotik" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-network-wired"></i> Export MikroTik
|
||||||
|
</a>
|
||||||
|
<a href="#export-cidr" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-sitemap"></i> Export CIDR
|
||||||
|
</a>
|
||||||
|
<a href="#export-csv" class="list-group-item list-group-item-action">
|
||||||
|
<i class="fas fa-file-csv"></i> Export CSV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<!-- POST /api/analyze -->
|
||||||
|
<div class="card endpoint-card mb-4" id="analyze">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/analyze
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Analyze a list of IP addresses and return WHOIS information (ASN, owner, country, network).</p>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Request Body:</h6>
|
||||||
|
<div class="position-relative">
|
||||||
|
<button class="btn btn-sm btn-secondary position-absolute top-0 end-0 m-2" onclick="copyCode(this)" style="z-index: 10;">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<pre class="mb-0"><code class="language-json">{
|
||||||
|
"ips": "1.1.1.1, 8.8.8.8, 9.9.9.9"
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Python Example:</h6>
|
||||||
|
<div class="position-relative">
|
||||||
|
<button class="btn btn-sm btn-secondary position-absolute top-0 end-0 m-2" onclick="copyCode(this)" style="z-index: 10;">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<pre class="mb-0"><code class="language-python">import requests
|
||||||
|
|
||||||
|
response = requests.post('http://localhost:5000/api/analyze',
|
||||||
|
json={'ips': '1.1.1.1, 8.8.8.8, 9.9.9.9'})
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
print(f"Total IPs: {data['stats']['total']}")
|
||||||
|
for ip_info in data['results']:
|
||||||
|
print(f"{ip_info['ip']} -> {ip_info['country']} ({ip_info['asn']})")</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- POST /api/filter -->
|
||||||
|
<div class="card endpoint-card mb-4" id="filter">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/filter
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Filter results by countries, ASNs, or owners.</p>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Python Example:</h6>
|
||||||
|
<div class="position-relative">
|
||||||
|
<button class="btn btn-sm btn-secondary position-absolute top-0 end-0 m-2" onclick="copyCode(this)" style="z-index: 10;">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<pre class="mb-0"><code class="language-python">import requests
|
||||||
|
|
||||||
|
# First, analyze
|
||||||
|
analyze_response = requests.post('http://localhost:5000/api/analyze',
|
||||||
|
json={'ips': 'your IP list...'})
|
||||||
|
results = analyze_response.json()['results']
|
||||||
|
|
||||||
|
# Then filter
|
||||||
|
filter_response = requests.post('http://localhost:5000/api/filter',
|
||||||
|
json={
|
||||||
|
'results': results,
|
||||||
|
'filters': {
|
||||||
|
'countries': ['CN', 'RU'],
|
||||||
|
'asns': ['AS4134']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
filtered = filter_response.json()['filtered']
|
||||||
|
print(f"Filtered: {len(filtered)} IPs")</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- POST /api/export/ipset -->
|
||||||
|
<div class="card endpoint-card mb-4" id="export-ipset">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/ipset
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Generate IPSet rules with timeout (default 24 hours).</p>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Python Example:</h6>
|
||||||
|
<div class="position-relative">
|
||||||
|
<button class="btn btn-sm btn-secondary position-absolute top-0 end-0 m-2" onclick="copyCode(this)" style="z-index: 10;">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<pre class="mb-0"><code class="language-python">import requests
|
||||||
|
|
||||||
|
response = requests.post('http://localhost:5000/api/export/ipset',
|
||||||
|
json={
|
||||||
|
'ips': ['1.1.1.1', '8.8.8.8'],
|
||||||
|
'timeout': 43200 # 12 hours
|
||||||
|
})
|
||||||
|
|
||||||
|
rules = response.text
|
||||||
|
with open('block_ips.sh', 'w') as f:
|
||||||
|
f.write(rules)
|
||||||
|
print("Saved to block_ips.sh")</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Other endpoints (shorter) -->
|
||||||
|
<div class="card endpoint-card mb-4" id="export-iptables">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/iptables
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Generate iptables DROP rules for INPUT and FORWARD chains.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card endpoint-card mb-4" id="export-nginx">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/nginx
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Generate Nginx deny directives.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card endpoint-card mb-4" id="export-apache">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/apache
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Generate Apache deny rules (.htaccess or VirtualHost).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card endpoint-card mb-4" id="export-firewalld">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/firewalld
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Generate Firewalld rich rules.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card endpoint-card mb-4" id="export-mikrotik">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/mikrotik
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Generate MikroTik RouterOS commands (address-list + firewall filter).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card endpoint-card mb-4" id="export-cidr">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/cidr
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Export unique CIDR network blocks.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card endpoint-card mb-4" id="export-csv">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
<span class="method-badge method-post">POST</span>
|
||||||
|
/api/export/csv
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Export data in CSV format with automatic download.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Complete Workflow Example -->
|
||||||
|
<div class="card border-success mb-4">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h4 class="mb-0"><i class="fas fa-code-branch"></i> Complete Workflow Example</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Full example: analyze → filter → export to MikroTik</p>
|
||||||
|
|
||||||
|
<div class="position-relative">
|
||||||
|
<button class="btn btn-sm btn-secondary position-absolute top-0 end-0 m-2" onclick="copyCode(this)" style="z-index: 10;">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<pre class="mb-0"><code class="language-python">import requests
|
||||||
|
|
||||||
|
BASE_URL = 'http://localhost:5000'
|
||||||
|
|
||||||
|
# 1. Analyze IPs from log file
|
||||||
|
with open('/var/log/attacks.log', 'r') as f:
|
||||||
|
log_content = f.read()
|
||||||
|
|
||||||
|
response = requests.post(f'{BASE_URL}/api/analyze',
|
||||||
|
json={'ips': log_content})
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
results = data['results']
|
||||||
|
print(f"Analyzed: {len(results)} IPs")
|
||||||
|
|
||||||
|
# 2. Filter China and Russia only
|
||||||
|
filter_response = requests.post(f'{BASE_URL}/api/filter',
|
||||||
|
json={
|
||||||
|
'results': results,
|
||||||
|
'filters': {'countries': ['CN', 'RU']}
|
||||||
|
})
|
||||||
|
|
||||||
|
filtered = filter_response.json()['filtered']
|
||||||
|
ips_to_block = [item['ip'] for item in filtered]
|
||||||
|
|
||||||
|
# 3. Generate MikroTik rules
|
||||||
|
mikrotik_response = requests.post(f'{BASE_URL}/api/export/mikrotik',
|
||||||
|
json={'ips': ips_to_block})
|
||||||
|
|
||||||
|
# 4. Save to file
|
||||||
|
with open('block_cn_ru.rsc', 'w') as f:
|
||||||
|
f.write(mikrotik_response.text)
|
||||||
|
|
||||||
|
print(f"Generated rules for {len(ips_to_block)} IPs")</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_script %}
|
||||||
|
<script src="{{ static_hash('js/api.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
59
templates/base.html
Normal file
59
templates/base.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}IP WHOIS Analyzer{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
<link rel="stylesheet" href="{{ static_hash('css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Top Navigation -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top border-bottom">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<i class="fas fa-shield-alt"></i> IP WHOIS Analyzer
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% block nav_home %}{% endblock %}" href="/">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% block nav_api %}{% endblock %}" href="/api">
|
||||||
|
<i class="fas fa-code"></i> API Documentation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="btn btn-outline-light btn-sm ms-2" onclick="toggleTheme()" title="Toggle theme">
|
||||||
|
<i class="fas fa-moon" id="themeIcon"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mt-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="text-center text-muted py-4 mt-5">
|
||||||
|
<small>{% block footer %}IP WHOIS Analyzer | #MG{% endblock %}</small>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_scripts %}{% endblock %}
|
||||||
|
{% block page_script %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
191
templates/index.html
Normal file
191
templates/index.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Home - IP WHOIS Analyzer{% endblock %}
|
||||||
|
|
||||||
|
{% block nav_home %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Input Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-paste"></i> Paste IP Addresses</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<textarea class="form-control font-monospace" id="ipInput" rows="10"
|
||||||
|
placeholder="Paste IP addresses here (comma, semicolon, space, or newline separated)...
|
||||||
|
|
||||||
|
Example:
|
||||||
|
192.168.1.1, 8.8.8.8
|
||||||
|
1.1.1.1; 9.9.9.9
|
||||||
|
45.131.219.163"></textarea>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<i class="fas fa-info-circle"></i> Supported separators: comma, semicolon, space, newline, tab
|
||||||
|
</small>
|
||||||
|
<button class="btn btn-primary btn-lg mt-3" onclick="analyzeIPs()">
|
||||||
|
<i class="fas fa-search"></i> Analyze IP Addresses
|
||||||
|
</button>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<i class="fas fa-keyboard"></i> Keyboard shortcut: Ctrl+Enter
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div class="text-center py-5 d-none" id="loading">
|
||||||
|
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-primary fw-bold">Analyzing IP addresses...</p>
|
||||||
|
<small class="text-muted">Using Team Cymru bulk WHOIS service</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div class="alert alert-danger d-none" id="error" role="alert"></div>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<div id="results" class="d-none">
|
||||||
|
<!-- Statistics -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-chart-pie"></i> Statistics</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row" id="stats"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-filter"></i> Filters</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-globe"></i> Countries
|
||||||
|
</label>
|
||||||
|
<div id="countryFilters" class="filter-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-hashtag"></i> ASN
|
||||||
|
</label>
|
||||||
|
<div id="asnFilters" class="filter-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label fw-bold">
|
||||||
|
<i class="fas fa-building"></i> Owners
|
||||||
|
</label>
|
||||||
|
<div id="ownerFilters" class="filter-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
||||||
|
<button class="btn btn-secondary mb-2" onclick="clearFilters()">
|
||||||
|
<i class="fas fa-times"></i> Clear all filters
|
||||||
|
</button>
|
||||||
|
<span class="text-muted mb-2" id="filterCount"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-download"></i> Export Rules</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted mb-3">
|
||||||
|
<i class="fas fa-info-circle"></i> Generate firewall rules based on filtered and selected IPs
|
||||||
|
</p>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportIPSet()">
|
||||||
|
<i class="fas fa-list"></i> IPSet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportIPTables()">
|
||||||
|
<i class="fas fa-shield-alt"></i> iptables
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportNginx()">
|
||||||
|
<i class="fas fa-server"></i> Nginx
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportApache()">
|
||||||
|
<i class="fas fa-feather"></i> Apache
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportFirewalld()">
|
||||||
|
<i class="fas fa-fire"></i> Firewalld
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportMikrotik()">
|
||||||
|
<i class="fas fa-network-wired"></i> MikroTik
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportCIDR()">
|
||||||
|
<i class="fas fa-sitemap"></i> CIDR
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="exportCSV()">
|
||||||
|
<i class="fas fa-file-csv"></i> CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Output -->
|
||||||
|
<div id="exportOutput" class="export-section d-none mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h6 class="mb-0" id="exportTitle"></h6>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="copyToClipboard()">
|
||||||
|
<i class="fas fa-copy"></i> Copy to clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre><code id="exportCode" class="language-bash"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Table -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-dark text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-table"></i> Detailed Results
|
||||||
|
(<span id="tableCount">0</span> IPs)
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50px;">
|
||||||
|
<input type="checkbox" class="form-check-input"
|
||||||
|
id="selectAll" onclick="toggleAll()" checked>
|
||||||
|
</th>
|
||||||
|
<th><i class="fas fa-map-marker-alt"></i> IP Address</th>
|
||||||
|
<th><i class="fas fa-hashtag"></i> ASN</th>
|
||||||
|
<th><i class="fas fa-building"></i> Owner</th>
|
||||||
|
<th><i class="fas fa-flag"></i> Country</th>
|
||||||
|
<th><i class="fas fa-sitemap"></i> Network</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_script %}
|
||||||
|
<script src="{{ static_hash('js/main.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user