regscale-cli 6.16.3.0__py3-none-any.whl → 6.16.4.1__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.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (42) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/internal/control_editor.py +26 -2
  3. regscale/core/app/internal/model_editor.py +39 -26
  4. regscale/integrations/commercial/grype/scanner.py +37 -29
  5. regscale/integrations/commercial/opentext/commands.py +2 -0
  6. regscale/integrations/commercial/opentext/scanner.py +45 -31
  7. regscale/integrations/commercial/qualys.py +3 -1
  8. regscale/integrations/commercial/sicura/commands.py +9 -14
  9. regscale/integrations/commercial/tenablev2/click.py +25 -13
  10. regscale/integrations/commercial/tenablev2/scanner.py +12 -3
  11. regscale/integrations/commercial/trivy/scanner.py +14 -6
  12. regscale/integrations/commercial/wizv2/click.py +15 -37
  13. regscale/integrations/jsonl_scanner_integration.py +120 -16
  14. regscale/integrations/public/fedramp/click.py +8 -8
  15. regscale/integrations/public/fedramp/fedramp_cis_crm.py +499 -106
  16. regscale/integrations/public/fedramp/ssp_logger.py +2 -9
  17. regscale/integrations/scanner_integration.py +17 -11
  18. regscale/models/integration_models/cisa_kev_data.json +39 -8
  19. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  20. regscale/models/integration_models/tenable_models/integration.py +23 -3
  21. regscale/models/regscale_models/control_implementation.py +18 -0
  22. regscale/models/regscale_models/control_objective.py +2 -1
  23. regscale/models/regscale_models/facility.py +10 -26
  24. regscale/models/regscale_models/functional_roles.py +38 -0
  25. regscale/models/regscale_models/issue.py +3 -1
  26. regscale/models/regscale_models/parameter.py +21 -3
  27. regscale/models/regscale_models/profile.py +22 -0
  28. regscale/models/regscale_models/profile_mapping.py +48 -3
  29. regscale/models/regscale_models/regscale_model.py +2 -0
  30. regscale/models/regscale_models/risk.py +38 -30
  31. regscale/models/regscale_models/security_plan.py +1 -0
  32. regscale/models/regscale_models/supply_chain.py +1 -1
  33. regscale/models/regscale_models/user.py +16 -2
  34. regscale/utils/threading/__init__.py +1 -0
  35. regscale/utils/threading/threadsafe_list.py +10 -0
  36. regscale/utils/threading/threadsafe_set.py +116 -0
  37. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.1.dist-info}/METADATA +1 -1
  38. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.1.dist-info}/RECORD +42 -40
  39. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.1.dist-info}/LICENSE +0 -0
  40. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.1.dist-info}/WHEEL +0 -0
  41. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.1.dist-info}/entry_points.txt +0 -0
  42. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.1.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,12 @@ from typing import Any, Iterator, List, Optional, Tuple
9
9
  from regscale.core.app.utils.app_utils import epoch_to_datetime
10
10
  from regscale.integrations.commercial.tenablev2.utils import get_filtered_severities
11
11
  from regscale.integrations.integration_override import IntegrationOverride
12
- from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding, ScannerIntegration
12
+ from regscale.integrations.scanner_integration import (
13
+ IntegrationAsset,
14
+ IntegrationFinding,
15
+ ScannerIntegration,
16
+ issue_due_date,
17
+ )
13
18
  from regscale.models import regscale_models
14
19
  from regscale.models.integration_models.tenable_models.models import TenableAsset
15
20
 
@@ -32,6 +37,16 @@ class SCIntegration(ScannerIntegration):
32
37
  title = "Tenable SC"
33
38
  asset_identifier_field = "tenableId"
34
39
 
40
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
41
+ """
42
+ Initializes the SCIntegration class
43
+
44
+ :param Tuple args: Additional arguments
45
+ :param dict kwargs: Additional keyword arguments
46
+ """
47
+ super().__init__(*args, **kwargs)
48
+ self.scan_date = kwargs.get("scan_date")
49
+
35
50
  def fetch_assets(self, *args: Any, **kwargs: Any) -> Iterator[IntegrationAsset]:
