ipilot 0.0.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.
- ipilot/__init__.py +0 -0
- ipilot/args.py +40 -0
- ipilot/ip_info.py +61 -0
- ipilot/main.py +76 -0
- ipilot/print_table.py +22 -0
- ipilot/query_normalisation.py +34 -0
- ipilot-0.0.0.dist-info/METADATA +52 -0
- ipilot-0.0.0.dist-info/RECORD +10 -0
- ipilot-0.0.0.dist-info/WHEEL +4 -0
- ipilot-0.0.0.dist-info/entry_points.txt +3 -0
ipilot/__init__.py
ADDED
|
File without changes
|
ipilot/args.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/local/env python3
|
|
2
|
+
|
|
3
|
+
"""MODULE: Provides CLI arguments to the application."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ipilot.query_normalisation import get_public_ip
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_args() -> argparse.Namespace: # pragma: no cover
|
|
11
|
+
"""Get arguments from user via the command line.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
None
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
argparse.Namespace: parsed arguments
|
|
18
|
+
"""
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description="Query information about an IP address or domain name."
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-q",
|
|
24
|
+
"--query",
|
|
25
|
+
help="IP/domain name to query (default: current public IP)",
|
|
26
|
+
default=get_public_ip(),
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-p",
|
|
30
|
+
"--prefixes",
|
|
31
|
+
help="show advertised prefixes",
|
|
32
|
+
action="store_true",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-n",
|
|
36
|
+
"--noheader",
|
|
37
|
+
help="do not print header",
|
|
38
|
+
action="store_true",
|
|
39
|
+
)
|
|
40
|
+
return parser.parse_args()
|
ipilot/ip_info.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""MODULE: Provides functions to call various APIs to retrieve IP/prefix information."""
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_ip_information(ipv4_address: ipaddress.IPv4Address) -> Optional[dict]:
|
|
13
|
+
"""Retrieves information about a given IPv4 address from IP-API.com.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
ipv4_address (ipaddress.IPv4Address): IPv4 address to query
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Optional[dict]: API response
|
|
20
|
+
"""
|
|
21
|
+
api_endpoint: str = f"http://ip-api.com/json/{ipv4_address}"
|
|
22
|
+
try:
|
|
23
|
+
resp: requests.Response = requests.get(api_endpoint, timeout=10)
|
|
24
|
+
resp.raise_for_status()
|
|
25
|
+
ret: dict | None = resp.json() if resp.json().get("status") == "success" else None
|
|
26
|
+
except (requests.exceptions.JSONDecodeError, requests.exceptions.HTTPError):
|
|
27
|
+
ret = None
|
|
28
|
+
return ret
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_autonomous_system_number(as_info: str) -> str:
|
|
32
|
+
"""Parses AS number from provided AS information.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
as_info (str): AS information
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: AS number
|
|
39
|
+
"""
|
|
40
|
+
as_number: str = as_info.split(" ")[0]
|
|
41
|
+
return as_number
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_prefix_information(autonomous_system: str) -> Optional[list]:
|
|
45
|
+
"""Retrieves prefix information about a given autonomous system.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
autonomous_system (str): autonomous system to query, e.g. AS123
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Optional[list]: API response
|
|
52
|
+
"""
|
|
53
|
+
api_endpoint: str = f"https://api.hackertarget.com/aslookup/?q={str(autonomous_system)}"
|
|
54
|
+
try:
|
|
55
|
+
resp: requests.Response = requests.get(api_endpoint, timeout=10)
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
except requests.exceptions.HTTPError:
|
|
58
|
+
return None
|
|
59
|
+
prefixes: list[str] = resp.text.split("\n")
|
|
60
|
+
prefixes.pop(0)
|
|
61
|
+
return prefixes
|
ipilot/main.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/local/bin/python3
|
|
2
|
+
|
|
3
|
+
"""MODULE: Main application module."""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ipilot.args import parse_args
|
|
8
|
+
from ipilot.ip_info import (
|
|
9
|
+
get_autonomous_system_number,
|
|
10
|
+
get_ip_information,
|
|
11
|
+
get_prefix_information,
|
|
12
|
+
)
|
|
13
|
+
from ipilot.print_table import generate_prefix_string, print_table
|
|
14
|
+
from ipilot.query_normalisation import is_ip_address, resolve_domain_name
|
|
15
|
+
|
|
16
|
+
HEADER = """-----------------------------------------------
|
|
17
|
+
| IP Address Information Lookup Tool (iPilot) |
|
|
18
|
+
| By Luke Tainton (@luketainton) |
|
|
19
|
+
-----------------------------------------------\n"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""Main function.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
None
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
None
|
|
30
|
+
"""
|
|
31
|
+
args = parse_args()
|
|
32
|
+
if not args.noheader:
|
|
33
|
+
print(HEADER)
|
|
34
|
+
|
|
35
|
+
# Set IP to passed IP address, or resolve passed domain name to IPv4
|
|
36
|
+
ip_address = resolve_domain_name(args.query) if not is_ip_address(args.query) else args.query
|
|
37
|
+
|
|
38
|
+
# If not given an IPv4, and can't resolve to IPv4, then throw error and exit
|
|
39
|
+
if not ip_address:
|
|
40
|
+
print("ERROR: could not resolve query to IPv4 address.")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
# Get information from API
|
|
44
|
+
ip_info: dict | None = get_ip_information(ip_address)
|
|
45
|
+
if not ip_info:
|
|
46
|
+
print("ERROR: could not retrieve IP information from API.")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
as_number: str = get_autonomous_system_number(ip_info["as"])
|
|
49
|
+
|
|
50
|
+
# Assemble list for table generation
|
|
51
|
+
country: str = ip_info["country"]
|
|
52
|
+
region: str = ip_info["regionName"]
|
|
53
|
+
city: str = ip_info["city"]
|
|
54
|
+
table_data: list = [
|
|
55
|
+
["IP Address", ip_info["query"]],
|
|
56
|
+
["Organization", ip_info["org"]],
|
|
57
|
+
["Location", f"{country}/{region}/{city}"],
|
|
58
|
+
["Timezone", ip_info["timezone"]],
|
|
59
|
+
["Internet Service Provider", ip_info["isp"]],
|
|
60
|
+
["Autonomous System", as_number],
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# If wanted, get prefix information
|
|
64
|
+
if args.prefixes:
|
|
65
|
+
prefix_info = get_prefix_information(as_number)
|
|
66
|
+
if not prefix_info:
|
|
67
|
+
print("ERROR: could not retrieve prefix information from API.")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
else:
|
|
70
|
+
table_data.append(["Prefixes", generate_prefix_string(prefix_info)])
|
|
71
|
+
|
|
72
|
+
print_table(table_data)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
ipilot/print_table.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""MODULE: Provides functions for preparing, then printing, retrieved data."""
|
|
4
|
+
|
|
5
|
+
from tabulate import tabulate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_prefix_string(prefixes: list) -> str | None:
|
|
9
|
+
"""Generate a string that spilts prefixes into rows of 4."""
|
|
10
|
+
num_per_row = 4
|
|
11
|
+
try:
|
|
12
|
+
ret: str = ""
|
|
13
|
+
for i in range(0, len(prefixes), num_per_row):
|
|
14
|
+
ret += ", ".join(prefixes[i : i + num_per_row]) + "\n"
|
|
15
|
+
return ret
|
|
16
|
+
except AttributeError:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_table(table_data) -> None: # pragma: no cover
|
|
21
|
+
"""Print table generated by tabulate."""
|
|
22
|
+
print(tabulate(table_data))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""MODULE: Provides functions that ensure an IP address is
|
|
4
|
+
available to query the APIs for."""
|
|
5
|
+
|
|
6
|
+
import ipaddress
|
|
7
|
+
import socket
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_ip_address(query: str) -> bool:
|
|
13
|
+
"""Verifies if a given query is a valid IPv4 address."""
|
|
14
|
+
try:
|
|
15
|
+
ipaddress.ip_address(query)
|
|
16
|
+
return True
|
|
17
|
+
except ValueError:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_domain_name(domain_name: str) -> ipaddress.IPv4Address | None:
|
|
22
|
+
"""Resolve a domain name via DNS or return None."""
|
|
23
|
+
try:
|
|
24
|
+
result: str = socket.gethostbyname(domain_name)
|
|
25
|
+
ip_address: ipaddress.IPv4Address = ipaddress.IPv4Address(result)
|
|
26
|
+
return ip_address
|
|
27
|
+
except (socket.gaierror, ipaddress.AddressValueError):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_public_ip() -> ipaddress.IPv4Address:
|
|
32
|
+
"""Get the user's current public IPv4 address."""
|
|
33
|
+
ip_address: str = requests.get("https://api.ipify.org", timeout=10).text
|
|
34
|
+
return ipaddress.IPv4Address(ip_address)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ipilot
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: IP Information Lookup Tool
|
|
5
|
+
Author: Luke Tainton
|
|
6
|
+
Author-email: luke@tainton.uk
|
|
7
|
+
Requires-Python: >=3.8,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: astroid (==3.2.4)
|
|
16
|
+
Requires-Dist: attrs (==24.3.0)
|
|
17
|
+
Requires-Dist: certifi (==2024.12.14)
|
|
18
|
+
Requires-Dist: charset-normalizer (==3.4.1)
|
|
19
|
+
Requires-Dist: click (==8.1.8)
|
|
20
|
+
Requires-Dist: dill (==0.3.9)
|
|
21
|
+
Requires-Dist: exceptiongroup (==1.2.2)
|
|
22
|
+
Requires-Dist: idna (==3.10)
|
|
23
|
+
Requires-Dist: iniconfig (==2.0.0)
|
|
24
|
+
Requires-Dist: lazy-object-proxy (==1.10.0)
|
|
25
|
+
Requires-Dist: mccabe (==0.7.0)
|
|
26
|
+
Requires-Dist: mypy-extensions (==1.0.0)
|
|
27
|
+
Requires-Dist: packaging (==24.2)
|
|
28
|
+
Requires-Dist: pathspec (==0.12.1)
|
|
29
|
+
Requires-Dist: platformdirs (==4.3.6)
|
|
30
|
+
Requires-Dist: pluggy (==1.5.0)
|
|
31
|
+
Requires-Dist: py (==1.11.0)
|
|
32
|
+
Requires-Dist: pyparsing (==3.1.4)
|
|
33
|
+
Requires-Dist: requests (==2.32.3)
|
|
34
|
+
Requires-Dist: six (==1.17.0)
|
|
35
|
+
Requires-Dist: tabulate (==0.9.0)
|
|
36
|
+
Requires-Dist: tomli (==2.2.1)
|
|
37
|
+
Requires-Dist: tomlkit (==0.13.2)
|
|
38
|
+
Requires-Dist: urllib3 (==2.2.3)
|
|
39
|
+
Requires-Dist: wrapt (==1.17.0)
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# iPilot [](https://github.com/luketainton/pypilot/actions/workflows/ci.yml) [](https://sonarcloud.io/summary/new_code?id=luketainton_pypilot)
|
|
43
|
+
|
|
44
|
+
## Description
|
|
45
|
+
IP Information Lookup Tool
|
|
46
|
+
|
|
47
|
+
## How to install
|
|
48
|
+
`pip install ipilot`
|
|
49
|
+
|
|
50
|
+
## How to use
|
|
51
|
+
`ipilot --help`
|
|
52
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ipilot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ipilot/args.py,sha256=xtjr6B1XH9wUswMDK5cpTDWRrNA7Ld3RWmvNrW0F104,954
|
|
3
|
+
ipilot/ip_info.py,sha256=NAy3Wdo92YhlKP1PCjXOsO7a4IwAPKyQP-9KyVb4l2E,1787
|
|
4
|
+
ipilot/main.py,sha256=tiY9ZGNyN9cFamREDZPgImCs3wi4G_j-Io1ZQ1XI4zQ,2259
|
|
5
|
+
ipilot/print_table.py,sha256=3QP3mdN5_0n4JVZ_S3r1Rn4FgmoHkWcZhmaM3MCp-WU,634
|
|
6
|
+
ipilot/query_normalisation.py,sha256=9nOYhVwvr8eOfWfmE8_CptHpRSwCKXH9YDyKzUqUWe8,980
|
|
7
|
+
ipilot-0.0.0.dist-info/METADATA,sha256=NvYyjD7Stv4_sg3-754mno4BTHhbB-hxoyj38ykWvS4,1877
|
|
8
|
+
ipilot-0.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
9
|
+
ipilot-0.0.0.dist-info/entry_points.txt,sha256=2Ve389A36UooJzr-7C1hdB99TlYR0aoBkrL4TDrgRqg,40
|
|
10
|
+
ipilot-0.0.0.dist-info/RECORD,,
|