Skip to content

Commit aa7e53a

Browse files
committed
Refactor: Move code examples to example/, handle API format compat
1 parent 0bc3391 commit aa7e53a

5 files changed

Lines changed: 94 additions & 209 deletions

File tree

README.md

Lines changed: 1 addition & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -13,48 +13,6 @@ pip install leakix
1313

1414
To run tests, use `poetry run pytest`.
1515

16-
## Quick Start
17-
18-
```python
19-
from leakix import Client
20-
21-
client = Client(api_key="your-api-key")
22-
23-
# Simple search - same syntax as the website
24-
results = client.search("+plugin:GitConfigHttpPlugin", scope="leak")
25-
for event in results.json():
26-
print(event.ip, event.host)
27-
28-
# Search services
29-
results = client.search("+country:FR +port:22", scope="service")
30-
```
31-
32-
## Async Client
33-
34-
For async applications, use `AsyncClient`:
35-
36-
```python
37-
import asyncio
38-
from leakix import AsyncClient
39-
40-
async def main():
41-
async with AsyncClient(api_key="your-api-key") as client:
42-
# Simple search
43-
results = await client.search("+plugin:GitConfigHttpPlugin", scope="leak")
44-
for event in results:
45-
print(event.ip, event.host)
46-
47-
# Host lookup
48-
host = await client.get_host("8.8.8.8")
49-
print(host["services"])
50-
51-
# Streaming bulk export
52-
async for aggregation in client.bulk_export_stream(queries):
53-
print(aggregation.events[0].ip)
54-
55-
asyncio.run(main())
56-
```
57-
5816
## Documentation
5917

6018
Docstrings are used to document the library.
@@ -95,160 +53,4 @@ If you need commercial support, have a look at https://leakix.net/plans.
9553

9654
## Examples
9755