36
51
  """
37
52
  Fetches assets from SCIntegration
@@ -138,6 +153,7 @@ class SCIntegration(ScannerIntegration):
138
153
  fixed_versions = re.findall(r"Fixed version\s*:\s*(.+)", vuln.pluginText)
139
154
  fixed_versions_str = ", ".join(fixed_versions)
140
155
 
156
+ first_seen = epoch_to_datetime(vuln.firstSeen) if vuln.firstSeen else self.scan_date
141
157
  return IntegrationFinding(
142
158
  control_labels=[], # Add an empty list for control_labels
143
159
  category="Tenable SC Vulnerability", # Add a default category
@@ -148,9 +164,9 @@ class SCIntegration(ScannerIntegration):
148
164
  status=regscale_models.IssueStatus.Open, # Findings of > Low are considered as FAIL
149
165
  asset_identifier=asset_identifier,
150
166
  external_id=vuln.pluginID, # Weakness Source Identifier
151
- first_seen=epoch_to_datetime(vuln.firstSeen),
167
+ first_seen=first_seen,
152
168
  last_seen=epoch_to_datetime(vuln.lastSeen),
153
- date_created=epoch_to_datetime(vuln.firstSeen),
169
+ date_created=first_seen,
154
170
  date_last_updated=epoch_to_datetime(vuln.lastSeen),
155
171
  recommendation_for_mitigation=vuln.solution,
156
172
  cve=cve,
@@ -175,6 +191,10 @@ class SCIntegration(ScannerIntegration):
175
191
  installed_versions=installed_versions_str,
176
192
  fixed_versions=fixed_versions_str,
177
193
  fix_status="",
194
+ scan_date=self.scan_date,
195
+ due_date=issue_due_date(
196
+ severity=severity, created_date=first_seen, title="tenable", config=self.app.config
197
+ ),
178
198
  )
179
199
 
180
200
  def get_cvss_scores(self, vuln: TenableAsset) -> dict:
@@ -212,8 +212,26 @@ class ControlImplementation(RegScaleModel):
212
212
  get_all_asset_controls_by_component="/api/{model_slug}/getAllAssetControlsByComponent/{int_id}",
213
213
  drilldown_asset_controls_by_component="/api/{model_slug}/drilldownAssetControlsByComponent/{component_id}/{str_field}/{str_value}", # noqa: E501
214
214
  get_control_context="/api/{model_slug}/getControlContext/{int_control_id}/{int_parent_id}/{str_module}",
215
+ get_control_with_all_child_details="/api/{model_slug}/getControlWithAllChildDetails/{intId}",
215
216
  )
216
217
 
218
+ @classmethod
219
+ def get_with_child_details(cls, implementation_id: int) -> Optional[list[dict]]:
220
+ """
221
+ Get control implementation with all child details.
222
+
223
+ Retrieve a control implementation with all supporting data in a single call.
224
+
225
+ :param int implementation_id: The ID of the control implementation
226
+ :return: A list of control implementation details or None
227
+ :rtype: Optional[list[dict]]
228
+ """
229
+ endpoint = cls.get_endpoint("get_control_with_all_child_details").format(intId=implementation_id)
230
+ response = cls._get_api_handler().get(endpoint)
231
+ if response and response.ok:
232
+ return response.json()
233
+ return None
234
+
217
235
  def find_by_unique(self, **kwargs: dict) -> Optional["ControlImplementation"]:
218
236
  """
219
237
  Find an object by unique query.
@@ -4,7 +4,7 @@
4
4
  from collections import defaultdict
5
5
  from typing import Any, Optional
6
6
 
7
- from pydantic import Field, ConfigDict
7
+ from pydantic import ConfigDict, Field
8
8
 
9
9
  from regscale.core.app.utils.app_utils import get_current_datetime
10
10
  from regscale.models.regscale_models.regscale_model import RegScaleModel
@@ -25,6 +25,7 @@ class ControlObjective(RegScaleModel):
25
25
  otherId: str = "" # API does not return if None
26
26
  archived: bool = False
27
27
  securityControlId: int
28
+ parentObjectiveId: Optional[int] = None
28
29
  dateCreated: str = Field(default_factory=get_current_datetime)
29
30
  dateLastUpdated: str = Field(default_factory=get_current_datetime)
30
31
  objectiveType: str = "objective"
@@ -1,11 +1,9 @@
1
1
  """Facility model for RegScale."""
