pyattackforge 0.1.1__py3-none-any.whl → 0.1.7__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.
@@ -0,0 +1,384 @@
1
+ """
2
+ PyAttackForge is free software: you can redistribute it and/or modify
3
+ it under the terms of the GNU Affero General Public License as published by
4
+ the Free Software Foundation, either version 3 of the License, or
5
+ (at your option) any later version.
6
+
7
+ PyAttackForge is distributed in the hope that it will be useful,
8
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ GNU Affero General Public License for more details.
11
+
12
+ You should have received a copy of the GNU Affero General Public License
13
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
14
+ """
15
+
16
+ import requests
17
+ import logging
18
+ from datetime import datetime, timezone, timedelta
19
+ from typing import Any, Dict, Optional, Set, Tuple, List
20
+
21
+
22
+ logger = logging.getLogger("pyattackforge")
23
+
24
+
25
+ class PyAttackForgeClient:
26
+ """
27
+ Python client for interacting with the AttackForge API.
28
+
29
+ Provides methods to manage assets, projects, and vulnerabilities.
30
+ Supports dry-run mode for testing without making real API calls.
31
+ """
32
+
33
+ def __init__(self, api_key: str, base_url: str = "https://demo.attackforge.com", dry_run: bool = False):
34
+ """
35
+ Initialize the PyAttackForgeClient.
36
+
37
+ Args:
38
+ api_key (str): Your AttackForge API key.
39
+ base_url (str, optional): The base URL for the AttackForge instance. Defaults to "https://demo.attackforge.com".
40
+ dry_run (bool, optional): If True, no real API calls are made. Defaults to False.
41
+ """
42
+ self.base_url = base_url.rstrip("/")
43
+ self.headers = {
44
+ "X-SSAPI-KEY": api_key,
45
+ "Content-Type": "application/json",
46
+ "Connection": "close"
47
+ }
48
+ self.dry_run = dry_run
49
+ self._asset_cache = None
50
+ self._project_scope_cache = {} # {project_id: set(asset_names)}
51
+
52
+ def _request(
53
+ self,
54
+ method: str,
55
+ endpoint: str,
56
+ json_data: Optional[Dict[str, Any]] = None,
57
+ params: Optional[Dict[str, Any]] = None
58
+ ) -> Any:
59
+ """
60
+ Internal method to send an HTTP request to the AttackForge API.
61
+
62
+ Args:
63
+ method (str): HTTP method (get, post, put, etc.).
64
+ endpoint (str): API endpoint path.
65
+ json_data (dict, optional): JSON payload for the request.
66
+ params (dict, optional): Query parameters.
67
+
68
+ Returns:
69
+ Response: The HTTP response object.
70
+ """
71
+ url = f"{self.base_url}{endpoint}"
72
+ if self.dry_run:
73
+ logger.info("[DRY RUN] %s %s", method.upper(), url)
74
+ if json_data:
75
+ logger.info("Payload: %s", json_data)
76
+ if params:
77
+ logger.info("Params: %s", params)
78
+ return DummyResponse()
79
+ return requests.request(method, url, headers=self.headers, json=json_data, params=params)
80
+
81
+ def get_assets(self) -> Dict[str, Dict[str, Any]]:
82
+ """
83
+ Retrieve all assets from AttackForge.
84
+
85
+ Returns:
86
+ dict: Mapping of asset names to asset details.
87
+ """
88
+ if self._asset_cache is None:
89
+ self._asset_cache = {}
90
+ skip, limit = 0, 500
91
+ while True:
92
+ resp = self._request("get", "/api/ss/assets", params={"skip": skip, "limit": limit})
93
+ data = resp.json()
94
+ for asset in data.get("assets", []):
95
+ name = asset.get("asset")
96
+ if name:
97
+ self._asset_cache[name] = asset
98
+ if skip + limit >= data.get("count", 0):
99
+ break
100
+ skip += limit
101
+ return self._asset_cache
102
+
103
+ def get_asset_by_name(self, name: str) -> Optional[Dict[str, Any]]:
104
+ """
105
+ Retrieve an asset by its name.
106
+
107
+ Args:
108
+ name (str): The asset name.
109
+
110
+ Returns:
111
+ dict or None: Asset details if found, else None.
112
+ """
113
+ return self.get_assets().get(name)
114
+
115
+ def create_asset(self, asset_data: Dict[str, Any]) -> Dict[str, Any]:
116
+ """
117
+ Create a new asset in AttackForge.
118
+
119
+ Args:
120
+ asset_data (dict): Asset details.
121
+
122
+ Returns:
123
+ dict: Created asset details.
124
+
125
+ Raises:
126
+ RuntimeError: If asset creation fails.
127
+ """
128
+ resp = self._request("post", "/api/ss/library/asset", json_data=asset_data)
129
+ if resp.status_code in (200, 201):
130
+ asset = resp.json()
131
+ self._asset_cache = None # Invalidate cache
132
+ return asset
133
+ if "Asset Already Exists" in resp.text:
134
+ return self.get_asset_by_name(asset_data["name"])
135
+ raise RuntimeError(f"Asset creation failed: {resp.text}")
136
+
137
+ def get_project_by_name(self, name: str) -> Optional[Dict[str, Any]]:
138
+ """
139
+ Retrieve a project by its name.
140
+
141
+ Args:
142
+ name (str): The project name.
143
+
144
+ Returns:
145
+ dict or None: Project details if found, else None.
146
+ """
147
+ params = {
148
+ "startDate": "2000-01-01T00:00:00.000Z",
149
+ "endDate": "2100-01-01T00:00:00.000Z",
150
+ "status": "All"
151
+ }
152
+ resp = self._request("get", "/api/ss/projects", params=params)
153
+ for proj in resp.json().get("projects", []):
154
+ if proj.get("project_name") == name:
155
+ return proj
156
+ return None
157
+
158
+ def get_project_scope(self, project_id: str) -> Set[str]:
159
+ """
160
+ Retrieve the scope (assets) of a project.
161
+
162
+ Args:
163
+ project_id (str): The project ID.
164
+
165
+ Returns:
166
+ set: Set of asset names in the project scope.
167
+
168
+ Raises:
169
+ RuntimeError: If project retrieval fails.
170
+ """
171
+ if project_id in self._project_scope_cache:
172
+ return self._project_scope_cache[project_id]
173
+
174
+ resp = self._request("get", f"/api/ss/project/{project_id}")
175
+ if resp.status_code != 200:
176
+ raise RuntimeError(f"Failed to retrieve project: {resp.text}")
177
+
178
+ scope = set(resp.json().get("scope", []))
179
+ self._project_scope_cache[project_id] = scope
180
+ return scope
181
+
182
+ def update_project_scope(self, project_id: str, new_assets: List[str]) -> Dict[str, Any]:
183
+ """
184
+ Update the scope (assets) of a project.
185
+
186
+ Args:
187
+ project_id (str): The project ID.
188
+ new_assets (iterable): Asset names to add to the scope.
189
+
190
+ Returns:
191
+ dict: Updated project details.
192
+
193
+ Raises:
194
+ RuntimeError: If update fails.
195
+ """
196
+ current_scope = self.get_project_scope(project_id)
197
+ updated_scope = list(current_scope.union(new_assets))
198
+ resp = self._request("put", f"/api/ss/project/{project_id}", json_data={"scope": updated_scope})
199
+ if resp.status_code not in (200, 201):
200
+ raise RuntimeError(f"Failed to update project scope: {resp.text}")
201
+ self._project_scope_cache[project_id] = set(updated_scope)
202
+ return resp.json()
203
+
204
+ def create_project(self, name: str, **kwargs) -> Dict[str, Any]:
205
+ """
206
+ Create a new project in AttackForge.
207
+
208
+ Args:
209
+ name (str): Project name.
210
+ **kwargs: Additional project fields.
211
+
212
+ Returns:
213
+ dict: Created project details.
214
+
215
+ Raises:
216
+ RuntimeError: If project creation fails.
217
+ """
218
+ start, end = get_default_dates()
219
+ payload = {
220
+ "name": name,
221
+ "code": kwargs.get("code", "DEFAULT"),
222
+ "groups": kwargs.get("groups", []),
223
+ "startDate": kwargs.get("startDate", start),
224
+ "endDate": kwargs.get("endDate", end),
225
+ "scope": kwargs.get("scope", []),
226
+ "testsuites": kwargs.get("testsuites", []),
227
+ "organization_code": kwargs.get("organization_code", "ORG_DEFAULT"),
228
+ "vulnerability_code": kwargs.get("vulnerability_code", "VULN_"),
229
+ "scoringSystem": kwargs.get("scoringSystem", "CVSSv3.1"),
230
+ "team_notifications": kwargs.get("team_notifications", []),
231
+ "admin_notifications": kwargs.get("admin_notifications", []),
232
+ "custom_fields": kwargs.get("custom_fields", []),
233
+ "asset_library_ids": kwargs.get("asset_library_ids", []),
234
+ "sla_activation": kwargs.get("sla_activation", "automatic")
235
+ }
236
+ resp = self._request("post", "/api/ss/project", json_data=payload)
237
+ if resp.status_code in (200, 201):
238
+ return resp.json()
239
+ raise RuntimeError(f"Project creation failed: {resp.text}")
240
+
241
+ def update_project(self, project_id: str, update_fields: Dict[str, Any]) -> Dict[str, Any]:
242
+ """
243
+ Update an existing project.
244
+
245
+ Args:
246
+ project_id (str): The project ID.
247
+ update_fields (dict): Fields to update.
248
+
249
+ Returns:
250
+ dict: Updated project details.
251
+
252
+ Raises:
253
+ RuntimeError: If update fails.
254
+ """
255
+ resp = self._request("put", f"/api/ss/project/{project_id}", json_data=update_fields)
256
+ if resp.status_code in (200, 201):
257
+ return resp.json()
258
+ raise RuntimeError(f"Project update failed: {resp.text}")
259
+
260
+ def create_vulnerability(
261
+ self,
262
+ project_id: str,
263
+ title: str,
264
+ affected_asset_name: str,
265
+ priority: str,
266
+ likelihood_of_exploitation: int,
267
+ description: str,
268
+ attack_scenario: str,
269
+ remediation_recommendation: str,
270
+ steps_to_reproduce: str,
271
+ tags: Optional[list] = None,
272
+ notes: Optional[list] = None,
273
+ is_zeroday: bool = False,
274
+ is_visible: bool = True,
275
+ import_to_library: Optional[str] = None,
276
+ import_source: Optional[str] = None,
277
+ import_source_id: Optional[str] = None,
278
+ custom_fields: Optional[list] = None,
279
+ linked_testcases: Optional[list] = None,
280
+ custom_tags: Optional[list] = None,
281
+ ) -> Dict[str, Any]:
282
+ """
283
+ Create a new security finding (vulnerability) in AttackForge.
284
+
285
+ Args:
286
+ project_id (str): The project ID.
287
+ title (str): The title of the finding.
288
+ affected_asset_name (str): The name of the affected asset.
289
+ priority (str): The priority (e.g., "Critical").
290
+ likelihood_of_exploitation (int): Likelihood of exploitation (e.g., 10).
291
+ description (str): Description of the finding.
292
+ attack_scenario (str): Attack scenario details.
293
+ remediation_recommendation (str): Remediation recommendation.
294
+ steps_to_reproduce (str): Steps to reproduce the finding.
295
+ tags (list, optional): List of tags.
296
+ notes (list, optional): List of notes.
297
+ is_zeroday (bool, optional): Whether this is a zero-day finding.
298
+ is_visible (bool, optional): Whether the finding is visible.
299
+ import_to_library (str, optional): Library to import to.
300
+ import_source (str, optional): Source of import.
301
+ import_source_id (str, optional): Source ID for import.
302
+ custom_fields (list, optional): List of custom fields.
303
+ linked_testcases (list, optional): List of linked testcases.
304
+ custom_tags (list, optional): List of custom tags.
305
+
306
+ Returns:
307
+ dict: Created vulnerability details.
308
+
309
+ Raises:
310
+ ValueError: If any required field is missing.
311
+ RuntimeError: If vulnerability creation fails.
312
+ """
313
+ # Validate required fields
314
+ required_fields = [
315
+ ("project_id", project_id),
316
+ ("title", title),
317
+ ("affected_asset_name", affected_asset_name),
318
+ ("priority", priority),
319
+ ("likelihood_of_exploitation", likelihood_of_exploitation),
320
+ ("description", description),
321
+ ("attack_scenario", attack_scenario),
322
+ ("remediation_recommendation", remediation_recommendation),
323
+ ("steps_to_reproduce", steps_to_reproduce),
324
+ ]
325
+ for field_name, value in required_fields:
326
+ if value is None:
327
+ raise ValueError(f"Missing required field: {field_name}")
328
+
329
+ payload = {
330
+ "projectId": project_id,
331
+ "title": title,
332
+ "affected_asset_name": affected_asset_name,
333
+ "priority": priority,
334
+ "likelihood_of_exploitation": likelihood_of_exploitation,
335
+ "description": description,
336
+ "attack_scenario": attack_scenario,
337
+ "remediation_recommendation": remediation_recommendation,
338
+ "steps_to_reproduce": steps_to_reproduce,
339
+ "tags": tags or [],
340
+ "is_zeroday": is_zeroday,
341
+ "is_visible": is_visible,
342
+ "import_to_library": import_to_library,
343
+ "import_source": import_source,
344
+ "import_source_id": import_source_id,
345
+ "custom_fields": custom_fields or [],
346
+ "linked_testcases": linked_testcases or [],
347
+ "custom_tags": custom_tags or [],
348
+ }
349
+ # Only include notes if it is a non-empty list
350
+ if notes:
351
+ payload["notes"] = notes
352
+
353
+ # Remove None values (for optional fields)
354
+ payload = {k: v for k, v in payload.items() if v is not None}
355
+
356
+ resp = self._request("post", "/api/ss/vulnerability", json_data=payload)
357
+ if resp.status_code in (200, 201):
358
+ return resp.json()
359
+ raise RuntimeError(f"Vulnerability creation failed: {resp.text}")
360
+
361
+
362
+ class DummyResponse:
363
+ """
364
+ Dummy response object for dry-run mode.
365
+ """
366
+ def __init__(self) -> None:
367
+ self.status_code = 200
368
+ self.text = "[DRY RUN] No real API call performed."
369
+
370
+ def json(self) -> Dict[str, Any]:
371
+ return {}
372
+
373
+
374
+ def get_default_dates() -> Tuple[str, str]:
375
+ """
376
+ Get default start and end dates for a project (now and 30 days from now, in ISO format).
377
+
378
+ Returns:
379
+ tuple: (start_date, end_date) as ISO 8601 strings.
380
+ """
381
+ now = datetime.now(timezone.utc)
382
+ start = now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
383
+ end = (now + timedelta(days=30)).isoformat(timespec="milliseconds").replace("+00:00", "Z")
384
+ return start, end
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyattackforge
3
- Version: 0.1.1
3
+ Version: 0.1.7
4
4
  Summary: Python wrapper for the AttackForge API
