regscale-cli 6.24.0.0__py3-none-any.whl → 6.25.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (32) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/api.py +1 -1
  3. regscale/core/app/application.py +5 -3
  4. regscale/core/app/internal/evidence.py +308 -202
  5. regscale/dev/code_gen.py +84 -3
  6. regscale/integrations/commercial/__init__.py +2 -0
  7. regscale/integrations/commercial/jira.py +95 -22
  8. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  9. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  10. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  11. regscale/integrations/commercial/synqly/assets.py +99 -16
  12. regscale/integrations/commercial/synqly/query_builder.py +533 -0
  13. regscale/integrations/commercial/synqly/vulnerabilities.py +134 -14
  14. regscale/integrations/commercial/wizv2/click.py +23 -0
  15. regscale/integrations/commercial/wizv2/compliance_report.py +137 -26
  16. regscale/integrations/compliance_integration.py +247 -5
  17. regscale/integrations/scanner_integration.py +16 -0
  18. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  19. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +12 -2
  20. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  21. regscale/models/integration_models/synqly_models/synqly_model.py +47 -3
  22. regscale/models/regscale_models/compliance_settings.py +28 -0
  23. regscale/models/regscale_models/component.py +1 -0
  24. regscale/models/regscale_models/control_implementation.py +143 -4
  25. regscale/regscale.py +1 -1
  26. regscale/validation/record.py +23 -1
  27. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/METADATA +9 -9
  28. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +32 -30
  29. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
  30. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
  31. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
  32. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,33 @@ def vulnerabilities() -> None:
14
14
  pass
15
15
 
16
16
 
17
+ @vulnerabilities.command(name="build-query")
18
+ @click.option(
19
+ "--provider",
20
+ required=False,
21
+ help="Provider ID (e.g., vulnerabilities_armis_centrix). If not specified, starts interactive mode.",
22
+ )
23
+ @click.option("--validate", help="Validate a filter string against provider capabilities")
24
+ @click.option("--list-fields", is_flag=True, default=False, help="List all available fields for the provider")
25
+ def build_query(provider, validate, list_fields):
26
+ """
27
+ Build and validate filter queries for Vulnerabilities connectors.
28
+
29
+ Examples:
30
+ # Build a filter query
31
+ regscale vulnerabilities build-query
32
+
33
+ # List all fields for a specific provider
34
+ regscale vulnerabilities build-query --provider vulnerabilities_armis_centrix --list-fields
35
+
36
+ # Validate a filter string
37
+ regscale vulnerabilities build-query --provider vulnerabilities_armis_centrix --validate "device.ip[eq]192.168.1.1"
38
+ """
39
+ from regscale.integrations.commercial.synqly.query_builder import handle_build_query
40
+
41
+ handle_build_query("vulnerabilities", provider, validate, list_fields)
42
+
43
+
17
44
  @vulnerabilities.command(name="sync_crowdstrike")
18
45
  @regscale_ssp_id()
19
46
  @click.option(
@@ -37,19 +64,33 @@ def vulnerabilities() -> None:
37
64
  is_flag=True,
38
65
  default=False,
39
66
  )
67
+ @click.option(
68
+ "--asset_filter",
69
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
70
+ required=False,
71
+ type=str,
72
+ default=None,
73
+ )
40
74
  @click.option(
41
75
  "--url",
42
76
  type=click.STRING,
43
77
  help="Base URL for the CrowdStrike Falcon® Spotlight API.",
44
78
  required=False,
45
79
  )
46
- def sync_crowdstrike(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, url: str) -> None:
80
+ def sync_crowdstrike(
81
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str, url: str
82
+ ) -> None:
47
83
  """Sync Vulnerabilities from Crowdstrike to RegScale."""
48
84
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
49
85
 
50
86
  vulnerabilities_crowdstrike = Vulnerabilities("crowdstrike")
51
87
  vulnerabilities_crowdstrike.run_sync(
52
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans, url=url
88
+ regscale_ssp_id=regscale_ssp_id,
89
+ vuln_filter=vuln_filter,
90
+ scan_date=scan_date,
91
+ all_scans=all_scans,
92
+ filter=asset_filter.split(";") if asset_filter else [],
93
+ url=url,
53
94
  )
54
95
 
55
96
 
