regscale-cli 6.24.0.0__py3-none-any.whl → 6.25.0.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/_version.py +1 -1
- regscale/core/app/api.py +1 -1
- regscale/core/app/application.py +5 -3
- regscale/core/app/internal/evidence.py +308 -202
- regscale/dev/code_gen.py +84 -3
- regscale/integrations/commercial/__init__.py +2 -0
- regscale/integrations/commercial/jira.py +95 -22
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/synqly/assets.py +99 -16
- regscale/integrations/commercial/synqly/query_builder.py +533 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +134 -14
- regscale/integrations/commercial/wizv2/click.py +23 -0
- regscale/integrations/commercial/wizv2/compliance_report.py +137 -26
- regscale/integrations/compliance_integration.py +247 -5
- regscale/integrations/scanner_integration.py +16 -0
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +12 -2
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/synqly_model.py +47 -3
- regscale/models/regscale_models/compliance_settings.py +28 -0
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +143 -4
- regscale/regscale.py +1 -1
- regscale/validation/record.py +23 -1
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/METADATA +9 -9
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +32 -30
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/top_level.txt +0 -0
|
@@ -99,11 +99,17 @@ class Vulnerabilities(SynqlyModel):
|
|
|
99
99
|
"""
|
|
100
100
|
vuln_filter = self._handle_scan_date_options(regscale_ssp_id=regscale_ssp_id, **kwargs)
|
|
101
101
|
self.logger.debug(f"Vulnerability filter: {vuln_filter}")
|
|
102
|
+
|
|
103
|
+
# Pop the filter from kwargs so it doesn't get passed to query_findings
|
|
104
|
+
asset_filter = kwargs.pop("filter", [])
|
|
105
|
+
if asset_filter:
|
|
106
|
+
self.logger.debug(f"Asset filter: {asset_filter}")
|
|
107
|
+
|
|
102
108
|
self.logger.info(f"Fetching vulnerability data from {self.integration_name}...")
|
|
103
109
|
findings = (
|
|
104
110
|
self.fetch_integration_data(
|
|
105
111
|
func=self.tenant.engine_client.vulnerabilities.query_findings,
|
|
106
|
-
filter=vuln_filter,
|
|
112
|
+
filter=vuln_filter, # Only severity/date filters for findings
|
|
107
113
|
**kwargs,
|
|
108
114
|
)
|
|
109
115
|
if self.can_fetch_vulns
|
|
@@ -111,7 +117,11 @@ class Vulnerabilities(SynqlyModel):
|
|
|
111
117
|
)
|
|
112
118
|
self.logger.info(f"Fetching asset data from {self.integration_name}...")
|
|
113
119
|
assets = (
|
|
114
|
-
self.fetch_integration_data(
|
|
120
|
+
self.fetch_integration_data(
|
|
121
|
+
func=self.tenant.engine_client.vulnerabilities.query_assets,
|
|
122
|
+
filter=asset_filter, # Field-based filters only for assets
|
|
123
|
+
**kwargs,
|
|
124
|
+
)
|
|
115
125
|
if self.can_fetch_assets
|
|
116
126
|
else []
|
|
117
127
|
)
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized parser for extracting filter definitions from Synqly capabilities.
|
|
3
|
+
Used by both code generation and query builder.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
9
|
+
import importlib.resources as pkg_resources
|
|
10
|
+
|
|
11
|
+
from regscale.models.integration_models.synqly_models.connector_types import ConnectorType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FilterParser:
|
|
15
|
+
"""Parser for Synqly filter definitions from capabilities.json"""
|
|
16
|
+
|
|
17
|
+
# Define which connectors support filtering
|
|
18
|
+
FILTERABLE_CONNECTORS: Set[str] = {ConnectorType.Assets.value, ConnectorType.Vulnerabilities.value}
|
|
19
|
+
|
|
20
|
+
def __init__(self, capabilities_data: Optional[List[dict]] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the filter parser with provided or loaded capabilities.
|
|
23
|
+
|
|
24
|
+
:param Optional[List[dict]] capabilities_data: Pre-loaded capabilities data.
|
|
25
|
+
If None, will load from package resources.
|
|
26
|
+
"""
|
|
27
|
+
if capabilities_data is None:
|
|
28
|
+
self.capabilities_data = self._load_capabilities()
|
|
29
|
+
else:
|
|
30
|
+
self.capabilities_data = capabilities_data
|
|
31
|
+
self.filter_mapping = self._build_filter_mapping()
|
|
32
|
+
|
|
33
|
+
def _load_capabilities(self) -> List[dict]:
|
|
34
|
+
"""
|
|
35
|
+
Load capabilities.json from package resources.
|
|
36
|
+
|
|
37
|
+
:return: List of capability definitions
|
|
38
|
+
:rtype: List[dict]
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
files = pkg_resources.files("regscale.models.integration_models.synqly_models")
|
|
42
|
+
capabilities_file = files / "capabilities.json"
|
|
43
|
+
with capabilities_file.open("r") as file:
|
|
44
|
+
data = json.load(file)
|
|
45
|
+
return data.get("result", [])
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(f"Error loading capabilities.json: {e}")
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
def _build_filter_mapping(self) -> Dict[str, Dict[str, List[dict]]]:
|
|
51
|
+
"""
|
|
52
|
+
Build comprehensive filter mapping for all providers.
|
|
53
|
+
|
|
54
|
+
Structure:
|
|
55
|
+
{
|
|
56
|
+
'assets_armis_centrix': {
|
|
57
|
+
'query_devices': [
|
|
58
|
+
{
|
|
59
|
+
'name': 'device.ip',
|
|
60
|
+
'type': 'string',
|
|
61
|
+
'operators': ['eq', 'ne', 'in', 'not_in'],
|
|
62
|
+
'values': [] # For enum types
|
|
63
|
+
},
|
|
64
|
+
...
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
'vulnerabilities_qualys': {
|
|
68
|
+
'query_findings': [...],
|
|
69
|
+
'query_assets': [...]
|
|
70
|
+
},
|
|
71
|
+
...
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:return: Mapping of provider IDs to their operations and filters
|
|
75
|
+
:rtype: Dict[str, Dict[str, List[dict]]]
|
|
76
|
+
"""
|
|
77
|
+
filter_mapping = {}
|
|
78
|
+
|
|
79
|
+
for provider in self.capabilities_data:
|
|
80
|
+
provider_id = provider.get("id", "")
|
|
81
|
+
connector_type = provider.get("connector", "")
|
|
82
|
+
|
|
83
|
+
# Only process filterable connector types
|
|
84
|
+
if connector_type not in self.FILTERABLE_CONNECTORS:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Skip mock providers if desired (they end with _mock)
|
|
88
|
+
# if provider_id.endswith('_mock'):
|
|
89
|
+
# continue
|
|
90
|
+
|
|
91
|
+
operations = provider.get("operations", [])
|
|
92
|
+
for operation in operations:
|
|
93
|
+
# Only process supported operations with filters
|
|
94
|
+
if not operation.get("supported", False):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
filters = operation.get("filters", [])
|
|
98
|
+
if filters:
|
|
99
|
+
if provider_id not in filter_mapping:
|
|
100
|
+
filter_mapping[provider_id] = {}
|
|
101
|
+
|
|
102
|
+
operation_name = operation.get("name", "")
|
|
103
|
+
filter_mapping[provider_id][operation_name] = filters
|
|
104
|
+
|
|
105
|
+
return filter_mapping
|
|
106
|
+
|
|
107
|
+
def get_filters_for_provider(self, provider_id: str, operation: Optional[str] = None) -> List[dict]:
|
|
108
|
+
"""
|
|
109
|
+
Get filters for a specific provider and optionally a specific operation.
|
|
110
|
+
|
|
111
|
+
:param str provider_id: Provider ID (e.g., 'assets_armis_centrix')
|
|
112
|
+
:param Optional[str] operation: Operation name (e.g., 'query_devices')
|
|
113
|
+
:return: List of filter definitions
|
|
114
|
+
:rtype: List[dict]
|
|
115
|
+
"""
|
|
116
|
+
provider_filters = self.filter_mapping.get(provider_id, {})
|
|
117
|
+
|
|
118
|
+
if operation:
|
|
119
|
+
return provider_filters.get(operation, [])
|
|
120
|
+
|
|
121
|
+
# Return all filters for all operations if no specific operation
|
|
122
|
+
all_filters = []
|
|
123
|
+
seen_fields = set() # Avoid duplicates
|
|
124
|
+
|
|
125
|
+
for op_filters in provider_filters.values():
|
|
126
|
+
for filter_def in op_filters:
|
|
127
|
+
field_name = filter_def.get("name", "")
|
|
128
|
+
if field_name not in seen_fields:
|
|
129
|
+
all_filters.append(filter_def)
|
|
130
|
+
seen_fields.add(field_name)
|
|
131
|
+
|
|
132
|
+
return all_filters
|
|
133
|
+
|
|
134
|
+
def get_providers_with_filters(self, connector_type: str) -> List[str]:
|
|
135
|
+
"""
|
|
136
|
+
Get list of providers that support filtering for a connector type.
|
|
137
|
+
|
|
138
|
+
:param str connector_type: Connector type (e.g., 'assets', 'vulnerabilities')
|
|
139
|
+
:return: List of provider IDs that have filters
|
|
140
|
+
:rtype: List[str]
|
|
141
|
+
"""
|
|
142
|
+
providers = []
|
|
143
|
+
|
|
144
|
+
for provider_id, operations in self.filter_mapping.items():
|
|
145
|
+
# Check if provider matches connector type and has filters
|
|
146
|
+
if provider_id.startswith(f"{connector_type}_") and operations:
|
|
147
|
+
providers.append(provider_id)
|
|
148
|
+
|
|
149
|
+
# Sort for consistent ordering
|
|
150
|
+
return sorted(providers)
|
|
151
|
+
|
|
152
|
+
def has_filters(self, provider_id: str) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Check if a provider has any filters defined.
|
|
155
|
+
|
|
156
|
+
:param str provider_id: Provider ID to check
|
|
157
|
+
:return: True if provider has filters
|
|
158
|
+
:rtype: bool
|
|
159
|
+
"""
|
|
160
|
+
return provider_id in self.filter_mapping and bool(self.filter_mapping[provider_id])
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def format_filter_string(field: str, operator: str, value: str) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Convert user input to Synqly filter format.
|
|
166
|
+
|
|
167
|
+
:param str field: Field name (e.g., 'device.ip')
|
|
168
|
+
:param str operator: Operator (e.g., 'eq', 'gte')
|
|
169
|
+
:param str value: Filter value
|
|
170
|
+
:return: Formatted filter string
|
|
171
|
+
:rtype: str
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
format_filter_string('device.ip', 'eq', '192.168.1.1')
|
|
175
|
+
Returns: 'device.ip[eq]192.168.1.1'
|
|
176
|
+
"""
|
|
177
|
+
return f"{field}[{operator}]{value}"
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def parse_filter_string(filter_string: str) -> Optional[Tuple[str, str, str]]:
|
|
181
|
+
"""
|
|
182
|
+
Parse a filter string into its components.
|
|
183
|
+
|
|
184
|
+
:param str filter_string: Filter in format 'field[operator]value'
|
|
185
|
+
:return: Tuple of (field, operator, value) or None if invalid
|
|
186
|
+
:rtype: Optional[Tuple[str, str, str]]
|
|
187
|
+
"""
|
|
188
|
+
match = re.match(r"^([a-z._]+)\[([a-z_]+)\](.+)$", filter_string, re.IGNORECASE)
|
|
189
|
+
if match:
|
|
190
|
+
return match.groups()
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def validate_filter(self, provider_id: str, filter_string: str) -> Tuple[bool, str]:
|
|
194
|
+
"""
|
|
195
|
+
Validate a filter string against provider capabilities.
|
|
196
|
+
|
|
197
|
+
:param str provider_id: Provider ID (e.g., 'assets_armis_centrix')
|
|
198
|
+
:param str filter_string: Filter in format 'field[operator]value'
|
|
199
|
+
:return: Tuple of (is_valid, error_message)
|
|
200
|
+
:rtype: Tuple[bool, str]
|
|
201
|
+
"""
|
|
202
|
+
# Parse the filter string
|
|
203
|
+
parsed = self.parse_filter_string(filter_string)
|
|
204
|
+
if not parsed:
|
|
205
|
+
return False, f"Invalid filter format: {filter_string}. Expected format: field[operator]value"
|
|
206
|
+
|
|
207
|
+
field, operator, value = parsed
|
|
208
|
+
|
|
209
|
+
# Get all filters for this provider
|
|
210
|
+
provider_filters = self.get_filters_for_provider(provider_id)
|
|
211
|
+
|
|
212
|
+
if not provider_filters:
|
|
213
|
+
return False, f"Provider '{provider_id}' does not support filtering"
|
|
214
|
+
|
|
215
|
+
# Check if field exists
|
|
216
|
+
field_filter = None
|
|
217
|
+
for f in provider_filters:
|
|
218
|
+
if f.get("name") == field:
|
|
219
|
+
field_filter = f
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
if not field_filter:
|
|
223
|
+
available_fields = [f.get("name", "") for f in provider_filters]
|
|
224
|
+
return (
|
|
225
|
+
False,
|
|
226
|
+
f"Field '{field}' not supported by {provider_id}. Available fields: {', '.join(available_fields)}",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Check if operator is valid for this field
|
|
230
|
+
valid_operators = field_filter.get("operators", [])
|
|
231
|
+
if operator not in valid_operators:
|
|
232
|
+
return (
|
|
233
|
+
False,
|
|
234
|
+
f"Operator '{operator}' not valid for field '{field}'. Valid operators: {', '.join(valid_operators)}",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Optionally validate value type and format
|
|
238
|
+
field_type = field_filter.get("type", "string")
|
|
239
|
+
|
|
240
|
+
# Handle comma-separated values for 'in' and 'not_in' operators
|
|
241
|
+
if operator in ["in", "not_in"]:
|
|
242
|
+
values_to_check = [v.strip() for v in value.split(",")]
|
|
243
|
+
else:
|
|
244
|
+
values_to_check = [value]
|
|
245
|
+
|
|
246
|
+
for val in values_to_check:
|
|
247
|
+
if field_type == "number":
|
|
248
|
+
try:
|
|
249
|
+
float(val)
|
|
250
|
+
except ValueError:
|
|
251
|
+
return False, f"Value '{val}' is not a valid number for field '{field}'"
|
|
252
|
+
elif field_type == "enum":
|
|
253
|
+
valid_values = field_filter.get("values", [])
|
|
254
|
+
if valid_values and val not in valid_values:
|
|
255
|
+
return (
|
|
256
|
+
False,
|
|
257
|
+
f"Value '{val}' not valid for field '{field}'. Valid values: {', '.join(valid_values)}",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return True, ""
|
|
261
|
+
|
|
262
|
+
def get_operator_display_name(self, operator: str) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Get human-friendly display name for an operator.
|
|
265
|
+
|
|
266
|
+
:param str operator: Operator code
|
|
267
|
+
:return: Display name
|
|
268
|
+
:rtype: str
|
|
269
|
+
"""
|
|
270
|
+
operator_map = {
|
|
271
|
+
"eq": "equals",
|
|
272
|
+
"ne": "not equals",
|
|
273
|
+
"in": "in list",
|
|
274
|
+
"not_in": "not in list",
|
|
275
|
+
"like": "matches pattern",
|
|
276
|
+
"not_like": "does not match pattern",
|
|
277
|
+
"gt": "greater than",
|
|
278
|
+
"gte": "greater than or equal to",
|
|
279
|
+
"lt": "less than",
|
|
280
|
+
"lte": "less than or equal to",
|
|
281
|
+
}
|
|
282
|
+
return operator_map.get(operator, operator)
|
|
283
|
+
|
|
284
|
+
def get_field_display_name(self, field: str) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Convert field name to human-friendly display name.
|
|
287
|
+
|
|
288
|
+
:param str field: Field name (e.g., 'device.hw_info.serial_number')
|
|
289
|
+
:return: Display name (e.g., 'Device Hardware Info Serial Number')
|
|
290
|
+
:rtype: str
|
|
291
|
+
"""
|
|
292
|
+
# Replace dots and underscores with spaces, then title case
|
|
293
|
+
display = field.replace(".", " ").replace("_", " ").title()
|
|
294
|
+
return display
|
|
295
|
+
|
|
296
|
+
def get_connector_operations(self, connector_type: str) -> Dict[str, List[str]]:
|
|
297
|
+
"""
|
|
298
|
+
Get all operations that support filtering for a connector type.
|
|
299
|
+
|
|
300
|
+
:param str connector_type: Connector type (e.g., 'assets')
|
|
301
|
+
:return: Dict mapping provider IDs to their filterable operations
|
|
302
|
+
:rtype: Dict[str, List[str]]
|
|
303
|
+
"""
|
|
304
|
+
operations_map = {}
|
|
305
|
+
|
|
306
|
+
for provider_id, operations in self.filter_mapping.items():
|
|
307
|
+
if provider_id.startswith(f"{connector_type}_"):
|
|
308
|
+
operations_map[provider_id] = list(operations.keys())
|
|
309
|
+
|
|
310
|
+
return operations_map
|
|
311
|
+
|
|
312
|
+
def get_stats(self) -> dict:
|
|
313
|
+
"""
|
|
314
|
+
Get statistics about loaded filters.
|
|
315
|
+
|
|
316
|
+
:return: Dictionary with filter statistics
|
|
317
|
+
:rtype: dict
|
|
318
|
+
"""
|
|
319
|
+
stats = {
|
|
320
|
+
"total_providers": len(self.capabilities_data),
|
|
321
|
+
"providers_with_filters": len(self.filter_mapping),
|
|
322
|
+
"total_filters": 0,
|
|
323
|
+
"by_connector": {},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for connector in self.FILTERABLE_CONNECTORS:
|
|
327
|
+
providers = self.get_providers_with_filters(connector)
|
|
328
|
+
filter_count = sum(len(self.get_filters_for_provider(p)) for p in providers)
|
|
329
|
+
stats["by_connector"][connector] = {"providers": len(providers), "filters": filter_count}
|
|
330
|
+
stats["total_filters"] += filter_count
|
|
331
|
+
|
|
332
|
+
return stats
|
|
@@ -20,6 +20,7 @@ from regscale.core.app.api import Api
|
|
|
20
20
|
from regscale.core.app.application import Application
|
|
21
21
|
from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit
|
|
22
22
|
from regscale.models.integration_models.synqly_models.connector_types import ConnectorType
|
|
23
|
+
from regscale.models.integration_models.synqly_models.filter_parser import FilterParser
|
|
23
24
|
from regscale.models.integration_models.synqly_models.ocsf_mapper import Mapper
|
|
24
25
|
from regscale.models.integration_models.synqly_models.param import Param
|
|
25
26
|
from regscale.models.integration_models.synqly_models.tenants import Tenant
|
|
@@ -37,7 +38,7 @@ class SynqlyModel(BaseModel, ABC):
|
|
|
37
38
|
client: Optional[Any] = None
|
|
38
39
|
connectors: dict = Field(default_factory=dict)
|
|
39
40
|
# defined using the openApi spec on 7/16/2024, this is updated via _get_integrations_and_secrets()
|
|
40
|
-
connector_types: set = Field(default_factory=lambda:
|
|
41
|
+
connector_types: set = Field(default_factory=lambda: {connector.__str__() for connector in ConnectorType})
|
|
41
42
|
terminated: Optional[bool] = False
|
|
42
43
|
app: Application = Field(default_factory=Application, alias="app")
|
|
43
44
|
api: Api = Field(default_factory=Api, alias="api")
|
|
@@ -60,6 +61,7 @@ class SynqlyModel(BaseModel, ABC):
|
|
|
60
61
|
created_regscale_objects: list = Field(default_factory=list)
|
|
61
62
|
updated_regscale_objects: list = Field(default_factory=list)
|
|
62
63
|
regscale_objects_to_update: list = Field(default_factory=list)
|
|
64
|
+
filter_parser: Optional[FilterParser] = None
|
|
63
65
|
|
|
64
66
|
def __init__(self: S, connector_type: Optional[str] = None, integration: Optional[str] = None, **kwargs):
|
|
65
67
|
try:
|
|
@@ -278,6 +280,8 @@ class SynqlyModel(BaseModel, ABC):
|
|
|
278
280
|
:rtype: dict
|
|
279
281
|
"""
|
|
280
282
|
raw_data = self._load_from_package()
|
|
283
|
+
# Initialize FilterParser with the loaded capabilities data
|
|
284
|
+
self.filter_parser = FilterParser(capabilities_data=raw_data)
|
|
281
285
|
return self._parse_api_spec_data(raw_data, return_params)
|
|
282
286
|
|
|
283
287
|
def _parse_api_spec_data(self, data: dict, return_params: bool = False) -> dict:
|
|
@@ -441,12 +445,12 @@ class SynqlyModel(BaseModel, ABC):
|
|
|
441
445
|
|
|
442
446
|
operations = schema.get("operations", [])
|
|
443
447
|
capabilities = [item["name"] for item in operations if item.get("supported")]
|
|
444
|
-
capabilities_params =
|
|
448
|
+
capabilities_params = [
|
|
445
449
|
field
|
|
446
450
|
for item in operations
|
|
447
451
|
if item.get("supported") and "required_fields" in item.keys()
|
|
448
452
|
for field in item.get("required_fields", [])
|
|
449
|
-
|
|
453
|
+
]
|
|
450
454
|
if self.integration.lower() in key.lower():
|
|
451
455
|
self.capabilities = capabilities
|
|
452
456
|
schema = schema["provider_config"]
|
|
@@ -636,6 +640,42 @@ class SynqlyModel(BaseModel, ABC):
|
|
|
636
640
|
"""
|
|
637
641
|
pass
|
|
638
642
|
|
|
643
|
+
def validate_filters(self, filters: Union[tuple, list, str]) -> list[str]:
|
|
644
|
+
"""
|
|
645
|
+
Validate filter strings against provider capabilities.
|
|
646
|
+
|
|
647
|
+
:param Union[tuple, list, str] filters: Filter(s) to validate
|
|
648
|
+
:return: Validated filter list
|
|
649
|
+
:rtype: list[str]
|
|
650
|
+
:raises: SystemExit if validation fails
|
|
651
|
+
"""
|
|
652
|
+
if not self.filter_parser:
|
|
653
|
+
self.logger.warning("FilterParser not available for filter validation")
|
|
654
|
+
if isinstance(filters, list):
|
|
655
|
+
return filters
|
|
656
|
+
elif filters:
|
|
657
|
+
return [filters]
|
|
658
|
+
else:
|
|
659
|
+
return []
|
|
660
|
+
|
|
661
|
+
provider_id = f"{self._connector_type}_{self.integration}"
|
|
662
|
+
validated_filters = []
|
|
663
|
+
|
|
664
|
+
# Normalize to list for processing
|
|
665
|
+
if isinstance(filters, str):
|
|
666
|
+
filters = [filters]
|
|
667
|
+
elif filters is None:
|
|
668
|
+
return []
|
|
669
|
+
|
|
670
|
+
for filter_string in filters:
|
|
671
|
+
is_valid, error_message = self.filter_parser.validate_filter(provider_id, filter_string)
|
|
672
|
+
if not is_valid:
|
|
673
|
+
error_and_exit(f"Filter validation failed: {error_message}")
|
|
674
|
+
validated_filters.append(filter_string)
|
|
675
|
+
self.logger.debug(f"Filter '{filter_string}' validated successfully")
|
|
676
|
+
|
|
677
|
+
return validated_filters
|
|
678
|
+
|
|
639
679
|
def fetch_integration_data(
|
|
640
680
|
self, func: Callable, **kwargs
|
|
641
681
|
) -> list[Union["InventoryAsset", "SecurityFinding", "Ticket"]]:
|
|
@@ -648,6 +688,10 @@ class SynqlyModel(BaseModel, ABC):
|
|
|
648
688
|
"""
|
|
649
689
|
query_filter = kwargs.get("filter")
|
|
650
690
|
limit = kwargs.get("limit", 200)
|
|
691
|
+
|
|
692
|
+
# Validate filters if provided
|
|
693
|
+
if query_filter:
|
|
694
|
+
query_filter = self.validate_filters(query_filter)
|
|
651
695
|
integration_data: list = []
|
|
652
696
|
fetch_res = func(
|
|
653
697
|
filter=query_filter,
|
|
@@ -110,3 +110,31 @@ class ComplianceSettings(RegScaleModel):
|
|
|
110
110
|
:rtype: List[str]
|
|
111
111
|
"""
|
|
112
112
|
return self.__class__.get_labels(self.id, setting_field)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_settings_list(cls) -> List[dict]:
|
|
116
|
+
"""
|
|
117
|
+
Get all compliance settings list items from settingsList endpoint.
|
|
118
|
+
|
|
119
|
+
:return: A list of compliance settings list items
|
|
120
|
+
:rtype: List[dict]
|
|
121
|
+
"""
|
|
122
|
+
response = cls._get_api_handler().get(endpoint="/api/compliance/settingsList")
|
|
123
|
+
if response and response.ok:
|
|
124
|
+
return response.json()
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_default_responsibility_for_compliance_setting(cls, compliance_setting_id: int) -> Optional[str]:
|
|
129
|
+
"""
|
|
130
|
+
Get the default responsibility value for a specific compliance setting.
|
|
131
|
+
|
|
132
|
+
:param int compliance_setting_id: The compliance setting ID
|
|
133
|
+
:return: The default responsibility value or None if not found
|
|
134
|
+
:rtype: Optional[str]
|
|
135
|
+
"""
|
|
136
|
+
settings_list = cls.get_settings_list()
|
|
137
|
+
for setting in settings_list:
|
|
138
|
+
if setting.get("complianceSettingId") == compliance_setting_id and setting.get("isDefault", False):
|
|
139
|
+
return setting.get("statusName")
|
|
140
|
+
return None
|