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
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(
|
|
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 :=
|
|
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
|
-
|
|
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(),
|