2
2
 
3
- import warnings
4
3
  from typing import Optional
5
- from urllib.parse import urljoin
6
4
 
7
- from regscale.core.app.api import Api
8
- from regscale.core.app.application import Application
5
+ from pydantic import ConfigDict
6
+
9
7
  from regscale.models.regscale_models.regscale_model import RegScaleModel
10
8
 
11
9
 
@@ -32,28 +30,14 @@ class Facility(RegScaleModel):
32
30
  isPublic: bool = True
33
31
  dateLastUpdated: Optional[str] = None
34
32
 
35
- def post(self, app: Application) -> Optional[dict]:
36
- """Post a Facility to RegScale
33
+ @staticmethod
34
+ def _get_additional_endpoints() -> dict:
35
+ """
36
+ Get additional endpoints for the Facility model.
37
37
 
38
- :param Application app: The application instance
39
- :return: The response from the API or None
40
- :rtype: Optional[dict]
38
+ :return: A dictionary of additional endpoints
39
+ :rtype: dict
41
40
  """
42
- warnings.warn(
43
- "The 'post' method is deprecated, use 'create' method instead",
44
- DeprecationWarning,
45
- )
46
- api = Api()
47
- url = urljoin(app.config.get("domain", ""), "/api/facilities")
48
- data = self.dict()
49
- response = api.post(url, json=data)
50
- return response.json() if response.ok else None
51
-
52
-
53
- class Facilities(Facility):
54
- def __init__(self, *args, **kwargs):
55
- warnings.warn(
56
- "The 'Facilities' class is deprecated, use 'Facility' instead",
57
- DeprecationWarning,
41
+ return ConfigDict(
42
+ get_list="/api/{model_slug}/get_list",
58
43
  )
59
- super().__init__(*args, **kwargs)
@@ -0,0 +1,38 @@
1
+ from typing import List
2
+
3
+ from pydantic import ConfigDict
4
+
5
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
6
+
7
+
8
+ class FunctionalRole(RegScaleModel):
9
+ """
10
+ Functional Role Model
11
+ """
12
+
13
+ _module_slug = "functionalroles"
14
+
15
+ id: int = 0
16
+ role: str
17
+
18
+ @classmethod
19
+ def get_list(cls) -> List["FunctionalRole"]:
20
+ """
21
+ Get list of Functional Roles
22
+
23
+ :return: list of Functional Roles
24
+ :rtype: List["FunctionalRole"]
25
+ """
26
+ return cls._handle_list_response(cls._get_api_handler().get(cls.get_endpoint("get_list")))
27
+
28
+ @staticmethod
29
+ def _get_additional_endpoints() -> ConfigDict:
30
+ """
31
+ Get additional endpoints for the FunctionalRole
32
+
33
+ :return: Additional endpoints for the FunctionalRole
34
+ :rtype: ConfigDict
35
+ """
36
+ return ConfigDict( # type: ignore
37
+ get_list="/api/{model_slug}/getList",
38
+ )
@@ -788,7 +788,9 @@ class Issue(RegScaleModel):
788
788
  take = 50
789
789
  skip = 0
790
790
  control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
791
- logger.info("Fetching open issues for controls and for security plan %i...", plan_id)
791
+ logger.info(
792
+ f"Fetching open issues for controls and for security plan {plan_id}...",
793
+ )
792
794
  supports_multiple_controls: bool = cls.is_multiple_controls_supported()
793
795
 
794
796
  # Define fields based on version
@@ -3,10 +3,9 @@
3
3
  """Regscale Model for Implementation Parameter in the application"""
4
4
 
5
5
  from enum import Enum
6
- from typing import Optional, List
6
+ from typing import List, Optional
7
7
 
8
- from pydantic import ConfigDict
9
- from pydantic import Field
8
+ from pydantic import ConfigDict, Field
10
9
 
11
10
  from regscale.core.app.utils.app_utils import get_current_datetime
12
11
  from regscale.models.regscale_models.regscale_model import RegScaleModel
@@ -85,3 +84,22 @@ class Parameter(RegScaleModel):
85
84
  if response and response.ok:
86
85
  return [Parameter(**param) for param in response.json()]
87
86
  return []
87
+
88
+ @classmethod
89
+ def merge_parameters(cls, implementation_id: int, security_control_id: int) -> List["Parameter"]:
90
+ """
91
+ Get a list of parameters by implementation ID and security control ID.
92
+
93
+ :param int implementation_id: The ID of the controlImplementation
94
+ :param int security_control_id: The ID of the security control
95
+ :return: A list of parameters
96
+ :rtype: List["Parameter"]
97
+ """
98
+ response = cls._get_api_handler().get(
99
+ endpoint=cls.get_endpoint("merge").format(
100
+ implementationID=implementation_id, securityControlID=security_control_id
101
+ )
102
+ )
103
+ if response and response.ok:
104
+ return [cls(**ci) for ci in response.json()]
105
+ return []
@@ -30,6 +30,28 @@ class Profile(RegScaleModel):
30
30
  confidentiality: Optional[str] = None
31
31
  integrity: Optional[str] = None
32
32
 
33
+ @classmethod
34
+ def apply_profile(cls, module_id: int, module_name: str, profile_id: int, is_public: bool) -> bool:
35
+ """
36
+ Apply a profile to a module.
37
+
38
+ :param int module_id: The module ID
39
+ :param str module_name: The module name
40
+ :param int profile_id: The profile ID
41
+ :param bool is_public: Whether the profile is public
42
+ :return: True if the profile was applied
43
+ :rtype: bool
44
+ """
45
+ endpoint = cls.get_endpoint("apply_profile").format(
46
+ model_slug=cls._module_slug,
47
+ moduleId=module_id,
48
+ moduleName=module_name,
49
+ profileId=profile_id,
50
+ isPublic=is_public,
51
+ )
52
+ response = cls._get_api_handler().post(endpoint)
53
+ return response.ok
54
+
33
55
  @staticmethod
34
56
  def _get_additional_endpoints() -> ConfigDict:
35
57
  """
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  """Model for Profile Mapping in the application"""
4
- from typing import List, Optional
4
+ import importlib
5
+ from typing import TYPE_CHECKING, List, Optional
5
6
  from urllib.parse import urljoin