5
5
  Home-page: https://github.com/Tantalum-Labs/PyAttackForge
6
6
  Author: Shane S
@@ -37,6 +37,10 @@ A lightweight Python library for interacting with the AttackForge API.
37
37
  - Create and fetch projects
38
38
  - Manage assets
39
39
  - Submit vulnerabilities
40
+ - Create findings from existing writeups by passing a `writeup_id`
41
+ - Upload evidence to findings or testcases
42
+ - Update/assign testcases to link findings or add notes
43
+ - Link vulnerabilities to testcases via the client
40
44
  - Dry-run mode for testing
41
45
 
42
46
  ---
@@ -66,13 +70,14 @@ A lightweight Python library for interacting with the AttackForge API.
66
70
  client.create_vulnerability(
67
71
  project_id="abc123",
68
72
  title="Open SSH Port",
69
- affected_asset_name="ssh-prod-1",
73
+ affected_assets=[{"name": "ssh-prod-1"}],
70
74
  priority="High",
71
75
  likelihood_of_exploitation=10,
72
76
  description="SSH port 22 is open to the internet.",
73
77
  attack_scenario="An attacker can brute-force SSH credentials.",
74
78
  remediation_recommendation="Restrict SSH access to trusted IPs.",
75
79
  steps_to_reproduce="1. Scan the host\n2. Observe port 22 is open",
80
+ writeup_id="68e92c7a821c05c8405a8003", # optional: use an existing writeup
76
81
  tags=["ssh", "exposure"],
77
82
  notes=["Observed on 2025-09-09"],
78
83
  is_zeroday=False,
@@ -91,13 +96,14 @@ To create a security finding (vulnerability) in AttackForge, use the `create_vul
91
96
  client.create_vulnerability(
92
97
  project_id="abc123",
93
98
  title="Open SSH Port",
94
- affected_asset_name="ssh-prod-1",
99
+ affected_assets=[{"name": "ssh-prod-1"}],
95
100
  priority="High",
96
101
  likelihood_of_exploitation=10,
97
102
  description="SSH port 22 is open to the internet.",
98
103
  attack_scenario="An attacker can brute-force SSH credentials.",
99
104
  remediation_recommendation="Restrict SSH access to trusted IPs.",
100
105
  steps_to_reproduce="1. Scan the host\n2. Observe port 22 is open",
106
+ writeup_id="68e92c7a821c05c8405a8003", # optional: reuse an existing writeup
101
107
  tags=["ssh", "exposure"],
102
108
  notes=["Observed on 2025-09-09"],
103
109
  is_zeroday=False,
@@ -105,16 +111,111 @@ client.create_vulnerability(
105
111
  )
106
112
  ```
107
113
 
114
+ ### Creating a finding from an existing writeup
115
+
116
+ If you already have a writeup/library entry and just need to create a finding bound to it, you can either pass `writeup_id` to `create_vulnerability` (as above) or call `create_finding_from_writeup` directly. Prefer the 24-character writeup id (`id` / `_id`); if only a numeric `reference_id` is available, use that. You can also specify the library key (e.g., `approved_writeups`, `Main Vulnerabilities`):
117
+
118
+ ```python
119
+ client.create_finding_from_writeup(
120
+ project_id="abc123",
121
+ writeup_id="68e92c7a821c05c8405a8003", # writeup id
122
+ library="approved_writeups", # optional: library key/name
123
+ priority="High",
124
+ affected_assets=[{"name": "ssh-prod-1"}],
125
+ linked_testcases=["5e8017d2e1385f0c58e8f4f8"], # optional: link testcases at creation
126
+ likelihood_of_exploitation=5,
127
+ steps_to_reproduce="1. Do something\n2. Observe result",
128
+ notes=[{"note": "Created via API", "type": "PLAINTEXT"}],
129
+ tags=["automation"]
130
+ )
131
+ ```
132
+
133
+ ### Evidence and testcase helpers
134
+
135
+ Upload evidence to an existing finding:
136
+ ```python
137
+ client.upload_finding_evidence(
138
+ vulnerability_id="6768d29db1782d7362a2df5f",
139
+ file_path="evidence.png"
140
+ )
141
+ ```
142
+
143
+ Upload evidence to a testcase:
144
+ ```python
145
+ client.upload_testcase_evidence(
146
+ project_id="abc123",
147
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
148
+ file_path="testcase-evidence.png"
149
+ )
150
+ ```
151
+
152
+ Add a note to a finding (deduplicates by note text):
153
+ ```python
154
+ client.add_note_to_finding(
155
+ vulnerability_id="6768d29db1782d7362a2df5f",
156
+ note="Observed during retest on 2025-09-19."
157
+ )
158
+ ```
159
+
160
+ Add a note/update to a testcase (PUT to the testcase endpoint):
161
+ ```python
162
+ client.add_note_to_testcase(
163
+ project_id="abc123",
164
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
165
+ note="Observed during retest on 2025-09-19.",
166
+ status="Tested" # optional
167
+ )
168
+ ```
169
+
170
+ Associate findings to a testcase:
171
+ ```python
172
+ client.assign_findings_to_testcase(
173
+ project_id="abc123",
174
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
175
+ vulnerability_ids=["66849b77950ab45e68fc7b48", "6768d29db1782d7362a2df5f"],
176
+ additional_fields={"status": "Tested"} # optional
177
+ )
178
+ ```
179
+ Or link from the vulnerability side using its update endpoint:
180
+ ```python
181
+ client.link_vulnerability_to_testcases(
182
+ vulnerability_id="69273ef0f4a7c85d03930667",
183
+ testcase_ids=["5e8017d2e1385f0c58e8f4f8"],
184
+ project_id="abc123", # optional
185
+ )
186
+ ```
187
+
188
+ Fetch project testcases:
189
+ ```python
190
+ testcases = client.get_testcases("abc123")
191
+ ```
192
+
193
+ Fetch a single testcase (if supported in your tenant):
194
+ ```python
195
+ testcase = client.get_testcase("abc123", "5e8017d2e1385f0c58e8f4f8")
196
+ ```
197
+
198
+ Merge and add findings to a testcase in one call:
199
+ ```python
200
+ client.add_findings_to_testcase(
201
+ project_id="abc123",
202
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
203
+ vulnerability_ids=["69273ef0f4a7c85d03930667"],
204
+ additional_fields={"status": "Tested"} # optional
205
+ )
206
+ ```
207
+
108
208
  **Parameters:**
109
209
  - `project_id` (str): The project ID.
110
210
  - `title` (str): The title of the finding.
111
- - `affected_asset_name` (str): The name of the affected asset.
211
+ - `affected_assets` (list): List of affected assets (e.g., `[{"name": "host1"}]`).
112
212
  - `priority` (str): The priority (e.g., "Critical", "High", "Medium", "Low").
113
213
  - `likelihood_of_exploitation` (int): Likelihood of exploitation (e.g., 10).
114
214
  - `description` (str): Description of the finding.
115
215
  - `attack_scenario` (str): Attack scenario details.
116
216
  - `remediation_recommendation` (str): Remediation recommendation.
117
217
  - `steps_to_reproduce` (str): Steps to reproduce the finding.
218
+ - `writeup_id` (str, optional): Existing writeup/library reference ID to use directly.
118
219
  - `tags` (list, optional): List of tags.
119
220
  - `notes` (list, optional): List of notes.
120
221
  - `is_zeroday` (bool, optional): Whether this is a zero-day finding.
@@ -146,32 +247,7 @@ See the source code for full details and docstrings.
146
247
  - `create_vulnerability(
147
248
  project_id: str,
148
249
  title: str,
149
- affected_asset_name: str,
150
- priority: str,
151
- likelihood_of_exploitation: int,
152
- description: str,
153
- attack_scenario: str,
154
- remediation_recommendation: str,
155
- steps_to_reproduce: str,
156
- tags: Optional[list] = None,
157
- notes: Optional[list] = None,
158
- is_zeroday: bool = False,
159
- is_visible: bool = True,
160
- import_to_library: Optional[str] = None,
161
- import_source: Optional[str] = None,
162
- import_source_id: Optional[str] = None,
163
- custom_fields: Optional[list] = None,
164
- linked_testcases: Optional[list] = None,
165
- custom_tags: Optional[list] = None,
166
- ) -> dict`
167
-
168
- See the source code for full details and docstrings.
169
-
170
- ---
171
- - `create_vulnerability(
172
- project_id: str,
173
- title: str,
174
- affected_asset_name: str,
250
+ affected_assets: list,
175
251
  priority: str,
176
252
  likelihood_of_exploitation: int,
177
253
  description: str,
@@ -188,7 +264,21 @@ See the source code for full details and docstrings.
188
264
  custom_fields: Optional[list] = None,
189
265
  linked_testcases: Optional[list] = None,
190
266
  custom_tags: Optional[list] = None,
267
+ writeup_custom_fields: Optional[list] = None,
191
268
  ) -> dict`
269
+ - `create_finding_from_writeup(project_id: str, writeup_id: str, priority: str, affected_assets: Optional[list] = None, linked_testcases: Optional[list] = None, **kwargs) -> dict`
270
+ - `get_findings_for_project(project_id: str, priority: Optional[str] = None) -> list`
271
+ - `upsert_finding_for_project(...)`
272
+ - `get_vulnerability(vulnerability_id: str) -> dict`
273
+ - `add_note_to_finding(vulnerability_id: str, note: Any, note_type: str = "PLAINTEXT") -> dict`
274
+ - `upload_finding_evidence(vulnerability_id: str, file_path: str) -> dict`
275
+ - `upload_testcase_evidence(project_id: str, testcase_id: str, file_path: str) -> dict`
276
+ - `get_testcases(project_id: str) -> list`
277
+ - `get_testcase(project_id: str, testcase_id: str) -> dict or None`
278
+ - `link_vulnerability_to_testcases(vulnerability_id: str, testcase_ids: List[str], project_id: Optional[str] = None) -> dict`
279
+ - `assign_findings_to_testcase(project_id: str, testcase_id: str, vulnerability_ids: List[str], existing_linked_vulnerabilities: Optional[List[str]] = None, additional_fields: Optional[Dict[str, Any]] = None) -> dict`
280
+ - `add_findings_to_testcase(project_id: str, testcase_id: str, vulnerability_ids: List[str], additional_fields: Optional[Dict[str, Any]] = None) -> dict`
281
+ - `add_note_to_testcase(project_id: str, testcase_id: str, note: str, status: Optional[str] = None) -> dict`
192
282
 
193
283
  See the source code for full details and docstrings.
194
284
 
@@ -0,0 +1,8 @@
1
+ pyattackforge/__init__.py,sha256=r4FT68O03ZuyQJ70RLwlLDqp7AqNTTcMm-FLU4zHwIU,807
2
+ pyattackforge/client.py,sha256=Z-rGVPYpmG8x4N79Izv1Q4sC3-aX40Lf7XdT8kidor8,45464
3
+ pyattackforge/prev_client.py,sha256=Wu-5BVCJbLkWBsxjQmL3nZoyWqAqOACKM9ScXfl6C5o,14277
4
+ pyattackforge-0.1.7.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
5
+ pyattackforge-0.1.7.dist-info/METADATA,sha256=fU2BIgYD4OqcLMIqeLWdtvBfHBf09M_OnAtUkkhnbHc,10754
6
+ pyattackforge-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ pyattackforge-0.1.7.dist-info/top_level.txt,sha256=1rDeMkWvFWuX3MS8V65no7KuybYyvtfIgbYSt5m_uPU,14
8
+ pyattackforge-0.1.7.dist-info/RECORD,,