98-
```python
99-
import decouple
100-
from leakix import Client
101-
from leakix.query import MustQuery, MustNotQuery, RawQuery
102-
from leakix.field import PluginField, CountryField, TimeField, Operator
103-
from leakix.plugin import Plugin
104-
from datetime import datetime, timedelta
105-
106-
107-
API_KEY = decouple.config("API_KEY")
108-
BASE_URL = decouple.config("LEAKIX_HOST", default=None)
109-
CLIENT = Client(api_key=API_KEY)
110-
111-
112-
def example_get_host_filter_plugin():
113-
response = CLIENT.get_host(ipv4="33.33.33.33")
114-
assert response.status_code() == 200
115-
116-
117-
def example_get_service_filter_plugin():
118-
"""
119-
Filter by fields. In this example, we want to have the NTLM services.
120-
A list of plugins can be found in leakix.plugin
121-
"""
122-
query_http_ntlm = MustQuery(field=PluginField(Plugin.HttpNTLM))
123-
response = CLIENT.get_service(queries=[query_http_ntlm])
124-
assert response.status_code() == 200
125-
# check we only get NTML related services
126-
assert all((i.tags == ["ntlm"] for i in response.json()))
127-
128-
129-
def example_get_service_filter_plugin_with_pagination():
130-
"""
131-
Filter by fields. In this example, we want to have the NTLM services.
132-
A list of plugins can be found in leakix.plugin.
133-
Ask for page 1 (starts at 0)
134-
"""
135-
query_http_ntlm = MustQuery(field=PluginField(Plugin.HttpNTLM))
136-
response = CLIENT.get_service(queries=[query_http_ntlm], page=1)
137-
assert response.status_code() == 200
138-
# check we only get NTML related services
139-
assert all((i.tags == ["ntlm"] for i in response.json()))
140-
141-
142-
def example_get_leaks_filter_multiple_plugins():
143-
query_http_ntlm = MustQuery(field=PluginField(Plugin.HttpNTLM))
144-
query_country = MustQuery(field=CountryField("France"))
145-
response = CLIENT.get_leak(queries=[query_http_ntlm, query_country])
146-
assert response.status_code() == 200
147-
assert all(
148-
(
149-
i.geoip.country_name == "France" and i.tags == ["ntlm"]
150-
for i in response.json()
151-
)
152-
)
153-
154-
155-
def example_get_leaks_multiple_filter_plugins_must_not():
156-
query_http_ntlm = MustQuery(field=PluginField(Plugin.HttpNTLM))
157-
query_country = MustNotQuery(field=CountryField("France"))
158-
response = CLIENT.get_leak(queries=[query_http_ntlm, query_country])
159-
assert response.status_code() == 200
160-
assert all(
161-
(
162-
i.geoip.country_name != "France" and i.tags == ["ntlm"]
163-
for i in response.json()
164-
)
165-
)
166-
167-
168-
def example_get_leak_raw_query():
169-
raw_query = '+plugin:HttpNTLM +country:"France"'
170-
query = RawQuery(raw_query)
171-
response = CLIENT.get_leak(queries=[query])
172-
assert response.status_code() == 200
173-
assert all(
174-
(
175-
i.geoip.country_name == "France" and i.tags == ["ntlm"]
176-
for i in response.json()
177-
)
178-
)
179-
180-
181-
def example_get_leak_plugins_with_time():
182-
query_plugin = MustQuery(field=PluginField(Plugin.GitConfigHttpPlugin))
183-
today = datetime.now()
184-
one_month_ago = today - timedelta(days=30)
185-
query_today = MustQuery(field=TimeField(today, Operator.StrictlySmaller))
186-
query_yesterday = MustQuery(
187-
field=TimeField(one_month_ago, Operator.StrictlyGreater)
188-
)
189-
queries = [query_today, query_yesterday, query_plugin]
190-
response = CLIENT.get_leak(queries=queries)
191-
assert response.status_code() == 200
192-
193-
194-
def example_get_plugins():
195-
response = CLIENT.get_plugins()
196-
for p in response.json():
197-
print(p.name)
198-
print(p.description)
199-
200-
201-
def example_search_simple():
202-
"""
203-
Simple search using query string syntax (same as the website).
204-
No need to build Query objects manually.
205-
"""
206-
response = CLIENT.search("+plugin:GitConfigHttpPlugin", scope="leak")
207-
for event in response.json():
208-
print(event.ip)
209-
210-
211-
def example_search_service():
212-
"""
213-
Search for services with multiple filters.
214-
"""
215-
response = CLIENT.search("+country:FR +port:22", scope="service")
216-
for event in response.json():
217-
print(event.ip, event.port)
218-
219-
220-
def example_get_domain():
221-
"""
222-
Get services and leaks for a domain.
223-
"""
224-
response = CLIENT.get_domain("example.com")
225-
if response.is_success():
226-
print("Services:", response.json()["services"])
227-
print("Leaks:", response.json()["leaks"])
228-
229-
230-
def example_bulk_stream():
231-
"""
232-
Streaming bulk export - memory efficient for large datasets.
233-
Results are yielded one by one instead of loading all into memory.
234-
"""
235-
query = MustQuery(field=PluginField(Plugin.GitConfigHttpPlugin))
236-
for aggregation in CLIENT.bulk_export_stream(queries=[query]):
237-
for event in aggregation.events:
238-
print(event.ip)
239-
240-
241-
if __name__ == "__main__":
242-
example_get_host_filter_plugin()
243-
example_get_service_filter_plugin()
244-
example_get_service_filter_plugin_with_pagination()
245-
example_get_leaks_filter_multiple_plugins()
246-
example_get_leaks_multiple_filter_plugins_must_not()
247-
example_get_leak_plugins_with_time()
248-
example_get_leak_raw_query()
249-
example_get_plugins()
250-
example_search_simple()
251-
example_search_service()
252-
example_get_domain()
253-
example_bulk_stream()
254-
```
56+
See [example/](example/) for complete usage examples.

example/example_async_client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import asyncio
2+
3+
import decouple
4+
5+
from leakix import AsyncClient
6+
7+
API_KEY = decouple.config("API_KEY")
8+
9+
10+
async def example_search():
11+
"""Simple async search using query string syntax."""
12+
async with AsyncClient(api_key=API_KEY) as client:
13+
results = await client.search("+plugin:GitConfigHttpPlugin", scope="leak")
14+
for event in results:
15+
print(event.ip, event.host)
16+
17+
18+
async def example_get_host():
19+
"""Async host lookup."""
20+
async with AsyncClient(api_key=API_KEY) as client:
21+
host = await client.get_host("8.8.8.8")
22+
print("Services:", host["services"])
23+
print("Leaks:", host["leaks"])
24+
25+
26+
async def example_get_domain():
27+
"""Async domain lookup."""
28+
async with AsyncClient(api_key=API_KEY) as client:
29+
domain = await client.get_domain("example.com")
30+
print("Services:", domain["services"])
31+
print("Leaks:", domain["leaks"])
32+
33+
34+
async def example_bulk_export_stream():
35+
"""Async streaming bulk export - memory efficient for large datasets."""
36+
async with AsyncClient(api_key=API_KEY) as client:
37+
async for aggregation in client.bulk_export_stream("+plugin:GitConfigHttpPlugin"):
38+
for event in aggregation["events"]:
39+
print(event["ip"])
40+
41+
42+
if __name__ == "__main__":
43+
asyncio.run(example_search())

