regscale-cli 6.20.10.0__py3-none-any.whl → 6.21.1.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 (64) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +12 -5
  3. regscale/core/app/internal/set_permissions.py +58 -27
  4. regscale/integrations/commercial/__init__.py +1 -2
  5. regscale/integrations/commercial/amazon/common.py +79 -2
  6. regscale/integrations/commercial/aws/cli.py +183 -9
  7. regscale/integrations/commercial/aws/scanner.py +544 -9
  8. regscale/integrations/commercial/cpe.py +18 -1
  9. regscale/integrations/commercial/nessus/scanner.py +2 -0
  10. regscale/integrations/commercial/sonarcloud.py +35 -36
  11. regscale/integrations/commercial/synqly/ticketing.py +51 -0
  12. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
  13. regscale/integrations/commercial/wizv2/async_client.py +10 -3
  14. regscale/integrations/commercial/wizv2/click.py +102 -26
  15. regscale/integrations/commercial/wizv2/constants.py +249 -1
  16. regscale/integrations/commercial/wizv2/issue.py +2 -2
  17. regscale/integrations/commercial/wizv2/parsers.py +3 -2
  18. regscale/integrations/commercial/wizv2/policy_compliance.py +1858 -0
  19. regscale/integrations/commercial/wizv2/scanner.py +15 -21
  20. regscale/integrations/commercial/wizv2/utils.py +258 -85
  21. regscale/integrations/commercial/wizv2/variables.py +4 -3
  22. regscale/integrations/compliance_integration.py +1455 -0
  23. regscale/integrations/integration_override.py +15 -6
  24. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  25. regscale/integrations/public/fedramp/markdown_parser.py +7 -1
  26. regscale/integrations/scanner_integration.py +193 -37
  27. regscale/models/app_models/__init__.py +1 -0
  28. regscale/models/integration_models/amazon_models/inspector_scan.py +32 -57
  29. regscale/models/integration_models/aqua.py +92 -78
  30. regscale/models/integration_models/cisa_kev_data.json +117 -5
  31. regscale/models/integration_models/defenderimport.py +64 -59
  32. regscale/models/integration_models/ecr_models/ecr.py +100 -147
  33. regscale/models/integration_models/flat_file_importer/__init__.py +52 -38
  34. regscale/models/integration_models/ibm.py +29 -47
  35. regscale/models/integration_models/nexpose.py +156 -68
  36. regscale/models/integration_models/prisma.py +46 -66
  37. regscale/models/integration_models/qualys.py +99 -93
  38. regscale/models/integration_models/snyk.py +229 -158
  39. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  40. regscale/models/integration_models/veracode.py +15 -20
  41. regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
  42. regscale/models/integration_models/xray.py +276 -82
  43. regscale/models/regscale_models/control_implementation.py +14 -12
  44. regscale/models/regscale_models/file.py +4 -0
  45. regscale/models/regscale_models/issue.py +123 -0
  46. regscale/models/regscale_models/milestone.py +1 -1
  47. regscale/models/regscale_models/rbac.py +22 -0
  48. regscale/models/regscale_models/regscale_model.py +4 -2
  49. regscale/models/regscale_models/security_plan.py +1 -1
  50. regscale/utils/graphql_client.py +3 -1
  51. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/METADATA +9 -9
  52. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/RECORD +64 -60
  53. tests/fixtures/test_fixture.py +58 -2
  54. tests/regscale/core/test_app.py +5 -3
  55. tests/regscale/core/test_version_regscale.py +5 -3
  56. tests/regscale/integrations/test_integration_mapping.py +522 -40
  57. tests/regscale/integrations/test_issue_due_date.py +1 -1
  58. tests/regscale/integrations/test_update_finding_dates.py +336 -0
  59. tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
  60. tests/regscale/models/test_asset.py +406 -50
  61. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/LICENSE +0 -0
  62. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/WHEEL +0 -0
  63. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/entry_points.txt +0 -0
  64. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/top_level.txt +0 -0
