regscale-cli 6.24.0.1__py3-none-any.whl → 6.25.0.1__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 +4 -4
- 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/compliance_report.py +22 -0
- regscale/integrations/compliance_integration.py +17 -0
- 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 +130 -1
- regscale/regscale.py +1 -1
- regscale/validation/record.py +23 -1
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.1.dist-info}/METADATA +1 -1
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.1.dist-info}/RECORD +31 -29
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.1.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
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# standard python imports
|
|
5
5
|
import logging
|
|
6
6
|
from enum import Enum
|
|
7
|
+
from functools import lru_cache
|
|
7
8
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
8
9
|
from urllib.parse import urljoin
|
|
9
10
|
|
|
@@ -104,7 +105,9 @@ class ControlImplementation(RegScaleModel):
|
|
|
104
105
|
qiVendorCompliance: Optional[str] = None
|
|
105
106
|
qiIssues: Optional[str] = None
|
|
106
107
|
qiOverall: Optional[str] = None
|
|
107
|
-
responsibility:
|
|
108
|
+
responsibility: str = Field(
|
|
109
|
+
default_factory=lambda: ControlImplementation.get_default_responsibility()
|
|
110
|
+
) # Required field - Control Origination
|
|
108
111
|
inheritedControlId: Optional[int] = None
|
|
109
112
|
inheritedRequirementId: Optional[int] = None
|
|
110
113
|
inheritedSecurityPlanId: Optional[int] = None
|
|
@@ -157,6 +160,17 @@ class ControlImplementation(RegScaleModel):
|
|
|
157
160
|
if self.controlOwnersIds is None and self.controlOwnerId:
|
|
158
161
|
self.controlOwnersIds = [self.controlOwnerId]
|
|
159
162
|
|
|
163
|
+
# Set intelligent default responsibility if not explicitly set and we have parent info
|
|
164
|
+
if (
|
|
165
|
+
self.responsibility == self.get_default_responsibility()
|
|
166
|
+
and self.parentId
|
|
167
|
+
and self.parentModule == "securityplans"
|
|
168
|
+
):
|
|
169
|
+
# Try to get a more specific default based on the actual security plan's compliance settings
|
|
170
|
+
better_default = self.get_default_responsibility(parent_id=self.parentId)
|
|
171
|
+
if better_default != self.responsibility:
|
|
172
|
+
self.responsibility = better_default
|
|
173
|
+
|
|
160
174
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
161
175
|
"""
|
|
162
176
|
Override __setattr__ to update status_lst when status changes and handle backwards compatibility.
|
|
@@ -175,6 +189,121 @@ class ControlImplementation(RegScaleModel):
|
|
|
175
189
|
):
|
|
176
190
|
super().__setattr__("controlOwnersIds", [value])
|
|
177
191
|
|
|
192
|
+
@classmethod
|
|
193
|
+
@lru_cache(maxsize=256)
|
|
194
|
+
def get_default_responsibility(
|
|
195
|
+
cls, parent_id: Optional[int] = None, compliance_setting_id: Optional[int] = None
|
|
196
|
+
) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Get default responsibility (control origination) based on compliance settings.
|
|
199
|
+
|
|
200
|
+
Cached for high-performance bulk operations.
|
|
201
|
+
|
|
202
|
+
:param Optional[int] parent_id: The parent security plan ID to get compliance settings from
|
|
203
|
+
:param Optional[int] compliance_setting_id: Specific compliance setting ID override
|
|
204
|
+
:return: Default responsibility string
|
|
205
|
+
:rtype: str
|
|
206
|
+
"""
|
|
207
|
+
actual_compliance_setting_id = compliance_setting_id or cls._get_compliance_setting_id_from_parent(parent_id)
|
|
208
|
+
|
|
209
|
+
if actual_compliance_setting_id:
|
|
210
|
+
responsibility = cls._get_responsibility_from_compliance_settings(actual_compliance_setting_id)
|
|
211
|
+
if responsibility:
|
|
212
|
+
return responsibility
|
|
213
|
+
|
|
214
|
+
return cls._get_fallback_responsibility(actual_compliance_setting_id)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
@lru_cache(maxsize=128)
|
|
218
|
+
def _get_compliance_setting_id_from_parent(cls, parent_id: Optional[int]) -> Optional[int]:
|
|
219
|
+
"""
|
|
220
|
+
Get compliance setting ID from parent security plan.
|
|
221
|
+
|
|
222
|
+
Cached to avoid repeated API calls for the same security plan.
|
|
223
|
+
"""
|
|
224
|
+
if not parent_id:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
from regscale.models.regscale_models.security_plan import SecurityPlan
|
|
229
|
+
|
|
230
|
+
security_plan = SecurityPlan.get_object(parent_id)
|
|
231
|
+
return security_plan.complianceSettingsId if security_plan else None
|
|
232
|
+
except Exception:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
@lru_cache(maxsize=32)
|
|
237
|
+
def _get_responsibility_from_compliance_settings(cls, compliance_setting_id: int) -> Optional[str]:
|
|
238
|
+
"""
|
|
239
|
+
Get default responsibility from compliance settings API using settingsList endpoint.
|
|
240
|
+
|
|
241
|
+
Cached to avoid repeated API calls for the same compliance setting.
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
from regscale.models.regscale_models.compliance_settings import ComplianceSettings
|
|
245
|
+
|
|
246
|
+
return ComplianceSettings.get_default_responsibility_for_compliance_setting(compliance_setting_id)
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def _get_fallback_responsibility(cls, compliance_setting_id: Optional[int] = None) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Get intelligent fallback responsibility using framework-specific defaults.
|
|
256
|
+
|
|
257
|
+
:param Optional[int] compliance_setting_id: Compliance setting ID to determine framework type
|
|
258
|
+
:return: Fallback responsibility string
|
|
259
|
+
:rtype: str
|
|
260
|
+
"""
|
|
261
|
+
if compliance_setting_id:
|
|
262
|
+
return cls._get_framework_default_responsibility(compliance_setting_id)
|
|
263
|
+
|
|
264
|
+
# Ultimate fallback for unknown compliance settings
|
|
265
|
+
return ControlImplementationOrigin.ProviderSS.value
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def _get_framework_default_responsibility(cls, compliance_setting_id: int) -> str:
|
|
269
|
+
"""
|
|
270
|
+
Get default responsibility for a specific compliance framework.
|
|
271
|
+
|
|
272
|
+
:param int compliance_setting_id: The compliance setting ID (1=RegScale, 2=FedRAMP, 3=PCI, 4=DoD, 5=CMMC)
|
|
273
|
+
:return: Default responsibility string
|
|
274
|
+
:rtype: str
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
from regscale.models.regscale_models.compliance_settings import ComplianceSettings
|
|
278
|
+
|
|
279
|
+
default_value = ComplianceSettings.get_default_responsibility_for_compliance_setting(compliance_setting_id)
|
|
280
|
+
if default_value:
|
|
281
|
+
return default_value
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
# Framework-specific fallbacks if API fails
|
|
286
|
+
fallback_map = {
|
|
287
|
+
1: "Provider", # RegScale Default
|
|
288
|
+
2: ImplementationControlOrigin.SERVICE_PROVIDER_CORPORATE.value, # FedRAMP
|
|
289
|
+
3: ImplementationControlOrigin.SERVICE_PROVIDER_CORPORATE.value, # PCI
|
|
290
|
+
4: "System-Specific", # DoD
|
|
291
|
+
5: "Provider", # CMMC
|
|
292
|
+
}
|
|
293
|
+
return fallback_map.get(compliance_setting_id, "Service Provider Corporate")
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def clear_responsibility_cache(cls) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Clear the responsibility lookup cache.
|
|
299
|
+
|
|
300
|
+
Call this method when compliance settings have been updated to ensure
|
|
301
|
+
fresh data is retrieved from the API.
|
|
302
|
+
"""
|
|
303
|
+
cls.get_default_responsibility.cache_clear()
|
|
304
|
+
cls._get_compliance_setting_id_from_parent.cache_clear()
|
|
305
|
+
cls._get_responsibility_from_compliance_settings.cache_clear()
|
|
306
|
+
|
|
178
307
|
@classmethod
|
|
179
308
|
def _get_additional_endpoints(cls) -> ConfigDict:
|
|
180
309
|
"""
|
regscale/regscale.py
CHANGED
|
@@ -293,7 +293,7 @@ def banner():
|
|
|
293
293
|
\t[#05d1b7].clicli, [#15cfec].;loooool'
|
|
294
294
|
\t[#05d1b7].clicli, [#18a8e9].:oolloc.
|
|
295
295
|
\t[#05d1b7].clicli, [#ef7f2e].,cli,. [#18a8e9].clllll,
|
|
296
|
-
\t[#05d1b7].clicli. [#ef7f2e].,oxxxxd; [#
|
|
296
|
+
\t[#05d1b7].clicli. [#ef7f2e].,oxxxxd; [#18a8e9].:lllll;
|
|
297
297
|
\t[#05d1b7] ..cli. [#f68d1f]';cdxxxxxo, [#18a8e9].cllllc,
|
|
298
298
|
\t [#f68d1f].:odddddddc. [#1b97d5] .;ccccc:.
|
|
299
299
|
\t[#ffc42a] ..'. [#f68d1f].;ldddddddl' [#0c8cd7].':ccccc:.
|