Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions skills/miketyzhang/ip-lookup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: ip-lookup
description: Investigate any IP address or hostname β€” geolocation, ASN/ISP, reverse DNS (PTR), RDAP/WHOIS network block, and optional AbuseIPDB reputation check. No API keys needed for core features. Use when the user asks about an IP address, wants to geolocate an IP, look up who owns a network block, find the ISP or ASN for an IP, check abuse reputation, or do a reverse DNS lookup. Trigger phrases include "who owns this IP", "where is this IP located", "look up IP", "check if IP is malicious", "reverse DNS for", "what ASN is", "whois for IP".
metadata: {"openclaw":{"emoji":"πŸ”","requires":{"bins":["python3"]}}}
---

# IP Lookup

Zero-dependency network intelligence for any IP or hostname. Uses only Python stdlib β€” **no pip install required**.

## Quick Start

```bash
python3 {baseDir}/scripts/ip_lookup.py <ip_or_hostname>
```

**Examples:**

```bash
python3 {baseDir}/scripts/ip_lookup.py 8.8.8.8
python3 {baseDir}/scripts/ip_lookup.py github.com
python3 {baseDir}/scripts/ip_lookup.py 1.1.1.1 --no-rdap # skip WHOIS (faster)
python3 {baseDir}/scripts/ip_lookup.py 185.220.101.1 --abuse # + AbuseIPDB check
python3 {baseDir}/scripts/ip_lookup.py 8.8.8.8 --json # machine-readable JSON
```

## Output Panels

| Panel | Data | API | Auth |
|---|---|---|---|
| 🌍 Geolocation | Country, city, coords, timezone, ISP | ip-api.com (ipwho.is fallback) | None |
| πŸ”„ Reverse DNS | PTR record | dns.google | None |
| πŸ“‹ RDAP / WHOIS | Network name, CIDR block, abuse contact, registration date | rdap.arin.net (RIPE fallback) | None |
| πŸ›‘ Abuse (opt) | Confidence score, report count, last seen | api.abuseipdb.com | Free key |

## Flags

| Flag | Effect |
|---|---|
| `--json` | Raw JSON output (pipe-friendly) |
| `--abuse` | Enable AbuseIPDB check (set `ABUSEIPDB_KEY` env var) |
| `--no-rdap` | Skip RDAP/WHOIS (faster for simple geo queries) |
| `--no-ptr` | Skip reverse DNS lookup |

## AbuseIPDB Setup (optional)

1. Create a free account at https://www.abuseipdb.com
2. Get API key from the dashboard
3. `export ABUSEIPDB_KEY=your_key_here`
4. Run with `--abuse` flag

## Notes

- Hostnames are auto-resolved to IP before lookup
- RDAP uses ARIN first, falls back to RIPE for European addresses
- ip-api.com free tier: 45 requests/minute
- IPv6 supported for geo/RDAP; PTR lookup is IPv4-only
11 changes: 11 additions & 0 deletions skills/miketyzhang/ip-lookup/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"owner": "miketyzhang",
"slug": "ip-lookup",
"displayName": "IP Lookup",
"latest": {
"version": "1.0.0",
"publishedAt": 1741571400000,
"commit": ""
},
"history": []
}
294 changes: 294 additions & 0 deletions skills/miketyzhang/ip-lookup/scripts/ip_lookup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""
ip-lookup β€” Network intelligence CLI (stdlib only, no pip required)
Combines IP geolocation, ASN, reverse DNS, PTR, and RDAP/WHOIS
using free public APIs with no authentication required.

