yougotmapped 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
yougotmapped/cli.py CHANGED
@@ -1,174 +1,217 @@
1
- import os
2
- import sys
3
1
  import argparse
4
- import ipaddress
5
- from datetime import datetime
6
2
  from pathlib import Path
3
+ from datetime import datetime
7
4
 
8
5
  from yougotmapped.utils.dependencies import check_dependencies
9
- from yougotmapped.utils.network import get_public_ip, get_geolocation, resolve_domain_to_ip
10
- from yougotmapped.utils.mapping import plot_ip_location, plot_multiple_ip_locations
11
- from yougotmapped.utils.token import get_api_token
12
- from yougotmapped.utils.ping import ping_target
13
- from yougotmapped.utils.trace import run_traceroute
6
+ from yougotmapped.utils.network import get_public_ip, get_geolocation
7
+ from yougotmapped.utils.ping import ping_target, format_ping_result
8
+ from yougotmapped.utils.jitter import jitter_test, format_jitter_result
9
+ from yougotmapped.utils.mss import discover_mss, format_mss_result
10
+ from yougotmapped.utils.bandwidth import estimate_bandwidth, format_bandwidth_result
11
+ from yougotmapped.utils.trace import run_traceroute, format_traceroute
14
12
  from yougotmapped.utils.anonymity import detect_anonymity, format_anonymity_result
15
- from yougotmapped.utils.output import write_formatted_output
13
+ from yougotmapped.utils.mapping import (
14
+ plot_ip_location,
15
+ plot_multiple_ip_locations,
16
+ plot_traceroute_path,
17
+ save_map,
18
+ )
19
+ from yougotmapped.utils.output import write_output
16
20
 
21
+ def parse_args():
22
+ parser = argparse.ArgumentParser(
23
+ description="Geolocate IPs/domains, analyze network paths, and visualize routing."
24
+ )
17
25
 
18
- def main():
19
- parser = argparse.ArgumentParser(description="Geolocate one or more IPs/domains and generate an interactive map.")
20
- parser.add_argument('-i', '--ip', nargs='*', help="One or more IPs/domains separated by space")
21
- parser.add_argument('-f', '--file', type=str, help="Path to a file containing IPs/domains (one per line)")
22
- parser.add_argument('-p', '--ping', action='store_true', help="Ping each IP or domain and show latency")
23
- parser.add_argument('-t', '--trace', action='store_true', help="Show traceroute to each IP or domain")
24
- parser.add_argument('-c', '--hidecheck', action='store_true', help="Check if the IP is a Tor exit node or VPN")
25
- parser.add_argument('-o', '--output', type=str, help="Specify output file or use format shorthand (e.g., f:csv, f:json, f:normal)")
26
- parser.add_argument('--no-map', action='store_true', help="Do not generate a map")
27
- parser.add_argument('--delete-map', action='store_true', help="Delete the map after generating")
28
- args = parser.parse_args()
26
+ parser.add_argument("-i", "--ip", nargs="*", help="IP addresses or domains")
27
+ parser.add_argument("-f", "--file", help="File with IPs/domains (one per line)")
29
28
 
30
- check_dependencies()
29
+ parser.add_argument("-p", "--ping", action="store_true", help="Ping test")
30
+ parser.add_argument("-j", "--jitter", action="store_true", help="Jitter analysis")
31
+ parser.add_argument("-m", "--mtu", action="store_true", help="MTU/MSS discovery")
32
+ parser.add_argument("-b", "--bandwidth", action="store_true", help="Bandwidth estimate")
33
+ parser.add_argument("-t", "--trace", action="store_true", help="Traceroute")
34
+
35
+ parser.add_argument("-c", "--hidecheck", action="store_true", help="Anonymity detection")
36
+ parser.add_argument("-a", "--all", action="store_true", help="Run all modules")
37
+
38
+ parser.add_argument("--no-map", action="store_true", help="Disable map output")
39
+ parser.add_argument(
40
+ "-o",
41
+ "--output",
42
+ help="Output file or shorthand (f:json, f:csv, f:normal)",
43
+ )
44
+
45
+ return parser.parse_args()
31
46
 
32
- API_TOKEN = get_api_token()
33
- if not API_TOKEN:
34
- return
35
47
 
