regscale-cli 6.16.0.0__py3-none-any.whl → 6.16.2.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 (65) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/login.py +1 -1
  4. regscale/core/app/internal/poam_editor.py +1 -1
  5. regscale/core/app/utils/app_utils.py +1 -1
  6. regscale/core/app/utils/parser_utils.py +2 -2
  7. regscale/integrations/commercial/__init__.py +2 -2
  8. regscale/integrations/commercial/ad.py +1 -1
  9. regscale/integrations/commercial/azure/intune.py +1 -0
  10. regscale/integrations/commercial/grype/__init__.py +3 -0
  11. regscale/integrations/commercial/grype/commands.py +72 -0
  12. regscale/integrations/commercial/grype/scanner.py +390 -0
  13. regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
  14. regscale/integrations/commercial/nessus/scanner.py +3 -0
  15. regscale/integrations/commercial/opentext/__init__.py +6 -0
  16. regscale/integrations/commercial/opentext/commands.py +77 -0
  17. regscale/integrations/commercial/opentext/scanner.py +449 -85
  18. regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +4 -0
  19. regscale/integrations/commercial/sap/tenable/click.py +1 -1
  20. regscale/integrations/commercial/sap/tenable/scanner.py +8 -2
  21. regscale/integrations/commercial/tenablev2/click.py +39 -16
  22. regscale/integrations/commercial/trivy/__init__.py +5 -0
  23. regscale/integrations/commercial/trivy/commands.py +74 -0
  24. regscale/integrations/commercial/trivy/scanner.py +276 -0
  25. regscale/integrations/commercial/wizv2/click.py +9 -21
  26. regscale/integrations/commercial/wizv2/scanner.py +2 -1
  27. regscale/integrations/commercial/wizv2/utils.py +146 -70
  28. regscale/integrations/jsonl_scanner_integration.py +869 -0
  29. regscale/integrations/public/fedramp/fedramp_common.py +4 -4
  30. regscale/integrations/public/fedramp/import_workbook.py +1 -1
  31. regscale/integrations/public/fedramp/inventory_items.py +3 -3
  32. regscale/integrations/public/fedramp/poam/scanner.py +51 -44
  33. regscale/integrations/public/fedramp/ssp_logger.py +6 -6
  34. regscale/integrations/scanner_integration.py +268 -64
  35. regscale/models/app_models/mapping.py +3 -3
  36. regscale/models/integration_models/amazon_models/inspector.py +15 -17
  37. regscale/models/integration_models/aqua.py +1 -5
  38. regscale/models/integration_models/cisa_kev_data.json +100 -10
  39. regscale/models/integration_models/ecr_models/ecr.py +2 -6
  40. regscale/models/integration_models/{flat_file_importer.py → flat_file_importer/__init__.py} +7 -4
  41. regscale/models/integration_models/grype_import.py +3 -3
  42. regscale/models/integration_models/prisma.py +3 -3
  43. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  44. regscale/models/integration_models/synqly_models/connectors/assets.py +1 -0
  45. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +2 -0
  46. regscale/models/integration_models/tenable_models/integration.py +46 -10
  47. regscale/models/integration_models/trivy_import.py +1 -1
  48. regscale/models/integration_models/xray.py +1 -1
  49. regscale/models/regscale_models/__init__.py +2 -0
  50. regscale/models/regscale_models/control_implementation.py +18 -44
  51. regscale/models/regscale_models/inherited_control.py +61 -0
  52. regscale/models/regscale_models/issue.py +3 -2
  53. regscale/models/regscale_models/mixins/parent_cache.py +1 -1
  54. regscale/models/regscale_models/regscale_model.py +73 -7
  55. regscale/models/regscale_models/vulnerability.py +61 -8
  56. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.2.0.dist-info}/METADATA +3 -3
  57. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.2.0.dist-info}/RECORD +62 -56
  58. tests/regscale/core/test_logz.py +8 -0
  59. regscale/integrations/commercial/grype.py +0 -165
  60. regscale/integrations/commercial/opentext/click.py +0 -99
  61. regscale/integrations/commercial/trivy.py +0 -162
  62. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.2.0.dist-info}/LICENSE +0 -0
  63. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.2.0.dist-info}/WHEEL +0 -0
  64. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.2.0.dist-info}/entry_points.txt +0 -0
  65. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.2.0.dist-info}/top_level.txt +0 -0
@@ -84,6 +84,7 @@ class Assets(SynqlyModel):
84
84
  title=f"{self.integration_name} Assets",