@@ -72,13 +113,26 @@ def sync_crowdstrike(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime
72
113
  @click.option(
73
114
  "--all_scans", help="Whether to sync all vulnerabilities from Nucleus", required=False, is_flag=True, default=False
74
115
  )
75
- def sync_nucleus(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool) -> None:
116
+ @click.option(
117
+ "--asset_filter",
118
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
119
+ required=False,
120
+ type=str,
121
+ default=None,
122
+ )
123
+ def sync_nucleus(
124
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
125
+ ) -> None:
76
126
  """Sync Vulnerabilities from Nucleus to RegScale."""
77
127
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
78
128
 
79
129
  vulnerabilities_nucleus = Vulnerabilities("nucleus")
80
130
  vulnerabilities_nucleus.run_sync(
81
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans
131
+ regscale_ssp_id=regscale_ssp_id,
132
+ vuln_filter=vuln_filter,
133
+ scan_date=scan_date,
134
+ all_scans=all_scans,
135
+ filter=asset_filter.split(";") if asset_filter else [],
82
136
  )
83
137
 
84
138
 
@@ -105,13 +159,26 @@ def sync_nucleus(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, al
105
159
  is_flag=True,
106
160
  default=False,
107
161
  )
108
- def sync_qualys_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool) -> None:
162
+ @click.option(
163
+ "--asset_filter",
164
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
165
+ required=False,
166
+ type=str,
167
+ default=None,
168
+ )
169
+ def sync_qualys_cloud(
170
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
171
+ ) -> None:
109
172
  """Sync Vulnerabilities from Qualys Cloud to RegScale."""
110
173
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
111
174
 
112
175
  vulnerabilities_qualys_cloud = Vulnerabilities("qualys_cloud")
113
176
  vulnerabilities_qualys_cloud.run_sync(
114
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans
177
+ regscale_ssp_id=regscale_ssp_id,
178
+ vuln_filter=vuln_filter,
179
+ scan_date=scan_date,
180
+ all_scans=all_scans,
181
+ filter=asset_filter.split(";") if asset_filter else [],
115
182
  )
116
183
 
117
184
 
@@ -138,13 +205,26 @@ def sync_qualys_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetim
138
205
  is_flag=True,
139
206
  default=False,
140
207
  )
141
- def sync_rapid7_insight_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool) -> None:
208
+ @click.option(
209
+ "--asset_filter",
210
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
211
+ required=False,
212
+ type=str,
213
+ default=None,
214
+ )
215
+ def sync_rapid7_insight_cloud(
216
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
217
+ ) -> None:
142
218
  """Sync Vulnerabilities from Rapid7 Insight Cloud to RegScale."""
143
219
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
144
220
 
145
221
  vulnerabilities_rapid7_insight_cloud = Vulnerabilities("rapid7_insight_cloud")
146
222
  vulnerabilities_rapid7_insight_cloud.run_sync(
147
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans
223
+ regscale_ssp_id=regscale_ssp_id,
224
+ vuln_filter=vuln_filter,
225
+ scan_date=scan_date,
226
+ all_scans=all_scans,
227
+ filter=asset_filter.split(";") if asset_filter else [],
148
228
  )
149
229
 
150
230
 
@@ -171,13 +251,26 @@ def sync_rapid7_insight_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date:
171
251
  is_flag=True,
172
252
  default=False,
173
253
  )
174
- def sync_servicenow_vr(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool) -> None:
254
+ @click.option(
255
+ "--asset_filter",
256
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
257
+ required=False,
258
+ type=str,
259
+ default=None,
260
+ )
261
+ def sync_servicenow_vr(
262
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
263
+ ) -> None:
175
264
  """Sync Vulnerabilities from Servicenow Vr to RegScale."""
176
265
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
177
266
 
178
267
  vulnerabilities_servicenow_vr = Vulnerabilities("servicenow_vr")
179
268
  vulnerabilities_servicenow_vr.run_sync(
180
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans
269
+ regscale_ssp_id=regscale_ssp_id,
270
+ vuln_filter=vuln_filter,
271
+ scan_date=scan_date,
272
+ all_scans=all_scans,
273
+ filter=asset_filter.split(";") if asset_filter else [],
181
274
  )
182
275
 
183
276
 
@@ -204,13 +297,26 @@ def sync_servicenow_vr(regscale_ssp_id: int, vuln_filter: str, scan_date: dateti
204
297
  is_flag=True,
205
298
  default=False,
206
299
  )
