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.

Files changed (30) 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/microsoft_defender/defender.py +326 -5
  8. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  9. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  10. regscale/integrations/commercial/synqly/assets.py +99 -16
  11. regscale/integrations/commercial/synqly/query_builder.py +533 -0
  12. regscale/integrations/commercial/synqly/vulnerabilities.py +134 -14
  13. regscale/integrations/commercial/wizv2/compliance_report.py +22 -0
  14. regscale/integrations/compliance_integration.py +17 -0
  15. regscale/integrations/scanner_integration.py +16 -0
  16. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  17. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +12 -2
  18. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  19. regscale/models/integration_models/synqly_models/synqly_model.py +47 -3
  20. regscale/models/regscale_models/compliance_settings.py +28 -0
  21. regscale/models/regscale_models/component.py +1 -0
  22. regscale/models/regscale_models/control_implementation.py +130 -1
  23. regscale/regscale.py +1 -1
  24. regscale/validation/record.py +23 -1
  25. {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/METADATA +1 -1
  26. {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +30 -28
  27. {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
  28. {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
  29. {regscale_cli-6.24.0.1.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
  30. {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 .defender_constants import APP_JSON
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
- data = {
61
- "resource": resource,
62
- "client_id": client_id,
63
- "client_secret": client_secret,
64
- "grant_type": "client_credentials",
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 and Microsoft Defender for Cloud."
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
- defender_data = response_data["value"]
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 = """