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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +150 -96
- regscale/models/integration_models/cisa_kev_data.json +154 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
regscale/models/locking.py
CHANGED
|
@@ -90,11 +90,15 @@ class FileLock:
|
|
|
90
90
|
|
|
91
91
|
:rtype: None
|
|
92
92
|
"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
regscale/models/platform.py
CHANGED
|
@@ -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] =
|
|
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[
|
|
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
|
-
#
|
|
164
|
-
|
|
165
|
-
self.responsibility
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
self.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
273
|
+
return responsibility
|
|
249
274
|
|
|
250
|
-
return
|
|
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.
|
|
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 [
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
937
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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 {
|
|
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
|
|
@@ -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
|
-
|
|
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
|