207
- def sync_tanium_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool) -> None:
300
+ @click.option(
301
+ "--asset_filter",
302
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
303
+ required=False,
304
+ type=str,
305
+ default=None,
306
+ )
307
+ def sync_tanium_cloud(
308
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str
309
+ ) -> None:
208
310
  """Sync Vulnerabilities from Tanium Cloud to RegScale."""
209
311
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
210
312
 
211
313
  vulnerabilities_tanium_cloud = Vulnerabilities("tanium_cloud")
212
314
  vulnerabilities_tanium_cloud.run_sync(
213
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans
315
+ regscale_ssp_id=regscale_ssp_id,
316
+ vuln_filter=vuln_filter,
317
+ scan_date=scan_date,
318
+ all_scans=all_scans,
319
+ filter=asset_filter.split(";") if asset_filter else [],
214
320
  )
215
321
 
216
322
 
@@ -237,19 +343,33 @@ def sync_tanium_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetim
237
343
  is_flag=True,
238
344
  default=False,
239
345
  )
346
+ @click.option(
347
+ "--asset_filter",
348
+ help='STRING: Apply filters to asset queries. Can be a single filter "field[operator]value" or semicolon-separated filters "field1[op]value1;field2[op]value2"',
349
+ required=False,
350
+ type=str,
351
+ default=None,
352
+ )
240
353
  @click.option(
241
354
  "--url",
242
355
  type=click.STRING,
243
356
  help="Base URL for the Tenable Cloud API.",
244
357
  required=False,
245
358
  )
246
- def sync_tenable_cloud(regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, url: str) -> None:
359
+ def sync_tenable_cloud(
360
+ regscale_ssp_id: int, vuln_filter: str, scan_date: datetime, all_scans: bool, asset_filter: str, url: str
361
+ ) -> None:
247
362
  """Sync Vulnerabilities from Tenable Cloud to RegScale."""
248
363
  from regscale.models.integration_models.synqly_models.connectors import Vulnerabilities
249
364
 
250
365
  vulnerabilities_tenable_cloud = Vulnerabilities("tenable_cloud")
251
366
  vulnerabilities_tenable_cloud.run_sync(
252
- regscale_ssp_id=regscale_ssp_id, vuln_filter=vuln_filter, scan_date=scan_date, all_scans=all_scans, url=url
367
+ regscale_ssp_id=regscale_ssp_id,
368
+ vuln_filter=vuln_filter,
369
+ scan_date=scan_date,
370
+ all_scans=all_scans,
371
+ filter=asset_filter.split(";") if asset_filter else [],
372
+ url=url,
253
373
  )
254
374
 
255
375
 
@@ -533,6 +533,18 @@ def sync_compliance(
533
533
  default=False,
534
534
  help="Mark created issues as POAMs (default: disabled)",
535
535
  )