85
85
  plan_id=regscale_ssp_id,
86
86
  integration_assets=integration_assets,
87
+ asset_count=len(integration_assets),
87
88
  )
88
89
 
89
90
  self.logger.info(f"[green]Sync from {self.integration_name} to RegScale completed.")
@@ -133,6 +133,7 @@ class Vulnerabilities(SynqlyModel):
133
133
  title=f"{self.integration_name} Vulnerabilities",
134
134
  plan_id=regscale_ssp_id,
135
135
  integration_assets=integration_assets,
136
+ asset_count=len(integration_assets),
136
137
  )
137
138
 
138
139
  if findings:
@@ -157,6 +158,7 @@ class Vulnerabilities(SynqlyModel):
157
158
  title=f"{self.integration_name} Vulnerabilities",
158
159
  plan_id=regscale_ssp_id,
159
160
  integration_findings=integration_findings,
161
+ finding_count=len(integration_findings),
160
162
  )
161
163
  self.logger.info(f"[green]Sync from {self.integration_name} to RegScale completed.")
162
164
 
@@ -3,9 +3,9 @@ This module contains the Tenable SC Integration class that is responsible for fe
3
3
  """
4
4
 
5
5
  import logging
6
+ import re
6
7
  from typing import Any, Iterator, List, Optional, Tuple
7
8
 
8
- from regscale.core.app.application import Application
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
@@ -13,6 +13,8 @@ from regscale.integrations.scanner_integration import IntegrationAsset, Integrat
13
13
  from regscale.models import regscale_models
14
14
  from regscale.models.integration_models.tenable_models.models import TenableAsset
15
15
 
16
+ logger = logging.getLogger("regscale")
17
+
16
18
 
17
19
  class SCIntegration(ScannerIntegration):
18
20
  """
@@ -38,7 +40,7 @@ class SCIntegration(ScannerIntegration):
38
40
  :param dict kwargs: Additional keyword arguments
39
41
  :yields: Iterator[IntegrationAsset]
40
42
  """
41
- integration_assets = kwargs.get("integration_assets")
43
+ integration_assets = kwargs.get("integration_assets", [])
42
44
  yield from integration_assets
43
45
 
44
46
  def fetch_findings(self, *args: Tuple, **kwargs: dict) -> Iterator[IntegrationFinding]:
@@ -50,7 +52,7 @@ class SCIntegration(ScannerIntegration):
50
52
  :yields: Iterator[IntegrationFinding]
51
53
 
52
54
  """
53
- integration_findings = kwargs.get("integration_findings")
55
+ integration_findings = kwargs.get("integration_findings", [])
54
56
  yield from integration_findings
55
57
 
56
58
  def parse_findings(self, vuln: TenableAsset, integration_mapping: Any) -> List[IntegrationFinding]:
@@ -113,14 +115,34 @@ class SCIntegration(ScannerIntegration):
113
115
 
114
116
  validated_match = integration_mapping.field_map_validation(obj=vuln, model_type="asset")
115
117
  asset_identifier = validated_match or vuln.dnsName or vuln.dns or vuln.ip
116
- cvss_score = self.get_cvss_score(vuln)
118
+ cvss_scores = self.get_cvss_scores(vuln)
117
119
  severity = self.finding_severity_map.get(vuln.severity.name, regscale_models.IssueSeverity.Low)
118
120
 
121
+ installed_versions_str = ""
122
+ fixed_versions_str = ""
123
+ package_path_str = ""
124
+
125
+ if "Installed package" in vuln.pluginText:
126
+ installed_versions = re.findall(r"Installed package\s*:\s*(\S+)", vuln.pluginText)
127
+ installed_versions_str = ", ".join(installed_versions)
128
+ if "Fixed package" in vuln.pluginText:
129
+ fixed_versions = re.findall(r"Fixed package\s*:\s*(\S+)", vuln.pluginText)
130
+ fixed_versions_str = ", ".join(fixed_versions)
131
+ if "Path" in vuln.pluginText:
132
+ package_path = re.findall(r"Path\s*:\s*(\S+)", vuln.pluginText)
133
+ package_path_str = ", ".join(package_path)
134
+ if "Installed version" in vuln.pluginText:
135
+ installed_versions = re.findall(r"Installed version\s*:\s*(.+)", vuln.pluginText)
136
+ installed_versions_str = ", ".join(installed_versions)
137
+ if "Fixed version" in vuln.pluginText:
138
+ fixed_versions = re.findall(r"Fixed version\s*:\s*(.+)", vuln.pluginText)
139
+ fixed_versions_str = ", ".join(fixed_versions)
140
+
119
141
  return IntegrationFinding(
120
142
  control_labels=[], # Add an empty list for control_labels
121
143
  category="Tenable SC Vulnerability", # Add a default category
122
144
  dns=vuln.dnsName,
123
- title=getter("title") or (vuln.synopsis or vuln.pluginName), # BMC Add CVE to title
145
+ title=getter("title") or f"{cve}: {vuln.synopsis}" if cve else (vuln.synopsis or vuln.pluginName),
124
146
  description=getter("description") or (vuln.description or vuln.pluginInfo),
125
147
  severity=severity,
126
148
  status=regscale_models.IssueStatus.Open, # Findings of > Low are considered as FAIL
@@ -132,8 +154,11 @@ class SCIntegration(ScannerIntegration):
132
154
  date_last_updated=epoch_to_datetime(vuln.lastSeen),
133
155
  recommendation_for_mitigation=vuln.solution,
134
156
  cve=cve,
135
- cvss_v3_score=cvss_score,
136
- cvss_score=cvss_score,
157
+ cvss_v3_score=cvss_scores.get("cvss_v3_base_score", 0.0),
158
+ cvss_score=cvss_scores.get("cvss_v3_base_score", 0.0),
159
+ cvss_v3_vector=vuln.cvssV3Vector,
160
+ cvss_v2_score=cvss_scores.get("cvss_v2_base_score", 0.0),
161
+ cvss_v2_vector=vuln.cvssVector,
137
162
  vpr_score=float(vuln.vprScore) if vuln.vprScore else None,
138
163
  comments=vuln.cvssV3Vector,
139
164
  plugin_id=vuln.pluginID,
@@ -143,9 +168,16 @@ class SCIntegration(ScannerIntegration):
143
168
  basis_for_adjustment="Tenable SC import",
144
169
  vulnerability_type="Tenable SC Vulnerability",
145
170
  vulnerable_asset=vuln.dnsName,
171
+ build_version="",
172
+ affected_os=vuln.operatingSystem,
173
+ affected_packages=vuln.pluginName,
174
+ package_path=package_path_str,
175
+ installed_versions=installed_versions_str,
176
+ fixed_versions=fixed_versions_str,
177
+ fix_status="",
146
178
  )
147
179
 
148
- def get_cvss_score(self, vuln: TenableAsset) -> float:
180
+ def get_cvss_scores(self, vuln: TenableAsset) -> dict:
149
181
  """
150
182
  Returns the CVSS score for the finding
151
183
 
@@ -153,10 +185,14 @@ class SCIntegration(ScannerIntegration):
153
185
  :return: The CVSS score
154
186
  :rtype: float
155
187
  """
188
+ res = {}
156
189
  try:
157
- res = float(vuln.cvssV3BaseScore) if vuln.cvssV3BaseScore else 0.0
190
+ res["cvss_v3_base_score"] = float(vuln.cvssV3BaseScore) if vuln.cvssV3BaseScore else 0.0
191
+ res["cvss_v2_base_score"] = float(vuln.baseScore) if vuln.baseScore else 0.0
158
192
  except (ValueError, TypeError):
159
- res = 0.0
193
+ res["cvss_v3_base_score"] = 0.0
194
+ res["cvss_v2_base_score"] = 0.0
195
+
160
196
  return res
161
197
 
162
198
  def to_integration_asset(self, asset: TenableAsset, **kwargs: dict) -> IntegrationAsset:
@@ -221,7 +221,7 @@ class TrivyImport(FlatFileImporter):
221
221
  """
222
222
  data = self.validater.data
223
223
  assets: List[IntegrationAsset] = []
224
- os_data = self.mapping.get_value(data, "Metadata", {}, warnings=False).get("OS", {})
224
+ os_data = self.mapping.get_value(data, "Metadata", {}).get("OS", {})
225
225
  try:
226
226
  assets.append(self.parse_asset(asset=data, os_data=os_data))
227
227
  return assets
@@ -123,7 +123,7 @@ class XRay(FlatFileImporter):
123
123
  cve=cve,
124
124
  vprScore=None,
125
125
  tenantsId=0, # Need a way to figure this out programmatically
126
- title=f"{self.mapping.get_value(dat, 'issue_id', warnings=False) or self.mapping.get_value(dat, 'summary', f'XRay Vulnerability from Import {get_current_datetime()}', warnings=False)} on asset {asset.name}",
126
+ title=f"{self.mapping.get_value(dat, 'issue_id') or self.mapping.get_value(dat, 'summary', f'XRay Vulnerability from Import {get_current_datetime()}')} on asset {asset.name}",
127
127
  description=self.mapping.get_value(dat, "summary"),
128
128
  plugInText=vuln.get("cve"),
129
129
  extra_data={
@@ -8,6 +8,7 @@ from .cci import *
8
8
  from .change import *
9
9
  from .checklist import *
10
10
  from .comment import *
11
+ from .compliance_settings import *
11
12
  from .component import *
12
13
  from .component_mapping import *
13
14
  from .control import *
@@ -26,6 +27,7 @@ from .file import *
26
27
  from .implementation_objective import *
27
28
  from .implementation_option import *
28
29
  from .incident import *
30
+ from .inherited_control import *
29
31
  from .interconnection import *
30
32
  from .issue import *
31
33
  from .leveraged_authorization import *
@@ -13,7 +13,7 @@ from pydantic import ConfigDict, Field
13
13
 
14
14
  from regscale.core.app.api import Api
15
15
  from regscale.core.app.application import Application
16
- from regscale.core.app.utils.app_utils import get_current_datetime, remove_keys
16
+ from regscale.core.app.utils.app_utils import get_current_datetime
17
17
  from regscale.core.app.utils.catalog_utils.common import parentheses_to_dot
18
18
  from regscale.models.regscale_models.implementation_role import ImplementationRole
19
19
  from regscale.models.regscale_models.regscale_model import RegScaleModel
@@ -178,7 +178,7 @@ class ControlImplementation(RegScaleModel):
178
178
  export="/api/{model_slug}/export/{int_id}",
179
179
  wizard="/api/{model_slug}/wizard/{int_id}/{str_module}",
180
180
  get_date_last_assessed_by_parent="/api/{model_slug}/getDateLastAssessedByParent/{int_record}",
181
- get_date_last_assessed_by_parent_and_module="/api/{model_slug}/getDateLastAssessedByParentAndModule/{str_module}/{int_record}",
181
+ get_date_last_assessed_by_parent_and_module="/api/{model_slug}/getDateLastAssessedByParentAndModule/{str_module}/{int_record}", # noqa: E501
182
182
  get_date_last_assessed_for_all_assets="/api/{model_slug}/getDateLastAssessedForAllAssets/{int_record}",
183
183
  graph_controls_by_date="/api/{model_slug}/graphControlsByDate/{year}",
184
184
  get_date_last_assessed_by_control="/api/{model_slug}/getDateLastAssessedByControl/{int_control}",
@@ -203,14 +203,14 @@ class ControlImplementation(RegScaleModel):
203
203
  quick_update="/api/{model_slug}/quickUpdate/{id}/{str_status}/{int_weight}/{str_user}",
204
204
  dashboard_by_parent="/api/{model_slug}/dashboardByParent/{str_group_by}/{int_id}/{str_module}",
205
205
  security_control_dashboard="/api/{model_slug}/securityControlDashboard/{str_group_by}/{int_id}",
206
- dashboard_by_parent_and_catalogue="/api/{model_slug}/dashboardByParentAndCatalogue/{str_group_by}/{int_id}/{int_cat_id}",
206
+ dashboard_by_parent_and_catalogue="/api/{model_slug}/dashboardByParentAndCatalogue/{str_group_by}/{int_id}/{int_cat_id}", # noqa: E501
207
207
  group_by_family="/api/{model_slug}/groupByFamily/{int_security_plan}",
208
208
  dashboard_by_sp="/api/{model_slug}/dashboardBySP/{str_group_by}/{int_security_plan}",
209
209
  report="/api/{model_slug}/report/{str_report}",
210
210
  get_by_parent="/api/{model_slug}/getByParent/{int_id}/{str_module}",
211
211
  get_count_by_parent="/api/{model_slug}/getCountByParent/{int_id}/{str_module}",
212
212
  get_all_asset_controls_by_component="/api/{model_slug}/getAllAssetControlsByComponent/{int_id}",
213
- drilldown_asset_controls_by_component="/api/{model_slug}/drilldownAssetControlsByComponent/{component_id}/{str_field}/{str_value}",
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
215
  )
216
216
 
@@ -662,7 +662,6 @@ class ControlImplementation(RegScaleModel):
662
662
  parent_module: str,
663
663
  existing_implementation_dict: dict,
664
664
  full_controls: dict,
665
- partial_controls: dict,
666
665
  failing_controls: dict,
667
666
  include_not_implemented: Optional[bool] = False,
668
667
  ) -> None:
@@ -674,7 +673,6 @@ class ControlImplementation(RegScaleModel):
674
673
  :param str parent_module: Name of the parent module
675
674
  :param dict existing_implementation_dict: Dictionary of existing implementations
676
675
  :param dict full_controls: Dictionary of fully implemented controls
677
- :param dict partial_controls: Dictionary of partially implemented controls
678
676
  :param dict failing_controls: Dictionary of failing controls
679
677
  :param Optional[bool] include_not_implemented: Whether to include not implemented controls, defaults to False
680
678
  :rtype: None
@@ -688,7 +686,6 @@ class ControlImplementation(RegScaleModel):
688
686
  parent_module,
689
687
  existing_implementation_dict,
690
688
  full_controls,
691
- partial_controls,
692
689
  failing_controls,
693
690
  user_id,
694
691
  include_not_implemented,
@@ -705,7 +702,6 @@ class ControlImplementation(RegScaleModel):
705
702
  parent_module: str,
706
703
  existing_implementation_dict: dict,
707
704
  full_controls: dict,
708
- partial_controls: dict,
709
705
  failing_controls: dict,
710
706
  user_id: Optional[str] = None,
711
707
  include_not_implemented: Optional[bool] = False,
@@ -718,7 +714,6 @@ class ControlImplementation(RegScaleModel):
718
714
  :param str parent_module: Name of the parent module
719
715
  :param dict existing_implementation_dict: Dictionary of existing implementations
720
716
  :param dict full_controls: Dictionary of fully implemented controls
721
- :param dict partial_controls: Dictionary of partially implemented controls
722
717
  :param dict failing_controls: Dictionary of failing controls
723
718
  :param Optional[str] user_id: ID of the user performing the operation, defaults to None
724
719
  :param Optional[bool] include_not_implemented: Whether to include not implemented controls, defaults to False
@@ -729,21 +724,12 @@ class ControlImplementation(RegScaleModel):
729
724
  to_update = []
730
725
 
731
726
  for control in controls:
732
- # if otherid exists in catalog object make sure it has something in it before matching
733
- # this case may exist while new catalogs are being migrated to for customers
734
- if len(control.get("otherId", [])) > 0:
735
- lower_case_control_id = control["otherId"].lower()
736
- else:
737
- lower_case_control_id = control["controlId"].lower()
738
-
739
- status = cls.check_implementation(full_controls, partial_controls, failing_controls, lower_case_control_id)
727
+ lower_case_control_id = control["controlId"].lower()
728
+ status = cls.check_implementation(full_controls, failing_controls, lower_case_control_id)
740
729
  if not include_not_implemented and status == ControlImplementationStatus.NotImplemented.value:
741
730
  continue
742
731
 
743
- if len(control.get("otherId", [])) > 0:
744
- controlid = control["otherId"]
745
- else:
746
- controlid = control["controlId"]
732
+ controlid = control.get("controlId")
747
733
 
748
734
  if controlid not in existing_implementation_dict:
749
735
  cim = cls.create_new_control_implementation(control, parent_id, parent_module, status, user_id)
@@ -808,18 +794,11 @@ class ControlImplementation(RegScaleModel):
808
794
  :param list to_update: List of controls to update
809
795
  :param Optional[str] user_id: ID of the user performing the operation, defaults to None
810
796
  """
811
- existing_imp = existing_implementation_dict[control["controlId"]]
812
- existing_imp.update(
813
- {
814
- "implementation": control.get("implementation"),
815
- "status": status,
816
- "dateLastAssessed": get_current_datetime(),
817
- "lastUpdatedById": user_id,
818
- "dateLastUpdated": get_current_datetime(),
819
- }
820
- )
821
-
822
- remove_keys(existing_imp, ["createdBy", "systemRole", "controlOwner", "lastUpdatedBy"])
797
+ existing_imp: ControlImplementation = existing_implementation_dict[control["controlId"]]
798
+ existing_imp.status = status
799
+ existing_imp.dateLastAssessed = get_current_datetime()
800
+ existing_imp.lastUpdatedById = user_id
801
+ existing_imp.dateLastUpdated = get_current_datetime()
823
802
 
824
803
  if existing_imp not in to_update:
825
804
  to_update.append(existing_imp)
@@ -860,7 +839,6 @@ class ControlImplementation(RegScaleModel):
860
839
  @staticmethod
861
840
  def check_implementation(
862
841
  full_controls: dict,
863
- partial_controls: dict,
864
842
  failing_controls: dict,
865
843
  control_id: str,
866
844
  ) -> str:
@@ -874,18 +852,14 @@ class ControlImplementation(RegScaleModel):
874
852
  :return: status of control implementation