6
7
 
7
8
  from pydantic import ConfigDict, Field
@@ -11,6 +12,9 @@ from regscale.core.app.application import Application
11
12
  from regscale.models.regscale_models.regscale_model import RegScaleModel
12
13
  from regscale.models.regscale_models.security_control import SecurityControl
13
14
 
15
+ if TYPE_CHECKING:
16
+ from regscale.models import ControlImplementation, ControlImplementationStatus
17
+
14
18
 
15
19
  class ProfileMapping(RegScaleModel):
16
20
  """
@@ -33,6 +37,41 @@ class ProfileMapping(RegScaleModel):
33
37
  isPublic: bool = True
34
38
  lastUpdatedById: Optional[str] = None
35
39
 
40
+ def __init__(self, **data):
41
+ super().__init__(**data)
42
+
43
+ # Create a lazily loaded reference to avoid circular imports
44
+ def _get_control_implementation_classes(self):
45
+ """
46
+ Get the control implementation classes in a lazy manner
47
+
48
+ :return: The control implementation classes
49
+ :rtype: Tuple[ControlImplementation, ControlImplementationStatus]
50
+ :raises ImportError: If the module cannot be imported
51
+ """
52
+ module = importlib.import_module("regscale.models")
53
+ return (module.ControlImplementation, module.ControlImplementationStatus)
54
+
55
+ def to_implementation(self, security_plan_id: int):
56
+ """
57
+ Convert a profile mapping to a control implementation.
58
+
59
+ :param int security_plan_id: The parent Security Plan ID
60
+ :rtype: ControlImplementation
61
+ :return: The control implementation
62
+ """
63
+ # Create a lazily loaded reference to avoid circular imports, will only import once, and subsequent calls will
64
+ # use the already imported classes
65
+ ControlImplementation, ControlImplementationStatus = self._get_control_implementation_classes()
66
+
67
+ return ControlImplementation(
68
+ status=ControlImplementationStatus.NotImplemented.value,
69
+ controlID=self.controlID,
70
+ assessmentFrequency=365,
71
+ parentId=security_plan_id,
72
+ parentModule="securityplans",
73
+ )
74
+
36
75
  @staticmethod
37
76
  def _get_additional_endpoints() -> ConfigDict:
38
77
  """