536
+ @click.option(
537
+ "--reuse-existing-reports/--no-reuse-existing-reports",
538
+ "-rer/-nrer",
539
+ default=True,
540
+ help="Reuse existing Wiz compliance reports instead of creating new ones (default: enabled)",
541
+ )
542
+ @click.option(
543
+ "--force-fresh-report/--no-force-fresh-report",
544
+ "-ffr/-nffr",
545
+ default=False,
546
+ help="Force creation of a fresh compliance report, ignoring existing reports (default: disabled)",
547
+ )
536
548
  def compliance_report(
537
549
  wiz_project_id,
538
550
  regscale_id,
@@ -543,6 +555,8 @@ def compliance_report(
543
555
  create_issues,
544
556
  update_control_status,
545
557
  create_poams,
558
+ reuse_existing_reports,
559
+ force_fresh_report,
546
560
  ):
547
561
  """
548
562
  Process Wiz compliance reports and create assessments in RegScale.
@@ -557,6 +571,13 @@ def compliance_report(
557
571
  - Create issues for failed compliance assessments (if --create-issues enabled)
558
572
  - Update control implementation status (if --update-control-status enabled)
559
573
  - Support POAM creation for compliance issues
574
+
575
+ REPORT MANAGEMENT:
576
+ By default, the command will look for existing compliance reports in Wiz for the
577
+ specified project and rerun them instead of creating new ones. This prevents the
578
+ accumulation of duplicate reports in Wiz. Use --no-reuse-existing-reports to
579
+ always create new reports, or --force-fresh-report to force a new report even
580
+ when reuse is enabled.
560
581
  """
561
582
  from regscale.integrations.commercial.wizv2.compliance_report import WizComplianceReportProcessor
562
583
 
@@ -579,6 +600,8 @@ def compliance_report(
579
600
  update_control_status=update_control_status,
580
601
  report_file_path=report_file_path,
581
602
  bypass_control_filtering=True, # Bypass filtering for performance with large control sets
603
+ reuse_existing_reports=reuse_existing_reports,
604
+ force_fresh_report=force_fresh_report,
582
605
  )
583
606
 
584
607
  # Process the compliance report using new ComplianceIntegration pattern
@@ -103,27 +103,42 @@ class WizComplianceReportItem(ComplianceItem):
103
103
  return control_ids[0] if control_ids else ""
104
104
 
105
105
  def get_all_control_ids(self) -> list:
106
- """Extract all control IDs from compliance check name."""
106
+ """Extract all control IDs from compliance check name and normalize leading zeros."""
107
107
  if not self.compliance_check_name:
108
108
  return []
109
109
 
110
- # Parse control IDs from compliance check name
111
- # Format: "AC-2(4) Account Management | Automated Audit Actions, AC-6(9) Least Privilege | Log Use of Privileged Functions"
112
- # Use a regex that can find control IDs anywhere in the text
113
110
  control_id_pattern = r"([A-Za-z]{2}-\d+)(?:\s*\(\s*(\d+)\s*\))?"
114
-
115
111
  control_ids = []
112
+
116
113
  for part in self.compliance_check_name.split(", "):
117
114
  matches = re.findall(control_id_pattern, part.strip())
118
115
  for match in matches:
119
116
  base_control, enhancement = match
120
- if enhancement:
121
- control_ids.append(f"{base_control}({enhancement})")
122
- else:
123
- control_ids.append(base_control)
117
+ normalized_control = self._normalize_base_control(base_control)
118
+ formatted_control = self._format_control_id(normalized_control, enhancement)
119
+ control_ids.append(formatted_control)
124
120
 
125
121
  return control_ids
126
122
 
123
+ def _normalize_base_control(self, base_control: str) -> str:
124
+ """Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)."""
125
+ if "-" in base_control:
126
+ prefix, number = base_control.split("-", 1)
127
+ try:
128
+ normalized_number = str(int(number))
129
+ return f"{prefix.upper()}-{normalized_number}"
130
+ except ValueError:
131
+ return base_control.upper()
132
+ else:
133
+ return base_control.upper()
134
+
135
+ def _format_control_id(self, base_control: str, enhancement: str) -> str:
136
+ """Format control ID with optional enhancement."""
137
+ if enhancement:
138
+ return f"{base_control}({enhancement})"
139
+ else:
140
+ return base_control
141
+
127
142
  @property
128
143
  def affected_controls(self) -> str:
129
144
  """Get affected controls as comma-separated string for issues."""
@@ -217,6 +232,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
217
232
  bypass_control_filtering: bool = False,
218
233
  max_report_age_days: int = 7,
219
234
  force_fresh_report: bool = False,
235
+ reuse_existing_reports: bool = True,
220
236
  **kwargs,
221
237
  ):
222
238
  """
@@ -232,6 +248,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
232
248
  :param bool bypass_control_filtering: Skip control filtering for performance with large control sets
233
249
  :param int max_report_age_days: Maximum age in days for reusing existing reports (default: 7 days)
234
250
  :param bool force_fresh_report: Force creation of fresh report, ignoring existing reports
251
+ :param bool reuse_existing_reports: Whether to reuse existing Wiz reports instead of creating new ones (default: True)
235
252
  """
236
253
  # Call parent constructor with ComplianceIntegration parameters
