pyattackforge 0.1.0__py3-none-any.whl → 0.1.3__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
@@ -0,0 +1,281 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyattackforge
3
+ Version: 0.1.3
4
+ Summary: Python wrapper for the AttackForge API
5
+ Home-page: https://github.com/Tantalum-Labs/PyAttackForge
6
+ Author: Shane S
7
+ License: AGPL-3.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.7
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests>=2.20.0
18
+ Dynamic: author
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+ Dynamic: requires-python
27
+ Dynamic: summary
28
+
29
+ # PyAttackForge
30
+
31
+ A lightweight Python library for interacting with the AttackForge API.
32
+
33
+ ---
34
+
35
+ ## Features
36
+
37
+ - Create and fetch projects
38
+ - Manage assets
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
+ - Dry-run mode for testing
44
+
45
+ ---
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ mkdir PyAttackForgeEnv
51
+ cd PyAttackForgeEnv
52
+ virtualenv venv
53
+ source ./venv/bin/activate
54
+ pip install git+https://github.com/Tantalum-Labs/PyAttackForge.git
55
+ ```
56
+
57
+ ## Use
58
+
59
+ ```python
60
+ from pyattackforge import PyAttackForgeClient
61
+
62
+ # Initialize client - Note: Make sure to set your AttackForge URL and API Key
63
+ client = PyAttackForgeClient(api_key="your-api-key", base_url="https://demo.attackforge.com", dry_run=False)
64
+
65
+ # Create a project
66
+ project = client.create_project("My Project", scope=["Asset1", "Asset2"])
67
+
68
+ ## Create a security finding (vulnerability)
69
+ client.create_vulnerability(
70
+ project_id="abc123",
71
+ title="Open SSH Port",
72
+ affected_assets=[{"name": "ssh-prod-1"}],
73
+ priority="High",
74
+ likelihood_of_exploitation=10,
75
+ description="SSH port 22 is open to the internet.",
76
+ attack_scenario="An attacker can brute-force SSH credentials.",
77
+ remediation_recommendation="Restrict SSH access to trusted IPs.",
78
+ steps_to_reproduce="1. Scan the host\n2. Observe port 22 is open",
79
+ writeup_id="68e92c7a821c05c8405a8003", # optional: use an existing writeup
80
+ tags=["ssh", "exposure"],
81
+ notes=["Observed on 2025-09-09"],
82
+ is_zeroday=False,
83
+ is_visible=True
84
+ )
85
+
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Creating Security Findings
91
+
92
+ To create a security finding (vulnerability) in AttackForge, use the `create_vulnerability` method:
93
+
94
+ ```python
95
+ client.create_vulnerability(
96
+ project_id="abc123",
97
+ title="Open SSH Port",
98
+ affected_assets=[{"name": "ssh-prod-1"}],
99
+ priority="High",
100
+ likelihood_of_exploitation=10,
101
+ description="SSH port 22 is open to the internet.",
102
+ attack_scenario="An attacker can brute-force SSH credentials.",
103
+ remediation_recommendation="Restrict SSH access to trusted IPs.",
104
+ steps_to_reproduce="1. Scan the host\n2. Observe port 22 is open",
105
+ writeup_id="68e92c7a821c05c8405a8003", # optional: reuse an existing writeup
106
+ tags=["ssh", "exposure"],
107
+ notes=["Observed on 2025-09-09"],
108
+ is_zeroday=False,
109
+ is_visible=True
110
+ )
111
+ ```
112
+
113
+ ### Creating a finding from an existing writeup
114
+
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
+
117
+ ```python
118
+ client.create_finding_from_writeup(
119
+ project_id="abc123",
120
+ writeup_id="68e92c7a821c05c8405a8003",
121
+ priority="High",
122
+ affected_assets=[{"name": "ssh-prod-1"}]
123
+ )
124
+ ```
125
+
126
+ ### Evidence and testcase helpers
127
+
128
+ Upload evidence to an existing finding:
129
+ ```python
130
+ client.upload_finding_evidence(
131
+ vulnerability_id="6768d29db1782d7362a2df5f",
132
+ file_path="evidence.png"
133
+ )
134
+ ```
135
+
136
+ Upload evidence to a testcase:
137
+ ```python
138
+ client.upload_testcase_evidence(
139
+ project_id="abc123",
140
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
141
+ file_path="testcase-evidence.png"
142
+ )
143
+ ```
144
+
145
+ Add a note to a finding (deduplicates by note text):
146
+ ```python
147
+ client.add_note_to_finding(
148
+ vulnerability_id="6768d29db1782d7362a2df5f",
149
+ note="Observed during retest on 2025-09-19."
150
+ )
151
+ ```
152
+
153
+ Add a note/update to a testcase (PUT to the testcase endpoint):
154
+ ```python
155
+ client.update_testcase(
156
+ project_id="abc123",
157
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
158
+ update_fields={
159
+ "details": "Observed during retest on 2025-09-19."
160
+ }
161
+ )
162
+ ```
163
+
164
+ Associate findings to a testcase (merges with existing linked vulnerabilities if provided):
165
+ ```python
166
+ client.assign_findings_to_testcase(
167
+ project_id="abc123",
168
+ testcase_id="5e8017d2e1385f0c58e8f4f8",
169
+ vulnerability_ids=["66849b77950ab45e68fc7b48", "6768d29db1782d7362a2df5f"]
170
+ )
171
+ ```
172
+
173
+ **Parameters:**
174
+ - `project_id` (str): The project ID.
175
+ - `title` (str): The title of the finding.
176
+ - `affected_assets` (list): List of affected assets (e.g., `[{"name": "host1"}]`).
177
+ - `priority` (str): The priority (e.g., "Critical", "High", "Medium", "Low").
178
+ - `likelihood_of_exploitation` (int): Likelihood of exploitation (e.g., 10).
179
+ - `description` (str): Description of the finding.
180
+ - `attack_scenario` (str): Attack scenario details.
181
+ - `remediation_recommendation` (str): Remediation recommendation.
182
+ - `steps_to_reproduce` (str): Steps to reproduce the finding.
183
+ - `writeup_id` (str, optional): Existing writeup/library reference ID to use directly.
184
+ - `tags` (list, optional): List of tags.
185
+ - `notes` (list, optional): List of notes.
186
+ - `is_zeroday` (bool, optional): Whether this is a zero-day finding.
187
+ - `is_visible` (bool, optional): Whether the finding is visible.
188
+ - `import_to_library` (str, optional): Library to import to.
189
+ - `import_source` (str, optional): Source of import.
190
+ - `import_source_id` (str, optional): Source ID for import.
191
+ - `custom_fields` (list, optional): List of custom fields.
192
+ - `linked_testcases` (list, optional): List of linked testcases.
193
+ - `custom_tags` (list, optional): List of custom tags.
194
+
195
+ See the source code for full details and docstrings.
196
+
197
+ ---
198
+
199
+ ## API Reference
200
+
201
+ ### `PyAttackForgeClient`
202
+
203
+ - `__init__(api_key: str, base_url: str = ..., dry_run: bool = False)`
204
+ - `get_assets() -> dict`
205
+ - `get_asset_by_name(name: str) -> dict or None`
206
+ - `create_asset(asset_data: dict) -> dict`
207
+ - `get_project_by_name(name: str) -> dict or None`
208
+ - `get_project_scope(project_id: str) -> set`
209
+ - `update_project_scope(project_id: str, new_assets: list) -> dict`
210
+ - `create_project(name: str, **kwargs) -> dict`
211
+ - `update_project(project_id: str, update_fields: dict) -> dict`
212
+ - `create_vulnerability(
213
+ project_id: str,
214
+ title: str,
215
+ affected_asset_name: str,
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,
241
+ priority: str,
242
+ likelihood_of_exploitation: int,
243
+ description: str,
244
+ attack_scenario: str,
245
+ remediation_recommendation: str,
246
+ steps_to_reproduce: str,
247
+ tags: Optional[list] = None,
248
+ notes: Optional[list] = None,
249
+ is_zeroday: bool = False,
250
+ is_visible: bool = True,
251
+ import_to_library: Optional[str] = None,
252
+ import_source: Optional[str] = None,
253
+ import_source_id: Optional[str] = None,
254
+ custom_fields: Optional[list] = None,
255
+ linked_testcases: Optional[list] = None,
256
+ custom_tags: Optional[list] = None,
257
+ ) -> dict`
258
+
259
+ See the source code for full details and docstrings.
260
+
261
+ ---
262
+
263
+ ## Contributing
264
+
265
+ Contributions are welcome! Please open issues or submit pull requests via GitHub.
266
+
267
+ - Ensure code is PEP8-compliant and includes docstrings and type hints.
268
+ - Add or update tests for new features or bugfixes.
269
+ - Do **not** commit API keys or other secrets.
270
+
271
+ ---
272
+
273
+ ## Security
274
+
275
+ **Never commit your API keys or other sensitive information to version control.**
276
+
277
+ ---
278
+
279
+ ## License
280
+
281
+ This project is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html).
@@ -0,0 +1,8 @@
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,,