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
regscale/dev/code_gen.py CHANGED
@@ -7,9 +7,13 @@ if TYPE_CHECKING:
7
7
 
8
8
  from regscale.models.integration_models.synqly_models.connector_types import ConnectorType
9
9
  from regscale.models.integration_models.synqly_models.param import Param
10
+ from regscale.models.integration_models.synqly_models.filter_parser import FilterParser
10
11
 
11
12
  SUPPORTED_CONNECTORS = [ConnectorType.Ticketing, ConnectorType.Vulnerabilities, ConnectorType.Assets, ConnectorType.Edr]
12
13
 
14
+ # Initialize FilterParser once at module level
15
+ filter_parser = FilterParser()
16
+
13
17
 
14
18
  def generate_dags() -> None:
15
19
  """Generate Airflow DAGs for the platform"""
@@ -189,6 +193,22 @@ def _build_op_kwargs_and_docstring(
189
193
  ),
190
194
  }
191
195
  config[param_type] = {**config[param_type], **vuln_params}
196
+
197
+ # Add filter parameter for Assets and Vulnerabilities connectors
198
+ if (
199
+ ConnectorType.Assets.lower() in integration or ConnectorType.Vulnerabilities.lower() in integration
200
+ ) and param_type == "optional_params":
201
+ # Use 'asset_filter' for vulnerabilities, 'filter' for assets
202
+ param_name = "asset_filter" if ConnectorType.Vulnerabilities.lower() in integration else "filter"
203
+ filter_param = {
204
+ param_name: Param(
205
+ name=param_name,
206
+ type="string",
207
+ description="Semicolon separated filters of format filter[operator]value",
208
+ default=None,
209
+ )
210
+ }
211
+ config[param_type] = {**config.get(param_type, {}), **filter_param}
192
212
  if config.get(param_type):
193
213
  if proper_type not in doc_string:
194
214
  doc_string += f"{proper_type}:\n"
