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.
- regscale/_version.py +1 -1
- regscale/core/app/api.py +1 -1
- regscale/core/app/application.py +5 -3
- regscale/core/app/internal/evidence.py +308 -202
- regscale/dev/code_gen.py +84 -3
- regscale/integrations/commercial/__init__.py +2 -0
- regscale/integrations/commercial/jira.py +95 -22
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/synqly/assets.py +99 -16
- regscale/integrations/commercial/synqly/query_builder.py +533 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +134 -14
- regscale/integrations/commercial/wizv2/click.py +23 -0
- regscale/integrations/commercial/wizv2/compliance_report.py +137 -26
- regscale/integrations/compliance_integration.py +247 -5
- regscale/integrations/scanner_integration.py +16 -0
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +12 -2
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/synqly_model.py +47 -3
- regscale/models/regscale_models/compliance_settings.py +28 -0
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +143 -4
- regscale/regscale.py +1 -1
- regscale/validation/record.py +23 -1
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/METADATA +9 -9
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +32 -30
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
777
|
+
def _find_existing_compliance_report(self) -> Optional[str]:
|
|
760
778
|
"""
|
|
761
|
-
|
|
779
|
+
Find existing compliance report for the current project.
|
|
762
780
|
|
|
763
|
-
:return:
|
|
781
|
+
:return: Report ID if found, None otherwise
|
|
764
782
|
:rtype: Optional[str]
|
|
765
783
|
"""
|
|
766
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
|
840
|
-
|
|
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
|
|
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
|
|
950
|
-
|
|
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
|
|
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:
|