36
- targets = []
48
+ def collect_targets(args) -> list[str]:
49
+ targets: list[str] = []
37
50
 
38
51
  if args.ip:
39
52
  targets.extend(args.ip)
40
53
 
41
54
  if args.file:
42
55
  try:
43
- with open(args.file, 'r') as f:
56
+ with open(args.file, "r") as f:
44
57
  targets.extend(line.strip() for line in f if line.strip())
45
58
  except FileNotFoundError:
46
59
  print(f"File not found: {args.file}")
47
- return
48
60
 
49
61
  if not targets:
50
- print("No IP or file input provided. Defaulting to public IP lookup.")
51
- ip_or_domain = get_public_ip()
52
- if not ip_or_domain:
53
- print("Could not determine your public IP.")
54
- return
55
- targets = [ip_or_domain]
62
+ print("No input provided. Defaulting to public IP.")
63
+ public_ip = get_public_ip()
64
+ if public_ip:
65
+ targets.append(public_ip)
66
+
67
+ return targets
68
+
69
+
70
+ def print_geo_rich(geo: dict) -> None:
71
+ raw = geo.get("raw", {})
72
+ connection = raw.get("connection", {})
73
+ timezone = raw.get("timezone", {})
74
+
75
+ print("\n[ GEOLOCATION ]")
76
+ print(f"IP: {raw.get('ip')}")
77
+ print(f"Type: {raw.get('type')}")
78
+ print(f"ASN: AS{connection.get('asn')}" if connection.get("asn") else "ASN: N/A")
79
+ print(f"ISP / Org: {connection.get('org') or connection.get('isp') or 'N/A'}")
80
+ print(f"Domain: {connection.get('domain') or 'N/A'}")
81
+
82
+ print("\n[ LOCATION ]")
83
+ print(f"Continent: {raw.get('continent')} ({raw.get('continent_code')})")
84
+ print(f"Country: {raw.get('country')} ({raw.get('country_code')})")
85
+ print(f"Region: {raw.get('region')} ({raw.get('region_code')})")
86
+ print(f"City: {raw.get('city')}")
87
+ print(f"Postal Code: {raw.get('postal') or ''}")
88
+ print(f"Latitude: {raw.get('latitude')}")
89
+ print(f"Longitude: {raw.get('longitude')}")
90
+ print(f"Capital: {raw.get('capital')}")
91
+ print(f"Borders: {raw.get('borders')}")
92
+
93
+ print("\n[ TIMEZONE ]")
94
+ print(f"Zone: {timezone.get('id')}")
95
+ print(f"Abbreviation: {timezone.get('abbr')}")
96
+ print(f"UTC Offset: {timezone.get('utc')}")
97
+ print(f"DST Active: {timezone.get('is_dst')}")
98
+ print(f"Local Time: {timezone.get('current_time')}")
56
99
 
57
- geolocated = []
58
- full_results = []
100
+
101
+ def main():
102
+ args = parse_args()
103
+ check_dependencies()
104
+
105
+ if args.all:
106
+ args.ping = True
107
+ args.jitter = True
108
+ args.mtu = True
109
+ args.bandwidth = True
110
+ args.trace = True
111
+ args.hidecheck = True
112
+
113
+ targets = collect_targets(args)
114
+ if not targets:
115
+ print("No valid targets to process.")
116
+ return
117
+
118
+ results = []
119
+ geos_for_map = []
120
+ last_trace_result = None
59
121
 
60
122
  for target in targets:
61
- target_result = {}
62
- try:
63
- ipaddress.ip_address(target)
64
- ip_or_domain = target
65
- except ValueError:
66
- resolved_ip = resolve_domain_to_ip(target)
67
- if resolved_ip:
68
- ip_or_domain = resolved_ip
69
- else:
70
- print(f"Skipping unresolved domain: {target}")
123
+ print(f"\nTarget: {target}")
124
+
125
+ geo = get_geolocation(target)
126
+ if not geo:
127
+ print("Failed to retrieve geolocation data.")
128
+ continue
129
+
130
+ results.append(geo)
131
+ geos_for_map.append(geo)
132
+
133
+ print_geo_rich(geo)
134
+
135
+ ping_result = None
136
+ trace_result = None
137
+
138
+ if args.ping:
139
+ print("\n[ PING ]")
140
+ ping_result = ping_target(target)
141
+ format_ping_result(ping_result)
142
+ geo["ping"] = ping_result
143
+
144
+ if not ping_result.get("reachable"):
145
+ print("Stopping analysis: host unreachable.")
71
146
  continue
