regscale-cli 6.21.1.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 +8 -8
- 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/wizv2/click.py +4 -1
- regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
- regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +1402 -203
- regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
- regscale/integrations/commercial/wizv2/scanner.py +4 -4
- regscale/integrations/compliance_integration.py +212 -60
- regscale/integrations/public/fedramp/fedramp_five.py +92 -7
- regscale/integrations/scanner_integration.py +27 -4
- regscale/models/__init__.py +1 -1
- regscale/models/integration_models/cisa_kev_data.json +33 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/issue.py +29 -9
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +32 -27
- tests/regscale/test_authorization.py +0 -65
- tests/regscale/test_init.py +0 -96
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.1.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)
|
|
@@ -1532,10 +1532,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1532
1532
|
logger.debug("WizVulnerabilityIntegration.get_asset_by_identifier called for %s", identifier)
|
|
1533
1533
|
|
|
1534
1534
|
# Try to provide more diagnostic information
|
|
1535
|
-
self
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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)
|
|
1539
1539
|
|
|
1540
1540
|
return asset
|
|
1541
1541
|
|