asndb 1.0.0__tar.gz

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.
asndb-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: asndb
3
+ Version: 1.0.0
4
+ Summary: A simple Python CLI + library for looking up ASNs by IP or AS number. Uses BBOT.IO API.
5
+ Author: TheTechromancer
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: cachetools>=6.2.1
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: radixtarget>=4.0.1
11
+ Requires-Dist: typer>=0.19.2
12
+
13
+ # ASNDB
14
+
15
+ [![Python Version](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org) ![PyPI - Version](https://img.shields.io/pypi/v/asndb) [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/radixtarget/blob/master/LICENSE) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/blacklanternsecurity/asndb/actions/workflows/tests.yml/badge.svg)](https://github.com/blacklanternsecurity/asndb/actions/workflows/tests.yml)
16
+
17
+ A simple Python CLI + library for instant lookup of ASN data by IP address, AS number, or organization. Uses BBOT.IO API (`asndb.api.bbot.io`).
18
+
19
+ ASNs are automatically cached for instant lookups and minimal network traffic.
20
+
21
+ ## Installation
22
+
23
+ ```
24
+ pip install asndb
25
+
26
+ # or using uv
27
+ uv add asndb
28
+ ```
29
+
30
+ ## Usage (CLI)
31
+
32
+ Note: To avoid rate limits, export your BBOT.IO API key as an environment variable:
33
+ ```bash
34
+ export BBOT_IO_API_KEY=<your_api_key>
35
+ ```
36
+
37
+ ### IP Lookup
38
+ ```bash
39
+ asndb ip 1.1.1.1
40
+ ```
41
+
42
+ Output:
43
+ ```json
44
+ {
45
+ "asn": 13335,
46
+ "asn_name": "CLOUDFLARENET",
47
+ "country": "US",
48
+ "ip": "1.1.1.1",
49
+ "org": "Cloudflare, Inc.",
50
+ "org_id": "CLOUD14-ARIN",
51
+ "rir": "ARIN",
52
+ "subnets": [
53
+ "1.0.0.0/24",
54
+ "1.0.1.0/24"
55
+ ]
56
+ }
57
+ ```
58
+
59
+ ### AS Number Lookup
60
+
61
+ ```bash
62
+ asndb asn 13335
63
+ ```
64
+
65
+ Output:
66
+ ```json
67
+ {
68
+ "asn": 13335,
69
+ "asn_name": "CLOUDFLARENET",
70
+ "country": "US",
71
+ "ip": "1.1.1.1",
72
+ "org": "Cloudflare, Inc.",
73
+ "org_id": "CLOUD14-ARIN",
74
+ "rir": "ARIN",
75
+ "subnets": [
76
+ "1.0.0.0/24",
77
+ "1.0.1.0/24"
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ### Organization Lookup
83
+
84
+ ```bash
85
+ # Look up an organization
86
+ asndb org CLOUD14-ARIN
87
+ ```
88
+
89
+ Output:
90
+ ```json
91
+ {
92
+ "asns": [
93
+ 13335,
94
+ 14789,
95
+ 395747,
96
+ 394536
97
+ ]
98
+ }
99
+ ```
100
+
101
+ ### CLI Help
102
+
103
+ ```
104
+ $ uv run asndb --help
105
+
106
+ Usage: asndb [OPTIONS] COMMAND [ARGS]...
107
+
108
+ ╭─ Options ──────────────────────────────────────────────────────────────────────────────────╮
109
+ │ --install-completion Install completion for the current shell. │
110
+ │ --show-completion Show completion for the current shell, to copy it or │
111
+ │ customize the installation. │
112
+ │ --help Show this message and exit. │
113
+ ╰────────────────────────────────────────────────────────────────────────────────────────────╯
114
+ ╭─ Commands ─────────────────────────────────────────────────────────────────────────────────╮
115
+ │ ip Lookup ASN by IP address │
116
+ │ asn Lookup ASN by AS number │
117
+ │ org Get all the ASNs for an organization, by its registered organization ID, e.g. │
118
+ │ GOGL-ARIN │
119
+ ╰────────────────────────────────────────────────────────────────────────────────────────────╯
120
+ ```
121
+
122
+ ## Usage (Python)
123
+
124
+ ```python
125
+ from asndb import ASNDB
126
+
127
+ # Create a new ASNDB client
128
+ asndb = ASNDB()
129
+
130
+
131
+ # Look up an IP address
132
+ asn = asndb.lookup_ip_sync("1.1.1.1")
133
+ # {
134
+ # "asn": 13335,
135
+ # "asn_name": "CLOUDFLARENET",
136
+ # "country": "US",
137
+ # "ip": "1.1.1.1",
138
+ # "org": "Cloudflare, Inc.",
139
+ # "org_id": "CLOUD14-ARIN",
140
+ # "rir": "ARIN",
141
+ # "subnets": [
142
+ # "1.0.0.0/24",
143
+ # "1.0.1.0/24"
144
+ # ]
145
+ # }
146
+
147
+ # Look up an AS number
148
+ asn = asndb.lookup_asn_sync(13335)
149
+ # {
150
+ # "asn": 13335,
151
+ # "asn_name": "CLOUDFLARENET",
152
+ # "country": "US",
153
+ # "ip": "1.1.1.1",
154
+ # "org": "Cloudflare, Inc.",
155
+ # "org_id": "CLOUD14-ARIN",
156
+ # "rir": "ARIN",
157
+ # "subnets": [
158
+ # "1.0.0.0/24",
159
+ # "1.0.1.0/24"
160
+ # ]
161
+ # }
162
+
163
+ # Look up an organization
164
+ org = asndb.lookup_org_sync("CLOUD14-ARIN")
165
+ # {
166
+ # "asns": [
167
+ # 13335,
168
+ # 14789,
169
+ # 395747,
170
+ # 394536
171
+ # ]
172
+ # }
173
+ ```
174
+
175
+ ## Environment Variables
176
+
177
+ You can customize the behavior of the ASNDB client by exporting the following environment variables:
178
+
179
+ - `BBOT_IO_API_KEY`: Your BBOT.IO API key.
180
+ - `ASNDB_BASE_URL`: The base URL of the ASNDB API (default: https://asndb.api.bbot.io/v1).
181
+ - `ASNDB_TIMEOUT`: The timeout for the ASNDB API requests (default: 60 seconds).
182
+ - `ASNDB_CACHE_SIZE`: The size of the cache for the ASNDB API requests (default: 10000).
asndb-1.0.0/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # ASNDB
2
+
3
+ [![Python Version](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org) ![PyPI - Version](https://img.shields.io/pypi/v/asndb) [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/radixtarget/blob/master/LICENSE) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/blacklanternsecurity/asndb/actions/workflows/tests.yml/badge.svg)](https://github.com/blacklanternsecurity/asndb/actions/workflows/tests.yml)
4
+
5
+ A simple Python CLI + library for instant lookup of ASN data by IP address, AS number, or organization. Uses BBOT.IO API (`asndb.api.bbot.io`).
6
+
7
+ ASNs are automatically cached for instant lookups and minimal network traffic.
8
+
9
+ ## Installation
10
+
11
+ ```
12
+ pip install asndb
13
+
14
+ # or using uv
15
+ uv add asndb
16
+ ```
17
+
18
+ ## Usage (CLI)
19
+
20
+ Note: To avoid rate limits, export your BBOT.IO API key as an environment variable:
21
+ ```bash
22
+ export BBOT_IO_API_KEY=<your_api_key>
23
+ ```
24
+
25
+ ### IP Lookup
26
+ ```bash
27
+ asndb ip 1.1.1.1
28
+ ```
29
+
30
+ Output:
31
+ ```json
32
+ {
33
+ "asn": 13335,
34
+ "asn_name": "CLOUDFLARENET",
35
+ "country": "US",
36
+ "ip": "1.1.1.1",
37
+ "org": "Cloudflare, Inc.",
38
+ "org_id": "CLOUD14-ARIN",
39
+ "rir": "ARIN",
40
+ "subnets": [
41
+ "1.0.0.0/24",
42
+ "1.0.1.0/24"
43
+ ]
44
+ }
45
+ ```
46
+
47
+ ### AS Number Lookup
48
+
49
+ ```bash
50
+ asndb asn 13335
51
+ ```
52
+
53
+ Output:
54
+ ```json
55
+ {
56
+ "asn": 13335,
57
+ "asn_name": "CLOUDFLARENET",
58
+ "country": "US",
59
+ "ip": "1.1.1.1",
60
+ "org": "Cloudflare, Inc.",
61
+ "org_id": "CLOUD14-ARIN",
62
+ "rir": "ARIN",
63
+ "subnets": [
64
+ "1.0.0.0/24",
65
+ "1.0.1.0/24"
66
+ ]
67
+ }
68
+ ```
69
+
70
+ ### Organization Lookup
71
+
72
+ ```bash
73
+ # Look up an organization
74
+ asndb org CLOUD14-ARIN
75
+ ```
76
+
77
+ Output:
78
+ ```json
79
+ {
80
+ "asns": [
81
+ 13335,
82
+ 14789,
83
+ 395747,
84
+ 394536
85
+ ]
86
+ }
87
+ ```
88
+
89
+ ### CLI Help
90
+
91
+ ```
92
+ $ uv run asndb --help
93
+
94
+ Usage: asndb [OPTIONS] COMMAND [ARGS]...
95
+
96
+ ╭─ Options ──────────────────────────────────────────────────────────────────────────────────╮
97
+ │ --install-completion Install completion for the current shell. │
98
+ │ --show-completion Show completion for the current shell, to copy it or │
99
+ │ customize the installation. │
100
+ │ --help Show this message and exit. │
101
+ ╰────────────────────────────────────────────────────────────────────────────────────────────╯
102
+ ╭─ Commands ─────────────────────────────────────────────────────────────────────────────────╮
103
+ │ ip Lookup ASN by IP address │
104
+ │ asn Lookup ASN by AS number │
105
+ │ org Get all the ASNs for an organization, by its registered organization ID, e.g. │
106
+ │ GOGL-ARIN │
107
+ ╰────────────────────────────────────────────────────────────────────────────────────────────╯
108
+ ```
109
+
110
+ ## Usage (Python)
111
+
112
+ ```python
113
+ from asndb import ASNDB
114
+
115
+ # Create a new ASNDB client
116
+ asndb = ASNDB()
117
+
118
+
119
+ # Look up an IP address
120
+ asn = asndb.lookup_ip_sync("1.1.1.1")
121
+ # {
122
+ # "asn": 13335,
123
+ # "asn_name": "CLOUDFLARENET",
124
+ # "country": "US",
125
+ # "ip": "1.1.1.1",
126
+ # "org": "Cloudflare, Inc.",
127
+ # "org_id": "CLOUD14-ARIN",
128
+ # "rir": "ARIN",
129
+ # "subnets": [
130
+ # "1.0.0.0/24",
131
+ # "1.0.1.0/24"
132
+ # ]
133
+ # }
134
+
135
+ # Look up an AS number
136
+ asn = asndb.lookup_asn_sync(13335)
137
+ # {
138
+ # "asn": 13335,
139
+ # "asn_name": "CLOUDFLARENET",
140
+ # "country": "US",
141
+ # "ip": "1.1.1.1",
142
+ # "org": "Cloudflare, Inc.",
143
+ # "org_id": "CLOUD14-ARIN",
144
+ # "rir": "ARIN",
145
+ # "subnets": [
146
+ # "1.0.0.0/24",
147
+ # "1.0.1.0/24"
148
+ # ]
149
+ # }
150
+
151
+ # Look up an organization
152
+ org = asndb.lookup_org_sync("CLOUD14-ARIN")
153
+ # {
154
+ # "asns": [
155
+ # 13335,
156
+ # 14789,
157
+ # 395747,
158
+ # 394536
159
+ # ]
160
+ # }
161
+ ```
162
+
163
+ ## Environment Variables
164
+
165
+ You can customize the behavior of the ASNDB client by exporting the following environment variables:
166
+
167
+ - `BBOT_IO_API_KEY`: Your BBOT.IO API key.
168
+ - `ASNDB_BASE_URL`: The base URL of the ASNDB API (default: https://asndb.api.bbot.io/v1).
169
+ - `ASNDB_TIMEOUT`: The timeout for the ASNDB API requests (default: 60 seconds).
170
+ - `ASNDB_CACHE_SIZE`: The size of the cache for the ASNDB API requests (default: 10000).
@@ -0,0 +1 @@
1
+ from asndb.asndb import ASNDB
@@ -0,0 +1,186 @@
1
+ import os
2
+ import time
3
+ import asyncio
4
+ from contextlib import contextmanager, suppress
5
+
6
+ import httpx
7
+ import cachetools
8
+ from radixtarget import RadixTarget
9
+
10
+
11
+ class ASNDBError(Exception):
12
+ pass
13
+
14
+
15
+ class ASNDBTimeoutError(ASNDBError):
16
+ pass
17
+
18
+
19
+ class ASNDBClient:
20
+ BASE_URL = "https://asndb.api.bbot.io/v1"
21
+ DEFAULT_CACHE_SIZE = 10000
22
+ DEFAULT_TIMEOUT = 60
23
+
24
+ # Default record used when no ASN data can be found
25
+ UNKNOWN_ASN = {
26
+ "asn": 0,
27
+ "subnets": [],
28
+ "name": "Unknown",
29
+ "description": "Unknown ASN",
30
+ "country": "Unknown",
31
+ }
32
+
33
+ def __init__(self, bbot_io_api_key=None):
34
+ self.bbot_io_api_key = os.getenv("BBOT_IO_API_KEY", None) or bbot_io_api_key
35
+ self.base_url = os.getenv("ASNDB_BASE_URL", None) or self.BASE_URL
36
+ self.timeout = int(os.getenv("ASNDB_TIMEOUT", self.DEFAULT_TIMEOUT))
37
+ self.client = httpx.AsyncClient(timeout=self.timeout)
38
+
39
+ self.headers = {}
40
+ if self.bbot_io_api_key:
41
+ self.headers["Authorization"] = f"Bearer {self.bbot_io_api_key}"
42
+
43
+ self._cache_size = os.getenv("ASNDB_CACHE_SIZE", self.DEFAULT_CACHE_SIZE)
44
+ # IPNetwork -> ASN Number
45
+ self._subnet_cache = RadixTarget()
46
+ # ASN Number -> ASN
47
+ self._asn_cache = cachetools.LRUCache(maxsize=self._cache_size)
48
+
49
+ self._event_loop = asyncio.get_event_loop()
50
+
51
+ async def request(self, url, **kwargs):
52
+ """
53
+ Make a request to the ASNDB API, respecting its retry-after mechanism.
54
+ """
55
+ start = time.time()
56
+ retry_after = 10
57
+ try:
58
+ while True:
59
+ elapsed = time.time() - start
60
+ if elapsed > self.timeout:
61
+ raise ASNDBTimeoutError(f"Timeout after {self.timeout} seconds")
62
+
63
+ try:
64
+ response = await self.client.get(url, headers=self.headers, **kwargs)
65
+ except httpx.TimeoutException:
66
+ continue
67
+ except httpx.HTTPError as e:
68
+ raise ASNDBError(f"HTTP error: {e}")
69
+
70
+ status_code = getattr(response, "status_code", 0)
71
+
72
+ if status_code == 200:
73
+ try:
74
+ return response.json()
75
+ except Exception as e:
76
+ raise ASNDBError(f"Error parsing JSON: {e}")
77
+ elif status_code == 404:
78
+ return None
79
+ elif status_code == 429:
80
+ retry_after = int(response.headers.get("Retry-After", 10))
81
+ await asyncio.sleep(retry_after)
82
+ continue
83
+ else:
84
+ raise ASNDBError(f"Unexpected status code: {response.status_code}: {response.text}")
85
+
86
+ except Exception as e:
87
+ raise ASNDBError(f"Unexpected error while requesting {url}: {e}")
88
+
89
+ async def lookup_ip(self, ip: str):
90
+ """
91
+ Given an IP address, return the ASN data and subnet.
92
+
93
+ For convenience, will put the parent subnet of the requested IP in the "subnet" key of the returned ASN data.
94
+ """
95
+ try:
96
+ asn_number, subnet = self._cache_get_ip(ip)
97
+ asn = dict(self._cache_get_asn(asn_number))
98
+ asn["subnet"] = subnet
99
+ return asn
100
+ except KeyError:
101
+ url = f"{self.base_url}/ip/{ip}"
102
+ asn = await self.request(url)
103
+ if asn:
104
+ self._cache_put_asn(asn)
105
+ asn = dict(asn)
106
+ with suppress(KeyError):
107
+ asn_number, subnet = self._cache_get_ip(ip)
108
+ asn["subnet"] = subnet
109
+ return asn
110
+ return self.UNKNOWN_ASN
111
+
112
+ def lookup_ip_sync(self, ip: str):
113
+ return self._event_loop.run_until_complete(self.lookup_ip(ip))
114
+
115
+ async def ip_to_subnet(self, ip: str):
116
+ """
117
+ Given an IP address, return the AS number and subnet it belongs to.
118
+ """
119
+ # first make sure we have the ASN data for the IP
120
+ await self.lookup_ip(ip)
121
+ # then get it from the cache
122
+ result = self._cache_get_ip(ip)
123
+ if result is None:
124
+ raise KeyError(f"IP address {ip} not found in cache")
125
+ asn_number, subnet = result
126
+ return asn_number, subnet
127
+
128
+ def ip_to_subnet_sync(self, ip: str):
129
+ return self._event_loop.run_until_complete(self.ip_to_subnet(ip))
130
+
131
+ async def lookup_asn(self, asn: str):
132
+ try:
133
+ return self._cache_get_asn(asn)
134
+ except KeyError:
135
+ url = f"{self.base_url}/asn/{asn}"
136
+ asn = await self.request(url)
137
+ if asn:
138
+ self._cache_put_asn(asn)
139
+ return asn
140
+ return self.UNKNOWN_ASN
141
+
142
+ def lookup_asn_sync(self, asn: str):
143
+ return self._event_loop.run_until_complete(self.lookup_asn(asn))
144
+
145
+ async def lookup_org(self, org: str):
146
+ url = f"{self.base_url}/org/{org}"
147
+ return (await self.request(url)) or []
148
+
149
+ def lookup_org_sync(self, org: str):
150
+ return self._event_loop.run_until_complete(self.lookup_org(org))
151
+
152
+ def _cache_get_ip(self, ip: str) -> tuple[int, str]:
153
+ """
154
+ ip -> asn, subnet
155
+ """
156
+ result = self._subnet_cache.get(ip)
157
+ if result is None:
158
+ raise KeyError(f"IP address {ip} not found in cache")
159
+ asn, subnet = result
160
+ return asn, subnet
161
+
162
+ def _cache_get_asn(self, asn: int) -> dict:
163
+ """
164
+ asn number -> asn data
165
+ """
166
+ return self._asn_cache[asn]
167
+
168
+ def _cache_put_asn(self, asn: str):
169
+ asn_number = int(asn["asn"])
170
+ for subnet in asn.get("subnets", []):
171
+ self._subnet_cache.add(subnet, data=(asn_number, subnet))
172
+ self._asn_cache[asn_number] = asn
173
+
174
+ async def cleanup(self):
175
+ self._asn_cache.clear()
176
+ await self.client.aclose()
177
+
178
+
179
+ asndb_client = None
180
+
181
+
182
+ def ASNDB():
183
+ global asndb_client
184
+ if asndb_client is None:
185
+ asndb_client = ASNDBClient()
186
+ return asndb_client
@@ -0,0 +1,57 @@
1
+ import re
2
+ import json
3
+ import typer
4
+ from contextlib import contextmanager
5
+
6
+ from asndb.asndb import ASNDB
7
+
8
+
9
+ app = typer.Typer()
10
+
11
+ asn_regex = re.compile(r"^(?:AS)?(\d+)$", re.IGNORECASE)
12
+
13
+
14
+ @contextmanager
15
+ def asndb_cli_context():
16
+ client = ASNDB()
17
+ try:
18
+ yield client
19
+ except Exception as e:
20
+ typer.echo(f"Error: {e}", err=True)
21
+ raise typer.Exit(1)
22
+
23
+
24
+ @app.command(help="Lookup ASN by IP address")
25
+ def ip(
26
+ ip_or_asn: str = typer.Argument(..., help="IP address to lookup"),
27
+ ):
28
+ with asndb_cli_context() as client:
29
+ asn = client.lookup_ip_sync(ip_or_asn)
30
+ print(json.dumps(asn, indent=2))
31
+
32
+
33
+ @app.command(help="Lookup ASN by AS number")
34
+ def asn(
35
+ asn: str = typer.Argument(..., help="AS number to lookup"),
36
+ ):
37
+ as_number = int(asn_regex.match(asn).group(1))
38
+ with asndb_cli_context() as client:
39
+ asn = client.lookup_asn_sync(as_number)
40
+ print(json.dumps(asn, indent=2))
41
+
42
+
43
+ @app.command(help="Get all the ASNs for an organization, by its registered organization ID, e.g. GOGL-ARIN")
44
+ def org(
45
+ org: str = typer.Argument(..., help="Organization to lookup, e.g. GOGL-ARIN or CLOUD14-ARIN"),
46
+ ):
47
+ with asndb_cli_context() as client:
48
+ org = client.lookup_org_sync(org)
49
+ print(json.dumps(org, indent=2))
50
+
51
+
52
+ def main():
53
+ app()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: asndb
3
+ Version: 1.0.0
4
+ Summary: A simple Python CLI + library for looking up ASNs by IP or AS number. Uses BBOT.IO API.
5
+ Author: TheTechromancer
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: cachetools>=6.2.1
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: radixtarget>=4.0.1
11
+ Requires-Dist: typer>=0.19.2
12
+
13
+ # ASNDB
14
+
15
+ [![Python Version](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org) ![PyPI - Version](https://img.shields.io/pypi/v/asndb) [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/radixtarget/blob/master/LICENSE) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/blacklanternsecurity/asndb/actions/workflows/tests.yml/badge.svg)](https://github.com/blacklanternsecurity/asndb/actions/workflows/tests.yml)
16
+
17
+ A simple Python CLI + library for instant lookup of ASN data by IP address, AS number, or organization. Uses BBOT.IO API (`asndb.api.bbot.io`).
18
+
19
+ ASNs are automatically cached for instant lookups and minimal network traffic.
20
+
21
+ ## Installation
22
+
23
+ ```
24
+ pip install asndb
25
+
26
+ # or using uv
27
+ uv add asndb
28
+ ```
29
+
30
+ ## Usage (CLI)
31
+
32
+ Note: To avoid rate limits, export your BBOT.IO API key as an environment variable:
33
+ ```bash
34
+ export BBOT_IO_API_KEY=<your_api_key>
35
+ ```
36
+
37
+ ### IP Lookup
38
+ ```bash
39
+ asndb ip 1.1.1.1
40
+ ```
41
+
42
+ Output:
43
+ ```json
44
+ {
45
+ "asn": 13335,
46
+ "asn_name": "CLOUDFLARENET",
47
+ "country": "US",
48
+ "ip": "1.1.1.1",
49
+ "org": "Cloudflare, Inc.",
50
+ "org_id": "CLOUD14-ARIN",
51
+ "rir": "ARIN",
52
+ "subnets": [
53
+ "1.0.0.0/24",
54
+ "1.0.1.0/24"
55
+ ]
56
+ }
57
+ ```
58
+
59
+ ### AS Number Lookup
60
+
61
+ ```bash
62
+ asndb asn 13335
63
+ ```
64
+
65
+ Output:
66
+ ```json
67
+ {
68
+ "asn": 13335,
69
+ "asn_name": "CLOUDFLARENET",
70
+ "country": "US",
71
+ "ip": "1.1.1.1",
72
+ "org": "Cloudflare, Inc.",
73
+ "org_id": "CLOUD14-ARIN",
74
+ "rir": "ARIN",
75
+ "subnets": [
76
+ "1.0.0.0/24",
77
+ "1.0.1.0/24"
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ### Organization Lookup
83
+
84
+ ```bash
85
+ # Look up an organization
86
+ asndb org CLOUD14-ARIN
87
+ ```
88
+
89
+ Output:
90
+ ```json
91
+ {
92
+ "asns": [
93
+ 13335,
94
+ 14789,
95
+ 395747,
96
+ 394536
97
+ ]
98
+ }
99
+ ```
100
+
101
+ ### CLI Help
102
+
103
+ ```
104
+ $ uv run asndb --help
105
+
106
+ Usage: asndb [OPTIONS] COMMAND [ARGS]...
107
+
108
+ ╭─ Options ──────────────────────────────────────────────────────────────────────────────────╮
109
+ │ --install-completion Install completion for the current shell. │
110
+ │ --show-completion Show completion for the current shell, to copy it or │
111
+ │ customize the installation. │
112
+ │ --help Show this message and exit. │
113
+ ╰────────────────────────────────────────────────────────────────────────────────────────────╯
114
+ ╭─ Commands ─────────────────────────────────────────────────────────────────────────────────╮
115
+ │ ip Lookup ASN by IP address │
116
+ │ asn Lookup ASN by AS number │
117
+ │ org Get all the ASNs for an organization, by its registered organization ID, e.g. │
118
+ │ GOGL-ARIN │
119
+ ╰────────────────────────────────────────────────────────────────────────────────────────────╯
120
+ ```
121
+
122
+ ## Usage (Python)
123
+
124
+ ```python
125
+ from asndb import ASNDB
126
+
127
+ # Create a new ASNDB client
128
+ asndb = ASNDB()
129
+
130
+
131
+ # Look up an IP address
132
+ asn = asndb.lookup_ip_sync("1.1.1.1")
133
+ # {
134
+ # "asn": 13335,
135
+ # "asn_name": "CLOUDFLARENET",
136
+ # "country": "US",
137
+ # "ip": "1.1.1.1",
138
+ # "org": "Cloudflare, Inc.",
139
+ # "org_id": "CLOUD14-ARIN",
140
+ # "rir": "ARIN",
141
+ # "subnets": [
142
+ # "1.0.0.0/24",
143
+ # "1.0.1.0/24"
144
+ # ]
145
+ # }
146
+
147
+ # Look up an AS number
148
+ asn = asndb.lookup_asn_sync(13335)
149
+ # {
150
+ # "asn": 13335,
151
+ # "asn_name": "CLOUDFLARENET",
152
+ # "country": "US",
153
+ # "ip": "1.1.1.1",
154
+ # "org": "Cloudflare, Inc.",
155
+ # "org_id": "CLOUD14-ARIN",
156
+ # "rir": "ARIN",
157
+ # "subnets": [
158
+ # "1.0.0.0/24",
159
+ # "1.0.1.0/24"
160
+ # ]
161
+ # }
162
+
163
+ # Look up an organization
164
+ org = asndb.lookup_org_sync("CLOUD14-ARIN")
165
+ # {
166
+ # "asns": [
167
+ # 13335,
168
+ # 14789,
169
+ # 395747,
170
+ # 394536
171
+ # ]
172
+ # }
173
+ ```
174
+
175
+ ## Environment Variables
176
+
177
+ You can customize the behavior of the ASNDB client by exporting the following environment variables:
178
+
179
+ - `BBOT_IO_API_KEY`: Your BBOT.IO API key.
180
+ - `ASNDB_BASE_URL`: The base URL of the ASNDB API (default: https://asndb.api.bbot.io/v1).
181
+ - `ASNDB_TIMEOUT`: The timeout for the ASNDB API requests (default: 60 seconds).
182
+ - `ASNDB_CACHE_SIZE`: The size of the cache for the ASNDB API requests (default: 10000).
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ asndb/__init__.py
4
+ asndb/asndb.py
5
+ asndb/main.py
6
+ asndb.egg-info/PKG-INFO
7
+ asndb.egg-info/SOURCES.txt
8
+ asndb.egg-info/dependency_links.txt
9
+ asndb.egg-info/entry_points.txt
10
+ asndb.egg-info/requires.txt
11
+ asndb.egg-info/top_level.txt
12
+ tests/test_asndb.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ asndb = asndb.main:main
@@ -0,0 +1,4 @@
1
+ cachetools>=6.2.1
2
+ httpx>=0.28.1
3
+ radixtarget>=4.0.1
4
+ typer>=0.19.2
@@ -0,0 +1 @@
1
+ asndb
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "asndb"
3
+ version = "1.0.0"
4
+ description = "A simple Python CLI + library for looking up ASNs by IP or AS number. Uses BBOT.IO API."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ authors = [
8
+ {name = "TheTechromancer"}
9
+ ]
10
+ dependencies = [
11
+ "cachetools>=6.2.1",
12
+ "httpx>=0.28.1",
13
+ "radixtarget>=4.0.1",
14
+ "typer>=0.19.2",
15
+ ]
16
+
17
+ [tool.uv]
18
+ package = true
19
+
20
+ [tool.ruff]
21
+ line-length = 119
22
+ lint.ignore = ["E402", "E713", "E721", "E741", "F401", "F403", "F405"]
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "maturin>=1.9.6",
27
+ "pytest>=8.4.2",
28
+ "pytest-asyncio>=1.2.0",
29
+ "pytest-cov>=7.0.0",
30
+ "ruff>=0.14.1",
31
+ ]
32
+
33
+ [project.scripts]
34
+ asndb = "asndb.main:main"
asndb-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,133 @@
1
+ import json
2
+ import pytest
3
+ from asndb.asndb import ASNDBClient
4
+
5
+
6
+ class ASNDBTestClient(ASNDBClient):
7
+ def __init__(self, *args, **kwargs):
8
+ self.requested_urls = []
9
+ super().__init__(*args, **kwargs)
10
+
11
+ async def request(self, url, **kwargs):
12
+ self.requested_urls.append(url)
13
+ return await super().request(url, **kwargs)
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_asndb():
18
+ asndb = ASNDBTestClient()
19
+
20
+ # IP lookup
21
+ asn = await asndb.lookup_ip("1.1.1.1")
22
+ assert asn["asn"] == 13335
23
+ assert asn["asn_name"] == "CLOUDFLARENET"
24
+ assert asn["subnet"] == "1.1.1.0/24"
25
+ assert asndb.requested_urls == ["https://asndb.api.bbot.io/v1/ip/1.1.1.1"]
26
+
27
+ # IP to subnet lookup
28
+ asn_number, subnet = await asndb.ip_to_subnet("1.1.1.1")
29
+ assert asn_number == 13335
30
+ assert subnet == "1.1.1.0/24"
31
+
32
+ # Bad IP
33
+ asn = await asndb.lookup_ip("127.0.0.1")
34
+ assert asn == ASNDBClient.UNKNOWN_ASN
35
+ assert asndb.requested_urls == [
36
+ "https://asndb.api.bbot.io/v1/ip/1.1.1.1",
37
+ "https://asndb.api.bbot.io/v1/ip/127.0.0.1",
38
+ ]
39
+
40
+ # IP lookup with cache
41
+ asn = await asndb.lookup_ip("1.1.1.2")
42
+ assert asn["asn"] == 13335
43
+ assert asn["asn_name"] == "CLOUDFLARENET"
44
+ assert asn["subnet"] == "1.1.1.0/24"
45
+ assert len(asndb.requested_urls) == 2
46
+
47
+ # ASN Lookup
48
+ asn = await asndb.lookup_asn(13335)
49
+ assert asn["asn"] == 13335
50
+ assert asn["asn_name"] == "CLOUDFLARENET"
51
+ # we should not have made a second request
52
+ assert len(asndb.requested_urls) == 2
53
+
54
+ # Organization Lookup
55
+ org = await asndb.lookup_org("CLOUD14-ARIN")
56
+ assert org["asns"] == [13335, 14789, 395747, 394536]
57
+
58
+
59
+ def test_asndb_sync():
60
+ asndb = ASNDBTestClient()
61
+
62
+ # IP lookup
63
+ asn = asndb.lookup_ip_sync("1.1.1.1")
64
+ assert asn["asn"] == 13335
65
+ assert asn["asn_name"] == "CLOUDFLARENET"
66
+ assert asn["subnet"] == "1.1.1.0/24"
67
+ assert len(asndb.requested_urls) == 1
68
+
69
+ # IP to subnet lookup
70
+ asn_number, subnet = asndb.ip_to_subnet_sync("1.1.1.1")
71
+ assert asn_number == 13335
72
+ assert subnet == "1.1.1.0/24"
73
+
74
+ # Bad IP
75
+ asn = asndb.lookup_ip_sync("127.0.0.1")
76
+ assert asn == ASNDBClient.UNKNOWN_ASN
77
+ assert len(asndb.requested_urls) == 2
78
+
79
+ # IP lookup with cache
80
+ asn = asndb.lookup_ip_sync("1.1.1.2")
81
+ assert asn["asn"] == 13335
82
+ assert asn["subnet"] == "1.1.1.0/24"
83
+ assert len(asndb.requested_urls) == 2
84
+
85
+ # ASN Lookup
86
+ asn = asndb.lookup_asn_sync(13335)
87
+ assert asn["asn"] == 13335
88
+ assert asn["asn_name"] == "CLOUDFLARENET"
89
+ assert len(asndb.requested_urls) == 2
90
+
91
+ # Organization Lookup
92
+ org = asndb.lookup_org_sync("CLOUD14-ARIN")
93
+ assert org["asns"] == [13335, 14789, 395747, 394536]
94
+
95
+
96
+ def test_asndb_cli(monkeypatch, capsys):
97
+ from asndb.main import main
98
+ import sys
99
+
100
+ # patch sys.exit to prevent the CLI from actually exiting
101
+ def mock_exit(code=0):
102
+ pass
103
+
104
+ monkeypatch.setattr(sys, "exit", mock_exit)
105
+
106
+ # patch sys.argv to include the command
107
+ monkeypatch.setattr("sys.argv", ["asndb", "ip", "1.1.1.1"])
108
+ # run the CLI
109
+ main()
110
+ # capture the output
111
+ out, err = capsys.readouterr()
112
+ out_json = json.loads(out.strip())
113
+ assert err == ""
114
+ assert out_json["asn"] == 13335
115
+ assert out_json["asn_name"] == "CLOUDFLARENET"
116
+ assert out_json["subnet"] == "1.1.1.0/24"
117
+
118
+ # look up asn
119
+ monkeypatch.setattr("sys.argv", ["asndb", "asn", "13335"])
120
+ main()
121
+ out, err = capsys.readouterr()
122
+ out_json = json.loads(out.strip())
123
+ assert err == ""
124
+ assert out_json["asn"] == 13335
125
+ assert out_json["asn_name"] == "CLOUDFLARENET"
126
+
127
+ # look up org
128
+ monkeypatch.setattr("sys.argv", ["asndb", "org", "CLOUD14-ARIN"])
129
+ main()
130
+ out, err = capsys.readouterr()
131
+ out_json = json.loads(out.strip())
132
+ assert err == ""
133
+ assert out_json["asns"] == [13335, 14789, 395747, 394536]