pyattackforge 0.1.3__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.
- pyattackforge/__init__.py +22 -24
- pyattackforge/client.py +205 -22
- {pyattackforge-0.1.3.dist-info → pyattackforge-0.1.7.dist-info}/METADATA +60 -36
- pyattackforge-0.1.7.dist-info/RECORD +8 -0
- pyattackforge-0.1.3.dist-info/RECORD +0 -8
- {pyattackforge-0.1.3.dist-info → pyattackforge-0.1.7.dist-info}/WHEEL +0 -0
- {pyattackforge-0.1.3.dist-info → pyattackforge-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {pyattackforge-0.1.3.dist-info → pyattackforge-0.1.7.dist-info}/top_level.txt +0 -0
pyattackforge/__init__.py
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PyAttackForge
|
|
3
|
-
|
|
4
|
-
A lightweight Python library for interacting with the AttackForge API.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
GNU Affero General Public License
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
__all__ = ["PyAttackForgeClient"]
|
|
1
|
+
"""
|
|
2
|
+
PyAttackForge
|
|
3
|
+
|
|
4
|
+
A lightweight Python library for interacting with the AttackForge API.
|
|
5
|
+
|
|
6
|
+
PyAttackForge is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
PyAttackForge is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import PyAttackForgeClient
|
|
21
|
+
|
|
22
|
+
__all__ = ["PyAttackForgeClient"]
|
pyattackforge/client.py
CHANGED
|
@@ -31,7 +31,7 @@ class PyAttackForgeClient:
|
|
|
31
31
|
Supports dry-run mode for testing without making real API calls.
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
def upsert_finding_for_project(
|
|
34
|
+
def upsert_finding_for_project( # noqa: C901
|
|
35
35
|
self,
|
|
36
36
|
project_id: str,
|
|
37
37
|
title: str,
|
|
@@ -87,20 +87,29 @@ class PyAttackForgeClient:
|
|
|
87
87
|
asset_names = []
|
|
88
88
|
for asset in affected_assets:
|
|
89
89
|
name = asset["name"] if isinstance(asset, dict) and "name" in asset else asset
|
|
90
|
-
|
|
91
|
-
#if not asset_obj:
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
90
|
+
self.get_asset_by_name(name)
|
|
91
|
+
# if not asset_obj:
|
|
92
|
+
# try:
|
|
93
|
+
# asset_obj = self.create_asset({"name": name})
|
|
94
|
+
# except Exception as e:
|
|
95
|
+
# raise RuntimeError(f"Asset '{name}' does not exist and could not be created: {e}")
|
|
96
96
|
asset_names.append(name)
|
|
97
97
|
|
|
98
98
|
# Fetch all findings for the project
|
|
99
99
|
findings = self.get_findings_for_project(project_id)
|
|
100
|
-
|
|
100
|
+
logger.debug(
|
|
101
|
+
"Found %s findings for project %s",
|
|
102
|
+
len(findings),
|
|
103
|
+
project_id
|
|
104
|
+
)
|
|
101
105
|
for f in findings:
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
logger.debug(
|
|
107
|
+
"Finding id=%s title=%s steps=%s",
|
|
108
|
+
f.get("vulnerability_id"),
|
|
109
|
+
f.get("vulnerability_title"),
|
|
110
|
+
f.get("vulnerability_steps_to_reproduce"),
|
|
111
|
+
)
|
|
112
|
+
logger.debug("Finding payload: %s", f)
|
|
104
113
|
match = None
|
|
105
114
|
for f in findings:
|
|
106
115
|
if f.get("vulnerability_title") == title:
|
|
@@ -293,6 +302,88 @@ class PyAttackForgeClient:
|
|
|
293
302
|
raise RuntimeError(f"Failed to add note: {resp.text}")
|
|
294
303
|
return resp.json()
|
|
295
304
|
|
|
305
|
+
def link_vulnerability_to_testcases(
|
|
306
|
+
self,
|
|
307
|
+
vulnerability_id: str,
|
|
308
|
+
testcase_ids: List[str],
|
|
309
|
+
project_id: Optional[str] = None,
|
|
310
|
+
) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Link a vulnerability to one or more testcases.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
vulnerability_id (str): The vulnerability ID.
|
|
316
|
+
testcase_ids (list): List of testcase IDs to link.
|
|
317
|
+
project_id (str, optional): Project ID if required by the API.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
dict: API response.
|
|
321
|
+
"""
|
|
322
|
+
if not vulnerability_id:
|
|
323
|
+
raise ValueError("Missing required field: vulnerability_id")
|
|
324
|
+
if not testcase_ids:
|
|
325
|
+
raise ValueError("testcase_ids must contain at least one ID")
|
|
326
|
+
payload: Dict[str, Any] = {
|
|
327
|
+
"linked_testcases": testcase_ids,
|
|
328
|
+
}
|
|
329
|
+
if project_id:
|
|
330
|
+
payload["project_id"] = project_id
|
|
331
|
+
resp = self._request(
|
|
332
|
+
"put",
|
|
333
|
+
f"/api/ss/vulnerability/{vulnerability_id}",
|
|
334
|
+
json_data=payload,
|
|
335
|
+
)
|
|
336
|
+
if resp.status_code not in (200, 201):
|
|
337
|
+
raise RuntimeError(f"Failed to link vulnerability to testcases: {resp.text}")
|
|
338
|
+
return resp.json()
|
|
339
|
+
|
|
340
|
+
def get_testcases(self, project_id: str) -> List[Dict[str, Any]]:
|
|
341
|
+
"""
|
|
342
|
+
Retrieve testcases for a project.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
project_id (str): Project ID.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
list: List of testcase dicts.
|
|
349
|
+
"""
|
|
350
|
+
if not project_id:
|
|
351
|
+
raise ValueError("Missing required field: project_id")
|
|
352
|
+
resp = self._request("get", f"/api/ss/project/{project_id}/testcases")
|
|
353
|
+
if resp.status_code not in (200, 201):
|
|
354
|
+
raise RuntimeError(f"Failed to fetch testcases: {resp.text}")
|
|
355
|
+
data = resp.json()
|
|
356
|
+
if isinstance(data, dict) and "testcases" in data:
|
|
357
|
+
return data.get("testcases", [])
|
|
358
|
+
if isinstance(data, list):
|
|
359
|
+
return data
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
def get_testcase(self, project_id: str, testcase_id: str) -> Optional[Dict[str, Any]]:
|
|
363
|
+
"""
|
|
364
|
+
Retrieve a single testcase by ID.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
project_id (str): Project ID.
|
|
368
|
+
testcase_id (str): Testcase ID.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
dict or None: Testcase details if found, else None.
|
|
372
|
+
"""
|
|
373
|
+
if not project_id:
|
|
374
|
+
raise ValueError("Missing required field: project_id")
|
|
375
|
+
if not testcase_id:
|
|
376
|
+
raise ValueError("Missing required field: testcase_id")
|
|
377
|
+
resp = self._request("get", f"/api/ss/project/{project_id}/testcase/{testcase_id}")
|
|
378
|
+
if resp.status_code == 404:
|
|
379
|
+
return None
|
|
380
|
+
if resp.status_code not in (200, 201):
|
|
381
|
+
raise RuntimeError(f"Failed to fetch testcase: {resp.text}")
|
|
382
|
+
data = resp.json()
|
|
383
|
+
if isinstance(data, dict) and "testcase" in data:
|
|
384
|
+
return data["testcase"]
|
|
385
|
+
return data if isinstance(data, dict) else None
|
|
386
|
+
|
|
296
387
|
def upload_finding_evidence(self, vulnerability_id: str, file_path: str) -> Dict[str, Any]:
|
|
297
388
|
"""
|
|
298
389
|
Upload evidence to a finding/vulnerability.
|
|
@@ -363,6 +454,47 @@ class PyAttackForgeClient:
|
|
|
363
454
|
raise RuntimeError(f"Testcase evidence upload failed: {resp.text}")
|
|
364
455
|
return resp.json()
|
|
365
456
|
|
|
457
|
+
def add_note_to_testcase(
|
|
458
|
+
self,
|
|
459
|
+
project_id: str,
|
|
460
|
+
testcase_id: str,
|
|
461
|
+
note: str,
|
|
462
|
+
status: Optional[str] = None
|
|
463
|
+
) -> Dict[str, Any]:
|
|
464
|
+
"""
|
|
465
|
+
Create a testcase note via the dedicated note endpoint, optionally updating status via update_testcase.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
project_id (str): Project ID.
|
|
469
|
+
testcase_id (str): Testcase ID.
|
|
470
|
+
note (str): Note text to set in the details field.
|
|
471
|
+
status (str, optional): Status to set (e.g., "Tested").
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
dict: API response.
|
|
475
|
+
"""
|
|
476
|
+
if not project_id:
|
|
477
|
+
raise ValueError("Missing required field: project_id")
|
|
478
|
+
if not testcase_id:
|
|
479
|
+
raise ValueError("Missing required field: testcase_id")
|
|
480
|
+
if not note:
|
|
481
|
+
raise ValueError("Missing required field: note")
|
|
482
|
+
endpoint = f"/api/ss/project/{project_id}/testcase/{testcase_id}/note"
|
|
483
|
+
payload: Dict[str, Any] = {"note": note, "note_type": "PLAINTEXT"}
|
|
484
|
+
resp = self._request("post", endpoint, json_data=payload)
|
|
485
|
+
if resp.status_code not in (200, 201):
|
|
486
|
+
raise RuntimeError(f"Failed to add testcase note: {resp.text}")
|
|
487
|
+
result = resp.json()
|
|
488
|
+
|
|
489
|
+
# Optionally update status using update_testcase
|
|
490
|
+
if status:
|
|
491
|
+
try:
|
|
492
|
+
self.update_testcase(project_id, testcase_id, {"status": status})
|
|
493
|
+
except Exception:
|
|
494
|
+
# If status update fails, still return note creation response
|
|
495
|
+
pass
|
|
496
|
+
return result
|
|
497
|
+
|
|
366
498
|
def assign_findings_to_testcase(
|
|
367
499
|
self,
|
|
368
500
|
project_id: str,
|
|
@@ -429,6 +561,53 @@ class PyAttackForgeClient:
|
|
|
429
561
|
raise RuntimeError(f"Failed to update testcase: {resp.text}")
|
|
430
562
|
return resp.json()
|
|
431
563
|
|
|
564
|
+
def add_findings_to_testcase(
|
|
565
|
+
self,
|
|
566
|
+
project_id: str,
|
|
567
|
+
testcase_id: str,
|
|
568
|
+
vulnerability_ids: List[str],
|
|
569
|
+
additional_fields: Optional[Dict[str, Any]] = None
|
|
570
|
+
) -> Dict[str, Any]:
|
|
571
|
+
"""
|
|
572
|
+
Fetch a testcase, merge existing linked vulnerabilities with the provided list, and update it.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
project_id (str): The project ID.
|
|
576
|
+
testcase_id (str): The testcase ID.
|
|
577
|
+
vulnerability_ids (list): List of vulnerability IDs to add.
|
|
578
|
+
additional_fields (dict, optional): Extra fields to include (e.g., status).
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
dict: API response from the update.
|
|
582
|
+
"""
|
|
583
|
+
if not project_id:
|
|
584
|
+
raise ValueError("Missing required field: project_id")
|
|
585
|
+
if not testcase_id:
|
|
586
|
+
raise ValueError("Missing required field: testcase_id")
|
|
587
|
+
if not vulnerability_ids:
|
|
588
|
+
raise ValueError("vulnerability_ids must contain at least one ID")
|
|
589
|
+
|
|
590
|
+
testcases = self.get_testcases(project_id)
|
|
591
|
+
testcase = next((t for t in testcases if t.get("id") == testcase_id), None)
|
|
592
|
+
if not testcase:
|
|
593
|
+
raise RuntimeError(f"Testcase '{testcase_id}' not found in project '{project_id}'")
|
|
594
|
+
|
|
595
|
+
existing_raw = testcase.get("linked_vulnerabilities", []) or []
|
|
596
|
+
existing_ids: List[str] = []
|
|
597
|
+
for item in existing_raw:
|
|
598
|
+
if isinstance(item, dict) and item.get("id"):
|
|
599
|
+
existing_ids.append(item["id"])
|
|
600
|
+
elif isinstance(item, str):
|
|
601
|
+
existing_ids.append(item)
|
|
602
|
+
|
|
603
|
+
return self.assign_findings_to_testcase(
|
|
604
|
+
project_id=project_id,
|
|
605
|
+
testcase_id=testcase_id,
|
|
606
|
+
vulnerability_ids=vulnerability_ids,
|
|
607
|
+
existing_linked_vulnerabilities=existing_ids,
|
|
608
|
+
additional_fields=additional_fields,
|
|
609
|
+
)
|
|
610
|
+
|
|
432
611
|
def __init__(self, api_key: str, base_url: str = "https://demo.attackforge.com", dry_run: bool = False):
|
|
433
612
|
"""
|
|
434
613
|
Initialize the PyAttackForgeClient.
|
|
@@ -550,14 +729,14 @@ class PyAttackForgeClient:
|
|
|
550
729
|
|
|
551
730
|
def create_asset(self, asset_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
552
731
|
pass
|
|
553
|
-
#resp = self._request("post", "/api/ss/library/asset", json_data=asset_data)
|
|
554
|
-
#if resp.status_code in (200, 201):
|
|
555
|
-
#
|
|
556
|
-
#
|
|
557
|
-
#
|
|
558
|
-
#if "Asset Already Exists" in resp.text:
|
|
559
|
-
#
|
|
560
|
-
#raise RuntimeError(f"Asset creation failed: {resp.text}")
|
|
732
|
+
# resp = self._request("post", "/api/ss/library/asset", json_data=asset_data)
|
|
733
|
+
# if resp.status_code in (200, 201):
|
|
734
|
+
# asset = resp.json()
|
|
735
|
+
# self._asset_cache = None # Invalidate cache
|
|
736
|
+
# return asset
|
|
737
|
+
# if "Asset Already Exists" in resp.text:
|
|
738
|
+
# return self.get_asset_by_name(asset_data["name"])
|
|
739
|
+
# raise RuntimeError(f"Asset creation failed: {resp.text}")
|
|
561
740
|
|
|
562
741
|
def get_project_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
|
563
742
|
params = {
|
|
@@ -653,6 +832,7 @@ class PyAttackForgeClient:
|
|
|
653
832
|
writeup_id: str,
|
|
654
833
|
priority: str,
|
|
655
834
|
affected_assets: Optional[list] = None,
|
|
835
|
+
linked_testcases: Optional[list] = None,
|
|
656
836
|
**kwargs
|
|
657
837
|
) -> Dict[str, Any]:
|
|
658
838
|
"""
|
|
@@ -663,6 +843,7 @@ class PyAttackForgeClient:
|
|
|
663
843
|
writeup_id (str): The writeup/library ID.
|
|
664
844
|
priority (str): The priority.
|
|
665
845
|
affected_assets (list, optional): List of affected asset objects or names.
|
|
846
|
+
linked_testcases (list, optional): List of testcase IDs to link.
|
|
666
847
|
**kwargs: Additional fields.
|
|
667
848
|
|
|
668
849
|
Returns:
|
|
@@ -684,6 +865,8 @@ class PyAttackForgeClient:
|
|
|
684
865
|
for asset in affected_assets
|
|
685
866
|
]
|
|
686
867
|
payload["affected_assets"] = [{"assetName": n} for n in asset_names]
|
|
868
|
+
if linked_testcases:
|
|
869
|
+
payload["linked_testcases"] = linked_testcases
|
|
687
870
|
payload.update(kwargs)
|
|
688
871
|
resp = self._request("post", "/api/ss/vulnerability-with-library", json_data=payload)
|
|
689
872
|
if resp.status_code in (200, 201):
|
|
@@ -749,9 +932,9 @@ class PyAttackForgeClient:
|
|
|
749
932
|
name = asset["assetName"] if isinstance(asset, dict) and "assetName" in asset \
|
|
750
933
|
else asset["name"] if isinstance(asset, dict) and "name" in asset \
|
|
751
934
|
else asset
|
|
752
|
-
|
|
753
|
-
#if not asset_obj:
|
|
754
|
-
#
|
|
935
|
+
self.get_asset_by_name(name)
|
|
936
|
+
# if not asset_obj:
|
|
937
|
+
# asset_obj = self.create_asset({"name": name})
|
|
755
938
|
asset_names.append(name)
|
|
756
939
|
# Ensure all assets are in project scope
|
|
757
940
|
scope = self.get_project_scope(project_id)
|
|
@@ -808,7 +991,6 @@ class PyAttackForgeClient:
|
|
|
808
991
|
)
|
|
809
992
|
return result
|
|
810
993
|
|
|
811
|
-
|
|
812
994
|
def create_vulnerability_old(
|
|
813
995
|
self,
|
|
814
996
|
project_id: str,
|
|
@@ -906,6 +1088,7 @@ class PyAttackForgeClient:
|
|
|
906
1088
|
return resp.json()
|
|
907
1089
|
raise RuntimeError(f"Vulnerability creation failed: {resp.text}")
|
|
908
1090
|
|
|
1091
|
+
|
|
909
1092
|
class DummyResponse:
|
|
910
1093
|
def __init__(self) -> None:
|
|
911
1094
|
self.status_code = 200
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyattackforge
|
|
3
|
-
Version: 0.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
|
|
@@ -40,6 +40,7 @@ A lightweight Python library for interacting with the AttackForge API.
|
|
|
40
40
|
- Create findings from existing writeups by passing a `writeup_id`
|
|
41
41
|
- Upload evidence to findings or testcases
|
|
42
42
|
- Update/assign testcases to link findings or add notes
|
|
43
|
+
- Link vulnerabilities to testcases via the client
|
|
43
44
|
- Dry-run mode for testing
|
|
44
45
|
|
|
45
46
|
---
|
|
@@ -112,14 +113,20 @@ client.create_vulnerability(
|
|
|
112
113
|
|
|
113
114
|
### Creating a finding from an existing writeup
|
|
114
115
|
|
|
115
|
-
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:
|
|
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`):
|
|
116
117
|
|
|
117
118
|
```python
|
|
118
119
|
client.create_finding_from_writeup(
|
|
119
120
|
project_id="abc123",
|
|
120
|
-
writeup_id="68e92c7a821c05c8405a8003",
|
|
121
|
+
writeup_id="68e92c7a821c05c8405a8003", # writeup id
|
|
122
|
+
library="approved_writeups", # optional: library key/name
|
|
121
123
|
priority="High",
|
|
122
|
-
affected_assets=[{"name": "ssh-prod-1"}]
|
|
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"]
|
|
123
130
|
)
|
|
124
131
|
```
|
|
125
132
|
|
|
@@ -152,21 +159,49 @@ client.add_note_to_finding(
|
|
|
152
159
|
|
|
153
160
|
Add a note/update to a testcase (PUT to the testcase endpoint):
|
|
154
161
|
```python
|
|
155
|
-
client.
|
|
162
|
+
client.add_note_to_testcase(
|
|
156
163
|
project_id="abc123",
|
|
157
164
|
testcase_id="5e8017d2e1385f0c58e8f4f8",
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
165
|
+
note="Observed during retest on 2025-09-19.",
|
|
166
|
+
status="Tested" # optional
|
|
161
167
|
)
|
|
162
168
|
```
|
|
163
169
|
|
|
164
|
-
Associate findings to a testcase
|
|
170
|
+
Associate findings to a testcase:
|
|
165
171
|
```python
|
|
166
172
|
client.assign_findings_to_testcase(
|
|
167
173
|
project_id="abc123",
|
|
168
174
|
testcase_id="5e8017d2e1385f0c58e8f4f8",
|
|
169
|
-
vulnerability_ids=["66849b77950ab45e68fc7b48", "6768d29db1782d7362a2df5f"]
|
|
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
|
|
170
205
|
)
|
|
171
206
|
```
|
|
172
207
|
|
|
@@ -212,32 +247,7 @@ See the source code for full details and docstrings.
|
|
|
212
247
|
- `create_vulnerability(
|
|
213
248
|
project_id: str,
|
|
214
249
|
title: str,
|
|
215
|
-
|
|
216
|
-
priority: str,
|
|
217
|
-
likelihood_of_exploitation: int,
|
|
218
|
-
description: str,
|
|
219
|
-
attack_scenario: str,
|
|
220
|
-
remediation_recommendation: str,
|
|
221
|
-
steps_to_reproduce: str,
|
|
222
|
-
tags: Optional[list] = None,
|
|
223
|
-
notes: Optional[list] = None,
|
|
224
|
-
is_zeroday: bool = False,
|
|
225
|
-
is_visible: bool = True,
|
|
226
|
-
import_to_library: Optional[str] = None,
|
|
227
|
-
import_source: Optional[str] = None,
|
|
228
|
-
import_source_id: Optional[str] = None,
|
|
229
|
-
custom_fields: Optional[list] = None,
|
|
230
|
-
linked_testcases: Optional[list] = None,
|
|
231
|
-
custom_tags: Optional[list] = None,
|
|
232
|
-
) -> dict`
|
|
233
|
-
|
|
234
|
-
See the source code for full details and docstrings.
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
- `create_vulnerability(
|
|
238
|
-
project_id: str,
|
|
239
|
-
title: str,
|
|
240
|
-
affected_asset_name: str,
|
|
250
|
+
affected_assets: list,
|
|
241
251
|
priority: str,
|
|
242
252
|
likelihood_of_exploitation: int,
|
|
243
253
|
description: str,
|
|
@@ -254,7 +264,21 @@ See the source code for full details and docstrings.
|
|
|
254
264
|
custom_fields: Optional[list] = None,
|
|
255
265
|
linked_testcases: Optional[list] = None,
|
|
256
266
|
custom_tags: Optional[list] = None,
|
|
267
|
+
writeup_custom_fields: Optional[list] = None,
|
|
257
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`
|
|
258
282
|
|
|
259
283
|
See the source code for full details and docstrings.
|
|
260
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,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pyattackforge/__init__.py,sha256=xZxubdjv_Fc1bRqfVRMR4j9SCTLXg9TfckWn9CQHH_E,839
|
|
2
|
-
pyattackforge/client.py,sha256=gPd5CwP714NKe0IwhmKUrCMklQg1ARVOfcB4ibAne7o,38753
|
|
3
|
-
pyattackforge/prev_client.py,sha256=Wu-5BVCJbLkWBsxjQmL3nZoyWqAqOACKM9ScXfl6C5o,14277
|
|
4
|
-
pyattackforge-0.1.3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
5
|
-
pyattackforge-0.1.3.dist-info/METADATA,sha256=xzIKr5FGJ37Enc5QaCPyiVj0wFlgyUdR63Wq_u_M9sE,8770
|
|
6
|
-
pyattackforge-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
pyattackforge-0.1.3.dist-info/top_level.txt,sha256=1rDeMkWvFWuX3MS8V65no7KuybYyvtfIgbYSt5m_uPU,14
|
|
8
|
-
pyattackforge-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|