72
147
 
73
- print(f"Looking up location for {target}...")
74
- data = get_geolocation(ip_or_domain, API_TOKEN)
75
- if data:
76
- geolocated.append(data)
77
- target_result.update(data)
78
- print("---")
79
- for key in ['ip', 'hostname', 'city', 'region', 'country', 'loc', 'org', 'postal', 'timezone']:
80
- print(f"{key.title()}: {data.get(key, 'N/A')}")
81
-
82
- if args.ping:
83
- print("\n[ PING RESULT ]")
84
- ping_result = ping_target(target)
85
- print(ping_result)
86
- target_result["ping"] = ping_result
87
-
88
- if args.trace:
89
- print("\n[ TRACEROUTE RESULT ]")
90
- trace_output = run_traceroute(target)
91
- print(trace_output)
92
- target_result["traceroute"] = trace_output
93
-
94
- if args.hidecheck:
95
- print("\n[ ANONYMITY CHECK ]")
96
- anonymity = detect_anonymity(data)
97
- format_anonymity_result(anonymity)
98
- target_result["anonymity"] = anonymity
99
-
100
- print("---")
101
- full_results.append(target_result)
148
+ if args.jitter:
149
+ print("\n[ JITTER ]")
150
+ jitter_result = jitter_test(target)
151
+ format_jitter_result(jitter_result)
152
+ geo["jitter"] = jitter_result
153
+
154
+ if args.mtu:
155
+ print("\n[ MSS ]")
156
+ mss_result = discover_mss(target)
157
+ format_mss_result(mss_result)
158
+ geo["mss"] = mss_result
159
+
160
+ if args.bandwidth:
161
+ print("\n[ BANDWIDTH ]")
162
+ bandwidth_result = estimate_bandwidth(ping_result)
163
+ format_bandwidth_result(bandwidth_result)
164
+ geo["bandwidth"] = bandwidth_result
165
+
166
+ if args.trace:
167
+ print("\n[ TRACEROUTE ]")
168
+ trace_result = run_traceroute(target)
169
+ format_traceroute(trace_result)
170
+ geo["traceroute"] = trace_result
171
+ last_trace_result = trace_result
172
+
173
+ if args.hidecheck:
174
+ print("\n[ ANONYMITY ]")
175
+ anonymity = detect_anonymity(geo)
176
+ format_anonymity_result(anonymity)
177
+ geo["anonymity"] = anonymity
178
+
179
+
180
+ map_path = None
181
+ if geos_for_map and not args.no_map:
182
+ if len(geos_for_map) == 1:
183
+ m = plot_ip_location(geos_for_map[0])
184
+ if m and args.trace and last_trace_result:
185
+ plot_traceroute_path(last_trace_result, m)
102
186
  else:
103
- print(f"Failed to get location data for {target}.")
104
-
105
- # --- Output & Summary ---
106
- map_saved = False
107
- map_path = Path("ip_geolocation_map.html").resolve()
108
- log_path = None
109
-
110
- if not args.no_map and geolocated:
111
- if len(geolocated) == 1:
112
- m = plot_ip_location(geolocated[0], color="red")
113
-
114
- if args.trace and isinstance(trace_output, str):
115
- lines = trace_output.strip().split("\n")
116
- for line in reversed(lines):
117
- if "(" in line and ")" in line:
118
- last_ip = line.split()[1]
119
- try:
120
- ipaddress.ip_address(last_ip)
121
- last_data = get_geolocation(last_ip, API_TOKEN)
122
- if last_data:
123
- plot_ip_location(last_data, color="blue", map_object=m)
124
- except Exception:
125
- pass
126
- break
127
- if m:
128
- m.save(str(map_path))
129
- map_saved = True
130
-
131
- elif len(geolocated) > 1:
132
- plot_multiple_ip_locations(geolocated)
133
- map_saved = True
134
-
135
- if args.delete_map:
136
- try:
137
- os.remove(map_path)
138
- map_saved = False
139
- except FileNotFoundError:
140
- pass
187
+ m = plot_multiple_ip_locations(geos_for_map)
188
+
189
+ if m:
190
+ map_path = save_map(m, "ip_geolocation_map.html")
141
191
 
