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.
Files changed (45) hide show
  1. blctl/__init__.py +5 -0
  2. blctl/api/__init__.py +9 -0
  3. blctl/api/client/__init__.py +9 -0
  4. blctl/api/client/api_client.py +248 -0
  5. blctl/api/client/api_error.py +43 -0
  6. blctl/api/client/api_response_error.py +38 -0
  7. blctl/api/enums/__init__.py +9 -0
  8. blctl/api/enums/scan_intensity.py +33 -0
  9. blctl/api/enums/score_severity.py +30 -0
  10. blctl/api/enums/vulnerability_confidence.py +32 -0
  11. blctl/api/models/__init__.py +26 -0
  12. blctl/api/models/api_error_body.py +17 -0
  13. blctl/api/models/engagement_created.py +27 -0
  14. blctl/api/models/engagement_request_base.py +71 -0
  15. blctl/api/models/external_asset_item.py +17 -0
  16. blctl/api/models/external_asset_result.py +34 -0
  17. blctl/api/models/external_network_engagement_request.py +21 -0
  18. blctl/api/models/external_web_engagement_request.py +19 -0
  19. blctl/api/models/internal_network_engagement_request.py +25 -0
  20. blctl/api/models/internal_web_engagement_request.py +19 -0
  21. blctl/api/models/network_engagement_request_base.py +25 -0
  22. blctl/api/models/notify_url.py +20 -0
  23. blctl/api/models/request_model.py +18 -0
  24. blctl/api/models/response_model.py +18 -0
  25. blctl/api/models/web_engagement_credentials.py +25 -0
  26. blctl/api/models/web_engagement_request_base.py +30 -0
  27. blctl/api/models/webhook_header.py +17 -0
  28. blctl/commands/__init__.py +7 -0
  29. blctl/commands/engage/__init__.py +16 -0
  30. blctl/commands/engage/cli_errors.py +68 -0
  31. blctl/commands/engage/common_engagement_fields.py +65 -0
  32. blctl/commands/engage/engage_command.py +426 -0
  33. blctl/commands/engage/engagement_validation.py +160 -0
  34. blctl/commands/engage/external_assets.py +47 -0
  35. blctl/commands/engage/field_builders.py +125 -0
  36. blctl/commands/engage/network_engagements.py +92 -0
  37. blctl/commands/engage/network_recon_fields.py +20 -0
  38. blctl/commands/engage/parsing.py +52 -0
  39. blctl/commands/engage/web_engagements.py +92 -0
  40. blctl/main.py +29 -0
  41. breachlock_blctl-0.1.0.dist-info/LICENSE +21 -0
  42. breachlock_blctl-0.1.0.dist-info/METADATA +256 -0
  43. breachlock_blctl-0.1.0.dist-info/RECORD +45 -0
  44. breachlock_blctl-0.1.0.dist-info/WHEEL +4 -0
  45. breachlock_blctl-0.1.0.dist-info/entry_points.txt +3 -0
blctl/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Defines the `blctl` command-line interface package.
2
+
3
+ Author: Saul Johnson <saul.j@breachlock.com>
4
+ Since: 15/06/2026
5
+ """
blctl/api/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Defines the BreachLock AEV public v1 API client and data contracts.
2
+
3
+ Author: Saul Johnson <saul.j@breachlock.com>
4
+ Since: 15/06/2026
5
+ """
6
+
7
+ from .client import *
8
+ from .enums import *
9
+ from .models import *
@@ -0,0 +1,9 @@
1
+ """Defines the HTTP client for the BreachLock AEV public v1 API.
2
+
3
+ Author: Saul Johnson <saul.j@breachlock.com>
4
+ Since: 15/06/2026
5
+ """
6
+
7
+ from .api_client import *
8
+ from .api_error import *
9
+ from .api_response_error import *
@@ -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,9 @@
1
+ """Defines string enumerations mirroring Prisma enums for the public v1 API.
2
+
3
+ Author: Saul Johnson <saul.j@breachlock.com>
4
+ Since: 15/06/2026
5
+ """
6
+
7
+ from .scan_intensity import *
8
+ from .score_severity import *
9
+ from .vulnerability_confidence import *
@@ -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."""