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
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
Module to handle API calls to Microsoft Defender for Cloud
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os.path
|
|
5
6
|
from json import JSONDecodeError
|
|
6
7
|
from logging import getLogger
|
|
7
8
|
from typing import Any, Literal, Optional
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
from requests import Response
|
|
10
12
|
|
|
11
13
|
from regscale.core.app.api import Api
|
|
12
|
-
from regscale.core.app.utils.app_utils import error_and_exit
|
|
13
|
-
from .
|
|
14
|
+
from regscale.core.app.utils.app_utils import error_and_exit, get_current_datetime, check_file_path, save_data_to
|
|
15
|
+
from urllib.parse import urljoin
|
|
16
|
+
from .defender_constants import APP_JSON, DATA_TYPE, GRAPH_BASE_URL, ENTRA_ENDPOINTS, ENTRA_SAVE_DIR
|
|
14
17
|
|
|
15
18
|
logger = getLogger("regscale")
|
|
16
19
|
|
|
@@ -22,10 +25,10 @@ class DefenderApi:
|
|
|
22
25
|
:param Literal["cloud", "365"] system: Which system to make API calls to, either cloud or 365
|
|
23
26
|
"""
|
|
24
27
|
|
|
25
|
-
def __init__(self, system: Literal["cloud", "365"]):
|
|
28
|
+
def __init__(self, system: Literal["cloud", "365", "entra"]):
|
|
26
29
|
self.api: Api = Api()
|
|
27
30
|
self.config: dict = self.api.config
|
|
28
|
-
self.system: Literal["cloud", "365"] = system
|
|
31
|
+
self.system: Literal["cloud", "365", "entra"] = system
|
|
29
32
|
self.headers: dict = self.set_headers()
|
|
30
33
|
self.decode_error: str = "JSON Decode error"
|
|
31
34
|
self.skip_token_key: str = "$skipToken"
|
|
@@ -57,12 +60,26 @@ class DefenderApi:
|
|
|
57
60
|
client_secret = self.config["azureCloudSecret"]
|
|
58
61
|
resource = "https://management.azure.com"
|
|
59
62
|
key = "azureCloudAccessToken"
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
elif self.system == "entra":
|
|
64
|
+
url = f'https://login.microsoftonline.com/{self.config["azureEntraTenantId"]}/oauth2/v2.0/token'
|
|
65
|
+
client_id = self.config["azureEntraClientId"]
|
|
66
|
+
client_secret = self.config["azureEntraSecret"]
|
|
67
|
+
resource = "https://graph.microsoft.com/.default"
|
|
68
|
+
key = "azureEntraAccessToken"
|
|
69
|
+
if self.system == "entra":
|
|
70
|
+
data = {
|
|
71
|
+
"scope": resource,
|
|
72
|
+
"client_id": client_id,
|
|
73
|
+
"client_secret": client_secret,
|
|
74
|
+
"grant_type": "client_credentials",
|
|
75
|
+
}
|
|
76
|
+
else:
|
|
77
|
+
data = {
|
|
78
|
+
"resource": resource,
|
|
79
|
+
"client_id": client_id,
|
|
80
|
+
"client_secret": client_secret,
|
|
81
|
+
"grant_type": "client_credentials",
|
|
82
|
+
}
|
|
66
83
|
# get the data
|
|
67
84
|
response = self.api.post(
|
|
68
85
|
url=url,
|
|
@@ -91,9 +108,11 @@ class DefenderApi:
|
|
|
91
108
|
key = "azureCloudAccessToken"
|
|
92
109
|
elif self.system.lower() == "365":
|
|
93
110
|
key = "azure365AccessToken"
|
|
111
|
+
elif self.system == "entra":
|
|
112
|
+
key = "azureEntraAccessToken"
|
|
94
113
|
else:
|
|
95
114
|
error_and_exit(
|
|
96
|
-
f"{self.system.title()} is not supported, only Microsoft 365 Defender
|
|
115
|
+
f"{self.system.title()} is not supported, only Microsoft 365 Defender, Microsoft Defender for Cloud, and Azure Entra."
|
|
97
116
|
)
|
|
98
117
|
current_token = self.config[key]
|
|
99
118
|
# check the token if it isn't blank
|
|
@@ -207,11 +226,12 @@ class DefenderApi:
|
|
|
207
226
|
# return the defender recommendations
|
|
208
227
|
return defender_data
|
|
209
228
|
|
|
210
|
-
def get_items_from_azure(self, url: str) -> list:
|
|
229
|
+
def get_items_from_azure(self, url: str, parse_value: Optional[bool] = True) -> list:
|
|
211
230
|
"""
|
|
212
231
|
Function to get data from Microsoft Defender returns the data as a list while handling pagination
|
|
213
232
|
|
|
214
233
|
:param str url: URL to use for the API call
|
|
234
|
+
:param Optional[bool] parse_value: Whether to parse the value from the API response, defaults to True
|
|
215
235
|
:return: list of recommendations
|
|
216
236
|
:rtype: list
|
|
217
237
|
"""
|
|
@@ -226,7 +246,10 @@ class DefenderApi:
|
|
|
226
246
|
try:
|
|
227
247
|
response_data = response.json()
|
|
228
248
|
# try to get the values from the api response
|
|
229
|
-
|
|
249
|
+
if parse_value:
|
|
250
|
+
defender_data = response_data["value"]
|
|
251
|
+
else:
|
|
252
|
+
defender_data = response_data
|
|
230
253
|
except JSONDecodeError:
|
|
231
254
|
# notify user if there was a json decode error from API response and exit
|
|
232
255
|
error_and_exit(self.decode_error)
|
|
@@ -236,7 +259,7 @@ class DefenderApi:
|
|
|
236
259
|
f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.text}"
|
|
237
260
|
)
|
|
238
261
|
# check if pagination is required to fetch all data from Microsoft Defender
|
|
239
|
-
if next_link := response_data.get("nextLink"):
|
|
262
|
+
if next_link := (response_data.get("nextLink") or response_data.get("@odata.nextLink")):
|
|
240
263
|
# get the rest of the data
|
|
241
264
|
defender_data.extend(self.get_items_from_azure(url=next_link))
|
|
242
265
|
# return the defender recommendations
|
|
@@ -284,3 +307,314 @@ class DefenderApi:
|
|
|
284
307
|
)
|
|
285
308
|
query_string = response.json().get("properties", {}).get("query")
|
|
286
309
|
return self.execute_resource_graph_query(query=query_string)
|
|
310
|
+
|
|
311
|
+
def get_and_save_entra_evidence(self, endpoint_key: str, **kwargs) -> list[Path]:
|
|
312
|
+
"""
|
|
313
|
+
Function to get Azure Entra evidence data from Microsoft Graph API and saves it to a csv file
|
|
314
|
+
|
|
315
|
+
:param str endpoint_key: Key from ENTRA_ENDPOINTS to specify which endpoint to call
|
|
316
|
+
:param kwargs: Additional parameters for URL formatting
|
|
317
|
+
:return: List of Paths to the saved csv files
|
|
318
|
+
:rtype: list[Path]
|
|
319
|
+
"""
|
|
320
|
+
if self.system != "entra":
|
|
321
|
+
error_and_exit("This method can only be used with system='entra'")
|
|
322
|
+
|
|
323
|
+
if endpoint_key not in ENTRA_ENDPOINTS:
|
|
324
|
+
error_and_exit(f"Unknown endpoint key: {endpoint_key}")
|
|
325
|
+
|
|
326
|
+
endpoint = ENTRA_ENDPOINTS[endpoint_key]
|
|
327
|
+
|
|
328
|
+
# Handle URL parameter substitution
|
|
329
|
+
if "{start_date}" in endpoint:
|
|
330
|
+
start_date = kwargs.get("start_date", get_current_datetime("%Y-%m-%dT00:00:00Z"))
|
|
331
|
+
endpoint = endpoint.replace("{start_date}", start_date)
|
|
332
|
+
|
|
333
|
+
if "{group-id}" in endpoint:
|
|
334
|
+
group_id = kwargs.get("group_id")
|
|
335
|
+
if not group_id:
|
|
336
|
+
error_and_exit("group_id parameter is required for this endpoint")
|
|
337
|
+
endpoint = endpoint.replace("{group-id}", group_id)
|
|
338
|
+
|
|
339
|
+
if "{def_id}" in endpoint:
|
|
340
|
+
def_id = kwargs.get("def_id")
|
|
341
|
+
if not def_id:
|
|
342
|
+
error_and_exit("def_id parameter is required for this endpoint")
|
|
343
|
+
endpoint = endpoint.replace("{def_id}", def_id)
|
|
344
|
+
|
|
345
|
+
if "{instance_id}" in endpoint:
|
|
346
|
+
instance_id = kwargs.get("instance_id")
|
|
347
|
+
if not instance_id:
|
|
348
|
+
error_and_exit("instance_id parameter is required for this endpoint")
|
|
349
|
+
endpoint = endpoint.replace("{instance_id}", instance_id)
|
|
350
|
+
|
|
351
|
+
url = f"{GRAPH_BASE_URL}{endpoint}"
|
|
352
|
+
logger.info(f"Retrieving Azure Entra evidence from: {endpoint_key}")
|
|
353
|
+
|
|
354
|
+
data = self.get_items_from_azure(url=url, parse_value=kwargs.get("parse_value", True))
|
|
355
|
+
save_path = Path(
|
|
356
|
+
os.path.join(ENTRA_SAVE_DIR, f"azure_entra_{endpoint_key}_{get_current_datetime('%Y%m%d')}.csv")
|
|
357
|
+
)
|
|
358
|
+
save_data_to(file=save_path, data=data, transpose_data=False)
|
|
359
|
+
return [save_path]
|
|
360
|
+
|
|
361
|
+
def collect_all_entra_evidence(self, days_back: int = 30) -> dict[str, list[Path]]:
|
|
362
|
+
"""
|
|
363
|
+
Function to collect all Azure Entra evidence data for FedRAMP compliance
|
|
364
|
+
|
|
365
|
+
:param int days_back: Number of days back to collect audit logs, defaults to 30
|
|
366
|
+
:return: Dict containing all evidence data categorized by type and list of Paths to the saved csv evidence files
|
|
367
|
+
:rtype: dict[str, list[Path]]
|
|
368
|
+
"""
|
|
369
|
+
from datetime import datetime, timedelta
|
|
370
|
+
|
|
371
|
+
evidence_data = {}
|
|
372
|
+
start_date = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%dT00:00:00Z")
|
|
373
|
+
|
|
374
|
+
check_file_path(ENTRA_SAVE_DIR)
|
|
375
|
+
|
|
376
|
+
# Users and Groups
|
|
377
|
+
try:
|
|
378
|
+
evidence_data["users"] = self.get_and_save_entra_evidence("users")
|
|
379
|
+
evidence_data["users_delta"] = self.get_and_save_entra_evidence("users_delta")
|
|
380
|
+
evidence_data["guest_users"] = self.get_and_save_entra_evidence("guest_users")
|
|
381
|
+
evidence_data["groups_and_members"] = self.get_and_save_entra_evidence("groups_and_members")
|
|
382
|
+
evidence_data["security_groups"] = self.get_and_save_entra_evidence("security_groups")
|
|
383
|
+
logger.info("Successfully collected user and group evidence")
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error(f"Error collecting user/group evidence: {e}")
|
|
386
|
+
evidence_data["users"] = []
|
|
387
|
+
evidence_data["users_delta"] = []
|
|
388
|
+
evidence_data["guest_users"] = []
|
|
389
|
+
evidence_data["groups_and_members"] = []
|
|
390
|
+
evidence_data["security_groups"] = []
|
|
391
|
+
|
|
392
|
+
# RBAC and PIM
|
|
393
|
+
try:
|
|
394
|
+
evidence_data["role_assignments"] = self.get_and_save_entra_evidence("role_assignments")
|
|
395
|
+
evidence_data["role_definitions"] = self.get_and_save_entra_evidence("role_definitions")
|
|
396
|
+
evidence_data["pim_assignments"] = self.get_and_save_entra_evidence("pim_assignments")
|
|
397
|
+
evidence_data["pim_eligibility"] = self.get_and_save_entra_evidence("pim_eligibility")
|
|
398
|
+
logger.info("Successfully collected RBAC and PIM evidence")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"Error collecting RBAC/PIM evidence: {e}")
|
|
401
|
+
evidence_data["role_assignments"] = []
|
|
402
|
+
evidence_data["role_definitions"] = []
|
|
403
|
+
evidence_data["pim_assignments"] = []
|
|
404
|
+
evidence_data["pim_eligibility"] = []
|
|
405
|
+
|
|
406
|
+
# Conditional Access
|
|
407
|
+
try:
|
|
408
|
+
evidence_data["conditional_access"] = self.get_and_save_entra_evidence("conditional_access")
|
|
409
|
+
logger.info("Successfully collected conditional access evidence")
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.error(f"Error collecting conditional access evidence: {e}")
|
|
412
|
+
evidence_data["conditional_access"] = []
|
|
413
|
+
|
|
414
|
+
# Authentication Methods
|
|
415
|
+
try:
|
|
416
|
+
evidence_data["auth_methods_policy"] = self.get_and_save_entra_evidence(
|
|
417
|
+
"auth_methods_policy", parse_value=False
|
|
418
|
+
)
|
|
419
|
+
evidence_data["user_mfa_registration"] = self.get_and_save_entra_evidence("user_mfa_registration")
|
|
420
|
+
evidence_data["mfa_registered_users"] = self.get_and_save_entra_evidence("mfa_registered_users")
|
|
421
|
+
logger.info("Successfully collected authentication methods evidence")
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error(f"Error collecting authentication methods evidence: {e}")
|
|
424
|
+
evidence_data["auth_methods_policy"] = []
|
|
425
|
+
evidence_data["user_mfa_registration"] = []
|
|
426
|
+
evidence_data["mfa_registered_users"] = []
|
|
427
|
+
|
|
428
|
+
# Audit Logs (may require additional permissions)
|
|
429
|
+
try:
|
|
430
|
+
evidence_data["sign_in_logs"] = self.get_and_save_entra_evidence("sign_in_logs", start_date=start_date)
|
|
431
|
+
evidence_data["directory_audits"] = self.get_and_save_entra_evidence(
|
|
432
|
+
"directory_audits", start_date=start_date
|
|
433
|
+
)
|
|
434
|
+
evidence_data["provisioning_logs"] = self.get_and_save_entra_evidence(
|
|
435
|
+
"provisioning_logs", start_date=start_date
|
|
436
|
+
)
|
|
437
|
+
logger.info("Successfully collected audit log evidence")
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Error collecting audit log evidence (may require additional permissions): {e}")
|
|
440
|
+
evidence_data["sign_in_logs"] = []
|
|
441
|
+
evidence_data["directory_audits"] = []
|
|
442
|
+
evidence_data["provisioning_logs"] = []
|
|
443
|
+
|
|
444
|
+
# Access Reviews
|
|
445
|
+
try:
|
|
446
|
+
evidence_data["access_review_definitions"] = self.collect_entra_access_reviews()
|
|
447
|
+
logger.info("Successfully collected access review evidence")
|
|
448
|
+
except Exception as e:
|
|
449
|
+
logger.error(f"Error collecting access review evidence: {e}")
|
|
450
|
+
evidence_data["access_review_definitions"] = []
|
|
451
|
+
|
|
452
|
+
return evidence_data
|
|
453
|
+
|
|
454
|
+
def collect_entra_access_reviews(self) -> list[Path]:
|
|
455
|
+
"""
|
|
456
|
+
Function to collect access reviews from Microsoft Graph API
|
|
457
|
+
|
|
458
|
+
:return: List of paths to the saved csv files
|
|
459
|
+
:rtype: list[Path]
|
|
460
|
+
"""
|
|
461
|
+
file_paths = []
|
|
462
|
+
url = GRAPH_BASE_URL + ENTRA_ENDPOINTS["access_review_definitions"]
|
|
463
|
+
definitions = self.get_items_from_azure(url=url)
|
|
464
|
+
current_date = get_current_datetime("%Y-%m-%d")
|
|
465
|
+
|
|
466
|
+
for definition in definitions:
|
|
467
|
+
definition_name = definition["displayName"].replace("/", "_").replace(" ", "_")
|
|
468
|
+
|
|
469
|
+
# Save flattened definition data
|
|
470
|
+
definition_path = Path(
|
|
471
|
+
os.path.join(ENTRA_SAVE_DIR, f"access_reviews_definitions_{definition_name}_{current_date}.csv")
|
|
472
|
+
)
|
|
473
|
+
flattened_definition = self._flatten_access_review_definition(definition)
|
|
474
|
+
save_data_to(file=definition_path, data=[flattened_definition], transpose_data=False)
|
|
475
|
+
file_paths.append(definition_path)
|
|
476
|
+
|
|
477
|
+
# Get instances and decisions
|
|
478
|
+
instance_url = GRAPH_BASE_URL + ENTRA_ENDPOINTS["access_review_instances"].format(def_id=definition["id"])
|
|
479
|
+
instances = self.get_items_from_azure(url=instance_url)
|
|
480
|
+
|
|
481
|
+
# Save flattened instances data
|
|
482
|
+
instances_path = Path(
|
|
483
|
+
os.path.join(ENTRA_SAVE_DIR, f"access_reviews_instances_{definition_name}_{current_date}.csv")
|
|
484
|
+
)
|
|
485
|
+
flattened_instances = []
|
|
486
|
+
for instance in instances:
|
|
487
|
+
flattened_instance = self._flatten_access_review_instance(definition["id"], instance)
|
|
488
|
+
flattened_instances.append(flattened_instance)
|
|
489
|
+
|
|
490
|
+
if flattened_instances:
|
|
491
|
+
save_data_to(file=instances_path, data=flattened_instances, transpose_data=False)
|
|
492
|
+
file_paths.append(instances_path)
|
|
493
|
+
|
|
494
|
+
# Save flattened decisions data
|
|
495
|
+
decisions_path = Path(
|
|
496
|
+
os.path.join(ENTRA_SAVE_DIR, f"access_reviews_decisions_{definition_name}_{current_date}.csv")
|
|
497
|
+
)
|
|
498
|
+
all_decisions = []
|
|
499
|
+
for instance in instances:
|
|
500
|
+
decision_url = GRAPH_BASE_URL + ENTRA_ENDPOINTS["access_review_decisions"].format(
|
|
501
|
+
def_id=definition["id"], instance_id=instance["id"]
|
|
502
|
+
)
|
|
503
|
+
decisions = self.get_items_from_azure(url=decision_url)
|
|
504
|
+
for decision in decisions:
|
|
505
|
+
flattened_decision = self._flatten_access_review_decision(
|
|
506
|
+
definition["id"], instance["id"], decision
|
|
507
|
+
)
|
|
508
|
+
all_decisions.append(flattened_decision)
|
|
509
|
+
|
|
510
|
+
if all_decisions:
|
|
511
|
+
save_data_to(file=decisions_path, data=all_decisions, transpose_data=False)
|
|
512
|
+
file_paths.append(decisions_path)
|
|
513
|
+
|
|
514
|
+
return file_paths
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def _flatten_access_review_definition(definition: dict) -> dict:
|
|
518
|
+
"""
|
|
519
|
+
Flatten access review definition for CSV export
|
|
520
|
+
|
|
521
|
+
:param dict definition: Definition data
|
|
522
|
+
:return: Flattened definition data
|
|
523
|
+
:rtype: dict
|
|
524
|
+
"""
|
|
525
|
+
return {
|
|
526
|
+
"id": definition.get("id"),
|
|
527
|
+
"displayName": definition.get("displayName"),
|
|
528
|
+
"status": definition.get("status"),
|
|
529
|
+
"createdDateTime": definition.get("createdDateTime"),
|
|
530
|
+
"lastModifiedDateTime": definition.get("lastModifiedDateTime"),
|
|
531
|
+
"descriptionForAdmins": definition.get("descriptionForAdmins"),
|
|
532
|
+
"descriptionForReviewers": definition.get("descriptionForReviewers"),
|
|
533
|
+
"createdBy_displayName": definition.get("createdBy", {}).get("displayName"),
|
|
534
|
+
"createdBy_id": definition.get("createdBy", {}).get("id"),
|
|
535
|
+
"createdBy_userPrincipalName": definition.get("createdBy", {}).get("userPrincipalName"),
|
|
536
|
+
"scope_type": definition.get("scope", {}).get(DATA_TYPE),
|
|
537
|
+
"scope_query": definition.get("scope", {}).get("query"),
|
|
538
|
+
"scope_inactiveDuration": definition.get("scope", {}).get("inactiveDuration"),
|
|
539
|
+
"instanceEnumerationScope_type": definition.get("instanceEnumerationScope", {}).get(DATA_TYPE),
|
|
540
|
+
"instanceEnumerationScope_query": definition.get("instanceEnumerationScope", {}).get("query"),
|
|
541
|
+
"settings_defaultDecision": definition.get("settings", {}).get("defaultDecision"),
|
|
542
|
+
"settings_autoApplyDecisionsEnabled": definition.get("settings", {}).get("autoApplyDecisionsEnabled"),
|
|
543
|
+
"settings_instanceDurationInDays": definition.get("settings", {}).get("instanceDurationInDays"),
|
|
544
|
+
"settings_justificationRequiredOnApproval": definition.get("settings", {}).get(
|
|
545
|
+
"justificationRequiredOnApproval"
|
|
546
|
+
),
|
|
547
|
+
"settings_mailNotificationsEnabled": definition.get("settings", {}).get("mailNotificationsEnabled"),
|
|
548
|
+
"settings_recommendationsEnabled": definition.get("settings", {}).get("recommendationsEnabled"),
|
|
549
|
+
"settings_recurrence_type": definition.get("settings", {})
|
|
550
|
+
.get("recurrence", {})
|
|
551
|
+
.get("pattern", {})
|
|
552
|
+
.get("type"),
|
|
553
|
+
"settings_recurrence_interval": definition.get("settings", {})
|
|
554
|
+
.get("recurrence", {})
|
|
555
|
+
.get("pattern", {})
|
|
556
|
+
.get("interval"),
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
@staticmethod
|
|
560
|
+
def _flatten_access_review_instance(definition_id: str, instance: dict) -> dict:
|
|
561
|
+
"""
|
|
562
|
+
Flatten access review instance for CSV export
|
|
563
|
+
|
|
564
|
+
:param str definition_id: ID of the access review definition
|
|
565
|
+
:param dict instance: Instance data
|
|
566
|
+
:return: Flattened instance data
|
|
567
|
+
:rtype: dict
|
|
568
|
+
"""
|
|
569
|
+
return {
|
|
570
|
+
"definition_id": definition_id,
|
|
571
|
+
"id": instance.get("id"),
|
|
572
|
+
"status": instance.get("status"),
|
|
573
|
+
"startDateTime": instance.get("startDateTime"),
|
|
574
|
+
"endDateTime": instance.get("endDateTime"),
|
|
575
|
+
"scope_type": instance.get("scope", {}).get(DATA_TYPE),
|
|
576
|
+
"scope_query": instance.get("scope", {}).get("query"),
|
|
577
|
+
"scope_inactiveDuration": instance.get("scope", {}).get("inactiveDuration"),
|
|
578
|
+
"reviewers_count": len(instance.get("reviewers", [])),
|
|
579
|
+
"fallbackReviewers_count": len(instance.get("fallbackReviewers", [])),
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
@staticmethod
|
|
583
|
+
def _flatten_access_review_decision(definition_id: str, instance_id: str, decision: dict) -> dict:
|
|
584
|
+
"""
|
|
585
|
+
Flatten access review decision for CSV export
|
|
586
|
+
|
|
587
|
+
:param str definition_id: ID of the access review definition
|
|
588
|
+
:param str instance_id: ID of the access review instance
|
|
589
|
+
:param dict decision: Decision data
|
|
590
|
+
:return: Flattened decision data
|
|
591
|
+
:rtype: dict
|
|
592
|
+
"""
|
|
593
|
+
return {
|
|
594
|
+
"definition_id": definition_id,
|
|
595
|
+
"instance_id": instance_id,
|
|
596
|
+
"decision_id": decision.get("id"),
|
|
597
|
+
"accessReviewId": decision.get("accessReviewId"),
|
|
598
|
+
"decision": decision.get("decision"),
|
|
599
|
+
"recommendation": decision.get("recommendation"),
|
|
600
|
+
"justification": decision.get("justification"),
|
|
601
|
+
"reviewedDateTime": decision.get("reviewedDateTime"),
|
|
602
|
+
"appliedDateTime": decision.get("appliedDateTime"),
|
|
603
|
+
"applyResult": decision.get("applyResult"),
|
|
604
|
+
"principalLink": decision.get("principalLink"),
|
|
605
|
+
"resourceLink": decision.get("resourceLink"),
|
|
606
|
+
"reviewedBy_id": decision.get("reviewedBy", {}).get("id"),
|
|
607
|
+
"reviewedBy_displayName": decision.get("reviewedBy", {}).get("displayName"),
|
|
608
|
+
"reviewedBy_userPrincipalName": decision.get("reviewedBy", {}).get("userPrincipalName"),
|
|
609
|
+
"appliedBy_id": decision.get("appliedBy", {}).get("id"),
|
|
610
|
+
"appliedBy_displayName": decision.get("appliedBy", {}).get("displayName"),
|
|
611
|
+
"appliedBy_userPrincipalName": decision.get("appliedBy", {}).get("userPrincipalName"),
|
|
612
|
+
"target_type": decision.get("target", {}).get(DATA_TYPE),
|
|
613
|
+
"target_userId": decision.get("target", {}).get("userId"),
|
|
614
|
+
"target_userDisplayName": decision.get("target", {}).get("userDisplayName"),
|
|
615
|
+
"target_userPrincipalName": decision.get("target", {}).get("userPrincipalName"),
|
|
616
|
+
"principal_type": decision.get("principal", {}).get(DATA_TYPE),
|
|
617
|
+
"principal_id": decision.get("principal", {}).get("id"),
|
|
618
|
+
"principal_displayName": decision.get("principal", {}).get("displayName"),
|
|
619
|
+
"principal_userPrincipalName": decision.get("principal", {}).get("userPrincipalName"),
|
|
620
|
+
}
|
|
@@ -2,11 +2,168 @@
|
|
|
2
2
|
Module to store constants for Microsoft Defender for Cloud
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
|
|
5
7
|
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
6
8
|
IDENTIFICATION_TYPE = "Vulnerability Assessment"
|
|
7
9
|
CLOUD_RECS = "Microsoft Defender for Cloud Recommendation"
|
|
8
10
|
APP_JSON = "application/json"
|
|
9
11
|
AFD_ENDPOINTS = "microsoft.cdn/profiles/afdendpoints"
|
|
12
|
+
DATA_TYPE = "@odata.type"
|
|
13
|
+
ENTRA_SAVE_DIR = os.path.join("artifacts", "defender", "entra")
|
|
14
|
+
|
|
15
|
+
# Azure Entra (Azure AD) Microsoft Graph API endpoints
|
|
16
|
+
GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
|
|
17
|
+
GRAPH_BETA_URL = "https://graph.microsoft.com/beta"
|
|
18
|
+
|
|
19
|
+
# Azure Entra API endpoints for FedRAMP evidence collection
|
|
20
|
+
ENTRA_ENDPOINTS = {
|
|
21
|
+
"users": "/users?$select=id,displayName,userPrincipalName,accountEnabled,userType,createdDateTime&$top=999",
|
|
22
|
+
"users_delta": "/users/delta?$select=id,displayName,userPrincipalName,accountEnabled,userType,createdDateTime",
|
|
23
|
+
"guest_users": "/users?$filter=userType eq 'Guest'&$select=id,displayName,userPrincipalName,accountEnabled",
|
|
24
|
+
"security_groups": "/groups?$filter=securityEnabled eq true&$select=id,displayName,securityEnabled",
|
|
25
|
+
"groups_and_members": "/groups?$expand=members($select=id,displayName,userPrincipalName)",
|
|
26
|
+
# "groups": "/groups",
|
|
27
|
+
# "group_members": "/groups/{group-id}/members?$select=id,displayName,userPrincipalName",
|
|
28
|
+
"role_assignments": "/roleManagement/directory/roleAssignments?$expand=roleDefinition",
|
|
29
|
+
"role_definitions": "/roleManagement/directory/roleDefinitions?$select=id,displayName,description",
|
|
30
|
+
"pim_assignments": "/roleManagement/directory/roleAssignmentScheduleInstances?$expand=activatedUsing,principal,roleDefinition",
|
|
31
|
+
"pim_eligibility": "/roleManagement/directory/roleEligibilityScheduleInstances?$expand=roleDefinition",
|
|
32
|
+
"conditional_access": "/identity/conditionalAccess/policies",
|
|
33
|
+
"auth_methods_policy": "/policies/authenticationMethodsPolicy",
|
|
34
|
+
"user_mfa_registration": "/reports/authenticationMethods/userRegistrationDetails?$top=999",
|
|
35
|
+
"mfa_registered_users": "/reports/authenticationMethods/userRegistrationDetails?$filter=isMfaRegistered eq true",
|
|
36
|
+
"sign_in_logs": "/auditLogs/signIns?$filter=createdDateTime ge {start_date}&$top=1000",
|
|
37
|
+
"directory_audits": "/auditLogs/directoryAudits?$filter=activityDateTime ge {start_date}&$top=1000",
|
|
38
|
+
"provisioning_logs": "/auditLogs/provisioning?$filter=activityDateTime ge {start_date}&$top=1000",
|
|
39
|
+
"access_review_definitions": "/identityGovernance/accessReviews/definitions",
|
|
40
|
+
"access_review_instances": "/identityGovernance/accessReviews/definitions/{def_id}/instances",
|
|
41
|
+
"access_review_decisions": "/identityGovernance/accessReviews/definitions/{def_id}/instances/{instance_id}/decisions?$top=100",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Evidence categories for FedRAMP controls
|
|
45
|
+
EVIDENCE_CATEGORIES = {
|
|
46
|
+
"users_groups": "Users & Groups Management",
|
|
47
|
+
"rbac_pim": "Role-Based Access Control & Privileged Identity Management",
|
|
48
|
+
"conditional_access": "Conditional Access Policies",
|
|
49
|
+
"authentication": "Authentication Methods & Multi-Factor Authentication",
|
|
50
|
+
"audit_logs": "Audit & Sign-in Logs",
|
|
51
|
+
"access_reviews": "Access Reviews & Governance",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AC_1 = "AC-1"
|
|
55
|
+
AC_2 = "AC-2"
|
|
56
|
+
AC_2_1 = "AC-2(1)"
|
|
57
|
+
AC_2_3 = "AC-2(3)"
|
|
58
|
+
AC_2_5 = "AC-2(5)"
|
|
59
|
+
AC_2_7 = "AC-2(7)"
|
|
60
|
+
AC_2_12 = "AC-2(12)"
|
|
61
|
+
AC_365 = ["AC-3", "AC-6", "AC-5"]
|
|
62
|
+
AU_2 = "AU-2"
|
|
63
|
+
AU_3 = "AU-3"
|
|
64
|
+
AU_6 = "AU-6"
|
|
65
|
+
AU_12 = "AU-12"
|
|
66
|
+
IA_2 = "IA-2"
|
|
67
|
+
IA_2_1 = "IA-2(1)"
|
|
68
|
+
IA_2_2 = "IA-2(2)"
|
|
69
|
+
IA_2_3 = "IA-2(3)"
|
|
70
|
+
IA_2_4 = "IA-2(4)"
|
|
71
|
+
IA_2_5 = "IA-2(5)"
|
|
72
|
+
IA_2_6 = "IA-2(6)"
|
|
73
|
+
IA_2_7 = "IA-2(7)"
|
|
74
|
+
IA_2_8 = "IA-2(8)"
|
|
75
|
+
IA_2_9 = "IA-2(9)"
|
|
76
|
+
IA_2_10 = "IA-2(10)"
|
|
77
|
+
IA_2_11 = "IA-2(11)"
|
|
78
|
+
IA_2_12 = "IA-2(12)"
|
|
79
|
+
IA_4 = "IA-4"
|
|
80
|
+
IA_5 = "IA-5"
|
|
81
|
+
|
|
82
|
+
# Mapping between Azure Entra evidence types and FedRAMP control identifiers
|
|
83
|
+
# Based on Azure_Entra_FedRAMP_High_Evidence.docx mapping table
|
|
84
|
+
EVIDENCE_TO_CONTROLS_MAPPING = {
|
|
85
|
+
# Users & Groups evidence maps to Account Management controls
|
|
86
|
+
"users": [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12],
|
|
87
|
+
"users_delta": [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12],
|
|
88
|
+
"guest_users": [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12, "IA-8", "IA-8(1)"],
|
|
89
|
+
"security_groups": [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12],
|
|
90
|
+
"groups_and_members": [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12],
|
|
91
|
+
# RBAC & PIM evidence maps to Access Control and Privilege Management controls
|
|
92
|
+
"role_assignments": AC_365,
|
|
93
|
+
"role_definitions": AC_365,
|
|
94
|
+
"pim_assignments": AC_365,
|
|
95
|
+
"pim_eligibility": AC_365,
|
|
96
|
+
# Conditional Access maps to Access Enforcement controls
|
|
97
|
+
"conditional_access": ["AC-17", "AC-19"],
|
|
98
|
+
# Authentication methods map to Identification & Authentication controls
|
|
99
|
+
"auth_methods_policy": [
|
|
100
|
+
IA_2,
|
|
101
|
+
IA_2_1,
|
|
102
|
+
IA_2_2,
|
|
103
|
+
IA_2_3,
|
|
104
|
+
IA_2_4,
|
|
105
|
+
IA_2_5,
|
|
106
|
+
IA_2_6,
|
|
107
|
+
IA_2_7,
|
|
108
|
+
IA_2_8,
|
|
109
|
+
IA_2_9,
|
|
110
|
+
IA_2_10,
|
|
111
|
+
IA_2_11,
|
|
112
|
+
IA_2_12,
|
|
113
|
+
IA_4,
|
|
114
|
+
IA_5,
|
|
115
|
+
],
|
|
116
|
+
"user_mfa_registration": [
|
|
117
|
+
IA_2,
|
|
118
|
+
IA_2_1,
|
|
119
|
+
IA_2_2,
|
|
120
|
+
IA_2_3,
|
|
121
|
+
IA_2_4,
|
|
122
|
+
IA_2_5,
|
|
123
|
+
IA_2_6,
|
|
124
|
+
IA_2_7,
|
|
125
|
+
IA_2_8,
|
|
126
|
+
IA_2_9,
|
|
127
|
+
IA_2_10,
|
|
128
|
+
IA_2_11,
|
|
129
|
+
IA_2_12,
|
|
130
|
+
IA_4,
|
|
131
|
+
IA_5,
|
|
132
|
+
],
|
|
133
|
+
"mfa_registered_users": [
|
|
134
|
+
IA_2,
|
|
135
|
+
IA_2_1,
|
|
136
|
+
IA_2_2,
|
|
137
|
+
IA_2_3,
|
|
138
|
+
IA_2_4,
|
|
139
|
+
IA_2_5,
|
|
140
|
+
IA_2_6,
|
|
141
|
+
IA_2_7,
|
|
142
|
+
IA_2_8,
|
|
143
|
+
IA_2_9,
|
|
144
|
+
IA_2_10,
|
|
145
|
+
IA_2_11,
|
|
146
|
+
IA_2_12,
|
|
147
|
+
IA_4,
|
|
148
|
+
IA_5,
|
|
149
|
+
],
|
|
150
|
+
# Sign-in logs map to Unsuccessful Logon controls and Audit controls
|
|
151
|
+
"sign_in_logs": ["AC-7", AU_2, AU_3, AU_6, AU_12],
|
|
152
|
+
# Directory audits map to Audit controls and Account Management
|
|
153
|
+
"directory_audits": [AU_2, AU_3, AU_6, AU_12, IA_4, IA_5],
|
|
154
|
+
# Provisioning logs map to Audit controls
|
|
155
|
+
"provisioning_logs": [AU_2, AU_3, AU_6, AU_12],
|
|
156
|
+
# Access reviews map to Account Management and Privilege controls
|
|
157
|
+
"access_review_definitions": ["AC-3", "AC-6", "AC-5", "IA-8", "IA-8(1)"],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Reverse mapping for easy lookup of which evidence types are needed for a control
|
|
161
|
+
CONTROLS_TO_EVIDENCE_MAPPING = {}
|
|
162
|
+
for evidence_type, controls in EVIDENCE_TO_CONTROLS_MAPPING.items():
|
|
163
|
+
for control in controls:
|
|
164
|
+
if control not in CONTROLS_TO_EVIDENCE_MAPPING:
|
|
165
|
+
CONTROLS_TO_EVIDENCE_MAPPING[control] = []
|
|
166
|
+
CONTROLS_TO_EVIDENCE_MAPPING[control].append(evidence_type)
|
|
10
167
|
|
|
11
168
|
|
|
12
169
|
RESOURCES_QUERY = """
|