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 +25 -0
- honeydb/api/__init__.py +5 -0
- honeydb/api/client.py +518 -0
- honeydb/cli.py +425 -0
- honeydb/exceptions.py +54 -0
- honeydb/py.typed +0 -0
- honeydb-2.0.0.dist-info/METADATA +252 -0
- honeydb-2.0.0.dist-info/RECORD +11 -0
- honeydb-2.0.0.dist-info/WHEEL +4 -0
- honeydb-2.0.0.dist-info/entry_points.txt +2 -0
- honeydb-2.0.0.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|
honeydb/api/__init__.py
ADDED
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
|
+
[](https://github.com/honeydbio/honeydb-python/actions/workflows/format-lint.yml?query=branch%3Amaster)
|
|
36
|
+
[](https://pypi.org/project/honeydb/)
|
|
37
|
+
[](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,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.
|