@@ -62,14 +101,20 @@ class ProfileMapping(RegScaleModel):
62
101
  :return: List of profile mappings
63
102
  :rtype: List[ProfileMapping]
64
103
  """
104
+ from concurrent.futures import ThreadPoolExecutor, as_completed
105
+
65
106
  response = cls._get_api_handler().get(
66
107
  endpoint=cls.get_endpoint("get_by_profile").format(intProfileID=profile_id)
67
108
  )
68
109
  mappings = []
69
110
  if response and response.ok:
70
111
  mappings = [cls(**map) for map in response.json()]
71
- for mapping in mappings:
72
- if control := SecurityControl.get_object(object_id=mapping.controlID):
112
+ with ThreadPoolExecutor() as executor:
113
+ futures = [
114
+ executor.submit(SecurityControl.get_object, object_id=mapping.controlID) for mapping in mappings
115
+ ]
116
+ for future, mapping in zip(as_completed(futures), mappings):
117
+ if control := future.result():
73
118
  mapping.control = control
74
119
  return mappings
75
120
 
@@ -1315,6 +1315,7 @@ class RegScaleModel(BaseModel, ABC):
1315
1315
  :param Optional[Progress] progress: Optional progress context for tracking
1316
1316
  :param Optional[bool] remove_progress_bar: Whether to remove the progress bar after completion, defaults to False
1317
1317
  """
1318
+ # noqa: F824
1318
1319
  nonlocal results
1319
1320
  create_job = None
1320
1321
  if progress:
@@ -1372,6 +1373,7 @@ class RegScaleModel(BaseModel, ABC):
1372
1373
  :param Optional[Progress] progress: Optional progress context for tracking
1373
1374
  :param Optional[bool] remove_progress_bar: Whether to remove the progress bar after completion, defaults to False
1374
1375
  """
1376
+ # noqa: F824
1375
1377
  nonlocal results
1376
1378
  update_job = None
1377
1379
  if progress:
@@ -48,17 +48,18 @@ class Risk(RegScaleModel):
48
48
  dateClosed: Optional[str] = None
49
49
  facilityId: Optional[int] = None
50
50
  orgId: Optional[int] = None
51
+ scenarioId: Optional[int] = None
51
52
  comments: Optional[str] = None
52
53
  riskTier: Optional[str] = None
53
54
  title: Optional[str] = None
54
55
  recommendations: Optional[str] = None
55
56
  impactDescription: Optional[str] = None
56
- inherentRiskScore: Optional[int] = None
57
- residualRiskScore: Optional[int] = None
58
- targetRiskScore: Optional[int] = None
59
- difference: Optional[int] = None
60
- futureCosts: Optional[int] = None
61
- costToMitigate: Optional[int] = None
57
+ inherentRiskScore: Optional[float] = None
58
+ residualRiskScore: Optional[float] = None
59
+ targetRiskScore: Optional[float] = None
60
+ difference: Optional[float] = None
61
+ futureCosts: Optional[float] = None
62
+ costToMitigate: Optional[float] = None
62
63
  controlId: Optional[int] = None
63
64
  assessmentId: Optional[int] = None
64
65
  requirementId: Optional[int] = None
@@ -73,6 +74,9 @@ class Risk(RegScaleModel):
73
74
  nextAssessmentDueDate: Optional[str] = None
74
75
  riskOwnerId: Optional[str] = None
75
76
  isPublic: bool = True
77
+ averageRiskScore: Optional[float] = None
78
+ weightedRiskScore: Optional[float] = None
79
+ parentRiskModelId: Optional[int] = None
76
80
 
77
81
  @staticmethod
78
82
  def fetch_all_risks(app: Application) -> list["Risk"]:
@@ -145,31 +149,35 @@ class Risk(RegScaleModel):
145
149
  "dateClosed": 26,
146
150
  "facilityId": 27,
147
151
  "orgId": 28,
148
- "comments": 29,
149
- "riskTier": 30,
150
- "title": 31,
151
- "recommendations": 32,
152
- "impactDescription": 33,
153
- "inherentRiskScore": 34,
154
- "residualRiskScore": 35,
155
- "targetRiskScore": 36,
156
- "difference": 37,
157
- "futureCosts": 38,
158
- "costToMitigate": 39,
159
- "controlId": 40,
160
- "assessmentId": 41,
161
- "requirementId": 42,
162
- "securityPlanId": 43,
163
- "projectId": 44,
164
- "supplyChainId": 45,
165
- "policyId": 46,
166
- "componentId": 47,
167
- "incidentId": 48,
168
- "riskAssessmentFrequency": 49,
169
- "dateLastAssessed": 50,
170
- "nextAssessmentDueDate": 51,
171
- "riskOwnerId": 52,
152
+ "scenarioId": 29,
153
+ "comments": 30,
154
+ "riskTier": 31,
155
+ "title": 32,
156
+ "recommendations": 33,
157
+ "impactDescription": 34,
158
+ "inherentRiskScore": 35,
159
+ "residualRiskScore": 36,
160
+ "targetRiskScore": 37,
161
+ "difference": 38,
162
+ "futureCosts": 39,
163
+ "costToMitigate": 40,
164
+ "controlId": 41,
165
+ "assessmentId": 42,
166
+ "requirementId": 43,
167
+ "securityPlanId": 44,
168
+ "projectId": 45,
169
+ "supplyChainId": 46,
170
+ "policyId": 47,
171
+ "componentId": 48,
172
+ "incidentId": 49,
173
+ "riskAssessmentFrequency": 50,
174
+ "dateLastAssessed": 51,
175
+ "nextAssessmentDueDate": 52,
176
+ "riskOwnerId": 53,
172
177
  "isPublic": -1,
178
+ "averageRiskScore": 54,
179
+ "weightedRiskScore": 55,
180
+ "parentRiskModelId": 56,
173
181
  }
174
182
 
175
183
  # pylint: disable=W0613
@@ -18,6 +18,7 @@ class SecurityPlan(RegScaleModel):
18
18
  """
