regscale-cli 6.20.1.1__py3-none-any.whl → 6.20.3.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/core/app/utils/variables.py +5 -3
- regscale/integrations/commercial/__init__.py +15 -0
- regscale/integrations/commercial/axonius/__init__.py +0 -0
- regscale/integrations/commercial/axonius/axonius_integration.py +70 -0
- regscale/integrations/commercial/burp.py +14 -0
- regscale/integrations/commercial/grype/commands.py +8 -1
- regscale/integrations/commercial/grype/scanner.py +2 -1
- regscale/integrations/commercial/jira.py +288 -137
- regscale/integrations/commercial/opentext/commands.py +14 -5
- regscale/integrations/commercial/opentext/scanner.py +3 -2
- regscale/integrations/commercial/qualys/__init__.py +3 -3
- regscale/integrations/commercial/stigv2/click_commands.py +6 -37
- regscale/integrations/commercial/synqly/assets.py +10 -0
- regscale/integrations/commercial/tenablev2/commands.py +12 -4
- regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
- regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
- regscale/integrations/commercial/trivy/commands.py +11 -4
- regscale/integrations/commercial/trivy/scanner.py +2 -1
- regscale/integrations/commercial/wizv2/constants.py +4 -0
- regscale/integrations/commercial/wizv2/scanner.py +67 -14
- regscale/integrations/commercial/wizv2/utils.py +24 -10
- regscale/integrations/commercial/wizv2/variables.py +7 -0
- regscale/integrations/jsonl_scanner_integration.py +8 -1
- regscale/integrations/public/cisa.py +58 -63
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +153 -104
- regscale/integrations/scanner_integration.py +30 -8
- regscale/integrations/variables.py +1 -0
- regscale/models/app_models/click.py +49 -1
- regscale/models/app_models/import_validater.py +3 -1
- regscale/models/integration_models/axonius_models/__init__.py +0 -0
- regscale/models/integration_models/axonius_models/connectors/__init__.py +3 -0
- regscale/models/integration_models/axonius_models/connectors/assets.py +111 -0
- regscale/models/integration_models/burp.py +11 -8
- regscale/models/integration_models/cisa_kev_data.json +204 -23
- regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
- regscale/models/integration_models/jira_task_sync.py +27 -0
- regscale/models/integration_models/qualys.py +6 -7
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +2 -1
- regscale/models/regscale_models/control_implementation.py +39 -2
- regscale/models/regscale_models/issue.py +1 -0
- regscale/models/regscale_models/regscale_model.py +49 -1
- regscale/models/regscale_models/risk_issue_mapping.py +61 -0
- regscale/models/regscale_models/task.py +1 -0
- regscale/regscale.py +1 -4
- regscale/utils/graphql_client.py +4 -4
- regscale/utils/string.py +13 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/RECORD +54 -48
- regscale/integrations/commercial/synqly_jira.py +0 -840
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/top_level.txt +0 -0
|
@@ -27,9 +27,10 @@ def web_inspect():
|
|
|
27
27
|
|
|
28
28
|
@web_inspect.command(name="import_scans")
|
|
29
29
|
@FlatFileImporter.common_scanner_options(
|
|
30
|
-
message="File path to the folder containing
|
|
31
|
-
prompt="File path for
|
|
32
|
-
import_name="
|
|
30
|
+
message="File path to the folder containing OpenText WebInspect .xml files to process to RegScale.",
|
|
31
|
+
prompt="File path for OpenText WebInspect files",
|
|
32
|
+
import_name="opentext",
|
|
33
|
+
support_component=True,
|
|
33
34
|
)
|
|
34
35
|
@click.option(
|
|
35
36
|
"--destination",
|
|
@@ -41,7 +42,7 @@ def web_inspect():
|
|
|
41
42
|
@click.option(
|
|
42
43
|
"--file_pattern",
|
|
43
44
|
"-fp",
|
|
44
|
-
help="[Optional] File pattern to match (e.g., '*.
|
|
45
|
+
help="[Optional] File pattern to match (e.g., '*.xml')",
|
|
45
46
|
required=False,
|
|
46
47
|
)
|
|
47
48
|
def import_scans(
|
|
@@ -49,6 +50,7 @@ def import_scans(
|
|
|
49
50
|
file_pattern: str,
|
|
50
51
|
folder_path: Path,
|
|
51
52
|
regscale_ssp_id: int,
|
|
53
|
+
component_id: int,
|
|
52
54
|
scan_date: datetime,
|
|
53
55
|
mappings_path: Path,
|
|
54
56
|
disable_mapping: bool,
|
|
@@ -63,8 +65,15 @@ def import_scans(
|
|
|
63
65
|
# Use the new WebInspectIntegration class to sync assets and findings
|
|
64
66
|
if s3_bucket and not folder_path:
|
|
65
67
|
folder_path = s3_bucket
|
|
68
|
+
|
|
69
|
+
if not regscale_ssp_id and not component_id:
|
|
70
|
+
raise click.UsageError(
|
|
71
|
+
"You must provide either a --regscale_ssp_id or a --component_id to import OpenText scans."
|
|
72
|
+
)
|
|
73
|
+
|
|
66
74
|
wi = WebInspectIntegration(
|
|
67
|
-
plan_id=regscale_ssp_id,
|
|
75
|
+
plan_id=component_id if component_id else regscale_ssp_id,
|
|
76
|
+
is_component=True if component_id else False,
|
|
68
77
|
file_path=str(folder_path) if folder_path else None,
|
|
69
78
|
s3_bucket=s3_bucket,
|
|
70
79
|
s3_prefix=s3_prefix,
|
|
@@ -48,6 +48,7 @@ class WebInspectIntegration(JSONLScannerIntegration):
|
|
|
48
48
|
kwargs["read_files_only"] = True
|
|
49
49
|
self.disable_mapping = kwargs["disable_mapping"] = True
|
|
50
50
|
self.set_scan_date(kwargs.get("scan_date"))
|
|
51
|
+
self.is_component = kwargs.get("is_component", False)
|
|
51
52
|
# logger.debug(f"scan_date: {self.scan_date}"
|
|
52
53
|
super().__init__(*args, **kwargs)
|
|
53
54
|
logger.debug(f"WebInspectIntegration initialized with scan date: {self.scan_date}")
|
|
@@ -400,7 +401,7 @@ class WebInspectIntegration(JSONLScannerIntegration):
|
|
|
400
401
|
asset_type="Other",
|
|
401
402
|
asset_category="Hardware",
|
|
402
403
|
parent_id=self.plan_id,
|
|
403
|
-
parent_module="securityplans",
|
|
404
|
+
parent_module="securityplans" if not self.is_component else "components",
|
|
404
405
|
)
|
|
405
406
|
|
|
406
407
|
# Get the host from the first issue
|
|
@@ -416,7 +417,7 @@ class WebInspectIntegration(JSONLScannerIntegration):
|
|
|
416
417
|
asset_type="Other",
|
|
417
418
|
asset_category="Hardware",
|
|
418
419
|
parent_id=self.plan_id,
|
|
419
|
-
parent_module="securityplans",
|
|
420
|
+
parent_module="securityplans" if not self.is_component else "components",
|
|
420
421
|
fqdn=url,
|
|
421
422
|
)
|
|
422
423
|
|
|
@@ -718,7 +718,7 @@ def export_past_scans(save_output_to: Path, days: int, export: bool = True):
|
|
|
718
718
|
|
|
719
719
|
@qualys.command(name="import_scans")
|
|
720
720
|
@FlatFileImporter.common_scanner_options(
|
|
721
|
-
message="File path to the folder containing
|
|
721
|
+
message="File path to the folder containing Qualys .csv files to process to RegScale.",
|
|
722
722
|
prompt="File path for Qualys files",
|
|
723
723
|
import_name="qualys",
|
|
724
724
|
)
|
|
@@ -740,7 +740,7 @@ def import_scans(
|
|
|
740
740
|
aws_profile: str,
|
|
741
741
|
upload_file: bool,
|
|
742
742
|
):
|
|
743
|
-
"""Import scans from Qualys"""
|
|
743
|
+
"""Import .csv scans from Qualys"""
|
|
744
744
|
import_qualys_scans(
|
|
745
745
|
folder_path=folder_path,
|
|
746
746
|
regscale_ssp_id=regscale_ssp_id,
|
|
@@ -801,7 +801,7 @@ def import_qualys_scans(
|
|
|
801
801
|
|
|
802
802
|
@qualys.command(name="import_policy_scans")
|
|
803
803
|
@FlatFileImporter.common_scanner_options(
|
|
804
|
-
message="File path to the folder containing policy .csv files to process to RegScale.",
|
|
804
|
+
message="File path to the folder containing Qualys policy .csv files to process to RegScale.",
|
|
805
805
|
prompt="File path for Qualys files",
|
|
806
806
|
import_name="qualys_policy_scan",
|
|
807
807
|
)
|
|
@@ -7,7 +7,7 @@ RegScale STIG Integration
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from regscale.integrations.commercial.stigv2.stig_integration import StigIntegration
|
|
10
|
-
from regscale.models.app_models.click import
|
|
10
|
+
from regscale.models.app_models.click import ssp_or_component_id
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@click.group(name="stigv2")
|
|
@@ -16,24 +16,7 @@ def stigv2():
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@stigv2.command(name="sync_findings")
|
|
19
|
-
@
|
|
20
|
-
"-p",
|
|
21
|
-
"--regscale_ssp_id",
|
|
22
|
-
type=click.INT,
|
|
23
|
-
help="The ID number from RegScale of the System Security Plan",
|
|
24
|
-
prompt="Enter RegScale System Security Plan ID",
|
|
25
|
-
cls=NotRequiredIf,
|
|
26
|
-
not_required_if=["component_id"],
|
|
27
|
-
)
|
|
28
|
-
@click.option(
|
|
29
|
-
"-c",
|
|
30
|
-
"--component_id",
|
|
31
|
-
type=click.INT,
|
|
32
|
-
help="The ID number from RegScale of the Component",
|
|
33
|
-
prompt="Enter RegScale Component ID",
|
|
34
|
-
cls=NotRequiredIf,
|
|
35
|
-
not_required_if=["regscale_ssp_id"],
|
|
36
|
-
)
|
|
19
|
+
@ssp_or_component_id()
|
|
37
20
|
@click.option(
|
|
38
21
|
"-d",
|
|
39
22
|
"--stig_directory",
|
|
@@ -46,28 +29,14 @@ def sync_findings(regscale_ssp_id, component_id, stig_directory):
|
|
|
46
29
|
"""Sync GCP Findings to RegScale."""
|
|
47
30
|
if component_id:
|
|
48
31
|
StigIntegration.sync_findings(plan_id=component_id, path=stig_directory, is_component=True)
|
|
49
|
-
|
|
32
|
+
elif regscale_ssp_id:
|
|
50
33
|
StigIntegration.sync_findings(plan_id=regscale_ssp_id, path=stig_directory, is_component=False)
|
|
34
|
+
else:
|
|
35
|
+
raise click.UsageError("Either --regscale_ssp_id or --component_id must be provided.")
|
|
51
36
|
|
|
52
37
|
|
|
53
38
|
@stigv2.command(name="sync_assets")
|
|
54
|
-
@
|
|
55
|
-
"-p",
|
|
56
|
-
"--regscale_ssp_id",
|
|
57
|
-
type=click.INT,
|
|
58
|
-
help="The ID number from RegScale of the System Security Plan to sync assets to.",
|
|
59
|
-
cls=NotRequiredIf,
|
|
60
|
-
not_required_if=["component_id"],
|
|
61
|
-
)
|
|
62
|
-
@click.option(
|
|
63
|
-
"-c",
|
|
64
|
-
"--component_id",
|
|
65
|
-
type=click.INT,
|
|
66
|
-
help="The ID number from RegScale of the Component to sync assets to.",
|
|
67
|
-
cls=NotRequiredIf,
|
|
68
|
-
not_required_if=["regscale_ssp_id"],
|
|
69
|
-
default=None,
|
|
70
|
-
)
|
|
39
|
+
@ssp_or_component_id()
|
|
71
40
|
@click.option(
|
|
72
41
|
"-d",
|
|
73
42
|
"--stig_directory",
|
|
@@ -23,6 +23,16 @@ def sync_armis_centrix(regscale_ssp_id: int) -> None:
|
|
|
23
23
|
assets_armis_centrix.run_sync(regscale_ssp_id=regscale_ssp_id)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
@assets.command(name="sync_axonius")
|
|
27
|
+
@regscale_ssp_id()
|
|
28
|
+
def sync_axonius(regscale_ssp_id: int) -> None:
|
|
29
|
+
"""Sync Assets from Axonius to RegScale."""
|
|
30
|
+
from regscale.models.integration_models.synqly_models.connectors import Assets
|
|
31
|
+
|
|
32
|
+
assets_axonius = Assets("axonius")
|
|
33
|
+
assets_axonius.run_sync(regscale_ssp_id=regscale_ssp_id)
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
@assets.command(name="sync_crowdstrike")
|
|
27
37
|
@regscale_ssp_id()
|
|
28
38
|
@click.option(
|
|
@@ -30,7 +30,7 @@ from regscale.integrations.commercial.tenablev2.jsonl_scanner import TenableSCJs
|
|
|
30
30
|
from regscale.integrations.commercial.tenablev2.sc_scanner import SCIntegration
|
|
31
31
|
from regscale.integrations.commercial.tenablev2.variables import TenableVariables
|
|
32
32
|
from regscale.models import regscale_id, regscale_ssp_id
|
|
33
|
-
from regscale.models.app_models.click import file_types, hidden_file_path, save_output_to
|
|
33
|
+
from regscale.models.app_models.click import file_types, hidden_file_path, save_output_to, ssp_or_component_id
|
|
34
34
|
from regscale.models.regscale_models import SecurityPlan
|
|
35
35
|
|
|
36
36
|
logger = logging.getLogger("regscale")
|
|
@@ -468,7 +468,7 @@ def get_queries() -> list:
|
|
|
468
468
|
prompt="Enter Tenable query ID",
|
|
469
469
|
required=True,
|
|
470
470
|
)
|
|
471
|
-
@
|
|
471
|
+
@ssp_or_component_id()
|
|
472
472
|
@click.option(
|
|
473
473
|
"--scan_date",
|
|
474
474
|
"-sd",
|
|
@@ -476,7 +476,7 @@ def get_queries() -> list:
|
|
|
476
476
|
help="The scan date of the file.",
|
|
477
477
|
required=False,
|
|
478
478
|
)
|
|
479
|
-
def query_vuln(query_id: int, regscale_ssp_id: int, scan_date: datetime = None):
|
|
479
|
+
def query_vuln(query_id: int, regscale_ssp_id: int, component_id: int, scan_date: datetime = None):
|
|
480
480
|
"""Query Tenable SC vulnerabilities and sync assets to RegScale."""
|
|
481
481
|
try:
|
|
482
482
|
# Validate license
|
|
@@ -485,7 +485,15 @@ def query_vuln(query_id: int, regscale_ssp_id: int, scan_date: datetime = None):
|
|
|
485
485
|
console.print("[bold]Starting Tenable SC vulnerability query...[/bold]")
|
|
486
486
|
|
|
487
487
|
# Use the SCIntegration class method to fetch vulnerabilities by query ID
|
|
488
|
-
|
|
488
|
+
if component_id:
|
|
489
|
+
sc_integration = SCIntegration(plan_id=component_id, scan_date=scan_date, is_component=True)
|
|
490
|
+
elif regscale_ssp_id:
|
|
491
|
+
sc_integration = SCIntegration(plan_id=regscale_ssp_id, scan_date=scan_date)
|
|
492
|
+
else:
|
|
493
|
+
raise click.UsageError(
|
|
494
|
+
"You must provide either a --regscale_ssp_id or a --component_id to query Tenable vulnerabilities."
|
|
495
|
+
)
|
|
496
|
+
|
|
489
497
|
sc_integration.fetch_vulns_query(query_id=query_id)
|
|
490
498
|
|
|
491
499
|
console.print("[bold green]Tenable SC vulnerability query complete.[/bold green]")
|
|
@@ -61,9 +61,20 @@ class SCIntegration(ScannerIntegration):
|
|
|
61
61
|
super().__init__(*args, **kwargs)
|
|
62
62
|
self.scan_date = kwargs.get("scan_date")
|
|
63
63
|
self.plan_id = kwargs.get("plan_id")
|
|
64
|
+
self.is_component = kwargs.get("is_component", False) is True
|
|
64
65
|
self.client = None
|
|
65
66
|
self.closed_count = 0
|
|
66
|
-
self.batch_size = kwargs.get("batch_size", 1000)
|
|
67
|
+
self.batch_size = kwargs.get("batch_size", 1000)
|
|
68
|
+
if self.is_component:
|
|
69
|
+
from regscale.validation.record import validate_regscale_object
|
|
70
|
+
|
|
71
|
+
if validate_regscale_object(
|
|
72
|
+
parent_id=self.plan_id, parent_module=regscale_models.Component.get_module_string()
|
|
73
|
+
):
|
|
74
|
+
component = regscale_models.Component.get_object(self.plan_id)
|
|
75
|
+
self.component_title = component.title
|
|
76
|
+
else:
|
|
77
|
+
self.component_title = None
|
|
67
78
|
|
|
68
79
|
def authenticate(self) -> None:
|
|
69
80
|
"""Authenticate to Tenable SC."""
|
|
@@ -255,6 +266,13 @@ class SCIntegration(ScannerIntegration):
|
|
|
255
266
|
status="Active (On Network)" if asset.family.type else "Off-Network",
|
|
256
267
|
asset_type="Other",
|
|
257
268
|
asset_category="Hardware",
|
|
269
|
+
parent_id=self.plan_id,
|
|
270
|
+
parent_module=(
|
|
271
|
+
regscale_models.Component.get_module_string()
|
|
272
|
+
if self.is_component
|
|
273
|
+
else regscale_models.SecurityPlan.get_module_string()
|
|
274
|
+
),
|
|
275
|
+
component_names=[self.component_title],
|
|
258
276
|
)
|
|
259
277
|
|
|
260
278
|
def is_empty(self, file_path: Path) -> bool:
|
|
@@ -337,6 +355,7 @@ class SCIntegration(ScannerIntegration):
|
|
|
337
355
|
plan_id=self.plan_id,
|
|
338
356
|
integration_assets=(asset for sublist in iterables[0] for asset in sublist),
|
|
339
357
|
asset_count=assets_count,
|
|
358
|
+
is_component=self.is_component,
|
|
340
359
|
)
|
|
341
360
|
|
|
342
361
|
# Sync findings
|
|
@@ -344,6 +363,7 @@ class SCIntegration(ScannerIntegration):
|
|
|
344
363
|
plan_id=self.plan_id,
|
|
345
364
|
integration_findings=(finding for sublist in iterables[1] for finding in sublist),
|
|
346
365
|
finding_count=findings_count,
|
|
366
|
+
is_component=self.is_component,
|
|
347
367
|
)
|
|
348
368
|
|
|
349
369
|
logger.info(f"Successfully synced {assets_count} assets and {findings_count} findings")
|
|
@@ -55,6 +55,9 @@ def sync_compliance_data(ssp_id: int, catalog_id: int, framework: str, offline:
|
|
|
55
55
|
failing_controls: Dict = dict()
|
|
56
56
|
for findings in compliance_data:
|
|
57
57
|
asset_check = AssetCheck(**findings)
|
|
58
|
+
if not asset_check.reference:
|
|
59
|
+
logger.warning(f"Asset check {asset_check.check_name} has no references, skipping.")
|
|
60
|
+
continue
|
|
58
61
|
for ref in asset_check.reference:
|
|
59
62
|
if ref.framework not in framework_controls:
|
|
60
63
|
framework_controls[ref.framework] = []
|
|
@@ -19,9 +19,10 @@ def trivy():
|
|
|
19
19
|
|
|
20
20
|
@trivy.command("import_scans")
|
|
21
21
|
@FlatFileImporter.common_scanner_options(
|
|
22
|
-
message="File path to the folder containing
|
|
23
|
-
prompt="File path for
|
|
24
|
-
import_name="
|
|
22
|
+
message="File path to the folder containing Trivy .json files to process to RegScale.",
|
|
23
|
+
prompt="File path for Trivy files",
|
|
24
|
+
import_name="trivy",
|
|
25
|
+
support_component=True,
|
|
25
26
|
)
|
|
26
27
|
@click.option(
|
|
27
28
|
"--destination",
|
|
@@ -41,6 +42,7 @@ def import_scans(
|
|
|
41
42
|
file_pattern: str,
|
|
42
43
|
folder_path: Path,
|
|
43
44
|
regscale_ssp_id: int,
|
|
45
|
+
component_id: int,
|
|
44
46
|
scan_date: datetime,
|
|
45
47
|
mappings_path: Path,
|
|
46
48
|
disable_mapping: bool,
|
|
@@ -56,8 +58,13 @@ def import_scans(
|
|
|
56
58
|
|
|
57
59
|
if s3_bucket and not folder_path:
|
|
58
60
|
folder_path = s3_bucket
|
|
61
|
+
|
|
62
|
+
if not regscale_ssp_id and not component_id:
|
|
63
|
+
raise click.UsageError("You must provide either a --regscale_ssp_id or a --component_id to import Trivy scans.")
|
|
64
|
+
|
|
59
65
|
ti = TrivyIntegration(
|
|
60
|
-
plan_id=regscale_ssp_id,
|
|
66
|
+
plan_id=component_id if component_id else regscale_ssp_id,
|
|
67
|
+
is_component=True if component_id else False,
|
|
61
68
|
file_path=str(folder_path) if folder_path else None,
|
|
62
69
|
s3_bucket=s3_bucket,
|
|
63
70
|
s3_prefix=s3_prefix,
|
|
@@ -53,6 +53,7 @@ class TrivyIntegration(JSONLScannerIntegration):
|
|
|
53
53
|
self.scan_date = kwargs.get("scan_date") if "scan_date" in kwargs else None
|
|
54
54
|
if self.scan_date:
|
|
55
55
|
self.scan_date = self.clean_scan_date(self.scan_date)
|
|
56
|
+
self.is_component = kwargs.get("is_component", False)
|
|
56
57
|
super().__init__(*args, **kwargs)
|
|
57
58
|
|
|
58
59
|
def is_valid_file(self, data: Any, file_path: Union[Path, str]) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
|
@@ -128,7 +129,7 @@ class TrivyIntegration(JSONLScannerIntegration):
|
|
|
128
129
|
notes=f"{os.path.basename(file_path_str)}",
|
|
129
130
|
other_tracking_number=artifact_name,
|
|
130
131
|
parent_id=self.plan_id,
|
|
131
|
-
parent_module="securityplans",
|
|
132
|
+
parent_module="securityplans" if not self.is_component else "components",
|
|
132
133
|
fqdn=artifact_name,
|
|
133
134
|
)
|
|
134
135
|
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
-
from typing import Any, Dict, Iterator, List, Optional
|
|
8
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
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
|
|
@@ -60,7 +60,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
60
60
|
wiz_token = None
|
|
61
61
|
|
|
62
62
|
@staticmethod
|
|
63
|
-
def get_variables():
|
|
63
|
+
def get_variables() -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Returns default variables for first and filterBy for Wiz GraphQL queries.
|
|
66
|
+
|
|
67
|
+
:return: Default variables for Wiz queries
|
|
68
|
+
:rtype: Dict[str, Any]
|
|
69
|
+
"""
|
|
64
70
|
return {
|
|
65
71
|
"first": 100,
|
|
66
72
|
"filterBy": {},
|
|
@@ -115,7 +121,6 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
115
121
|
topic_key=wiz_vulnerability_type["topic_key"],
|
|
116
122
|
file_path=wiz_vulnerability_type["file_path"],
|
|
117
123
|
)
|
|
118
|
-
self.num_findings_to_process += len(nodes)
|
|
119
124
|
yield from self.parse_findings(nodes, wiz_vulnerability_type["type"])
|
|
120
125
|
logger.info("Finished fetching Wiz findings.")
|
|
121
126
|
|
|
@@ -131,8 +136,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
131
136
|
:rtype: Iterator[IntegrationFinding]
|
|
132
137
|
"""
|
|
133
138
|
for node in nodes:
|
|
134
|
-
finding
|
|
135
|
-
|
|
139
|
+
if finding := self.parse_finding(node, vulnerability_type):
|
|
140
|
+
self.num_findings_to_process += 1
|
|
136
141
|
yield finding
|
|
137
142
|
|
|
138
143
|
@classmethod
|
|
@@ -232,17 +237,24 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
232
237
|
self.num_assets_to_process = len(nodes)
|
|
233
238
|
|
|
234
239
|
for node in nodes:
|
|
235
|
-
asset
|
|
236
|
-
if asset:
|
|
240
|
+
if asset := self.parse_asset(node):
|
|
237
241
|
yield asset
|
|
238
242
|
|
|
239
243
|
@staticmethod
|
|
240
|
-
def get_filter_by(filter_by_override, wiz_project_id):
|
|
244
|
+
def get_filter_by(filter_by_override: Union[str, Dict[str, Any]], wiz_project_id: str) -> Dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Constructs the filter_by dictionary for fetching assets
|
|
247
|
+
|
|
248
|
+
:param Union[str, Dict[str, Any]] filter_by_override: Override for the filter_by dictionary
|
|
249
|
+
:param str wiz_project_id: The Wiz project ID
|
|
250
|
+
:return: The filter_by dictionary
|
|
251
|
+
:rtype: Dict[str, Any]
|
|
252
|
+
"""
|
|
241
253
|
if filter_by_override:
|
|
242
254
|
return json.loads(filter_by_override) if isinstance(filter_by_override, str) else filter_by_override
|
|
243
255
|
filter_by = {"project": wiz_project_id}
|
|
244
256
|
if WizVariables.wizLastInventoryPull and not WizVariables.wizFullPullLimitHours:
|
|
245
|
-
filter_by["updatedAt"] = {"after": WizVariables.wizLastInventoryPull}
|
|
257
|
+
filter_by["updatedAt"] = {"after": WizVariables.wizLastInventoryPull} # type: ignore
|
|
246
258
|
return filter_by
|
|
247
259
|
|
|
248
260
|
def parse_asset(self, node: Dict[str, Any]) -> Optional[IntegrationAsset]:
|
|
@@ -278,6 +290,14 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
278
290
|
software_name = self.get_software_name(software_name_dict, wiz_entity_properties, node)
|
|
279
291
|
software_vendor = self.get_software_vendor(software_name_dict, wiz_entity_properties, node)
|
|
280
292
|
|
|
293
|
+
if WizVariables.useWizHardwareAssetTypes and node.get("graphEntity", {}).get("technologies", []):
|
|
294
|
+
technologies = node.get("graphEntity", {}).get("technologies", [])
|
|
295
|
+
deployment_models: set[str] = {
|
|
296
|
+
tech.get("deploymentModel") for tech in technologies if tech.get("deploymentModel")
|
|
297
|
+
}
|
|
298
|
+
else:
|
|
299
|
+
deployment_models = set()
|
|
300
|
+
|
|
281
301
|
return IntegrationAsset(
|
|
282
302
|
name=name,
|
|
283
303
|
external_id=node.get("name"),
|
|
@@ -288,7 +308,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
288
308
|
asset_owner_id=ScannerVariables.userId,
|
|
289
309
|
parent_id=self.plan_id,
|
|
290
310
|
parent_module=regscale_models.SecurityPlan.get_module_slug(),
|
|
291
|
-
asset_category=map_category(node.get("type", "")),
|
|
311
|
+
asset_category=map_category(deployment_models or node.get("type", "")),
|
|
292
312
|
date_last_updated=wiz_entity.get("lastSeen", ""),
|
|
293
313
|
management_type=handle_management_type(wiz_entity_properties),
|
|
294
314
|
status=self.map_wiz_status(wiz_entity_properties.get("status")),
|
|
@@ -326,7 +346,14 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
326
346
|
)
|
|
327
347
|
|
|
328
348
|
@staticmethod
|
|
329
|
-
def get_ports_and_protocols(wiz_entity_properties):
|
|
349
|
+
def get_ports_and_protocols(wiz_entity_properties: dict) -> List[Dict[str, Union[int, str]]]:
|
|
350
|
+
"""
|
|
351
|
+
Extracts ports and protocols from Wiz entity properties using the "portStart","portEnd", and "protocol" keys.
|
|
352
|
+
|
|
353
|
+
:param dict wiz_entity_properties: Dictionary containing Wiz entity properties
|
|
354
|
+
:return: A list of dictionaries containing start_port, end_port, and protocol
|
|
355
|
+
:rtype: List[Dict[str, Union[int, str]]]
|
|
356
|
+
"""
|
|
330
357
|
start_port = wiz_entity_properties.get("portStart")
|
|
331
358
|
if start_port:
|
|
332
359
|
end_port = wiz_entity_properties.get("portEnd") or start_port
|
|
@@ -337,19 +364,45 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
337
364
|
return []
|
|
338
365
|
|
|
339
366
|
@staticmethod
|
|
340
|
-
def get_software_vendor(software_name_dict, wiz_entity_properties, node):
|
|
367
|
+
def get_software_vendor(software_name_dict: dict, wiz_entity_properties: dict, node: dict) -> Optional[str]:
|
|
368
|
+
"""
|
|
369
|
+
Gets the software vendor from the software name dictionary or Wiz entity properties.
|
|
370
|
+
|
|
371
|
+
:param dict software_name_dict: Dictionary containing software name and vendor
|
|
372
|
+
:param dict wiz_entity_properties: Properties of the Wiz entity
|
|
373
|
+
:param dict node: Node dictionary
|
|
374
|
+
:return: Software vendor
|
|
375
|
+
:rtype: Optional[str]
|
|
376
|
+
"""
|
|
341
377
|
if map_category(node.get("type")) == regscale_models.AssetCategory.Software:
|
|
342
378
|
return software_name_dict.get("software_vendor") or wiz_entity_properties.get("cloudPlatform")
|
|
343
379
|
return None
|
|
344
380
|
|
|
345
381
|
@staticmethod
|
|
346
|
-
def get_software_version(wiz_entity_properties, node):
|
|
382
|
+
def get_software_version(wiz_entity_properties: dict, node: dict) -> Optional[str]:
|
|
383
|
+
"""
|
|
384
|
+
Gets the software version from the Wiz entity properties or handles it based on the node type.
|
|
385
|
+
|
|
386
|
+
:param dict wiz_entity_properties: Properties of the Wiz entity
|
|
387
|
+
:param dict node: Node dictionary
|
|
388
|
+
:return: Software version
|
|
389
|
+
:rtype: Optional[str]
|
|
390
|
+
"""
|
|
347
391
|
if map_category(node.get("type")) == regscale_models.AssetCategory.Software:
|
|
348
392
|
return handle_software_version(wiz_entity_properties, map_category(node.get("type"))) or "1.0"
|
|
349
393
|
return None
|
|
350
394
|
|
|
351
395
|
@staticmethod
|
|
352
|
-
def get_software_name(software_name_dict, wiz_entity_properties, node):
|
|
396
|
+
def get_software_name(software_name_dict: dict, wiz_entity_properties: dict, node: dict) -> Optional[str]:
|
|
397
|
+
"""
|
|
398
|
+
Gets the software name from the software name dictionary or Wiz entity properties.
|
|
399
|
+
|
|
400
|
+
:param dict software_name_dict: Dictionary containing software name and vendor
|
|
401
|
+
:param dict wiz_entity_properties: Properties of the Wiz entity
|
|
402
|
+
:param dict node: Node dictionary
|
|
403
|
+
:return: Software name
|
|
404
|
+
:rtype: Optional[str]
|
|
405
|
+
"""
|
|
353
406
|
if map_category(node.get("type")) == regscale_models.AssetCategory.Software:
|
|
354
407
|
return software_name_dict.get("software_name") or wiz_entity_properties.get("nativeType")
|
|
355
408
|
return None
|
|
@@ -10,7 +10,7 @@ import logging
|
|
|
10
10
|
import time
|
|
11
11
|
import traceback
|
|
12
12
|
from contextlib import closing
|
|
13
|
-
from typing import Dict, List, Any, Optional
|
|
13
|
+
from typing import Dict, List, Any, Optional, Union
|
|
14
14
|
from zipfile import ZipFile
|
|
15
15
|
|
|
16
16
|
import cachetools
|
|
@@ -111,22 +111,36 @@ def create_asset_type(asset_type: str) -> str:
|
|
|
111
111
|
return asset_type
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def map_category(asset_string: str) -> regscale_models.AssetCategory:
|
|
114
|
+
def map_category(asset_string: Union[set[str], str]) -> regscale_models.AssetCategory:
|
|
115
115
|
"""
|
|
116
|
-
category
|
|
116
|
+
Map the asset category based on the asset string. If the asset string is not found in the wizHardwareAssetTypes or
|
|
117
|
+
in the AssetCategory enum, it will be mapped to "Software"
|
|
117
118
|
|
|
118
|
-
:param str asset_string:
|
|
119
|
-
|
|
119
|
+
:param Union[set[str], str] asset_string: Set of strings from the Wiz asset's technologies.deploymentModel or
|
|
120
|
+
the node's type
|
|
121
|
+
:return: RegScale AssetCategory
|
|
120
122
|
:rtype: regscale_models.AssetCategory
|
|
121
123
|
"""
|
|
122
124
|
try:
|
|
123
|
-
if asset_string
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
if isinstance(asset_string, set):
|
|
126
|
+
hardware_count = sum(
|
|
127
|
+
asset.lower() == type.lower() for type in WizVariables.wizHardwareAssetTypes for asset in asset_string
|
|
128
|
+
)
|
|
129
|
+
software_count = len(asset_string) - hardware_count
|
|
130
|
+
return (
|
|
131
|
+
regscale_models.AssetCategory.Hardware
|
|
132
|
+
if hardware_count > software_count
|
|
133
|
+
else regscale_models.AssetCategory.Software
|
|
134
|
+
)
|
|
135
|
+
if asset_string in WizVariables.wizHardwareAssetTypes:
|
|
136
|
+
return regscale_models.AssetCategory.Hardware
|
|
137
|
+
elif asset_category := getattr(regscale_models.AssetCategory, asset_string):
|
|
138
|
+
return asset_category
|
|
139
|
+
return regscale_models.AssetCategory.Software
|
|
126
140
|
except (KeyError, AttributeError) as ex:
|
|
127
141
|
# why map AssetCategory of everything is software?
|
|
128
|
-
logger.debug("Unable to find %s in AssetType enum \n", ex)
|
|
129
|
-
return regscale_models.AssetCategory.
|
|
142
|
+
logger.debug("Unable to find %s in AssetType enum. Defaulting to Software\n", ex)
|
|
143
|
+
return regscale_models.AssetCategory.Software
|
|
130
144
|
|
|
131
145
|
|
|
132
146
|
def convert_first_seen_to_days(first_seen: str) -> int:
|
|
@@ -37,3 +37,10 @@ class WizVariables(metaclass=RsVariablesMeta):
|
|
|
37
37
|
wizClientId: RsVariableType(str, "", sensitive=True) # type: ignore
|
|
38
38
|
wizClientSecret: RsVariableType(str, "", sensitive=True) # type: ignore
|
|
39
39
|
wizLastInventoryPull: RsVariableType(str, "2022-01-01T00:00:00Z", required=False) # type: ignore
|
|
40
|
+
useWizHardwareAssetTypes: RsVariableType(bool, False, required=False) # type: ignore
|
|
41
|
+
wizHardwareAssetTypes: RsVariableType(
|
|
42
|
+
list,
|
|
43
|
+
'["SERVER_APPLICATION", "CLIENT_APPLICATION", "VIRTUAL_APPLIANCE"]',
|
|
44
|
+
default=["SERVER_APPLICATION", "CLIENT_APPLICATION", "VIRTUAL_APPLIANCE"],
|
|
45
|
+
required=False,
|
|
46
|
+
) # type: ignore
|
|
@@ -61,6 +61,7 @@ class JSONLScannerIntegration(ScannerIntegration):
|
|
|
61
61
|
|
|
62
62
|
# plan_id is required for all integrations
|
|
63
63
|
super().__init__(**kwargs)
|
|
64
|
+
self.is_component = kwargs.get("is_component", False)
|
|
64
65
|
# Extract S3-related kwargs
|
|
65
66
|
self.s3_bucket = kwargs.get("s3_bucket", None)
|
|
66
67
|
self.s3_prefix = kwargs.get("s3_prefix", "")
|
|
@@ -127,7 +128,11 @@ class JSONLScannerIntegration(ScannerIntegration):
|
|
|
127
128
|
logger.info(f"Creating ScanHistory with scan_date: {scan_date}")
|
|
128
129
|
scan_history = regscale_models.ScanHistory(
|
|
129
130
|
parentId=self.plan_id,
|
|
130
|
-
parentModule=
|
|
131
|
+
parentModule=(
|
|
132
|
+
regscale_models.Component.get_module_string()
|
|
133
|
+
if self.is_component
|
|
134
|
+
else regscale_models.SecurityPlan.get_module_string()
|
|
135
|
+
),
|
|
131
136
|
scanningTool=self.title,
|
|
132
137
|
scanDate=scan_date,
|
|
133
138
|
createdById=self.assessor_id,
|
|
@@ -1255,6 +1260,7 @@ class JSONLScannerIntegration(ScannerIntegration):
|
|
|
1255
1260
|
use_jsonl_file=True,
|
|
1256
1261
|
asset_count=total_assets,
|
|
1257
1262
|
scan_date=self.scan_date,
|
|
1263
|
+
is_component=self.is_component,
|
|
1258
1264
|
)
|
|
1259
1265
|
|
|
1260
1266
|
logger.info("Syncing %d findings to RegScale", total_findings)
|
|
@@ -1264,6 +1270,7 @@ class JSONLScannerIntegration(ScannerIntegration):
|
|
|
1264
1270
|
use_jsonl_file=True,
|
|
1265
1271
|
finding_count=total_findings,
|
|
1266
1272
|
scan_date=self.scan_date,
|
|
1273
|
+
is_component=self.is_component,
|
|
1267
1274
|
)
|
|
1268
1275
|
|
|
1269
1276
|
logger.info("Assets and findings sync complete")
|