regscale-cli 6.16.2.0__py3-none-any.whl → 6.16.4.0__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 (52) 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/core/app/utils/api_handler.py +4 -11
  5. regscale/integrations/commercial/crowdstrike.py +0 -1
  6. regscale/integrations/commercial/grype/scanner.py +37 -29
  7. regscale/integrations/commercial/opentext/commands.py +2 -0
  8. regscale/integrations/commercial/opentext/scanner.py +45 -31
  9. regscale/integrations/commercial/qualys.py +52 -61
  10. regscale/integrations/commercial/servicenow.py +1 -0
  11. regscale/integrations/commercial/sicura/commands.py +9 -14
  12. regscale/integrations/commercial/snyk.py +2 -2
  13. regscale/integrations/commercial/synqly/ticketing.py +29 -0
  14. regscale/integrations/commercial/tenablev2/click.py +25 -13
  15. regscale/integrations/commercial/tenablev2/scanner.py +12 -3
  16. regscale/integrations/commercial/trivy/scanner.py +14 -6
  17. regscale/integrations/commercial/veracode.py +1 -1
  18. regscale/integrations/commercial/wizv2/click.py +15 -37
  19. regscale/integrations/jsonl_scanner_integration.py +120 -16
  20. regscale/integrations/public/fedramp/click.py +8 -8
  21. regscale/integrations/public/fedramp/fedramp_cis_crm.py +499 -106
  22. regscale/integrations/public/fedramp/ssp_logger.py +2 -9
  23. regscale/integrations/scanner_integration.py +67 -27
  24. regscale/models/integration_models/cisa_kev_data.json +86 -12
  25. regscale/models/integration_models/flat_file_importer/__init__.py +29 -8
  26. regscale/models/integration_models/snyk.py +141 -15
  27. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  28. regscale/models/integration_models/tenable_models/integration.py +23 -3
  29. regscale/models/integration_models/veracode.py +91 -48
  30. regscale/models/regscale_models/control_implementation.py +18 -0
  31. regscale/models/regscale_models/control_objective.py +2 -1
  32. regscale/models/regscale_models/facility.py +10 -26
  33. regscale/models/regscale_models/functional_roles.py +38 -0
  34. regscale/models/regscale_models/issue.py +3 -1
  35. regscale/models/regscale_models/parameter.py +21 -3
  36. regscale/models/regscale_models/profile.py +22 -0
  37. regscale/models/regscale_models/profile_mapping.py +48 -3
  38. regscale/models/regscale_models/regscale_model.py +2 -0
  39. regscale/models/regscale_models/risk.py +38 -30
  40. regscale/models/regscale_models/security_plan.py +1 -0
  41. regscale/models/regscale_models/supply_chain.py +1 -1
  42. regscale/models/regscale_models/user.py +19 -6
  43. regscale/utils/threading/__init__.py +1 -0
  44. regscale/utils/threading/threadsafe_list.py +10 -0
  45. regscale/utils/threading/threadsafe_set.py +116 -0
  46. regscale/utils/version.py +3 -5
  47. {regscale_cli-6.16.2.0.dist-info → regscale_cli-6.16.4.0.dist-info}/METADATA +1 -1
  48. {regscale_cli-6.16.2.0.dist-info → regscale_cli-6.16.4.0.dist-info}/RECORD +52 -50
  49. {regscale_cli-6.16.2.0.dist-info → regscale_cli-6.16.4.0.dist-info}/LICENSE +0 -0
  50. {regscale_cli-6.16.2.0.dist-info → regscale_cli-6.16.4.0.dist-info}/WHEEL +0 -0
  51. {regscale_cli-6.16.2.0.dist-info → regscale_cli-6.16.4.0.dist-info}/entry_points.txt +0 -0
  52. {regscale_cli-6.16.2.0.dist-info → regscale_cli-6.16.4.0.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:
@@ -1,8 +1,9 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
 
3
3
  from regscale.core.app.logz import create_logger
4
4
  from regscale.core.app.utils.app_utils import get_current_datetime
5
- from regscale.models import Asset, Vulnerability, Mapping, ImportValidater
5
+ from regscale.integrations.scanner_integration import IntegrationFinding
6
+ from regscale.models import Asset, Vulnerability, ImportValidater, IssueStatus
6
7
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
7
8
 
8
9
  APP_NAME = "@app_name"
@@ -17,26 +18,34 @@ class Veracode(FlatFileImporter):
17
18
  self.vuln_title = "PROBLEM_TITLE"
18
19
  self.fmt = "%Y-%m-%d"
19
20
  self.dt_format = "%Y-%m-%d %H:%M:%S"
20
- csv_headers = [
21
+ xlsx_headers = [
21
22
  "Source",
22
23
  ]
23
24
  xml_headers = [
24
25
  "app_name",
25
26
  ]
27
+ json_headers = [
28
+ "findings",
29
+ "project_name",
30
+ ]
26
31
  self.mapping_file = kwargs.get("mappings_path")
27
32
  self.disable_mapping = kwargs.get("disable_mapping")
28
33
  file_type = kwargs.get("file_type")
29
- if file_type == ".xml":
34
+ xml_tag = None
35
+ if "xml" in file_type:
30
36
  self.required_headers = xml_headers
31
37
  xml_tag = "detailedreport"
38
+ elif "xlsx" in file_type:
39
+ self.required_headers = xlsx_headers
32
40
  else:
33
- self.required_headers = csv_headers
34
- xml_tag = None
41
+ self.required_headers = json_headers
35
42
  self.validater = ImportValidater(
36
43
  self.required_headers, kwargs.get("file_path"), self.mapping_file, self.disable_mapping, xml_tag=xml_tag
37
44
  )
38
45
  self.headers = self.validater.parsed_headers
39
46
  self.mapping = self.validater.mapping
47
+ if file_type == ".json":
48
+ self.asset_identifier = self.mapping.get_value(self.validater.data, "project_name", "")
40
49
  super().__init__(
41
50
  logger=logger,
42
51
  headers=self.headers,
@@ -57,10 +66,14 @@ class Veracode(FlatFileImporter):
57
66
  version = None
58
67
  # Veracode is a Web Application Security Scanner, so these will be software assets, scanning a
59
68
  # single web application
60
- if "detailedreport" in self.mapping.mapping.keys():
61
- name = self.mapping.get_value(dat, "detailedreport", {}).get(APP_NAME, "")
62
- account_id = self.mapping.get_value(dat, "detailedreport", {}).get(ACCOUNT_ID, "")
63
- version = self.mapping.get_value(dat, "detailedreport", {}).get(VERSION, "")
69
+ if "xml" in self.attributes.file_type:
70
+ detailed_report_data = dat.get("detailedreport", {})
71
+ name = detailed_report_data.get(APP_NAME, "")
72
+ account_id = detailed_report_data.get(ACCOUNT_ID, "")
73
+ version = detailed_report_data.get(VERSION, "")
74
+ elif "json" in self.attributes.file_type:
75
+ name = self.mapping.get_value(dat, "project_name", "")
76
+ account_id = self.asset_identifier
64
77
  else:
65
78
  name = self.mapping.get_value(dat, "Source", "")
66
79
  account_id = str(self.mapping.get_value(dat, "ID", ""))
@@ -88,7 +101,7 @@ class Veracode(FlatFileImporter):
88
101
  )
89
102
  return [asset]
90
103
 
91
- def create_vuln(self, dat: Optional[dict] = None, **kwargs) -> List[Vulnerability]:
104
+ def create_vuln(self, dat: Optional[dict] = None, **kwargs) -> Union[List[Vulnerability], List[IntegrationFinding]]:
92
105
  """
93
106
  Create a RegScale vulnerability from a vulnerability in the Veracode export file
94
107
 
@@ -96,30 +109,62 @@ class Veracode(FlatFileImporter):
96
109
  :return: List of RegScale Vulnerability objects
97
110
  :rtype: List[Vulnerability]
98
111
  """
99
- import_type = "xml" if isinstance(dat, str) else "csv"
100
112
  # Veracode is a Web Application Security Scanner, so these will be software assets,
101
113
  # scanning a single web application
102
- if import_type == "xml":
103
- name = self.mapping.get_value(dat, "detailedreport", {}).get(APP_NAME, "")
104
- all_sev_data = self.mapping.get_value(dat, "detailedreport", {}).get("severity", [])
114
+ if "xml" in self.attributes.file_type:
115
+ detailed_report_data = dat.get("detailedreport", {})
116
+ name = detailed_report_data.get(APP_NAME, "")
117
+ all_sev_data = detailed_report_data.get("severity", [])
105
118
  severity = self.severity_info(all_sev_data)[0] if all_sev_data else "low"
106
119
  if severity_data := self.severity_info(all_sev_data):
107
- if isinstance(severity_data, list) and len(severity_data) >= 2:
120
+ if isinstance(severity_data, tuple) and len(severity_data) >= 2:
108
121
  cwes = [
109
- f"{c.get('cweid')} {c.get('cwename')}" for c in severity_data[1].get("cwe", [])
122
+ f"{c.get('@cweid')} {c.get('@cwename')}" for c in severity_data[1].get("cwe", [])
110
123
  ] # Multiple cwes per asset in official XML
111
124
  else:
112
125
  cwes = []
113
- else:
126
+ elif "xlsx" in self.attributes.file_type:
114
127
  name = self.mapping.get_value(dat, "Source", "")
115
128
  severity = self.mapping.get_value(dat, "Sev", "").lower()
116
129
  cwes = [self.mapping.get_value(dat, "CWE ID & Name", [])] # Coalfire should flatten data for asset -> cwes
130
+ elif "json" in self.attributes.file_type:
131
+ return self._parse_json_findings(**kwargs)
117
132
 
118
- return self.process_csv_vulns(name, cwes, severity)
133
+ return self.process_vuln_data(name, cwes, severity)
119
134
 
120
- def process_csv_vulns(self, hostname: str, cwes: List[str], severity: str) -> List[Vulnerability]:
135
+ def _parse_json_findings(self, **kwargs) -> List[IntegrationFinding]:
121
136
  """
122
- Process the CSV findings from the ECR scan
137
+ Parse the JSON findings from the Veracode .json export file
138
+
139
+ :return: List of IntegrationFinding objects
140
+ :rtype: List[IntegrationFinding]
141
+ """
142
+ findings: List[IntegrationFinding] = []
143
+ for vuln in self.mapping.get_value(kwargs.get("data", self.validater.data), "findings", []):
144
+ if title := vuln.get("issue_type", vuln.get("title", "")):
145
+ findings.append(
146
+ IntegrationFinding(
147
+ title=title,
148
+ description=vuln.get("display_text", "No description available"),
149
+ severity=self.finding_severity_map.get(self.hit_mapping().get(vuln.get("severity", 0)), "Low"),
150
+ status=IssueStatus.Open,
151
+ plugin_name=vuln.get(title, self.name),
152
+ plugin_id=vuln.get("cwe_id", vuln.get("issue_id", "")),
153
+ plugin_text=vuln.get("issue_type", ""),
154
+ asset_identifier=self.asset_identifier,
155
+ first_seen=self.scan_date,
156
+ last_seen=self.scan_date,
157
+ scan_date=self.scan_date,
158
+ category="Software",
159
+ is_cwe=True,
160
+ control_labels=[],
161
+ )
162
+ )
163
+ return findings
164
+
165
+ def process_vuln_data(self, hostname: str, cwes: List[str], severity: str) -> List[Vulnerability]:
166
+ """
167
+ Process the vulnerability data to create a list of vulnerabilities
123
168
 
124
169
  :param str hostname: The hostname
125
170
  :param List[str] cwes: The CWEs
@@ -137,7 +182,7 @@ class Veracode(FlatFileImporter):
137
182
 
138
183
  def create_vulnerability_object(
139
184
  self, asset: Asset, hostname: str, cwe: str, severity: str, description: str
140
- ) -> Vulnerability:
185
+ ) -> IntegrationFinding:
141
186
  """
142
187
  Create a vulnerability from a row in the Veracode file
143
188
 
@@ -146,33 +191,25 @@ class Veracode(FlatFileImporter):
146
191
  :param str cwe: The CWE
147
192
  :param str severity: The severity
148
193
  :param str description: The description
149
- :return: The vulnerability
150
- :rtype: Vulnerability
151
- """
152
- config = self.attributes.app.config
153
-
154
- return Vulnerability( # type: ignore
155
- id=0,
156
- scanId=0,
157
- parentId=asset.id,
158
- parentModule="assets",
159
- ipAddress="0.0.0.0",
160
- lastSeen=get_current_datetime(), # No timestamp on Veracode
161
- firstSeen=get_current_datetime(), # No timestamp on Veracode
162
- daysOpen=None,
163
- dns=hostname,
164
- mitigated=None,
165
- operatingSystem=asset.operatingSystem,
166
- severity=severity,
167
- plugInName=cwe,
168
- cve="",
169
- tenantsId=0,
194
+ :return: The equivalent IntegrationFinding object
195
+ :rtype: IntegrationFinding
196
+ """
197
+ return IntegrationFinding(
170
198
  title=f"{cwe} on asset {asset.name}",
171
- description=cwe,
172
- plugInText=description,
173
- createdById=config["userId"],
174
- lastUpdatedById=config["userId"],
175
- dateCreated=get_current_datetime(),
199
+ description=description,
200
+ status=IssueStatus.Open,
201
+ dns=hostname,
202
+ first_seen=self.scan_date,
203
+ last_seen=self.scan_date,
204
+ scan_date=self.scan_date,
205
+ category="Software",
206
+ is_cwe=True,
207
+ severity=self.finding_severity_map.get(self.hit_mapping().get(severity.title(), "Low")),
208
+ plugin_name=cwe,
209
+ plugin_id=cwe,
210
+ plugin_text=description,
211
+ asset_identifier=hostname,
212
+ control_labels=[],
176
213
  )
177
214
 
178
215
  def get_asset(self, hostname: str) -> Optional[Asset]:
@@ -214,4 +251,10 @@ class Veracode(FlatFileImporter):
214
251
  "2": "low",
215
252
  "1": "low",
216
253
  "0": "info",
254
+ 5: "Critical",
255
+ 4: "High",
256
+ 3: "Medium",
257
+ 2: "Low",
258
+ 1: "Low",
259
+ 0: "Low",
217
260
  }
@@ -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: