forked from ek0mssavi0r/Flock_SCAN
262 lines
8.1 KiB
Python
262 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
cloud_enrich.py — IP-to-cloud-provider enrichment for FLOCK_scan
|
|
|
|
Turns hardcoded FLOCK_CLOUD_IPS into dynamic enrichment:
|
|
- WHOIS / ASN lookup via ip-api.com or ipinfo.io (free, no key)
|
|
- Maps IP → { org, asn, country, region, cloud_provider }
|
|
- Cloud provider detection from ASN + org name
|
|
|
|
Usage:
|
|
from modules.cloud_enrich import enrich_ip, enrich_ip_batch
|
|
info = enrich_ip("52.72.49.79")
|
|
# → {"ip": "...", "org": "Amazon Technologies", "asn": "AS16509",
|
|
# "country": "US", "region": "Virginia", "cloud": "AWS"}
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import socket
|
|
|
|
# ── Fallback ASN database (no network call) ────────────────────────
|
|
# Maps ASN prefixes to known cloud providers.
|
|
# Helps when ip-api.com is unavailable.
|
|
|
|
CLOUD_ASN_MAP = {
|
|
# AWS
|
|
"16509": "AWS", "14618": "AWS", "7224": "AWS",
|
|
"8987": "AWS", "17493": "AWS", "39111": "AWS",
|
|
"31763": "AWS", "38895": "AWS", "7018": "AWS",
|
|
# GCP / Google Cloud
|
|
"15169": "GCP", "36040": "GCP", "36384": "GCP",
|
|
"41264": "GCP", "19448": "GCP", "26910": "GCP",
|
|
# Azure / Microsoft
|
|
"8075": "Azure", "12076": "Azure", "63314": "Azure",
|
|
"13100": "Azure", "31898": "Azure", "13526": "Azure",
|
|
# CloudFlare
|
|
"13335": "CloudFlare", "209242": "CloudFlare",
|
|
"14789": "CloudFlare", "203898": "CloudFlare",
|
|
# DigitalOcean
|
|
"14061": "DigitalOcean", "62567": "DigitalOcean",
|
|
# OVH
|
|
"16276": "OVH", "35540": "OVH",
|
|
# Linode
|
|
"63949": "Linode", "48270": "Linode",
|
|
# Vultr
|
|
"20473": "Vultr", "208722": "Vultr",
|
|
# Hetzner
|
|
"24940": "Hetzner", "213230": "Hetzner",
|
|
# Oracle Cloud
|
|
"31898": "Oracle", "395050": "Oracle",
|
|
# Fastly
|
|
"54113": "Fastly", "201737": "Fastly",
|
|
# Akamai
|
|
"16625": "Akamai", "12222": "Akamai", "21399": "Akamai",
|
|
# Linode
|
|
"63949": "Linode",
|
|
# Scaleway
|
|
"12876": "Scaleway",
|
|
# UpCloud
|
|
"202053": "UpCloud",
|
|
}
|
|
|
|
CLOUD_ORG_KEYWORDS = [
|
|
("amazon", "AWS"),
|
|
("aws", "AWS"),
|
|
("amazon technologies", "AWS"),
|
|
("amazon data services", "AWS"),
|
|
("amazon web services", "AWS"),
|
|
("amazon.com", "AWS"),
|
|
("elastic load balancing", "AWS"),
|
|
("google cloud", "GCP"),
|
|
("google compute", "GCP"),
|
|
("gcp", "GCP"),
|
|
("microsoft azure", "Azure"),
|
|
("azure", "Azure"),
|
|
("microsoft corporation", "Azure"),
|
|
("cloudflare", "CloudFlare"),
|
|
("digitalocean", "DigitalOcean"),
|
|
("linode", "Linode"),
|
|
("vultr", "Vultr"),
|
|
("hetzner", "Hetzner"),
|
|
("oracle cloud", "Oracle"),
|
|
("oracle public cloud", "Oracle"),
|
|
("ovh", "OVH"),
|
|
("fastly", "Fastly"),
|
|
("akamai", "Akamai"),
|
|
("scaleway", "Scaleway"),
|
|
("upcloud", "UpCloud"),
|
|
]
|
|
|
|
|
|
# ── ASN / WHOIS Lookup ──────────────────────────────────────────────
|
|
|
|
def _reverse_dns(ip, timeout=3):
|
|
"""Try to PTR the IP — sometimes reveals cloud hostname directly."""
|
|
try:
|
|
name, _, _ = socket.gethostbyaddr(ip)
|
|
return name
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def enrich_ip(ip, timeout=5):
|
|
"""
|
|
Look up IP enrichment data from ip-api.com (free, no API key).
|
|
|
|
Returns dict with:
|
|
ip, org, asn, country, region, city, cloud, reverse_dns
|
|
|
|
Falls back gracefully if the HTTP lookup fails.
|
|
"""
|
|
import requests
|
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
|
|
result = {
|
|
"ip": ip,
|
|
"org": None,
|
|
"asn": None,
|
|
"country": None,
|
|
"region": None,
|
|
"city": None,
|
|
"cloud": None,
|
|
"reverse_dns": None,
|
|
}
|
|
|
|
# PTR first (fast, local)
|
|
try:
|
|
rdns = _reverse_dns(ip)
|
|
result["reverse_dns"] = rdns
|
|
except Exception:
|
|
pass
|
|
|
|
# ip-api.com — limited to 45 req/min from a single IP (free tier)
|
|
try:
|
|
r = requests.get(
|
|
f"http://ip-api.com/json/{ip}",
|
|
timeout=timeout,
|
|
headers={"User-Agent": "FLOCK_scan/3.0"},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if data.get("status") == "success":
|
|
result["org"] = data.get("org")
|
|
result["asn"] = data.get("asn")
|
|
result["country"] = data.get("country")
|
|
result["region"] = data.get("regionName")
|
|
result["city"] = data.get("city")
|
|
result["isp"] = data.get("isp")
|
|
|
|
# Determine cloud provider
|
|
org = (result["org"] or "").lower()
|
|
asn = result.get("asn") or ""
|
|
result["cloud"] = _detect_cloud_provider(org, asn)
|
|
return result
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: cli-based whois
|
|
try:
|
|
import subprocess
|
|
whois_out = subprocess.run(
|
|
["whois", ip],
|
|
capture_output=True, text=True, timeout=timeout
|
|
).stdout.lower()
|
|
for line in whois_out.split("\n"):
|
|
if "orgname:" in line:
|
|
result["org"] = line.split(":", 1)[1].strip()
|
|
if "originas:" in line or "origin:" in line:
|
|
asn = line.split(":", 1)[1].strip().lstrip("AS")
|
|
result["asn"] = f"AS{asn}"
|
|
if "netname:" in line:
|
|
if not result.get("org"):
|
|
result["org"] = line.split(":", 1)[1].strip()
|
|
if "country:" in line and not result.get("country"):
|
|
result["country"] = line.split(":", 1)[1].strip().upper()
|
|
|
|
org = (result.get("org") or "").lower()
|
|
asn = result.get("asn", "").replace("AS", "")
|
|
result["cloud"] = _detect_cloud_provider(org, asn)
|
|
except Exception:
|
|
pass
|
|
|
|
return result
|
|
|
|
|
|
def _detect_cloud_provider(org, asn):
|
|
"""Match org string + ASN against known cloud providers."""
|
|
# ASN match first
|
|
if asn and asn in CLOUD_ASN_MAP:
|
|
return CLOUD_ASN_MAP[asn]
|
|
|
|
# Org keyword match
|
|
for keyword, provider in CLOUD_ORG_KEYWORDS:
|
|
if keyword in org.lower():
|
|
return provider
|
|
|
|
# PTR-based: if we have a reverse DNS, check for cloud patterns
|
|
return None
|
|
|
|
|
|
def enrich_ip_batch(ips, timeout=10):
|
|
"""
|
|
Batch enrich multiple IPs.
|
|
Handles ip-api.com's 45 req/min rate limit with simple sleep.
|
|
Also uses batch endpoint for efficiency.
|
|
|
|
Returns dict of ip -> result.
|
|
"""
|
|
import requests
|
|
import time
|
|
|
|
# Try batch endpoint first (ip-api.com supports up to 100 IPs)
|
|
try:
|
|
r = requests.post(
|
|
"http://ip-api.com/batch",
|
|
json=ips[:100], # max 100 per batch
|
|
timeout=timeout,
|
|
headers={"User-Agent": "FLOCK_scan/3.0"},
|
|
)
|
|
if r.status_code == 200:
|
|
batch_data = r.json()
|
|
results = {}
|
|
for item in batch_data:
|
|
ip = item.get("query")
|
|
if not ip:
|
|
continue
|
|
org = item.get("org", "")
|
|
asn = item.get("asn", "")
|
|
results[ip] = {
|
|
"ip": ip,
|
|
"org": org,
|
|
"asn": asn,
|
|
"country": item.get("country"),
|
|
"region": item.get("regionName"),
|
|
"city": item.get("city"),
|
|
"isp": item.get("isp"),
|
|
"cloud": _detect_cloud_provider(
|
|
(org or "").lower(),
|
|
(asn or "").replace("AS", "")
|
|
),
|
|
"reverse_dns": _reverse_dns(ip),
|
|
}
|
|
return results
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: one by one
|
|
results = {}
|
|
for ip in ips:
|
|
results[ip] = enrich_ip(ip, timeout=5)
|
|
time.sleep(1.5) # rate limit: ~40/min
|
|
return results
|
|
|
|
|
|
# ── CLI test ─────────────────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
target = sys.argv[1] if len(sys.argv) > 1 else "52.72.49.79"
|
|
print(json.dumps(enrich_ip(target), indent=2, default=str))
|