Skip to content

Commit 1beaf5b

Browse files
authored
Merge pull request #1 from crowdsecurity/$main-032f7c1
Update python SDK version: 1.0.0
2 parents 032f7c1 + b4fdcf5 commit 1beaf5b

File tree

18 files changed

+2628
-0
lines changed

18 files changed

+2628
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Crowdsec
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.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# crowdsec_tracker_api
2+
3+
**crowdsec_tracker_api** is a Python SDK for the [CrowdSec Tracker API](https://docs.crowdsec.net/u/service_api/intro/).
4+
This library enables you to manage CrowdSec Live Exploit Tracker resources such as CVEs data, IPs from a specific CVE and manage integrations.
5+
6+
## Installation
7+
8+
```bash
9+
pip install crowdsec_tracker_api
10+
```
11+
12+
## Usage
13+
14+
You can follow this [documentation](https://docs.crowdsec.net/u/service_api/quickstart/blocklists) to see the basic usage of the SDK.
15+
16+
## Documentation
17+
You can access the full usage documentation [here](https://github.com/crowdsecurity/crowdsec-tracker-api-sdk-python/tree/main/doc).
18+
19+
## Contributing
20+
21+
This SDK is auto-generated from the API specification. You can open an issue on this repository if you find a bug or if you want to add a feature.

crowdsec_tracker_api/__init__.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from enum import Enum
2+
from .models import *
3+
from .base_model import Page
4+
from .services.integrations import Integrations
5+
from .services.cves import Cves
6+
from .http_client import ApiKeyAuth
7+
8+
class Server(Enum):
9+
production_server = 'https://admin.api.crowdsec.net/v1'
10+
11+
__all__ = [
12+
'Integrations',
13+
'Cves',
14+
'ApiKeyCredentials',
15+
'BasicAuthCredentials',
16+
'BlocklistSubscription',
17+
'HTTPValidationError',
18+
'IntegrationCreateRequest',
19+
'IntegrationCreateResponse',
20+
'IntegrationGetResponse',
21+
'IntegrationGetResponsePage',
22+
'IntegrationType',
23+
'IntegrationUpdateRequest',
24+
'IntegrationUpdateResponse',
25+
'Links',
26+
'OutputFormat',
27+
'Stats',
28+
'ValidationError',
29+
'AffectedComponent',
30+
'AllowlistSubscription',
31+
'AttackDetail',
32+
'Behavior',
33+
'CVEEvent',
34+
'CVEResponseBase',
35+
'CVEsubscription',
36+
'CWE',
37+
'Classification',
38+
'Classifications',
39+
'EntityType',
40+
'GetCVEIPsResponsePage',
41+
'GetCVEResponse',
42+
'GetCVESubscribedIntegrationsResponsePage',
43+
'GetCVEsResponsePage',
44+
'History',
45+
'IPItem',
46+
'IntegrationResponse',
47+
'Location',
48+
'MitreTechnique',
49+
'Reference',
50+
'ScoreBreakdown',
51+
'Scores',
52+
'SubscribeCVEIntegrationRequest',
53+
'ApiKeyAuth',
54+
'Server',
55+
'Page'
56+
]
4.25 KB
Binary file not shown.
7.45 KB
Binary file not shown.

crowdsec_tracker_api/base_model.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from urllib.parse import urlparse
2+
from pydantic import BaseModel, ConfigDict, PrivateAttr, RootModel
3+
from typing import Generic, Sequence, Optional, TypeVar, Any
4+
from httpx import Auth
5+
from .http_client import HttpClient
6+
7+
8+
class BaseModelSdk(BaseModel):
9+
model_config = ConfigDict(
10+
extra="ignore",
11+
)
12+
_client: Optional["Service"] = PrivateAttr(default=None)
13+
14+
def __init__(self, /, _client: "Service" = None, **data):
15+
super().__init__(**data)
16+
self._client = _client
17+
18+
def next(self, client: "Service" = None) -> Optional["BaseModelSdk"]:
19+
return (client if client is not None else self._client).next_page(self)
20+
21+
22+
class RootModelSdk(RootModel):
23+
def __getattr__(self, item: str) -> Any:
24+
return getattr(self.root, item)
25+
26+
27+
T = TypeVar("T")
28+
29+
30+
class Page(BaseModelSdk, Generic[T]):
31+
items: Sequence[T]
32+
total: Optional[int]
33+
page: Optional[int]
34+
size: Optional[int]
35+
pages: Optional[int] = None
36+
links: Optional[dict] = None
37+
38+
39+
class Service:
40+
def __init__(self, base_url: str, auth: Auth) -> None:
41+
self.http_client = HttpClient(base_url=base_url, auth=auth)
42+
43+
def next_page(self, page: BaseModelSdk) -> Optional[BaseModelSdk]:
44+
if not hasattr(page, "links") or not page.links:
45+
raise ValueError(
46+
"No links found in the response, this is not a paginated response."
47+
)
48+
if page.links.next:
49+
# links are relative to host not to full base url. We need to pass a full formatted url here
50+
parsed_url = urlparse(self.http_client.base_url)
51+
response = self.http_client.get(
52+
f"{parsed_url.scheme}://{parsed_url.netloc}{page.links.next}",
53+
path_params=None,
54+
params=None,
55+
headers=None,
56+
)
57+
return page.__class__(_client=self, **response.json())
58+
return None
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from urllib.parse import quote, urlparse
2+
3+
from botocore.auth import SigV4Auth
4+
from botocore.awsrequest import AWSRequest
5+
import botocore.session
6+
7+
import httpx
8+
9+
10+
class AWSSignV4Auth(httpx.Auth):
11+
def __init__(self, aws_region="eu-west-1") -> None:
12+
self.aws_region = aws_region
13+
14+
def auth_flow(self, request):
15+
request = self.sign_request(request)
16+
yield request
17+
18+
def sign_request(self, request: httpx.Request) -> httpx.Request:
19+
"""Signs an httpx request with AWS Signature Version 4."""
20+
21+
session = botocore.session.get_session()
22+
credentials = session.get_credentials()
23+
aws_request = AWSRequest(
24+
method=request.method.upper(), url=str(request.url), data=request.content
25+
)
26+
region = self.aws_region
27+
service = "execute-api"
28+
29+
# Sign the request
30+
SigV4Auth(credentials, service, region).add_auth(aws_request)
31+
32+
# Update the httpx request headers with the signed headers
33+
request.headers.update(dict(aws_request.headers))
34+
35+
return request
36+
37+
38+
class ApiKeyAuth(httpx.Auth):
39+
def __init__(self, api_key: str) -> None:
40+
self.api_key = api_key
41+
42+
def auth_flow(self, request):
43+
request.headers["x-api-key"] = self.api_key
44+
yield request
45+
46+
47+
class HttpClient:
48+
def __init__(self, base_url: str, auth: httpx.Auth, aws_region="eu-west-1") -> None:
49+
self.aws_region = aws_region
50+
self.base_url = base_url
51+
self.auth = auth
52+
self.client = httpx.Client()
53+
self.timeout = 30
54+
55+
def _replace_path_params(self, url: str, path_params: dict):
56+
if path_params:
57+
for param, value in path_params.items():
58+
if not value:
59+
raise ValueError(
60+
f"Parameter {param} is required, cannot be empty or blank."
61+
)
62+
url = url.replace(f"{{{param}}}", quote(str(value)))
63+
return url
64+
65+
def _normalize_url(self, url: str):
66+
self.base_url = self.base_url.rstrip("/")
67+
parsed_url = urlparse(url)
68+
if not parsed_url.netloc:
69+
return f"{self.base_url}{url}"
70+
return url
71+
72+
def get(
73+
self,
74+
url: str,
75+
path_params: dict = None,
76+
params: dict = None,
77+
headers: dict = None,
78+
):
79+
url = self._replace_path_params(
80+
url=self._normalize_url(url), path_params=path_params
81+
)
82+
response = self.client.get(
83+
url=url,
84+
params=params,
85+
headers=headers,
86+
auth=self.auth,
87+
timeout=self.timeout,
88+
)
89+
response.raise_for_status()
90+
return response
91+
92+
def post(
93+
self, url: str, path_params: dict, params: dict, headers: dict, json: dict
94+
):
95+
url = self._replace_path_params(
96+
url=self._normalize_url(url), path_params=path_params
97+
)
98+
response = self.client.post(
99+
url=url,
100+
params=params,
101+
headers=headers,
102+
json=json,
103+
auth=self.auth,
104+
timeout=self.timeout,
105+
)
106+
response.raise_for_status()
107+
return response
108+
109+
def put(self, url: str, path_params: dict, params: dict, headers: dict, json: dict):
110+
url = self._replace_path_params(
111+
url=self._normalize_url(url), path_params=path_params
112+
)
113+
response = self.client.put(
114+
url=url,
115+
params=params,
116+
headers=headers,
117+
json=json,
118+
auth=self.auth,
119+
timeout=self.timeout,
120+
)
121+
response.raise_for_status()
122+
return response
123+
124+
def patch(
125+
self, url: str, path_params: dict, params: dict, headers: dict, json: dict
126+
):
127+
url = self._replace_path_params(
128+
url=self._normalize_url(url), path_params=path_params
129+
)
130+
response = self.client.patch(
131+
url=url,
132+
params=params,
133+
headers=headers,
134+
json=json,
135+
auth=self.auth,
136+
timeout=self.timeout,
137+
)
138+
response.raise_for_status()
139+
return response
140+
141+
def delete(self, url: str, path_params: dict, params: dict, headers: dict):
142+
url = self._replace_path_params(
143+
url=self._normalize_url(url), path_params=path_params
144+
)
145+
response = self.client.delete(
146+
url=url,
147+
params=params,
148+
headers=headers,
149+
auth=self.auth,
150+
timeout=self.timeout,
151+
)
152+
response.raise_for_status()
153+
return response

0 commit comments

Comments
 (0)