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
@@ -2,8 +2,8 @@ from typing import List, Optional, Union
2
2
 
3
3
  from regscale.core.app.logz import create_logger
4
4
  from regscale.core.app.utils.app_utils import get_current_datetime
5
- from regscale.integrations.scanner_integration import IntegrationFinding
6
- from regscale.models import Asset, Vulnerability, ImportValidater, IssueStatus
5
+ from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
6
+ from regscale.models import Asset, ImportValidater, IssueStatus, Vulnerability
7
7
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
8
8
 
9
9
  APP_NAME = "@app_name"
@@ -77,31 +77,26 @@ class Veracode(FlatFileImporter):
77
77
  else:
78
78
  name = self.mapping.get_value(dat, "Source", "")
79
79
  account_id = str(self.mapping.get_value(dat, "ID", ""))
80
- asset = Asset(
80
+ asset = IntegrationAsset(
81
81
  **{
82
- "id": 0,
83
82
  "name": name,
84
- "otherTrackingNumber": account_id,
85
- "ipAddress": "0.0.0.0",
86
- "isPublic": True,
83
+ "ip_address": "0.0.0.0",
84
+ "identifier": name,
85
+ "other_tracking_number": account_id,
87
86
  "status": "Active (On Network)",
88
- "assetCategory": "Software",
89
- "bLatestScan": True,
90
- "bAuthenticatedScan": True,
91
- "scanningTool": self.name,
92
- "assetOwnerId": self.config["userId"],
93
- "assetType": "Other",
94
- "softwareVendor": "Veracode",
95
- "softwareName": name,
96
- "softwareVersion": version,
97
- "systemAdministratorId": self.config["userId"],
98
- "parentId": self.attributes.parent_id,
99
- "parentModule": self.attributes.parent_module,
87
+ "asset_category": "Hardware",
88
+ "software_vendor": "Veracode",
89
+ "software_name": name,
90
+ "software_version": version,
91
+ "asset_type": "Other",
92
+ "scanning_tool": self.name,
100
93
  }
101
94
  )
102
95
  return [asset]
103
96
 
104
- def create_vuln(self, dat: Optional[dict] = None, **kwargs) -> Union[List[Vulnerability], List[IntegrationFinding]]:
97
+ def create_vuln(
98
+ self, dat: Optional[dict] = None, **kwargs
99
+ ) -> Union[List[IntegrationFinding], List[IntegrationFinding]]:
105
100
  """
106
101
  Create a RegScale vulnerability from a vulnerability in the Veracode export file
107
102
 
@@ -1,19 +1,18 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """Class for a Wiz.io integration"""
3
+ """Wiz v2 Integration Models (RegScale pattern)."""
4
4
 
5
- # standard python imports
6
5
  from enum import Enum
7
6
  from typing import Optional
7
+ from datetime import datetime
8
8
 
9
9
  from pydantic import BaseModel, Field
10
- from datetime import datetime
11
10
 
12
11
  from regscale.models import regscale_models
13
12
 
14
13
 
15
14
  class AssetCategory(Enum):
16
- """Map Wiz assetTypes with RegScale assetCategories"""
15
+ """Map Wiz assetTypes with RegScale assetCategories."""
17
16
 
18
17
  SERVICE_USAGE_TECHNOLOGY = regscale_models.AssetCategory.Hardware
19
18
  GATEWAY = regscale_models.AssetCategory.Hardware
@@ -90,6 +89,7 @@ class ComplianceReport(BaseModel):
90
89
  control_id: Optional[str] = Field(None, alias="Control ID")
91
90
  compliance_check: Optional[str] = Field(None, alias="Compliance Check Name (Wiz Subcategory)")
92
91
  control_description: Optional[str] = Field(None, alias="Control Description")
92
+ issue_finding_id: Optional[str] = Field(None, alias="Issue/Finding ID")
93
93
  severity: Optional[str] = Field(None, alias="Severity")
94
94
  result: str = Field(..., alias="Result")
95
95
  framework: Optional[str] = Field(None, alias="Framework")
@@ -102,11 +102,3 @@ class ComplianceReport(BaseModel):
102
102
  resource_id: str = Field(..., alias="Resource ID")
103
103
  resource_region: Optional[str] = Field(None, alias="Resource Region")
104
104
  resource_cloud_platform: Optional[str] = Field(None, alias="Resource Cloud Platform")
105
-
106
-
107
- # # Attempt to create an instance of the model again
108
- # example_row = data.iloc[0].to_dict()
109
- # example_compliance_report = ComplianceReport(**example_row)
110
- #
111
- # # Display the instance
112
- # example_compliance_report.dict()
@@ -1,16 +1,21 @@
1
1
  """
2
- Nexpose Scan information
2
+ JFrog Xray Scan information
3
3
  """
4
4
 
5
- from typing import List, Optional
5
+ import logging
6
+ import traceback
7
+ from typing import Callable, Iterator, Optional
6
8
 
7
9
  from regscale.core.app.application import Application
8
10
  from regscale.core.app.logz import create_logger
9
- from regscale.core.app.utils.app_utils import epoch_to_datetime, get_current_datetime
10
- from regscale.models import ImportValidater, Mapping
11
+ from regscale.core.app.utils.app_utils import epoch_to_datetime, get_current_datetime, is_valid_fqdn
12
+ from regscale.exceptions import ValidationException
13
+ from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
14
+ from regscale.models import ImportValidater
11
15
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
12
- from regscale.models.regscale_models.asset import Asset
13
- from regscale.models.regscale_models.vulnerability import Vulnerability
16
+ from regscale.models.regscale_models import IssueStatus, Vulnerability
17
+
18
+ logger = logging.getLogger(__name__)
14
19
 
15
20
 
16
21
  class XRay(FlatFileImporter):
@@ -25,31 +30,46 @@ class XRay(FlatFileImporter):
25
30
  def __init__(self, **kwargs):
26
31
  self.name = kwargs.get("name")
27
32
  regscale_ssp_id = kwargs.get("regscale_ssp_id")
28
- self.cvss3_score = "cvss_v3_score"
29
- self.vuln_title = "cve"
30
- self.required_headers = [
31
- "impacted_artifact",
32
- ]
33
- self.mapping_file = kwargs.get("mappings_path")
34
- self.disable_mapping = kwargs.get("disable_mapping")
33
+ # Combine related attributes to reduce instance attribute count
34
+ self.scanner_config = {
35
+ "cvss3_score": "cvss_v3_score",
36
+ "vuln_title": "cve",
37
+ "required_headers": ["impacted_artifact"],
38
+ }
39
+ # Combine mapping-related attributes
40
+ self.mapping_config = {
41
+ "mapping_file": kwargs.get("mappings_path"),
42
+ "disable_mapping": kwargs.get("disable_mapping"),
43
+ }
35
44
  self.validater = ImportValidater(
36
- self.required_headers, kwargs.get("file_path"), self.mapping_file, self.disable_mapping
45
+ self.scanner_config["required_headers"],
46
+ kwargs.get("file_path"),
47
+ self.mapping_config["mapping_file"],
48
+ self.mapping_config["disable_mapping"],
37
49
  )
38
50
  self.headers = self.validater.parsed_headers
39
51
  self.mapping = self.validater.mapping
40
- logger = create_logger()
52
+ xray_logger = create_logger()
53
+ # set vuln count and asset count in constructor
54
+ vuln_count = 0
55
+ asset_count = 0
56
+ for dat in self.validater.data:
57
+ vuln_count += len(self.mapping.get_value(dat, "cves", []))
58
+ asset_count += 1
41
59
  super().__init__(
42
- logger=logger,
60
+ logger=xray_logger,
43
61
  app=Application(),
44
62
  headers=None,
45
63
  parent_id=regscale_ssp_id,
46
64
  parent_module="securityplans",
47
65
  asset_func=self.create_asset,
48
66
  vuln_func=self.create_vuln,
67
+ vuln_count=vuln_count,
68
+ asset_count=asset_count,
49
69
  **kwargs,
50
70
  )
51
71
 
52
- def create_asset(self, dat: Optional[dict] = None) -> Optional[Asset]:
72
+ def create_asset(self, dat: Optional[dict] = None) -> Optional[IntegrationAsset]:
53
73
  """
54
74
  Create an asset from a row in the Xray JSON file
55
75
 
@@ -59,77 +79,251 @@ class XRay(FlatFileImporter):
59
79
  """
60
80
 
61
81
  if asset_name := self.mapping.get_value(dat, "impacted_artifact") if isinstance(dat, dict) else dat:
62
- return Asset(
82
+ return IntegrationAsset(
63
83
  **{
64
- "id": 0,
65
84
  "name": asset_name,
66
- "ipAddress": "0.0.0.0",
67
- "isPublic": True,
85
+ "ip_address": "0.0.0.0",
86
+ "identifier": asset_name,
87
+ "other_tracking_number": asset_name,
68
88
  "status": "Active (On Network)",
69
- "assetCategory": "Software",
70
- "bLatestScan": True,
71
- "bAuthenticatedScan": True,
72
- "scanningTool": self.name,
73
- "assetOwnerId": self.config["userId"],
74
- "assetType": "Other",
75
- "fqdn": None,
76
- "operatingSystem": "Linux",
77
- "systemAdministratorId": self.config["userId"],
78
- "parentId": self.attributes.parent_id,
79
- "parentModule": self.attributes.parent_module,
89
+ "asset_category": "Hardware",
90
+ "asset_type": "Other",
91
+ "scanning_tool": self.name,
92
+ "fqdn": asset_name if is_valid_fqdn(asset_name) else None,
93
+ "operating_system": "Linux",
80
94
  }
81
95
  )
82
96
  return None
83
97
 
84
- def create_vuln(self, dat: Optional[dict] = None, **kwargs) -> List[Vulnerability]:
98
+ def create_asset_from_name(self, asset_name: str) -> IntegrationAsset:
99
+ """Create an IntegrationAsset from an asset name
100
+
101
+ :param str asset_name: The name of the asset
102
+ :return: IntegrationAsset object
103
+ :rtype: IntegrationAsset
85
104
  """
86
- Create a vulnerability from a row in the JFrog Xray JSON file
105
+ return IntegrationAsset(
106
+ **{
107
+ "name": asset_name,
108
+ "ip_address": "0.0.0.0",
109
+ "identifier": asset_name,
110
+ "other_tracking_number": asset_name,
111
+ "status": "Active (On Network)",
112
+ "asset_category": "Hardware",
113
+ "asset_type": "Other",
114
+ "scanning_tool": self.name,
115
+ "fqdn": asset_name if is_valid_fqdn(asset_name) else None,
116
+ "operating_system": "Linux",
117
+ }
118
+ )
87
119
 
88
- :param Optional[dict] dat: Data row from JSON file, defaults to None
89
- :return: List of RegScale Vulnerability object, if any
90
- :rtype: List[Vulnerability]
91
- """
92
- asset_match = [
93
- asset for asset in self.data["assets"] if asset.name == self.mapping.get_value(dat, "impacted_artifact")
94
- ]
95
- asset = asset_match[0] if asset_match else None
96
- vulns = []
97
- for vuln in self.mapping.get_value(dat, "cves", []):
98
- # CVE IS A VULN, A VULN IS A CVE, Finkle is Einhorn
99
- regscale_vuln = None
100
- severity = (
101
- Vulnerability.determine_cvss3_severity_text(float(vuln[self.cvss3_score]))
102
- if vuln.get(self.cvss3_score)
103
- else "low"
104
- )
105
- if asset_match:
106
- cves = [c["cve"] for c in self.mapping.get_value(dat, "cves", []) if c.get("cve")]
107
- for cve in cves:
108
- regscale_vuln = Vulnerability(
109
- id=0,
110
- scanId=0, # set later
111
- parentId=asset.id,
112
- parentModule="assets",
113
- ipAddress="0.0.0.0", # No ip address available
114
- lastSeen=self.scan_date,
115
- firstSeen=epoch_to_datetime(self.create_epoch),
116
- daysOpen=None,
117
- dns=self.mapping.get_value(dat, "impacted_artifact"),
118
- mitigated=None,
119
- operatingSystem="Linux",
120
- severity=severity,
121
- plugInName=self.mapping.get_value(dat, "issue_id", "XRay"),
122
- plugInId=int(self.mapping.get_value(dat, "issue_id", "Xray-0000")[5:]),
123
- cve=cve,
124
- vprScore=None,
125
- tenantsId=0, # Need a way to figure this out programmatically
126
- title=f"{self.mapping.get_value(dat, 'issue_id') or self.mapping.get_value(dat, 'summary', f'XRay Vulnerability from Import {get_current_datetime()}')} on asset {asset.name}",
127
- description=self.mapping.get_value(dat, "summary"),
128
- plugInText=vuln.get("cve"),
129
- extra_data={
130
- "references": self.mapping.get_value(dat, "references", "None"),
131
- "solution": self.mapping.get_value(dat, "fixed_versions", "None"),
132
- },
133
- )
134
- vulns.append(regscale_vuln)
135
- return vulns
120
+ def _extract_asset_name(self, data_item) -> Optional[str]:
121
+ """Extract asset name from data item
122
+
123
+ :param data_item: Data item to extract asset name from
124
+ :return: Asset name if found, None otherwise
125
+ :rtype: Optional[str]
126
+ """
127
+ if isinstance(data_item, dict):
128
+ return self.mapping.get_value(data_item, "impacted_artifact")
129
+ return data_item if data_item else None
130
+
131
+ def _process_list_data(self) -> Iterator[IntegrationAsset]:
132
+ """Process list data and yield assets
133
+
134
+ :return: Iterator of IntegrationAsset objects
135
+ :rtype: Iterator[IntegrationAsset]
136
+ """
137
+ for data_item in self.file_data:
138
+ if asset_name := self._extract_asset_name(data_item):
139
+ yield self.create_asset_from_name(asset_name)
140
+
141
+ def _process_dict_data(self) -> Iterator[IntegrationAsset]:
142
+ """Process dict data and yield assets
143
+
144
+ :return: Iterator of IntegrationAsset objects
145
+ :rtype: Iterator[IntegrationAsset]
146
+ """
147
+ if asset_name := self._extract_asset_name(self.file_data):
148
+ yield self.create_asset_from_name(asset_name)
149
+
150
+ def asset_generator(self) -> Iterator[IntegrationAsset]:
151
+ """Generate IntegrationAsset objects from the data
152
+
153
+ :return: Iterator of IntegrationAsset objects
154
+ :rtype: Iterator[IntegrationAsset]
155
+ """
156
+ if isinstance(self.file_data, list):
157
+ yield from self._process_list_data()
158
+ elif isinstance(self.file_data, dict):
159
+ yield from self._process_dict_data()
160
+
161
+ def process_assets(self, func: Callable) -> None:
162
+ """
163
+ Process the assets in the data and create an iterator of IntegrationAsset objects
164
+
165
+ :param Callable func: Function to create asset (not used)
166
+ :return: None
167
+ """
168
+
169
+ # Set the assets as an iterator directly
170
+ self.data["assets"] = self.asset_generator()
171
+ self.integration_assets = self.data["assets"]
172
+
173
+ def create_vuln(self, _dat: Optional[dict] = None, **kwargs) -> Iterator[IntegrationFinding]:
174
+ """
175
+ Fetches findings from the processed json files
176
+
177
+ :param Optional[dict] _dat: Data row from JSON file (unused, kept for compatibility)
178
+ :param **kwargs: Additional keyword arguments including index
179
+ :return: A list of findings
180
+ :rtype: Iterator[IntegrationFinding]
181
+ """
182
+ if findings := self.fetch_findings(**kwargs):
183
+ yield from findings
184
+
185
+ def _validate_cve_data(self, cve_data) -> bool:
186
+ """Validate CVE data structure
187
+
188
+ :param cve_data: CVE data to validate
189
+ :return: True if valid, False otherwise
190
+ :rtype: bool
191
+ """
192
+ if not isinstance(cve_data, list):
193
+ logger.warning("CVE data is not a list, skipping vulnerability creation")
194
+ return False
195
+ return True
196
+
197
+ def _get_valid_cve_data(self, cve_data: list) -> list:
198
+ """Filter CVE data to only include valid entries
199
+
200
+ :param list cve_data: Raw CVE data
201
+ :return: Filtered CVE data with actual CVE IDs
202
+ :rtype: list
203
+ """
204
+ return [c for c in cve_data if c.get("cve")]
205
+
206
+ def _determine_severity_from_cve(self, cve_dat: dict) -> str:
207
+ """Determine severity from CVE data
208
+
209
+ :param dict cve_dat: CVE data dictionary
210
+ :return: Severity string
211
+ :rtype: str
212
+ """
213
+ cvss3_score = cve_dat.get("cvss_v3_score", 0.0)
214
+ if cve_dat.get(self.scanner_config["cvss3_score"]):
215
+ return Vulnerability.determine_cvss3_severity_text(float(cvss3_score))
216
+ return "low"
217
+
218
+ def _extract_plugin_id(self, data_item: dict) -> int:
219
+ """Extract plugin ID from issue ID
220
+
221
+ :param dict data_item: Data item containing issue information
222
+ :return: Plugin ID as integer
223
+ :rtype: int
224
+ """
225
+ issue_id = self.mapping.get_value(data_item, "issue_id", "Xray-0000")
226
+ try:
227
+ if len(issue_id) > 5:
228
+ return int(issue_id[5:])
229
+ return 0
230
+ except (ValueError, TypeError):
231
+ logger.warning("Could not parse plugin_id from issue_id: %s", issue_id)
232
+ return 0
233
+
234
+ def _get_title_base(self, data_item: dict) -> str:
235
+ """Get title base for the finding
236
+
237
+ :param dict data_item: Data item containing issue information
238
+ :return: Title base string
239
+ :rtype: str
240
+ """
241
+ return self.mapping.get_value(data_item, "issue_id") or self.mapping.get_value(
242
+ data_item, "summary", f"XRay Vulnerability from Import {get_current_datetime()}"
243
+ )
244
+
245
+ def _create_finding_from_cve(self, data_item: dict, asset_name: str, cve_dat: dict) -> IntegrationFinding:
246
+ """Create a single finding from CVE data
247
+
248
+ :param dict data_item: Data item containing vulnerability information
249
+ :param str asset_name: Asset name for the finding
250
+ :param dict cve_dat: CVE data dictionary
251
+ :return: IntegrationFinding object
252
+ :rtype: IntegrationFinding
253
+ """
254
+ cve = cve_dat.get("cve")
255
+ cvss3_score = cve_dat.get("cvss_v3_score", 0.0)
256
+ severity = self._determine_severity_from_cve(cve_dat)
257
+ plugin_id = self._extract_plugin_id(data_item)
258
+ title_base = self._get_title_base(data_item)
259
+
260
+ return IntegrationFinding(
261
+ title=f"{title_base} on asset {asset_name}",
262
+ description=self.mapping.get_value(data_item, "summary"),
263
+ severity=self.determine_severity(severity),
264
+ status=IssueStatus.Open.value,
265
+ cvss_v3_score=cvss3_score,
266
+ cvss_v3_vector=cve_dat.get("cvss_v3_vector", ""),
267
+ plugin_name=self.mapping.get_value(data_item, "issue_id", "XRay"),
268
+ plugin_id=plugin_id,
269
+ asset_identifier=asset_name,
270
+ cve=cve,
271
+ first_seen=epoch_to_datetime(self.create_epoch),
272
+ last_seen=self.scan_date,
273
+ scan_date=self.scan_date,
274
+ category="Software",
275
+ control_labels=[],
276
+ )
277
+
278
+ def _create_findings_from_data_item(self, data_item: dict, asset_name: str) -> Iterator[IntegrationFinding]:
279
+ """Create findings from a single data item
280
+
281
+ :param dict data_item: The data item containing vulnerability information
282
+ :param str asset_name: The asset name for the finding
283
+ :yields: IntegrationFinding objects
284
+ """
285
+ cve_data = self.mapping.get_value(data_item, "cves", [])
286
+ if not self._validate_cve_data(cve_data):
287
+ return
288
+
289
+ valid_cve_data = self._get_valid_cve_data(cve_data)
290
+ for cve_dat in valid_cve_data:
291
+ yield self._create_finding_from_cve(data_item, asset_name, cve_dat)
292
+
293
+ def _process_list_findings(self) -> Iterator[IntegrationFinding]:
294
+ """Process findings from list data
295
+
296
+ :yields: IntegrationFinding objects
297
+ """
298
+ for data_item in self.file_data:
299
+ if isinstance(data_item, list):
300
+ continue
301
+ asset_name = self._extract_asset_name(data_item)
302
+ if asset_name:
303
+ yield from self._create_findings_from_data_item(data_item, asset_name)
304
+
305
+ def _process_dict_findings(self) -> Iterator[IntegrationFinding]:
306
+ """Process findings from dict data
307
+
308
+ :yields: IntegrationFinding objects
309
+ """
310
+ asset_name = self._extract_asset_name(self.file_data)
311
+ if asset_name:
312
+ yield from self._create_findings_from_data_item(self.file_data, asset_name)
313
+
314
+ def fetch_findings(self, **_) -> Iterator[IntegrationFinding]:
315
+ """
316
+ Fetch findings from Xray scan data.
317
+
318
+ :raises ValidationException: If there is an error fetching/parsing findings
319
+ :yields: Iterator[IntegrationFinding]
320
+ """
321
+ try:
322
+ if isinstance(self.file_data, list):
323
+ yield from self._process_list_findings()
324
+ elif isinstance(self.file_data, dict):
325
+ yield from self._process_dict_findings()
326
+ except Exception as exc:
327
+ error_message = traceback.format_exc()
328
+ logger.error("Error fetching findings: %s", error_message)
329
+ raise ValidationException(f"Error fetching findings: {error_message}") from exc
@@ -70,6 +70,7 @@ class ControlImplementation(RegScaleModel):
70
70
 
71
71
  _module_slug = "controlImplementation"
72
72
  _module_string = "controls"
73
+ _unique_fields = [["controlID", "parentId", "parentModule"]]
73
74
  _get_objects_for_list = True
74
75
 
75
76
  controlOwnerId: str = Field(default_factory=RegScaleModel.get_user_id)
@@ -248,18 +249,19 @@ class ControlImplementation(RegScaleModel):
248
249
  return response.json()
249
250
  return None
250
251
 
251
- def find_by_unique(self, **kwargs: dict) -> Optional["ControlImplementation"]:
252
- """
253
- Find an object by unique query.
254
-
255
- :param dict **kwargs: The unique query parameters
256
- :return: The object or None if not found
257
- :rtype: Optional[ControlImplementation]
258
- """
259
-
260
- for instance in self.get_by_security_control_id(security_control_id=self.controlID):
261
- return instance
262
- return None
252
+ # Removed for now, will need to be added back once platform changes are made
253
+ # def find_by_unique(self, **kwargs: dict) -> Optional["ControlImplementation"]:
254
+ # """
255
+ # Find an object by unique query.
256
+ #
257
+ # :param dict **kwargs: The unique query parameters
258
+ # :return: The object or None if not found
259
+ # :rtype: Optional[ControlImplementation]
260
+ # """
261
+ #
262
+ # for instance in self.get_by_security_control_id(security_control_id=self.controlID):
263
+ # return instance
264
+ # return None
263
265
 
264
266
  def _get_status_enum(self) -> List["ControlImplementationStatus"]:
265
267
  """
@@ -375,6 +375,10 @@ class File(BaseModel):
375
375
  except KeyError:
376
376
  if file_type == ".xlsx":
377
377
  file_type_header = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
378
+ elif file_type == ".docx":
379
+ file_type_header = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
380
+ elif file_type == ".pptx":
381
+ file_type_header = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
378
382
  elif file_type == ".nessus":
379
383
  file_type_header = "text/xml"
380
384
  elif file_type == ".gz":