pyattackforge 0.2.1__py3-none-any.whl → 0.2.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.
pyattackforge/client.py CHANGED
@@ -11,6 +11,7 @@ from .transport import AttackForgeTransport
11
11
  from .resources import (
12
12
  AssetsResource,
13
13
  ProjectsResource,
14
+ GroupsResource,
14
15
  FindingsResource,
15
16
  WriteupsResource,
16
17
  TestcasesResource,
@@ -51,6 +52,7 @@ class AttackForgeClient:
51
52
 
52
53
  self.assets = AssetsResource(self._transport)
53
54
  self.projects = ProjectsResource(self._transport)
55
+ self.groups = GroupsResource(self._transport)
54
56
  self.findings = FindingsResource(self._transport)
55
57
  self.writeups = WriteupsResource(self._transport)
56
58
  self.testcases = TestcasesResource(self._transport)
pyattackforge/config.py CHANGED
@@ -16,7 +16,7 @@ class ClientConfig:
16
16
  timeout: float = 30.0
17
17
  max_retries: int = 3
18
18
  backoff_factor: float = 0.5
19
- user_agent: str = "pyattackforge/0.2.1"
19
+ user_agent: str = "pyattackforge/0.2.3"
20
20
  http2: bool = True
21
21
  # Default visibility for newly created findings. False = pending/hidden.
22
22
  default_findings_visible: bool = False
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .assets import AssetsResource
4
4
  from .projects import ProjectsResource
5
+ from .groups import GroupsResource
5
6
  from .findings import FindingsResource
6
7
  from .writeups import WriteupsResource
7
8
  from .testcases import TestcasesResource
@@ -13,6 +14,7 @@ from .reports import ReportsResource
13
14
  __all__ = [
14
15
  "AssetsResource",
15
16
  "ProjectsResource",
17
+ "GroupsResource",
16
18
  "FindingsResource",
17
19
  "WriteupsResource",
18
20
  "TestcasesResource",
@@ -36,6 +36,9 @@ class FindingsResource(BaseResource):
36
36
  def get_vulnerability(self, vulnerability_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
37
37
  return self._get(f"/api/ss/vulnerability/{vulnerability_id}", params=params)
38
38
 
39
+ def get_vulnerabilities_by_group(self, group_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
40
+ return self._get(f"/api/ss/groups/{group_id}/vulnerabilities", params=params)
41
+
39
42
  def get_project_vulnerabilities(self, project_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
40
43
  return self._get(f"/api/ss/project/{project_id}/vulnerabilities", params=params)
41
44
 
@@ -0,0 +1,35 @@
1
+ """Resource: groups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from .base import BaseResource
8
+
9
+
10
+ class GroupsResource(BaseResource):
11
+ """Groups API resource wrapper."""
12
+
13
+ def create_group(self, payload: Dict[str, Any]) -> Any:
14
+ return self._post("/api/ss/group", json=payload)
15
+
16
+ def get_groups(self, params: Optional[Dict[str, Any]] = None) -> Any:
17
+ return self._get("/api/ss/groups", params=params)
18
+
19
+ def get_group(self, group_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
20
+ return self._get(f"/api/ss/group/{group_id}", params=params)
21
+
22
+ def update_group(self, group_id: str, payload: Dict[str, Any]) -> Any:
23
+ return self._put(f"/api/ss/group/{group_id}", json=payload)
24
+
25
+ def archive_group(self, group_id: str) -> Any:
26
+ return self._put(f"/api/ss/group/{group_id}/archive")
27
+
28
+ def restore_group(self, group_id: str) -> Any:
29
+ return self._put(f"/api/ss/group/{group_id}/restore")
30
+
31
+ def get_group_projects(self, group_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
32
+ return self._get(f"/api/ss/groups/{group_id}/projects", params=params)
33
+
34
+ def get_group_vulnerabilities(self, group_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
35
+ return self._get(f"/api/ss/groups/{group_id}/vulnerabilities", params=params)
@@ -33,6 +33,9 @@ class ProjectsResource(BaseResource):
33
33
  def get_projects(self, params: Optional[Dict[str, Any]] = None) -> Any:
34
34
  return self._get("/api/ss/projects", params=params)
35
35
 
36
+ def get_projects_by_group(self, group_id: str, params: Optional[Dict[str, Any]] = None) -> Any:
37
+ return self._get(f"/api/ss/groups/{group_id}/projects", params=params)
38
+
36
39
  def get_projects_and_vulnerabilities(self, params: Optional[Dict[str, Any]] = None) -> Any:
37
40
  return self._get("/api/ss/projects-and-vulnerabilities", params=params)
38
41
 
@@ -130,6 +133,48 @@ class ProjectsResource(BaseResource):
130
133
  def update_user_access_on_project(self, project_id: str, user_id: str, payload: Dict[str, Any]) -> Any:
131
134
  return self._put(f"/api/ss/project/{project_id}/access/{user_id}", json=payload)
132
135
 
136
+ def add_project_to_group(
137
+ self,
138
+ project_id: str,
139
+ group_id: str,
140
+ *,
141
+ project_data: Optional[Dict[str, Any]] = None,
142
+ ) -> Any:
143
+ """
144
+ Convenience helper to add a project to a group by updating the project's groups list.
145
+ """
146
+ if not group_id:
147
+ raise ValueError("group_id is required")
148
+ data = project_data if project_data is not None else self.get_project(project_id)
149
+ project = data
150
+ if isinstance(project, dict) and isinstance(project.get("data"), dict):
151
+ project = project["data"]
152
+ if isinstance(project, dict) and isinstance(project.get("project"), dict):
153
+ project = project["project"]
154
+ if not isinstance(project, dict):
155
+ raise ValueError("Project response is not a dict; cannot resolve groups")
156
+ groups = project.get("groups") or project.get("project_groups") or []
157
+ group_ids: List[str] = []
158
+ if isinstance(groups, list):
159
+ for entry in groups:
160
+ if isinstance(entry, str) and entry:
161
+ group_ids.append(entry)
162
+ continue
163
+ if isinstance(entry, dict):
164
+ candidate = entry.get("id") or entry.get("group_id") or entry.get("_id")
165
+ if isinstance(candidate, str) and candidate:
166
+ group_ids.append(candidate)
167
+ seen: set = set()
168
+ deduped: List[str] = []
169
+ for value in group_ids:
170
+ if value in seen:
171
+ continue
172
+ seen.add(value)
173
+ deduped.append(value)
174
+ if group_id not in seen:
175
+ deduped.append(group_id)
176
+ return self.update_project(project_id, {"groups": deduped})
177
+
133
178
  def extract_projects_list(self, projects_data: Any) -> List[Dict[str, Any]]:
134
179
  if not isinstance(projects_data, dict):
135
180
  return []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyattackforge
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Python SDK for the AttackForge SSAPI
5
5
  Author: hackman238
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -812,6 +812,25 @@ with AttackForgeClient() as client:
812
812
  client.link_vulnerability_to_testcases("project_id", "vuln_id", ["testcase_id"])
813
813
  ```
814
814
 
815
+ Group management (examples):
816
+
817
+ ```python
818
+ from pyattackforge import AttackForgeClient
819
+
820
+ with AttackForgeClient() as client:
821
+ group = client.groups.create_group({
822
+ "name": "SDK Group",
823
+ "group_owner": "Owner Name",
824
+ "primary_contact_name": "Owner Name",
825
+ "primary_contact_email": "owner@example.com",
826
+ "primary_contact_number": "0000000000",
827
+ })
828
+ group_id = group.get("group_id") or group.get("id")
829
+ client.projects.add_project_to_group("project_id", group_id)
830
+ client.groups.get_group_projects(group_id)
831
+ client.groups.get_group_vulnerabilities(group_id)
832
+ ```
833
+
815
834
  ## Testing
816
835
 
817
836
  Unit tests:
@@ -1,20 +1,21 @@
1
1
  pyattackforge/__init__.py,sha256=ebRH36uyrbMl6C4H9JejeW0cA6S9xTTzScyD69I0Jso,97
2
2
  pyattackforge/cache.py,sha256=zTGWzL4F6n4u6opCNCi0CYkkSWPx5ShZMVmDagMpPQ0,933
3
- pyattackforge/client.py,sha256=E99KuD71q_geRiwMLdcJMUi_hdRJJUu8lPyC6Q-hVAk,5590
4
- pyattackforge/config.py,sha256=tqiE_ZtbpanWRLUcTh0TRprHXSdySAFP-24h6GlIsi8,2240
3
+ pyattackforge/client.py,sha256=C_nCWuQByRr3GwZVIVUjdsrNA5j-Gv0ttkqig1GkwFg,5664
4
+ pyattackforge/config.py,sha256=m6Fpqj8JUGbeWG5SXLIXZR-yO0YYvYxCd3ahWNiH5TI,2240
5
5
  pyattackforge/exceptions.py,sha256=YFzV7VQ3UkOGXKpHhXctuM7fZUtR4LgYuPR_po4pksA,592
6
6
  pyattackforge/transport.py,sha256=slYM9zqleyWlTiUpZZfF0CVB95I0u8UMkqsK09Qa9W4,6500
7
- pyattackforge/resources/__init__.py,sha256=n0C-HZDqBHKfxM7utqKXkGtDES3i-7OzEHcRyUstXHY,589
7
+ pyattackforge/resources/__init__.py,sha256=Tim25zMXM8FoprSyh_r2PJQjouWUq4FxfqMvNT6RI_g,646
8
8
  pyattackforge/resources/assets.py,sha256=cVe4PrLp3Afxv52SBekQZaiDvQpV7zCKN6bLkUHl-uA,1420
9
9
  pyattackforge/resources/base.py,sha256=x5C8SG2MfwCtOkcRi8plDTF6TkJqXz-b58KS4qkP0Zw,1286
10
- pyattackforge/resources/findings.py,sha256=krl-IODoUFQ1pfsx4SkTCuKICdSsp1Zc1MWwrF2iYRM,34147
10
+ pyattackforge/resources/findings.py,sha256=KjCKUqdffL8-5j4I0srxNgpjR3gLbfck_WQKyX04FEE,34341
11
+ pyattackforge/resources/groups.py,sha256=RR1kWW0kBPT6QPoV0pD59X-tnu5ViP_cTjLE9p7ghaA,1358
11
12
  pyattackforge/resources/notes.py,sha256=r-TR3QuatrhGzwmoR7AQt7wiHXTrvmMvm7rYtkBCOkQ,5741
12
- pyattackforge/resources/projects.py,sha256=6pDvOp7s81KvzNgONo04rqWzVXGggbdPXvOnEnBEz9A,7262
13
+ pyattackforge/resources/projects.py,sha256=i9IU4yObv4u4hmcmg7aiXrucsLw966ywhpEBILYx2XE,9201
13
14
  pyattackforge/resources/reports.py,sha256=43wDK8SB2qqz-tjerjFBOV4G9EcgLk5WL3O8H-On5b0,805
14
15
  pyattackforge/resources/users.py,sha256=8SF_3QdNQalsyU-60V_oJtsJbo6nHkI3WPbCBebEkrk,2275
15
16
  pyattackforge/resources/writeups.py,sha256=iBShGQFrwYie3Gx8F0ekSOW0rf_ZzziRDsRpdQYUsSg,3001
16
- pyattackforge-0.2.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
17
- pyattackforge-0.2.1.dist-info/METADATA,sha256=0AaifLGznCHYGlElu6SUJzw-bid2LYmwVjKjn_uKAlU,46131
18
- pyattackforge-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
19
- pyattackforge-0.2.1.dist-info/top_level.txt,sha256=1rDeMkWvFWuX3MS8V65no7KuybYyvtfIgbYSt5m_uPU,14
20
- pyattackforge-0.2.1.dist-info/RECORD,,
17
+ pyattackforge-0.2.3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
18
+ pyattackforge-0.2.3.dist-info/METADATA,sha256=xiUYFVZ9dJcefPjZGtK_yx8OCJ-HvjnNP2FqSr3TR40,46741
19
+ pyattackforge-0.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ pyattackforge-0.2.3.dist-info/top_level.txt,sha256=1rDeMkWvFWuX3MS8V65no7KuybYyvtfIgbYSt5m_uPU,14
21
+ pyattackforge-0.2.3.dist-info/RECORD,,