237
254
  super().__init__(
@@ -250,6 +267,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
250
267
  self.bypass_control_filtering = bypass_control_filtering
251
268
  self.max_report_age_days = max_report_age_days
252
269
  self.force_fresh_report = force_fresh_report
270
+ self.reuse_existing_reports = reuse_existing_reports
253
271
  self.title = "Wiz Compliance" # Required by ScannerIntegration
254
272
 
255
273
  # Initialize Wiz authentication
@@ -698,7 +716,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
698
716
  # Handle force fresh report request
699
717
  if self.force_fresh_report:
700
718
  logger.info("Force fresh report requested, creating new compliance report...")
701
- return self._create_and_download_report()
719
+ return self._create_and_download_report(force_new=True)
702
720
 
703
721
  # Use instance variable max_report_age_days or legacy max_age_hours
704
722
  if max_age_hours is not None:
@@ -756,23 +774,82 @@ class WizComplianceReportProcessor(ComplianceIntegration):
756
774
  logger.info(f"Found recent report (age: {age_hours:.1f}h): {most_recent[0]}")
757
775
  return most_recent[0]
758
776
 
759
- def _create_and_download_report(self) -> Optional[str]:
777
+ def _find_existing_compliance_report(self) -> Optional[str]:
760
778
  """
761
- Create and download a new compliance report.
779
+ Find existing compliance report for the current project.
762
780
 
763
- :return: Path to downloaded report file
781
+ :return: Report ID if found, None otherwise
764
782
  :rtype: Optional[str]
765
783
  """
766
- logger.info(f"Creating compliance report for project: {self.wiz_project_id}")
784
+ try:
785
+ # Filter for compliance reports for this specific project
786
+ filter_by = {"project": [self.wiz_project_id], "type": ["COMPLIANCE_ASSESSMENTS"]}
767
787
 
768
- # Create report
769
- report_id = self.report_manager.create_compliance_report(self.wiz_project_id)
770
- if not report_id:
771
- logger.error("Failed to create compliance report")
788
+ logger.debug(f"Searching for existing compliance reports with filter: {filter_by}")
789
+ reports = self.report_manager.list_reports(filter_by=filter_by)
790
+
791
+ if not reports:
792
+ logger.info("No existing compliance reports found for this project")
793
+ return None
794
+
795
+ # Look for reports named "Compliance Report" (the default name)
796
+ compliance_reports = [report for report in reports if report.get("name", "").strip() == "Compliance Report"]
797
+
798
+ if not compliance_reports:
799
+ logger.info("No compliance reports with standard name found")
800
+ return None
801
+
802
+ # Return the first matching report (most recent will be used)
803
+ selected_report = compliance_reports[0]
804
+ report_id = selected_report.get("id")
805
+ report_name = selected_report.get("name", "Unknown")
806
+
807
+ logger.info(f"Found existing compliance report: '{report_name}' (ID: {report_id})")
808
+ return report_id
809
+
810
+ except Exception as e:
811
+ logger.error(f"Error searching for existing compliance reports: {e}")
772
812
  return None
773
813
 
774
- # Wait for completion and get download URL
775
- download_url = self.report_manager.wait_for_report_completion(report_id)
814
+ def _create_and_download_report(self, force_new: bool = False) -> Optional[str]:
815
+ """
816
+ Find existing compliance report and rerun it, or create a new one if none exists.
817
+
818
+ :param bool force_new: Force creation of new report, skip reuse logic
819
+ :return: Path to downloaded report file
820
+ :rtype: Optional[str]
821
+ """
822
+ if force_new or not self.reuse_existing_reports:
823
+ logger.info("Creating new compliance report (reuse disabled or forced)")
824
+ # Create new report
825
+ report_id = self.report_manager.create_compliance_report(self.wiz_project_id)
826
+ if not report_id:
827
+ logger.error("Failed to create compliance report")
828
+ return None
829
+
830
+ # Wait for completion and get download URL
831
+ download_url = self.report_manager.wait_for_report_completion(report_id)
832
+ else:
833
+ logger.info(f"Looking for existing compliance report for project: {self.wiz_project_id}")
834
+
835
+ # Try to find existing compliance report for this project
836
+ if existing_report_id := self._find_existing_compliance_report():
837
+ logger.info(
838
+ f"Found existing compliance report {existing_report_id}, rerunning instead of creating new one"
839
+ )
840
+ # Rerun existing report
841
+ download_url = self.report_manager.rerun_report(existing_report_id)
842
+ else:
843
+ logger.info("No existing compliance report found, creating new one")
844
+ # Create new report
845
+ report_id = self.report_manager.create_compliance_report(self.wiz_project_id)
846
+ if not report_id:
847
+ logger.error("Failed to create compliance report")
848
+ return None
849
+
850
+ # Wait for completion and get download URL
851
+ download_url = self.report_manager.wait_for_report_completion(report_id)
852
+
776
853
  if not download_url:
777
854
  logger.error("Failed to get download URL for report")
778
855
  return None
@@ -825,6 +902,10 @@ class WizComplianceReportProcessor(ComplianceIntegration):
825
902
  # Prepare batch updates for passing controls
826
903
  implementations_to_update = []
827
904
 
905
+ # Debug: Show what keys are actually in the control_impl_map
906
+ if control_impl_map:
907
+ logger.debug(f"Control implementation map keys: {list(control_impl_map.keys())[:20]}")
908
+
828
909
  for control_id in passing_control_ids:
829
910
  control_id_lower = control_id.lower()
830
911
  logger.debug(f"Looking for control '{control_id_lower}' in implementation map")
@@ -836,12 +917,27 @@ class WizComplianceReportProcessor(ComplianceIntegration):
836
917
  # Get the ControlImplementation object
837
918
  impl = ControlImplementation.get_object(object_id=impl_id)
838
919
  if impl:
839
- # Update status to Implemented
840
- impl.status = ControlImplementationStatus.Implemented.value
920
+ # Update status using compliance settings
921
+ new_status = self._get_implementation_status_from_result("Pass")
922
+ logger.debug(f"Setting control {control_id} status from 'Pass' result to: {new_status}")
923
+ impl.status = new_status
841
924
  impl.dateLastAssessed = get_current_datetime()
842
925
  impl.lastAssessmentResult = "Pass"
843
926
  impl.bStatusImplemented = True
844
927
 
928
+ # Ensure required fields are set if empty
929
+ if not impl.responsibility:
930
+ impl.responsibility = ControlImplementation.get_default_responsibility(
931
+ parent_id=impl.parentId
932
+ )
933
+ logger.debug(
934
+ f"Setting default responsibility for control {control_id}: {impl.responsibility}"
935
+ )
936
+
937
+ if not impl.implementation:
938
+ impl.implementation = f"Implementation details for {control_id} will be documented."
939
+ logger.debug(f"Setting default implementation statement for control {control_id}")
940
+
845
941
  # Set audit fields if available
846
942
  user_id = self.app.config.get("userId")
847
943
  if user_id:
@@ -849,7 +945,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
849
945
  impl.dateLastUpdated = get_current_datetime()
850
946
 
851
947
  implementations_to_update.append(impl.dict())
852
- logger.info(f"Marking control {control_id} as Implemented")
948
+ logger.info(f"Marking control {control_id} as {new_status}")
853
949
 
854
950
  # Batch update all implementations
855
951
  if implementations_to_update:
@@ -908,6 +1004,10 @@ class WizComplianceReportProcessor(ComplianceIntegration):
908
1004
  implementations_to_update = []
909
1005
  controls_not_found = []
910
1006
 
1007
+ # Debug: Show what keys are actually in the control_impl_map for comparison
1008
+ if control_impl_map:
1009
+ logger.debug(f"Control implementation map keys (first 20): {list(control_impl_map.keys())[:20]}")
1010
+
911
1011
  for control_id in control_ids:
912
1012
  control_id_normalized = control_id.lower()
913
1013
  logger.debug(f"Looking for control '{control_id_normalized}' in implementation map")
@@ -946,19 +1046,30 @@ class WizComplianceReportProcessor(ComplianceIntegration):
946
1046
  logger.warning(f"Could not retrieve implementation object for ID {impl_id}")
947
1047
  return None
948
1048
 
949
- # Update status to In Remediation
950
- impl.status = ControlImplementationStatus.InRemediation.value
1049
+ # Update status using compliance settings
1050
+ new_status = self._get_implementation_status_from_result("Fail")
1051
+ logger.debug(f"Setting control {control_id} status from 'Fail' result to: {new_status}")
1052
+ impl.status = new_status
951
1053
  impl.dateLastAssessed = get_current_datetime()
952
1054
  impl.lastAssessmentResult = "Fail"
953
1055
  impl.bStatusImplemented = False
954
1056
 
1057
+ # Ensure required fields are set if empty
1058
+ if not impl.responsibility:
1059
+ impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
1060
+ logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
1061
+
1062
+ if not impl.implementation:
1063
+ impl.implementation = f"Implementation details for {control_id} will be documented."
1064
+ logger.debug(f"Setting default implementation statement for control {control_id}")
1065
+
955
1066
  # Set audit fields if available
956
1067
  user_id = self.app.config.get("userId")
957
1068
  if user_id:
958
1069
  impl.lastUpdatedById = user_id
959
1070
  impl.dateLastUpdated = get_current_datetime()
960
1071
 
961
- logger.info(f"Marking control {control_id} as In Remediation")
1072
+ logger.info(f"Marking control {control_id} as {new_status}")
962
1073
  return impl.dict()
963
1074
 
964
1075
  def _log_update_summary(self, implementations_to_update: list, controls_not_found: list) -> None: