commit a547894adc436b20258478dbec49659b14a4cbf2 Author: Mateusz Gruszczyński Date: Thu Feb 5 12:11:00 2026 +0100 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..154356f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitignore +__pycache__ +*.pyc +venv +.env +README.md +.vscode +.idea \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d4188f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__ +venv/ +env +.env +.vscode/ +.idea/ +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cda8b26 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a1706d --- /dev/null +++ b/README.md @@ -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: +- API docs: + +You can change host/port in `ip_analyzer.py` if needed. + +## Usage (Web UI) + +1. Open 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. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ddccca2 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/ip_analyzer.py b/ip_analyzer.py new file mode 100644 index 0000000..4f21dd3 --- /dev/null +++ b/ip_analyzer.py @@ -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 + + + Require all granted +""" + + for ip in ips: + rules += f" Require not ip {ip}\n" + + rules += """ + +# 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 + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..86ffd36 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.0 +requests==2.31.0 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..fc38bfd --- /dev/null +++ b/static/css/style.css @@ -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; + } +} diff --git a/static/js/api.js b/static/js/api.js new file mode 100644 index 0000000..0574607 --- /dev/null +++ b/static/js/api.js @@ -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 = ' 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!'); + }); + } +}); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..18a9a82 --- /dev/null +++ b/static/js/main.js @@ -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 = ` +
+
+
+
Countries
+

${Object.keys(stats.countries).length}

+
+
+
+
+
+
+
ASNs
+

${Object.keys(stats.asns).length}

+
+
+
+
+
+
+
IP Addresses
+

${allResults.length}

+
+
+
+ `; +} + +// 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]) => + ` + ${country} (${count}) + ` + ).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]) => + ` + ${asn} (${count}) + ` + ).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 ` + ${shortOwner} (${count}) + `; + }).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) => ` + + + ${item.ip} + ${item.asn} + ${escapeHtml(item.owner)} + ${item.country} + ${item.network} + + `).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 = ' 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' }); + } + }); + }); +}); diff --git a/templates/api.html b/templates/api.html new file mode 100644 index 0000000..e57d868 --- /dev/null +++ b/templates/api.html @@ -0,0 +1,324 @@ +{% extends "base.html" %} + +{% block title %}API Documentation - IP WHOIS Analyzer Pro{% endblock %} + +{% block nav_api %}active{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+
+
+
API Information
+

Base URL: http://localhost:5000

+

Format: JSON

+

Authentication: None (local use)

+
+
+
+ +
+ + + + +
+ +
+
+

+ POST + /api/analyze +

+
+
+

Analyze a list of IP addresses and return WHOIS information (ASN, owner, country, network).

+ +
Request Body:
+
+ +
{
+  "ips": "1.1.1.1, 8.8.8.8, 9.9.9.9"
+}
+
+ +
Python Example:
+
+ +
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']})")
+
+
+
+ + +
+
+

+ POST + /api/filter +

+
+
+

Filter results by countries, ASNs, or owners.

+ +
Python Example:
+
+ +
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")
+
+
+
+ + +
+
+

+ POST + /api/export/ipset +

+
+
+

Generate IPSet rules with timeout (default 24 hours).

+ +
Python Example:
+
+ +
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")
+
+
+
+ + +
+
+

+ POST + /api/export/iptables +

+
+
+

Generate iptables DROP rules for INPUT and FORWARD chains.

+
+
+ +
+
+

+ POST + /api/export/nginx +

+
+
+

Generate Nginx deny directives.

+
+
+ +
+
+

+ POST + /api/export/apache +

+
+
+

Generate Apache deny rules (.htaccess or VirtualHost).

+
+
+ +
+
+

+ POST + /api/export/firewalld +

+
+
+

Generate Firewalld rich rules.

+
+
+ +
+
+

+ POST + /api/export/mikrotik +

+
+
+

Generate MikroTik RouterOS commands (address-list + firewall filter).

+
+
+ +
+
+

+ POST + /api/export/cidr +

+
+
+

Export unique CIDR network blocks.

+
+
+ +
+
+

+ POST + /api/export/csv +

+
+
+

Export data in CSV format with automatic download.

+
+
+ + +
+
+

Complete Workflow Example

+
+
+

Full example: analyze → filter → export to MikroTik

+ +
+ +
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")
+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + + + +{% endblock %} + +{% block page_script %} + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..150a47a --- /dev/null +++ b/templates/base.html @@ -0,0 +1,59 @@ + + + + + + {% block title %}IP WHOIS Analyzer{% endblock %} + + + {% block extra_css %}{% endblock %} + + + + + + + +
+ {% block content %}{% endblock %} +
+ + +
+ {% block footer %}IP WHOIS Analyzer | #MG{% endblock %} +
+ + + + {% block extra_scripts %}{% endblock %} + {% block page_script %}{% endblock %} + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f60cf19 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,191 @@ +{% extends "base.html" %} + +{% block title %}Home - IP WHOIS Analyzer{% endblock %} + +{% block nav_home %}active{% endblock %} + +{% block content %} + +
+
+
Paste IP Addresses
+
+
+ + + Supported separators: comma, semicolon, space, newline, tab + + + + Keyboard shortcut: Ctrl+Enter + +
+
+ + +
+
+ Loading... +
+

Analyzing IP addresses...

+ Using Team Cymru bulk WHOIS service +
+ + + + + +
+ +
+
+
Statistics
+
+
+
+
+
+ + +
+
+
Filters
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
+
+ + +
+
+
Export Rules
+
+
+

+ Generate firewall rules based on filtered and selected IPs +

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +
+
+
+
+
+ + +
+
+
+ Detailed Results + (0 IPs) +
+
+
+
+ + + + + + + + + + + + +
+ + IP Address ASN Owner Country Network
+
+
+
+
+{% endblock %} + +{% block page_script %} + +{% endblock %} \ No newline at end of file