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.
Files changed (54) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +7 -0
  3. regscale/integrations/commercial/__init__.py +9 -10
  4. regscale/integrations/commercial/amazon/common.py +79 -2
  5. regscale/integrations/commercial/aws/cli.py +183 -9
  6. regscale/integrations/commercial/aws/scanner.py +544 -9
  7. regscale/integrations/commercial/cpe.py +18 -1
  8. regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
  9. regscale/integrations/commercial/microsoft_defender/__init__.py +0 -0
  10. regscale/integrations/commercial/{defender.py → microsoft_defender/defender.py} +38 -612
  11. regscale/integrations/commercial/microsoft_defender/defender_api.py +286 -0
  12. regscale/integrations/commercial/microsoft_defender/defender_constants.py +80 -0
  13. regscale/integrations/commercial/microsoft_defender/defender_scanner.py +168 -0
  14. regscale/integrations/commercial/qualys/__init__.py +24 -86
  15. regscale/integrations/commercial/qualys/containers.py +2 -0
  16. regscale/integrations/commercial/qualys/scanner.py +7 -2
  17. regscale/integrations/commercial/sonarcloud.py +110 -71
  18. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
  19. regscale/integrations/commercial/wizv2/async_client.py +10 -3
  20. regscale/integrations/commercial/wizv2/click.py +105 -26
  21. regscale/integrations/commercial/wizv2/constants.py +249 -1
  22. regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
  23. regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
  24. regscale/integrations/commercial/wizv2/issue.py +2 -2
  25. regscale/integrations/commercial/wizv2/parsers.py +3 -2
  26. regscale/integrations/commercial/wizv2/policy_compliance.py +3057 -0
  27. regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
  28. regscale/integrations/commercial/wizv2/scanner.py +19 -25
  29. regscale/integrations/commercial/wizv2/utils.py +258 -85
  30. regscale/integrations/commercial/wizv2/variables.py +4 -3
  31. regscale/integrations/compliance_integration.py +1607 -0
  32. regscale/integrations/public/fedramp/fedramp_five.py +93 -8
  33. regscale/integrations/public/fedramp/markdown_parser.py +7 -1
  34. regscale/integrations/scanner_integration.py +57 -6
  35. regscale/models/__init__.py +1 -1
  36. regscale/models/app_models/__init__.py +1 -0
  37. regscale/models/integration_models/cisa_kev_data.json +103 -4
  38. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  39. regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
  40. regscale/models/regscale_models/file.py +4 -0
  41. regscale/models/regscale_models/issue.py +151 -8
  42. regscale/models/regscale_models/regscale_model.py +4 -2
  43. regscale/models/regscale_models/security_plan.py +1 -1
  44. regscale/utils/graphql_client.py +3 -1
  45. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +9 -9
  46. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +52 -44
  47. tests/regscale/core/test_version_regscale.py +5 -3
  48. tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
  49. tests/regscale/test_authorization.py +0 -65
  50. tests/regscale/test_init.py +0 -96
  51. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
  52. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
  53. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
  54. {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
- @staticmethod
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: Dict, node: Dict[str, Any], software_name_dict: Dict[str, str], name: str
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
- if exposure_count := public_exposures.get("totalCount"):
1317
- is_public = exposure_count > 0
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(deployment_models or node.get("type", "")),
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
- if map_category(node.get("type", "")) == regscale_models.AssetCategory.Software:
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.get("type", "")) == regscale_models.AssetCategory.Software:
1437
- return handle_software_version(wiz_entity_properties, map_category(node.get("type", ""))) or "1.0"
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.get("type", "")) == regscale_models.AssetCategory.Software:
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._log_missing_asset_diagnostics(identifier)
1542
-
1543
- # Still log the original error for consistency
1544
- self.log_error("1. Asset not found for identifier %s", identifier)
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