@@ -290,6 +310,7 @@ def add_connector_specific_params(
290
310
  sync_attachments_jinja = "'sync_attachments'"
291
311
  op_kwargs += f'\n "sync_attachments": "{{{{ dag_run.conf[{sync_attachments_jinja}] if {sync_attachments_jinja} in dag_run.conf else False }}}}",'
292
312
  doc_string += " sync_attachments: BOOLEAN Whether to sync attachments between integration and RegScale\n"
313
+
293
314
  return op_kwargs, doc_string
294
315
 
295
316
 
@@ -353,6 +374,44 @@ def {connector}() -> None:
353
374
  pass
354
375
  """
355
376
 
377
+ # Add build-query command for Assets and Vulnerabilities connectors
378
+ if connector in [ConnectorType.Assets, ConnectorType.Vulnerabilities]:
379
+ cli_code += f"""
380
+
381
+ @{connector}.command(name="build-query")
382
+ @click.option(
383
+ '--provider',
384
+ required=False,
385
+ help='Provider ID (e.g., {connector}_armis_centrix). If not specified, starts interactive mode.'
386
+ )
387
+ @click.option(
388
+ '--validate',
389
+ help='Validate a filter string against provider capabilities'
390
+ )
391
+ @click.option(
392
+ '--list-fields',
393
+ is_flag=True,
394
+ default=False,
395
+ help='List all available fields for the provider'
396
+ )
397
+ def build_query(provider, validate, list_fields):
398
+ \"\"\"
399
+ Build and validate filter queries for {connector.capitalize()} connectors.
400
+
401
+ Examples:
402
+ # Build a filter query
403
+ regscale {connector} build-query
404
+
405
+ # List all fields for a specific provider
406
+ regscale {connector} build-query --provider {connector}_armis_centrix --list-fields
407
+
408
+ # Validate a filter string
409
+ regscale {connector} build-query --provider {connector}_armis_centrix --validate "device.ip[eq]192.168.1.1"
410
+ \"\"\"
411
+ from regscale.integrations.commercial.synqly.query_builder import handle_build_query
412
+ handle_build_query('{connector}', provider, validate, list_fields)
413
+ """
414
+
356
415
  # replace the integration config with a flattened version
357
416
  for integration, config in integration_configs.items():
358
417
  capabilities = config.get("capabilities", [])
@@ -363,6 +422,7 @@ def {connector}() -> None:
363
422
  connector=connector,
364
423
  integration_name=integration_name,
365
424
  capabilities=capabilities,
425
+ provider_id=integration, # Pass the full provider ID for filter support
366
426
  )
367
427
  cli_code += f"\n\n{click_options_and_command}"
368
428
  integrations_count += 1
@@ -374,12 +434,15 @@ def {connector}() -> None:
374
434
  print(f"Generated click commands for {integrations_count} {connector} connector(s).")
375
435
 
376
436
 
377
- def _build_all_params(integration_name: str, connector: str) -> tuple[list[str], list[str], list[str]]:
437
+ def _build_all_params(
438
+ integration_name: str, connector: str, provider_id: str = None
439
+ ) -> tuple[list[str], list[str], list[str]]:
378
440
  """
379
441
  Function to build the click options, function params, and function kwargs for the integration
380
442
 
381
443
  :param str integration_name: The name of the integration
382
444
  :param str connector: The connector type
445
+ :param str provider_id: The provider ID for filter support (e.g., 'assets_armis_centrix')
383
446
  :return: The click options, function params, and function kwargs
384
447
  :rtype: tuple[list[str], list[str], list[str]]
385
448
  """
@@ -395,19 +458,36 @@ def _build_all_params(integration_name: str, connector: str) -> tuple[list[str],
395
458
  "scan_date=scan_date",
396
459
  "all_scans=all_scans",
397
460
  ]
461
+
462
+ # Add filter option if provider supports filtering
463
+ if provider_id and filter_parser.has_filters(provider_id):
464
+ filter_option = "@click.option(\n '--asset_filter',\n help='STRING: Apply filters to asset queries. Can be a single filter \"field[operator]value\" or semicolon-separated filters \"field1[op]value1;field2[op]value2\"',\n required=False,\n type=str,\n default=None)\n"
465
+ click_options.append(filter_option)
466
+ function_params.append("asset_filter: str")
467
+ function_kwargs.append("filter=asset_filter.split(';') if asset_filter else []")
468
+
398
469
  elif connector == ConnectorType.Ticketing:
399
470
  click_options = ["@regscale_id()", "@regscale_module()"]
400
471
  function_params = ["regscale_id: int", "regscale_module: str"]
401
472
  function_kwargs = ["regscale_id=regscale_id", "regscale_module=regscale_module"]
402
473
  else:
474
+ # Assets and other connectors
403
475
  click_options = ["@regscale_ssp_id()"]
404
476
  function_params = ["regscale_ssp_id: int"]
405
477
  function_kwargs = ["regscale_ssp_id=regscale_ssp_id"]
478
+
479
+ # Add filter option for Assets if provider supports filtering
480
+ if connector == ConnectorType.Assets and provider_id and filter_parser.has_filters(provider_id):
481
+ filter_option = "@click.option(\n '--filter',\n help='STRING: Apply filters to the query. Can be a single filter \"field[operator]value\" or semicolon-separated filters \"field1[op]value1;field2[op]value2\"',\n required=False,\n type=str,\n default=None)\n"
482
+ click_options.append(filter_option)
483
+ function_params.append("filter: str")
484
+ function_kwargs.append("filter=filter.split(';') if filter else []")
485
+
406
486
  return click_options, function_params, function_kwargs
407
487
 
408
488
 
409
489
  def _build_click_options_and_command(
410
- config: dict, connector: str, integration_name: str, capabilities: list[str]
490
+ config: dict, connector: str, integration_name: str, capabilities: list[str], provider_id: str = None
411
491
  ) -> str:
412
492
  """
413
493
  Function to use the config to build the click options and command for the integration
@@ -416,12 +496,13 @@ def _build_click_options_and_command(
416
496
  :param str connector: The connector type
417
497
  :param str integration_name: The name of the integration
418
498
  :param list[str] capabilities: The capabilities of the integration
499
+ :param str provider_id: The provider ID for filter support (e.g., 'assets_armis_centrix')
419
500
  :return: The click options as a string
420
501
  :rtype: str
421
502
  """
422
503
  doc_string_name = integration_name.replace("_", " ").title()
423
504
  # add regscale_ssp_id as a default option
424
- click_options, function_params, function_kwargs = _build_all_params(doc_string_name, connector)
505
+ click_options, function_params, function_kwargs = _build_all_params(doc_string_name, connector, provider_id)
425
506
  for param_type in ["expected_params", "optional_params"]:
426
507
  for param in config.get(param_type, []):
427
508
  param_data = config[param_type][param]
@@ -118,6 +118,8 @@ def crowdstrike():
118
118
  "sync_cloud_alerts": "regscale.integrations.commercial.microsoft_defender.defender.sync_cloud_alerts",
119
119
  "sync_cloud_recommendations": "regscale.integrations.commercial.microsoft_defender.defender.sync_cloud_recommendations",
120
120
  "import_alerts": "regscale.integrations.commercial.microsoft_defender.defender.import_alerts",
121
+ "collect_entra_evidence": "regscale.integrations.commercial.microsoft_defender.defender.collect_entra_evidence",
122
+ "show_entra_mappings": "regscale.integrations.commercial.microsoft_defender.defender.show_entra_mappings",
121
123
  },
122
124
  name="defender",
123
125
  )
@@ -719,10 +719,7 @@ def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
719
719
  parent_module=parent_module,
720
720
  )
721
721
  # create the issue in RegScale
722
- if regscale_issue := Issue.insert_issue(
723
- app=app,
724
- issue=issue,
725
- ):
722
+ if regscale_issue := issue.create():
726
723
  logger.debug(
727
724
  "Created issue #%i-%s in RegScale.",
728
725
  regscale_issue.id,
@@ -812,7 +809,7 @@ def fetch_jira_objects(
812
809
  jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, sync_tasks_only: bool = False
813
810
  ) -> list[jiraIssue]:
814
811
  """
815
- Fetch all issues from Jira for the provided project
812
+ Fetch all issues from Jira for the provided project using the enhanced search API.
816
813
 
817
814
  :param JIRA jira_client: Jira client to use for the request
818
815
  :param str jira_project: Name of the project in Jira
@@ -822,15 +819,77 @@ def fetch_jira_objects(
822
819
  :return: List of Jira issues
823
820
  :rtype: list[jiraIssue]
824
821
  """
825
- start_pointer = 0
826
- page_size = 100
827
- jira_objects = []
828
822
  if sync_tasks_only:
829
823
  validate_issue_type(jira_client, jira_issue_type)
830
824
  output_str = "task"
831
825
  else:
832
826
  output_str = "issue"
833
827
  logger.info("Fetching %s(s) from Jira...", output_str.lower())
828
+ try:
829
+ max_results = 100 # 100 is the max allowed by Jira
830
+ jira_issues = []
831
+ issue_response = jira_client.enhanced_search_issues(
832
+ jql_str=jql_str or f"project = {jira_project}",
833
+ maxResults=max_results,
834
+ )
835
+ jira_issues.extend(issue_response)
836
+ logger.info(
837
+ "%i Jira %s(s) retrieved.",
838
+ len(jira_issues),
839
+ output_str.lower(),
840
+ )
841
+ # Handle pagination if there are more issues to fetch
842
+ while issue_response.nextPageToken:
843
+ issue_response = jira_client.enhanced_search_issues(
844
+ jql_str=jql_str, maxResults=max_results, nextPageToken=issue_response.nextPageToken
845
+ )
846
+ jira_issues.extend(issue_response)
847
+ logger.info(
848
+ "%i Jira %s(s) retrieved.",
849
+ len(jira_issues),
850
+ output_str.lower(),
851
+ )
852
+ # Save artifacts file and log final result if we have issues
853
+ if jira_issues:
854
+ save_jira_issues(jira_issues, jira_project, jira_issue_type)
855
+ logger.info("%i %s(s) retrieved from Jira.", len(jira_issues), output_str.lower())
856
+ return jira_issues
857
+ except Exception as e:
858
+ logger.warning(
859
+ "An error occurred while fetching Jira issues using the enhanced_search_issues method: %s", str(e)
860
+ )
861
+ logger.info("Falling back to the deprecated fetch method...")
862
+
863
+ try:
864
+ return deprecated_fetch_jira_objects(
865
+ jira_client=jira_client,
866
+ jira_project=jira_project,
867
+ jira_issue_type=jira_issue_type,
868
+ jql_str=jql_str,
869
+ output_str=output_str,
870
+ )
871
+ except JIRAError as e:
872
+ error_and_exit(f"Unable to fetch issues from Jira: {e}")
873
+
874
+
875
+ def deprecated_fetch_jira_objects(
876
+ jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, output_str: str = "issue"
877
+ ) -> list[jiraIssue]:
878
+ """
879
+ Fetch all issues from Jira for the provided project using the old API method, used as a fallback method.
880
+
881
+ :param JIRA jira_client: Jira client to use for the request
882
+ :param str jira_project: Name of the project in Jira
883
+ :param str jira_issue_type: Type of issue to fetch from Jira
884
+ :param str jql_str: JQL string to use for the request, default None
885
+ :param str output_str: String to use for logging, either "issue" or "task"
886
+ :return: List of Jira issues
887
+ :rtype: list[jiraIssue]
888
+ """
889
+ start_pointer = 0
890
+ page_size = 100
891
+ jira_objects = []
892
+ logger.info("Fetching %s(s) from Jira...", output_str.lower())
834
893
  # get all issues for the Jira project
835
894
  while True:
836
895
  start = start_pointer * page_size
@@ -851,24 +910,36 @@ def fetch_jira_objects(
851
910
  output_str.lower(),
852
911
  )
853
912
  if jira_objects:
854
- check_file_path("artifacts")
855
- file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
856
- file_path = Path(f"./artifacts/{file_name}")
857
- save_data_to(
858
- file=file_path,
859
- data=[issue.raw for issue in jira_objects],
860
- output_log=False,
861
- )
862
- logger.info(
863
- "Saved %i Jira %s(s), see %s",
864
- len(jira_objects),
865
- jira_issue_type.lower(),
866
- str(file_path.absolute()),
867
- )
913
+ save_jira_issues(jira_objects, jira_project, jira_issue_type)
868
914
  logger.info("%i %s(s) retrieved from Jira.", len(jira_objects), output_str.lower())
869
915
  return jira_objects
870
916
 
871
917
 
918
+ def save_jira_issues(jira_issues: list[jiraIssue], jira_project: str, jira_issue_type: str) -> None:
919
+ """
920
+ Save Jira issues to a JSON file in the artifacts directory
921
+
922
+ :param list[jiraIssue] jira_issues: List of Jira issues to save
923
+ :param str jira_project: Name of the project in Jira
924
+ :param str jira_issue_type: Type of issue to fetch from Jira
925
+ :rtype: None
926
+ """
927
+ check_file_path("artifacts")
928
+ file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
929
+ file_path = Path(f"./artifacts/{file_name}")
930
+ save_data_to(
931
+ file=file_path,
932
+ data=[issue.raw for issue in jira_issues],
933
+ output_log=False,
934
+ )
935
+ logger.info(
936
+ "Saved %i Jira %s(s), see %s",
937
+ len(jira_issues),
938
+ jira_issue_type.lower(),
939
+ str(file_path.absolute()),
940
+ )
941
+
942
+
872
943
  def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str) -> Issue:
873
944
  """
874
945
  Map Jira issues to RegScale issues
@@ -893,6 +964,8 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
893
964
  ),
894
965
  status=("Closed" if jira_issue.fields.status.name.lower() == "done" else config["issues"]["jira"]["status"]),
895
966
  jiraId=jira_issue.key,
967
+ identification="Jira Sync",
968
+ sourceReport="Jira",
896
969
  parentId=parent_id,
897
970
  parentModule=parent_module,
898
971
  dateCreated=get_current_datetime(),