875
853
  :rtype: str
876
854
  """
855
+ status = ControlImplementationStatus.NotImplemented.value
877
856
  if control_id in full_controls.keys():
878
- logger.debug(f"Found fully implemented control: {control_id}")
879
- return ControlImplementationStatus.FullyImplemented.value
880
- elif control_id in partial_controls.keys():
881
- logger.debug(f"Found partially implemented control: {control_id}")
882
- return ControlImplementationStatus.PartiallyImplemented.value
857
+ logger.debug(f"Found control passing compliance check: {control_id}")
858
+ status = ControlImplementationStatus.PartiallyImplemented.value
883
859
  elif control_id in failing_controls.keys():
884
- logger.debug(f"Found failing control: {control_id}")
885
- return ControlImplementationStatus.InRemediation.value
886
- else:
887
- logger.debug(f"Found not implemented control: {control_id}")
888
- return ControlImplementationStatus.NotImplemented.value
860
+ logger.debug(f"Found control failing compliance check: {control_id}")
861
+ status = ControlImplementationStatus.InRemediation.value
862
+ return status
889
863
 
890
864
  @classmethod
891
865
  def get_sort_position_dict(cls) -> dict:
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Model for Security Plan in the application"""
4
+
5
+ from typing import Optional, Union
6
+
7
+ from pydantic import ConfigDict, Field, field_validator
8
+
9
+ from regscale.core.app.api import Api
10
+ from regscale.core.app.logz import create_logger
11
+ from regscale.core.app.utils.app_utils import get_current_datetime
12
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
13
+
14
+
15
+ class InheritedControl(RegScaleModel):
16
+ """
17
+ Inherited Control model
18
+ """
19
+
20
+ _module_slug = "inheritedControls"
21
+
22
+ id: int = 0
23
+ createdBy: Optional[str] = None
24
+ createdById: Optional[str] = None
25
+ dateCreated: Optional[str] = Field(default_factory=get_current_datetime)
26
+ lastUpdatedById: Optional[str] = Field(default_factory=RegScaleModel.get_user_id)
27
+ dateLastUpdated: Optional[str] = Field(default_factory=get_current_datetime)
28
+ isPublic: bool = True
29
+ parentId: int = 0
30
+ parentModule: str = ""
31
+ baseControlId: int = 0
32
+ inheritedControlId: int = 0
33
+
34
+ @staticmethod
35
+ def _get_additional_endpoints() -> ConfigDict:
36
+ """
37
+ Get additional endpoints for the Inherited Controls model.
38
+
39
+ :return: A dictionary of additional endpoints
40
+ :rtype: ConfigDict
41
+ """
42
+ return ConfigDict( # type: ignore
43
+ get_all_by_parent="/api/{model_slug}/getAllByParent/{intParentID}/{strModule}",
44
+ get_all_by_control="/api/{model_slug}/getAllByBaseControl/{control_id}",
45
+ )
46
+
47
+ @classmethod
48
+ def get_all_by_control(cls, control_id: int) -> dict:
49
+ """
50
+ Fetch the Mega API data for the given SSP ID
51
+
52
+ :param int ssp_id: RegScale SSP ID
53
+ :return: Mega API data
54
+ :rtype: dict
55
+ """
56
+ response = cls._get_api_handler().get(
57
+ endpoint=cls.get_endpoint("get_all_by_control").format(module_slug=cls._module_slug, control_id=control_id)
58
+ )
59
+ if not response.raise_for_status():
60
+ return response.json()
61
+ return {}
@@ -789,6 +789,7 @@ class Issue(RegScaleModel):
789
789
  skip = 0
790
790
  control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
791
791
  logger.info("Fetching open issues for controls and for security plan %i...", plan_id)
792
+ supports_multiple_controls: bool = cls.is_multiple_controls_supported()
792
793
 
793
794
  # Define fields based on version
794
795
  fields = """
@@ -797,7 +798,7 @@ class Issue(RegScaleModel):
797
798
  otherIdentifier
798
799
  {extra_fields}
799
800
  """.format(
800
- extra_fields="controlImplementations { id }" if cls.is_multiple_controls_supported() else ""
801
+ extra_fields="controlImplementations { id }" if supports_multiple_controls else ""
801
802
  )
802
803
 
803
804
  while True:
@@ -822,7 +823,7 @@ class Issue(RegScaleModel):
822
823
  items = response.get(cls.get_module_string(), {}).get("items", [])
