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.

Files changed (32) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/api.py +1 -1
  3. regscale/core/app/application.py +5 -3
  4. regscale/core/app/internal/evidence.py +308 -202
  5. regscale/dev/code_gen.py +84 -3
  6. regscale/integrations/commercial/__init__.py +2 -0
  7. regscale/integrations/commercial/jira.py +95 -22
  8. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  9. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  10. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  11. regscale/integrations/commercial/synqly/assets.py +99 -16
  12. regscale/integrations/commercial/synqly/query_builder.py +533 -0
  13. regscale/integrations/commercial/synqly/vulnerabilities.py +134 -14
  14. regscale/integrations/commercial/wizv2/click.py +23 -0
  15. regscale/integrations/commercial/wizv2/compliance_report.py +137 -26
  16. regscale/integrations/compliance_integration.py +247 -5
  17. regscale/integrations/scanner_integration.py +16 -0
  18. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  19. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +12 -2
  20. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  21. regscale/models/integration_models/synqly_models/synqly_model.py +47 -3
  22. regscale/models/regscale_models/compliance_settings.py +28 -0
  23. regscale/models/regscale_models/component.py +1 -0
  24. regscale/models/regscale_models/control_implementation.py +143 -4
  25. regscale/regscale.py +1 -1
  26. regscale/validation/record.py +23 -1
  27. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/METADATA +9 -9
  28. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +32 -30
  29. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
  30. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
  31. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
  32. {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")