breachlock-blctl 0.1.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.
- blctl/__init__.py +5 -0
- blctl/api/__init__.py +9 -0
- blctl/api/client/__init__.py +9 -0
- blctl/api/client/api_client.py +248 -0
- blctl/api/client/api_error.py +43 -0
- blctl/api/client/api_response_error.py +38 -0
- blctl/api/enums/__init__.py +9 -0
- blctl/api/enums/scan_intensity.py +33 -0
- blctl/api/enums/score_severity.py +30 -0
- blctl/api/enums/vulnerability_confidence.py +32 -0
- blctl/api/models/__init__.py +26 -0
- blctl/api/models/api_error_body.py +17 -0
- blctl/api/models/engagement_created.py +27 -0
- blctl/api/models/engagement_request_base.py +71 -0
- blctl/api/models/external_asset_item.py +17 -0
- blctl/api/models/external_asset_result.py +34 -0
- blctl/api/models/external_network_engagement_request.py +21 -0
- blctl/api/models/external_web_engagement_request.py +19 -0
- blctl/api/models/internal_network_engagement_request.py +25 -0
- blctl/api/models/internal_web_engagement_request.py +19 -0
- blctl/api/models/network_engagement_request_base.py +25 -0
- blctl/api/models/notify_url.py +20 -0
- blctl/api/models/request_model.py +18 -0
- blctl/api/models/response_model.py +18 -0
- blctl/api/models/web_engagement_credentials.py +25 -0
- blctl/api/models/web_engagement_request_base.py +30 -0
- blctl/api/models/webhook_header.py +17 -0
- blctl/commands/__init__.py +7 -0
- blctl/commands/engage/__init__.py +16 -0
- blctl/commands/engage/cli_errors.py +68 -0
- blctl/commands/engage/common_engagement_fields.py +65 -0
- blctl/commands/engage/engage_command.py +426 -0
- blctl/commands/engage/engagement_validation.py +160 -0
- blctl/commands/engage/external_assets.py +47 -0
- blctl/commands/engage/field_builders.py +125 -0
- blctl/commands/engage/network_engagements.py +92 -0
- blctl/commands/engage/network_recon_fields.py +20 -0
- blctl/commands/engage/parsing.py +52 -0
- blctl/commands/engage/web_engagements.py +92 -0
- blctl/main.py +29 -0
- breachlock_blctl-0.1.0.dist-info/LICENSE +21 -0
- breachlock_blctl-0.1.0.dist-info/METADATA +256 -0
- breachlock_blctl-0.1.0.dist-info/RECORD +45 -0
- breachlock_blctl-0.1.0.dist-info/WHEEL +4 -0
- breachlock_blctl-0.1.0.dist-info/entry_points.txt +3 -0
blctl/__init__.py
ADDED
blctl/api/__init__.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Defines the `ApiClient` HTTP client for the BreachLock public v1 API.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
14
|
+
|
|
15
|
+
from blctl.api.client.api_error import ApiError
|
|
16
|
+
from blctl.api.client.api_response_error import ApiResponseError
|
|
17
|
+
from blctl.api.models import (
|
|
18
|
+
ApiErrorBody,
|
|
19
|
+
EngagementCreated,
|
|
20
|
+
ExternalAssetItem,
|
|
21
|
+
ExternalAssetResult,
|
|
22
|
+
ExternalNetworkEngagementRequest,
|
|
23
|
+
ExternalWebEngagementRequest,
|
|
24
|
+
InternalNetworkEngagementRequest,
|
|
25
|
+
InternalWebEngagementRequest,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel)
|
|
29
|
+
"""Type variable for Pydantic response models validated by `ApiClient`."""
|
|
30
|
+
|
|
31
|
+
_EXTERNAL_ASSET_LIST_ADAPTER: TypeAdapter[list[ExternalAssetResult]] = TypeAdapter(
|
|
32
|
+
list[ExternalAssetResult]
|
|
33
|
+
)
|
|
34
|
+
"""Validates the list response from `POST /api/public/v1/external-asset`."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ApiClient:
|
|
38
|
+
"""Represents a client for the BreachLock AEV public v1 API."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
base_url: str,
|
|
43
|
+
api_key: str,
|
|
44
|
+
*,
|
|
45
|
+
timeout: float = 60.0,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initializes a new `ApiClient`.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
`base_url` (`str`): The portal base URL (e.g.
|
|
51
|
+
`https://tenant.app.breachlock.com`).
|
|
52
|
+
`api_key` (`str`): The Bearer API key for authentication.
|
|
53
|
+
`timeout` (`float`): The HTTP request timeout in seconds.
|
|
54
|
+
"""
|
|
55
|
+
normalised_base_url = base_url.rstrip("/")
|
|
56
|
+
self._client = httpx.Client(
|
|
57
|
+
base_url=normalised_base_url,
|
|
58
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
59
|
+
timeout=timeout,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def __enter__(self) -> ApiClient:
|
|
63
|
+
"""Enters the client context manager.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
`ApiClient`: This client instance.
|
|
67
|
+
"""
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(
|
|
71
|
+
self,
|
|
72
|
+
exception_type: type[BaseException] | None,
|
|
73
|
+
exception_value: BaseException | None,
|
|
74
|
+
traceback: TracebackType | None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Exits the client context manager and closes the underlying HTTP client."""
|
|
77
|
+
self.close()
|
|
78
|
+
|
|
79
|
+
def close(self) -> None:
|
|
80
|
+
"""Closes the underlying HTTP client and releases connections."""
|
|
81
|
+
self._client.close()
|
|
82
|
+
|
|
83
|
+
def create_external_assets(
|
|
84
|
+
self,
|
|
85
|
+
items: list[ExternalAssetItem],
|
|
86
|
+
) -> list[ExternalAssetResult]:
|
|
87
|
+
"""Registers manually added external assets in batch.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
`items` (`list[ExternalAssetItem]`): Each item must contain
|
|
91
|
+
`organization_id` and `hostname`.
|
|
92
|
+
Returns:
|
|
93
|
+
`list[ExternalAssetResult]`: The per-item result list as returned by
|
|
94
|
+
the portal.
|
|
95
|
+
"""
|
|
96
|
+
endpoint = "/api/public/v1/external-asset"
|
|
97
|
+
request_body = [self._serialise_request(item) for item in items]
|
|
98
|
+
response = self._client.post(endpoint, json=request_body)
|
|
99
|
+
payload = self._read_json(response)
|
|
100
|
+
try:
|
|
101
|
+
return _EXTERNAL_ASSET_LIST_ADAPTER.validate_python(payload)
|
|
102
|
+
except ValidationError as validation_error:
|
|
103
|
+
raise ApiResponseError(endpoint, validation_error) from validation_error
|
|
104
|
+
|
|
105
|
+
def create_external_network_engagement(
|
|
106
|
+
self,
|
|
107
|
+
request: ExternalNetworkEngagementRequest,
|
|
108
|
+
) -> EngagementCreated:
|
|
109
|
+
"""Creates and launches an external network engagement.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
`request` (`ExternalNetworkEngagementRequest`): The request model
|
|
113
|
+
matching the portal-side `CreateExternalEngagementSchema`.
|
|
114
|
+
Returns:
|
|
115
|
+
`EngagementCreated`: The created engagement summary.
|
|
116
|
+
"""
|
|
117
|
+
endpoint = "/api/public/v1/engagement/network/external"
|
|
118
|
+
request_body = self._serialise_request(request)
|
|
119
|
+
response = self._client.post(endpoint, json=request_body)
|
|
120
|
+
payload = self._read_json(response)
|
|
121
|
+
return self._validate_response(endpoint, payload, EngagementCreated)
|
|
122
|
+
|
|
123
|
+
def create_internal_network_engagement(
|
|
124
|
+
self,
|
|
125
|
+
request: InternalNetworkEngagementRequest,
|
|
126
|
+
) -> EngagementCreated:
|
|
127
|
+
"""Creates and launches an internal network engagement.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
`request` (`InternalNetworkEngagementRequest`): The request model
|
|
131
|
+
matching the portal-side `CreateInternalEngagementSchema`.
|
|
132
|
+
Returns:
|
|
133
|
+
`EngagementCreated`: The created engagement summary.
|
|
134
|
+
"""
|
|
135
|
+
endpoint = "/api/public/v1/engagement/network/internal"
|
|
136
|
+
request_body = self._serialise_request(request)
|
|
137
|
+
response = self._client.post(endpoint, json=request_body)
|
|
138
|
+
payload = self._read_json(response)
|
|
139
|
+
return self._validate_response(endpoint, payload, EngagementCreated)
|
|
140
|
+
|
|
141
|
+
def create_external_web_engagement(
|
|
142
|
+
self,
|
|
143
|
+
request: ExternalWebEngagementRequest,
|
|
144
|
+
) -> EngagementCreated:
|
|
145
|
+
"""Creates and launches an external web engagement.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
`request` (`ExternalWebEngagementRequest`): The request model
|
|
149
|
+
matching the portal-side `CreateExternalWebEngagementSchema`.
|
|
150
|
+
Returns:
|
|
151
|
+
`EngagementCreated`: The created engagement summary.
|
|
152
|
+
"""
|
|
153
|
+
endpoint = "/api/public/v1/engagement/web/external"
|
|
154
|
+
request_body = self._serialise_request(request)
|
|
155
|
+
response = self._client.post(endpoint, json=request_body)
|
|
156
|
+
payload = self._read_json(response)
|
|
157
|
+
return self._validate_response(endpoint, payload, EngagementCreated)
|
|
158
|
+
|
|
159
|
+
def create_internal_web_engagement(
|
|
160
|
+
self,
|
|
161
|
+
request: InternalWebEngagementRequest,
|
|
162
|
+
) -> EngagementCreated:
|
|
163
|
+
"""Creates and launches an internal web engagement.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
`request` (`InternalWebEngagementRequest`): The request model
|
|
167
|
+
matching the portal-side `CreateInternalWebEngagementSchema`.
|
|
168
|
+
Returns:
|
|
169
|
+
`EngagementCreated`: The created engagement summary.
|
|
170
|
+
"""
|
|
171
|
+
endpoint = "/api/public/v1/engagement/web/internal"
|
|
172
|
+
request_body = self._serialise_request(request)
|
|
173
|
+
response = self._client.post(endpoint, json=request_body)
|
|
174
|
+
payload = self._read_json(response)
|
|
175
|
+
return self._validate_response(endpoint, payload, EngagementCreated)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _serialise_request(model: BaseModel) -> dict[str, object]:
|
|
179
|
+
"""Serialises a Pydantic request model to a JSON-ready dict.
|
|
180
|
+
|
|
181
|
+
Omits fields whose value is `None` so optional portal fields accept
|
|
182
|
+
absent keys rather than explicit `null` (Zod `.optional()` semantics).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
`model` (`BaseModel`): The request model to serialise.
|
|
186
|
+
Returns:
|
|
187
|
+
`dict[str, object]`: The serialised request body.
|
|
188
|
+
"""
|
|
189
|
+
return model.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _read_json(response: httpx.Response) -> object:
|
|
193
|
+
"""Reads and validates the JSON body of an HTTP response.
|
|
194
|
+
|
|
195
|
+
Raises `ApiError` when the response status indicates failure.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
`response` (`httpx.Response`): The HTTP response to read.
|
|
199
|
+
Returns:
|
|
200
|
+
`object`: The parsed JSON body on success.
|
|
201
|
+
Raises:
|
|
202
|
+
`ApiError`: When the response status is not successful.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
body = response.json()
|
|
206
|
+
except ValueError:
|
|
207
|
+
body = None
|
|
208
|
+
if response.is_error:
|
|
209
|
+
error_body: ApiErrorBody | None = None
|
|
210
|
+
if isinstance(body, dict):
|
|
211
|
+
try:
|
|
212
|
+
error_body = ApiErrorBody.model_validate(body)
|
|
213
|
+
except ValidationError:
|
|
214
|
+
error_body = None
|
|
215
|
+
message = (
|
|
216
|
+
(error_body.message if error_body else None)
|
|
217
|
+
or response.reason_phrase
|
|
218
|
+
or "Request failed."
|
|
219
|
+
)
|
|
220
|
+
details = error_body.errors if error_body else None
|
|
221
|
+
raise ApiError(
|
|
222
|
+
status_code=response.status_code,
|
|
223
|
+
message=message,
|
|
224
|
+
details=details,
|
|
225
|
+
)
|
|
226
|
+
return body
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _validate_response(
|
|
230
|
+
endpoint: str,
|
|
231
|
+
payload: object,
|
|
232
|
+
model: type[ResponseModelT],
|
|
233
|
+
) -> ResponseModelT:
|
|
234
|
+
"""Validates a successful response payload against a Pydantic model.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
`endpoint` (`str`): The API path that returned the payload.
|
|
238
|
+
`payload` (`object`): The parsed JSON body to validate.
|
|
239
|
+
`model` (`type[ResponseModelT]`): The expected response model type.
|
|
240
|
+
Returns:
|
|
241
|
+
`ResponseModelT`: The validated response model instance.
|
|
242
|
+
Raises:
|
|
243
|
+
`ApiResponseError`: When the payload does not match the model.
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
return model.model_validate(payload)
|
|
247
|
+
except ValidationError as validation_error:
|
|
248
|
+
raise ApiResponseError(endpoint, validation_error) from validation_error
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Defines the `ApiError` exception for public API error responses.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiError(Exception):
|
|
11
|
+
"""Represents an error returned by the BreachLock AEV public API."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
status_code: int,
|
|
16
|
+
message: str,
|
|
17
|
+
details: object | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Initializes a new `ApiError`.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
`status_code` (`int`): The HTTP status code returned by the portal.
|
|
23
|
+
`message` (`str`): The human-readable error message.
|
|
24
|
+
`details` (`object | None`): Optional structured error details from
|
|
25
|
+
the response body (e.g. Zod validation errors).
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.message = message
|
|
30
|
+
self.details = details
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
"""Returns a human-readable representation of this error.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
`str`: The error message, HTTP status, and optional details.
|
|
37
|
+
"""
|
|
38
|
+
if self.details:
|
|
39
|
+
return (
|
|
40
|
+
f"{self.message} (HTTP {self.status_code}): "
|
|
41
|
+
f"{json.dumps(self.details, default=str)}"
|
|
42
|
+
)
|
|
43
|
+
return f"{self.message} (HTTP {self.status_code})"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Defines the `ApiResponseError` exception for unexpected 2xx response shapes.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiResponseError(Exception):
|
|
11
|
+
"""Represents a 2xx response body that does not match the expected schema.
|
|
12
|
+
|
|
13
|
+
This indicates either API contract drift on the server or a bug in the
|
|
14
|
+
client's models; it should not happen during normal operation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, endpoint: str, validation_error: ValidationError) -> None:
|
|
18
|
+
"""Initializes a new `ApiResponseError`.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
`endpoint` (`str`): The API path that returned the unexpected body.
|
|
22
|
+
`validation_error` (`ValidationError`): The Pydantic validation
|
|
23
|
+
failure describing the schema mismatch.
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(str(validation_error))
|
|
26
|
+
self.endpoint = endpoint
|
|
27
|
+
self.validation_error = validation_error
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
"""Returns a human-readable representation of this error.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
`str`: The endpoint and validation failure details.
|
|
34
|
+
"""
|
|
35
|
+
return (
|
|
36
|
+
f"Unexpected response shape from {self.endpoint}: "
|
|
37
|
+
f"{self.validation_error}"
|
|
38
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Defines the `ScanIntensity` enumeration for engagement scan depth.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScanIntensity(StrEnum):
|
|
11
|
+
"""An enumeration of scan intensity levels accepted by the public API."""
|
|
12
|
+
|
|
13
|
+
STEALTH = "STEALTH"
|
|
14
|
+
"""Represents the stealthiest scan intensity."""
|
|
15
|
+
|
|
16
|
+
QUIET = "QUIET"
|
|
17
|
+
"""Represents a quiet scan intensity."""
|
|
18
|
+
|
|
19
|
+
POLITE = "POLITE"
|
|
20
|
+
"""Represents a polite scan intensity."""
|
|
21
|
+
|
|
22
|
+
NORMAL = "NORMAL"
|
|
23
|
+
"""Represents the default scan intensity."""
|
|
24
|
+
|
|
25
|
+
AGGRESSIVE = "AGGRESSIVE"
|
|
26
|
+
"""Represents an aggressive scan intensity."""
|
|
27
|
+
|
|
28
|
+
EXTREME = "EXTREME"
|
|
29
|
+
"""Represents the most aggressive scan intensity."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
SCAN_INTENSITY_CHOICES: list[str] = [member.value for member in ScanIntensity]
|
|
33
|
+
"""Valid `--scan-intensity` values for Click option choices."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Defines the `ScoreSeverity` enumeration for engagement severity thresholds.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScoreSeverity(StrEnum):
|
|
11
|
+
"""An enumeration of severity thresholds accepted by the public API."""
|
|
12
|
+
|
|
13
|
+
NONE = "NONE"
|
|
14
|
+
"""Represents no minimum severity threshold."""
|
|
15
|
+
|
|
16
|
+
LOW = "LOW"
|
|
17
|
+
"""Represents a low severity threshold."""
|
|
18
|
+
|
|
19
|
+
MEDIUM = "MEDIUM"
|
|
20
|
+
"""Represents a medium severity threshold."""
|
|
21
|
+
|
|
22
|
+
HIGH = "HIGH"
|
|
23
|
+
"""Represents a high severity threshold."""
|
|
24
|
+
|
|
25
|
+
CRITICAL = "CRITICAL"
|
|
26
|
+
"""Represents a critical severity threshold."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
SCORE_SEVERITY_CHOICES: list[str] = [member.value for member in ScoreSeverity]
|
|
30
|
+
"""Valid `--severity-threshold` values for Click option choices."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Defines the `VulnerabilityConfidence` enumeration for confidence thresholds.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VulnerabilityConfidence(StrEnum):
|
|
11
|
+
"""An enumeration of vulnerability confidence thresholds accepted by the public API."""
|
|
12
|
+
|
|
13
|
+
VERY_LOW = "VERY_LOW"
|
|
14
|
+
"""Represents a very low confidence threshold."""
|
|
15
|
+
|
|
16
|
+
LOW = "LOW"
|
|
17
|
+
"""Represents a low confidence threshold."""
|
|
18
|
+
|
|
19
|
+
MEDIUM = "MEDIUM"
|
|
20
|
+
"""Represents a medium confidence threshold."""
|
|
21
|
+
|
|
22
|
+
HIGH = "HIGH"
|
|
23
|
+
"""Represents a high confidence threshold."""
|
|
24
|
+
|
|
25
|
+
VERY_HIGH = "VERY_HIGH"
|
|
26
|
+
"""Represents a very high confidence threshold."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
VULNERABILITY_CONFIDENCE_CHOICES: list[str] = [
|
|
30
|
+
member.value for member in VulnerabilityConfidence
|
|
31
|
+
]
|
|
32
|
+
"""Valid `--confidence-threshold` values for Click option choices."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Defines Pydantic models for BreachLock AEV public v1 API payloads.
|
|
2
|
+
|
|
3
|
+
Python attributes are snake_case; the wire format is camelCase via
|
|
4
|
+
`pydantic.alias_generators.to_camel`. Request models forbid unknown fields;
|
|
5
|
+
response models ignore unknown fields for forward compatibility.
|
|
6
|
+
|
|
7
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
8
|
+
Since: 15/06/2026
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .api_error_body import *
|
|
12
|
+
from .engagement_created import *
|
|
13
|
+
from .engagement_request_base import *
|
|
14
|
+
from .external_asset_item import *
|
|
15
|
+
from .external_asset_result import *
|
|
16
|
+
from .external_network_engagement_request import *
|
|
17
|
+
from .external_web_engagement_request import *
|
|
18
|
+
from .internal_network_engagement_request import *
|
|
19
|
+
from .internal_web_engagement_request import *
|
|
20
|
+
from .network_engagement_request_base import *
|
|
21
|
+
from .notify_url import *
|
|
22
|
+
from .request_model import *
|
|
23
|
+
from .response_model import *
|
|
24
|
+
from .web_engagement_credentials import *
|
|
25
|
+
from .web_engagement_request_base import *
|
|
26
|
+
from .webhook_header import *
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Defines the `ApiErrorBody` model for non-2xx public API responses.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from blctl.api.models.response_model import ResponseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiErrorBody(ResponseModel):
|
|
11
|
+
"""Represents a parsed non-2xx response body from the BreachLock public API."""
|
|
12
|
+
|
|
13
|
+
message: str | None = None
|
|
14
|
+
"""The human-readable error message, when present."""
|
|
15
|
+
|
|
16
|
+
errors: object | None = None
|
|
17
|
+
"""The Zod `flatten()` validation errors object, when the failure was a 400."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Defines the `EngagementCreated` model for engagement creation responses.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from blctl.api.models.response_model import ResponseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EngagementCreated(ResponseModel):
|
|
11
|
+
"""Represents the response body returned when an engagement is created.
|
|
12
|
+
|
|
13
|
+
All engagement creation endpoints (network external/internal, web
|
|
14
|
+
external/internal) share this minimal shape.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
"""The engagement ID (CUID)."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
"""The engagement name."""
|
|
22
|
+
|
|
23
|
+
deployment_id: str
|
|
24
|
+
"""The deployment ID (CUID) running the engagement."""
|
|
25
|
+
|
|
26
|
+
deployment_command_id: str
|
|
27
|
+
"""The deployment command ID (CUID) for the enqueued engage command."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Defines the `EngagementRequestBase` model shared by all engagement requests.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from blctl.api.enums import ScanIntensity, ScoreSeverity, VulnerabilityConfidence
|
|
10
|
+
from blctl.api.models.notify_url import NotifyUrl
|
|
11
|
+
from blctl.api.models.request_model import RequestModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EngagementRequestBase(RequestModel):
|
|
15
|
+
"""Represents fields shared by every engagement creation request.
|
|
16
|
+
|
|
17
|
+
Not intended to be instantiated directly; subclasses layer on type-specific
|
|
18
|
+
fields (network recon toggles, web credentials, etc.) and direction-specific
|
|
19
|
+
fields (`organization_id` for external, `deployment_id` for internal).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
"""The human-readable engagement name."""
|
|
24
|
+
|
|
25
|
+
asset_ids: list[str]
|
|
26
|
+
"""The asset IDs (CUIDs) to include in the engagement."""
|
|
27
|
+
|
|
28
|
+
severity_threshold: ScoreSeverity = ScoreSeverity.NONE
|
|
29
|
+
"""The minimum severity threshold for results."""
|
|
30
|
+
|
|
31
|
+
confidence_threshold: VulnerabilityConfidence = VulnerabilityConfidence.VERY_LOW
|
|
32
|
+
"""The minimum confidence threshold for vulnerabilities."""
|
|
33
|
+
|
|
34
|
+
attempt_credential_recovery: bool = False
|
|
35
|
+
"""Whether to attempt credential recovery during the engagement."""
|
|
36
|
+
|
|
37
|
+
provide_tailored_remediation_advice: bool = False
|
|
38
|
+
"""Whether to provide tailored remediation advice in findings."""
|
|
39
|
+
|
|
40
|
+
capture_screenshots: bool = False
|
|
41
|
+
"""Whether to capture screenshots during the engagement."""
|
|
42
|
+
|
|
43
|
+
attempt_exploitation: bool = False
|
|
44
|
+
"""Whether to attempt exploitation of discovered vulnerabilities."""
|
|
45
|
+
|
|
46
|
+
learn_findings: bool = False
|
|
47
|
+
"""Whether to feed findings back into the BreachLock learning pipeline."""
|
|
48
|
+
|
|
49
|
+
learn_exploits: bool = False
|
|
50
|
+
"""Whether to feed exploits back into the BreachLock learning pipeline."""
|
|
51
|
+
|
|
52
|
+
excluded_protocols: list[str] = Field(default_factory=list)
|
|
53
|
+
"""Protocol IDs or codes to exclude from the engagement."""
|
|
54
|
+
|
|
55
|
+
excluded_findings: list[str] = Field(default_factory=list)
|
|
56
|
+
"""Finding IDs or codes to exclude from the engagement."""
|
|
57
|
+
|
|
58
|
+
excluded_cves: list[str] = Field(default_factory=list)
|
|
59
|
+
"""CVE IDs to exclude from the engagement."""
|
|
60
|
+
|
|
61
|
+
included_cves: list[str] = Field(default_factory=list)
|
|
62
|
+
"""CVE IDs to explicitly include in the engagement."""
|
|
63
|
+
|
|
64
|
+
notify_urls: list[NotifyUrl] = Field(default_factory=list)
|
|
65
|
+
"""Webhook targets for engagement status updates."""
|
|
66
|
+
|
|
67
|
+
scan_intensity: ScanIntensity = ScanIntensity.NORMAL
|
|
68
|
+
"""The scan intensity (affects host timeout and depth)."""
|
|
69
|
+
|
|
70
|
+
threat_actor_assessment_ids: list[str] = Field(default_factory=list)
|
|
71
|
+
"""Threat actor assessment IDs (CUIDs) to associate with the engagement."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Defines the `ExternalAssetItem` model for the external-asset batch endpoint.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from blctl.api.models.request_model import RequestModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExternalAssetItem(RequestModel):
|
|
11
|
+
"""Represents one entry in the `POST /api/public/v1/external-asset` batch."""
|
|
12
|
+
|
|
13
|
+
organization_id: str
|
|
14
|
+
"""The organization ID (CUID) under which to register the asset."""
|
|
15
|
+
|
|
16
|
+
hostname: str
|
|
17
|
+
"""The hostname or IP address to register as a manually added external asset."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Defines the `ExternalAssetResult` model for external-asset registration responses.
|
|
2
|
+
|
|
3
|
+
Author: Saul Johnson <saul.j@breachlock.com>
|
|
4
|
+
Since: 15/06/2026
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from blctl.api.models.response_model import ResponseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExternalAssetResult(ResponseModel):
|
|
13
|
+
"""Represents one entry in the `POST /api/public/v1/external-asset` response."""
|
|
14
|
+
|
|
15
|
+
asset_id: str
|
|
16
|
+
"""The asset ID (CUID) assigned to the registered target."""
|
|
17
|
+
|
|
18
|
+
external_asset_id: str
|
|
19
|
+
"""The external asset ID (CUID) for the registration."""
|
|
20
|
+
|
|
21
|
+
domain_report_id: str
|
|
22
|
+
"""The domain report ID (CUID) created or updated for this asset."""
|
|
23
|
+
|
|
24
|
+
domain_investigation_id: str
|
|
25
|
+
"""The domain investigation ID (CUID) scoped to the organization."""
|
|
26
|
+
|
|
27
|
+
hostname: str
|
|
28
|
+
"""The hostname that was registered."""
|
|
29
|
+
|
|
30
|
+
ip_address: str
|
|
31
|
+
"""The resolved IP address for the hostname."""
|
|
32
|
+
|
|
33
|
+
status: Literal["created", "updated"]
|
|
34
|
+
"""Whether the asset was newly created or updated in place."""
|