know-your-ip 0.2.1__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.
- know_your_ip/__init__.py +50 -0
- know_your_ip/config.py +415 -0
- know_your_ip/know_your_ip.py +991 -0
- know_your_ip/ping.py +235 -0
- know_your_ip/traceroute.py +49 -0
- know_your_ip-0.2.1.dist-info/METADATA +315 -0
- know_your_ip-0.2.1.dist-info/RECORD +11 -0
- know_your_ip-0.2.1.dist-info/WHEEL +5 -0
- know_your_ip-0.2.1.dist-info/entry_points.txt +2 -0
- know_your_ip-0.2.1.dist-info/licenses/LICENSE +21 -0
- know_your_ip-0.2.1.dist-info/top_level.txt +1 -0
know_your_ip/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Know Your IP
|
|
2
|
+
|
|
3
|
+
A Python package to get comprehensive data about IP addresses including:
|
|
4
|
+
- Geolocation (latitude/longitude, country, city, timezone)
|
|
5
|
+
- Security analysis (blacklist status via multiple services)
|
|
6
|
+
- Network information (open ports, running services)
|
|
7
|
+
- Network diagnostics (ping, traceroute)
|
|
8
|
+
|
|
9
|
+
Supports multiple data sources including MaxMind, AbuseIPDB, VirusTotal,
|
|
10
|
+
Shodan, Censys, and more.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import version
|
|
14
|
+
|
|
15
|
+
__version__ = version("know_your_ip")
|
|
16
|
+
|
|
17
|
+
from .config import KnowYourIPConfig, load_config
|
|
18
|
+
from .know_your_ip import (
|
|
19
|
+
abuseipdb_api,
|
|
20
|
+
abuseipdb_web,
|
|
21
|
+
apivoid_api,
|
|
22
|
+
censys_api,
|
|
23
|
+
geonames_timezone,
|
|
24
|
+
ipvoid_scan,
|
|
25
|
+
maxmind_geocode_ip,
|
|
26
|
+
ping,
|
|
27
|
+
query_ip,
|
|
28
|
+
shodan_api,
|
|
29
|
+
traceroute,
|
|
30
|
+
tzwhere_timezone,
|
|
31
|
+
virustotal_api,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"KnowYourIPConfig",
|
|
36
|
+
"load_config",
|
|
37
|
+
"maxmind_geocode_ip",
|
|
38
|
+
"geonames_timezone",
|
|
39
|
+
"tzwhere_timezone",
|
|
40
|
+
"ipvoid_scan",
|
|
41
|
+
"abuseipdb_web",
|
|
42
|
+
"abuseipdb_api",
|
|
43
|
+
"censys_api",
|
|
44
|
+
"shodan_api",
|
|
45
|
+
"virustotal_api",
|
|
46
|
+
"ping",
|
|
47
|
+
"traceroute",
|
|
48
|
+
"query_ip",
|
|
49
|
+
"apivoid_api",
|
|
50
|
+
]
|
know_your_ip/config.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Modern configuration system using Pydantic for validation and type safety."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tomllib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MaxMindConfig(BaseModel):
|
|
14
|
+
"""MaxMind GeoIP configuration."""
|
|
15
|
+
|
|
16
|
+
enabled: bool = True
|
|
17
|
+
db_path: Path = Path("./db")
|
|
18
|
+
|
|
19
|
+
@field_validator("db_path", mode="before")
|
|
20
|
+
@classmethod
|
|
21
|
+
def resolve_db_path(cls, v: str | Path) -> Path:
|
|
22
|
+
"""Resolve relative paths to absolute paths."""
|
|
23
|
+
path = Path(v)
|
|
24
|
+
if not path.is_absolute():
|
|
25
|
+
# If relative, make it relative to package directory
|
|
26
|
+
package_dir = Path(__file__).parent
|
|
27
|
+
path = package_dir / path
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GeoNamesConfig(BaseModel):
|
|
32
|
+
"""GeoNames.org configuration."""
|
|
33
|
+
|
|
34
|
+
enabled: bool = False
|
|
35
|
+
username: str | None = None
|
|
36
|
+
|
|
37
|
+
@field_validator("username")
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate_username(cls, v: str | None) -> str | None:
|
|
40
|
+
"""Validate that username is provided if enabled."""
|
|
41
|
+
if v and v.startswith("<<<"):
|
|
42
|
+
return None # Placeholder value
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AbuseIPDBConfig(BaseModel):
|
|
47
|
+
"""AbuseIPDB configuration."""
|
|
48
|
+
|
|
49
|
+
enabled: bool = False
|
|
50
|
+
api_key: str | None = None
|
|
51
|
+
days: int = 180
|
|
52
|
+
|
|
53
|
+
@field_validator("api_key")
|
|
54
|
+
@classmethod
|
|
55
|
+
def validate_api_key(cls, v: str | None) -> str | None:
|
|
56
|
+
"""Validate that API key is provided if enabled."""
|
|
57
|
+
if v and v.startswith("<<<"):
|
|
58
|
+
return None # Placeholder value
|
|
59
|
+
return v
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PingConfig(BaseModel):
|
|
63
|
+
"""Ping configuration."""
|
|
64
|
+
|
|
65
|
+
enabled: bool = False
|
|
66
|
+
timeout: int = 3000
|
|
67
|
+
count: int = 3
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TracerouteConfig(BaseModel):
|
|
71
|
+
"""Traceroute configuration."""
|
|
72
|
+
|
|
73
|
+
enabled: bool = False
|
|
74
|
+
max_hops: int = 30
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TzwhereConfig(BaseModel):
|
|
78
|
+
"""tzwhere configuration."""
|
|
79
|
+
|
|
80
|
+
enabled: bool = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class IPVoidConfig(BaseModel):
|
|
84
|
+
"""IPVoid configuration."""
|
|
85
|
+
|
|
86
|
+
enabled: bool = True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class APIVoidConfig(BaseModel):
|
|
90
|
+
"""APIVoid configuration."""
|
|
91
|
+
|
|
92
|
+
enabled: bool = False
|
|
93
|
+
api_key: str | None = None
|
|
94
|
+
|
|
95
|
+
@field_validator("api_key")
|
|
96
|
+
@classmethod
|
|
97
|
+
def validate_api_key(cls, v: str | None) -> str | None:
|
|
98
|
+
"""Validate that API key is provided if enabled."""
|
|
99
|
+
if v and v.startswith("<<<"):
|
|
100
|
+
return None # Placeholder value
|
|
101
|
+
return v
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class CensysConfig(BaseModel):
|
|
105
|
+
"""Censys Platform API configuration.
|
|
106
|
+
|
|
107
|
+
Note: Legacy Censys Search v1/v2 APIs are deprecated as of 2025.
|
|
108
|
+
This uses the new Censys Platform API.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
enabled: bool = False
|
|
112
|
+
api_url: str = "https://search.censys.io/api"
|
|
113
|
+
api_key: str | None = None
|
|
114
|
+
|
|
115
|
+
@field_validator("api_key")
|
|
116
|
+
@classmethod
|
|
117
|
+
def validate_api_key(cls, v: str | None) -> str | None:
|
|
118
|
+
"""Validate that API key is provided if enabled."""
|
|
119
|
+
if v and v.startswith("<<<"):
|
|
120
|
+
return None # Placeholder value
|
|
121
|
+
return v
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ShodanConfig(BaseModel):
|
|
125
|
+
"""Shodan configuration."""
|
|
126
|
+
|
|
127
|
+
enabled: bool = False
|
|
128
|
+
api_key: str | None = None
|
|
129
|
+
|
|
130
|
+
@field_validator("api_key")
|
|
131
|
+
@classmethod
|
|
132
|
+
def validate_api_key(cls, v: str | None) -> str | None:
|
|
133
|
+
"""Validate that API key is provided if enabled."""
|
|
134
|
+
if v and v.startswith("<<<"):
|
|
135
|
+
return None # Placeholder value
|
|
136
|
+
return v
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class VirusTotalConfig(BaseModel):
|
|
140
|
+
"""VirusTotal configuration."""
|
|
141
|
+
|
|
142
|
+
enabled: bool = False
|
|
143
|
+
api_key: str | None = None
|
|
144
|
+
|
|
145
|
+
@field_validator("api_key")
|
|
146
|
+
@classmethod
|
|
147
|
+
def validate_api_key(cls, v: str | None) -> str | None:
|
|
148
|
+
"""Validate that API key is provided if enabled."""
|
|
149
|
+
if v and v.startswith("<<<"):
|
|
150
|
+
return None # Placeholder value
|
|
151
|
+
return v
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class OutputConfig(BaseModel):
|
|
155
|
+
"""Output configuration."""
|
|
156
|
+
|
|
157
|
+
columns: list[str] = Field(
|
|
158
|
+
default=[
|
|
159
|
+
"ip",
|
|
160
|
+
"maxmind.continent.names.en",
|
|
161
|
+
"maxmind.country.names.en",
|
|
162
|
+
"maxmind.location.time_zone",
|
|
163
|
+
"maxmind.postal.code",
|
|
164
|
+
"maxmind.registered_country.names.en",
|
|
165
|
+
"tzwhere.timezone",
|
|
166
|
+
"abuseipdb.bad_isp",
|
|
167
|
+
"abuseipdb.categories",
|
|
168
|
+
"ipvoid.blacklist_status",
|
|
169
|
+
"ipvoid.reverse_dns",
|
|
170
|
+
"apivoid.anonymity.is_hosting",
|
|
171
|
+
"apivoid.anonymity.is_proxy",
|
|
172
|
+
"apivoid.anonymity.is_tor",
|
|
173
|
+
"apivoid.anonymity.is_vpn",
|
|
174
|
+
"apivoid.anonymity.is_webproxy",
|
|
175
|
+
"apivoid.blacklists.detection_rate",
|
|
176
|
+
"apivoid.blacklists.detections",
|
|
177
|
+
"apivoid.blacklists.engines_count",
|
|
178
|
+
"apivoid.blacklists.scantime",
|
|
179
|
+
"apivoid.information.city_name",
|
|
180
|
+
"apivoid.information.continent_code",
|
|
181
|
+
"apivoid.information.continent_name",
|
|
182
|
+
"apivoid.information.country_calling_code",
|
|
183
|
+
"apivoid.information.country_code",
|
|
184
|
+
"apivoid.information.country_currency",
|
|
185
|
+
"apivoid.information.country_name",
|
|
186
|
+
"apivoid.information.isp",
|
|
187
|
+
"apivoid.information.latitude",
|
|
188
|
+
"apivoid.information.longitude",
|
|
189
|
+
"apivoid.information.region_name",
|
|
190
|
+
"apivoid.information.reverse_dns",
|
|
191
|
+
"shodan.asn",
|
|
192
|
+
"shodan.isp",
|
|
193
|
+
"shodan.vulns",
|
|
194
|
+
"shodan.os",
|
|
195
|
+
"shodan.ports",
|
|
196
|
+
]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class KnowYourIPConfig(BaseModel):
|
|
201
|
+
"""Main configuration for Know Your IP."""
|
|
202
|
+
|
|
203
|
+
maxmind: MaxMindConfig = MaxMindConfig()
|
|
204
|
+
geonames: GeoNamesConfig = GeoNamesConfig()
|
|
205
|
+
abuseipdb: AbuseIPDBConfig = AbuseIPDBConfig()
|
|
206
|
+
ping: PingConfig = PingConfig()
|
|
207
|
+
traceroute: TracerouteConfig = TracerouteConfig()
|
|
208
|
+
tzwhere: TzwhereConfig = TzwhereConfig()
|
|
209
|
+
ipvoid: IPVoidConfig = IPVoidConfig()
|
|
210
|
+
apivoid: APIVoidConfig = APIVoidConfig()
|
|
211
|
+
censys: CensysConfig = CensysConfig()
|
|
212
|
+
shodan: ShodanConfig = ShodanConfig()
|
|
213
|
+
virustotal: VirusTotalConfig = VirusTotalConfig()
|
|
214
|
+
output: OutputConfig = OutputConfig()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def load_from_env() -> dict[str, Any]:
|
|
218
|
+
"""Load configuration from environment variables.
|
|
219
|
+
|
|
220
|
+
Environment variables should follow the pattern:
|
|
221
|
+
KNOW_YOUR_IP_<SECTION>_<KEY>=value
|
|
222
|
+
|
|
223
|
+
Examples:
|
|
224
|
+
KNOW_YOUR_IP_MAXMIND_ENABLED=true
|
|
225
|
+
KNOW_YOUR_IP_GEONAMES_USERNAME=myusername
|
|
226
|
+
KNOW_YOUR_IP_ABUSEIPDB_API_KEY=myapikey
|
|
227
|
+
"""
|
|
228
|
+
config = {}
|
|
229
|
+
prefix = "KNOW_YOUR_IP_"
|
|
230
|
+
|
|
231
|
+
for key, value in os.environ.items():
|
|
232
|
+
if not key.startswith(prefix):
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Remove prefix and split into section and field
|
|
236
|
+
config_key = key[len(prefix) :].lower()
|
|
237
|
+
parts = config_key.split("_", 1)
|
|
238
|
+
|
|
239
|
+
if len(parts) != 2:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
section, field = parts
|
|
243
|
+
|
|
244
|
+
# Convert string values to appropriate types
|
|
245
|
+
match value.lower():
|
|
246
|
+
case "true" | "1" | "yes" | "on":
|
|
247
|
+
value = True
|
|
248
|
+
case "false" | "0" | "no" | "off":
|
|
249
|
+
value = False
|
|
250
|
+
case _ if value.isdigit():
|
|
251
|
+
value = int(value)
|
|
252
|
+
|
|
253
|
+
# Create nested dict structure
|
|
254
|
+
if section not in config:
|
|
255
|
+
config[section] = {}
|
|
256
|
+
config[section][field] = value
|
|
257
|
+
|
|
258
|
+
return config
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def find_config_file() -> Path | None:
|
|
262
|
+
"""Find configuration file in standard locations.
|
|
263
|
+
|
|
264
|
+
Search order:
|
|
265
|
+
1. ./know_your_ip.toml (current directory)
|
|
266
|
+
2. ~/.config/know-your-ip/config.toml (XDG config)
|
|
267
|
+
3. ~/.know-your-ip.toml (home directory)
|
|
268
|
+
"""
|
|
269
|
+
candidates = [
|
|
270
|
+
Path.cwd() / "know_your_ip.toml",
|
|
271
|
+
Path.home() / ".config" / "know-your-ip" / "config.toml",
|
|
272
|
+
Path.home() / ".know-your-ip.toml",
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
for candidate in candidates:
|
|
276
|
+
if candidate.exists():
|
|
277
|
+
return candidate
|
|
278
|
+
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def load_config(config_file: Path | None = None) -> KnowYourIPConfig:
|
|
283
|
+
"""Load configuration from multiple sources with proper validation.
|
|
284
|
+
|
|
285
|
+
Sources are loaded in this order (later sources override earlier ones):
|
|
286
|
+
1. Default configuration (embedded in code)
|
|
287
|
+
2. Configuration file (TOML format)
|
|
288
|
+
3. Environment variables
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
config_file: Path to configuration file. If None, will search standard locations.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Validated configuration object.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ConfigurationError: If configuration is invalid.
|
|
298
|
+
"""
|
|
299
|
+
config_dict = {}
|
|
300
|
+
|
|
301
|
+
# Load from file if provided or found
|
|
302
|
+
if config_file is None:
|
|
303
|
+
config_file = find_config_file()
|
|
304
|
+
|
|
305
|
+
if config_file and config_file.exists():
|
|
306
|
+
try:
|
|
307
|
+
with open(config_file, "rb") as f:
|
|
308
|
+
file_config = tomllib.load(f)
|
|
309
|
+
config_dict.update(file_config)
|
|
310
|
+
except (OSError, tomllib.TOMLDecodeError) as e:
|
|
311
|
+
raise ConfigurationError(
|
|
312
|
+
f"Failed to load config file {config_file}: {e}"
|
|
313
|
+
) from e
|
|
314
|
+
|
|
315
|
+
# Override with environment variables
|
|
316
|
+
env_config = load_from_env()
|
|
317
|
+
for section, values in env_config.items():
|
|
318
|
+
if section not in config_dict:
|
|
319
|
+
config_dict[section] = {}
|
|
320
|
+
config_dict[section].update(values)
|
|
321
|
+
|
|
322
|
+
# Validate and return typed config
|
|
323
|
+
try:
|
|
324
|
+
return KnowYourIPConfig(**config_dict)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
raise ConfigurationError(f"Configuration validation failed: {e}") from e
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def create_default_config(output_file: Path) -> None:
|
|
330
|
+
"""Create a default configuration file with sensible defaults."""
|
|
331
|
+
|
|
332
|
+
toml_content = """# Know Your IP Configuration
|
|
333
|
+
# See https://github.com/themains/know-your-ip for documentation
|
|
334
|
+
|
|
335
|
+
[maxmind]
|
|
336
|
+
enabled = true
|
|
337
|
+
db_path = "./db"
|
|
338
|
+
|
|
339
|
+
[geonames]
|
|
340
|
+
enabled = false
|
|
341
|
+
# username = "your_username_here" # Register at http://www.geonames.org/login
|
|
342
|
+
|
|
343
|
+
[abuseipdb]
|
|
344
|
+
enabled = false
|
|
345
|
+
# api_key = "your_api_key_here" # Register at https://www.abuseipdb.com/register
|
|
346
|
+
days = 180
|
|
347
|
+
|
|
348
|
+
[ping]
|
|
349
|
+
enabled = false
|
|
350
|
+
timeout = 3000
|
|
351
|
+
count = 3
|
|
352
|
+
|
|
353
|
+
[traceroute]
|
|
354
|
+
enabled = false
|
|
355
|
+
max_hops = 30
|
|
356
|
+
|
|
357
|
+
[tzwhere]
|
|
358
|
+
enabled = true
|
|
359
|
+
|
|
360
|
+
[ipvoid]
|
|
361
|
+
enabled = true
|
|
362
|
+
|
|
363
|
+
[apivoid]
|
|
364
|
+
enabled = false
|
|
365
|
+
# api_key = "your_api_key_here" # Register at https://app.apivoid.com/register
|
|
366
|
+
|
|
367
|
+
[censys]
|
|
368
|
+
enabled = false
|
|
369
|
+
api_url = "https://search.censys.io/api"
|
|
370
|
+
# api_key = "your_api_key_here" # Register at https://search.censys.io/register
|
|
371
|
+
|
|
372
|
+
[shodan]
|
|
373
|
+
enabled = false
|
|
374
|
+
# api_key = "your_api_key_here" # Register at https://account.shodan.io/register
|
|
375
|
+
|
|
376
|
+
[virustotal]
|
|
377
|
+
enabled = false
|
|
378
|
+
# api_key = "your_api_key_here" # Register at https://www.virustotal.com/
|
|
379
|
+
|
|
380
|
+
[output]
|
|
381
|
+
columns = [
|
|
382
|
+
"ip",
|
|
383
|
+
"maxmind.continent.names.en",
|
|
384
|
+
"maxmind.country.names.en",
|
|
385
|
+
"maxmind.location.time_zone",
|
|
386
|
+
"maxmind.postal.code",
|
|
387
|
+
"maxmind.registered_country.names.en",
|
|
388
|
+
"tzwhere.timezone",
|
|
389
|
+
"abuseipdb.bad_isp",
|
|
390
|
+
"abuseipdb.categories",
|
|
391
|
+
"ipvoid.blacklist_status",
|
|
392
|
+
"ipvoid.reverse_dns",
|
|
393
|
+
"apivoid.anonymity.is_hosting",
|
|
394
|
+
"apivoid.anonymity.is_proxy",
|
|
395
|
+
"apivoid.anonymity.is_tor",
|
|
396
|
+
"apivoid.anonymity.is_vpn",
|
|
397
|
+
"apivoid.anonymity.is_webproxy",
|
|
398
|
+
"apivoid.blacklists.detection_rate",
|
|
399
|
+
"apivoid.blacklists.detections",
|
|
400
|
+
"shodan.asn",
|
|
401
|
+
"shodan.isp",
|
|
402
|
+
"shodan.vulns",
|
|
403
|
+
"shodan.os",
|
|
404
|
+
"shodan.ports",
|
|
405
|
+
]
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
output_file.write_text(toml_content)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class ConfigurationError(Exception):
|
|
413
|
+
"""Configuration-related errors."""
|
|
414
|
+
|
|
415
|
+
pass
|