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.

Files changed (55) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/utils/variables.py +5 -3
  3. regscale/integrations/commercial/__init__.py +15 -0
  4. regscale/integrations/commercial/axonius/__init__.py +0 -0
  5. regscale/integrations/commercial/axonius/axonius_integration.py +70 -0
  6. regscale/integrations/commercial/burp.py +14 -0
  7. regscale/integrations/commercial/grype/commands.py +8 -1
  8. regscale/integrations/commercial/grype/scanner.py +2 -1
  9. regscale/integrations/commercial/jira.py +288 -137
  10. regscale/integrations/commercial/opentext/commands.py +14 -5
  11. regscale/integrations/commercial/opentext/scanner.py +3 -2
  12. regscale/integrations/commercial/qualys/__init__.py +3 -3
  13. regscale/integrations/commercial/stigv2/click_commands.py +6 -37
  14. regscale/integrations/commercial/synqly/assets.py +10 -0
  15. regscale/integrations/commercial/tenablev2/commands.py +12 -4
  16. regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
  17. regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
  18. regscale/integrations/commercial/trivy/commands.py +11 -4
  19. regscale/integrations/commercial/trivy/scanner.py +2 -1
  20. regscale/integrations/commercial/wizv2/constants.py +4 -0
  21. regscale/integrations/commercial/wizv2/scanner.py +67 -14
  22. regscale/integrations/commercial/wizv2/utils.py +24 -10
  23. regscale/integrations/commercial/wizv2/variables.py +7 -0
  24. regscale/integrations/jsonl_scanner_integration.py +8 -1
  25. regscale/integrations/public/cisa.py +58 -63
  26. regscale/integrations/public/fedramp/fedramp_cis_crm.py +153 -104
  27. regscale/integrations/scanner_integration.py +30 -8
  28. regscale/integrations/variables.py +1 -0
  29. regscale/models/app_models/click.py +49 -1
  30. regscale/models/app_models/import_validater.py +3 -1
  31. regscale/models/integration_models/axonius_models/__init__.py +0 -0
  32. regscale/models/integration_models/axonius_models/connectors/__init__.py +3 -0
  33. regscale/models/integration_models/axonius_models/connectors/assets.py +111 -0
  34. regscale/models/integration_models/burp.py +11 -8
  35. regscale/models/integration_models/cisa_kev_data.json +204 -23
  36. regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
  37. regscale/models/integration_models/jira_task_sync.py +27 -0
  38. regscale/models/integration_models/qualys.py +6 -7
  39. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  40. regscale/models/regscale_models/__init__.py +2 -1
  41. regscale/models/regscale_models/control_implementation.py +39 -2
  42. regscale/models/regscale_models/issue.py +1 -0
  43. regscale/models/regscale_models/regscale_model.py +49 -1
  44. regscale/models/regscale_models/risk_issue_mapping.py +61 -0
  45. regscale/models/regscale_models/task.py +1 -0
  46. regscale/regscale.py +1 -4
  47. regscale/utils/graphql_client.py +4 -4
  48. regscale/utils/string.py +13 -0
  49. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/METADATA +1 -1
  50. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/RECORD +54 -48
  51. regscale/integrations/commercial/synqly_jira.py +0 -840
  52. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/LICENSE +0 -0
  53. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/WHEEL +0 -0
  54. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/entry_points.txt +0 -0
  55. {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 JFrog XRay .json files to process to RegScale.",
31
- prompt="File path for Grype files",
32
- import_name="grype",
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., '*.json')",
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 Aqua .csv files to process to RegScale.",
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 NotRequiredIf
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
- @click.option(
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
- else:
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
- @click.option(
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
- @regscale_ssp_id()
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
- sc_integration = SCIntegration(plan_id=regscale_ssp_id, scan_date=scan_date)
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) # Default to 1000 if not provided
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 JFrog XRay .json files to process to RegScale.",
23
- prompt="File path for Grype files",
24
- import_name="grype",
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
 
@@ -136,6 +136,10 @@ INVENTORY_QUERY = """
136
136
  projects {
137
137
  id
138
138
  }
139
+ technologies {
140
+ name
141
+ deploymentModel
142
+ }
139
143
  properties
140
144
  firstSeen
141
145
  lastSeen
@@ -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 = self.parse_finding(node, vulnerability_type)
135
- if finding:
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 = self.parse_asset(node)
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 mapper
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
- :return: Category
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 in ["CONTAINER_IMAGE"]:
124
- return regscale_models.AssetCategory.Software
125
- return getattr(regscale_models.AssetCategory, asset_string)
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.Hardware
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=regscale_models.SecurityPlan.get_module_string(),
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")