192
+
193
+ output_path = None
142
194
  if args.output:
143
- os.makedirs("logs", exist_ok=True)
144
- now = datetime.now()
145
- date_str = now.strftime("%m-%d-%y")
146
- time_str = now.strftime("%H-%M")
195
+ Path("logs").mkdir(exist_ok=True)
196
+ timestamp = datetime.now().strftime("%m-%d-%y--%H-%M")
147
197
 
148
198
  if args.output.startswith("f:"):
149
- fmt_type = args.output[2:].lower()
150
- ext = {"json": "json", "csv": "csv"}.get(fmt_type, "txt")
151
- target_name = targets[0].replace(":", "-").replace("/", "-")
152
- filename = f"{target_name}--{date_str}--{time_str}--YouGotMapped.{ext}"
199
+ fmt = args.output[2:].lower()
200
+ ext = "json" if fmt == "json" else "csv" if fmt == "csv" else "txt"
201
+ name = targets[0].replace(":", "-").replace("/", "-")
202
+ filename = f"{name}--{timestamp}--YouGotMapped.{ext}"
153
203
  else:
154
204
  filename = args.output
155
- fmt_type = filename.split('.')[-1].lower()
156
- if fmt_type not in ["json", "csv", "txt"]:
157
- fmt_type = "normal"
158
-
159
- log_path = Path("logs") / filename
160
- write_formatted_output(full_results, str(log_path), fmt_type=fmt_type)
161
- log_path = log_path.resolve()
162
-
163
- # Final summary
164
- if map_saved or log_path:
165
- print("\n----- OUTPUT SUMMARY -----")
166
- if map_saved:
167
- print(f"Map Location: {map_path}")
168
- print(f"Map URL: file://{map_path}")
169
- if log_path:
170
- print(f"Log File: {log_path}")
171
- print(f"Log URL: file://{log_path}")
205
+ fmt = filename.split(".")[-1].lower()
206
+
207
+ output_path = write_output(results, Path("logs") / filename, fmt=fmt)
208
+
209
+ if map_path or output_path:
210
+ print("\nOutput Summary")
211
+ if map_path:
212
+ print(f" Map: file://{map_path}")
213
+ if output_path:
214
+ print(f" Log: file://{output_path}")
172
215
 
173
216
 
174
217
  if __name__ == "__main__":
@@ -1,53 +1,87 @@
1
- # utils/anonymity.py
2
- import requests
3
- import socket
1
+ # yougotmapped/utils/anonymity.py
4
2
 
5
- TOR_EXIT_LIST = "https://check.torproject.org/torbulkexitlist"
3
+ import requests
4
+ from functools import lru_cache
6
5
 
7
- VPN_KEYWORDS = [
8
- "vpn", "m247", "nord", "express", "ovh", "digitalocean", "linode",
9
- "datacamp", "host", "colo", "server", "vultr", "heficed", "leaseweb"
10
- ]
6
+ TOR_EXIT_LIST_URL = "https://check.torproject.org/torbulkexitlist"
11
7
 
8
+ VPN_KEYWORDS = {
9
+ "vpn", "proxy", "tor", "hosting", "cloud", "server", "colo",
10
+ "digitalocean", "linode", "ovh", "vultr", "hetzner", "leaseweb",
11
+ "m247", "nord", "express", "surfshark", "proton", "fastly",
12
+ "akamai", "cloudflare", "google", "amazon", "aws", "azure"
13
+ }
12
14
 
13
- def is_tor_exit_node(ip):
15
+ @lru_cache(maxsize=1)
16
+ def _get_tor_exit_nodes() -> set[str]:
14
17
  try:
15
- response = requests.get(TOR_EXIT_LIST, timeout=5)
16
- exit_nodes = response.text.strip().splitlines()
17
- return ip in exit_nodes
18
+ resp = requests.get(TOR_EXIT_LIST_URL, timeout=10)
19
+ resp.raise_for_status()
20
+ return set(
21
+ line.strip()
22
+ for line in resp.text.splitlines()
23
+ if line and not line.startswith("#")
24
+ )
18
25
  except Exception:
