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
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""Interactive query builder for Synqly connector filters"""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from regscale.models.integration_models.synqly_models.filter_parser import FilterParser
|
|
6
|
+
|
|
7
|
+
# Constants
|
|
8
|
+
INVALID_CHOICE_MSG = "Invalid choice"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_build_query(connector: str, provider: Optional[str], validate: Optional[str], list_fields: bool):
|
|
12
|
+
"""
|
|
13
|
+
Build and validate filter queries for Synqly connectors.
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
# Build a filter (interactive by default)
|
|
17
|
+
regscale assets build-query
|
|
18
|
+
|
|
19
|
+
# List all fields for a specific provider
|
|
20
|
+
regscale assets build-query --provider assets_armis_centrix --list-fields
|
|
21
|
+
|
|
22
|
+
# Validate a filter string
|
|
23
|
+
regscale assets build-query --provider assets_armis_centrix --validate "device.ip[eq]192.168.1.1"
|
|
24
|
+
"""
|
|
25
|
+
parser = FilterParser()
|
|
26
|
+
|
|
27
|
+
# Check for modes that require provider first
|
|
28
|
+
if validate or list_fields:
|
|
29
|
+
if not provider:
|
|
30
|
+
click.echo("Error: --provider is required when using --validate or --list-fields options.", err=True)
|
|
31
|
+
click.echo("\nAvailable providers with filters:")
|
|
32
|
+
_list_providers(parser, connector)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Handle provider-specific modes
|
|
36
|
+
if provider:
|
|
37
|
+
# Validate provider exists and has filters
|
|
38
|
+
if not parser.has_filters(provider):
|
|
39
|
+
click.echo(f"Error: Provider '{provider}' either doesn't exist or doesn't support filtering.", err=True)
|
|
40
|
+
click.echo("\nAvailable providers with filters:")
|
|
41
|
+
_list_providers(parser, connector)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Handle specific modes
|
|
45
|
+
if validate:
|
|
46
|
+
_validate_filter(parser, provider, validate)
|
|
47
|
+
elif list_fields:
|
|
48
|
+
_list_fields(parser, provider)
|
|
49
|
+
else:
|
|
50
|
+
# Show provider info
|
|
51
|
+
_show_provider_info(parser, provider)
|
|
52
|
+
else:
|
|
53
|
+
# Default behavior: interactive mode
|
|
54
|
+
_interactive_mode_full(parser, connector)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _list_providers(parser: FilterParser, connector: str):
|
|
58
|
+
"""List all providers that support filtering for a connector type"""
|
|
59
|
+
providers = parser.get_providers_with_filters(connector)
|
|
60
|
+
|
|
61
|
+
# Filter out mock providers
|
|
62
|
+
filtered_providers = [p for p in providers if not p.endswith("_mock")]
|
|
63
|
+
|
|
64
|
+
if not filtered_providers:
|
|
65
|
+
click.echo(f"No providers found with filter support for {connector} connector.")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
click.echo(f"\nProviders with filter support for {connector}:")
|
|
69
|
+
click.echo("=" * 60)
|
|
70
|
+
|
|
71
|
+
for provider_id in filtered_providers:
|
|
72
|
+
filters = parser.get_filters_for_provider(provider_id)
|
|
73
|
+
operations = parser.get_connector_operations(connector).get(provider_id, [])
|
|
74
|
+
|
|
75
|
+
# Extract provider name from ID
|
|
76
|
+
provider_name = provider_id.replace(f"{connector}_", "").replace("_", " ").title()
|
|
77
|
+
|
|
78
|
+
click.echo(f"\n{provider_name}")
|
|
79
|
+
click.echo(f" Provider ID: {provider_id}")
|
|
80
|
+
click.echo(f" Supported operations: {', '.join(operations)}")
|
|
81
|
+
click.echo(f" Available filters: {len(filters)}")
|
|
82
|
+
|
|
83
|
+
# Show first 3 filters as examples
|
|
84
|
+
if filters:
|
|
85
|
+
click.echo(" Example fields:")
|
|
86
|
+
for i, f in enumerate(filters[:3], 1):
|
|
87
|
+
ops = ", ".join(f.get("operators", []))
|
|
88
|
+
click.echo(f" - {f['name']} ({f['type']}) [{ops}]")
|
|
89
|
+
if len(filters) > 3:
|
|
90
|
+
click.echo(f" ... and {len(filters) - 3} more")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _list_fields(parser: FilterParser, provider: str):
|
|
94
|
+
"""List all available fields for a provider"""
|
|
95
|
+
filters = parser.get_filters_for_provider(provider)
|
|
96
|
+
|
|
97
|
+
if not filters:
|
|
98
|
+
click.echo(f"No filters available for provider '{provider}'")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
provider_name = provider.replace(provider.split("_")[0] + "_", "").replace("_", " ").title()
|
|
102
|
+
click.echo(f"\nAvailable filters for {provider_name}:")
|
|
103
|
+
click.echo("=" * 80)
|
|
104
|
+
|
|
105
|
+
# Group by field prefix for better organization
|
|
106
|
+
grouped = {}
|
|
107
|
+
for f in filters:
|
|
108
|
+
field_name = f["name"]
|
|
109
|
+
prefix = field_name.split(".")[0] if "." in field_name else "root"
|
|
110
|
+
if prefix not in grouped:
|
|
111
|
+
grouped[prefix] = []
|
|
112
|
+
grouped[prefix].append(f)
|
|
113
|
+
|
|
114
|
+
for prefix, fields in sorted(grouped.items()):
|
|
115
|
+
click.echo(f"\n{prefix.upper()} fields:")
|
|
116
|
+
for f in sorted(fields, key=lambda x: x["name"]):
|
|
117
|
+
field_display = parser.get_field_display_name(f["name"])
|
|
118
|
+
operators = [parser.get_operator_display_name(op) for op in f.get("operators", [])]
|
|
119
|
+
|
|
120
|
+
click.echo(f" {f['name']}")
|
|
121
|
+
click.echo(f" Display: {field_display}")
|
|
122
|
+
click.echo(f" Type: {f['type']}")
|
|
123
|
+
click.echo(f" Operators: {', '.join(operators)}")
|
|
124
|
+
|
|
125
|
+
# Show enum values if present
|
|
126
|
+
if f.get("values"):
|
|
127
|
+
click.echo(f" Allowed values: {', '.join(f['values'])}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _validate_filter(parser: FilterParser, provider: str, filter_string: str):
|
|
131
|
+
"""Validate a filter string against provider capabilities"""
|
|
132
|
+
# Check if it's a semicolon-separated list of filters
|
|
133
|
+
if ";" in filter_string:
|
|
134
|
+
filters = filter_string.split(";")
|
|
135
|
+
click.echo(f"Validating {len(filters)} filters...")
|
|
136
|
+
all_valid = True
|
|
137
|
+
|
|
138
|
+
for i, single_filter in enumerate(filters, 1):
|
|
139
|
+
single_filter = single_filter.strip()
|
|
140
|
+
is_valid, error = parser.validate_filter(provider, single_filter)
|
|
141
|
+
|
|
142
|
+
if is_valid:
|
|
143
|
+
click.echo(f" Filter {i} is valid: {single_filter}")
|
|
144
|
+
# Parse and show the components
|
|
145
|
+
field, operator, value = parser.parse_filter_string(single_filter)
|
|
146
|
+
click.echo(f" Field: {field}, Operator: {operator}, Value: {value}")
|
|
147
|
+
else:
|
|
148
|
+
click.echo(f" Filter {i} is invalid: {single_filter}", err=True)
|
|
149
|
+
click.echo(f" Error: {error}", err=True)
|
|
150
|
+
all_valid = False
|
|
151
|
+
|
|
152
|
+
if all_valid:
|
|
153
|
+
click.echo(f'\nAll filters are valid. Complete filter string: "{filter_string}"')
|
|
154
|
+
else:
|
|
155
|
+
click.echo("\nSome filters are invalid. Please fix the errors above.", err=True)
|
|
156
|
+
else:
|
|
157
|
+
# Single filter validation (original behavior)
|
|
158
|
+
is_valid, error = parser.validate_filter(provider, filter_string)
|
|
159
|
+
|
|
160
|
+
if is_valid:
|
|
161
|
+
click.echo(f"Filter is valid: {filter_string}")
|
|
162
|
+
|
|
163
|
+
# Parse and show the components
|
|
164
|
+
field, operator, value = parser.parse_filter_string(filter_string)
|
|
165
|
+
click.echo("\nParsed components:")
|
|
166
|
+
click.echo(f" Field: {field}")
|
|
167
|
+
click.echo(f" Operator: {operator} ({parser.get_operator_display_name(operator)})")
|
|
168
|
+
click.echo(f" Value: {value}")
|
|
169
|
+
else:
|
|
170
|
+
click.echo(f"Filter is invalid: {error}", err=True)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _interactive_mode_full(
|
|
174
|
+
parser: FilterParser,
|
|
175
|
+
connector: str,
|
|
176
|
+
provider: Optional[str] = None,
|
|
177
|
+
accumulated_filters: Optional[List[str]] = None,
|
|
178
|
+
):
|
|
179
|
+
"""Full interactive mode to build filters step by step"""
|
|
180
|
+
# Initialize accumulated filters on first call
|
|
181
|
+
if accumulated_filters is None:
|
|
182
|
+
accumulated_filters = []
|
|
183
|
+
|
|
184
|
+
# If no provider specified, let user choose
|
|
185
|
+
if not provider:
|
|
186
|
+
provider = _choose_provider_interactive(parser, connector)
|
|
187
|
+
if not provider:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Get operation
|
|
191
|
+
selected_operation = _select_operation(parser, provider)
|
|
192
|
+
if not selected_operation:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Get field
|
|
196
|
+
selected_field = _select_field(parser, provider, selected_operation)
|
|
197
|
+
if not selected_field:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Get operator
|
|
201
|
+
selected_operator = _select_operator(parser, selected_field)
|
|
202
|
+
if not selected_operator:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Get value
|
|
206
|
+
value = _get_filter_value(selected_field)
|
|
207
|
+
if value is None:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Build and validate filter
|
|
211
|
+
filter_str = _build_and_validate_filter(parser, provider, selected_field["name"], selected_operator, value)
|
|
212
|
+
if not filter_str:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Add to accumulated filters
|
|
216
|
+
accumulated_filters.append(filter_str)
|
|
217
|
+
|
|
218
|
+
# Show results with all accumulated filters
|
|
219
|
+
_display_filter_result(provider, accumulated_filters)
|
|
220
|
+
|
|
221
|
+
# Ask if they want to add more filters
|
|
222
|
+
click.echo("\nWould you like to add another filter?")
|
|
223
|
+
add_more = click.confirm("Add another filter", default=False)
|
|
224
|
+
|
|
225
|
+
if add_more:
|
|
226
|
+
_interactive_mode_full(
|
|
227
|
+
parser, provider.split("_")[0], provider, accumulated_filters
|
|
228
|
+
) # Recursive call with accumulated filters
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _select_operation(parser: FilterParser, provider: str) -> Optional[str]:
|
|
232
|
+
"""Select an operation for the provider"""
|
|
233
|
+
operations = parser.get_connector_operations(provider.split("_")[0]).get(provider, [])
|
|
234
|
+
if not operations:
|
|
235
|
+
click.echo(f"No operations available for {provider}")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
click.echo("\nInteractive Query Builder")
|
|
239
|
+
click.echo("=" * 60)
|
|
240
|
+
click.echo(f"Provider: {provider}")
|
|
241
|
+
|
|
242
|
+
click.echo("\nStep 1: Choose operation")
|
|
243
|
+
click.echo("-" * 30)
|
|
244
|
+
for i, op in enumerate(operations, 1):
|
|
245
|
+
click.echo(f" {i}. {op}")
|
|
246
|
+
|
|
247
|
+
operation_choice = click.prompt("\nSelect operation (number)", type=int)
|
|
248
|
+
if operation_choice < 1 or operation_choice > len(operations):
|
|
249
|
+
click.echo(INVALID_CHOICE_MSG)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
selected = operations[operation_choice - 1]
|
|
253
|
+
click.echo(f"Selected: {selected}")
|
|
254
|
+
return selected
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _select_field(parser: FilterParser, provider: str, operation: str) -> Optional[dict]:
|
|
258
|
+
"""Select a field to filter on"""
|
|
259
|
+
filters = parser.get_filters_for_provider(provider, operation)
|
|
260
|
+
if not filters:
|
|
261
|
+
click.echo(f"No filters available for operation '{operation}'")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
click.echo("\nStep 2: Choose field to filter")
|
|
265
|
+
click.echo("-" * 30)
|
|
266
|
+
for i, f in enumerate(filters, 1):
|
|
267
|
+
click.echo(f" {i}. {f['name']} ({f['type']})")
|
|
268
|
+
|
|
269
|
+
field_choice = click.prompt("\nSelect field (number)", type=int)
|
|
270
|
+
if field_choice < 1 or field_choice > len(filters):
|
|
271
|
+
click.echo(INVALID_CHOICE_MSG)
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
selected = filters[field_choice - 1]
|
|
275
|
+
click.echo(f"Selected: {selected['name']}")
|
|
276
|
+
return selected
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _select_operator(parser: FilterParser, field: dict) -> Optional[str]:
|
|
280
|
+
"""Select an operator for the field"""
|
|
281
|
+
operators = field.get("operators", [])
|
|
282
|
+
|
|
283
|
+
click.echo("\nStep 3: Choose operator")
|
|
284
|
+
click.echo("-" * 30)
|
|
285
|
+
for i, op in enumerate(operators, 1):
|
|
286
|
+
display = parser.get_operator_display_name(op)
|
|
287
|
+
click.echo(f" {i}. {op} ({display})")
|
|
288
|
+
|
|
289
|
+
operator_choice = click.prompt("\nSelect operator (number)", type=int)
|
|
290
|
+
if operator_choice < 1 or operator_choice > len(operators):
|
|
291
|
+
click.echo(INVALID_CHOICE_MSG)
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
selected = operators[operator_choice - 1]
|
|
295
|
+
click.echo(f"Selected: {selected}")
|
|
296
|
+
return selected
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _get_filter_value(field: dict) -> Optional[str]:
|
|
300
|
+
"""Get the value for the filter"""
|
|
301
|
+
click.echo("\nStep 4: Enter value")
|
|
302
|
+
click.echo("-" * 30)
|
|
303
|
+
|
|
304
|
+
if field.get("values"):
|
|
305
|
+
# Enum field - show choices
|
|
306
|
+
click.echo("Allowed values:")
|
|
307
|
+
for i, v in enumerate(field["values"], 1):
|
|
308
|
+
click.echo(f" {i}. {v}")
|
|
309
|
+
|
|
310
|
+
value_choice = click.prompt("\nSelect value (number)", type=int)
|
|
311
|
+
if value_choice < 1 or value_choice > len(field["values"]):
|
|
312
|
+
click.echo(INVALID_CHOICE_MSG)
|
|
313
|
+
return None
|
|
314
|
+
return field["values"][value_choice - 1]
|
|
315
|
+
|
|
316
|
+
# Free text/number
|
|
317
|
+
if field.get("type") == "number":
|
|
318
|
+
value = click.prompt("Enter numeric value", type=str)
|
|
319
|
+
try:
|
|
320
|
+
float(value)
|
|
321
|
+
return value
|
|
322
|
+
except ValueError:
|
|
323
|
+
click.echo("Invalid number value", err=True)
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
return click.prompt("Enter value", type=str)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _build_and_validate_filter(
|
|
330
|
+
parser: FilterParser, provider: str, field_name: str, operator: str, value: str
|
|
331
|
+
) -> Optional[str]:
|
|
332
|
+
"""Build and validate the filter string"""
|
|
333
|
+
filter_str = parser.format_filter_string(field_name, operator, value)
|
|
334
|
+
|
|
335
|
+
is_valid, error = parser.validate_filter(provider, filter_str)
|
|
336
|
+
if not is_valid:
|
|
337
|
+
click.echo(f"\nFilter validation failed: {error}", err=True)
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
return filter_str
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _display_filter_result(provider: str, filters: List[str]):
|
|
344
|
+
"""Display the created filters and command"""
|
|
345
|
+
click.echo("\n" + "=" * 60)
|
|
346
|
+
if len(filters) == 1:
|
|
347
|
+
click.echo("Filter created successfully.")
|
|
348
|
+
else:
|
|
349
|
+
click.echo(f"{len(filters)} filters created successfully.")
|
|
350
|
+
click.echo("=" * 60)
|
|
351
|
+
|
|
352
|
+
click.echo("\nFilter(s):")
|
|
353
|
+
for i, filter_str in enumerate(filters, 1):
|
|
354
|
+
click.echo(f" {i}. {filter_str}")
|
|
355
|
+
|
|
356
|
+
click.echo("\nComplete command:")
|
|
357
|
+
provider_name = provider.replace(provider.split("_")[0] + "_", "")
|
|
358
|
+
|
|
359
|
+
# Build command with semicolon-separated filters
|
|
360
|
+
if filters:
|
|
361
|
+
filter_string = ";".join(filters)
|
|
362
|
+
filter_option = f'--filter "{filter_string}"'
|
|
363
|
+
else:
|
|
364
|
+
filter_option = ""
|
|
365
|
+
|
|
366
|
+
example_cmd = f"regscale {provider.split('_')[0]} sync_{provider_name} --regscale_ssp_id <SSP_ID> {filter_option}"
|
|
367
|
+
click.echo(f" {example_cmd}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _choose_provider_interactive(parser: FilterParser, connector: str) -> Optional[str]:
|
|
371
|
+
"""Let user choose a provider interactively"""
|
|
372
|
+
providers = parser.get_providers_with_filters(connector)
|
|
373
|
+
|
|
374
|
+
# Filter out mock providers and those without click commands
|
|
375
|
+
# Mock providers end with '_mock', and we only want providers that have actual integrations
|
|
376
|
+
filtered_providers = []
|
|
377
|
+
for provider_id in providers:
|
|
378
|
+
# Skip mock providers
|
|
379
|
+
if provider_id.endswith("_mock"):
|
|
380
|
+
continue
|
|
381
|
+
# Only include providers that would have generated click commands
|
|
382
|
+
# These are the ones with actual integration configurations
|
|
383
|
+
filtered_providers.append(provider_id)
|
|
384
|
+
|
|
385
|
+
if not filtered_providers:
|
|
386
|
+
click.echo(f"No providers found with filter support for {connector}")
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
click.echo(f"\nAvailable {connector} providers with filter support:")
|
|
390
|
+
click.echo("-" * 40)
|
|
391
|
+
|
|
392
|
+
for i, provider_id in enumerate(filtered_providers, 1):
|
|
393
|
+
provider_name = provider_id.replace(f"{connector}_", "").replace("_", " ").title()
|
|
394
|
+
filter_count = len(parser.get_filters_for_provider(provider_id))
|
|
395
|
+
click.echo(f" {i}. {provider_name} ({filter_count} filters)")
|
|
396
|
+
|
|
397
|
+
choice = click.prompt("\nSelect provider (number)", type=int)
|
|
398
|
+
|
|
399
|
+
if choice < 1 or choice > len(filtered_providers):
|
|
400
|
+
click.echo(INVALID_CHOICE_MSG)
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
return filtered_providers[choice - 1]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _build_filter_interactive(
|
|
407
|
+
parser: FilterParser, provider: str, filters: List[dict], field_input: str
|
|
408
|
+
) -> Optional[str]:
|
|
409
|
+
"""Build a single filter interactively"""
|
|
410
|
+
# Find matching field
|
|
411
|
+
matching_field = _find_matching_field(filters, field_input)
|
|
412
|
+
if not matching_field:
|
|
413
|
+
click.echo(f"Field '{field_input}' not found. Use 'list' to see available fields.", err=True)
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
field_name = matching_field["name"]
|
|
417
|
+
field_type = matching_field["type"]
|
|
418
|
+
operators = matching_field.get("operators", [])
|
|
419
|
+
|
|
420
|
+
click.echo(f"\nField: {field_name} (type: {field_type})")
|
|
421
|
+
|
|
422
|
+
# Show and get operator
|
|
423
|
+
operator = _prompt_for_operator(parser, operators)
|
|
424
|
+
if not operator:
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
# Get value
|
|
428
|
+
value = _prompt_for_value(matching_field)
|
|
429
|
+
if value is None:
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
# Build and validate filter
|
|
433
|
+
return _create_validated_filter(parser, provider, field_name, operator, value)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _find_matching_field(filters: List[dict], field_input: str) -> Optional[dict]:
|
|
437
|
+
"""Find a field matching the user input"""
|
|
438
|
+
field_lower = field_input.lower()
|
|
439
|
+
for f in filters:
|
|
440
|
+
if f["name"].lower() == field_lower or f["name"].lower().endswith(f".{field_lower}"):
|
|
441
|
+
return f
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _prompt_for_operator(parser: FilterParser, operators: List[str]) -> Optional[str]:
|
|
446
|
+
"""Prompt user to select an operator"""
|
|
447
|
+
click.echo("Available operators:")
|
|
448
|
+
for op in operators:
|
|
449
|
+
display = parser.get_operator_display_name(op)
|
|
450
|
+
click.echo(f" - {op} ({display})")
|
|
451
|
+
|
|
452
|
+
return click.prompt("Operator", type=click.Choice(operators))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _prompt_for_value(field: dict) -> Optional[str]:
|
|
456
|
+
"""Prompt user for a value based on field type"""
|
|
457
|
+
if field.get("values"):
|
|
458
|
+
# Enum field
|
|
459
|
+
click.echo(f"Allowed values: {', '.join(field['values'])}")
|
|
460
|
+
return click.prompt("Value", type=click.Choice(field["values"]))
|
|
461
|
+
|
|
462
|
+
# Free text/number
|
|
463
|
+
if field.get("type") == "number":
|
|
464
|
+
value = click.prompt("Value", type=str)
|
|
465
|
+
try:
|
|
466
|
+
float(value)
|
|
467
|
+
return value
|
|
468
|
+
except ValueError:
|
|
469
|
+
click.echo("Invalid number value", err=True)
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
return click.prompt("Value", type=str)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _create_validated_filter(
|
|
476
|
+
parser: FilterParser, provider: str, field_name: str, operator: str, value: str
|
|
477
|
+
) -> Optional[str]:
|
|
478
|
+
"""Create and validate a filter string"""
|
|
479
|
+
filter_str = parser.format_filter_string(field_name, operator, value)
|
|
480
|
+
is_valid, error = parser.validate_filter(provider, filter_str)
|
|
481
|
+
|
|
482
|
+
if is_valid:
|
|
483
|
+
click.echo(f"Filter created: {filter_str}")
|
|
484
|
+
return filter_str
|
|
485
|
+
|
|
486
|
+
click.echo(f"Filter validation failed: {error}", err=True)
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _show_field_list(filters: List[dict]):
|
|
491
|
+
"""Show a compact list of available fields"""
|
|
492
|
+
click.echo("\nAvailable fields:")
|
|
493
|
+
for f in sorted(filters, key=lambda x: x["name"]):
|
|
494
|
+
ops = ", ".join(f.get("operators", []))
|
|
495
|
+
click.echo(f" - {f['name']} ({f['type']}) [{ops}]")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _show_interactive_help():
|
|
499
|
+
"""Show help for interactive mode"""
|
|
500
|
+
click.echo("\nInteractive Mode Commands:")
|
|
501
|
+
click.echo(" <field_name> - Start building a filter for this field")
|
|
502
|
+
click.echo(" list - Show all available fields")
|
|
503
|
+
click.echo(" clear - Clear all filters")
|
|
504
|
+
click.echo(" done - Finish and show the final query")
|
|
505
|
+
click.echo(" help - Show this help message")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _show_provider_info(parser: FilterParser, provider: str):
|
|
509
|
+
"""Show general information about a provider's filtering capabilities"""
|
|
510
|
+
filters = parser.get_filters_for_provider(provider)
|
|
511
|
+
operations = list(parser.filter_mapping.get(provider, {}).keys())
|
|
512
|
+
|
|
513
|
+
provider_name = provider.replace(provider.split("_")[0] + "_", "").replace("_", " ").title()
|
|
514
|
+
|
|
515
|
+
click.echo(f"\nProvider: {provider_name}")
|
|
516
|
+
click.echo(f"Provider ID: {provider}")
|
|
517
|
+
click.echo(f"Supported operations: {', '.join(operations)}")
|
|
518
|
+
click.echo(f"Total available filters: {len(filters)}")
|
|
519
|
+
|
|
520
|
+
click.echo("\nExample filters:")
|
|
521
|
+
# Show a few example filters
|
|
522
|
+
for f in filters[:3]:
|
|
523
|
+
example_value = "value"
|
|
524
|
+
if f["type"] == "number":
|
|
525
|
+
example_value = "100"
|
|
526
|
+
elif f.get("values"):
|
|
527
|
+
example_value = f["values"][0]
|
|
528
|
+
|
|
529
|
+
example = parser.format_filter_string(f["name"], f["operators"][0], example_value)
|
|
530
|
+
click.echo(f" {example}")
|
|
531
|
+
|
|
532
|
+
click.echo("\nUse --list-fields to see all available fields")
|
|
533
|
+
click.echo("Use --validate '<filter>' to validate a filter string")
|