regscale-cli 6.21.0.0__py3-none-any.whl → 6.21.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +7 -0
- regscale/integrations/commercial/__init__.py +9 -10
- regscale/integrations/commercial/amazon/common.py +79 -2
- regscale/integrations/commercial/aws/cli.py +183 -9
- regscale/integrations/commercial/aws/scanner.py +544 -9
- regscale/integrations/commercial/cpe.py +18 -1
- regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
- regscale/integrations/commercial/microsoft_defender/__init__.py +0 -0
- regscale/integrations/commercial/{defender.py → microsoft_defender/defender.py} +38 -612
- regscale/integrations/commercial/microsoft_defender/defender_api.py +286 -0
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +80 -0
- regscale/integrations/commercial/microsoft_defender/defender_scanner.py +168 -0
- regscale/integrations/commercial/qualys/__init__.py +24 -86
- regscale/integrations/commercial/qualys/containers.py +2 -0
- regscale/integrations/commercial/qualys/scanner.py +7 -2
- regscale/integrations/commercial/sonarcloud.py +110 -71
- regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
- regscale/integrations/commercial/wizv2/async_client.py +10 -3
- regscale/integrations/commercial/wizv2/click.py +105 -26
- regscale/integrations/commercial/wizv2/constants.py +249 -1
- regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
- regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
- regscale/integrations/commercial/wizv2/issue.py +2 -2
- regscale/integrations/commercial/wizv2/parsers.py +3 -2
- regscale/integrations/commercial/wizv2/policy_compliance.py +3057 -0
- regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
- regscale/integrations/commercial/wizv2/scanner.py +19 -25
- regscale/integrations/commercial/wizv2/utils.py +258 -85
- regscale/integrations/commercial/wizv2/variables.py +4 -3
- regscale/integrations/compliance_integration.py +1607 -0
- regscale/integrations/public/fedramp/fedramp_five.py +93 -8
- regscale/integrations/public/fedramp/markdown_parser.py +7 -1
- regscale/integrations/scanner_integration.py +57 -6
- regscale/models/__init__.py +1 -1
- regscale/models/app_models/__init__.py +1 -0
- regscale/models/integration_models/cisa_kev_data.json +103 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
- regscale/models/regscale_models/file.py +4 -0
- regscale/models/regscale_models/issue.py +151 -8
- regscale/models/regscale_models/regscale_model.py +4 -2
- regscale/models/regscale_models/security_plan.py +1 -1
- regscale/utils/graphql_client.py +3 -1
- {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +9 -9
- {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +52 -44
- tests/regscale/core/test_version_regscale.py +5 -3
- tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
- tests/regscale/test_authorization.py +0 -65
- tests/regscale/test_init.py +0 -96
- {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Helper classes and utilities for Wiz Policy Compliance Integration."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
|
|
10
|
+
from regscale.core.app.utils.app_utils import get_current_datetime, regscale_string_to_datetime
|
|
11
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
12
|
+
from regscale.models import regscale_models
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("regscale")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ControlAssessmentResult:
|
|
19
|
+
"""Result of a control assessment operation."""
|
|
20
|
+
|
|
21
|
+
control_id: str
|
|
22
|
+
implementation_id: Optional[int]
|
|
23
|
+
assessment_id: Optional[int]
|
|
24
|
+
result: str
|
|
25
|
+
asset_count: int
|
|
26
|
+
created: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class IssueProcessingResult:
|
|
31
|
+
"""Result of issue processing operation."""
|
|
32
|
+
|
|
33
|
+
control_id: Optional[str]
|
|
34
|
+
implementation_id: Optional[int]
|
|
35
|
+
assessment_id: Optional[int]
|
|
36
|
+
success: bool
|
|
37
|
+
error_message: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ControlImplementationCache:
|
|
41
|
+
"""Cache for control implementation lookups to avoid repeated database queries."""
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self._impl_id_by_control: Dict[str, int] = {}
|
|
45
|
+
self._assessment_by_impl_today: Dict[int, regscale_models.Assessment] = {}
|
|
46
|
+
self._security_control_cache: Dict[int, regscale_models.SecurityControl] = {}
|
|
47
|
+
self._loaded = False
|
|
48
|
+
|
|
49
|
+
def get_implementation_id(self, control_id: str) -> Optional[int]:
|
|
50
|
+
"""
|
|
51
|
+
Get control implementation ID for normalized control ID.
|
|
52
|
+
|
|
53
|
+
:param control_id: Normalized control ID (e.g., 'AC-2(1)')
|
|
54
|
+
:return: Control implementation ID if found, None otherwise
|
|
55
|
+
"""
|
|
56
|
+
return self._impl_id_by_control.get(control_id)
|
|
57
|
+
|
|
58
|
+
def set_implementation_id(self, control_id: str, impl_id: int) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Cache control implementation ID.
|
|
61
|
+
|
|
62
|
+
:param control_id: Normalized control ID (e.g., 'AC-2(1)')
|
|
63
|
+
:param impl_id: Control implementation ID to cache
|
|
64
|
+
"""
|
|
65
|
+
self._impl_id_by_control[control_id] = impl_id
|
|
66
|
+
|
|
67
|
+
def get_assessment(self, impl_id: int) -> Optional[regscale_models.Assessment]:
|
|
68
|
+
"""
|
|
69
|
+
Get assessment for implementation ID.
|
|
70
|
+
|
|
71
|
+
:param impl_id: Control implementation ID
|
|
72
|
+
:return: Cached assessment object if found, None otherwise
|
|
73
|
+
"""
|
|
74
|
+
return self._assessment_by_impl_today.get(impl_id)
|
|
75
|
+
|
|
76
|
+
def set_assessment(self, impl_id: int, assessment: regscale_models.Assessment) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Cache assessment for implementation.
|
|
79
|
+
|
|
80
|
+
:param impl_id: Control implementation ID
|
|
81
|
+
:param assessment: Assessment object to cache
|
|
82
|
+
"""
|
|
83
|
+
self._assessment_by_impl_today[impl_id] = assessment
|
|
84
|
+
|
|
85
|
+
def get_security_control(self, control_id: int) -> Optional[regscale_models.SecurityControl]:
|
|
86
|
+
"""
|
|
87
|
+
Get cached security control.
|
|
88
|
+
|
|
89
|
+
:param control_id: Security control ID
|
|
90
|
+
:return: Cached security control object if found, None otherwise
|
|
91
|
+
"""
|
|
92
|
+
return self._security_control_cache.get(control_id)
|
|
93
|
+
|
|
94
|
+
def set_security_control(self, control_id: int, security_control: regscale_models.SecurityControl) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Cache security control.
|
|
97
|
+
|
|
98
|
+
:param control_id: Security control ID
|
|
99
|
+
:param security_control: Security control object to cache
|
|
100
|
+
"""
|
|
101
|
+
self._security_control_cache[control_id] = security_control
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def implementation_count(self) -> int:
|
|
105
|
+
"""
|
|
106
|
+
Number of cached implementations.
|
|
107
|
+
|
|
108
|
+
:return: Count of cached control implementation mappings
|
|
109
|
+
"""
|
|
110
|
+
return len(self._impl_id_by_control)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def assessment_count(self) -> int:
|
|
114
|
+
"""
|
|
115
|
+
Number of cached assessments.
|
|
116
|
+
|
|
117
|
+
:return: Count of cached assessment objects
|
|
118
|
+
"""
|
|
119
|
+
return len(self._assessment_by_impl_today)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AssetConsolidator:
|
|
123
|
+
"""Handles consolidation of asset identifiers for findings."""
|
|
124
|
+
|
|
125
|
+
MAX_DISPLAY_ASSETS = 10
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def create_consolidated_asset_identifier(asset_mappings: Dict[str, Dict[str, str]]) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Create a consolidated asset identifier from asset mappings.
|
|
131
|
+
|
|
132
|
+
:param asset_mappings: Dict mapping resource IDs to asset info
|
|
133
|
+
:return: Consolidated asset identifier string
|
|
134
|
+
"""
|
|
135
|
+
if not asset_mappings:
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
# Create clean format: "Asset Name (wiz-resource-id)"
|
|
139
|
+
identifiers = []
|
|
140
|
+
for resource_id, info in asset_mappings.items():
|
|
141
|
+
asset_name = info.get("name", resource_id)
|
|
142
|
+
identifier = f"{asset_name} ({resource_id})"
|
|
143
|
+
identifiers.append(identifier)
|
|
144
|
+
|
|
145
|
+
# Sort by asset name for consistency
|
|
146
|
+
identifiers.sort(key=lambda x: x.split(" (")[0])
|
|
147
|
+
|
|
148
|
+
return "\n".join(identifiers)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def update_finding_description_for_multiple_assets(
|
|
152
|
+
finding: IntegrationFinding, asset_count: int, asset_names: List[str]
|
|
153
|
+
) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Update finding description to indicate multiple affected assets.
|
|
156
|
+
|
|
157
|
+
:param finding: Finding to update
|
|
158
|
+
:param asset_count: Number of affected assets
|
|
159
|
+
:param asset_names: List of asset names
|
|
160
|
+
"""
|
|
161
|
+
if asset_count <= 1:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
display_names = asset_names[: AssetConsolidator.MAX_DISPLAY_ASSETS]
|
|
165
|
+
description_suffix = f"\n\nThis control failure affects {asset_count} assets: {', '.join(display_names)}"
|
|
166
|
+
|
|
167
|
+
if asset_count > AssetConsolidator.MAX_DISPLAY_ASSETS:
|
|
168
|
+
remaining = asset_count - AssetConsolidator.MAX_DISPLAY_ASSETS
|
|
169
|
+
description_suffix += f" (and {remaining} more)"
|
|
170
|
+
|
|
171
|
+
finding.description = f"{finding.description}{description_suffix}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class IssueFieldSetter:
|
|
175
|
+
"""Handles setting control and assessment IDs on issues."""
|
|
176
|
+
|
|
177
|
+
def __init__(self, cache: ControlImplementationCache, plan_id: int, parent_module: str) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Initialize the issue field setter.
|
|
180
|
+
|
|
181
|
+
:param cache: Control implementation cache for lookups
|
|
182
|
+
:param plan_id: RegScale security plan ID
|
|
183
|
+
:param parent_module: Parent module name (e.g., 'securityplans')
|
|
184
|
+
"""
|
|
185
|
+
self.cache = cache
|
|
186
|
+
self.plan_id = plan_id
|
|
187
|
+
self.parent_module = parent_module
|
|
188
|
+
|
|
189
|
+
def set_control_and_assessment_ids(self, issue: regscale_models.Issue, control_id: str) -> IssueProcessingResult:
|
|
190
|
+
"""
|
|
191
|
+
Set control implementation and assessment IDs on an issue.
|
|
192
|
+
|
|
193
|
+
:param issue: Issue to update
|
|
194
|
+
:param control_id: Normalized control ID
|
|
195
|
+
:return: Result of the operation
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
# Get or find control implementation ID
|
|
199
|
+
impl_id = self._get_or_find_implementation_id(control_id)
|
|
200
|
+
if not impl_id:
|
|
201
|
+
return IssueProcessingResult(
|
|
202
|
+
control_id=control_id,
|
|
203
|
+
implementation_id=None,
|
|
204
|
+
assessment_id=None,
|
|
205
|
+
success=False,
|
|
206
|
+
error_message=f"No control implementation found for control '{control_id}'",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Set control implementation ID
|
|
210
|
+
issue.controlId = impl_id
|
|
211
|
+
|
|
212
|
+
# Get or find assessment ID
|
|
213
|
+
assess_id = self._get_or_find_assessment_id(impl_id)
|
|
214
|
+
if assess_id:
|
|
215
|
+
issue.assessmentId = assess_id
|
|
216
|
+
|
|
217
|
+
# Verify the field is set correctly
|
|
218
|
+
if not (hasattr(issue, "assessmentId") and issue.assessmentId == assess_id):
|
|
219
|
+
logger.error(
|
|
220
|
+
f"❌ VERIFICATION FAILED: Expected {assess_id}, got {getattr(issue, 'assessmentId', 'NO_ATTR')}"
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
logger.warning(
|
|
224
|
+
f"⚠️ No assessment found for control implementation {impl_id} (control '{control_id}') - assessmentId will not be set"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return IssueProcessingResult(
|
|
228
|
+
control_id=control_id, implementation_id=impl_id, assessment_id=assess_id, success=True
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error setting control and assessment IDs: {e}")
|
|
233
|
+
return IssueProcessingResult(
|
|
234
|
+
control_id=control_id, implementation_id=None, assessment_id=None, success=False, error_message=str(e)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def _get_or_find_implementation_id(self, control_id: str) -> Optional[int]:
|
|
238
|
+
"""
|
|
239
|
+
Get implementation ID from cache or database.
|
|
240
|
+
|
|
241
|
+
:param control_id: Normalized control ID to search for
|
|
242
|
+
:return: Control implementation ID if found, None otherwise
|
|
243
|
+
"""
|
|
244
|
+
# Check cache first
|
|
245
|
+
impl_id = self.cache.get_implementation_id(control_id)
|
|
246
|
+
if impl_id:
|
|
247
|
+
return impl_id
|
|
248
|
+
|
|
249
|
+
# Query database
|
|
250
|
+
impl_id = self._find_implementation_id_in_database(control_id)
|
|
251
|
+
if impl_id:
|
|
252
|
+
self.cache.set_implementation_id(control_id, impl_id)
|
|
253
|
+
|
|
254
|
+
return impl_id
|
|
255
|
+
|
|
256
|
+
def _find_implementation_id_in_database(self, control_id: str) -> Optional[int]:
|
|
257
|
+
"""
|
|
258
|
+
Find control implementation ID by querying database.
|
|
259
|
+
|
|
260
|
+
:param control_id: Normalized control ID to search for
|
|
261
|
+
:return: Control implementation ID if found, None otherwise
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
implementations = regscale_models.ControlImplementation.get_all_by_parent(
|
|
265
|
+
parent_id=self.plan_id, parent_module=self.parent_module
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
for impl in implementations:
|
|
269
|
+
if not hasattr(impl, "controlID") or not impl.controlID:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Check cache for security control
|
|
273
|
+
security_control = self.cache.get_security_control(impl.controlID)
|
|
274
|
+
if not security_control:
|
|
275
|
+
security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
|
|
276
|
+
if security_control:
|
|
277
|
+
self.cache.set_security_control(impl.controlID, security_control)
|
|
278
|
+
|
|
279
|
+
if security_control and hasattr(security_control, "controlId"):
|
|
280
|
+
from regscale.integrations.commercial.wizv2.policy_compliance import WizPolicyComplianceIntegration
|
|
281
|
+
|
|
282
|
+
impl_control_id = WizPolicyComplianceIntegration._normalize_control_id_string(
|
|
283
|
+
security_control.controlId
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if impl_control_id == control_id:
|
|
287
|
+
logger.debug(f"✓ Found control implementation {impl.id} for control {control_id}")
|
|
288
|
+
return impl.id
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"Error finding control implementation for {control_id}: {e}")
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def _get_or_find_assessment_id(self, impl_id: int) -> Optional[int]:
|
|
296
|
+
"""
|
|
297
|
+
Get assessment ID from cache or database.
|
|
298
|
+
|
|
299
|
+
IMPROVED: More robust assessment lookup with better logging.
|
|
300
|
+
|
|
301
|
+
:param impl_id: Control implementation ID to search for
|
|
302
|
+
:return: Assessment ID if found, None otherwise
|
|
303
|
+
"""
|
|
304
|
+
# Check cache first
|
|
305
|
+
assessment = self.cache.get_assessment(impl_id)
|
|
306
|
+
if assessment and hasattr(assessment, "id"):
|
|
307
|
+
return assessment.id
|
|
308
|
+
|
|
309
|
+
# Query database
|
|
310
|
+
assessment = self._find_most_recent_assessment(impl_id)
|
|
311
|
+
if assessment:
|
|
312
|
+
self.cache.set_assessment(impl_id, assessment)
|
|
313
|
+
return assessment.id
|
|
314
|
+
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def _find_most_recent_assessment(self, impl_id: int) -> Optional[regscale_models.Assessment]:
|
|
318
|
+
"""
|
|
319
|
+
Find most recent assessment for implementation.
|
|
320
|
+
|
|
321
|
+
IMPROVED: Better error handling, logging, and assessment selection logic.
|
|
322
|
+
|
|
323
|
+
:param impl_id: Control implementation ID to search for
|
|
324
|
+
:return: Most recent assessment object if found, None otherwise
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
assessments = regscale_models.Assessment.get_all_by_parent(parent_id=impl_id, parent_module="controls")
|
|
328
|
+
|
|
329
|
+
if not assessments:
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
# Find today's assessments first
|
|
333
|
+
today = datetime.now().date()
|
|
334
|
+
today_assessments = []
|
|
335
|
+
other_assessments = []
|
|
336
|
+
|
|
337
|
+
for assessment in assessments:
|
|
338
|
+
assessment_date = self._extract_assessment_date(assessment)
|
|
339
|
+
if assessment_date == today:
|
|
340
|
+
today_assessments.append(assessment)
|
|
341
|
+
else:
|
|
342
|
+
other_assessments.append((assessment, assessment_date))
|
|
343
|
+
|
|
344
|
+
# Prefer today's assessments (most recently created)
|
|
345
|
+
if today_assessments:
|
|
346
|
+
best_assessment = max(today_assessments, key=lambda a: getattr(a, "id", 0))
|
|
347
|
+
return best_assessment
|
|
348
|
+
|
|
349
|
+
# Fall back to most recent overall (by date, then by ID)
|
|
350
|
+
if other_assessments:
|
|
351
|
+
best_assessment = max(
|
|
352
|
+
other_assessments, key=lambda x: (x[1] or datetime.min.date(), getattr(x[0], "id", 0))
|
|
353
|
+
)[0]
|
|
354
|
+
return best_assessment
|
|
355
|
+
|
|
356
|
+
return None
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(f"Error finding assessment for implementation {impl_id}: {e}")
|
|
359
|
+
import traceback
|
|
360
|
+
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def _extract_assessment_date(self, assessment) -> Optional[datetime.date]:
|
|
364
|
+
"""
|
|
365
|
+
Extract date from assessment object.
|
|
366
|
+
|
|
367
|
+
:param assessment: Assessment object to extract date from
|
|
368
|
+
:return: Extracted date if found, None otherwise
|
|
369
|
+
"""
|
|
370
|
+
try:
|
|
371
|
+
date_fields = ["plannedStart", "actualFinish", "plannedFinish", "dateCreated"]
|
|
372
|
+
for field in date_fields:
|
|
373
|
+
if hasattr(assessment, field):
|
|
374
|
+
date_value = getattr(assessment, field)
|
|
375
|
+
if date_value:
|
|
376
|
+
if isinstance(date_value, str):
|
|
377
|
+
return regscale_string_to_datetime(date_value).date()
|
|
378
|
+
elif hasattr(date_value, "date"):
|
|
379
|
+
return date_value.date()
|
|
380
|
+
else:
|
|
381
|
+
return date_value
|
|
382
|
+
return None
|
|
383
|
+
except Exception:
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class ControlAssessmentProcessor:
|
|
388
|
+
"""Handles control assessment creation and updates."""
|
|
389
|
+
|
|
390
|
+
def __init__(self, plan_id: int, parent_module: str, scan_date: str, title: str, framework: str) -> None:
|
|
391
|
+
"""
|
|
392
|
+
Initialize the control assessment processor.
|
|
393
|
+
|
|
394
|
+
:param plan_id: RegScale security plan ID
|
|
395
|
+
:param parent_module: Parent module name (e.g., 'securityplans')
|
|
396
|
+
:param scan_date: Date of the assessment scan
|
|
397
|
+
:param title: Title for assessments
|
|
398
|
+
:param framework: Framework name (e.g., 'NIST800-53R5')
|
|
399
|
+
"""
|
|
400
|
+
self.plan_id = plan_id
|
|
401
|
+
self.parent_module = parent_module
|
|
402
|
+
self.scan_date = scan_date
|
|
403
|
+
self.title = title
|
|
404
|
+
self.framework = framework
|
|
405
|
+
self.cache = ControlImplementationCache()
|
|
406
|
+
|
|
407
|
+
def create_or_update_assessment(
|
|
408
|
+
self,
|
|
409
|
+
implementation: regscale_models.ControlImplementation,
|
|
410
|
+
control_id: str,
|
|
411
|
+
result: str,
|
|
412
|
+
compliance_items: List[Any],
|
|
413
|
+
) -> Optional[regscale_models.Assessment]:
|
|
414
|
+
"""
|
|
415
|
+
Create or update a control assessment.
|
|
416
|
+
|
|
417
|
+
:param implementation: Control implementation
|
|
418
|
+
:param control_id: Control identifier
|
|
419
|
+
:param result: Assessment result ('Pass' or 'Fail')
|
|
420
|
+
:param compliance_items: List of compliance items for this control
|
|
421
|
+
:return: Created or updated assessment
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
# Check for existing assessment today
|
|
425
|
+
existing_assessment = self._find_existing_assessment_for_today(implementation.id)
|
|
426
|
+
|
|
427
|
+
assessment_report = self._create_assessment_report(control_id, result, compliance_items)
|
|
428
|
+
|
|
429
|
+
if existing_assessment:
|
|
430
|
+
# Update existing
|
|
431
|
+
existing_assessment.assessmentResult = result
|
|
432
|
+
existing_assessment.assessmentReport = assessment_report
|
|
433
|
+
existing_assessment.actualFinish = get_current_datetime()
|
|
434
|
+
existing_assessment.dateLastUpdated = get_current_datetime()
|
|
435
|
+
existing_assessment.save()
|
|
436
|
+
|
|
437
|
+
self.cache.set_assessment(implementation.id, existing_assessment)
|
|
438
|
+
logger.info(f"✅ Updated existing assessment {existing_assessment.id} for control {control_id}")
|
|
439
|
+
return existing_assessment
|
|
440
|
+
else:
|
|
441
|
+
# Create new
|
|
442
|
+
assessment = regscale_models.Assessment(
|
|
443
|
+
leadAssessorId=implementation.createdById,
|
|
444
|
+
title=f"{self.title} compliance assessment for {control_id.upper()}",
|
|
445
|
+
assessmentType="Control Testing",
|
|
446
|
+
plannedStart=get_current_datetime(),
|
|
447
|
+
plannedFinish=get_current_datetime(),
|
|
448
|
+
actualFinish=get_current_datetime(),
|
|
449
|
+
assessmentResult=result,
|
|
450
|
+
assessmentReport=assessment_report,
|
|
451
|
+
status="Complete",
|
|
452
|
+
parentId=implementation.id,
|
|
453
|
+
parentModule="controls",
|
|
454
|
+
isPublic=True,
|
|
455
|
+
).create()
|
|
456
|
+
|
|
457
|
+
self.cache.set_assessment(implementation.id, assessment)
|
|
458
|
+
logger.info(f"✅ Created new assessment {assessment.id} for control {control_id}")
|
|
459
|
+
return assessment
|
|
460
|
+
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.error(f"Error creating/updating assessment for control {control_id}: {e}")
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
def _find_existing_assessment_for_today(self, impl_id: int) -> Optional[regscale_models.Assessment]:
|
|
466
|
+
"""
|
|
467
|
+
Find existing assessment for today.
|
|
468
|
+
|
|
469
|
+
:param impl_id: Control implementation ID to search for
|
|
470
|
+
:return: Today's assessment if found, None otherwise
|
|
471
|
+
"""
|
|
472
|
+
# Check cache first
|
|
473
|
+
cached = self.cache.get_assessment(impl_id)
|
|
474
|
+
if cached:
|
|
475
|
+
return cached
|
|
476
|
+
|
|
477
|
+
# Query database for today's assessments
|
|
478
|
+
try:
|
|
479
|
+
today = datetime.now().date()
|
|
480
|
+
assessments = regscale_models.Assessment.get_all_by_parent(parent_id=impl_id, parent_module="controls")
|
|
481
|
+
|
|
482
|
+
for assessment in assessments:
|
|
483
|
+
if hasattr(assessment, "actualFinish") and assessment.actualFinish:
|
|
484
|
+
try:
|
|
485
|
+
if isinstance(assessment.actualFinish, str):
|
|
486
|
+
assessment_date = regscale_string_to_datetime(assessment.actualFinish).date()
|
|
487
|
+
elif hasattr(assessment.actualFinish, "date"):
|
|
488
|
+
assessment_date = assessment.actualFinish.date()
|
|
489
|
+
else:
|
|
490
|
+
assessment_date = assessment.actualFinish
|
|
491
|
+
|
|
492
|
+
if assessment_date == today:
|
|
493
|
+
self.cache.set_assessment(impl_id, assessment)
|
|
494
|
+
return assessment
|
|
495
|
+
except Exception:
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
return None
|
|
499
|
+
except Exception:
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
def _create_assessment_report(self, control_id: str, result: str, compliance_items: List[Any]) -> str:
|
|
503
|
+
"""
|
|
504
|
+
Create HTML assessment report.
|
|
505
|
+
|
|
506
|
+
:param control_id: Control identifier (e.g., 'AC-2(1)')
|
|
507
|
+
:param result: Assessment result ('Pass' or 'Fail')
|
|
508
|
+
:param compliance_items: List of compliance items for this control
|
|
509
|
+
:return: HTML formatted assessment report
|
|
510
|
+
"""
|
|
511
|
+
result_color = "#d32f2f" if result == "Fail" else "#2e7d32"
|
|
512
|
+
bg_color = "#ffebee" if result == "Fail" else "#e8f5e8"
|
|
513
|
+
|
|
514
|
+
html_parts = [
|
|
515
|
+
f"""
|
|
516
|
+
<div style="margin-bottom: 20px; padding: 15px; border: 2px solid {result_color};
|
|
517
|
+
border-radius: 5px; background-color: {bg_color};">
|
|
518
|
+
<h3 style="margin: 0 0 10px 0; color: {result_color};">
|
|
519
|
+
{self.title} Compliance Assessment for Control {control_id.upper()}
|
|
520
|
+
</h3>
|
|
521
|
+
<p><strong>Overall Result:</strong>
|
|
522
|
+
<span style="color: {result_color}; font-weight: bold;">{result}</span></p>
|
|
523
|
+
<p><strong>Assessment Date:</strong> {self.scan_date}</p>
|
|
524
|
+
<p><strong>Framework:</strong> {self.framework}</p>
|
|
525
|
+
<p><strong>Total Policy Assessments:</strong> {len(compliance_items)}</p>
|
|
526
|
+
</div>
|
|
527
|
+
"""
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
if compliance_items:
|
|
531
|
+
pass_count = len(
|
|
532
|
+
[
|
|
533
|
+
item
|
|
534
|
+
for item in compliance_items
|
|
535
|
+
if hasattr(item, "compliance_result")
|
|
536
|
+
and item.compliance_result in ["PASS", "PASSED", "pass", "passed"]
|
|
537
|
+
]
|
|
538
|
+
)
|
|
539
|
+
fail_count = len(compliance_items) - pass_count
|
|
540
|
+
|
|
541
|
+
unique_resources = set()
|
|
542
|
+
unique_policies = set()
|
|
543
|
+
|
|
544
|
+
for item in compliance_items:
|
|
545
|
+
if hasattr(item, "resource_id"):
|
|
546
|
+
unique_resources.add(item.resource_id)
|
|
547
|
+
if hasattr(item, "description") and item.description:
|
|
548
|
+
policy_desc = item.description[:50] + "..." if len(item.description) > 50 else item.description
|
|
549
|
+
unique_policies.add(policy_desc)
|
|
550
|
+
|
|
551
|
+
html_parts.append(
|
|
552
|
+
f"""
|
|
553
|
+
<div style="margin-top: 20px;">
|
|
554
|
+
<h4>Assessment Summary</h4>
|
|
555
|
+
<p><strong>Policy Assessments:</strong> {len(compliance_items)} total</p>
|
|
556
|
+
<p><strong>Unique Policies:</strong> {len(unique_policies)}</p>
|
|
557
|
+
<p><strong>Unique Resources:</strong> {len(unique_resources)}</p>
|
|
558
|
+
<p><strong>Passing:</strong> <span style="color: #2e7d32;">{pass_count}</span></p>
|
|
559
|
+
<p><strong>Failing:</strong> <span style="color: #d32f2f;">{fail_count}</span></p>
|
|
560
|
+
</div>
|
|
561
|
+
"""
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return "\n".join(html_parts)
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
from typing import Any, Dict, Iterator, List, Optional, Union, Tuple
|
|
9
9
|
|
|
10
|
-
from regscale.core.app.utils.app_utils import check_file_path, get_current_datetime
|
|
10
|
+
from regscale.core.app.utils.app_utils import check_file_path, get_current_datetime, error_and_exit
|
|
11
11
|
from regscale.core.utils import get_base_protocol_from_port
|
|
12
12
|
from regscale.core.utils.date import format_to_regscale_iso
|
|
13
13
|
from regscale.integrations.commercial.wizv2.async_client import run_async_queries
|
|
@@ -46,7 +46,6 @@ from regscale.integrations.scanner_integration import IntegrationAsset, Integrat
|
|
|
46
46
|
from regscale.integrations.variables import ScannerVariables
|
|
47
47
|
from regscale.models import IssueStatus, regscale_models
|
|
48
48
|
from regscale.models.regscale_models.compliance_settings import ComplianceSettings
|
|
49
|
-
from regscale.core.app.utils.app_utils import error_and_exit
|
|
50
49
|
|
|
51
50
|
logger = logging.getLogger("regscale")
|
|
52
51
|
|
|
@@ -127,14 +126,14 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
127
126
|
# Use synchronous method if explicitly requested
|
|
128
127
|
yield from self.fetch_findings_sync(**kwargs)
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
def _validate_project_id(project_id: Optional[str]) -> str:
|
|
129
|
+
def _validate_project_id(self, project_id: Optional[str]) -> str:
|
|
132
130
|
"""
|
|
133
131
|
Validate and format the Wiz project ID.
|
|
134
132
|
|
|
135
133
|
:param Optional[str] project_id: Project ID to validate
|
|
136
134
|
:return: Validated project ID
|
|
137
135
|
:rtype: str
|
|
136
|
+
:raises ValueError: If project ID is invalid or missing
|
|
138
137
|
"""
|
|
139
138
|
if not project_id:
|
|
140
139
|
error_and_exit("Wiz project ID is required")
|
|
@@ -387,6 +386,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
387
386
|
:return: Results in the same format as async queries
|
|
388
387
|
:rtype: List[Tuple[str, List[Dict[str, Any]], Optional[Exception]]]
|
|
389
388
|
"""
|
|
389
|
+
|
|
390
390
|
results = []
|
|
391
391
|
cache_task = self.finding_progress.add_task("[green]Loading cached Wiz data...", total=len(query_configs))
|
|
392
392
|
|
|
@@ -1271,7 +1271,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1271
1271
|
return filter_by
|
|
1272
1272
|
|
|
1273
1273
|
def get_software_details(
|
|
1274
|
-
self, wiz_entity_properties:
|
|
1274
|
+
self, wiz_entity_properties: dict, node: dict[str, Any], software_name_dict: dict[str, str], name: str
|
|
1275
1275
|
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
1276
1276
|
"""
|
|
1277
1277
|
Gets the software version, vendor, and name from the Wiz entity properties and node.
|
|
@@ -1312,9 +1312,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1312
1312
|
|
|
1313
1313
|
wiz_entity_properties = wiz_entity.get("properties", {})
|
|
1314
1314
|
is_public = False
|
|
1315
|
-
if public_exposures := wiz_entity.get("publicExposures")
|
|
1316
|
-
|
|
1317
|
-
|
|
1315
|
+
if (public_exposures := wiz_entity.get("publicExposures")) and (
|
|
1316
|
+
exposure_count := public_exposures.get("totalCount")
|
|
1317
|
+
):
|
|
1318
|
+
is_public = exposure_count > 0
|
|
1318
1319
|
|
|
1319
1320
|
network_dict = get_network_info(wiz_entity_properties)
|
|
1320
1321
|
handle_provider_dict = handle_provider(wiz_entity_properties)
|
|
@@ -1326,14 +1327,6 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1326
1327
|
wiz_entity_properties, node, software_name_dict, name
|
|
1327
1328
|
)
|
|
1328
1329
|
|
|
1329
|
-
if WizVariables.useWizHardwareAssetTypes and node.get("graphEntity", {}).get("technologies", []):
|
|
1330
|
-
technologies = node.get("graphEntity", {}).get("technologies", [])
|
|
1331
|
-
deployment_models: set[str] = {
|
|
1332
|
-
tech.get("deploymentModel") for tech in technologies if tech.get("deploymentModel")
|
|
1333
|
-
}
|
|
1334
|
-
else:
|
|
1335
|
-
deployment_models = set()
|
|
1336
|
-
|
|
1337
1330
|
return IntegrationAsset(
|
|
1338
1331
|
name=name,
|
|
1339
1332
|
external_id=node.get("name"),
|
|
@@ -1344,7 +1337,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1344
1337
|
asset_owner_id=ScannerVariables.userId,
|
|
1345
1338
|
parent_id=self.plan_id,
|
|
1346
1339
|
parent_module=regscale_models.SecurityPlan.get_module_slug(),
|
|
1347
|
-
asset_category=map_category(
|
|
1340
|
+
asset_category=map_category(node),
|
|
1348
1341
|
date_last_updated=wiz_entity.get("lastSeen", ""),
|
|
1349
1342
|
management_type=handle_management_type(wiz_entity_properties),
|
|
1350
1343
|
status=self.map_wiz_status(wiz_entity_properties.get("status")),
|
|
@@ -1419,7 +1412,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1419
1412
|
:return: Software vendor
|
|
1420
1413
|
:rtype: Optional[str]
|
|
1421
1414
|
"""
|
|
1422
|
-
|
|
1415
|
+
|
|
1416
|
+
if map_category(node) == regscale_models.AssetCategory.Software:
|
|
1423
1417
|
return software_name_dict.get("software_vendor") or wiz_entity_properties.get("cloudPlatform")
|
|
1424
1418
|
return None
|
|
1425
1419
|
|
|
@@ -1433,8 +1427,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1433
1427
|
:return: Software version
|
|
1434
1428
|
:rtype: Optional[str]
|
|
1435
1429
|
"""
|
|
1436
|
-
if map_category(node
|
|
1437
|
-
return handle_software_version(wiz_entity_properties,
|
|
1430
|
+
if map_category(node) == regscale_models.AssetCategory.Software:
|
|
1431
|
+
return handle_software_version(wiz_entity_properties, regscale_models.AssetCategory.Software) or "1.0"
|
|
1438
1432
|
return None
|
|
1439
1433
|
|
|
1440
1434
|
@staticmethod
|
|
@@ -1448,7 +1442,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1448
1442
|
:return: Software name
|
|
1449
1443
|
:rtype: Optional[str]
|
|
1450
1444
|
"""
|
|
1451
|
-
if map_category(node
|
|
1445
|
+
if map_category(node) == regscale_models.AssetCategory.Software:
|
|
1452
1446
|
return software_name_dict.get("software_name") or wiz_entity_properties.get("nativeType")
|
|
1453
1447
|
return None
|
|
1454
1448
|
|
|
@@ -1538,10 +1532,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1538
1532
|
logger.debug("WizVulnerabilityIntegration.get_asset_by_identifier called for %s", identifier)
|
|
1539
1533
|
|
|
1540
1534
|
# Try to provide more diagnostic information
|
|
1541
|
-
self
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1535
|
+
if not getattr(self, "suppress_asset_not_found_errors", False):
|
|
1536
|
+
self._log_missing_asset_diagnostics(identifier)
|
|
1537
|
+
# Still log the original error for consistency
|
|
1538
|
+
self.log_error("1. Asset not found for identifier %s", identifier)
|
|
1545
1539
|
|
|
1546
1540
|
return asset
|
|
1547
1541
|
|