APIs used:
- ip-api.com : Geolocation + ASN (free, no key, 45 req/min)
- ipwho.is : Geolocation fallback (free, no key)
- rdap.arin.net : WHOIS/RDAP (free, no key) β€” falls back to RIPE
- dns.google : Reverse DNS / PTR (free, no key)
- api.abuseipdb.com: Abuse check (requires free API key via ABUSEIPDB_KEY env var)
"""

import argparse
import json
import os
import socket
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone

# ── ANSI colours (gracefully disabled when not a TTY) ────────────────────────

USE_COLOR = sys.stdout.isatty()

def c(text: str, code: str) -> str:
if not USE_COLOR:
return text
codes = {
"bold": "\033[1m", "dim": "\033[2m",
"red": "\033[31m", "green": "\033[32m", "yellow": "\033[33m",
"blue": "\033[34m", "cyan": "\033[36m", "white": "\033[37m",
"reset": "\033[0m",
}
return f"{codes.get(code, '')}{text}{codes['reset']}"

def box(title: str, lines: list[tuple[str, str]], border: str = "cyan") -> None:
"""Print a simple bordered panel with key-value rows."""
print()
print(c(f" ╔══ {title} ", border))
for key, val in lines:
k = c(f" β•‘ {key:<22}", "dim")
print(f"{k}{val}")
print(c(" β•š" + "═" * 50, border))

# ── Networking helpers ────────────────────────────────────────────────────────

def fetch(url: str, params: dict | None = None,
headers: dict | None = None, timeout: int = 8) -> dict | None:
if params:
url = url + "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
except Exception:
return None

def resolve_target(target: str) -> tuple[str, str | None]:
"""Return (ip_address, hostname_if_resolved). Resolves hostnames β†’ IP."""
if _is_ip(target):
return target, None
try:
ip = socket.gethostbyname(target)
return ip, target
except socket.gaierror as e:
print(c(f"Cannot resolve '{target}': {e}", "red"), file=sys.stderr)
sys.exit(1)

def _is_ip(s: str) -> bool:
for af in (socket.AF_INET, socket.AF_INET6):
try:
socket.inet_pton(af, s)
return True
except socket.error:
pass
return False

# ── Data fetchers ─────────────────────────────────────────────────────────────

def fetch_geo(ip: str) -> dict:
"""ip-api.com geolocation + ASN. Falls back to ipwho.is."""
fields = ("status,message,country,countryCode,regionName,city,zip,"
"lat,lon,timezone,isp,org,as,asname,mobile,proxy,hosting,query")
data = fetch(f"http://ip-api.com/json/{ip}", params={"fields": fields})
if data and data.get("status") == "success":
return {"_source": "ip-api.com", **data}
data2 = fetch(f"https://ipwho.is/{ip}")
if data2 and data2.get("success"):
return {"_source": "ipwho.is", **data2}
return {}

def fetch_ptr(ip: str) -> str | None:
"""Reverse DNS via Google Public DNS API."""
parts = ip.split(".")
if len(parts) == 4:
arpa = ".".join(reversed(parts)) + ".in-addr.arpa"
else:
return None # IPv6 PTR not handled
data = fetch("https://dns.google/resolve", params={"name": arpa, "type": "PTR"})
if data and data.get("Answer"):
return data["Answer"][0].get("data", "").rstrip(".")
return None

def fetch_rdap(ip: str) -> dict:
"""ARIN RDAP for IP/network block info + abuse contacts. Falls back to RIPE."""
data = fetch(f"https://rdap.arin.net/registry/ip/{ip}")
if data:
return data
return fetch(f"https://rdap.db.ripe.net/ip/{ip}") or {}

def fetch_abuse(ip: str, api_key: str) -> dict:
"""AbuseIPDB reputation check (90-day window)."""
data = fetch(
"https://api.abuseipdb.com/api/v2/check",
params={"ipAddress": ip, "maxAgeInDays": "90"},
headers={"Key": api_key, "Accept": "application/json"},
)
return (data or {}).get("data", {})

# ── Display ───────────────────────────────────────────────────────────────────

def display_geo(geo: dict, hostname: str | None):
rows: list[tuple[str, str]] = []
if hostname:
rows.append(("Resolved From", c(hostname, "yellow")))
ip = geo.get("query") or geo.get("ip", "β€”")
rows.append(("IP Address", c(ip, "bold")))
country = geo.get("country", "β€”")
cc = geo.get("countryCode") or geo.get("country_code", "β€”")
rows.append(("Country", f"{country} [{cc}]"))
rows.append(("Region", geo.get("regionName") or geo.get("region", "β€”")))
rows.append(("City", geo.get("city", "β€”")))
rows.append(("ZIP", geo.get("zip") or geo.get("postal", "β€”")))
lat = geo.get("lat") or geo.get("latitude", "β€”")
lon = geo.get("lon") or geo.get("longitude", "β€”")
rows.append(("Coordinates", f"{lat}, {lon}"))
rows.append(("Timezone", geo.get("timezone", "β€”")))
rows.append(("ISP", geo.get("isp") or str(geo.get("connection", {}).get("isp", "β€”"))))
rows.append(("Org", geo.get("org") or str(geo.get("connection", {}).get("org", "β€”"))))
asn = geo.get("as") or f"AS{geo.get('connection', {}).get('asn', 'β€”')}"
rows.append(("ASN", asn))

flags = []
if geo.get("proxy") or (geo.get("security") or {}).get("proxy"):
flags.append(c("PROXY", "blue"))
if geo.get("hosting") or (geo.get("security") or {}).get("hosting"):
flags.append(c("HOSTING/VPN", "yellow"))
if geo.get("mobile") or (geo.get("security") or {}).get("mobile"):
flags.append(c("MOBILE", "cyan"))
rows.append(("Flags", " ".join(flags) if flags else c("none", "dim")))

src = geo.get("_source", "unknown")
box(c(f"🌍 Geolocation [via {src}]", "green"), rows, "green")

def display_ptr(ptr: str | None):
val = c(ptr, "cyan") if ptr else c("(no PTR record)", "dim")
box(c("πŸ”„ Reverse DNS (PTR)", "yellow"), [("PTR Record", val)], "yellow")

def display_rdap(rdap: dict):
if not rdap:
print(c(" RDAP: no data returned", "dim"))
return
rows: list[tuple[str, str]] = []
rows.append(("Network Name", rdap.get("name", "β€”")))
rows.append(("Handle", rdap.get("handle", "β€”")))
rows.append(("Type", rdap.get("type", "β€”")))

cidr_list = rdap.get("cidr0_cidrs", [])
if cidr_list:
cidrs = ", ".join(
f"{e.get('v4prefix') or e.get('v6prefix')}/{e.get('length')}"
for e in cidr_list
)
rows.append(("CIDR Block(s)", cidrs))
else:
start = rdap.get("startAddress", "")
end = rdap.get("endAddress", "")
if start:
rows.append(("IP Range", f"{start} – {end}"))

for entity in rdap.get("entities", []):
if "abuse" in entity.get("roles", []):
vcard = entity.get("vcardArray", [])
if len(vcard) > 1:
for entry in vcard[1]:
if entry[0] == "email":
rows.append(("Abuse Email", c(str(entry[3]), "red")))
elif entry[0] == "fn":
rows.append(("Abuse Contact", str(entry[3])))

for ev in rdap.get("events", []):
action = ev.get("eventAction", "")
date = ev.get("eventDate", "")
if action in ("registration", "last changed"):
rows.append((action.title(), date[:10] if date else "β€”"))

box(c("πŸ“‹ RDAP / WHOIS", "blue"), rows, "blue")

def display_abuse(abuse: dict):
if not abuse:
return
score = abuse.get("abuseConfidenceScore", 0)
if score == 0:
score_str = c(f"{score}% (clean)", "green")
elif score < 50:
score_str = c(f"{score}% (suspicious)", "yellow")
else:
score_str = c(f"{score}% (likely malicious)", "red")

rows: list[tuple[str, str]] = [
("Abuse Score", score_str),
("Total Reports (90d)", str(abuse.get("totalReports", 0))),
("Last Reported", str(abuse.get("lastReportedAt") or "never")[:19]),
("Usage Type", abuse.get("usageType", "β€”")),
("Domain", abuse.get("domain", "β€”")),
]
box(c("πŸ›‘ Abuse Reputation (AbuseIPDB)", "red"), rows, "red")

# ── CLI entry point ───────────────────────────────────────────────────────────

def main():
parser = argparse.ArgumentParser(
description="IP network intelligence: geolocation, RDAP/WHOIS, reverse DNS, abuse check",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
ip_lookup.py 8.8.8.8
ip_lookup.py github.com
ip_lookup.py 1.1.1.1 --json
ip_lookup.py 185.220.101.1 --abuse (requires ABUSEIPDB_KEY env var)
ip_lookup.py 8.8.8.8 --no-rdap (skip RDAP, faster)
""",
)
parser.add_argument("target", help="IP address or hostname to investigate")
parser.add_argument("--json", action="store_true", help="Output raw JSON (machine-readable)")
parser.add_argument("--abuse", action="store_true",
help="Include AbuseIPDB reputation check (set ABUSEIPDB_KEY env var)")
parser.add_argument("--no-rdap", action="store_true", help="Skip RDAP/WHOIS lookup")
parser.add_argument("--no-ptr", action="store_true", help="Skip reverse DNS lookup")
args = parser.parse_args()