823
824
 
824
825
  for item in items:
825
- if cls.is_multiple_controls_supported() and item.get("controlImplementations"):
826
+ if supports_multiple_controls and item.get("controlImplementations"):
826
827
  for control in item.get("controlImplementations", []):
827
828
  control_issues[control["id"]].append(
828
829
  OpenIssueDict(id=item["id"], otherIdentifier=item["otherIdentifier"])
@@ -51,7 +51,7 @@ class PlanCacheMixin(Generic[T]):
51
51
  objects = cls.get_plan_objects(plan_id)
52
52
  for obj in objects:
53
53
  cls.cache_object(obj)
54
- logger.info("Cached %s %s objects", len(objects), cls.__name__)
54
+ logger.debug("Cached %s %s objects.", len(objects), cls.__name__)
55
55
 
56
56
  @classmethod
57
57
  def get_plan_objects(cls: Type[T], plan_id: int) -> List[T]:
@@ -53,6 +53,7 @@ class RegScaleModel(BaseModel, ABC):
53
53
  _original_data: Optional[Dict[str, Any]] = None
54
54
 
55
55
  # Caching
56
+ _disable_cache: bool = False # flag to disable caching
56
57
  _object_cache: ClassVar[Cache] = Cache(maxsize=100000)
57
58
  _parent_cache: ClassVar[Cache] = Cache(maxsize=50000)
58
59
  _lock_registry: ClassVar[ThreadSafeDict] = ThreadSafeDict()
@@ -80,9 +81,46 @@ class RegScaleModel(BaseModel, ABC):
80
81
  super().__init__(*args, **data)
81
82
  # Capture initial state after initialization
82
83
  self._original_data = self.dict(exclude_unset=True)
84
+ self._disable_cache = self._fetch_disabled_cache_setting()
85
+ if self._disable_cache:
86
+ logger.debug("cache is disabled")
83
87
  except Exception as e:
84
88
  logger.error(f"Error creating {self.__class__.__name__}: {e} {data}", exc_info=True)
85
89
 
90
+ def _fetch_disabled_cache_setting(self) -> bool:
91
+ """
92
+ Check if caching is disabled based on the application config.
93
+
94
+ :return: True if caching is disabled, False otherwise
95
+ :rtype: bool
96
+ """
97
+ is_disabled = False
98
+ if config := self._get_api_handler().config:
99
+ is_disabled = config.get("disableCache", False)
100
+ return is_disabled
101
+
102
+ @classmethod
103
+ def disable_cache(cls) -> bool:
104
+ """
105
+ Disable caching for the model.
106
+
107
+ :return: True if caching is disabled, False otherwise
108
+ :rtype: bool
109
+ """
110
+ cls._disable_cache = True
111
+ return cls._disable_cache
112
+
113
+ @classmethod
114
+ def enable_cache(cls) -> bool:
115
+ """
116
+ Enable caching for the model.
117
+
118
+ :return: True if caching is enabled, False otherwise
119
+ :rtype: bool
120
+ """
121
+ cls._disable_cache = False
122
+ return cls._disable_cache
123
+
86
124
  @classmethod
87
125
  def _get_api_handler(cls) -> APIHandler:
88
126
  """
@@ -169,6 +207,8 @@ class RegScaleModel(BaseModel, ABC):
169
207
  :return: The cached object if found, None otherwise
170
208
  :rtype: Optional[T]
171
209
  """
210
+ if cls._disable_cache:
211
+ return None
172
212
  with cls._get_lock(cache_key):
173
213
  return cls._object_cache.get(cache_key)
174
214
 
@@ -181,6 +221,8 @@ class RegScaleModel(BaseModel, ABC):
181
221
  :return: None
182
222
  :rtype: None
183
223
  """
224
+ if cls._disable_cache:
225
+ return
184
226
  try:
185
227
  if not obj:
186
228
  return
@@ -229,6 +271,8 @@ class RegScaleModel(BaseModel, ABC):
229
271
  :return: None
230
272
  :rtype: None
231
273
  """
274
+ if cls._disable_cache:
275
+ return
232
276
  parent_id = getattr(obj, cls._parent_id_field, None)
233
277
  parent_module = getattr(obj, "parentModule", getattr(obj, "parent_module", ""))
234
278
  if parent_id and parent_module:
@@ -252,6 +296,8 @@ class RegScaleModel(BaseModel, ABC):
252
296
  :return: None
253
297
  :rtype: None
254
298
  """
299
+ if cls._disable_cache:
300
+ return
255
301
  with cls._get_lock(cache_key):
256
302
  for obj in objects:
257
303
  cls.cache_object(obj)
@@ -265,6 +311,8 @@ class RegScaleModel(BaseModel, ABC):
265
311
  :return: None
266
312
  :rtype: None
267
313
  """
314
+ if cls._disable_cache:
315
+ return
268
316
  cls._object_cache.clear()
269
317
 
270
318
  @classmethod
@@ -276,6 +324,8 @@ class RegScaleModel(BaseModel, ABC):
276
324
  :return: None
277
325
  :rtype: None
278
326
  """
327
+ if cls._disable_cache:
328
+ return
279
329
  cache_key = cls._get_cache_key(obj)
280
330
  with cls._get_lock(cache_key):
281
331
  cls._object_cache.delete(cache_key)
@@ -1058,12 +1108,12 @@ class RegScaleModel(BaseModel, ABC):
1058
1108
  return endpoint
1059
1109
 
1060
1110
  @classmethod
1061
- def _get_pending_updates(cls) -> Set[int]:
1111
+ def _get_pending_updates(cls) -> Set[Union[int, str]]:
1062
1112
  """
1063
1113
  Get the set of pending updates for the class.
1064
1114
 
1065
1115
  :return: Set of pending update IDs
1066
- :rtype: Set[int]
1116
+ :rtype: Set[Union[int, str]]
1067
1117
  """
1068
1118
  class_name = cls.__name__
1069
1119
  if class_name not in cls._pending_updates:
@@ -1138,18 +1188,34 @@ class RegScaleModel(BaseModel, ABC):
1138
1188
  # Handle updates
1139
1189
  pending_updates = cls._get_pending_updates()
1140
1190
  if pending_updates:
1141
- logger.info(f"Performing bulk update for {len(pending_updates)} {cls.__name__} objects")
1142
- objects_to_update = [cls.get_cached_object(cache_key=cache_key) for cache_key in pending_updates]
1191
+ logger.debug(f"Analyzing {len(pending_updates)} {cls.__name__} objects for bulk update...")
1192
+ objects_to_update = [
1193
+ cls.get_cached_object(cache_key=cache_key)
1194
+ for cache_key in pending_updates
1195
+ if cls.get_cached_object(cache_key=cache_key)
1196
+ ]
1197
+ logger.debug(
1198
+ f"{len(objects_to_update)}/{len(pending_updates)} {cls.__name__} objects qualify for bulk update."
1199
+ )
1143
1200
  if objects_to_update:
1201
+ logger.info(f"Performing bulk update for {len(objects_to_update)} {cls.__name__} objects...")
1144
1202
  result["updated"] = cls.batch_update(items=objects_to_update, progress_context=progress_context)
1145
1203
  pending_updates.clear()
1146
1204
 
1147
1205
  # Handle creations
1148
1206
  pending_creations = cls._get_pending_creations()
1149
1207
  if pending_creations:
1150
- logger.info(f"Performing bulk creation for {len(pending_creations)} {cls.__name__} objects")
1151
- objects_to_create = [cls.get_cached_object(cache_key=cache_key) for cache_key in pending_creations]
1208
+ logger.debug(f"Analyzing {len(pending_creations)} {cls.__name__} objects for bulk creation...")
1209
+ objects_to_create = [
1210
+ cls.get_cached_object(cache_key=cache_key)
1211
+ for cache_key in pending_creations
1212
+ if cls.get_cached_object(cache_key=cache_key)
1213
+ ]
1214
+ logger.debug(
1215
+ f"{len(objects_to_create)}/{len(pending_creations)} {cls.__name__} objects qualify for bulk creation."
1216
+ )
1152
1217
  if objects_to_create:
1218
+ logger.info(f"Performing bulk creation for {len(pending_creations)} {cls.__name__} objects...")
1153
1219
  result["created"] = cls.batch_create(items=objects_to_create, progress_context=progress_context)
1154
1220
  pending_creations.clear()
1155
1221
 
@@ -1437,7 +1503,7 @@ class RegScaleModel(BaseModel, ABC):
1437
1503
  :rtype: Optional[T]
1438
1504
  """
1439
1505
  if response and response.ok:
1440
- logger.info(json.dumps(response.json(), indent=4))
1506
+ logger.debug(json.dumps(response.json(), indent=4))
1441
1507
  return cast(T, cls(**response.json()))
1442
1508
  else:
1443
1509
  cls.log_response_error(response=response, suppress_error=suppress_error)