regscale-cli 6.24.0.1__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/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.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +30 -28
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.24.0.1.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
|
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""RegScale Microsoft Defender recommendations and alerts integration"""
|
|
4
4
|
# standard python imports
|
|
5
|
+
|
|
5
6
|
import logging
|
|
6
7
|
from datetime import datetime, timedelta
|
|
7
8
|
from os import PathLike
|
|
@@ -10,6 +11,7 @@ from typing import Literal, Optional, Tuple, Union
|
|
|
10
11
|
|
|
11
12
|
import click
|
|
12
13
|
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
13
15
|
|
|
14
16
|
from regscale.core.app.api import Api
|
|
15
17
|
from regscale.core.app.internal.login import is_valid
|
|
@@ -24,6 +26,7 @@ from regscale.core.app.utils.app_utils import (
|
|
|
24
26
|
uncamel_case,
|
|
25
27
|
)
|
|
26
28
|
from regscale.integrations.commercial.microsoft_defender.defender_api import DefenderApi
|
|
29
|
+
from regscale.integrations.commercial.microsoft_defender.defender_constants import EVIDENCE_TO_CONTROLS_MAPPING
|
|
27
30
|
from regscale.models import File, Issue, regscale_id, regscale_module, ssp_or_component_id
|
|
28
31
|
from regscale.models.app_models.click import NotRequiredIf
|
|
29
32
|
from regscale.models.integration_models.defender_data import DefenderData
|
|
@@ -68,12 +71,12 @@ def defender():
|
|
|
68
71
|
@defender.command(name="authenticate")
|
|
69
72
|
@click.option(
|
|
70
73
|
"--system",
|
|
71
|
-
type=click.Choice(["cloud", "365"], case_sensitive=False),
|
|
72
|
-
help="Pull recommendations from Microsoft Defender 365
|
|
74
|
+
type=click.Choice(["cloud", "365", "entra"], case_sensitive=False),
|
|
75
|
+
help="Pull recommendations from Microsoft Defender 365, Microsoft Defender for Cloud, or Azure Entra.",
|
|
73
76
|
prompt="Please choose a system",
|
|
74
77
|
required=True,
|
|
75
78
|
)
|
|
76
|
-
def authenticate_in_defender(system: Literal["cloud", "365"]):
|
|
79
|
+
def authenticate_in_defender(system: Literal["cloud", "365", "entra"]):
|
|
77
80
|
"""Obtains an access token using the credentials provided in init.yaml."""
|
|
78
81
|
authenticate(system=system)
|
|
79
82
|
|
|
@@ -225,6 +228,85 @@ def import_alerts(
|
|
|
225
228
|
)
|
|
226
229
|
|
|
227
230
|
|
|
231
|
+
@defender.command(name="collect_entra_evidence")
|
|
232
|
+
@ssp_or_component_id()
|
|
233
|
+
@click.option(
|
|
234
|
+
"--days_back",
|
|
235
|
+
"-d",
|
|
236
|
+
type=click.INT,
|
|
237
|
+
help="Number of days back to collect audit logs",
|
|
238
|
+
default=30,
|
|
239
|
+
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--evidence_type",
|
|
242
|
+
"-t",
|
|
243
|
+
type=click.Choice(
|
|
244
|
+
["all", "users_groups", "rbac_pim", "conditional_access", "authentication", "audit_logs", "access_reviews"],
|
|
245
|
+
case_sensitive=False,
|
|
246
|
+
),
|
|
247
|
+
help="Type of evidence to collect",
|
|
248
|
+
default="all",
|
|
249
|
+
)
|
|
250
|
+
def collect_entra_evidence(regscale_ssp_id: int, component_id: int, days_back: int, evidence_type: str):
|
|
251
|
+
"""
|
|
252
|
+
Collect Azure Entra evidence for FedRAMP compliance controls and upload to RegScale
|
|
253
|
+
"""
|
|
254
|
+
# Validate parent module for evidence collection
|
|
255
|
+
from regscale.validation.record import validate_component_or_ssp
|
|
256
|
+
|
|
257
|
+
validate_component_or_ssp(ssp_id=regscale_ssp_id, component_id=component_id)
|
|
258
|
+
parent_id = regscale_ssp_id or component_id
|
|
259
|
+
parent_module = "securityplans" if regscale_ssp_id else "components"
|
|
260
|
+
|
|
261
|
+
collect_and_upload_entra_evidence(
|
|
262
|
+
parent_id=parent_id, parent_module=parent_module, days_back=days_back, evidence_type=evidence_type
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@defender.command(name="show_entra_mappings")
|
|
267
|
+
@click.option(
|
|
268
|
+
"--evidence_type",
|
|
269
|
+
"-t",
|
|
270
|
+
type=click.Choice(
|
|
271
|
+
["all", "users_groups", "rbac_pim", "conditional_access", "authentication", "audit_logs", "access_reviews"],
|
|
272
|
+
case_sensitive=False,
|
|
273
|
+
),
|
|
274
|
+
help="Show mappings for specific evidence type",
|
|
275
|
+
default="all",
|
|
276
|
+
)
|
|
277
|
+
def show_entra_mappings(evidence_type: str):
|
|
278
|
+
"""
|
|
279
|
+
Show which FedRAMP controls are mapped to each Azure Entra evidence type
|
|
280
|
+
"""
|
|
281
|
+
if evidence_type == "all":
|
|
282
|
+
evidence_types_to_show = EVIDENCE_TO_CONTROLS_MAPPING.keys()
|
|
283
|
+
else:
|
|
284
|
+
# Map category to specific evidence types
|
|
285
|
+
category_to_evidence = {
|
|
286
|
+
"users_groups": ["users", "guest_users", "security_groups"],
|
|
287
|
+
"rbac_pim": ["role_assignments", "role_definitions", "pim_assignments", "pim_eligibility"],
|
|
288
|
+
"conditional_access": ["conditional_access"],
|
|
289
|
+
"authentication": ["auth_methods_policy", "user_mfa_registration", "mfa_registered_users"],
|
|
290
|
+
"audit_logs": ["sign_in_logs", "directory_audits", "provisioning_logs"],
|
|
291
|
+
"access_reviews": ["access_review_definitions"],
|
|
292
|
+
}
|
|
293
|
+
evidence_types_to_show = category_to_evidence.get(evidence_type, [evidence_type])
|
|
294
|
+
# create a table using rich and add a row for each evidence type
|
|
295
|
+
table = Table(title="Azure Entra Evidence to FedRAMP Controls Mapping", show_lines=True)
|
|
296
|
+
table.add_column("Evidence Type", style="#10c4d3")
|
|
297
|
+
table.add_column("Controls", style="#18a8e9")
|
|
298
|
+
table.add_column("Total Controls", style="#ff9d20")
|
|
299
|
+
for evidence_key in evidence_types_to_show:
|
|
300
|
+
if evidence_key in EVIDENCE_TO_CONTROLS_MAPPING:
|
|
301
|
+
controls = EVIDENCE_TO_CONTROLS_MAPPING[evidence_key]
|
|
302
|
+
table.add_row(evidence_key.replace("_", " ").title(), ", ".join(controls), str(len(controls)))
|
|
303
|
+
console.print(table)
|
|
304
|
+
|
|
305
|
+
console.print(
|
|
306
|
+
"[dim]Use 'regscale defender collect_entra_evidence' to collect and upload evidence to these controls[/dim]"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
228
310
|
def import_defender_alerts(
|
|
229
311
|
folder_path: PathLike[str],
|
|
230
312
|
regscale_ssp_id: int,
|
|
@@ -268,11 +350,11 @@ def import_defender_alerts(
|
|
|
268
350
|
)
|
|
269
351
|
|
|
270
352
|
|
|
271
|
-
def authenticate(system: Literal["cloud", "365"]) -> None:
|
|
353
|
+
def authenticate(system: Literal["cloud", "365", "entra"]) -> None:
|
|
272
354
|
"""
|
|
273
355
|
Obtains an access token using the credentials provided in init.yaml
|
|
274
356
|
|
|
275
|
-
:param Literal["cloud", "365"] system: The system to authenticate for, either Defender 365
|
|
357
|
+
:param Literal["cloud", "365", "entra"] system: The system to authenticate for, either Defender 365, Defender for Cloud, or Azure Entra
|
|
276
358
|
:rtype: None
|
|
277
359
|
"""
|
|
278
360
|
_ = check_license()
|
|
@@ -947,3 +1029,242 @@ def fetch_save_and_upload_query(
|
|
|
947
1029
|
api=defender_api.api,
|
|
948
1030
|
):
|
|
949
1031
|
logger.info(f"Successfully uploaded {file_path.name} to {parent_module} #{parent_id} in RegScale.")
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def collect_and_upload_entra_evidence(
|
|
1035
|
+
parent_id: int, parent_module: str, days_back: int = 30, evidence_type: str = "all"
|
|
1036
|
+
) -> None:
|
|
1037
|
+
"""
|
|
1038
|
+
Collect Azure Entra evidence for FedRAMP compliance controls and upload to RegScale
|
|
1039
|
+
|
|
1040
|
+
:param int parent_id: The RegScale ID to upload evidence to
|
|
1041
|
+
:param str parent_module: The RegScale module to upload evidence to
|
|
1042
|
+
:param int days_back: Number of days back to collect audit logs
|
|
1043
|
+
:param str evidence_type: Type of evidence to collect
|
|
1044
|
+
:rtype: None
|
|
1045
|
+
"""
|
|
1046
|
+
app = check_license()
|
|
1047
|
+
api = Api()
|
|
1048
|
+
|
|
1049
|
+
if not is_valid(app=app):
|
|
1050
|
+
error_and_exit(LOGIN_ERROR)
|
|
1051
|
+
|
|
1052
|
+
logger.info(f"Starting Azure Entra evidence collection for {evidence_type}...")
|
|
1053
|
+
|
|
1054
|
+
defender_api = DefenderApi(system="entra")
|
|
1055
|
+
|
|
1056
|
+
try:
|
|
1057
|
+
if evidence_type == "all":
|
|
1058
|
+
evidence_data = defender_api.collect_all_entra_evidence(days_back=days_back)
|
|
1059
|
+
else:
|
|
1060
|
+
evidence_data = collect_specific_evidence_type(defender_api, evidence_type, days_back)
|
|
1061
|
+
|
|
1062
|
+
upload_evidence_files(evidence_data, parent_id, parent_module, api, evidence_type)
|
|
1063
|
+
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
error_and_exit(f"Error collecting Azure Entra evidence: {e}")
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def collect_specific_evidence_type(
|
|
1069
|
+
defender_api: DefenderApi, evidence_type: str, days_back: int
|
|
1070
|
+
) -> dict[str, list[Path]]:
|
|
1071
|
+
"""
|
|
1072
|
+
Collect specific type of Azure Entra evidence
|
|
1073
|
+
|
|
1074
|
+
:param DefenderApi defender_api: The Defender API instance
|
|
1075
|
+
:param str evidence_type: Type of evidence to collect
|
|
1076
|
+
:param int days_back: Number of days back for audit logs
|
|
1077
|
+
:return: Dictionary containing evidence data and file paths to saved csv evidence files
|
|
1078
|
+
:rtype: dict[str, list[Path]]
|
|
1079
|
+
"""
|
|
1080
|
+
evidence_data = {}
|
|
1081
|
+
start_date = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%dT00:00:00Z")
|
|
1082
|
+
|
|
1083
|
+
if evidence_type == "users_groups":
|
|
1084
|
+
evidence_data["users"] = defender_api.get_and_save_entra_evidence("users")
|
|
1085
|
+
evidence_data["guest_users"] = defender_api.get_and_save_entra_evidence("guest_users")
|
|
1086
|
+
evidence_data["groups_and_members"] = defender_api.get_and_save_entra_evidence("groups_and_members")
|
|
1087
|
+
evidence_data["security_groups"] = defender_api.get_and_save_entra_evidence("security_groups")
|
|
1088
|
+
|
|
1089
|
+
elif evidence_type == "rbac_pim":
|
|
1090
|
+
evidence_data["role_assignments"] = defender_api.get_and_save_entra_evidence("role_assignments")
|
|
1091
|
+
evidence_data["role_definitions"] = defender_api.get_and_save_entra_evidence("role_definitions")
|
|
1092
|
+
evidence_data["pim_assignments"] = defender_api.get_and_save_entra_evidence("pim_assignments")
|
|
1093
|
+
evidence_data["pim_eligibility"] = defender_api.get_and_save_entra_evidence("pim_eligibility")
|
|
1094
|
+
|
|
1095
|
+
elif evidence_type == "conditional_access":
|
|
1096
|
+
evidence_data["conditional_access"] = defender_api.get_and_save_entra_evidence("conditional_access")
|
|
1097
|
+
|
|
1098
|
+
elif evidence_type == "authentication":
|
|
1099
|
+
evidence_data["auth_methods_policy"] = defender_api.get_and_save_entra_evidence("auth_methods_policy")
|
|
1100
|
+
evidence_data["user_mfa_registration"] = defender_api.get_and_save_entra_evidence("user_mfa_registration")
|
|
1101
|
+
evidence_data["mfa_registered_users"] = defender_api.get_and_save_entra_evidence("mfa_registered_users")
|
|
1102
|
+
|
|
1103
|
+
elif evidence_type == "audit_logs":
|
|
1104
|
+
evidence_data["sign_in_logs"] = defender_api.get_and_save_entra_evidence("sign_in_logs", start_date=start_date)
|
|
1105
|
+
evidence_data["directory_audits"] = defender_api.get_and_save_entra_evidence(
|
|
1106
|
+
"directory_audits", start_date=start_date
|
|
1107
|
+
)
|
|
1108
|
+
evidence_data["provisioning_logs"] = defender_api.get_and_save_entra_evidence(
|
|
1109
|
+
"provisioning_logs", start_date=start_date
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
elif evidence_type == "access_reviews":
|
|
1113
|
+
evidence_data["access_review_definitions"] = defender_api.collect_entra_access_reviews()
|
|
1114
|
+
|
|
1115
|
+
return evidence_data
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def get_control_implementations_map(parent_id: int, parent_module: str) -> dict[str, int]:
|
|
1119
|
+
"""
|
|
1120
|
+
Get a mapping of control identifiers to control implementation IDs
|
|
1121
|
+
|
|
1122
|
+
:param int parent_id: RegScale parent ID
|
|
1123
|
+
:param str parent_module: RegScale parent module
|
|
1124
|
+
:return: Dictionary mapping control identifiers (e.g., "AC-2") to control implementation IDs
|
|
1125
|
+
:rtype: dict[str, int]
|
|
1126
|
+
"""
|
|
1127
|
+
from regscale.models import ControlImplementation
|
|
1128
|
+
|
|
1129
|
+
try:
|
|
1130
|
+
control_implementations = ControlImplementation.get_list_by_parent(parent_id, parent_module)
|
|
1131
|
+
if not control_implementations:
|
|
1132
|
+
logger.warning(f"No control implementations found for {parent_module} #{parent_id}")
|
|
1133
|
+
return {}
|
|
1134
|
+
|
|
1135
|
+
control_map = {}
|
|
1136
|
+
for control_impl in control_implementations:
|
|
1137
|
+
# Try to get control identifier from the control object
|
|
1138
|
+
control_id = control_impl.get("controlId") if isinstance(control_impl, dict) else control_impl.controlId
|
|
1139
|
+
id_number = control_impl.get("id") if isinstance(control_impl, dict) else control_impl.id
|
|
1140
|
+
control_map[control_id] = id_number
|
|
1141
|
+
logger.debug(f"Mapped control #{id_number}: {control_id} to implementation.")
|
|
1142
|
+
|
|
1143
|
+
logger.info(f"Found {len(control_map)} control implementations for evidence mapping")
|
|
1144
|
+
return control_map
|
|
1145
|
+
|
|
1146
|
+
except Exception as e:
|
|
1147
|
+
logger.error(f"Error fetching control implementations: {e}")
|
|
1148
|
+
return {}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def upload_evidence_to_controls(
|
|
1152
|
+
evidence_key: str,
|
|
1153
|
+
evidence_file_list: list[Path],
|
|
1154
|
+
control_implementations_map: dict[str, int],
|
|
1155
|
+
api: Api,
|
|
1156
|
+
) -> int:
|
|
1157
|
+
"""
|
|
1158
|
+
Upload evidence file to specific control implementations
|
|
1159
|
+
|
|
1160
|
+
:param str evidence_key: Type of evidence (e.g., "users", "sign_in_logs")
|
|
1161
|
+
:param list evidence_file_list: List of evidence files
|
|
1162
|
+
:param dict control_implementations_map: Map of control identifiers to implementation IDs
|
|
1163
|
+
:param Api api: API instance
|
|
1164
|
+
:return: Number of successful uploads
|
|
1165
|
+
:rtype: int
|
|
1166
|
+
"""
|
|
1167
|
+
# Get the controls this evidence type maps to
|
|
1168
|
+
mapped_controls = EVIDENCE_TO_CONTROLS_MAPPING.get(evidence_key, [])
|
|
1169
|
+
if not mapped_controls:
|
|
1170
|
+
logger.warning(f"No control mapping found for evidence type: {evidence_key}")
|
|
1171
|
+
return 0
|
|
1172
|
+
|
|
1173
|
+
# Write evidence data to CSV file
|
|
1174
|
+
successful_uploads = 0
|
|
1175
|
+
for file_path in evidence_file_list:
|
|
1176
|
+
controls_uploaded_to = []
|
|
1177
|
+
file_name = file_path.name
|
|
1178
|
+
|
|
1179
|
+
for control_identifier in mapped_controls:
|
|
1180
|
+
if control_identifier in control_implementations_map:
|
|
1181
|
+
control_impl_id = control_implementations_map[control_identifier]
|
|
1182
|
+
|
|
1183
|
+
# Upload file to specific control implementation
|
|
1184
|
+
if File.upload_file_to_regscale(
|
|
1185
|
+
file_name=file_path.absolute(), # type: ignore
|
|
1186
|
+
parent_id=control_impl_id,
|
|
1187
|
+
parent_module="controls",
|
|
1188
|
+
api=api,
|
|
1189
|
+
):
|
|
1190
|
+
successful_uploads += 1
|
|
1191
|
+
controls_uploaded_to.append(control_identifier)
|
|
1192
|
+
logger.debug(
|
|
1193
|
+
f"Successfully uploaded {file_name} to control {control_identifier} (ID: {control_impl_id})"
|
|
1194
|
+
)
|
|
1195
|
+
else:
|
|
1196
|
+
logger.error(
|
|
1197
|
+
f"Failed to upload {file_name} to control {control_identifier} (ID: {control_impl_id})"
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
if controls_uploaded_to:
|
|
1201
|
+
logger.info(
|
|
1202
|
+
f"Successfully uploaded {file_name} to {len(controls_uploaded_to)} controls: {', '.join(controls_uploaded_to)}"
|
|
1203
|
+
)
|
|
1204
|
+
else:
|
|
1205
|
+
logger.warning(f"No matching control implementations found for {evidence_key} evidence")
|
|
1206
|
+
|
|
1207
|
+
return successful_uploads
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def upload_evidence_files(
|
|
1211
|
+
evidence_data: dict[str, list[Path]], parent_id: int, parent_module: str, api: Api, evidence_type: str
|
|
1212
|
+
) -> None:
|
|
1213
|
+
"""
|
|
1214
|
+
Upload evidence files to specific RegScale control implementations
|
|
1215
|
+
|
|
1216
|
+
:param dict[str, list[Path]] evidence_data: Dictionary containing evidence data
|
|
1217
|
+
:param int parent_id: RegScale parent ID
|
|
1218
|
+
:param str parent_module: RegScale parent module
|
|
1219
|
+
:param Api api: API instance
|
|
1220
|
+
:param str evidence_type: Type of evidence collected
|
|
1221
|
+
:rtype: None
|
|
1222
|
+
"""
|
|
1223
|
+
from regscale.integrations.commercial.microsoft_defender.defender_constants import EVIDENCE_CATEGORIES
|
|
1224
|
+
|
|
1225
|
+
artifacts_dir = Path("./artifacts")
|
|
1226
|
+
artifacts_dir.mkdir(exist_ok=True)
|
|
1227
|
+
|
|
1228
|
+
# Get control implementations mapping for evidence targeting
|
|
1229
|
+
control_implementations_map = get_control_implementations_map(parent_id, parent_module)
|
|
1230
|
+
|
|
1231
|
+
if not control_implementations_map:
|
|
1232
|
+
logger.error(
|
|
1233
|
+
f"No control implementations found for {parent_module} #{parent_id}. Cannot map evidence to controls."
|
|
1234
|
+
)
|
|
1235
|
+
return
|
|
1236
|
+
|
|
1237
|
+
total_successful_uploads = 0
|
|
1238
|
+
total_evidence_items = 0
|
|
1239
|
+
evidence_summary = []
|
|
1240
|
+
|
|
1241
|
+
for evidence_key, evidence_list in evidence_data.items():
|
|
1242
|
+
if not evidence_list:
|
|
1243
|
+
logger.warning(f"No data found for {evidence_key}")
|
|
1244
|
+
continue
|
|
1245
|
+
|
|
1246
|
+
total_evidence_items += len(evidence_list)
|
|
1247
|
+
|
|
1248
|
+
# Upload evidence to specific control implementations
|
|
1249
|
+
uploads_for_evidence = upload_evidence_to_controls(
|
|
1250
|
+
evidence_key=evidence_key,
|
|
1251
|
+
evidence_file_list=evidence_list,
|
|
1252
|
+
control_implementations_map=control_implementations_map,
|
|
1253
|
+
api=api,
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
total_successful_uploads += uploads_for_evidence
|
|
1257
|
+
evidence_summary.append(f"{evidence_key}: {len(evidence_list)} items → {uploads_for_evidence} control uploads")
|
|
1258
|
+
|
|
1259
|
+
# Summary
|
|
1260
|
+
category_name = EVIDENCE_CATEGORIES.get(evidence_type, f"Azure Entra {evidence_type.replace('_', ' ').title()}")
|
|
1261
|
+
logger.info(
|
|
1262
|
+
f"Azure Entra evidence collection complete for {category_name}. "
|
|
1263
|
+
f"Collected {total_evidence_items} total items across {total_successful_uploads} control-specific uploads."
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
# Detailed summary
|
|
1267
|
+
if evidence_summary:
|
|
1268
|
+
logger.info("Evidence upload summary:")
|
|
1269
|
+
for summary in evidence_summary:
|
|
1270
|
+
logger.info(f" - {summary}")
|