ip, hostname = resolve_target(args.target)

if not args.json:
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
print()
target_str = c(args.target, "cyan")
resolved = (f" β†’ {c(ip, 'yellow')}" if hostname else "")
print(f" {c('πŸ” Investigating:', 'bold')} {target_str}{resolved} {c(ts, 'dim')}")

results: dict = {"ip": ip, "hostname": hostname, "queried_at": datetime.now(timezone.utc).isoformat()}

# Geolocation
geo = fetch_geo(ip)
results["geo"] = geo
if not args.json:
display_geo(geo, hostname)

# PTR / Reverse DNS
if not args.no_ptr:
ptr = fetch_ptr(ip)
results["ptr"] = ptr
if not args.json:
display_ptr(ptr)

# RDAP / WHOIS
if not args.no_rdap:
rdap = fetch_rdap(ip)
results["rdap"] = rdap
if not args.json:
display_rdap(rdap)

# Abuse reputation
if args.abuse:
api_key = os.environ.get("ABUSEIPDB_KEY", "")
if not api_key:
print(c(" βœ— --abuse requires ABUSEIPDB_KEY environment variable", "red"), file=sys.stderr)
else:
abuse = fetch_abuse(ip, api_key)
results["abuse"] = abuse
if not args.json:
display_abuse(abuse)

if args.json:
print(json.dumps(results, indent=2, default=str))
else:
print()

if __name__ == "__main__":
main()