example/example_client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,36 @@ def example_get_subdomains():
118118
print(response.json())
119119

120120

121+
def example_search_simple():
122+
"""Simple search using query string syntax (same as the website)."""
123+
response = CLIENT.search("+plugin:GitConfigHttpPlugin", scope="leak")
124+
for event in response.json():
125+
print(event.ip)
126+
127+
128+
def example_search_service():
129+
"""Search for services with multiple filters."""
130+
response = CLIENT.search("+country:FR +port:22", scope="service")
131+
for event in response.json():
132+
print(event.ip, event.port)
133+
134+
135+
def example_get_domain():
136+
"""Get services and leaks for a domain."""
137+
response = CLIENT.get_domain("example.com")
138+
if response.is_success():
139+
print("Services:", response.json()["services"])
140+
print("Leaks:", response.json()["leaks"])
141+
142+
143+
def example_bulk_export_stream():
144+
"""Streaming bulk export - memory efficient for large datasets."""
145+
query = MustQuery(field=PluginField(Plugin.GitConfigHttpPlugin))
146+
for aggregation in CLIENT.bulk_export_stream(queries=[query]):
147+
for event in aggregation.events:
148+
print(event.ip)
149+
150+
121151
if __name__ == "__main__":
122152
example_get_host_filter_plugin()
123153
example_get_service_filter_plugin()
@@ -131,3 +161,7 @@ def example_get_subdomains():
131161
example_bulk_service()
132162
example_bulk_export_last_event()
133163
example_get_subdomains()
164+
example_search_simple()
165+
example_search_service()
166+
example_get_domain()
167+
example_bulk_export_stream()

leakix/async_client.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,9 @@ async def get_host(self, ip: str) -> dict:
176176
"""
177177
status, data = await self._get(f"/host/{ip}")
178178
if status == 200 and isinstance(data, dict):
179-
services = data.get("Services") or []
180-
leaks = data.get("Leaks") or []
179+
# Handle both old (uppercase) and new (lowercase) API response formats
180+
services = data.get("services") or data.get("Services") or []
181+
leaks = data.get("leaks") or data.get("Leaks") or []
181182
return {
182183
"services": self._parse_events(services),
183184
"leaks": self._parse_events(leaks),
@@ -206,8 +207,9 @@ async def get_domain(self, domain: str) -> dict:
206207
"""
207208
status, data = await self._get(f"/domain/{domain}")
208209
if status == 200 and isinstance(data, dict):
209-
services = data.get("Services") or []
210-
leaks = data.get("Leaks") or []
210+
# Handle both old (uppercase) and new (lowercase) API response formats
211+
services = data.get("services") or data.get("Services") or []
212+
leaks = data.get("leaks") or data.get("Leaks") or []
211213
return {
212214
"services": self._parse_events(services),
213215
"leaks": self._parse_events(leaks),

leakix/client.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,12 @@ def get_host(self, ipv4: str) -> AbstractResponse:
148148
r = self.__get(url, params=None)
149149
if r.is_success():
150150
response_json = r.json()
151-
formatted_result = HostResult.from_dict(response_json)
151+
# Handle both old (uppercase) and new (lowercase) API response formats
152+
services = response_json.get("services") or response_json.get("Services") or []
153+
leaks = response_json.get("leaks") or response_json.get("Leaks") or []
152154
response_json = {
153-
"services": formatted_result.Services,
154-
"leaks": formatted_result.Leaks,
155+
"services": services,
156+
"leaks": leaks,
155157
}
156158
r.response_json = response_json
157159
return r
@@ -253,10 +255,12 @@ def get_domain(self, domain: str) -> AbstractResponse:
253255
r = self.__get(url, params=None)
254256
if r.is_success():
255257
response_json = r.json()
256-
formatted_result = HostResult.from_dict(response_json)
258+
# Handle both old (uppercase) and new (lowercase) API response formats
259+
services = response_json.get("services") or response_json.get("Services") or []
260+
leaks = response_json.get("leaks") or response_json.get("Leaks") or []
257261
response_json = {
258-
"services": formatted_result.Services,
259-
"leaks": formatted_result.Leaks,
262+
"services": services,
263+
"leaks": leaks,
260264
}
261265
r.response_json = response_json
262266
return r

0 commit comments

Comments
 (0)