19
19
 
20
20
  _module_slug = "securityplans"
21
+ _unique_fields = ["systemName", "tenantsId"]
21
22
 
22
23
  id: int = 0
23
24
  uuid: str = ""
@@ -106,7 +106,7 @@ class SupplyChain(RegScaleModel):
106
106
  actualCosts: Optional[int] = 0
107
107
  strategicTier: Optional[Union[SupplyChainTier, str]] = None
108
108
  contractType: Optional[Union[SupplyChainContractType, str]] = None
109
- scope: Optional[str] = (None,)
109
+ scope: Optional[str] = None
110
110
  startDate: Optional[str] = None
111
111
  endDate: Optional[str] = None
112
112
  violationURL: Optional[str] = None
@@ -5,11 +5,12 @@
5
5
  # standard python imports
6
6
  import random
7
7
  import string
8
- from typing import cast, Optional, List
8
+ from typing import List, Optional, cast
9
9
 
10
- from pydantic import Field, ConfigDict, field_validator
10
+ from pydantic import ConfigDict, Field, field_validator
11
11
 
12
12
  from regscale.core.app.utils.app_utils import get_current_datetime
13
+
13
14
  from .regscale_model import RegScaleModel, T
14
15
 
15
16
 
@@ -122,6 +123,19 @@ class User(RegScaleModel):
122
123
  change_avatar="/api/{model_slug}/changeAvatar/{strUsername}",
123
124
  )
124
125
 
126
+ @classmethod
127
+ def get_roles(cls) -> List[dict]:
128
+ """
129
+ Get all roles from RegScale
130
+
131
+ :return: List of RegScale roles
132
+ :rtype: List[dict]
133
+ """
134
+ response = cls._get_api_handler().get(endpoint=cls.get_endpoint("get_roles"))
135
+ if response and response.ok:
136
+ return response.json()
137
+ return []
138
+
125
139
  @classmethod
126
140
  def get_tenant_id_for_user_id(cls, user_id: str) -> Optional[int]:
