honeydb 2.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.
honeydb/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """HoneyDB — Python API wrapper and CLI for the HoneyDB API.
2
+
3
+ See https://honeydb.io for more information.
4
+ """
5
+
6
+ from honeydb.api.client import DATACENTER_PROVIDERS, IPINFO_SOURCES, Client
7
+ from honeydb.exceptions import (
8
+ HoneyDBAuthError,
9
+ HoneyDBError,
10
+ HoneyDBNotFoundError,
11
+ HoneyDBRateLimitError,
12
+ )
13
+
14
+ __version__ = "2.0.0"
15
+
16
+ __all__ = [
17
+ "Client",
18
+ "DATACENTER_PROVIDERS",
19
+ "IPINFO_SOURCES",
20
+ "HoneyDBError",
21
+ "HoneyDBAuthError",
22
+ "HoneyDBNotFoundError",
23
+ "HoneyDBRateLimitError",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,5 @@
1
+ """HoneyDB API module."""
2
+
3
+ from honeydb.api.client import DATACENTER_PROVIDERS, IPINFO_SOURCES, Client
4
+
5
+ __all__ = ["Client", "DATACENTER_PROVIDERS", "IPINFO_SOURCES"]
honeydb/api/client.py ADDED
@@ -0,0 +1,518 @@
1
+ """HoneyDB API client.
2
+
3
+ A thin, typed wrapper around the HoneyDB REST API (https://honeydb.io).
4
+ Requests are made over a pooled :class:`requests.Session` with automatic
5
+ retries on transient failures.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+ from urllib.parse import quote
12
+
13
+ import requests
14
+ from requests.adapters import HTTPAdapter
15
+ from urllib3.util.retry import Retry
16
+
17
+ from honeydb.exceptions import (
18
+ HoneyDBAuthError,
19
+ HoneyDBError,
20
+ HoneyDBNotFoundError,
21
+ HoneyDBRateLimitError,
22
+ )
23
+
24
+ __all__ = ["Client", "DATACENTER_PROVIDERS", "IPINFO_SOURCES"]
25
+
26
+ #: IP list sources supported by the ``/ipinfo/<source>`` endpoints.
27
+ IPINFO_SOURCES: tuple[str, ...] = (
28
+ "bogon",
29
+ "tor",
30
+ "sansip",
31
+ "ciarmy",
32
+ "et-compromised",
33
+ "project-honeypot",
34
+ "pallebone",
35
+ "threatfox",
36
+ "blocklist_net_ua",
37
+ )
38
+
39
+ #: Datacenter/cloud providers supported by the ``/datacenter/<provider>`` endpoint.
40
+ DATACENTER_PROVIDERS: tuple[str, ...] = (
41
+ "aws",
42
+ "azure",
43
+ "azure/china",
44
+ "azure/germany",
45
+ "azure/gov",
46
+ "cloudflare",
47
+ "gcp",
48
+ "ibm",
49
+ "oracle",
50
+ )
51
+
52
+ JSON = Any
53
+
54
+
55
+ class Client:
56
+ """Client for the HoneyDB API.
57
+
58
+ Args:
59
+ api_id: Your HoneyDB API ID.
60
+ api_key: Your HoneyDB API key.
61
+ timeout: Per-request timeout in seconds.
62
+ base_url: Base URL of the API (override for testing/proxies).
63
+ session: An existing :class:`requests.Session` to reuse. If omitted, a
64
+ pooled session with retries is created and owned by this client.
65
+ retries: Number of automatic retries for transient errors (429/5xx).
66
+
67
+ Example:
68
+ >>> from honeydb import Client
69
+ >>> with Client("api_id", "api_key") as honeydb:
70
+ ... hosts = honeydb.bad_hosts()
71
+ """
72
+
73
+ DEFAULT_BASE_URL = "https://honeydb.io/api"
74
+
75
+ def __init__(
76
+ self,
77
+ api_id: str,
78
+ api_key: str,
79
+ *,
80
+ timeout: float = 30.0,
81
+ base_url: str = DEFAULT_BASE_URL,
82
+ session: requests.Session | None = None,
83
+ retries: int = 3,
84
+ ) -> None:
85
+ self.api_id = api_id
86
+ self.api_key = api_key
87
+ self.timeout = timeout
88
+ self.base_url = base_url.rstrip("/")
89
+
90
+ self._owns_session = session is None
91
+ self.session = session or self._build_session(retries)
92
+ self.session.headers.update(
93
+ {
94
+ "X-HoneyDb-ApiId": self.api_id,
95
+ "X-HoneyDb-ApiKey": self.api_key,
96
+ "Accept": "application/json",
97
+ }
98
+ )
99
+
100
+ # -- infrastructure ---------------------------------------------------
101
+
102
+ @staticmethod
103
+ def _build_session(retries: int) -> requests.Session:
104
+ session = requests.Session()
105
+ retry = Retry(
106
+ total=retries,
107
+ connect=retries,
108
+ read=retries,
109
+ status=retries,
110
+ backoff_factor=0.5,
111
+ status_forcelist=(429, 500, 502, 503, 504),
112
+ allowed_methods=frozenset({"GET", "PUT", "DELETE"}),
113
+ respect_retry_after_header=True,
114
+ raise_on_status=False,
115
+ )
116
+ adapter = HTTPAdapter(max_retries=retry)
117
+ session.mount("https://", adapter)
118
+ session.mount("http://", adapter)
119
+ return session
120
+
121
+ def _request(
122
+ self,
123
+ method: str,
124
+ path: str,
125
+ *,
126
+ params: dict[str, Any] | None = None,
127
+ json: Any | None = None,
128
+ ) -> JSON:
129
+ """Send a request and return the parsed JSON body.
130
+
131
+ Raises:
132
+ HoneyDBAuthError: On HTTP 401/403.
133
+ HoneyDBNotFoundError: On HTTP 404.
134
+ HoneyDBRateLimitError: On HTTP 429.
135
+ HoneyDBError: On any other HTTP or transport error, or an
136
+ unparseable response body.
137
+ """
138
+ url = f"{self.base_url}{path}"
139
+ try:
140
+ response = self.session.request(
141
+ method,
142
+ url,
143
+ params=params,
144
+ json=json,
145
+ timeout=self.timeout,
146
+ )
147
+ except requests.RequestException as error:
148
+ raise HoneyDBError(f"Request to {url} failed: {error}") from error
149
+
150
+ self._raise_for_status(response)
151
+
152
+ # Some endpoints (e.g. datacenter feeds with no entitlement/data) return
153
+ # an empty or whitespace-only body with a success status; treat as None.
154
+ if not response.content or not response.text.strip():
155
+ return None
156
+ try:
157
+ return response.json()
158
+ except ValueError as error:
159
+ raise HoneyDBError(
160
+ f"Response from {url} was not valid JSON",
161
+ status_code=response.status_code,
162
+ response=response.text,
163
+ ) from error
164
+
165
+ @staticmethod
166
+ def _raise_for_status(response: requests.Response) -> None:
167
+ if response.ok:
168
+ return
169
+
170
+ status = response.status_code
171
+ body = response.text
172
+ message = f"HoneyDB API returned HTTP {status}: {body[:200]}"
173
+
174
+ if status in (401, 403):
175
+ raise HoneyDBAuthError(message, status_code=status, response=body)
176
+ if status == 404:
177
+ raise HoneyDBNotFoundError(message, status_code=status, response=body)
178
+ if status == 429:
179
+ retry_after = response.headers.get("Retry-After")
180
+ raise HoneyDBRateLimitError(
181
+ message,
182
+ status_code=status,
183
+ response=body,
184
+ retry_after=float(retry_after) if retry_after else None,
185
+ )
186
+ raise HoneyDBError(message, status_code=status, response=body)
187
+
188
+ @staticmethod
189
+ def _seg(value: Any) -> str:
190
+ """URL-encode a single path segment."""
191
+ return quote(str(value), safe="")
192
+
193
+ # -- lifecycle --------------------------------------------------------
194
+
195
+ def close(self) -> None:
196
+ """Close the underlying session if this client owns it."""
197
+ if self._owns_session:
198
+ self.session.close()
199
+
200
+ def __enter__(self) -> Client:
201
+ return self
202
+
203
+ def __exit__(self, *_exc: object) -> None:
204
+ self.close()
205
+
206
+ # -- bad hosts --------------------------------------------------------
207
+
208
+ def bad_hosts(self, mydata: bool = False) -> JSON:
209
+ """Return bad hosts seen in the last 24 hours.
210
+
211
+ Args:
212
+ mydata: If ``True``, return only data from sensors you operate.
213
+ """
214
+ path = "/bad-hosts/mydata" if mydata else "/bad-hosts"
215
+ return self._request("GET", path)
216
+
217
+ def bad_hosts_by_service(self, service: str, mydata: bool = False) -> JSON:
218
+ """Return bad hosts for a given service (last 24 hours).
219
+
220
+ Args:
221
+ service: Service/protocol name to filter on.
222
+ mydata: If ``True``, return only data from sensors you operate.
223
+ """
224
+ path = f"/bad-hosts/{self._seg(service)}"
225
+ if mydata:
226
+ path += "/mydata"
227
+ return self._request("GET", path)
228
+
229
+ # -- ip context -------------------------------------------------------
230
+
231
+ def ip(self, ip_address: str) -> JSON:
232
+ """Return full context for an IP (netinfo, threat, history, scanner...)."""
233
+ return self._request("GET", f"/ip/{self._seg(ip_address)}")
234
+
235
+ def ip_geo(self, ip_address: str) -> JSON:
236
+ """Return geolocation information for an IP."""
237
+ return self._request("GET", f"/ip/{self._seg(ip_address)}/geo")
238
+
239
+ def ip_netinfo(self, ip_address: str) -> JSON:
240
+ """Return AS number, organization and location data for an IP."""
241
+ return self._request("GET", f"/ip/{self._seg(ip_address)}/netinfo")
242
+
243
+ def ip_threatinfo(self, ip_address: str) -> JSON:
244
+ """Return threat intel information for an IP."""
245
+ return self._request("GET", f"/ip/{self._seg(ip_address)}/threatinfo")
246
+
247
+ def ip_internet_scanner(self, ip_address: str) -> JSON:
248
+ """Return whether an IP is a known internet scanner."""
249
+ return self._request("GET", f"/ip/{self._seg(ip_address)}/internet-scanner")
250
+
251
+ def ip_history(self, ip_address: str) -> JSON:
252
+ """Return history of an IP's interactions with the HoneyDB network."""
253
+ return self._request("GET", f"/ip/{self._seg(ip_address)}/history")
254
+
255
+ def ip_cve(self, ip_address: str) -> JSON:
256
+ """Return CVEs observed from an IP."""
257
+ return self._request("GET", f"/ip/{self._seg(ip_address)}/cve")
258
+
259
+ def ip_cidr(self, cidr: str) -> JSON:
260
+ """Return all IP addresses within a network range (CIDR)."""
261
+ return self._request("GET", f"/ip/cidr/{self._seg(cidr)}")
262
+
263
+ # -- asn --------------------------------------------------------------
264
+
265
+ def asn(self, as_number: int | str) -> JSON:
266
+ """Return AS number and organization name. Does not count against limits."""
267
+ return self._request("GET", f"/asn/{self._seg(as_number)}")
268
+
269
+ def asn_prefixes(self, as_number: int | str) -> JSON:
270
+ """Return IP prefixes for an ASN. Does not count against limits."""
271
+ return self._request("GET", f"/asn/{self._seg(as_number)}/prefixes")
272
+
273
+ def asns(self) -> JSON:
274
+ """Return ASNs that interacted with the network in the previous day."""
275
+ return self._request("GET", "/asns")
276
+
277
+ def asns_7d(self) -> JSON:
278
+ """Return ASNs that interacted with the network in the last 7 days."""
279
+ return self._request("GET", "/asns-7d")
280
+
281
+ # -- cve --------------------------------------------------------------
282
+
283
+ def cve(self, cve: str) -> JSON:
284
+ """Return IP history for a given CVE."""
285
+ return self._request("GET", f"/cve/{self._seg(cve)}")
286
+
287
+ def cve_ip(self, ip_address: str) -> JSON:
288
+ """Return CVE history for a given IP address."""
289
+ return self._request("GET", f"/cve/ip/{self._seg(ip_address)}")
290
+
291
+ # -- sensor data ------------------------------------------------------
292
+
293
+ def sensor_data(
294
+ self,
295
+ sensor_data_date: str,
296
+ from_id: int | str | None = None,
297
+ mydata: bool = True,
298
+ ) -> JSON:
299
+ """Return sensor event data for a given date.
300
+
301
+ Args:
302
+ sensor_data_date: Date in ``YYYY-MM-DD`` format.
303
+ from_id: Continue retrieving records after this event id (paging).
304
+ mydata: If ``True`` (default), return only data from your sensors.
305
+ """
306
+ path = "/sensor-data/mydata" if mydata else "/sensor-data"
307
+ params: dict[str, Any] = {"sensor-data-date": sensor_data_date}
308
+ if from_id is not None:
309
+ params["from-id"] = from_id
310
+ return self._request("GET", path, params=params)
311
+
312
+ def sensor_data_count(self, sensor_data_date: str, mydata: bool = True) -> JSON:
313
+ """Return a count of sensor event data for a given date.
314
+
315
+ Args:
316
+ sensor_data_date: Date in ``YYYY-MM-DD`` format.
317
+ mydata: If ``True`` (default), count only data from your sensors.
318
+ """
319
+ path = "/sensor-data/count/mydata" if mydata else "/sensor-data/count"
320
+ return self._request("GET", path, params={"sensor-data-date": sensor_data_date})
321
+
322
+ # -- services / stats -------------------------------------------------
323
+
324
+ def services(self) -> JSON:
325
+ """Return the network services (protocols) emulated by sensors."""
326
+ return self._request("GET", "/services")
327
+
328
+ def stats(self, year: int, month: int) -> JSON:
329
+ """Return summary stats for a given year and month."""
330
+ return self._request("GET", "/stats", params={"year": year, "month": month})
331
+
332
+ # -- monitors ---------------------------------------------------------
333
+
334
+ def monitors(self) -> JSON:
335
+ """Return the list of current monitors."""
336
+ return self._request("GET", "/monitors")
337
+
338
+ def create_monitors(self, monitors: list[dict[str, Any]]) -> JSON:
339
+ """Create one or more monitors.
340
+
341
+ Args:
342
+ monitors: A list of monitor definitions. Each is a dict such as
343
+ ``{"monitor_type": "ip_address", "ip_address": "1.2.3.4",
344
+ "description": "..."}``. Supported ``monitor_type`` values include
345
+ ``ip_address``, ``ip_range``, ``asn`` and ``string``.
346
+ """
347
+ return self._request("PUT", "/monitors", json=monitors)
348
+
349
+ def delete_monitors(self, ids: list[int]) -> JSON:
350
+ """Delete monitors by id.
351
+
352
+ Args:
353
+ ids: List of monitor ids to delete.
354
+ """
355
+ return self._request("DELETE", "/monitors", json={"ids": ids})
356
+
357
+ def monitors_logs(self) -> JSON:
358
+ """Return all monitor logs."""
359
+ return self._request("GET", "/monitors/logs")
360
+
361
+ def monitors_notifications(self) -> JSON:
362
+ """Return the list of configured monitor notifications."""
363
+ return self._request("GET", "/monitors/notifications")
364
+
365
+ # -- nodes ------------------------------------------------------------
366
+
367
+ def nodes(self, mydata: bool = False) -> JSON:
368
+ """Return honeydb-agent nodes seen in the last 3 days.
369
+
370
+ Args:
371
+ mydata: If ``True``, return only your nodes.
372
+ """
373
+ path = "/nodes/mydata" if mydata else "/nodes"
374
+ return self._request("GET", path)
375
+
376
+ # -- payload history --------------------------------------------------
377
+
378
+ def payload_history_remote_hosts(self) -> JSON:
379
+ """Return remote hosts from which payload data was extracted, by year."""
380
+ return self._request("GET", "/payload-history/remote-hosts")
381
+
382
+ def payload_history_attributes(self) -> JSON:
383
+ """Return the list of attributes extracted from payload data."""
384
+ return self._request("GET", "/payload-history/attributes")
385
+
386
+ def payload_history_attribute(self, attribute: str) -> JSON:
387
+ """Return historical payload data grouped by the given attribute."""
388
+ return self._request(
389
+ "GET", f"/payload-history/attributes/{self._seg(attribute)}"
390
+ )
391
+
392
+ def payload_history(self, year: int, month: int | None = None) -> JSON:
393
+ """Return payload data for a year (and optional month).
394
+
395
+ .. deprecated::
396
+ This endpoint is deprecated by the HoneyDB API and may be removed.
397
+ """
398
+ path = f"/payload-history/{self._seg(year)}"
399
+ if month is not None:
400
+ path += f"/{self._seg(month)}"
401
+ return self._request("GET", path)
402
+
403
+ def payload_history_services(self) -> JSON:
404
+ """Return services from which payload data was extracted.
405
+
406
+ .. deprecated:: Deprecated by the HoneyDB API.
407
+ """
408
+ return self._request("GET", "/payload-history/services")
409
+
410
+ def payload_history_service(self, service: str) -> JSON:
411
+ """Return payload data for a given service.
412
+
413
+ .. deprecated:: Deprecated by the HoneyDB API.
414
+ """
415
+ return self._request("GET", f"/payload-history/{self._seg(service)}")
416
+
417
+ def payload_history_hash(self, hash: str) -> JSON:
418
+ """Return payload data for a given hash.
419
+
420
+ .. deprecated:: Deprecated by the HoneyDB API.
421
+ """
422
+ return self._request("GET", f"/payload-history/{self._seg(hash)}")
423
+
424
+ def payload_history_hash_remote_hosts(self, hash: str, year: int) -> JSON:
425
+ """Return remote hosts for a given hash and year.
426
+
427
+ .. deprecated:: Deprecated by the HoneyDB API.
428
+ """
429
+ return self._request(
430
+ "GET",
431
+ f"/payload-history/{self._seg(hash)}/remote-hosts/{self._seg(year)}",
432
+ )
433
+
434
+ def payload_history_remote_host(self, remote_host: str) -> JSON:
435
+ """Return payload data hashes for a given remote host.
436
+
437
+ .. deprecated:: Deprecated by the HoneyDB API.
438
+ """
439
+ return self._request(
440
+ "GET", f"/payload-history/remote-hosts/{self._seg(remote_host)}"
441
+ )
442
+
443
+ # -- internet scanner -------------------------------------------------
444
+
445
+ def internet_scanner(self, ip_address: str) -> JSON:
446
+ """Return whether an IP is part of a known internet scanning service."""
447
+ return self._request("GET", f"/internet-scanner/{self._seg(ip_address)}")
448
+
449
+ def internet_scanner_info(self, ip_address: str) -> JSON:
450
+ """Return internet-scanner status plus details about the scanning entity."""
451
+ return self._request("GET", f"/internet-scanner/info/{self._seg(ip_address)}")
452
+
453
+ # -- ipinfo -----------------------------------------------------------
454
+
455
+ def ipinfo(self, ip_address: str) -> JSON:
456
+ """Return whether an IP is present on any of the known IP lists."""
457
+ return self._request("GET", f"/ipinfo/{self._seg(ip_address)}")
458
+
459
+ def ipinfo_source(self, source: str, ip_address: str) -> JSON:
460
+ """Return whether an IP is present on a specific IP list.
461
+
462
+ Args:
463
+ source: One of :data:`IPINFO_SOURCES` (e.g. ``"tor"``, ``"bogon"``).
464
+ ip_address: The IP address to look up.
465
+
466
+ Raises:
467
+ ValueError: If ``source`` is not a recognized IP list.
468
+ """
469
+ if source not in IPINFO_SOURCES:
470
+ raise ValueError(
471
+ f"Unknown ipinfo source {source!r}; "
472
+ f"expected one of {', '.join(IPINFO_SOURCES)}"
473
+ )
474
+ return self._request(
475
+ "GET", f"/ipinfo/{self._seg(source)}/{self._seg(ip_address)}"
476
+ )
477
+
478
+ # -- netinfo (no monthly limit) --------------------------------------
479
+
480
+ def netinfo_lookup(self, ip_address: str) -> JSON:
481
+ """Return AS, network info and geolocation for an IP. No monthly limit."""
482
+ return self._request("GET", f"/netinfo/lookup/{self._seg(ip_address)}")
483
+
484
+ def netinfo_network_addresses(self, cidr: str) -> JSON:
485
+ """Return all IP addresses within a network range. No monthly limit."""
486
+ return self._request("GET", f"/netinfo/network-addresses/{self._seg(cidr)}")
487
+
488
+ def netinfo_prefixes(self, asn: int | str) -> JSON:
489
+ """Return all prefixes advertised for an AS network. No monthly limit."""
490
+ return self._request("GET", f"/netinfo/prefixes/{self._seg(asn)}")
491
+
492
+ def netinfo_as_name(self, asn: int | str) -> JSON:
493
+ """Return the name of an AS network. No monthly limit."""
494
+ return self._request("GET", f"/netinfo/as-name/{self._seg(asn)}")
495
+
496
+ def netinfo_geolocation(self, ip_address: str) -> JSON:
497
+ """Return geolocation information for an IP. No monthly limit."""
498
+ return self._request("GET", f"/netinfo/geolocation/{self._seg(ip_address)}")
499
+
500
+ # -- datacenter -------------------------------------------------------
501
+
502
+ def datacenter(self, provider: str) -> JSON:
503
+ """Return datacenter/cloud IP ranges for a provider. No monthly limit.
504
+
505
+ Args:
506
+ provider: One of :data:`DATACENTER_PROVIDERS` (e.g. ``"aws"``,
507
+ ``"gcp"``, ``"azure/china"``).
508
+
509
+ Raises:
510
+ ValueError: If ``provider`` is not recognized.
511
+ """
512
+ if provider not in DATACENTER_PROVIDERS:
513
+ raise ValueError(
514
+ f"Unknown datacenter provider {provider!r}; "
515
+ f"expected one of {', '.join(DATACENTER_PROVIDERS)}"
516
+ )
517
+ # provider may contain a sub-path (e.g. "azure/china"); keep the slash.
518
+ return self._request("GET", f"/datacenter/{provider}")
honeydb/cli.py ADDED
@@ -0,0 +1,425 @@
1
+ """Command-line interface for the HoneyDB API.
2
+
3
+ Credentials are read from ``--api-id`` / ``--api-key`` or, if not given, from
4
+ the ``HONEYDB_API_ID`` / ``HONEYDB_API_KEY`` environment variables.
5
+
6
+ Run ``honeydb --help`` to see the available commands.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ from collections.abc import Sequence
16
+ from typing import Any
17
+
18
+ from honeydb import __version__
19
+ from honeydb.api.client import DATACENTER_PROVIDERS, IPINFO_SOURCES, Client
20
+ from honeydb.exceptions import HoneyDBError
21
+
22
+ PROG = "honeydb"
23
+
24
+
25
+ def emit(data: Any, pretty: bool) -> None:
26
+ """Print JSON data, optionally pretty-printed."""
27
+ if pretty:
28
+ print(json.dumps(data, indent=2, sort_keys=True))
29
+ else:
30
+ print(json.dumps(data))
31
+
32
+
33
+ # --------------------------------------------------------------------------
34
+ # Command handlers. Each takes (client, args) and returns the API response.
35
+ # --------------------------------------------------------------------------
36
+
37
+
38
+ def _cmd_bad_hosts(client: Client, args: argparse.Namespace) -> Any:
39
+ if args.service:
40
+ return client.bad_hosts_by_service(args.service, mydata=args.mydata)
41
+ return client.bad_hosts(mydata=args.mydata)
42
+
43
+
44
+ def _cmd_ip(client: Client, args: argparse.Namespace) -> Any:
45
+ dispatch = {
46
+ "geo": client.ip_geo,
47
+ "netinfo": client.ip_netinfo,
48
+ "threatinfo": client.ip_threatinfo,
49
+ "scanner": client.ip_internet_scanner,
50
+ "history": client.ip_history,
51
+ "cve": client.ip_cve,
52
+ }
53
+ if args.view:
54
+ return dispatch[args.view](args.ip_address)
55
+ return client.ip(args.ip_address)
56
+
57
+
58
+ def _cmd_ip_cidr(client: Client, args: argparse.Namespace) -> Any:
59
+ return client.ip_cidr(args.cidr)
60
+
61
+
62
+ def _cmd_asn(client: Client, args: argparse.Namespace) -> Any:
63
+ if args.prefixes:
64
+ return client.asn_prefixes(args.as_number)
65
+ return client.asn(args.as_number)
66
+
67
+
68
+ def _cmd_asns(client: Client, args: argparse.Namespace) -> Any:
69
+ return client.asns_7d() if args.days == 7 else client.asns()
70
+
71
+
72
+ def _cmd_cve(client: Client, args: argparse.Namespace) -> Any:
73
+ return client.cve(args.cve)
74
+
75
+
76
+ def _cmd_cve_ip(client: Client, args: argparse.Namespace) -> Any:
77
+ return client.cve_ip(args.ip_address)
78
+
79
+
80
+ def _cmd_sensor_data(client: Client, args: argparse.Namespace) -> Any:
81
+ if args.count:
82
+ return client.sensor_data_count(args.date, mydata=args.mydata)
83
+ return client.sensor_data(args.date, from_id=args.from_id, mydata=args.mydata)
84
+
85
+
86
+ def _cmd_services(client: Client, _args: argparse.Namespace) -> Any:
87
+ return client.services()
88
+
89
+
90
+ def _cmd_stats(client: Client, args: argparse.Namespace) -> Any:
91
+ return client.stats(year=args.year, month=args.month)
92
+
93
+
94
+ def _cmd_monitors(client: Client, args: argparse.Namespace) -> Any:
95
+ if args.action == "list":
96
+ return client.monitors()
97
+ if args.action == "logs":
98
+ return client.monitors_logs()
99
+ if args.action == "notifications":
100
+ return client.monitors_notifications()
101
+ if args.action == "create":
102
+ payload = _load_json_arg(args.file, args.json)
103
+ if not isinstance(payload, list):
104
+ payload = [payload]
105
+ return client.create_monitors(payload)
106
+ if args.action == "delete":
107
+ return client.delete_monitors(args.id)
108
+ raise ValueError(f"Unknown monitors action: {args.action}")
109
+
110
+
111
+ def _cmd_nodes(client: Client, args: argparse.Namespace) -> Any:
112
+ return client.nodes(mydata=args.mydata)
113
+
114
+
115
+ def _cmd_payload_history(client: Client, args: argparse.Namespace) -> Any:
116
+ action = args.action
117
+ if action == "remote-hosts":
118
+ return client.payload_history_remote_hosts()
119
+ if action == "attributes":
120
+ if args.attribute:
121
+ return client.payload_history_attribute(args.attribute)
122
+ return client.payload_history_attributes()
123
+ if action == "year":
124
+ return client.payload_history(args.year, args.month)
125
+ if action == "services":
126
+ return client.payload_history_services()
127
+ if action == "service":
128
+ return client.payload_history_service(args.name)
129
+ if action == "hash":
130
+ return client.payload_history_hash(args.value)
131
+ raise ValueError(f"Unknown payload-history action: {action}")
132
+
133
+
134
+ def _cmd_internet_scanner(client: Client, args: argparse.Namespace) -> Any:
135
+ if args.info:
136
+ return client.internet_scanner_info(args.ip_address)
137
+ return client.internet_scanner(args.ip_address)
138
+
139
+
140
+ def _cmd_ipinfo(client: Client, args: argparse.Namespace) -> Any:
141
+ if args.source:
142
+ return client.ipinfo_source(args.source, args.ip_address)
143
+ return client.ipinfo(args.ip_address)
144
+
145
+
146
+ def _cmd_netinfo(client: Client, args: argparse.Namespace) -> Any:
147
+ dispatch = {
148
+ "lookup": client.netinfo_lookup,
149
+ "network-addresses": client.netinfo_network_addresses,
150
+ "prefixes": client.netinfo_prefixes,
151
+ "as-name": client.netinfo_as_name,
152
+ "geolocation": client.netinfo_geolocation,
153
+ }
154
+ return dispatch[args.action](args.value)
155
+
156
+
157
+ def _cmd_datacenter(client: Client, args: argparse.Namespace) -> Any:
158
+ return client.datacenter(args.provider)
159
+
160
+
161
+ def _load_json_arg(file: str | None, raw: str | None) -> Any:
162
+ """Load JSON payload from a file, an inline string, or stdin ('-')."""
163
+ if file:
164
+ if file == "-":
165
+ return json.load(sys.stdin)
166
+ with open(file, encoding="utf-8") as handle:
167
+ return json.load(handle)
168
+ if raw:
169
+ return json.loads(raw)
170
+ raise SystemExit("error: provide --file or --json with the monitor definition")
171
+
172
+
173
+ # --------------------------------------------------------------------------
174
+ # Parser construction
175
+ # --------------------------------------------------------------------------
176
+
177
+
178
+ def build_parser() -> argparse.ArgumentParser:
179
+ # Global options live on a shared parent parser (with SUPPRESS defaults) so
180
+ # they may be given either before or after the subcommand, e.g. both
181
+ # ``honeydb --pretty services`` and ``honeydb services --pretty`` work.
182
+ common = argparse.ArgumentParser(add_help=False)
183
+ common.add_argument(
184
+ "--api-id",
185
+ default=argparse.SUPPRESS,
186
+ help="HoneyDB API ID (default: HONEYDB_API_ID env var).",
187
+ )
188
+ common.add_argument(
189
+ "--api-key",
190
+ default=argparse.SUPPRESS,
191
+ help="HoneyDB API key (default: HONEYDB_API_KEY env var).",
192
+ )
193
+ common.add_argument(
194
+ "-p",
195
+ "--pretty",
196
+ action="store_true",
197
+ default=argparse.SUPPRESS,
198
+ help="Pretty-print JSON output.",
199
+ )
200
+ common.add_argument(
201
+ "--timeout",
202
+ type=float,
203
+ default=argparse.SUPPRESS,
204
+ help="Per-request timeout in seconds (default: 30).",
205
+ )
206
+
207
+ parser = argparse.ArgumentParser(
208
+ prog=PROG,
209
+ parents=[common],
210
+ description="CLI for the HoneyDB API (https://honeydb.io).",
211
+ )
212
+ parser.add_argument(
213
+ "--version", action="version", version=f"%(prog)s {__version__}"
214
+ )
215
+ # Defaults are resolved in main() via getattr rather than set_defaults, so
216
+ # that a value given before the subcommand is not clobbered by the
217
+ # subparser (a known argparse interaction with parent parsers).
218
+
219
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
220
+
221
+ def add(name: str, **kwargs: Any) -> argparse.ArgumentParser:
222
+ return sub.add_parser(name, parents=[common], **kwargs)
223
+
224
+ # bad-hosts
225
+ p = add("bad-hosts", help="Get bad hosts (last 24h).")
226
+ p.add_argument("--service", help="Filter by service/protocol name.")
227
+ p.add_argument("--mydata", action="store_true", help="Only data from your sensors.")
228
+ p.set_defaults(func=_cmd_bad_hosts)
229
+
230
+ # ip
231
+ p = add("ip", help="Get context for an IP address.")
232
+ p.add_argument("ip_address", help="IP address to look up.")
233
+ view = p.add_mutually_exclusive_group()
234
+ for name, flag in (
235
+ ("geo", "--geo"),
236
+ ("netinfo", "--netinfo"),
237
+ ("threatinfo", "--threatinfo"),
238
+ ("scanner", "--scanner"),
239
+ ("history", "--history"),
240
+ ("cve", "--cve"),
241
+ ):
242
+ view.add_argument(
243
+ flag,
244
+ dest="view",
245
+ action="store_const",
246
+ const=name,
247
+ help=f"Return only the {name} view.",
248
+ )
249
+ p.set_defaults(func=_cmd_ip, view=None)
250
+
251
+ # ip-cidr
252
+ p = add("ip-cidr", help="Get all IPs within a CIDR range.")
253
+ p.add_argument("cidr", help="Network range in CIDR notation.")
254
+ p.set_defaults(func=_cmd_ip_cidr)
255
+
256
+ # asn
257
+ p = add("asn", help="Get ASN organization info.")
258
+ p.add_argument("as_number", help="Autonomous System number.")
259
+ p.add_argument(
260
+ "--prefixes", action="store_true", help="Return IP prefixes for the ASN."
261
+ )
262
+ p.set_defaults(func=_cmd_asn)
263
+
264
+ # asns
265
+ p = add("asns", help="List ASNs seen interacting with the network.")
266
+ p.add_argument(
267
+ "--days",
268
+ type=int,
269
+ choices=(1, 7),
270
+ default=1,
271
+ help="Window in days: 1 (previous day, default) or 7.",
272
+ )
273
+ p.set_defaults(func=_cmd_asns)
274
+
275
+ # cve
276
+ p = add("cve", help="Get IP history for a CVE.")
277
+ p.add_argument("cve", help="CVE identifier, e.g. CVE-2021-44228.")
278
+ p.set_defaults(func=_cmd_cve)
279
+
280
+ # cve-ip
281
+ p = add("cve-ip", help="Get CVE history for an IP.")
282
+ p.add_argument("ip_address", help="IP address to look up.")
283
+ p.set_defaults(func=_cmd_cve_ip)
284
+
285
+ # sensor-data
286
+ p = add("sensor-data", help="Get your sensor event data for a date.")
287
+ p.add_argument("--date", required=True, help="Date in YYYY-MM-DD format.")
288
+ p.add_argument("--from-id", dest="from_id", help="Continue paging from this id.")
289
+ p.add_argument(
290
+ "--count", action="store_true", help="Return a count instead of records."
291
+ )
292
+ p.add_argument(
293
+ "--all",
294
+ dest="mydata",
295
+ action="store_false",
296
+ help="Query all sensor data instead of only yours.",
297
+ )
298
+ p.set_defaults(func=_cmd_sensor_data, mydata=True)
299
+
300
+ # services
301
+ p = add("services", help="List emulated services (last 24h).")
302
+ p.set_defaults(func=_cmd_services)
303
+
304
+ # stats
305
+ p = add("stats", help="Get summary stats for a year/month.")
306
+ p.add_argument("--year", type=int, required=True, help="Year, e.g. 2024.")
307
+ p.add_argument("--month", type=int, required=True, help="Month (1-12).")
308
+ p.set_defaults(func=_cmd_stats)
309
+
310
+ # monitors
311
+ p = add("monitors", help="Manage monitors.")
312
+ msub = p.add_subparsers(dest="action", metavar="<action>", required=True)
313
+
314
+ def madd(name: str, **kwargs: Any) -> argparse.ArgumentParser:
315
+ return msub.add_parser(name, parents=[common], **kwargs)
316
+
317
+ madd("list", help="List current monitors.")
318
+ madd("logs", help="Show monitor logs.")
319
+ madd("notifications", help="Show monitor notifications.")
320
+ mc = madd("create", help="Create monitor(s) from JSON.")
321
+ mc.add_argument("--file", help="Path to a JSON file ('-' for stdin).")
322
+ mc.add_argument("--json", help="Inline JSON monitor definition.")
323
+ md = madd("delete", help="Delete monitor(s) by id.")
324
+ md.add_argument("--id", type=int, nargs="+", required=True, help="Monitor id(s).")
325
+ p.set_defaults(func=_cmd_monitors)
326
+
327
+ # nodes
328
+ p = add("nodes", help="List honeydb-agent nodes (last 3 days).")
329
+ p.add_argument("--mydata", action="store_true", help="Only your nodes.")
330
+ p.set_defaults(func=_cmd_nodes)
331
+
332
+ # payload-history
333
+ p = add("payload-history", help="Query payload history data.")
334
+ psub = p.add_subparsers(dest="action", metavar="<action>", required=True)
335
+
336
+ def padd(name: str, **kwargs: Any) -> argparse.ArgumentParser:
337
+ return psub.add_parser(name, parents=[common], **kwargs)
338
+
339
+ padd("remote-hosts", help="Remote hosts grouped by year.")
340
+ pa = padd("attributes", help="List attributes, or one attribute.")
341
+ pa.add_argument("--attribute", help="Group historical data by this attribute.")
342
+ py = padd("year", help="[deprecated] Payload data by year/month.")
343
+ py.add_argument("year", type=int, help="Year, e.g. 2024.")
344
+ py.add_argument("month", type=int, nargs="?", help="Optional month (1-12).")
345
+ padd("services", help="[deprecated] Services with payload data.")
346
+ ps = padd("service", help="[deprecated] Payload data by service.")
347
+ ps.add_argument("name", help="Service name.")
348
+ ph = padd("hash", help="[deprecated] Payload data by hash.")
349
+ ph.add_argument("value", help="Payload hash.")
350
+ p.set_defaults(func=_cmd_payload_history)
351
+
352
+ # internet-scanner
353
+ p = add("internet-scanner", help="Check if an IP is a scanner.")
354
+ p.add_argument("ip_address", help="IP address to look up.")
355
+ p.add_argument(
356
+ "--info", action="store_true", help="Include details about the scanner."
357
+ )
358
+ p.set_defaults(func=_cmd_internet_scanner)
359
+
360
+ # ipinfo
361
+ p = add("ipinfo", help="Check an IP against known IP lists.")
362
+ p.add_argument("ip_address", help="IP address to look up.")
363
+ p.add_argument(
364
+ "--source",
365
+ choices=IPINFO_SOURCES,
366
+ help="Check a single IP list instead of all.",
367
+ )
368
+ p.set_defaults(func=_cmd_ipinfo)
369
+
370
+ # netinfo
371
+ p = add("netinfo", help="Network info lookups (do not count against limits).")
372
+ p.add_argument(
373
+ "action",
374
+ choices=(
375
+ "lookup",
376
+ "network-addresses",
377
+ "prefixes",
378
+ "as-name",
379
+ "geolocation",
380
+ ),
381
+ help="Lookup type.",
382
+ )
383
+ p.add_argument("value", help="IP, CIDR or ASN depending on the lookup type.")
384
+ p.set_defaults(func=_cmd_netinfo)
385
+
386
+ # datacenter
387
+ p = add("datacenter", help="Get datacenter/cloud IP ranges.")
388
+ p.add_argument("provider", choices=DATACENTER_PROVIDERS, help="Cloud provider.")
389
+ p.set_defaults(func=_cmd_datacenter)
390
+
391
+ return parser
392
+
393
+
394
+ def main(argv: Sequence[str] | None = None) -> int:
395
+ parser = build_parser()
396
+ args = parser.parse_args(argv)
397
+
398
+ if not getattr(args, "func", None):
399
+ parser.print_help()
400
+ return 2
401
+
402
+ api_id = getattr(args, "api_id", None) or os.environ.get("HONEYDB_API_ID")
403
+ api_key = getattr(args, "api_key", None) or os.environ.get("HONEYDB_API_KEY")
404
+ pretty = getattr(args, "pretty", False)
405
+ timeout = getattr(args, "timeout", 30.0)
406
+
407
+ if not api_id or not api_key:
408
+ parser.error(
409
+ "API credentials required: set HONEYDB_API_ID and HONEYDB_API_KEY "
410
+ "environment variables, or pass --api-id and --api-key."
411
+ )
412
+
413
+ try:
414
+ with Client(api_id, api_key, timeout=timeout) as client:
415
+ result = args.func(client, args)
416
+ except HoneyDBError as error:
417
+ print(f"error: {error}", file=sys.stderr)
418
+ return 1
419
+
420
+ emit(result, pretty)
421
+ return 0
422
+
423
+
424
+ if __name__ == "__main__":
425
+ raise SystemExit(main())
honeydb/exceptions.py ADDED
@@ -0,0 +1,54 @@
1
+ """Exceptions raised by the HoneyDB API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class HoneyDBError(Exception):
7
+ """Base class for all HoneyDB API errors.
8
+
9
+ Attributes:
10
+ status_code: HTTP status code returned by the API, if available.
11
+ response: The raw text body of the response, if available.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ message: str,
17
+ *,
18
+ status_code: int | None = None,
19
+ response: str | None = None,
20
+ ) -> None:
21
+ super().__init__(message)
22
+ self.status_code = status_code
23
+ self.response = response
24
+
25
+
26
+ class HoneyDBAuthError(HoneyDBError):
27
+ """Raised on authentication/authorization failures (HTTP 401/403).
28
+
29
+ Usually indicates a missing or invalid ``api_id`` / ``api_key``.
30
+ """
31
+
32
+
33
+ class HoneyDBNotFoundError(HoneyDBError):
34
+ """Raised when a requested resource is not found (HTTP 404)."""
35
+
36
+
37
+ class HoneyDBRateLimitError(HoneyDBError):
38
+ """Raised when the API rate/quota limit is exceeded (HTTP 429).
39
+
40
+ Attributes:
41
+ retry_after: Seconds to wait before retrying, from the
42
+ ``Retry-After`` header, if provided.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ message: str,
48
+ *,
49
+ status_code: int | None = None,
50
+ response: str | None = None,
51
+ retry_after: float | None = None,
52
+ ) -> None:
53
+ super().__init__(message, status_code=status_code, response=response)
54
+ self.retry_after = retry_after
honeydb/py.typed ADDED
File without changes
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: honeydb
3
+ Version: 2.0.0
4
+ Summary: A Python API wrapper and CLI tool for HoneyDB.
5
+ Project-URL: Homepage, https://honeydb.io
6
+ Project-URL: Repository, https://github.com/honeydbio/honeydb-python
7
+ Author: foospidy
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: api,cli,honeydb,library,threat-intelligence,wrapper
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: requests>=2.31
25
+ Provides-Extra: dev
26
+ Requires-Dist: build; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: requests-mock; extra == 'dev'
29
+ Requires-Dist: ruff; extra == 'dev'
30
+ Requires-Dist: twine; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # honeydb-python
34
+
35
+ [![Format, Lint & Test](https://github.com/honeydbio/honeydb-python/actions/workflows/format-lint.yml/badge.svg?branch=master)](https://github.com/honeydbio/honeydb-python/actions/workflows/format-lint.yml?query=branch%3Amaster)
36
+ [![PyPI version](https://img.shields.io/pypi/v/honeydb.svg)](https://pypi.org/project/honeydb/)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/honeydb.svg)](https://pypi.org/project/honeydb/)
38
+
39
+ A Python API wrapper and command-line tool for the [HoneyDB](https://honeydb.io) API.
40
+
41
+ HoneyDB provides real-time threat intelligence collected from a distributed network of
42
+ honeypots — bad hosts, IP reputation, ASN activity, CVE sightings, network info, cloud/datacenter
43
+ IP ranges, and more.
44
+
45
+ - **Full API coverage** — every current HoneyDB endpoint is exposed.
46
+ - **Modern & typed** — Python 3.10+, full type hints, ships a `py.typed` marker.
47
+ - **Robust HTTP** — pooled `requests.Session` with automatic retries and typed exceptions.
48
+ - **Ergonomic CLI** — a git-style subcommand interface: `honeydb ip 8.8.8.8`.
49
+
50
+ ## Requirements
51
+
52
+ - Python 3.10+
53
+ - A HoneyDB API ID and API key ([sign in](https://honeydb.io) to get yours).
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install honeydb
59
+ ```
60
+
61
+ ## Authentication
62
+
63
+ All requests require an API ID and API key. Provide them via environment variables:
64
+
65
+ ```bash
66
+ export HONEYDB_API_ID=<your api id>
67
+ export HONEYDB_API_KEY=<your api key>
68
+ ```
69
+
70
+ The CLI also accepts `--api-id` / `--api-key`, and the library takes them as constructor
71
+ arguments.
72
+
73
+ ## CLI usage
74
+
75
+ ```bash
76
+ # Bad hosts seen in the last 24 hours
77
+ honeydb bad-hosts
78
+
79
+ # Full context for an IP (pretty-printed)
80
+ honeydb ip 8.8.8.8 --pretty
81
+
82
+ # Just the geolocation view of that IP
83
+ honeydb ip 8.8.8.8 --geo
84
+
85
+ # ASN organization + its prefixes
86
+ honeydb asn 15169
87
+ honeydb asn 15169 --prefixes
88
+
89
+ # Check an IP against a specific list
90
+ honeydb ipinfo 185.220.101.1 --source tor
91
+
92
+ # Cloud/datacenter IP ranges (does not count against monthly limits)
93
+ honeydb datacenter aws
94
+
95
+ # Your own sensor data for a date
96
+ honeydb sensor-data --date 2025-04-01
97
+ honeydb sensor-data --date 2025-04-01 --count
98
+
99
+ # Manage monitors
100
+ honeydb monitors list
101
+ honeydb monitors create --json '[{"monitor_type":"asn","monitor_value":"401120","description":"ASN Example"}]'
102
+ honeydb monitors delete --id 122 123
103
+ ```
104
+
105
+ Run `honeydb --help` or `honeydb <command> --help` for the full command tree. Global flags
106
+ `--pretty/-p` and `--timeout` apply to any command. Output is JSON on stdout; errors go to
107
+ stderr with a non-zero exit code.
108
+
109
+ ### Commands
110
+
111
+ | Command | Description |
112
+ | --- | --- |
113
+ | `bad-hosts [--service S] [--mydata]` | Bad hosts (last 24h), optionally by service. |
114
+ | `ip <ip> [--geo\|--netinfo\|--threatinfo\|--scanner\|--history\|--cve]` | IP context, or a single view. |
115
+ | `ip-cidr <cidr>` | All IP addresses within a network range. |
116
+ | `asn <n> [--prefixes]` | ASN organization info or its prefixes. |
117
+ | `asns [--days 1\|7]` | ASNs seen in the last 1 (default) or 7 days. |
118
+ | `cve <cve>` | IP history for a CVE. |
119
+ | `cve-ip <ip>` | CVE history for an IP. |
120
+ | `sensor-data --date D [--from-id ID] [--count] [--all]` | Your sensor event data for a date. |
121
+ | `services` | Emulated services (last 24h). |
122
+ | `stats --year Y --month M` | Summary stats for a year/month. |
123
+ | `monitors {list,logs,notifications,create,delete}` | Manage monitors. |
124
+ | `nodes [--mydata]` | honeydb-agent nodes (last 3 days). |
125
+ | `payload-history {remote-hosts,attributes,...}` | Payload history data. |
126
+ | `internet-scanner <ip> [--info]` | Whether an IP is a known internet scanner. |
127
+ | `ipinfo <ip> [--source SRC]` | Check an IP against known IP lists. |
128
+ | `netinfo {lookup,network-addresses,prefixes,as-name,geolocation} <arg>` | Network info (no monthly limit). |
129
+ | `datacenter <provider>` | Cloud/datacenter IP ranges (no monthly limit). |
130
+
131
+ `ipinfo --source` values: `bogon`, `tor`, `sansip`, `ciarmy`, `et-compromised`,
132
+ `project-honeypot`, `pallebone`, `threatfox`, `blocklist_net_ua`.
133
+
134
+ `datacenter` providers: `aws`, `azure`, `azure/china`, `azure/germany`, `azure/gov`,
135
+ `cloudflare`, `gcp`, `ibm`, `oracle`.
136
+
137
+ ## Library usage
138
+
139
+ ```python
140
+ from honeydb import Client
141
+
142
+ with Client("api_id", "api_key") as honeydb:
143
+ hosts = honeydb.bad_hosts()
144
+ context = honeydb.ip("8.8.8.8")
145
+ is_tor = honeydb.ipinfo_source("tor", "185.220.101.1")
146
+ ranges = honeydb.datacenter("aws")
147
+ ```
148
+
149
+ The client can also be used without the context manager (call `.close()` when done), and
150
+ you can pass a shared `requests.Session`, a custom `timeout`, or a different `base_url`:
151
+
152
+ ```python
153
+ client = Client("api_id", "api_key", timeout=10, retries=5)
154
+ try:
155
+ print(client.services())
156
+ finally:
157
+ client.close()
158
+ ```
159
+
160
+ ### Error handling
161
+
162
+ Every failed request raises a typed exception, all subclasses of `HoneyDBError`:
163
+
164
+ ```python
165
+ from honeydb import (
166
+ Client,
167
+ HoneyDBError,
168
+ HoneyDBAuthError,
169
+ HoneyDBNotFoundError,
170
+ HoneyDBRateLimitError,
171
+ )
172
+
173
+ with Client("api_id", "api_key") as honeydb:
174
+ try:
175
+ honeydb.ip("8.8.8.8")
176
+ except HoneyDBAuthError:
177
+ print("Check your API credentials.")
178
+ except HoneyDBRateLimitError as error:
179
+ print(f"Rate limited; retry after {error.retry_after}s")
180
+ except HoneyDBError as error:
181
+ print(f"Request failed with HTTP {error.status_code}: {error}")
182
+ ```
183
+
184
+ ### Monitors
185
+
186
+ ```python
187
+ with Client("api_id", "api_key") as honeydb:
188
+ honeydb.create_monitors([
189
+ {"monitor_type": "ip_address", "ip_address": "196.251.81.54",
190
+ "description": "IP Address Example"},
191
+ {"monitor_type": "asn", "monitor_value": "401120",
192
+ "description": "ASN Example"},
193
+ ])
194
+ monitors = honeydb.monitors()
195
+ honeydb.delete_monitors([m["id"] for m in monitors])
196
+ ```
197
+
198
+ ## API reference
199
+
200
+ The `Client` exposes one method per endpoint, grouped below.
201
+
202
+ - **Bad hosts:** `bad_hosts(mydata=False)`, `bad_hosts_by_service(service, mydata=False)`
203
+ - **IP context:** `ip(ip)`, `ip_geo(ip)`, `ip_netinfo(ip)`, `ip_threatinfo(ip)`,
204
+ `ip_internet_scanner(ip)`, `ip_history(ip)`, `ip_cve(ip)`, `ip_cidr(cidr)`
205
+ - **ASN:** `asn(n)`, `asn_prefixes(n)`, `asns()`, `asns_7d()`
206
+ - **CVE:** `cve(cve)`, `cve_ip(ip)`
207
+ - **Sensor data:** `sensor_data(date, from_id=None, mydata=True)`, `sensor_data_count(date, mydata=True)`
208
+ - **Services / stats:** `services()`, `stats(year, month)`
209
+ - **Monitors:** `monitors()`, `create_monitors(list)`, `delete_monitors(ids)`,
210
+ `monitors_logs()`, `monitors_notifications()`
211
+ - **Nodes:** `nodes(mydata=False)`
212
+ - **Payload history:** `payload_history_remote_hosts()`, `payload_history_attributes()`,
213
+ `payload_history_attribute(attr)` (plus API-deprecated helpers)
214
+ - **Internet scanner:** `internet_scanner(ip)`, `internet_scanner_info(ip)`
215
+ - **IP info lists:** `ipinfo(ip)`, `ipinfo_source(source, ip)`
216
+ - **Net info (no monthly limit):** `netinfo_lookup(ip)`, `netinfo_network_addresses(cidr)`,
217
+ `netinfo_prefixes(asn)`, `netinfo_as_name(asn)`, `netinfo_geolocation(ip)`
218
+ - **Datacenter (no monthly limit):** `datacenter(provider)`
219
+
220
+ See the [HoneyDB API documentation](https://honeydb.io/threats) for endpoint details and
221
+ response formats.
222
+
223
+ ## Migrating from v1.x
224
+
225
+ v2.0.0 is a ground-up rewrite. Notable changes:
226
+
227
+ - **New import surface.** `from honeydb import Client` (the `from honeydb import api;
228
+ api.Client(...)` form still works).
229
+ - **The CLI is now subcommand-based.** For example, `honeydb --bad-hosts` becomes
230
+ `honeydb bad-hosts`, and `honeydb --netinfo-lookup 1.2.3.4` becomes
231
+ `honeydb netinfo lookup 1.2.3.4`.
232
+ - **Replaced endpoints were dropped** in favor of their modern equivalents:
233
+ the old `ip_history()` / `ip-context` top-level methods are replaced by `ip()` and
234
+ `ip_history()` under the `/ip/<ip>` family, and `stats_asn()` is replaced by `asns()`.
235
+ - **Errors now raise typed exceptions** (`HoneyDBError` and subclasses) instead of returning
236
+ raw response bodies.
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ make env # create .env venv and install with dev extras
242
+ make format # ruff format
243
+ make lint # ruff check
244
+ make test # pytest (uses mocked HTTP, no API keys needed)
245
+ make build # build sdist + wheel
246
+ ```
247
+
248
+ This project uses [ruff](https://docs.astral.sh/ruff/) for both formatting and linting.
249
+
250
+ ## License
251
+
252
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,11 @@
1
+ honeydb/__init__.py,sha256=1KqJC_ga-3SSHe2GQqcmPnhxdWQ12d8xxuRUFr1aTUI,540
2
+ honeydb/cli.py,sha256=skIX33ztmPy8QPr1UsyT5OWavqDcHD_LJFLmEvDqMrY,14788
3
+ honeydb/exceptions.py,sha256=GN435o-SJU3TUe73We3tRuP46JKBMQ0mIVcDq5ebCnM,1463
4
+ honeydb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ honeydb/api/__init__.py,sha256=bK3G9YhLbWR6WXPJ9hIAWnQB4i_nNu1Jp-Td00b9giU,167
6
+ honeydb/api/client.py,sha256=FvAex8cdOuf8ZcLyIbkW1cXWgph3uTE_VsL8UKHWH8Q,19145
7
+ honeydb-2.0.0.dist-info/METADATA,sha256=pb0bmqPJPh4h-pO-RWC5hiLsj9d_yNMUmzy-1uR7f-w,9509
8
+ honeydb-2.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
9
+ honeydb-2.0.0.dist-info/entry_points.txt,sha256=b1d4UURHVLS5EpOhoeZnwa9AOXtAQefSRujOLUlgaAo,45
10
+ honeydb-2.0.0.dist-info/licenses/LICENSE,sha256=AbmIZiGE9mwAr03H-2D5qYYgFtvEFl7iLNgQSzSLicw,1062
11
+ honeydb-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ honeydb = honeydb.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Px Mx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.