@@ -3,13 +3,13 @@ Snyk Scan information
3
3
  """
4
4
 
5
5
  from datetime import datetime
6
- from typing import Optional, Union
6
+ from typing import List, Optional, Union
7
7
 
8
8
  from regscale.core.app.application import Application
9
9
  from regscale.core.app.logz import create_logger
10
- from regscale.core.app.utils.app_utils import epoch_to_datetime, get_current_datetime, is_valid_fqdn
10
+ from regscale.core.app.utils.app_utils import epoch_to_datetime, is_valid_fqdn
11
11
  from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
12
- from regscale.models import Asset, AssetCategory, AssetStatus, AssetType, ImportValidater, IssueStatus, Vulnerability
12
+ from regscale.models import Asset, AssetCategory, AssetStatus, AssetType, ImportValidater, IssueSeverity, IssueStatus
13
13
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
14
14
 
15
15
 
@@ -19,42 +19,60 @@ class Snyk(FlatFileImporter):
19
19
  """
20
20
 
21
21
  def __init__(self, **kwargs):
22
- self.not_implemented_error = "Unsupported file type for Snyk integration. Only XLSX and JSON are supported."
23
- self.name = kwargs.get("name")
24
- self.auto_fixable = "AUTOFIXABLE"
25
- self.fmt = "%Y-%m-%d"
26
- self.dt_format = "%Y-%m-%d %H:%M:%S"
27
- if "json" in kwargs.get("file_type", ""):
28
- self.project_name = "projectName"
29
- self.issue_severity = "severity"
30
- self.vuln_title = "title"
31
- self.required_headers = [
32
- "projectName",
33
- "vulnerabilities",
34
- ]
22
+ # Group related attributes to reduce instance attribute count
23
+ self.scanner_config = {
24
+ "name": kwargs.get("name"),
25
+ "auto_fixable": "AUTOFIXABLE",
26
+ "fmt": "%Y-%m-%d",
27
+ "dt_format": "%Y-%m-%d %H:%M:%S",
28
+ "not_implemented_error": "Unsupported file type for Snyk integration. Only XLSX and JSON are supported.",
29
+ }
30
+ self.mapping_config = {
31
+ "mapping_file": kwargs.get("mappings_path"),
32
+ "disable_mapping": kwargs.get("disable_mapping"),
33
+ }
34
+ self.file_config = {
35
+ "file_type": kwargs.get("file_type", ""),
36
+ }
37
+
38
+ # Set up file-specific configurations
39
+ if "json" in self.file_config["file_type"]:
40
+ self.file_specific_config = {
41
+ "project_name": "projectName",
42
+ "issue_severity": "severity",
43
+ "vuln_title": "title",
44
+ "required_headers": ["projectName", "vulnerabilities"],
45
+ }
35
46
  else:
36
- self.project_name = "PROJECT_NAME"
37
- self.issue_severity = "ISSUE_SEVERITY"
38
- self.vuln_title = "PROBLEM_TITLE"
39
- self.required_headers = [
40
- self.project_name,
41
- self.issue_severity,
42
- self.vuln_title,
43
- self.auto_fixable,
44
- ]
45
- self.mapping_file = kwargs.get("mappings_path")
46
- self.disable_mapping = kwargs.get("disable_mapping")
47
+ self.file_specific_config = {
48
+ "project_name": "PROJECT_NAME",
49
+ "issue_severity": "ISSUE_SEVERITY",
50
+ "vuln_title": "PROBLEM_TITLE",
51
+ "required_headers": [
52
+ "PROJECT_NAME",
53
+ "ISSUE_SEVERITY",
54
+ "PROBLEM_TITLE",
55
+ self.scanner_config["auto_fixable"],
56
+ ],
57
+ }
58
+
47
59
  self.validater = ImportValidater(
48
- self.required_headers, kwargs.get("file_path"), self.mapping_file, self.disable_mapping
60
+ self.file_specific_config["required_headers"],
61
+ kwargs.get("file_path"),
62
+ self.mapping_config["mapping_file"],
63
+ self.mapping_config["disable_mapping"],
49
64
  )
50
65
  self.headers = self.validater.parsed_headers
51
66
  self.mapping = self.validater.mapping
52
- if "json" in kwargs.get("file_type", ""):
67
+
68
+ # Set counts based on file type
69
+ if "json" in self.file_config["file_type"]:
53
70
  asset_count = 1
54
71
  vuln_count = len(self.mapping.get_value(self.validater.data, "vulnerabilities", []))
55
72
  else:
56
73
  asset_count = None
57
74
  vuln_count = None
75
+
58
76
  logger = create_logger()
59
77
  self.logger = logger
60
78
  super().__init__(
@@ -77,10 +95,12 @@ class Snyk(FlatFileImporter):
77
95
  :return: The first seen date as a string
78
96
  :rtype: str
79
97
  """
98
+ epoch_time = epoch_to_datetime(self.create_epoch, self.scanner_config["fmt"])
99
+ datetime_obj = datetime.strptime(epoch_time, self.scanner_config["dt_format"])
80
100
  return datetime.combine(
81
- datetime.strptime(epoch_to_datetime(self.create_epoch, self.fmt), self.dt_format),
101
+ datetime_obj,
82
102
  self.mapping.get_value(dat, "FIRST_INTRODUCED", datetime.now().time()),
83
- ).strftime(self.dt_format)
103
+ ).strftime(self.scanner_config["dt_format"])
84
104
 
85
105
  def create_asset(self, dat: Optional[dict] = None) -> Union[Asset, IntegrationAsset]:
86
106
  """
@@ -92,41 +112,41 @@ class Snyk(FlatFileImporter):
92
112
  """
93
113
  if "json" in self.attributes.file_type:
94
114
  return self._parse_json_asset(data=dat)
95
- elif "xlsx" in self.attributes.file_type:
115
+ if "xlsx" in self.attributes.file_type:
96
116
  return self._parse_xlsx_asset(dat)
97
- else:
98
- raise NotImplementedError(self.not_implemented_error)
117
+ raise NotImplementedError(self.scanner_config["not_implemented_error"])
99
118
 
100
- def _parse_xlsx_asset(self, dat: Optional[dict] = None) -> Asset:
119
+ def _create_asset(self, project_name: str) -> IntegrationAsset:
101
120
  """
102
- Create an asset from a row in the Snyk file
121
+ Helper function to create an IntegrationAsset with common attributes
103
122
 
104
- :param Optional[dict] dat: Data row from CSV file, defaults to None
105
- :return: RegScale Asset object
106
- :rtype: Asset
123
+ :param str project_name: The project name to extract hostname from
124
+ :return: IntegrationAsset object
125
+ :rtype: IntegrationAsset
107
126
  """
108
- name = self.extract_host(self.mapping.get_value(dat, self.project_name))
127
+ name = self.extract_host(project_name)
109
128
  valid_name = is_valid_fqdn(name)
110
- return Asset(
111
- **{
112
- "id": 0,
113
- "name": name,
114
- "ipAddress": "0.0.0.0",
115
- "isPublic": True,
116
- "status": "Active (On Network)",
117
- "assetCategory": "Software",
118
- "bLatestScan": True,
119
- "bAuthenticatedScan": True,
120
- "scanningTool": self.name,
121
- "assetOwnerId": self.config["userId"],
122
- "assetType": "Other",
123
- "fqdn": name if valid_name else None,
124
- "systemAdministratorId": self.config["userId"],
125
- "parentId": self.attributes.parent_id,
126
- "parentModule": self.attributes.parent_module,
127
- }
129
+ return IntegrationAsset(
130
+ scanning_tool=self.scanner_config["name"],
131
+ identifier=name,
132
+ name=name,
133
+ status=AssetStatus.Active,
134
+ asset_category=AssetCategory.Software,
135
+ asset_type=AssetType.Other,
136
+ fqdn=name if valid_name else None,
128
137
  )
129
138
 
139
+ def _parse_xlsx_asset(self, dat: Optional[dict] = None) -> IntegrationAsset:
140
+ """
141
+ Create an asset from a row in the Snyk XLSX file
142
+
143
+ :param Optional[dict] dat: Data row from XLSX file, defaults to None
144
+ :return: IntegrationAsset object
145
+ :rtype: IntegrationAsset
146
+ """
147
+ project_name = self.mapping.get_value(dat, self.file_specific_config["project_name"])
148
+ return self._create_asset(project_name)
149
+
130
150
  def _parse_json_asset(self, **kwargs) -> IntegrationAsset:
131
151
  """
132
152
  Parse assets from Snyk json scan data.
@@ -135,134 +155,185 @@ class Snyk(FlatFileImporter):
135
155
  :rtype: IntegrationAsset
136
156
  """
137
157
  data = kwargs.pop("data")
138
- name = self.extract_host(self.mapping.get_value(data, self.project_name))
139
- valid_name = is_valid_fqdn(name)
140
- return IntegrationAsset(
141
- identifier=name,
142
- name=name,
143
- status=AssetStatus.Active,
144
- asset_category=AssetCategory.Software,
145
- is_latest_scan=True,
146
- is_authenticated_scan=True,
147
- scanning_tool=self.name,
148
- asset_type=AssetType.Other,
149
- fqdn=name if valid_name else None,
150
- system_administrator_id=self.config["userId"],
151
- parent_id=self.attributes.parent_id,
152
- parent_module=self.attributes.parent_module,
153
- )
158
+ project_name = self.mapping.get_value(data, self.file_specific_config["project_name"])
159
+ return self._create_asset(project_name)
154
160
 
155
161
  def create_vuln(
156
162
  self, dat: Optional[dict] = None, **kwargs
157
- ) -> Optional[Union[list[IntegrationFinding], Vulnerability]]:
163
+ ) -> Optional[Union[List[IntegrationFinding], IntegrationFinding]]:
158
164
  """
159
165
  Create a vulnerability from a row in the Snyk file
160
166
 
161
167
  :param Optional[dict] dat: Data row from XLSX or JSON file, defaults to None
162
168
  :raises TypeError: If dat is not a dictionary
163
169
  :return: RegScale Vulnerability object if xlsx or list of IntegrationFindings if JSON
164
- :rtype: Optional[Union[list[IntegrationFinding], Vulnerability]]
170
+ :rtype: Optional[Union[List[IntegrationFinding], Vulnerability]]
165
171
  """
166
172
  if "json" in self.attributes.file_type:
167
173
  return self._parse_json_findings(**kwargs)
168
- elif "xlsx" in self.attributes.file_type:
169
- return self._parse_xlsx_finding(dat, **kwargs)
174
+ if "xlsx" in self.attributes.file_type:
175
+ if isinstance(dat, dict):
176
+ return self._parse_xlsx_finding(dat, **kwargs)
177
+ if isinstance(dat, list):
178
+ findings = []
179
+ for finding in dat:
180
+ findings.extend(self._parse_xlsx_finding(finding, **kwargs))
181
+ return findings
182
+ raise NotImplementedError(self.scanner_config["not_implemented_error"])
183
+
184
+ def _create_finding(self, finding_data: dict) -> IntegrationFinding:
185
+ """
186
+ Helper function to create an IntegrationFinding with common attributes
187
+
188
+ :param dict finding_data: Dictionary containing finding data
189
+ :return: IntegrationFinding object
190
+ :rtype: IntegrationFinding
191
+ """
192
+ if finding_data.get("title") is None:
193
+ finding_data["title"] = f"{finding_data['description']} on asset {finding_data['hostname']}"
194
+
195
+ return IntegrationFinding(
196
+ title=finding_data["title"],
197
+ description=finding_data["description"],
198
+ severity=finding_data["severity"],
199
+ status=IssueStatus.Open.value,
200
+ plugin_name=finding_data["description"],
201
+ plugin_id=finding_data.get("plugin_id"),
202
+ recommendation_for_mitigation=finding_data["solution"],
203
+ plugin_text=self.mapping.get_value(finding_data["dat"], self.file_specific_config["vuln_title"]),
204
+ asset_identifier=finding_data["hostname"],
205
+ cve=finding_data["cve"],
206
+ cvss_score=finding_data.get("cvss_score"),
207
+ first_seen=self.determine_first_seen(finding_data["dat"]),
208
+ last_seen=self.scan_date,
209
+ scan_date=self.scan_date,
210
+ dns=finding_data["hostname"],
211
+ vpr_score=finding_data.get("vpr_score"),
212
+ remediation=finding_data["solution"],
213
+ category="Software",
214
+ control_labels=[],
215
+ )
216
+
217
+ def _parse_xlsx_finding(self, dat: Optional[dict] = None, **_) -> List[IntegrationFinding]:
218
+ """
219
+ Create a list of IntegrationFinding objects from a row in the Snyk xlsx file
220
+
221
+ :param Optional[dict] dat: Data row from XLSX file, defaults to None
222
+ :return: A list of IntegrationFinding objects
223
+ :rtype: List[IntegrationFinding]
224
+ """
225
+ if not dat:
226
+ return []
227
+
228
+ findings: List[IntegrationFinding] = []
229
+ severity = self.determine_severity(
230
+ self.mapping.get_value(dat, self.file_specific_config["issue_severity"]).lower()
231
+ )
232
+ hostname = self.extract_host(self.mapping.get_value(dat, self.file_specific_config["project_name"]))
233
+ description = self.mapping.get_value(dat, self.file_specific_config["vuln_title"])
234
+ solution = self.mapping.get_value(dat, self.scanner_config["auto_fixable"])
235
+ cves = self.mapping.get_value(dat, "CVE", [])
236
+
237
+ if cves:
238
+ for cve in cves:
239
+ finding_data = {
240
+ "dat": dat,
241
+ "hostname": hostname,
242
+ "description": description,
243
+ "severity": severity,
244
+ "solution": solution,
245
+ "cve": cve,
246
+ }
247
+ findings.append(self._create_finding(finding_data))
170
248
  else:
171
- raise NotImplementedError(self.not_implemented_error)
249
+ finding_data = {
250
+ "dat": dat,
251
+ "hostname": hostname,
252
+ "description": description,
253
+ "severity": severity,
254
+ "solution": solution,
255
+ "cve": "",
256
+ }
257
+ findings.append(self._create_finding(finding_data))
258
+ return findings
172
259
 
173
- def _parse_xlsx_finding(self, dat: Optional[dict] = None, **_) -> Optional[Vulnerability]:
260
+ def _extract_json_finding_data(self, dat: dict) -> dict:
174
261
  """
175
- Create a vulnerability from a row in the Snyk csv file
262
+ Extract common finding data from JSON vulnerability data
176
263
 
177
- :param Optional[dict] dat: Data row from CSV file, defaults to None
178
- :return: RegScale Vulnerability object or None
179
- :rtype: Optional[Vulnerability]
264
+ :param dict dat: The vulnerability data
265
+ :return: Dictionary containing extracted finding data
266
+ :rtype: dict
180
267
  """
181
- regscale_vuln = None
182
- severity = self.mapping.get_value(dat, self.issue_severity).lower()
183
- hostname = self.extract_host(self.mapping.get_value(dat, self.project_name))
184
- description = self.mapping.get_value(dat, self.vuln_title)
185
- solution = self.mapping.get_value(dat, self.auto_fixable)
186
- config = self.attributes.app.config
187
- asset_match = [asset for asset in self.data["assets"] if asset.name == hostname]
188
- asset = asset_match[0] if asset_match else None
189
- if dat and asset_match:
190
- regscale_vuln = Vulnerability(
191
- id=0,
192
- scanId=0, # set later
193
- parentId=asset.id,
194
- parentModule="assets",
195
- ipAddress="0.0.0.0", # No ip address available
196
- lastSeen=get_current_datetime(),
197
- firstSeen=self.determine_first_seen(dat),
198
- daysOpen=None,
199
- dns=hostname,
200
- mitigated=None,
201
- operatingSystem=None,
202
- severity=severity,
203
- plugInName=description,
204
- cve=", ".join(self.mapping.get_value(dat, "CVE", "")),
205
- vprScore=None,
206
- tenantsId=0,
207
- title=f"{description} on asset {asset.name}",
208
- description=description,
209
- plugInText=self.mapping.get_value(dat, self.vuln_title),
210
- createdById=config["userId"],
211
- lastUpdatedById=config["userId"],
212
- dateCreated=get_current_datetime(),
213
- extra_data={"solution": solution},
214
- )
215
- return regscale_vuln
216
-
217
- def _parse_json_findings(self, **kwargs) -> list[IntegrationFinding]:
268
+ severity_key = self.file_specific_config["issue_severity"]
269
+ severity = self.determine_snyk_severity(dat.get(severity_key, "Low").lower())
270
+ project_name = self.file_specific_config["project_name"]
271
+ hostname = self.extract_host(self.mapping.get_value(dat, project_name)) or self.extract_host(
272
+ self.mapping.get_value(self.validater.data, project_name)
273
+ )
274
+ vuln_title = self.file_specific_config["vuln_title"]
275
+ description = self.mapping.get_value(dat, "description") or self.mapping.get_value(dat, vuln_title)
276
+ solution = self.mapping.get_value(dat, self.scanner_config["auto_fixable"])
277
+
278
+ # if auto fixable is not available, check for upgradeable or patchable, this is for .json files
279
+ if not solution:
280
+ upgradeable = self.mapping.get_value(dat, "isUpgradeable", False)
281
+ patchable = self.mapping.get_value(dat, "isPatchable", False)
282
+ if upgradeable or patchable:
283
+ solution = "Upgrade or patch the vulnerable component."
284
+
285
+ return {
286
+ "severity": severity,
287
+ "hostname": hostname,
288
+ "description": description,
289
+ "solution": solution,
290
+ }
291
+
292
+ def _parse_json_findings(self, **kwargs) -> List[IntegrationFinding]:
218
293
  """
219
- Create a vulnerability from a row in the Snyk csv file
294
+ Create a list of IntegrationFinding objects from the Snyk json file
220
295
 
221
296
  :return: List of IntegrationFinding objects
222
- :rtype: list[IntegrationFinding]
297
+ :rtype: List[IntegrationFinding]
223
298
  """
224
299
  findings = []
225
300
  vulns = self.mapping.get_value(kwargs.get("data", self.validater.data), "vulnerabilities", [])
226
301
  if not vulns:
227
302
  return findings
303
+
228
304
  for dat in vulns:
229
- severity = self.finding_severity_map.get(dat.get(self.issue_severity, "Low").title())
230
- hostname = self.extract_host(self.mapping.get_value(dat, self.project_name)) or self.extract_host(
231
- self.mapping.get_value(self.validater.data, self.project_name)
232
- )
233
- description = self.mapping.get_value(dat, "description") or self.mapping.get_value(dat, self.vuln_title)
234
- solution = self.mapping.get_value(dat, self.auto_fixable)
235
- # if auto fixable is not available, check for upgradeable or patchable, this is for .json files
236
- if not solution:
237
- upgradeable = self.mapping.get_value(dat, "isUpgradeable", False)
238
- patchable = self.mapping.get_value(dat, "isPatchable", False)
239
- if upgradeable or patchable:
240
- solution = "Upgrade or patch the vulnerable component."
241
- cves = ", ".join(self.mapping.get_value(dat, "CVE", ""))
305
+ finding_data = self._extract_json_finding_data(dat)
306
+ cves = self.mapping.get_value(dat, "CVE", [])
242
307
  if not cves:
243
- cves = ", ".join(dat.get("identifiers", {}).get("CVE", []))
244
- findings.append(
245
- IntegrationFinding(
246
- title=dat.get("title") or description,
247
- description=description,
248
- severity=severity,
249
- status=IssueStatus.Open,
250
- plugin_name=description,
251
- plugin_id=dat.get("id"),
252
- plugin_text=self.mapping.get_value(dat, self.vuln_title),
253
- asset_identifier=hostname,
254
- cve=cves,
255
- cvss_score=dat.get("cvssScore"),
256
- first_seen=self.determine_first_seen(dat),
257
- last_seen=get_current_datetime(),
258
- scan_date=self.attributes.scan_date,
259
- dns=hostname,
260
- vpr_score=None,
261
- remediation=solution,
262
- category="Software",
263
- control_labels=[],
308
+ cves = dat.get("identifiers", {}).get("CVE", [])
309
+
310
+ # Handle multiple CVEs or single CVE
311
+ if cves:
312
+ for cve in cves:
313
+ finding_data.update(
314
+ {
315
+ "dat": dat,
316
+ "cve": cve,
317
+ "title": dat.get("title") or finding_data["description"],
318
+ "plugin_id": dat.get("id"),
319
+ "cvss_score": dat.get("cvssScore"),
320
+ "vpr_score": None,
321
+ }
322
+ )
323
+ findings.append(self._create_finding(finding_data))
324
+ else:
325
+ finding_data.update(
326
+ {
327
+ "dat": dat,
328
+ "cve": "",
329
+ "title": dat.get("title") or finding_data["description"],
330
+ "plugin_id": dat.get("id"),
331
+ "cvss_score": dat.get("cvssScore"),
332
+ "vpr_score": None,
333
+ }
264
334
  )
265
- )
335
+ findings.append(self._create_finding(finding_data))
336
+
266
337
  return findings
267
338
 
268
339
  @staticmethod