regscale-cli 6.25.1.0__py3-none-any.whl → 6.26.0.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 (80) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +18 -3
  4. regscale/core/app/internal/login.py +0 -1
  5. regscale/core/app/utils/catalog_utils/common.py +1 -1
  6. regscale/integrations/commercial/sicura/api.py +14 -13
  7. regscale/integrations/commercial/sicura/commands.py +8 -2
  8. regscale/integrations/commercial/sicura/scanner.py +49 -39
  9. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  10. regscale/integrations/commercial/wizv2/click.py +26 -26
  11. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  12. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  13. regscale/integrations/compliance_integration.py +67 -2
  14. regscale/integrations/control_matcher.py +358 -0
  15. regscale/integrations/milestone_manager.py +291 -0
  16. regscale/integrations/public/__init__.py +1 -0
  17. regscale/integrations/public/cci_importer.py +37 -38
  18. regscale/integrations/public/fedramp/click.py +60 -2
  19. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  20. regscale/integrations/scanner_integration.py +150 -96
  21. regscale/models/integration_models/cisa_kev_data.json +154 -4
  22. regscale/models/integration_models/nexpose.py +36 -10
  23. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  24. regscale/models/locking.py +12 -8
  25. regscale/models/platform.py +1 -2
  26. regscale/models/regscale_models/control_implementation.py +46 -21
  27. regscale/models/regscale_models/issue.py +256 -94
  28. regscale/models/regscale_models/milestone.py +1 -1
  29. regscale/models/regscale_models/regscale_model.py +6 -1
  30. regscale/templates/__init__.py +0 -0
  31. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  32. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
  33. tests/regscale/integrations/commercial/__init__.py +0 -0
  34. tests/regscale/integrations/commercial/conftest.py +28 -0
  35. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  36. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  37. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  38. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  40. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  41. tests/regscale/integrations/commercial/test_burp.py +48 -0
  42. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  43. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  44. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  45. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  46. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  47. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  48. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  49. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  50. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  51. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  52. tests/regscale/integrations/commercial/test_snow.py +423 -0
  53. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  54. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  55. tests/regscale/integrations/commercial/test_stig.py +33 -0
  56. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  57. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  58. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  59. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  60. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  61. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  62. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  63. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  64. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  65. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  66. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  71. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  72. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  73. tests/regscale/integrations/test_control_matcher.py +1314 -0
  74. tests/regscale/integrations/test_control_matching.py +155 -0
  75. tests/regscale/integrations/test_milestone_manager.py +408 -0
  76. tests/regscale/models/test_issue.py +378 -1
  77. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  78. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  79. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  80. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
@@ -90,11 +90,15 @@ class FileLock:
90
90
 
91
91
  :rtype: None
92
92
  """
93
- if os.path.isfile(self.lock_file):
94
- with open(self.lock_file, "r") as lockfile:
95
- scope = str(lockfile.read().strip())
96
-
97
- if scope == str(self.lock_scope):
98
- os.remove(self.lock_file) # Lock released
99
- else:
100
- pass # Lock can only be released by {self.lock_scope}
93
+ try:
94
+ if os.path.isfile(self.lock_file):
95
+ with open(self.lock_file, "r") as lockfile:
96
+ scope = str(lockfile.read().strip())
97
+
98
+ if scope == str(self.lock_scope):
99
+ os.remove(self.lock_file) # Lock released
100
+ else:
101
+ pass # Lock can only be released by {self.lock_scope}
102
+ except FileNotFoundError:
103
+ # Lock file was already removed by another process, this is acceptable in parallel execution
104
+ pass
@@ -5,10 +5,9 @@ from typing import Optional, Union
5
5
 
6
6
  from pydantic import BaseModel, SecretStr
7
7
 
8
+ from regscale.core.app.api import Api
8
9
  from regscale.core.login import get_regscale_token
9
10
  from regscale.core.utils.urls import generate_regscale_domain_url
10
- from regscale.core.app.api import Api
11
- from regscale.core.app.application import Application
12
11
 
13
12
 
14
13
  class RegScaleAuth(BaseModel):
@@ -10,7 +10,7 @@ from urllib.parse import urljoin
10
10
 
11
11
  import requests
12
12
  from lxml.etree import Element
13
- from pydantic import ConfigDict, Field
13
+ from pydantic import ConfigDict, Field, field_validator
14
14
 
15
15
  from regscale.core.app.api import Api
16
16
  from regscale.core.app.application import Application
@@ -41,6 +41,8 @@ class ControlImplementationStatus(str, Enum):
41
41
 
42
42
 
43
43
  class ImplementationControlOrigin(str, Enum):
44
+ """Control Implementation Origination"""
45
+
44
46
  SERVICE_PROVIDER_CORPORATE = "Service Provider Corporate"
45
47
  SERVICE_PROVIDER_SYSTEM = "Service Provider System Specific"
46
48
  SERVICE_PROVIDER_HYBRID = "Service Provider Hybrid (Corporate and System Specific)"
@@ -63,6 +65,7 @@ class ControlImplementationOrigin(str, Enum):
63
65
  CustomerConfigured = "Customer Configured"
64
66
  CustomerProvided = "Customer"
65
67
  Inherited = "Inherited"
68
+ NotApplicable = "Not Applicable"
66
69
 
67
70
 
68
71
  class ControlImplementation(RegScaleModel):
@@ -85,7 +88,7 @@ class ControlImplementation(RegScaleModel):
85
88
  createdById: Optional[str] = Field(default_factory=RegScaleModel.get_user_id)
86
89
  uuid: Optional[str] = None
87
90
  policy: Optional[str] = None
88
- implementation: Optional[str] = None
91
+ implementation: Optional[str] = Field(default="N/A")
89
92
  dateLastAssessed: Optional[str] = None
90
93
  lastAssessmentResult: Optional[str] = None
91
94
  practiceLevel: Optional[str] = None
@@ -115,7 +118,7 @@ class ControlImplementation(RegScaleModel):
115
118
  dateCreated: Optional[str] = Field(default_factory=get_current_datetime)
116
119
  lastUpdatedById: Optional[str] = Field(default_factory=RegScaleModel.get_user_id)
117
120
  dateLastUpdated: Optional[str] = Field(default_factory=get_current_datetime)
118
- weight: Optional[int] = None
121
+ weight: Optional[float] = None
119
122
  isPublic: Optional[bool] = True
120
123
  inheritable: Optional[bool] = False
121
124
  systemRoleId: Optional[int] = None
@@ -144,6 +147,20 @@ class ControlImplementation(RegScaleModel):
144
147
  maturityLevel: Optional[str] = None
145
148
  assessmentFrequency: int = 0
146
149
 
150
+ @field_validator("implementation", mode="before")
151
+ @classmethod
152
+ def validate_implementation(cls, v: Optional[str]) -> str:
153
+ """
154
+ Validate implementation field - convert empty strings to 'N/A'.
155
+
156
+ :param Optional[str] v: The implementation value
157
+ :return: The validated implementation value
158
+ :rtype: str
159
+ """
160
+ if v is None or (isinstance(v, str) and v.strip() == ""):
161
+ return "N/A"
162
+ return v
163
+
147
164
  def __str__(self):
148
165
  return f"Control Implementation {self.id}: {self.controlID}"
149
166
 
@@ -160,16 +177,21 @@ class ControlImplementation(RegScaleModel):
160
177
  if self.controlOwnersIds is None and self.controlOwnerId:
161
178
  self.controlOwnersIds = [self.controlOwnerId]
162
179
 
163
- # Set intelligent default responsibility if not explicitly set and we have parent info
164
- if (
165
- self.responsibility == self.get_default_responsibility()
166
- and self.parentId
167
- and self.parentModule == "securityplans"
168
- ):
169
- # Try to get a more specific default based on the actual security plan's compliance settings
170
- better_default = self.get_default_responsibility(parent_id=self.parentId)
171
- if better_default != self.responsibility:
172
- self.responsibility = better_default
180
+ # Check if responsibility needs to be set (empty string, None, or default value)
181
+ should_update_responsibility = (
182
+ not self.responsibility # Handles empty string or None
183
+ or self.responsibility == self.get_default_responsibility()
184
+ )
185
+
186
+ if should_update_responsibility:
187
+ if self.parentId and self.parentModule == "securityplans":
188
+ # Try to get a more specific default based on the actual security plan's compliance settings
189
+ better_default = self.get_default_responsibility(parent_id=self.parentId)
190
+ if better_default and better_default != self.responsibility:
191
+ self.responsibility = better_default
192
+ elif not self.responsibility:
193
+ # If still empty/None and no parent info, set to generic default
194
+ self.responsibility = self.get_default_responsibility()
173
195
 
174
196
  def __setattr__(self, name: str, value: Any) -> None:
175
197
  """
@@ -207,8 +229,7 @@ class ControlImplementation(RegScaleModel):
207
229
  actual_compliance_setting_id = compliance_setting_id or cls._get_compliance_setting_id_from_parent(parent_id)
208
230
 
209
231
  if actual_compliance_setting_id:
210
- responsibility = cls._get_responsibility_from_compliance_settings(actual_compliance_setting_id)
211
- if responsibility:
232
+ if responsibility := cls._get_responsibility_from_compliance_settings(actual_compliance_setting_id):
212
233
  return responsibility
213
234
 
214
235
  return cls._get_fallback_responsibility(actual_compliance_setting_id)
@@ -227,7 +248,7 @@ class ControlImplementation(RegScaleModel):
227
248
  try:
228
249
  from regscale.models.regscale_models.security_plan import SecurityPlan
229
250
 
230
- security_plan = SecurityPlan.get_object(parent_id)
251
+ security_plan: SecurityPlan = SecurityPlan.get_object(parent_id)
231
252
  return security_plan.complianceSettingsId if security_plan else None
232
253
  except Exception:
233
254
  return None
@@ -240,14 +261,18 @@ class ControlImplementation(RegScaleModel):
240
261
 
241
262
  Cached to avoid repeated API calls for the same compliance setting.
242
263
  """
264
+ responsibility = ControlImplementationOrigin.NotApplicable.value
243
265
  try:
244
266
  from regscale.models.regscale_models.compliance_settings import ComplianceSettings
245
267
 
246
- return ComplianceSettings.get_default_responsibility_for_compliance_setting(compliance_setting_id)
268
+ if cs_responsibility := ComplianceSettings.get_default_responsibility_for_compliance_setting(
269
+ compliance_setting_id
270
+ ):
271
+ responsibility = cs_responsibility
247
272
  except Exception:
248
- pass
273
+ return responsibility
249
274
 
250
- return None
275
+ return responsibility
251
276
 
252
277
  @classmethod
253
278
  def _get_fallback_responsibility(cls, compliance_setting_id: Optional[int] = None) -> str:
@@ -262,7 +287,7 @@ class ControlImplementation(RegScaleModel):
262
287
  return cls._get_framework_default_responsibility(compliance_setting_id)
263
288
 
264
289
  # Ultimate fallback for unknown compliance settings
265
- return ControlImplementationOrigin.ProviderSS.value
290
+ return ControlImplementationOrigin.NotApplicable.value
266
291
 
267
292
  @classmethod
268
293
  def _get_framework_default_responsibility(cls, compliance_setting_id: int) -> str:
@@ -1146,7 +1171,7 @@ class ControlImplementation(RegScaleModel):
1146
1171
  if field_name == "status":
1147
1172
  return [imp_status.value for imp_status in ControlImplementationStatus]
1148
1173
  if field_name == "responsibility":
1149
- return ["Provider", "Customer", "Shared", "Not Applicable"]
1174
+ return [origin.value for origin in ControlImplementationOrigin]
1150
1175
  return cls.get_bool_enums(field_name)
1151
1176
 
1152
1177
  @classmethod
@@ -84,6 +84,35 @@ class IssueStatus(str, Enum):
84
84
  return self.value
85
85
 
86
86
 
87
+ class IssueIdentification(str, Enum):
88
+ """Issue Identification"""
89
+
90
+ A123Review = "A-123 Review"
91
+ AssessmentAuditInternal = "Assessment/Audit (Internal)"
92
+ AssessmentAuditExternal = "Assessment/Audit (External)"
93
+ CriticalControlReview = "Critical Control Review"
94
+ FDCCUSGCB = "FDCC/USGCB"
95
+ GAOAudit = "GAO Audit"
96
+ IGAudit = "IG Audit"
97
+ IncidentResponseLessonsLearned = "Incident Response Lessons Learned"
98
+ ITAR = "ITAR"
99
+ PenetrationTest = "Penetration Test"
100
+ RiskAssessment = "Risk Assessment"
101
+ SecurityAuthorization = "Security Authorization"
102
+ SecurityControlAssessment = "Security Control Assessment"
103
+ VulnerabilityAssessment = "Vulnerability Assessment"
104
+ Other = "Other"
105
+
106
+ def __str__(self) -> str:
107
+ """
108
+ Return the value of the Enum as a string
109
+
110
+ :return: The value of the Enum as a string
111
+ :rtype: str
112
+ """
113
+ return self.value
114
+
115
+
87
116
  class Issue(RegScaleModel):
88
117
  """Issue Model"""
89
118
 
@@ -917,103 +946,252 @@ class Issue(RegScaleModel):
917
946
  """
