regscale-cli 6.20.5.0__py3-none-any.whl → 6.20.7.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/__init__.py +1 -1
- regscale/_version.py +39 -0
- regscale/core/app/internal/__init__.py +13 -0
- regscale/core/app/internal/set_permissions.py +173 -0
- regscale/core/app/utils/file_utils.py +11 -1
- regscale/core/app/utils/regscale_utils.py +1 -133
- regscale/core/utils/date.py +62 -29
- regscale/integrations/commercial/qualys/__init__.py +7 -7
- regscale/integrations/commercial/wizv2/click.py +9 -5
- regscale/integrations/commercial/wizv2/constants.py +15 -0
- regscale/integrations/commercial/wizv2/parsers.py +23 -0
- regscale/integrations/commercial/wizv2/scanner.py +84 -29
- regscale/integrations/commercial/wizv2/utils.py +91 -4
- regscale/integrations/commercial/wizv2/variables.py +2 -1
- regscale/integrations/commercial/wizv2/wiz_auth.py +3 -3
- regscale/integrations/public/fedramp/fedramp_docx.py +2 -3
- regscale/integrations/scanner_integration.py +7 -2
- regscale/models/app_models/import_validater.py +5 -1
- regscale/models/app_models/mapping.py +3 -1
- regscale/models/integration_models/cisa_kev_data.json +140 -5
- regscale/models/integration_models/flat_file_importer/__init__.py +2 -3
- regscale/models/integration_models/qualys.py +24 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +2 -0
- regscale/models/regscale_models/asset.py +1 -1
- regscale/models/regscale_models/modules.py +88 -1
- regscale/models/regscale_models/regscale_model.py +7 -1
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/models/regscale_models/vulnerability_mapping.py +2 -2
- regscale/regscale.py +2 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/RECORD +37 -34
- tests/regscale/test_init.py +94 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/top_level.txt +0 -0
|
@@ -131,6 +131,9 @@ INVENTORY_QUERY = """
|
|
|
131
131
|
graphEntity{
|
|
132
132
|
id
|
|
133
133
|
providerUniqueId
|
|
134
|
+
publicExposures(first: 5) {
|
|
135
|
+
totalCount
|
|
136
|
+
}
|
|
134
137
|
name
|
|
135
138
|
type
|
|
136
139
|
projects {
|
|
@@ -435,6 +438,18 @@ VULNERABILITY_QUERY = """
|
|
|
435
438
|
name
|
|
436
439
|
detailedName
|
|
437
440
|
description
|
|
441
|
+
commentThread {
|
|
442
|
+
comments(first:100) {
|
|
443
|
+
edges {
|
|
444
|
+
node {
|
|
445
|
+
body,
|
|
446
|
+
author {
|
|
447
|
+
name
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
},
|
|
438
453
|
severity: vendorSeverity
|
|
439
454
|
weightedSeverity
|
|
440
455
|
status
|
|
@@ -271,6 +271,29 @@ def get_ip_address_from_props(network_dict: Dict) -> Optional[str]:
|
|
|
271
271
|
return network_dict.get("ip4_address") or network_dict.get("ip6_address")
|
|
272
272
|
|
|
273
273
|
|
|
274
|
+
def get_ip_v4_from_props(network_dict: Dict) -> Optional[str]:
|
|
275
|
+
"""
|
|
276
|
+
Get IPv4 address from properties
|
|
277
|
+
:param Dict network_dict: Network dictionary
|
|
278
|
+
:return: IPv4 address if it can be parsed from the network dictionary
|
|
279
|
+
:rtype: Optional[str]
|
|
280
|
+
"""
|
|
281
|
+
ip = network_dict.get("address")
|
|
282
|
+
if ip:
|
|
283
|
+
logger.info("get_ip_v4_from_props: %s", ip)
|
|
284
|
+
return network_dict.get("address")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_ip_v6_from_props(network_dict: Dict) -> Optional[str]:
|
|
288
|
+
"""
|
|
289
|
+
Get IPv6 address from properties
|
|
290
|
+
:param Dict network_dict: Network dictionary
|
|
291
|
+
:return: IPv6 address if it can be parsed from the network dictionary
|
|
292
|
+
:rtype: Optional[str]
|
|
293
|
+
"""
|
|
294
|
+
return network_dict.get("ip6_address")
|
|
295
|
+
|
|
296
|
+
|
|
274
297
|
def fetch_wiz_data(
|
|
275
298
|
query: str,
|
|
276
299
|
variables: dict,
|
|
@@ -5,10 +5,11 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
-
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
8
|
+
from typing import Any, Dict, Iterator, List, Optional, Union, Tuple
|
|
9
9
|
|
|
10
10
|
from regscale.core.app.utils.app_utils import check_file_path, get_current_datetime
|
|
11
11
|
from regscale.core.utils import get_base_protocol_from_port
|
|
12
|
+
from regscale.core.utils.date import format_to_regscale_iso
|
|
12
13
|
from regscale.integrations.commercial.wizv2.constants import (
|
|
13
14
|
INVENTORY_FILE_PATH,
|
|
14
15
|
INVENTORY_QUERY,
|
|
@@ -19,7 +20,6 @@ from regscale.integrations.commercial.wizv2.parsers import (
|
|
|
19
20
|
collect_components_to_create,
|
|
20
21
|
fetch_wiz_data,
|
|
21
22
|
get_disk_storage,
|
|
22
|
-
get_ip_address_from_props,
|
|
23
23
|
get_latest_version,
|
|
24
24
|
get_network_info,
|
|
25
25
|
get_product_ids,
|
|
@@ -101,7 +101,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
101
101
|
:yield: IntegrationFinding objects
|
|
102
102
|
:rtype: Iterator[IntegrationFinding]
|
|
103
103
|
"""
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
project_id = kwargs.get("wiz_project_id")
|
|
106
106
|
if not project_id:
|
|
107
107
|
raise ValueError("Wiz project ID is required")
|
|
@@ -137,7 +137,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
137
137
|
"""
|
|
138
138
|
for node in nodes:
|
|
139
139
|
if finding := self.parse_finding(node, vulnerability_type):
|
|
140
|
-
self.num_findings_to_process
|
|
140
|
+
self.num_findings_to_process = (self.num_findings_to_process or 0) + 1
|
|
141
141
|
yield finding
|
|
142
142
|
|
|
143
143
|
@classmethod
|
|
@@ -151,6 +151,25 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
151
151
|
"""
|
|
152
152
|
return cls.finding_severity_map.get(severity.capitalize(), regscale_models.IssueSeverity.Low)
|
|
153
153
|
|
|
154
|
+
def process_comments(self, comments_dict: Dict) -> Optional[str]:
|
|
155
|
+
"""
|
|
156
|
+
Processes comments from Wiz findings to match RegScale's comment format.
|
|
157
|
+
|
|
158
|
+
:param Dict comments_dict: The comments from the Wiz finding
|
|
159
|
+
:return: If available the Processed comments in RegScale format
|
|
160
|
+
:rtype: Optional[str]
|
|
161
|
+
"""
|
|
162
|
+
result = None
|
|
163
|
+
|
|
164
|
+
if comments := comments_dict.get("comments", {}).get("edges", []):
|
|
165
|
+
formatted_comments = [
|
|
166
|
+
f"{edge.get('node', {}).get('author', {}).get('name', 'Unknown')}: {edge.get('node', {}).get('body', 'No comment')}"
|
|
167
|
+
for edge in comments
|
|
168
|
+
]
|
|
169
|
+
# Join with newlines
|
|
170
|
+
result = "\n".join(formatted_comments)
|
|
171
|
+
return result
|
|
172
|
+
|
|
154
173
|
def parse_finding(
|
|
155
174
|
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
156
175
|
) -> Optional[IntegrationFinding]:
|
|
@@ -168,6 +187,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
168
187
|
return None
|
|
169
188
|
|
|
170
189
|
first_seen = node.get("firstDetectedAt") or node.get("firstSeenAt") or get_current_datetime()
|
|
190
|
+
first_seen = format_to_regscale_iso(first_seen)
|
|
171
191
|
severity = self.get_issue_severity(node.get("severity", "Low"))
|
|
172
192
|
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
173
193
|
|
|
@@ -179,6 +199,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
179
199
|
else node.get("cve", name)
|
|
180
200
|
)
|
|
181
201
|
|
|
202
|
+
comments_dict = node.get("commentThread", {})
|
|
203
|
+
formatted_comments = self.process_comments(comments_dict)
|
|
204
|
+
|
|
182
205
|
return IntegrationFinding(
|
|
183
206
|
control_labels=[],
|
|
184
207
|
category="Wiz Vulnerability",
|
|
@@ -190,7 +213,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
190
213
|
external_id=f"{node.get('sourceRule', {'id': cve}).get('id')}",
|
|
191
214
|
first_seen=first_seen,
|
|
192
215
|
date_created=first_seen,
|
|
193
|
-
last_seen=
|
|
216
|
+
last_seen=format_to_regscale_iso(
|
|
217
|
+
node.get("lastDetectedAt") or node.get("analyzedAt") or get_current_datetime()
|
|
218
|
+
),
|
|
194
219
|
remediation=node.get("description", ""),
|
|
195
220
|
cvss_score=node.get("score"),
|
|
196
221
|
cve=cve,
|
|
@@ -199,6 +224,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
199
224
|
source_rule_id=node.get("sourceRule", {}).get("id"),
|
|
200
225
|
vulnerability_type=vulnerability_type.value,
|
|
201
226
|
due_date=due_date,
|
|
227
|
+
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
228
|
+
identification="Vulnerability Assessment",
|
|
229
|
+
comments=formatted_comments,
|
|
230
|
+
poam_comments=formatted_comments,
|
|
202
231
|
)
|
|
203
232
|
except (KeyError, TypeError, ValueError) as e:
|
|
204
233
|
logger.error("Error parsing Wiz finding: %s", str(e), exc_info=True)
|
|
@@ -226,9 +255,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
226
255
|
:yields: Iterator[IntegrationAsset]
|
|
227
256
|
"""
|
|
228
257
|
self.authenticate(kwargs.get("client_id"), kwargs.get("client_secret"))
|
|
229
|
-
wiz_project_id = kwargs.get("wiz_project_id")
|
|
258
|
+
wiz_project_id: str = kwargs.get("wiz_project_id", "")
|
|
230
259
|
logger.info("Fetching Wiz assets...")
|
|
231
|
-
filter_by_override = kwargs.get("filter_by_override") or WizVariables.wizInventoryFilterBy
|
|
260
|
+
filter_by_override: Dict[str, Any] = kwargs.get("filter_by_override") or WizVariables.wizInventoryFilterBy or {}
|
|
232
261
|
filter_by = self.get_filter_by(filter_by_override, wiz_project_id)
|
|
233
262
|
|
|
234
263
|
variables = self.get_variables()
|
|
@@ -261,6 +290,32 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
261
290
|
filter_by["updatedAt"] = {"after": WizVariables.wizLastInventoryPull} # type: ignore
|
|
262
291
|
return filter_by
|
|
263
292
|
|
|
293
|
+
def get_software_details(
|
|
294
|
+
self, wiz_entity_properties: Dict, node: Dict[str, Any], software_name_dict: Dict[str, str], name: str
|
|
295
|
+
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
296
|
+
"""
|
|
297
|
+
Gets the software version, vendor, and name from the Wiz entity properties and node.
|
|
298
|
+
Handles container images differently by extracting the version and name from the image tags.
|
|
299
|
+
:param Dict wiz_entity_properties: The properties of the Wiz entity
|
|
300
|
+
:param Dict node: The Wiz node containing the entity
|
|
301
|
+
:param Dict software_name_dict: Dictionary containing software name and vendor
|
|
302
|
+
:param str name: The name of the software or container image
|
|
303
|
+
:return: A tuple containing software_version, software_vendor, and software_name
|
|
304
|
+
:rtype: Tuple[Optional[str], Optional[str], Optional[str]]
|
|
305
|
+
"""
|
|
306
|
+
if node.get("type", "") == "CONTAINER_IMAGE":
|
|
307
|
+
software_version = handle_container_image_version(
|
|
308
|
+
image_tags=wiz_entity_properties.get("imageTags", []), name=name
|
|
309
|
+
)
|
|
310
|
+
software_name = name.split(":")[0].split("/")[-1] if name else ""
|
|
311
|
+
software_vendor = name.split(":")[0].split("/")[1] if len(name.split(":")[0].split("/")) > 1 else None
|
|
312
|
+
else:
|
|
313
|
+
software_version = self.get_software_version(wiz_entity_properties, node)
|
|
314
|
+
software_name = self.get_software_name(software_name_dict, wiz_entity_properties, node)
|
|
315
|
+
software_vendor = self.get_software_vendor(software_name_dict, wiz_entity_properties, node)
|
|
316
|
+
|
|
317
|
+
return software_version, software_vendor, software_name
|
|
318
|
+
|
|
264
319
|
def parse_asset(self, node: Dict[str, Any]) -> Optional[IntegrationAsset]:
|
|
265
320
|
"""
|
|
266
321
|
Parses Wiz assets
|
|
@@ -276,23 +331,20 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
276
331
|
return None
|
|
277
332
|
|
|
278
333
|
wiz_entity_properties = wiz_entity.get("properties", {})
|
|
334
|
+
is_public = False
|
|
335
|
+
if public_exposures := wiz_entity.get("publicExposures"):
|
|
336
|
+
if exposure_count := public_exposures.get("totalCount"):
|
|
337
|
+
is_public = exposure_count > 0
|
|
338
|
+
|
|
279
339
|
network_dict = get_network_info(wiz_entity_properties)
|
|
280
340
|
handle_provider_dict = handle_provider(wiz_entity_properties)
|
|
281
341
|
software_name_dict = get_software_name_from_cpe(wiz_entity_properties, name)
|
|
282
342
|
software_list = self.create_name_version_dict(wiz_entity_properties.get("installedPackages", []))
|
|
283
|
-
|
|
284
343
|
ports_and_protocols = self.get_ports_and_protocols(wiz_entity_properties)
|
|
285
344
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
290
|
-
software_name = name.split(":")[0].split("/")[-1] if name else ""
|
|
291
|
-
software_vendor = name.split(":")[0].split("/")[1] if len(name.split(":")[0].split("/")) > 1 else None
|
|
292
|
-
else:
|
|
293
|
-
software_version = self.get_software_version(wiz_entity_properties, node)
|
|
294
|
-
software_name = self.get_software_name(software_name_dict, wiz_entity_properties, node)
|
|
295
|
-
software_vendor = self.get_software_vendor(software_name_dict, wiz_entity_properties, node)
|
|
345
|
+
software_version, software_vendor, software_name = self.get_software_details(
|
|
346
|
+
wiz_entity_properties, node, software_name_dict, name
|
|
347
|
+
)
|
|
296
348
|
|
|
297
349
|
if WizVariables.useWizHardwareAssetTypes and node.get("graphEntity", {}).get("technologies", []):
|
|
298
350
|
technologies = node.get("graphEntity", {}).get("technologies", [])
|
|
@@ -316,7 +368,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
316
368
|
date_last_updated=wiz_entity.get("lastSeen", ""),
|
|
317
369
|
management_type=handle_management_type(wiz_entity_properties),
|
|
318
370
|
status=self.map_wiz_status(wiz_entity_properties.get("status")),
|
|
319
|
-
ip_address=
|
|
371
|
+
ip_address=network_dict.get("ip4_address"),
|
|
372
|
+
ipv6_address=network_dict.get("ip6_address"),
|
|
320
373
|
software_vendor=software_vendor,
|
|
321
374
|
software_version=software_version,
|
|
322
375
|
software_name=software_name,
|
|
@@ -325,10 +378,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
325
378
|
model=wiz_entity_properties.get("nativeType"),
|
|
326
379
|
manufacturer=wiz_entity_properties.get("cloudPlatform"),
|
|
327
380
|
serial_number=get_product_ids(wiz_entity_properties),
|
|
328
|
-
is_public_facing=
|
|
329
|
-
azure_identifier=handle_provider_dict.get("azureIdentifier"),
|
|
381
|
+
is_public_facing=is_public,
|
|
382
|
+
azure_identifier=handle_provider_dict.get("azureIdentifier", ""),
|
|
330
383
|
mac_address=wiz_entity_properties.get("macAddress"),
|
|
331
|
-
fqdn=
|
|
384
|
+
fqdn=network_dict.get("dns") or wiz_entity_properties.get("dnsName"),
|
|
332
385
|
disk_storage=get_disk_storage(wiz_entity_properties) or 0,
|
|
333
386
|
cpu=pull_resource_info_from_props(wiz_entity_properties)[1] or 0,
|
|
334
387
|
ram=pull_resource_info_from_props(wiz_entity_properties)[0] or 0,
|
|
@@ -337,9 +390,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
337
390
|
end_of_life_date=wiz_entity_properties.get("versionEndOfLifeDate"),
|
|
338
391
|
vlan_id=wiz_entity_properties.get("zone"),
|
|
339
392
|
uri=network_dict.get("url"),
|
|
340
|
-
aws_identifier=handle_provider_dict.get("awsIdentifier"),
|
|
341
|
-
google_identifier=handle_provider_dict.get("googleIdentifier"),
|
|
342
|
-
other_cloud_identifier=handle_provider_dict.get("otherCloudIdentifier"),
|
|
393
|
+
aws_identifier=handle_provider_dict.get("awsIdentifier", ""),
|
|
394
|
+
google_identifier=handle_provider_dict.get("googleIdentifier", ""),
|
|
395
|
+
other_cloud_identifier=handle_provider_dict.get("otherCloudIdentifier", ""),
|
|
343
396
|
patch_level=get_latest_version(wiz_entity_properties),
|
|
344
397
|
cpe=wiz_entity_properties.get("cpe"),
|
|
345
398
|
component_names=collect_components_to_create([node], []),
|
|
@@ -378,7 +431,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
378
431
|
:return: Software vendor
|
|
379
432
|
:rtype: Optional[str]
|
|
380
433
|
"""
|
|
381
|
-
if map_category(node.get("type")) == regscale_models.AssetCategory.Software:
|
|
434
|
+
if map_category(node.get("type", "")) == regscale_models.AssetCategory.Software:
|
|
382
435
|
return software_name_dict.get("software_vendor") or wiz_entity_properties.get("cloudPlatform")
|
|
383
436
|
return None
|
|
384
437
|
|
|
@@ -392,8 +445,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
392
445
|
:return: Software version
|
|
393
446
|
:rtype: Optional[str]
|
|
394
447
|
"""
|
|
395
|
-
if map_category(node.get("type")) == regscale_models.AssetCategory.Software:
|
|
396
|
-
return handle_software_version(wiz_entity_properties, map_category(node.get("type"))) or "1.0"
|
|
448
|
+
if map_category(node.get("type", "")) == regscale_models.AssetCategory.Software:
|
|
449
|
+
return handle_software_version(wiz_entity_properties, map_category(node.get("type", ""))) or "1.0"
|
|
397
450
|
return None
|
|
398
451
|
|
|
399
452
|
@staticmethod
|
|
@@ -407,7 +460,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
407
460
|
:return: Software name
|
|
408
461
|
:rtype: Optional[str]
|
|
409
462
|
"""
|
|
410
|
-
if map_category(node.get("type")) == regscale_models.AssetCategory.Software:
|
|
463
|
+
if map_category(node.get("type", "")) == regscale_models.AssetCategory.Software:
|
|
411
464
|
return software_name_dict.get("software_name") or wiz_entity_properties.get("nativeType")
|
|
412
465
|
return None
|
|
413
466
|
|
|
@@ -458,6 +511,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
458
511
|
else:
|
|
459
512
|
logger.info("File %s does not exist. Fetching new data...", file_path)
|
|
460
513
|
|
|
514
|
+
self.authenticate(WizVariables.wizClientId, WizVariables.wizClientSecret)
|
|
515
|
+
|
|
461
516
|
if not self.wiz_token:
|
|
462
517
|
raise ValueError("Wiz token is not set. Please authenticate first.")
|
|
463
518
|
|
|
@@ -19,6 +19,7 @@ from pydantic import ValidationError
|
|
|
19
19
|
from rich.progress import Progress, TaskID
|
|
20
20
|
|
|
21
21
|
from regscale.core.app.api import Api
|
|
22
|
+
from regscale.core.app.application import Application
|
|
22
23
|
from regscale.core.app.utils.app_utils import (
|
|
23
24
|
error_and_exit,
|
|
24
25
|
check_file_path,
|
|
@@ -50,6 +51,8 @@ from regscale.models import (
|
|
|
50
51
|
ControlImplementation,
|
|
51
52
|
Assessment,
|
|
52
53
|
regscale_models,
|
|
54
|
+
ControlImplementationStatus,
|
|
55
|
+
ImplementationObjective,
|
|
53
56
|
)
|
|
54
57
|
from regscale.utils import PaginatedGraphQLClient
|
|
55
58
|
from regscale.utils.decorators import deprecated
|
|
@@ -58,6 +61,31 @@ logger = logging.getLogger("regscale")
|
|
|
58
61
|
compliance_job_progress = create_progress_object()
|
|
59
62
|
|
|
60
63
|
|
|
64
|
+
def is_report_expired(report_run_at: str, max_age_days: int) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Check if a report is expired based on its run date
|
|
67
|
+
|
|
68
|
+
:param str report_run_at: Report run date in ISO format
|
|
69
|
+
:param int max_age_days: Maximum age in days
|
|
70
|
+
:return: True if report is expired, False otherwise
|
|
71
|
+
:rtype: bool
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
run_date = datetime_obj(report_run_at)
|
|
75
|
+
if not run_date:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
# Convert to naive datetime for comparison
|
|
79
|
+
run_date_naive = run_date.replace(tzinfo=None)
|
|
80
|
+
current_date = datetime.datetime.now()
|
|
81
|
+
age_in_days = (current_date - run_date_naive).days
|
|
82
|
+
|
|
83
|
+
return age_in_days >= max_age_days
|
|
84
|
+
except (ValueError, TypeError):
|
|
85
|
+
# If we can't parse the date, consider it expired
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
|
|
61
89
|
def get_notes_from_wiz_props(wiz_entity_properties: Dict, external_id: str) -> str:
|
|
62
90
|
"""
|
|
63
91
|
Get notes from wiz properties
|
|
@@ -418,15 +446,28 @@ def get_or_create_report_id(
|
|
|
418
446
|
:param target_framework: Target framework name with underscores
|
|
419
447
|
:return: Single report ID
|
|
420
448
|
"""
|
|
449
|
+
app = Application()
|
|
450
|
+
report_age_days = app.config.get("wizReportAge", 15)
|
|
421
451
|
report_name = f"{target_framework}_project_{project_id}"
|
|
422
452
|
|
|
423
453
|
# Check for existing report with exact name
|
|
424
454
|
for report in existing_reports:
|
|
425
455
|
if report.get("name") == report_name:
|
|
426
456
|
logger.info(f"Found existing report '{report_name}' with ID {report['id']}")
|
|
427
|
-
return report["id"]
|
|
428
457
|
|
|
429
|
-
|
|
458
|
+
# Check if report is expired based on wizReportAge
|
|
459
|
+
run_at = report.get("lastRun", {}).get("runAt")
|
|
460
|
+
|
|
461
|
+
if run_at and is_report_expired(run_at, report_age_days):
|
|
462
|
+
logger.info(
|
|
463
|
+
f"Report '{report_name}' is expired (older than {report_age_days} days), will create new report"
|
|
464
|
+
)
|
|
465
|
+
break
|
|
466
|
+
else:
|
|
467
|
+
logger.info(f"Report '{report_name}' is still valid, using existing report")
|
|
468
|
+
return report["id"]
|
|
469
|
+
|
|
470
|
+
# Create new report if no valid existing report found
|
|
430
471
|
try:
|
|
431
472
|
framework_index = frameworks.index(target_framework)
|
|
432
473
|
framework_id = wiz_frameworks[framework_index].get("id")
|
|
@@ -756,7 +797,7 @@ def _sync_compliance(
|
|
|
756
797
|
compliance_job_progress.update(report_job, completed=True, advance=1)
|
|
757
798
|
|
|
758
799
|
if catalog_id:
|
|
759
|
-
logger.info("Fetching all Controls for catalog #%
|
|
800
|
+
logger.info("Fetching all Controls for catalog #%s...", catalog_id)
|
|
760
801
|
catalog = Catalog.get_with_all_details(catalog_id=catalog_id)
|
|
761
802
|
controls = catalog.get("controls") if catalog else []
|
|
762
803
|
else:
|
|
@@ -916,7 +957,7 @@ def create_report_assessment(filtered_results: List, reports: List, control_id:
|
|
|
916
957
|
for report in reports:
|
|
917
958
|
html_summary = format_dict_to_html(report.dict())
|
|
918
959
|
if implementation:
|
|
919
|
-
Assessment(
|
|
960
|
+
a = Assessment(
|
|
920
961
|
leadAssessorId=implementation.createdById,
|
|
921
962
|
title=f"Wiz compliance report assessment for {control_id}",
|
|
922
963
|
assessmentType="Control Testing",
|
|
@@ -930,3 +971,49 @@ def create_report_assessment(filtered_results: List, reports: List, control_id:
|
|
|
930
971
|
parentModule="controls",
|
|
931
972
|
isPublic=True,
|
|
932
973
|
).create()
|
|
974
|
+
update_implementation_status(
|
|
975
|
+
implementation=implementation,
|
|
976
|
+
result=report.result,
|
|
977
|
+
)
|
|
978
|
+
logger.info(f"Created report assessment for {control_id}: {a.id}")
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def update_implementation_status(implementation: ControlImplementation, result: str) -> ControlImplementation:
|
|
982
|
+
"""
|
|
983
|
+
Update implementation status based on the report result
|
|
984
|
+
|
|
985
|
+
:param ControlImplementation implementation: Control Implementation object
|
|
986
|
+
:param str result: Report result
|
|
987
|
+
:return: Updated Control Implementation object
|
|
988
|
+
:rtype: ControlImplementation
|
|
989
|
+
"""
|
|
990
|
+
objectives = ImplementationObjective.get_all_by_parent(
|
|
991
|
+
parent_module=implementation.get_module_slug(),
|
|
992
|
+
parent_id=implementation.id,
|
|
993
|
+
)
|
|
994
|
+
if objectives:
|
|
995
|
+
for objective in objectives:
|
|
996
|
+
objective.status = report_result_to_implementation_status(result)
|
|
997
|
+
objective.save()
|
|
998
|
+
logger.debug(f"Updated status for {objective.id}: {objective.status}")
|
|
999
|
+
else:
|
|
1000
|
+
implementation.objectives = []
|
|
1001
|
+
implementation.status = report_result_to_implementation_status(result)
|
|
1002
|
+
implementation.save()
|
|
1003
|
+
logger.info(f"Updated implementation status for {implementation.id}: {implementation.status}")
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def report_result_to_implementation_status(result: str) -> str:
|
|
1007
|
+
"""
|
|
1008
|
+
Convert report result to implementation status
|
|
1009
|
+
|
|
1010
|
+
:param str result: Report result
|
|
1011
|
+
:return: Implementation status
|
|
1012
|
+
:rtype: str
|
|
1013
|
+
"""
|
|
1014
|
+
if result == ComplianceCheckStatus.PASS.value:
|
|
1015
|
+
return ControlImplementationStatus.Implemented.value
|
|
1016
|
+
elif result == ComplianceCheckStatus.FAIL.value:
|
|
1017
|
+
return ControlImplementationStatus.InRemediation.value
|
|
1018
|
+
else:
|
|
1019
|
+
return ControlImplementationStatus.NotImplemented.value
|
|
@@ -31,7 +31,7 @@ class WizVariables(metaclass=RsVariablesMeta):
|
|
|
31
31
|
"PRIVATE_LINK", "RAW_ACCESS_POLICY", "REGISTERED_DOMAIN", "RESOURCE_GROUP", "SECRET",
|
|
32
32
|
"SECRET_CONTAINER", "SERVERLESS", "SERVERLESS_PACKAGE", "SERVICE_ACCOUNT", "SERVICE_CONFIGURATION",
|
|
33
33
|
"STORAGE_ACCOUNT", "SUBNET", "SUBSCRIPTION", "VIRTUAL_DESKTOP", "VIRTUAL_MACHINE",
|
|
34
|
-
"VIRTUAL_MACHINE_IMAGE", "VIRTUAL_NETWORK", "VOLUME", "WEB_SERVICE" ] }""",
|
|
34
|
+
"VIRTUAL_MACHINE_IMAGE", "VIRTUAL_NETWORK", "VOLUME", "WEB_SERVICE", "NETWORK_ADDRESS"] }""",
|
|
35
35
|
) # type: ignore
|
|
36
36
|
wizAccessToken: RsVariableType(str, "", sensitive=True, required=False) # type: ignore
|
|
37
37
|
wizClientId: RsVariableType(str, "", sensitive=True) # type: ignore
|
|
@@ -44,3 +44,4 @@ class WizVariables(metaclass=RsVariablesMeta):
|
|
|
44
44
|
default=["SERVER_APPLICATION", "CLIENT_APPLICATION", "VIRTUAL_APPLIANCE"],
|
|
45
45
|
required=False,
|
|
46
46
|
) # type: ignore
|
|
47
|
+
wizReportAge: RsVariableType(int, "14", default=14, required=False) # type: ignore
|
|
@@ -49,10 +49,10 @@ def wiz_authenticate(client_id: Optional[str] = None, client_secret: Optional[st
|
|
|
49
49
|
# get secrets
|
|
50
50
|
client_id = WizVariables.wizClientId if client_id is None else client_id
|
|
51
51
|
if not client_id:
|
|
52
|
-
|
|
52
|
+
error_and_exit("No Wiz Client ID provided in system environment or CLI command.")
|
|
53
53
|
client_secret = WizVariables.wizClientSecret if client_secret is None else client_secret
|
|
54
54
|
if not client_secret:
|
|
55
|
-
|
|
55
|
+
error_and_exit("No Wiz Client Secret provided in system environment or CLI command.")
|
|
56
56
|
wiz_auth_url = config.get("wizAuthUrl")
|
|
57
57
|
if not wiz_auth_url:
|
|
58
58
|
error_and_exit("No Wiz Authentication URL provided in the init.yaml file.")
|
|
@@ -100,7 +100,7 @@ def get_token(api: Api, client_id: str, client_secret: str, token_url: str) -> t
|
|
|
100
100
|
)
|
|
101
101
|
if response.ok:
|
|
102
102
|
status_code = 200
|
|
103
|
-
logger.
|
|
103
|
+
logger.info(response.reason)
|
|
104
104
|
# If response is unauthorized, try the first cognito url
|
|
105
105
|
if response.status_code == requests.codes.unauthorized:
|
|
106
106
|
try:
|
|
@@ -12,6 +12,7 @@ from ssp import SSP # type: ignore
|
|
|
12
12
|
|
|
13
13
|
from regscale.core.app.api import Api
|
|
14
14
|
from regscale.core.app.application import Application
|
|
15
|
+
from regscale.core.app.utils.app_utils import error_and_exit
|
|
15
16
|
from regscale.integrations.public.fedramp.fedramp_common import (
|
|
16
17
|
get_profile_info_by_id,
|
|
17
18
|
logger,
|
|
@@ -38,10 +39,8 @@ from regscale.integrations.public.fedramp.fedramp_common import (
|
|
|
38
39
|
post_ports,
|
|
39
40
|
post_links,
|
|
40
41
|
post_implementations,
|
|
41
|
-
debug_logger,
|
|
42
42
|
post_leveraged_authorizations,
|
|
43
43
|
)
|
|
44
|
-
from regscale.core.app.utils.app_utils import error_and_exit
|
|
45
44
|
from regscale.models import ProfileMapping
|
|
46
45
|
|
|
47
46
|
|
|
@@ -365,7 +364,7 @@ def process_fedramp_docx(
|
|
|
365
364
|
load_missing=load_missing,
|
|
366
365
|
)
|
|
367
366
|
except Exception as e:
|
|
368
|
-
|
|
367
|
+
logger.info(e)
|
|
369
368
|
logger.error(
|
|
370
369
|
f"Unable to gather implementations: {e}",
|
|
371
370
|
record_type="implementations",
|
|
@@ -247,6 +247,7 @@ class IntegrationAsset:
|
|
|
247
247
|
mac_address: Optional[str] = None
|
|
248
248
|
fqdn: Optional[str] = None
|
|
249
249
|
ip_address: Optional[str] = None
|
|
250
|
+
ipv6_address: Optional[str] = None
|
|
250
251
|
component_names: List[str] = dataclasses.field(default_factory=list)
|
|
251
252
|
is_virtual: bool = True
|
|
252
253
|
|
|
@@ -1112,6 +1113,7 @@ class ScannerIntegration(ABC):
|
|
|
1112
1113
|
azureIdentifier=asset.azure_identifier,
|
|
1113
1114
|
location=asset.location,
|
|
1114
1115
|
ipAddress=asset.ip_address,
|
|
1116
|
+
iPv6Address=asset.ipv6_address,
|
|
1115
1117
|
fqdn=asset.fqdn,
|
|
1116
1118
|
macAddress=asset.mac_address,
|
|
1117
1119
|
diskStorage=asset.disk_storage,
|
|
@@ -1651,6 +1653,7 @@ class ScannerIntegration(ABC):
|
|
|
1651
1653
|
issue.riskAdjustment = finding.risk_adjustment
|
|
1652
1654
|
issue.operationalRequirement = finding.operational_requirements
|
|
1653
1655
|
issue.deviationRationale = finding.deviation_rationale
|
|
1656
|
+
issue.dateLastUpdated = get_current_datetime()
|
|
1654
1657
|
|
|
1655
1658
|
if finding.cve:
|
|
1656
1659
|
issue = self.lookup_kev_and_upate_issue(cve=finding.cve, issue=issue, cisa_kevs=self._kev_data)
|
|
@@ -2323,7 +2326,7 @@ class ScannerIntegration(ABC):
|
|
|
2323
2326
|
or finding.observations, # or finding.evidence, whichever is more appropriate
|
|
2324
2327
|
port=finding.port if hasattr(finding, "port") else None,
|
|
2325
2328
|
protocol=finding.protocol if hasattr(finding, "protocol") else None,
|
|
2326
|
-
operatingSystem=asset.
|
|
2329
|
+
operatingSystem=asset.operatingSystem if hasattr(asset, "operatingSystem") else None,
|
|
2327
2330
|
fixedVersions=finding.fixed_versions,
|
|
2328
2331
|
buildVersion=finding.build_version,
|
|
2329
2332
|
fixStatus=finding.fix_status,
|
|
@@ -2339,7 +2342,7 @@ class ScannerIntegration(ABC):
|
|
|
2339
2342
|
vulnerabilityId=vulnerability.id,
|
|
2340
2343
|
assetId=asset.id,
|
|
2341
2344
|
scanId=scan_history.id,
|
|
2342
|
-
|
|
2345
|
+
securityPlanId=self.plan_id if not self.is_component else None,
|
|
2343
2346
|
createdById=self.assessor_id,
|
|
2344
2347
|
tenantsId=self.tenant_id,
|
|
2345
2348
|
isPublic=True,
|
|
@@ -2347,6 +2350,7 @@ class ScannerIntegration(ABC):
|
|
|
2347
2350
|
firstSeen=finding.first_seen,
|
|
2348
2351
|
lastSeen=finding.last_seen,
|
|
2349
2352
|
status=finding.status,
|
|
2353
|
+
dateLastUpdated=get_current_datetime(),
|
|
2350
2354
|
).create_unique()
|
|
2351
2355
|
return vulnerability
|
|
2352
2356
|
|
|
@@ -2534,6 +2538,7 @@ class ScannerIntegration(ABC):
|
|
|
2534
2538
|
f"{get_current_datetime('%b %d, %Y')} - Closed by {self.title} for having no current vulnerabilities."
|
|
2535
2539
|
)
|
|
2536
2540
|
issue.changes = f"{issue.changes}\n{changes_text}" if issue.changes else changes_text
|
|
2541
|
+
issue.dateLastUpdated = get_current_datetime()
|
|
2537
2542
|
issue.save()
|
|
2538
2543
|
|
|
2539
2544
|
with count_lock:
|
|
@@ -133,9 +133,13 @@ class ImportValidater:
|
|
|
133
133
|
:param Union[list, pd.Index] headers: List of headers from the file
|
|
134
134
|
"""
|
|
135
135
|
import re
|
|
136
|
+
from pandas import Index
|
|
136
137
|
|
|
137
138
|
from regscale.models import Mapping
|
|
138
139
|
|
|
140
|
+
if isinstance(headers, Index):
|
|
141
|
+
headers = [str(header) for header in headers] # Convert pd.Index to list of strings
|
|
142
|
+
|
|
139
143
|
if not self.ignore_unnamed and any(re.search(r"unnamed", header, re.IGNORECASE) for header in headers): # type: ignore
|
|
140
144
|
raise ValidationException(
|
|
141
145
|
f"Unable to parse headers from the file. Please ensure the headers are named in {self.file_path}"
|
|
@@ -227,7 +231,7 @@ class ImportValidater:
|
|
|
227
231
|
elif self.worksheet_name:
|
|
228
232
|
df = pandas.read_excel(file_path, sheet_name=self.worksheet_name)
|
|
229
233
|
elif self.skip_rows:
|
|
230
|
-
df = pandas.read_excel(file_path, skiprows=self.skip_rows)
|
|
234
|
+
df = pandas.read_excel(file_path, skiprows=self.skip_rows - 1)
|
|
231
235
|
else:
|
|
232
236
|
df = pandas.read_excel(file_path)
|
|
233
237
|
except Exception as e:
|
|
@@ -121,8 +121,10 @@ class Mapping(BaseModel):
|
|
|
121
121
|
mapping = values.data.get("mapping")
|
|
122
122
|
if mapping is not None and expected_field_names is not None:
|
|
123
123
|
if missing_fields := [field for field in expected_field_names if field not in mapping]:
|
|
124
|
+
# Get file_path_for_prompt from values instead of cls
|
|
125
|
+
file_path = values.data.get("file_path_for_prompt")
|
|
124
126
|
for field in missing_fields:
|
|
125
|
-
cls._prompt_for_field(field, mapping,
|
|
127
|
+
cls._prompt_for_field(field, mapping, file_path)
|
|
126
128
|
return expected_field_names
|
|
127
129
|
|
|
128
130
|
@field_validator("expected_field_names")
|