26
+ return set()
27
+
28
+
29
+ def is_tor_exit_node(ip: str) -> bool:
30
+ if not ip:
19
31
  return False
32
+ return ip in _get_tor_exit_nodes()
20
33
 
34
+ def _contains_vpn_keyword(value: str | None) -> bool:
35
+ if not value:
36
+ return False
37
+ value = value.lower()
38
+ return any(keyword in value for keyword in VPN_KEYWORDS)
21
39
 
22
- def detect_anonymity(ip_data):
23
- org = ip_data.get("org", "").lower()
24
- hostname = ip_data.get("hostname", "").lower()
40
+ def detect_anonymity(ip_data: dict) -> dict:
25
41
  ip = ip_data.get("ip")
42
+ hostname = ip_data.get("hostname")
43
+ org = ip_data.get("org")
44
+ raw = ip_data.get("raw", {})
26
45
 
27
46
  tor = is_tor_exit_node(ip)
28
- vpn = any(keyword in org or keyword in hostname for keyword in VPN_KEYWORDS)
29
47
 
30
- result = {
48
+ # ipwho.is connection hints
49
+ connection = raw.get("connection", {})
50
+ isp = connection.get("isp")
51
+ domain = connection.get("domain")
52
+ asn = connection.get("asn")
53
+
54
+ vpn = any([
55
+ _contains_vpn_keyword(org),
56
+ _contains_vpn_keyword(hostname),
57
+ _contains_vpn_keyword(isp),
58
+ _contains_vpn_keyword(domain),
59
+ ])
60
+
61
+ return {
31
62
  "ip": ip,
32
63
  "tor": tor,
33
64
  "vpn": vpn,
34
- "org": org,
35
65
  "hostname": hostname,
66
+ "org": org,
67
+ "isp": isp,
68
+ "asn": asn,
69
+ "confidence": _confidence_score(tor, vpn),
36
70
  }
37
71
 
38
- return result
39
-
40
-
41
- def format_anonymity_result(result):
42
- ip = result["ip"]
43
- print(f"IP: {ip}")
44
- print(f"Org: {result['org'] or 'N/A'}")
45
- print(f"Hostname: {result['hostname'] or 'N/A'}")
46
- if result['tor']:
47
- print("Tor Exit Node: YES")
48
- else:
49
- print("Tor Exit Node: No")
50
- if result['vpn']:
51
- print("VPN / Proxy Suspected: YES")
52
- else:
53
- print("VPN / Proxy Suspected: No")
72
+ def _confidence_score(tor: bool, vpn: bool) -> str:
73
+ if tor:
74
+ return "very high"
75
+ if vpn:
76
+ return "high"
77
+ return "low"
78
+
79
+
80
+ def format_anonymity_result(result: dict) -> None:
81
+ print(f"IP: {result.get('ip')}")
82
+ print(f"Hostname: {result.get('hostname') or 'N/A'}")
83
+ print(f"Org / ISP: {result.get('org') or result.get('isp') or 'N/A'}")
84
+
85
+ print(f"Tor Exit Node: {'YES' if result['tor'] else 'No'}")
86
+ print(f"VPN / Proxy Suspected: {'YES' if result['vpn'] else 'No'}")
87
+ print(f"Confidence: {result.get('confidence', 'unknown').upper()}")
@@ -0,0 +1,54 @@
1
+ # yougotmapped/utils/bandwidth.py
2
+
3
+ import math
4
+
5
+
6
+ DEFAULT_MSS_BYTES = 1460 # Ethernet MTU 1500 - IP/TCP headers
7
+
8
+
9
+ def estimate_bandwidth(ping_result: dict, mss_bytes: int = DEFAULT_MSS_BYTES) -> dict:
10
+ if not ping_result or not ping_result.get("reachable"):
11
+ return {
12
+ "available": False,
13
+ "reason": "no_ping_data",
14
+ }
15
+
16
+ rtt_ms = ping_result["rtt_ms"]["median"]
17
+ loss_percent = ping_result.get("packet_loss_percent", 0.0)
18
+
19
+ rtt_sec = rtt_ms / 1000.0
20
+ loss = loss_percent / 100.0
21
+
22
+ if rtt_sec <= 0:
23
+ return {
24
+ "available": False,
25
+ "reason": "invalid_rtt",
26
+ }
27
+
28
+ if loss == 0:
29
+ throughput_bps = (mss_bytes * 8) / rtt_sec
30
+ limiting_factor = "latency"
31
+ else:
32
+ throughput_bps = (mss_bytes * 8) / (rtt_sec * math.sqrt(loss))
33
+ limiting_factor = "packet_loss"
34
+
35
+ throughput_mbps = round(throughput_bps / 1_000_000, 2)
36
+
37
+ return {
38
+ "available": True,
39
+ "estimated_mbps": throughput_mbps,
40
+ "rtt_ms": rtt_ms,
41
+ "packet_loss_percent": loss_percent,
42
+ "limiting_factor": limiting_factor,
43
+ "model": "tcp_mathis",
44
+ }
45
+
46
+
47
+ def format_bandwidth_result(result: dict) -> None:
48
+ if not result.get("available"):
49
+ print("Bandwidth estimation unavailable.")
50
+ return
51
+
52
+ print(f"Estimated Throughput: ~{result['estimated_mbps']} Mbps")
53
+ print(f"Limiting Factor: {result['limiting_factor'].replace('_', ' ').title()}")
54
+ print("Model: TCP Mathis approximation")
@@ -1,33 +1,55 @@
1
- # utils/dependencies.py
1
+ # Dependency Definitions
2
+
2
3
  import importlib.util
3
4
  import subprocess
4
5
  import sys
5
6
 
6
7
 
7
- def check_dependencies():
8
- required = {
9
- "requests": "requests",
10
- "folium": "folium",
11
- "ping3": "ping3",
12
- "python-dotenv": "dotenv"
13
- }
8
+ REQUIRED_PACKAGES = {
9
+ "requests": "requests",
10
+ "folium": "folium",
11
+ "ping3": "ping3",
12
+ "scapy": "scapy"
13
+ }
14
+
15
+
16
+ def _is_installed(module_name: str) -> bool:
17
+ return importlib.util.find_spec(module_name) is not None
18
+
19
+
20
+ def _install_package(package_name: str) -> bool:
21
+ try:
22
+ subprocess.check_call(
23
+ [sys.executable, "-m", "pip", "install", package_name],
24
+ stdout=subprocess.DEVNULL,
25
+ stderr=subprocess.DEVNULL,
26
+ )
27
+ return True
28
+ except subprocess.CalledProcessError:
29
+ return False
14
30
 
31
+
32
+ def check_dependencies(interactive: bool = True) -> None:
15
33
  print("\nChecking dependencies:\n")
16
34
 
17
- for package_name, module_name in required.items():
18
- spec = importlib.util.find_spec(module_name)
19
- if spec is not None:
20
- print(f" [OK] {package_name} found")
35
+ for package, module in REQUIRED_PACKAGES.items():
36
+ if _is_installed(module):
37
+ print(f" [OK] {package} found")
38
+ continue
39
+
40
+ print(f" [MISSING] {package} not found")
41
+
42
+ if not interactive:
43
+ print(f" Install '{package}' manually and try again.")
44
+ sys.exit(1)
45
+
46
+ choice = input(f" Install '{package}' now? (yes/no): ").strip().lower()
47
+ if choice not in ("yes", "y"):
48
+ print(f" Cannot continue without '{package}'. Exiting.")
49
+ sys.exit(1)
50
+
51
+ if _install_package(package):
52
+ print(f" '{package}' successfully installed")
21
53
  else:
22
- print(f" [MISSING] {package_name} not found")
23
- choice = input(f" Install '{package_name}' now? (yes/no): ").strip().lower()
24
- if choice in ['yes', 'y']:
25
- try:
26
- subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
27
- print(f" '{package_name}' successfully installed\n")
28
- except subprocess.CalledProcessError:
29
- print(f" Failed to install '{package_name}'. Please install manually.")
30
- sys.exit(1)
31
- else:
32
- print(f" Cannot continue without '{package_name}'. Exiting.")
33
- sys.exit(1)
54
+ print(f" Failed to install '{package}'. Install manually.")
55
+ sys.exit(1)