918
947
  import logging
919
948
 
920
- cache_disabled = cls._is_cache_disabled()
921
- use_cache: bool = not cache_disabled
922
-
923
949
  logger = logging.getLogger("regscale")
950
+
924
951
  # Check cache first
925
- if use_cache:
926
- cached_data = cls._get_from_cache(plan_id)
927
- if cached_data is not None:
928
- logger.info(f"Using cached open issues data for security plan {plan_id}")
929
- return cached_data
930
-
931
- # Performance optimization: Use larger batch size and optimize query
932
- take = 50 # Increased from 50 to reduce API roundtrips
933
- skip = 0
934
- control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
952
+ cached_data = cls._check_cache(plan_id, logger)
953
+ if cached_data is not None:
954
+ return cached_data
935
955
 
936
- start_time = time.time()
937
- logger.info(f"Fetching open issues for controls and for security plan {plan_id}...")
956
+ # Fetch open issues from API
957
+ control_issues = cls._fetch_open_issues_from_api(plan_id, is_component, logger)
958
+
959
+ # Cache the results if caching is enabled
960
+ if not cls._is_cache_disabled():
961
+ cls._cache_data(plan_id, control_issues)
962
+
963
+ return control_issues
964
+
965
+ @classmethod
966
+ def _check_cache(cls, plan_id: int, logger) -> Optional[Dict[int, List[OpenIssueDict]]]:
967
+ """
968
+ Check cache for open issues data
969
+
970
+ :param int plan_id: The ID of the parent
971
+ :param logger: Logger instance
972
+ :return: Cached data if available and valid, None otherwise
973
+ :rtype: Optional[Dict[int, List[OpenIssueDict]]]
974
+ """
975
+ if cls._is_cache_disabled():
976
+ return None
977
+
978
+ cached_data = cls._get_from_cache(plan_id)
979
+ if cached_data is not None:
980
+ logger.info(f"Using cached open issues data for security plan {plan_id}")
981
+ return cached_data
982
+
983
+ @classmethod
984
+ def _fetch_open_issues_from_api(cls, plan_id: int, is_component: bool, logger) -> Dict[int, List[OpenIssueDict]]:
985
+ """
986
+ Fetch open issues from API with pagination
938
987
 
988
+ :param int plan_id: The ID of the parent
989
+ :param bool is_component: Whether parent is a component
990
+ :param logger: Logger instance
991
+ :return: Dictionary of control IDs to open issues
992
+ :rtype: Dict[int, List[OpenIssueDict]]
993
+ """
994
+ start_time = time.time()
939
995
  module_str = "component" if is_component else "security plan"
940
- logger.info(
941
- f"Fetching open issues for controls and for {module_str} {plan_id}...",
942
- )
943
- supports_multiple_controls: bool = cls.is_multiple_controls_supported()
996
+ logger.info(f"Fetching open issues for controls and for {module_str} {plan_id}...")
944
997
 
945
- # Optimize field selection - only get what we need
946
- if supports_multiple_controls:
947
- fields = "id, otherIdentifier, integrationFindingId, controlImplementations { id }"
948
- else:
949
- fields = "id, controlId, otherIdentifier, integrationFindingId"
998
+ control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
999
+
1000
+ try:
1001
+ total_fetched = cls._paginate_and_process_issues(plan_id, is_component, control_issues, logger)
1002
+ cls._log_completion(plan_id, total_fetched, len(control_issues), start_time, logger)
1003
+ except Exception as e:
1004
+ logger.error(f"Error fetching open issues for security plan {plan_id}: {e}")
1005
+ return defaultdict(list)
1006
+
1007
+ return control_issues
950
1008
 
1009
+ @classmethod
1010
+ def _paginate_and_process_issues(
1011
+ cls,
1012
+ plan_id: int,
1013
+ is_component: bool,
1014
+ control_issues: Dict[int, List[OpenIssueDict]],
1015
+ logger,
1016
+ ) -> int:
1017
+ """
1018
+ Paginate through API results and process issues
1019
+
1020
+ :param int plan_id: The ID of the parent
1021
+ :param bool is_component: Whether parent is a component
1022
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate with results
1023
+ :param logger: Logger instance
1024
+ :return: Total number of items fetched
1025
+ :rtype: int
1026
+ """
1027
+ take = 50
1028
+ skip = 0
951
1029
  total_fetched = 0
1030
+ supports_multiple_controls = cls.is_multiple_controls_supported()
1031
+ fields = cls._get_query_fields(supports_multiple_controls)
952
1032
 
953
- try:
954
- while True:
955
- query = f"""
956
- query GetOpenIssuesByPlanOrComponent {{
957
- {cls.get_module_string()}(
958
- skip: {skip},
959
- take: {take},
960
- where: {{
961
- {"componentId" if is_component else "securityPlanId"}: {{eq: {plan_id}}},
962
- status: {{eq: "Open"}}
963
- }}
964
- ) {{
965
- items {{ {fields} }}
966
- pageInfo {{ hasNextPage }}
967
- totalCount
968
- }}
1033
+ while True:
1034
+ query = cls._build_query(plan_id, is_component, skip, take, fields)
1035
+ response = cls._get_api_handler().graph(query)
1036
+
1037
+ items = response.get(cls.get_module_string(), {}).get("items", [])
1038
+ total_count = response.get(cls.get_module_string(), {}).get("totalCount", 0)
1039
+
1040
+ cls._log_progress(skip, take, len(items), total_count, logger)
1041
+ cls._process_issue_items(items, supports_multiple_controls, control_issues)
1042
+
1043
+ total_fetched += len(items)
1044
+
1045
+ if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
1046
+ break
1047
+
1048
+ skip += take
1049
+
1050
+ return total_fetched
1051
+
1052
+ @classmethod
1053
+ def _get_query_fields(cls, supports_multiple_controls: bool) -> str:
1054
+ """
1055
+ Get GraphQL query fields based on control support
1056
+
1057
+ :param bool supports_multiple_controls: Whether multiple controls are supported
1058
+ :return: GraphQL field selection string
1059
+ :rtype: str
1060
+ """
1061
+ if supports_multiple_controls:
1062
+ return "id, otherIdentifier, integrationFindingId, controlImplementations { id }"
1063
+ return "id, controlId, otherIdentifier, integrationFindingId"
1064
+
1065
+ @classmethod
1066
+ def _build_query(cls, plan_id: int, is_component: bool, skip: int, take: int, fields: str) -> str:
1067
+ """
1068
+ Build GraphQL query for fetching open issues
1069
+
1070
+ :param int plan_id: The ID of the parent
1071
+ :param bool is_component: Whether parent is a component
1072
+ :param int skip: Number of items to skip
1073
+ :param int take: Number of items to take
1074
+ :param str fields: GraphQL fields to select
1075
+ :return: GraphQL query string
1076
+ :rtype: str
1077
+ """
1078
+ parent_field = "componentId" if is_component else "securityPlanId"
1079
+ return f"""
1080
+ query GetOpenIssuesByPlanOrComponent {{
1081
+ {cls.get_module_string()}(
1082
+ skip: {skip},
1083
+ take: {take},
1084
+ where: {{
1085
+ {parent_field}: {{eq: {plan_id}}},
1086
+ status: {{eq: "Open"}}
969
1087
  }}
970
- """
1088
+ ) {{
1089
+ items {{ {fields} }}
1090
+ pageInfo {{ hasNextPage }}
1091
+ totalCount
1092
+ }}
1093
+ }}
1094
+ """
971
1095
 
972
- response = cls._get_api_handler().graph(query)
973
- items = response.get(cls.get_module_string(), {}).get("items", [])
974
- total_count = response.get(cls.get_module_string(), {}).get("totalCount", 0)
1096
+ @classmethod
1097
+ def _log_progress(cls, skip: int, take: int, items_count: int, total_count: int, logger) -> None:
1098
+ """
1099
+ Log progress for large datasets
975
1100
 
976
- # Log progress for large datasets
977
- if total_count > 1000:
978
- logger.info(
979
- f"Processing batch {skip // take + 1} - fetched {len(items)} items ({skip + len(items)}/{total_count})"
980
- )
1101
+ :param int skip: Number of items skipped
1102
+ :param int take: Batch size
1103
+ :param int items_count: Number of items in current batch
1104
+ :param int total_count: Total count of items
1105
+ :param logger: Logger instance
1106
+ :rtype: None
1107
+ """
1108
+ if total_count > 1000:
1109
+ logger.info(
1110
+ f"Processing batch {skip // take + 1} - fetched {items_count} items ({skip + items_count}/{total_count})"
1111
+ )
981
1112
 
982
- for item in items:
983
- issue_dict = OpenIssueDict(
984
- id=item["id"],
985
- otherIdentifier=item.get("otherIdentifier", ""),
986
- integrationFindingId=item.get("integrationFindingId", ""),
987
- )
1113
+ @classmethod
1114
+ def _process_issue_items(
1115
+ cls,
1116
+ items: List[Dict[str, Any]],
1117
+ supports_multiple_controls: bool,
1118
+ control_issues: Dict[int, List[OpenIssueDict]],
1119
+ ) -> None:
1120
+ """
1121
+ Process issue items and populate control_issues dictionary
988
1122
 
989
- if supports_multiple_controls and item.get("controlImplementations"):
990
- for control in item.get("controlImplementations", []):
991
- control_issues[control["id"]].append(issue_dict)
992
- elif item.get("controlId"):
993
- control_issues[item["controlId"]].append(issue_dict)
1123
+ :param List[Dict[str, Any]] items: List of issue items from API
1124
+ :param bool supports_multiple_controls: Whether multiple controls are supported
1125
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1126
+ :rtype: None
1127
+ """
1128
+ for item in items:
1129
+ issue_dict = OpenIssueDict(
1130
+ id=item["id"],
1131
+ otherIdentifier=item.get("otherIdentifier", ""),
1132
+ integrationFindingId=item.get("integrationFindingId", ""),
1133
+ )
994
1134
 
995
- total_fetched += len(items)
996
- if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
997
- break
998
- skip += take
1135
+ if supports_multiple_controls:
1136
+ cls._add_issue_to_multiple_controls(item, issue_dict, control_issues)
1137
+ else:
1138
+ cls._add_issue_to_single_control(item, issue_dict, control_issues)
999
1139
 
1000
- except Exception as e:
1001
- logger.error(f"Error fetching open issues for security plan {plan_id}: {e}")
1002
- # Return empty dict on error to prevent breaking the calling code
1003
- return defaultdict(list)
1140
+ @classmethod
1141
+ def _add_issue_to_multiple_controls(
1142
+ cls,
1143
+ item: Dict[str, Any],
1144
+ issue_dict: OpenIssueDict,
1145
+ control_issues: Dict[int, List[OpenIssueDict]],
1146
+ ) -> None:
1147
+ """
1148
+ Add issue to multiple control implementations
1149
+
1150
+ :param Dict[str, Any] item: Issue item from API
1151
+ :param OpenIssueDict issue_dict: Issue dictionary
1152
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1153
+ :rtype: None
1154
+ """
1155
+ if item.get("controlImplementations"):
1156
+ for control in item.get("controlImplementations", []):
1157
+ control_issues[control["id"]].append(issue_dict)
1004
1158
 
1159
+ @classmethod
1160
+ def _add_issue_to_single_control(
1161
+ cls,
1162
+ item: Dict[str, Any],
1163
+ issue_dict: OpenIssueDict,
1164
+ control_issues: Dict[int, List[OpenIssueDict]],
1165
+ ) -> None:
1166
+ """
1167
+ Add issue to single control
1168
+
1169
+ :param Dict[str, Any] item: Issue item from API
1170
+ :param OpenIssueDict issue_dict: Issue dictionary
1171
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1172
+ :rtype: None
1173
+ """
1174
+ if item.get("controlId"):
1175
+ control_issues[item["controlId"]].append(issue_dict)
1176
+
1177
+ @classmethod
1178
+ def _log_completion(cls, plan_id: int, total_fetched: int, control_count: int, start_time: float, logger) -> None:
1179
+ """
1180
+ Log completion statistics
1181
+
1182
+ :param int plan_id: The ID of the parent
1183
+ :param int total_fetched: Total number of items fetched
1184
+ :param int control_count: Number of controls with issues
1185
+ :param float start_time: Start time of the operation
1186
+ :param logger: Logger instance
1187
+ :rtype: None
1188
+ """
1005
1189
  elapsed_time = time.time() - start_time
1006
1190
  logger.info(
1007
- f"Finished fetching {total_fetched} open issue(s) for {len(control_issues)} control(s) "
1191
+ f"Finished fetching {total_fetched} open issue(s) for {control_count} control(s) "
1008
1192
  f"in security plan {plan_id} - took {elapsed_time:.2f} seconds"
1009
1193
  )
1010
1194
 
1011
- # Cache the results
1012
- if use_cache:
1013
- cls._cache_data(plan_id, control_issues)
1014
-
1015
- return control_issues
1016
-
1017
1195
  @classmethod
1018
1196
  def get_sort_position_dict(cls) -> Dict[str, int]:
1019
1197
  """
@@ -1103,36 +1281,20 @@ class Issue(RegScaleModel):
1103
1281
  }
1104
1282
 
1105
1283
  @classmethod
1106
- def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, str]]:
1284
+ def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, IssueIdentification, str]]:
1107
1285
  """
1108
1286
  Overrides the base method.
1109
1287
 
1110
1288
  :param str field_name: The property name to provide enum values for
1111
1289
  :return: List of enum values or strings
1112
- :rtype: List[Union[IssueSeverity, IssueStatus, str]]
1290
+ :rtype: List[Union[IssueSeverity, IssueStatus, IssueIdentification, str]]
1113
1291
  """
1114
1292
  if field_name == "severityLevel":
1115
1293
  return [severity.__str__() for severity in IssueSeverity]
1116
1294
  if field_name == "status":
1117
1295
  return [status.__str__() for status in IssueStatus]
1118
1296
  if field_name == "identification":
1119
- return [
1120
- "A-123 Review",
1121
- "Assessment/Audit (External)",
1122
- "Assessment/Audit (Internal)",
1123
- "Critical Control Review",
1124
- "FDCC/USGCB",
1125
- "GAO Audit",
1126
- "IG Audit",
1127
- "Incidnet Response Lessons Learned",
1128
- "ITAR",
1129
- "Other",
1130
- "Penetration Test",
1131
- "Risk Assessment",
1132
- "Security Authorization",
1133
- "Security Control Assessment",
1134
- "Vulnerability Assessment",
1135
- ]
1297
+ return [identification.__str__() for identification in IssueIdentification]
1136
1298
  return cls.get_bool_enums(field_name)
1137
1299
 
1138
1300
  @classmethod
@@ -15,7 +15,7 @@ class Milestone(RegScaleModel):
15
15
 
16
16
  _module_slug = "milestones"
17
17
  _module_string = "milestones"
18
- _unique_fields = ["title", "parentModule"]
18
+ _unique_fields = ["title", "parentModule", "parentID"]
19
19
  _parent_id_field = "parentID"
20
20
 
21
21
  title: str
@@ -60,6 +60,7 @@ class RegScaleModel(BaseModel, ABC):
60
60
 
61
61
  _pending_updates: ClassVar[Dict[str, Set[int]]] = {}
62
62
  _pending_creations: ClassVar[Dict[str, Set[str]]] = {}
63
+ _ignore_has_changed: bool = False
63
64
 
64
65
  id: int = 0
65
66
  extra_data: Dict[str, Any] = Field(default={}, exclude=True)
@@ -1309,7 +1310,11 @@ class RegScaleModel(BaseModel, ABC):
1309
1310
  """
1310
1311
  # Check if the model has change tracking and if there are changes
1311
1312
  has_change_tracking = hasattr(self, "has_changed") and callable(getattr(self, "has_changed", None))
1312
- should_save = not has_change_tracking or self.has_changed()
1313
+
1314
+ if hasattr(self, "_ignore_has_changed") and self._ignore_has_changed:
1315
+ should_save = True
1316
+ else:
1317
+ should_save = not has_change_tracking or self.has_changed()
1313
1318
 
1314
1319
  if should_save:
1315
1320
  if bulk:
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.25.1.0
3
+ Version: 6.26.0.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