127
141
  """
@@ -6,3 +6,4 @@ from .threadhandler import ThreadManager
6
6
  from .threadsafe_counter import ThreadSafeCounter
7
7
  from .threadsafe_dict import ThreadSafeDict
8
8
  from .threadsafe_list import ThreadSafeList
9
+ from .threadsafe_set import ThreadSafeSet
@@ -63,6 +63,16 @@ class ThreadSafeList(Generic[T]):
63
63
  with self._lock:
64
64
  return iter(self._list.copy())
65
65
 
66
+ def __list__(self) -> List[T]:
67
+ """
68
+ Convert ThreadSafeList to a regular list when list() is called
69
+
70
+ :return: A copy of the internal list
71
+ :rtype: List[T]
72
+ """
73
+ with self._lock:
74
+ return self._list.copy()
75
+
66
76
  def remove(self, item: T) -> None:
67
77
  """
68
78
  Remove an item from the list
@@ -0,0 +1,116 @@
1
+ """
2
+ This module contains the ThreadSafeSet class, which is a thread-safe set.
3
+ """
4
+
5
+ from threading import RLock
6
+ from typing import Generic, Iterator, Optional, Set, TypeVar
7
+
8
+ T = TypeVar("T") # Declare type variable
9
+
10
+
11
+ class ThreadSafeSet(Generic[T]):
12
+ """
13
+ ThreadSafeSet class to create a thread-safe set.
14
+ """
15
+
16
+ def __init__(self, initial_set: Optional[Set[T]] = None):
17
+ """
18
+ Initialize a new ThreadSafeSet
19
+
20
+ :param Set[T]|None initial_set: Optional initial set to populate the ThreadSafeSet
21
+ """
22
+ self._set: Set[T] = set(initial_set) if initial_set is not None else set()
23
+ self._lock = RLock()
24
+
25
+ def add(self, item: T) -> None:
26
+ """
27
+ Add an item to the set
28
+
29
+ :param T item: Item to add to the set
30
+ :rtype: None
31
+ """
32
+ with self._lock:
33
+ self._set.add(item)
34
+
35
+ def remove(self, item: T) -> None:
36
+ """
37
+ Remove an item from the set
38
+
39
+ :param T item: Item to remove from the set
40
+ :raises KeyError: If the item is not found
41
+ :rtype: None
42
+ """
43
+ with self._lock:
44
+ self._set.remove(item)
45
+
46
+ def discard(self, item: T) -> None:
47
+ """
48
+ Remove an item from the set if it exists
49
+
50
+ :param T item: Item to remove from the set
51
+ :rtype: None
52
+ """
53
+ with self._lock:
54
+ self._set.discard(item)
55
+
56
+ def __len__(self) -> int:
57
+ """
58
+ Get the length of the set
59
+
60
+ :return: The length of the set
61
+ :rtype: int
62
+ """
63
+ with self._lock:
64
+ return len(self._set)
65
+
66
+ def __iter__(self) -> Iterator[T]:
67
+ """
68
+ Return an iterator over the set
69
+
70
+ :return: An iterator over the set
71
+ :rtype: Iterator[T]
72
+ """
73
+ with self._lock:
74
+ return iter(self._set.copy())
75
+
76
+ def __contains__(self, item: T) -> bool:
77
+ """
78
+ Check if an item is in the set
79
+
80
+ :param T item: Item to check
81
+ :return: True if the item is in the set, False otherwise
82
+ :rtype: bool
83
+ """
84
+ with self._lock:
85
+ return item in self._set
86
+
87
+ def clear(self) -> None:
88
+ """
89
+ Clear the set
90
+
91
+ :rtype: None
92
+ """
93
+ with self._lock:
94
+ self._set.clear()
95
+
96
+ def union(self, other: Set[T]) -> Set[T]:
97
+ """
98
+ Return the union of this set and another
99
+
100
+ :param Set[T] other: Other set to union with
101
+ :return: A new set containing the union
102
+ :rtype: Set[T]
103
+ """
104
+ with self._lock:
105
+ return self._set.union(other)
106
+
107
+ def intersection(self, other: Set[T]) -> Set[T]:
108
+ """
109
+ Return the intersection of this set and another
110
+
111
+ :param Set[T] other: Other set to intersect with
112
+ :return: A new set containing the intersection
113
+ :rtype: Set[T]
114
+ """
115
+ with self._lock:
116
+ return self._set.intersection(other)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.16.3.0
3
+ Version: 6.16.4.1
4
4
  Summary: Command Line Interface (CLI) for bulk processing/loading data into RegScale
5
5
  Home-page: https://github.com/RegScale/regscale-cli
6
6
  Author: Travis Howerton