regscale-cli 6.16.0.0__py3-none-any.whl → 6.16.1.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 (45) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/utils/app_utils.py +1 -1
  4. regscale/core/app/utils/parser_utils.py +2 -2
  5. regscale/integrations/commercial/azure/intune.py +1 -0
  6. regscale/integrations/commercial/nessus/scanner.py +3 -0
  7. regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +4 -0
  8. regscale/integrations/commercial/sap/tenable/click.py +1 -1
  9. regscale/integrations/commercial/sap/tenable/scanner.py +8 -2
  10. regscale/integrations/commercial/tenablev2/click.py +39 -16
  11. regscale/integrations/commercial/wizv2/click.py +9 -21
  12. regscale/integrations/commercial/wizv2/scanner.py +2 -1
  13. regscale/integrations/commercial/wizv2/utils.py +145 -69
  14. regscale/integrations/public/fedramp/import_workbook.py +1 -1
  15. regscale/integrations/public/fedramp/poam/scanner.py +51 -44
  16. regscale/integrations/public/fedramp/ssp_logger.py +6 -6
  17. regscale/integrations/scanner_integration.py +96 -23
  18. regscale/models/app_models/mapping.py +3 -3
  19. regscale/models/integration_models/amazon_models/inspector.py +15 -17
  20. regscale/models/integration_models/aqua.py +1 -5
  21. regscale/models/integration_models/cisa_kev_data.json +85 -10
  22. regscale/models/integration_models/ecr_models/ecr.py +2 -6
  23. regscale/models/integration_models/flat_file_importer.py +7 -4
  24. regscale/models/integration_models/grype_import.py +3 -3
  25. regscale/models/integration_models/prisma.py +3 -3
  26. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  27. regscale/models/integration_models/synqly_models/connectors/assets.py +1 -0
  28. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +2 -0
  29. regscale/models/integration_models/tenable_models/integration.py +4 -3
  30. regscale/models/integration_models/trivy_import.py +1 -1
  31. regscale/models/integration_models/xray.py +1 -1
  32. regscale/models/regscale_models/__init__.py +2 -0
  33. regscale/models/regscale_models/control_implementation.py +18 -44
  34. regscale/models/regscale_models/inherited_control.py +61 -0
  35. regscale/models/regscale_models/issue.py +3 -2
  36. regscale/models/regscale_models/mixins/parent_cache.py +1 -1
  37. regscale/models/regscale_models/regscale_model.py +72 -6
  38. regscale/models/regscale_models/vulnerability.py +40 -8
  39. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.1.0.dist-info}/METADATA +1 -1
  40. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.1.0.dist-info}/RECORD +45 -44
  41. tests/regscale/core/test_logz.py +8 -0
  42. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.1.0.dist-info}/LICENSE +0 -0
  43. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.1.0.dist-info}/WHEEL +0 -0
  44. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.1.0.dist-info}/entry_points.txt +0 -0
  45. {regscale_cli-6.16.0.0.dist-info → regscale_cli-6.16.1.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
 
@@ -5,7 +5,6 @@ This module contains the Tenable SC Integration class that is responsible for fe
5
5
  import logging
6
6
  from typing import Any, Iterator, List, Optional, Tuple
7
7
 
8
- from regscale.core.app.application import Application
9
8
  from regscale.core.app.utils.app_utils import epoch_to_datetime
10
9
  from regscale.integrations.commercial.tenablev2.utils import get_filtered_severities
11
10
  from regscale.integrations.integration_override import IntegrationOverride
@@ -13,6 +12,8 @@ from regscale.integrations.scanner_integration import IntegrationAsset, Integrat
13
12
  from regscale.models import regscale_models
14
13
  from regscale.models.integration_models.tenable_models.models import TenableAsset
15
14
 
15
+ logger = logging.getLogger("regscale")
16
+
16
17
 
17
18
  class SCIntegration(ScannerIntegration):
18
19
  """
@@ -38,7 +39,7 @@ class SCIntegration(ScannerIntegration):
38
39
  :param dict kwargs: Additional keyword arguments
39
40
  :yields: Iterator[IntegrationAsset]
40
41
  """
41
- integration_assets = kwargs.get("integration_assets")
42
+ integration_assets = kwargs.get("integration_assets", [])
42
43
  yield from integration_assets
43
44
 
44
45
  def fetch_findings(self, *args: Tuple, **kwargs: dict) -> Iterator[IntegrationFinding]:
@@ -50,7 +51,7 @@ class SCIntegration(ScannerIntegration):
50
51
  :yields: Iterator[IntegrationFinding]
51
52
 
52
53
  """
53
- integration_findings = kwargs.get("integration_findings")
54
+ integration_findings = kwargs.get("integration_findings", [])
54
55
  yield from integration_findings
55
56
 
56
57
  def parse_findings(self, vuln: TenableAsset, integration_mapping: Any) -> List[IntegrationFinding]:
@@ -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
 
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  """Model for Vulnerability in the application"""
4
+ import logging
4
5
  from enum import Enum
5
6
  from typing import List, Optional, Union
6
7
  from urllib.parse import urljoin
7
8
  from warnings import warn
8
9
 
9
- from pydantic import ConfigDict, Field
10
+ from pydantic import ConfigDict, Field, field_validator
10
11
  from requests import Response
11
12
 
12
13
  from regscale.core.app.api import Api
@@ -16,6 +17,9 @@ from regscale.models import regscale_models
16
17
  from regscale.models.regscale_models.regscale_model import RegScaleModel
17
18
 
18
19
 
20
+ logger = logging.getLogger("regscale")
21
+
22
+
19
23
  def deprecated(message):
20
24
  def decorator(func):
21
25
  def wrapper(*args, **kwargs):
@@ -48,7 +52,7 @@ class Vulnerability(RegScaleModel):
48
52
 
49
53
  _module_slug = "vulnerability"
50
54
  _unique_fields = [
51
- ["plugInName", "parentId", "parentModule"],
55
+ ["plugInName", "plugInId", "parentId", "parentModule"],
52
56
  ]
53
57
 
54
58
  id: int = 0
@@ -64,14 +68,14 @@ class Vulnerability(RegScaleModel):
64
68
  firstSeen: Optional[str] = None
65
69
  daysOpen: Optional[int] = None
66
70
  dns: Optional[str] = None
67
- ipAddress: Optional[str] = Field(default="")
71
+ ipAddress: Optional[str] = ""
68
72
  mitigated: Optional[bool] = None
69
73
  operatingSystem: Optional[str] = None
70
74
  port: Optional[Union[str, int]] = None
71
75
  protocol: Optional[str] = None
72
76
  severity: Optional[Union[str, VulnerabilitySeverity]] = None
73
77
  plugInName: Optional[str] = None
74
- plugInId: Optional[int] = None
78
+ plugInId: Optional[Union[int, str]] = None
75
79
  cve: Optional[str] = None
76
80
  vprScore: Optional[Union[int, float]] = None
77
81
  exploitAvailable: Optional[bool] = None
@@ -84,10 +88,37 @@ class Vulnerability(RegScaleModel):
84
88
  dateClosed: Optional[str] = None
85
89
  status: Optional[VulnerabilityStatus] = VulnerabilityStatus.Open
86
90
 
87
- def __eq__(self, other) -> bool:
91
+ @field_validator("plugInId")
92
+ def validate_regscale_version_and_plugin_id_type(cls, v: Union[int, str]) -> Union[int, str, None]:
93
+ """
94
+ Validate the RegScale version and if plugInId should be a string or an integer, >= 6.16 is a string
95
+
96
+ :param int v: pluginId value
97
+ :return: The correct type for plugInId depending on the RegScale version, None if unable to determine
98
+ :rtype: Union[int, str, None]
99
+ """
100
+ from packaging.version import Version
101
+
102
+ try:
103
+ regscale_version = cls._get_api_handler().regscale_version
104
+
105
+ if len(regscale_version) >= 10 or Version(regscale_version) >= Version("6.16.0.0"):
106
+ return str(v) if v else None
107
+ else:
108
+ if isinstance(v, str):
109
+ return int(v) if v.isdigit() else None
110
+ elif isinstance(v, int):
111
+ return v
112
+ return None
113
+ except Exception as e:
114
+ logger.debug("Unable to determine RegScale version or pluginId type: %s", str(e))
115
+ return None
116
+
117
+ def __eq__(self, other: "Vulnerability") -> bool:
88
118
  """
89
119
  Check if two Vulnerability objects are equal is needed for comparison operations in flat file importer
90
- :param other:
120
+
121
+ :param Vulnerability other: the other Vulnerability object to compare
91
122
  :return: True if the two objects are equal, False otherwise
92
123
  :rtype: bool
93
124
  """
@@ -95,13 +126,14 @@ class Vulnerability(RegScaleModel):
95
126
  return False
96
127
  return (
97
128
  self.plugInName == other.plugInName
129
+ and self.plugInId == other.plugInId
98
130
  and self.parentId == other.parentId
99
131
  and self.parentModule == other.parentModule
100
132
  and self.ipAddress == other.ipAddress
101
133
  and self.dns == other.dns
102
134
  )
103
135
 
104
- def __init__(self, **data):
136
+ def __init__(self, *args, **data):
105
137
  # Map snake_case to camelCase before initialization
106
138
  field_mappings = {
107
139
  "scan_id": "scanId",
@@ -128,7 +160,7 @@ class Vulnerability(RegScaleModel):
128
160
  if snake_key in data:
129
161
  data[camel_key] = data.pop(snake_key)
130
162
 
131
- super().__init__(**data)
163
+ super().__init__(*args, **data)
132
164
 
133
165
  @staticmethod
134
166
  def _get_additional_endpoints() -> ConfigDict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.16.0.0
3
+ Version: 6.16.1.0
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