regscale-cli 6.21.1.0__py3-none-any.whl → 6.21.2.1__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 (35) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +8 -0
  3. regscale/integrations/commercial/__init__.py +8 -8
  4. regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
  5. regscale/integrations/commercial/microsoft_defender/__init__.py +0 -0
  6. regscale/integrations/commercial/{defender.py → microsoft_defender/defender.py} +38 -612
  7. regscale/integrations/commercial/microsoft_defender/defender_api.py +286 -0
  8. regscale/integrations/commercial/microsoft_defender/defender_constants.py +80 -0
  9. regscale/integrations/commercial/microsoft_defender/defender_scanner.py +168 -0
  10. regscale/integrations/commercial/qualys/__init__.py +24 -86
  11. regscale/integrations/commercial/qualys/containers.py +2 -0
  12. regscale/integrations/commercial/qualys/scanner.py +7 -2
  13. regscale/integrations/commercial/sonarcloud.py +110 -71
  14. regscale/integrations/commercial/wizv2/click.py +4 -1
  15. regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
  16. regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
  17. regscale/integrations/commercial/wizv2/policy_compliance.py +1471 -204
  18. regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
  19. regscale/integrations/commercial/wizv2/scanner.py +4 -4
  20. regscale/integrations/compliance_integration.py +213 -61
  21. regscale/integrations/public/fedramp/fedramp_five.py +92 -7
  22. regscale/integrations/scanner_integration.py +27 -4
  23. regscale/models/__init__.py +1 -1
  24. regscale/models/integration_models/cisa_kev_data.json +79 -3
  25. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  26. regscale/models/regscale_models/issue.py +29 -9
  27. regscale/models/regscale_models/milestone.py +15 -14
  28. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.1.dist-info}/METADATA +1 -1
  29. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.1.dist-info}/RECORD +33 -28
  30. tests/regscale/test_authorization.py +0 -65
  31. tests/regscale/test_init.py +0 -96
  32. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.1.dist-info}/LICENSE +0 -0
  33. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.1.dist-info}/WHEEL +0 -0
  34. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.1.dist-info}/entry_points.txt +0 -0
  35. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.1.dist-info}/top_level.txt +0 -0
@@ -2,21 +2,17 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """RegScale Microsoft Defender recommendations and alerts integration"""
4
4
  # standard python imports
5
- from concurrent.futures import ThreadPoolExecutor, as_completed
5
+ import logging
6
6
  from datetime import datetime, timedelta
7
- from json import JSONDecodeError
8
7
  from os import PathLike
8
+ from pathlib import Path
9
9
  from typing import Literal, Optional, Tuple, Union
10
10
 
11
11
  import click
12
- import requests
13
- from pathlib import Path
14
12
  from rich.console import Console
15
- from rich.progress import Progress
16
13
 
17
14
  from regscale.core.app.api import Api
18
15
  from regscale.core.app.internal.login import is_valid
19
- from regscale.core.app.logz import create_logger
20
16
  from regscale.core.app.utils.app_utils import (
21
17
  check_license,
22
18
  create_progress_object,
@@ -24,19 +20,19 @@ from regscale.core.app.utils.app_utils import (
24
20
  flatten_dict,
25
21
  get_current_datetime,
26
22
  reformat_str_date,
27
- uncamel_case,
28
23
  save_data_to,
24
+ uncamel_case,
29
25
  )
26
+ from regscale.integrations.commercial.microsoft_defender.defender_api import DefenderApi
27
+ from regscale.models import File, Issue, regscale_id, regscale_module, ssp_or_component_id
30
28
  from regscale.models.app_models.click import NotRequiredIf
31
- from regscale.models import regscale_id, regscale_module, regscale_ssp_id, Asset, Component, File, Issue
32
29
  from regscale.models.integration_models.defender_data import DefenderData
33
30
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
34
- from regscale.utils.string import generate_html_table_from_dict
35
31
 
36
32
  LOGIN_ERROR = "Login Invalid RegScale Credentials, please login for a new token."
37
33
  console = Console()
38
34
  job_progress = create_progress_object()
39
- logger = create_logger()
35
+ logger = logging.getLogger("regscale")
40
36
  unique_recs = []
41
37
  issues_to_create = []
42
38
  closed = []
@@ -109,13 +105,23 @@ def sync_365_recommendations(regscale_id: Optional[int] = None, regscale_module:
109
105
 
110
106
 
111
107
  @defender.command(name="sync_cloud_resources")
112
- @regscale_ssp_id()
113
- def sync_cloud_resources(regscale_ssp_id: int):
108
+ @ssp_or_component_id()
109
+ def sync_cloud_resources(regscale_ssp_id: Optional[int] = None, component_id: Optional[int] = None):
114
110
  """
115
111
  Get Microsoft Defender for Cloud resources and create RegScale assets with the information from Microsoft
116
112
  Defender for Cloud.
117
113
  """
118
- sync_resources(ssp_id=regscale_ssp_id)
114
+ if not regscale_ssp_id and not component_id:
115
+ error_and_exit("Please provide a RegScale SSP ID or component ID to sync Azure resources to.")
116
+ from regscale.integrations.commercial.microsoft_defender.defender_scanner import DefenderScanner
117
+
118
+ scanner_kwargs = {
119
+ "system": "cloud",
120
+ "plan_id": regscale_ssp_id or component_id,
121
+ "is_component": component_id is not None,
122
+ }
123
+ defender_scanner = DefenderScanner(**scanner_kwargs)
124
+ defender_scanner.sync_assets(**scanner_kwargs)
119
125
 
120
126
 
121
127
  @defender.command(name="export_resources")
@@ -269,18 +275,9 @@ def authenticate(system: Literal["cloud", "365"]) -> None:
269
275
  :param Literal["cloud", "365"] system: The system to authenticate for, either Defender 365 or Defender for Cloud
270
276
  :rtype: None
271
277
  """
272
- app = check_license()
273
- api = Api()
274
- if system == "365":
275
- url = "https://api.securitycenter.microsoft.com/api/alerts"
276
- elif system == "cloud":
277
- url = (
278
- f'https://management.azure.com/subscriptions/{app.config["azureCloudSubscriptionId"]}/'
279
- + "providers/Microsoft.Security/alerts?api-version=2022-01-01"
280
- )
281
- else:
282
- error_and_exit("Please enter 365 or cloud for the system.")
283
- check_token(api=api, system=system, url=url)
278
+ _ = check_license()
279
+ defender_api = DefenderApi(system=system)
280
+ defender_api.get_token()
284
281
 
285
282
 
286
283
  def sync_defender_and_regscale(
@@ -303,6 +300,7 @@ def sync_defender_and_regscale(
303
300
  # check if RegScale token is valid:
304
301
  if not is_valid(app=app):
305
302
  error_and_exit(LOGIN_ERROR)
303
+ defender_api = DefenderApi(system=system)
306
304
  mapping_key = f"{system}_{defender_object}"
307
305
  url_mapping = {
308
306
  "365_alerts": "https://api.securitycenter.microsoft.com/api/alerts",
@@ -320,19 +318,10 @@ def sync_defender_and_regscale(
320
318
  "cloud_alerts": map_cloud_alert_to_issue,
321
319
  "cloud_recommendations": map_cloud_recommendation_to_issue,
322
320
  }
323
- # check the azure token, get a new one if needed
324
- token = check_token(api=api, system=system, url=url)
325
-
326
- # set headers for the data
327
- headers = {"Content-Type": APP_JSON, "Authorization": token}
328
321
  logging_object = f"{defender_object[:-1]}(s)"
329
322
  logging_system = "365" if system == "365" else "for Cloud"
330
323
  logger.info(f"Retrieving Microsoft Defender {system.title()} {logging_object}...")
331
- if defender_objects := get_items_from_azure(
332
- api=api,
333
- headers=headers,
334
- url=url,
335
- ):
324
+ if defender_objects := defender_api.get_items_from_azure(url=url):
336
325
  defender_data = [
337
326
  DefenderData(id=data[defender_key], data=data, system=system, object=defender_object)
338
327
  for data in defender_objects
@@ -449,168 +438,6 @@ def sync_defender_and_regscale(
449
438
  )
450
439
 
451
440
 
452
- def check_token(api: Api, system: Literal["cloud", "365"], url: Optional[str] = None) -> str:
453
- """
454
- Function to check if current Azure token from init.yaml is valid, if not replace it
455
-
456
- :param Api api: API object
457
- :param Literal["cloud", "365"] system: Which system to check JWT for, either Defender 365 or Defender for Cloud
458
- :param str url: The URL to use for authentication, defaults to None
459
- :return: returns JWT for Microsoft 365 Defender or Microsoft Defender for Cloud depending on system provided
460
- :rtype: str
461
- """
462
- # set up variables for the provided system
463
- if system == "cloud":
464
- key = "azureCloudAccessToken"
465
- elif system.lower() == "365":
466
- key = "azure365AccessToken"
467
- else:
468
- error_and_exit(
469
- f"{system.title()} is not supported, only Microsoft 365 Defender and Microsoft Defender for Cloud."
470
- )
471
- current_token = api.config[key]
472
- # check the token if it isn't blank
473
- if current_token and url:
474
- # set the headers
475
- header = {"Content-Type": APP_JSON, "Authorization": current_token}
476
- # test current token by getting recommendations
477
- token_pass = api.get(url=url, headers=header)
478
- # check the status code
479
- if getattr(token_pass, "status_code", 0) == 200:
480
- # token still valid, return it
481
- token = api.config[key]
482
- logger.info(
483
- "Current token for %s is still valid and will be used for future requests.",
484
- system.title(),
485
- )
486
- elif getattr(token_pass, "status_code", 0) == 403:
487
- # token doesn't have permissions, notify user and exit
488
- error_and_exit(
489
- "Incorrect permissions set for application. Cannot retrieve recommendations.\n"
490
- + f"{token_pass.status_code}: {token_pass.reason}\n{token_pass.text}"
491
- )
492
- else:
493
- # token is no longer valid, get a new one
494
- token = get_token(api=api, system=system)
495
- # token is empty, get a new token
496
- else:
497
- token = get_token(api=api, system=system)
498
- return token
499
-
500
-
501
- def get_token(api: Api, system: Literal["cloud", "365"]) -> str:
502
- """
503
- Function to get a token from Microsoft Azure and saves it to init.yaml
504
-
505
- :param Api api: API object
506
- :param Literal[str] system: Which platform to authenticate for Microsoft Defender, cloud or 365
507
- :return: JWT from Azure
508
- :rtype: str
509
- """
510
- # set the url and body for request
511
- if system == "365":
512
- url = f'https://login.windows.net/{api.config["azure365TenantId"]}/oauth2/token'
513
- client_id = api.config["azure365ClientId"]
514
- client_secret = api.config["azure365Secret"]
515
- resource = "https://api.securitycenter.windows.com"
516
- key = "azure365AccessToken"
517
- elif system == "cloud":
518
- url = f'https://login.microsoftonline.com/{api.config["azureCloudTenantId"]}/oauth2/token'
519
- client_id = api.config["azureCloudClientId"]
520
- client_secret = api.config["azureCloudSecret"]
521
- resource = "https://management.azure.com"
522
- key = "azureCloudAccessToken"
523
- else:
524
- error_and_exit(
525
- f"{system.title()} is not supported, only Microsoft `365` Defender and Microsoft Defender for `Cloud`."
526
- )
527
- data = {
528
- "resource": resource,
529
- "client_id": client_id,
530
- "client_secret": client_secret,
531
- "grant_type": "client_credentials",
532
- }
533
- # get the data
534
- response = api.post(
535
- url=url,
536
- headers={"Content-Type": "application/x-www-form-urlencoded"},
537
- data=data,
538
- )
539
- try:
540
- return parse_and_save_token(response, api, key, system)
541
- except KeyError as ex:
542
- # notify user we weren't able to get a token and exit
543
- error_and_exit(f"Didn't receive token from Azure.\n{ex}\n{response.text}")
544
- except JSONDecodeError as ex:
545
- # notify user we weren't able to get a token and exit
546
- error_and_exit(f"Unable to authenticate with Azure.\n{ex}\n{response.text}")
547
-
548
-
549
- def parse_and_save_token(response: requests.Response, api: Api, key: str, system: str) -> str:
550
- """
551
- Function to parse the token from the response and save it to init.yaml
552
-
553
- :param requests.Response response: Response from API call
554
- :param Api api: API object
555
- :param str key: Key to use for init.yaml token update
556
- :param str system: Which system to check JWT for, either Defender 365 or Defender for Cloud
557
- :return: JWT from Azure for the provided system
558
- :rtype: str
559
- """
560
- # try to read the response and parse the token
561
- res = response.json()
562
- token = res["access_token"]
563
-
564
- # add the token to init.yaml
565
- api.config[key] = f"Bearer {token}"
566
-
567
- # write the changes back to file
568
- api.app.save_config(api.config) # type: ignore
569
-
570
- # notify the user we were successful
571
- logger.info(f"Azure {system.title()} Login Successful! Init.yaml file was updated with the new access token.")
572
- # return the token string
573
- return api.config[key]
574
-
575
-
576
- def get_items_from_azure(api: Api, headers: dict, url: str) -> list:
577
- """
578
- Function to get data from Microsoft Defender returns the data as a list while handling pagination
579
-
580
- :param Api api: API object
581
- :param dict headers: Headers used for API call
582
- :param str url: URL to use for the API call
583
- :return: list of recommendations
584
- :rtype: list
585
- """
586
- # get the data via api call
587
- response = api.get(url=url, headers=headers)
588
- if response.status_code != 200:
589
- error_and_exit(
590
- f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
591
- + f"\n{response.text}",
592
- )
593
- # try to read the response
594
- try:
595
- response_data = response.json()
596
- # try to get the values from the api response
597
- defender_data = response_data["value"]
598
- except JSONDecodeError:
599
- # notify user if there was a json decode error from API response and exit
600
- error_and_exit("JSON Decode error")
601
- except KeyError:
602
- # notify user there was no data from API response and exit
603
- error_and_exit(
604
- f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.text}"
605
- )
606
- # check if pagination is required to fetch all data from Microsoft Defender
607
- if next_link := response_data.get("nextLink"):
608
- # get the rest of the data
609
- defender_data.extend(get_items_from_azure(api=api, headers=headers, url=next_link))
610
- # return the defender recommendations
611
- return defender_data
612
-
613
-
614
441
  def get_due_date(score: Union[str, int, None], config: dict, key: str) -> str:
615
442
  """
616
443
  Function to return due date based on the severity score of
@@ -1038,381 +865,6 @@ def map_cloud_recommendation_to_issue(data: DefenderData, config: dict, descript
1038
865
  )
1039
866
 
1040
867
 
1041
- def fetch_resources_from_azure(
1042
- api: Api, headers: dict, query: Optional[str] = None, skip_token: Optional[str] = None, record_count: int = 0
1043
- ) -> list[dict]:
1044
- """
1045
- Function to fetch Microsoft Defender resources from Azure
1046
-
1047
- :param Api api: API object
1048
- :param dict headers: Headers used for API call
1049
- :param Optional[str] query: Query to use for the API call, if none provided,
1050
- :param Optional[str] skip_token: Token to skip results, used during pagination, defaults to None
1051
- :param int record_count: Number of records fetched, defaults to 0, used for logging during pagination
1052
- :return: list of Microsoft Defender resources
1053
- :rtype: list[dict]
1054
- """
1055
- url = "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01"
1056
- if query:
1057
- payload = {"query": query}
1058
- else:
1059
- payload = {
1060
- "query": query or "resources",
1061
- "subscriptions": [api.config["azureCloudSubscriptionId"]],
1062
- }
1063
- if skip_token:
1064
- payload["options"] = {"$skipToken": skip_token}
1065
- api.logger.info("Retrieving more Microsoft Defender resources from Azure...")
1066
- else:
1067
- api.logger.info("Retrieving Microsoft Defender resources from Azure...")
1068
- response = api.post(url=url, headers=headers, json=payload)
1069
- if response.status_code != 200:
1070
- error_and_exit(
1071
- f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
1072
- + f"\n{response.text}",
1073
- )
1074
- try:
1075
- response_data = response.json()
1076
- total_records = response_data.get("totalRecords", 0)
1077
- count = response_data.get("count", 0)
1078
- api.logger.info(f"Received {count + record_count}/{total_records} items from Microsoft Defender.")
1079
- # try to get the values from the api response
1080
- defender_data = response_data["data"]
1081
- except JSONDecodeError:
1082
- # notify user if there was a json decode error from API response and exit
1083
- error_and_exit("JSON Decode error")
1084
- except KeyError:
1085
- # notify user there was no data from API response and exit
1086
- error_and_exit(
1087
- f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.reason}\n"
1088
- + f"{response.text}"
1089
- )
1090
- # check if pagination is required to fetch all data from Microsoft Defender
1091
- skip_token = response_data.get("$skipToken")
1092
- if response.status_code == 200 and skip_token:
1093
- # get the rest of the data
1094
- defender_data.extend(
1095
- fetch_resources_from_azure(api=api, headers=headers, query=query, skip_token=skip_token, record_count=count)
1096
- )
1097
- # return the defender recommendations
1098
- return defender_data
1099
-
1100
-
1101
- def map_asset(data: dict, existing_assets: dict[str, Asset]) -> Asset:
1102
- """
1103
- Function to map data to an Asset object
1104
-
1105
- :param dict data: Data from Microsoft Defender
1106
- :param dict[str, Asset] existing_assets: Existing assets from RegScale
1107
- :return: Asset object
1108
- :rtype: Asset
1109
- """
1110
- asset_id = data.get("id")
1111
- properties = data.get("properties", {})
1112
- resource_type = data.get("type", "").lower()
1113
- try:
1114
- ip_mapping = {
1115
- "microsoft.network/networksecuritygroups": properties.get("securityRules", [{}])[0]
1116
- .get("properties", {})
1117
- .get("destinationAddressPrefix"),
1118
- "microsoft.network/virtualnetworks": properties.get("addressSpace", {}).get("addressPrefixes"),
1119
- "microsoft.app/managedenvironments": properties.get("staticIp"),
1120
- "microsoft.network/networkinterfaces": properties.get("ipConfigurations", [{}])[0]
1121
- .get("properties", {})
1122
- .get("privateIPAddress"),
1123
- }
1124
- except IndexError:
1125
- ip_mapping = {}
1126
- try:
1127
- fqdn_mapping = {
1128
- "microsoft.keyvault/vaults": properties.get("vaultUri"),
1129
- "microsoft.storage/storageaccounts": properties.get("primaryEndpoints", {}).get("blob"),
1130
- "microsoft.appconfiguration/configurationstores": properties.get("endpoint"),
1131
- "microsoft.dbforpostgresql/flexibleservers": properties.get("fullyQualifiedDomainName"),
1132
- AFD_ENDPOINTS: properties.get("hostName"),
1133
- "microsoft.containerregistry/registries": properties.get("loginServer"),
1134
- "microsoft.app/containerapps": properties.get("configuration", {}).get("ingress", {}).get("fqdn"),
1135
- "microsoft.network/privatednszones": data.get("name"),
1136
- "microsoft.cognitiveservices/accounts": properties.get("endpoint"),
1137
- }
1138
- except IndexError:
1139
- fqdn_mapping = {}
1140
- # pylint: disable=line-too-long
1141
- function_mapping = {
1142
- "microsoft.network/privateendpoints": "Private endpoint that links the private link and the nic together",
1143
- "microsoft.network/networkinterfaces": "Network Interface that connects to everything internal to the resource group",
1144
- "microsoft.network/privatednszones": "Dns zone that will connect to the private endpoint and network interfaces",
1145
- "microsoft.network/privatednszones/virtualnetworklinks": "Link for the Private DNS zone back to the vnet",
1146
- "microsoft.app/containerapps": "Application runner that houses the running Docker Container",
1147
- "microsoft.network/publicipaddresses": "Public ip address used for load balancing the container apps",
1148
- "microsoft.storage/storageaccounts": "Storage blob to house unstructured files uploaded to the platform",
1149
- "microsoft.network/networksecuritygroups": "Network protection for internal communications and load balancing",
1150
- "microsoft.network/networkwatchers/flowlogs": "Logs that determine the flow of traffic",
1151
- "microsoft.sql/servers/databases": "Database that houses application logs",
1152
- "microsoft.network/virtualnetworks": "Network Interface that determines what the valid IP range is for all internal resources",
1153
- "microsoft.portal/dashboards": "Dashboard that shows the status of the application and traffic",
1154
- "microsoft.dataprotection/backupvaults": "Azure Blob Storage Account backup location",
1155
- "microsoft.keyvault/vaults": "To securely store API keys, passwords, certificates, or cryptographic keys",
1156
- "microsoft.managedidentity/userassignedidentities": "Identity that connects all internal resources in the resource group",
1157
- "microsoft.app/managedenvironments": "Application environment to connect to the vnet",
1158
- "microsoft.sql/servers": "Server that will house the database for the application logs",
1159
- "microsoft.sql/servers/encryptionprotector": "Server encryption",
1160
- "microsoft.appconfiguration/configurationstores": "Configure, store, and retrieve parameters and settings. Store configuration for all system components in the environment",
1161
- "microsoft.insights/metricalerts": "Alerts that trigger when exceptions hit above 100",
1162
- "microsoft.insights/webtests": "Test to ensure the integrity of the app and alert when availability drops",
1163
- "microsoft.insights/components": "Insights and mapping for the data flow through the platform container application",
1164
- "microsoft.dbforpostgresql/flexibleservers": "Application Database for OpenAI and Automation containers",
1165
- "microsoft.network/loadbalancers": "Load Balancer that handles the load traffic for the containerapp",
1166
- "microsoft.insights/activitylogalerts": "Alert rule to send an email to the Action Group when the trigger event happens",
1167
- "microsoft.operationalinsights/workspaces": "Collection of Logs contained in a workspace",
1168
- "microsoft.insights/actiongroups": "Action Group to send Emails to when alerts trigger",
1169
- "microsoft.network/networkwatchers": "Monitor on the network to look for any suspecious activity",
1170
- "microsoft.app/managedenvironments/certificates": "Tls cert for the application environment",
1171
- "microsoft.authorization/roledefinitions": "Custom role definition",
1172
- "microsoft.alertsmanagement/actionrules": "Alert Processing Rule to show when to trigger",
1173
- "microsoft.network/frontdoorwebapplicationfirewallpolicies": "Waf protection policy that connects to the firewall and frontdoor",
1174
- "microsoft.cdn/profiles": "Monitoring and controlling inbound and outbound traffic to the environment. Functions as a Web Application Firewall (WAF) and performs Network Address Translation (NAT) connecting public networks to a series of private tenant Virtual Networks (VNets)",
1175
- "microsoft.resourcegraph/queries": "Query to return all resources in the SaaS subscription in the resource graph",
1176
- "microsoft.network/firewallpolicies": "Firewall policy that connects to frontdoor and handles our traffic coming into the system",
1177
- AFD_ENDPOINTS: "Endpoint that all of the routes attach to",
1178
- "microsoft.containerregistry/registries": "House the Docker container image for ContainerApp pull",
1179
- "microsoft.operationalinsights/querypacks": "Log analytics query that loads default queries for running",
1180
- "microsoft.alertsmanagement/smartdetectoralertrules": "Failure Anomalies notifies you of an unusual rise in the rate of failed HTTP requests or dependency calls.",
1181
- }
1182
- # pylint: enable=line-too-long
1183
- from regscale.models.regscale_models import AssetType, AssetCategory, AssetStatus
1184
-
1185
- if asset_id in existing_assets:
1186
- return existing_assets[asset_id]
1187
- mapped_asset = Asset(
1188
- extra_data={"type": f'{data.get("type")}'},
1189
- id=0,
1190
- description=generate_html_table_from_dict(data),
1191
- status=AssetStatus.Active.value,
1192
- name=data.get("name", asset_id),
1193
- assetType=AssetType.Other,
1194
- assetCategory=AssetCategory.Software,
1195
- otherTrackingNumber=asset_id,
1196
- softwareFunction=function_mapping.get(resource_type, properties.get("description")),
1197
- ipAddress=str(ip_mapping.get(resource_type, properties.get("ipAddress"))),
1198
- bPublicFacing=resource_type in ["microsoft.cdn/profiles", AFD_ENDPOINTS],
1199
- bAuthenticatedScan=resource_type
1200
- not in [
1201
- "microsoft.alertsmanagement/actionrules",
1202
- "microsoft.alertsmanagement/smartdetectoralertrules",
1203
- ],
1204
- bVirtual=True,
1205
- baselineConfiguration="Azure Hardening Guide",
1206
- )
1207
- if fqdn := fqdn_mapping.get(resource_type, properties.get("dnsSettings", {}).get("fqdn")):
1208
- mapped_asset.fqdn = fqdn
1209
- mapped_asset.description += f"<p>FQDN: {fqdn}</p>"
1210
- return mapped_asset
1211
-
1212
-
1213
- def map_assets(data: list[dict], existing_assets: list[Asset], progress: Progress) -> list[Asset]:
1214
- """
1215
- Function to map data to an Asset object using threads
1216
-
1217
- :param list[dict] data: Data from Microsoft Defender Resource APi
1218
- :param list[Asset] existing_assets: List of existing assets, used to prevent duplicates
1219
- :param Progress progress: Progress object to track progress
1220
- :return: List of Asset objects
1221
- :rtype: list[Asset]
1222
- """
1223
- existing_assets = {asset.otherTrackingNumber: asset for asset in existing_assets}
1224
- from regscale.integrations.variables import ScannerVariables
1225
-
1226
- with ThreadPoolExecutor(max_workers=ScannerVariables.threadMaxWorkers) as executor:
1227
- futures = [executor.submit(map_asset, asset, existing_assets) for asset in data]
1228
- mapping_assets = progress.add_task(
1229
- f"[#f8b737]Mapping Microsoft Defender {len(data)} resource(s) to RegScale assets...", total=len(data)
1230
- )
1231
- assets = []
1232
- for future in as_completed(futures):
1233
- if result := future.result():
1234
- assets.append(result)
1235
- progress.update(mapping_assets, advance=1)
1236
- logger.info(f"Mapped {len(assets)}/{len(data)} Microsoft Defender resource(s) to RegScale asset(s).")
1237
- return assets
1238
-
1239
-
1240
- def sync_resources(ssp_id: int):
1241
- """
1242
- Function to sync Microsoft Defender resources with RegScale assets
1243
-
1244
- :param int ssp_id: The RegScale SSP ID to sync resources to
1245
- :rtype: None
1246
- """
1247
- app = check_license()
1248
- api = Api()
1249
- # check if RegScale token is valid:
1250
- if not is_valid(app=app):
1251
- error_and_exit(LOGIN_ERROR)
1252
- token = check_token(api=api, system="cloud")
1253
- headers = {"Content-Type": APP_JSON, "Authorization": token}
1254
- cloud_resources = fetch_resources_from_azure(api=api, headers=headers)
1255
- app.logger.info(f"Retrieving assets from RegScale for security plan #{ssp_id}...")
1256
- if assets := Asset.get_map(plan_id=ssp_id):
1257
- assets = list(assets.values())
1258
- with create_progress_object() as progress:
1259
- logger.info(f"Retrieved {len(assets)} asset(s) from RegScale.")
1260
- cloud_assets = map_assets(data=cloud_resources, existing_assets=assets, progress=progress)
1261
- azure_comps = {asset.extra_data.get("type") for asset in cloud_assets if asset.extra_data.get("type")}
1262
- api.logger.info("Fetching components from RegScale...")
1263
- if existing_components := Component.get_map(plan_id=ssp_id):
1264
- logger.info(f"Retrieved {len(existing_components)} component(s) from RegScale.")
1265
- existing_components = list(existing_components.values())
1266
- comp_mapping = {
1267
- component.title: component for component in existing_components if component.title in azure_comps
1268
- }
1269
- logger.info(
1270
- f"Found {len(comp_mapping)}/{len(azure_comps)} component(s) required for importing "
1271
- "Microsoft Defender resources as asset(s) in RegScale."
1272
- )
1273
- else:
1274
- existing_components = []
1275
- comp_mapping = {}
1276
- if missing_comps_mapping := map_missing_components(
1277
- components=azure_comps,
1278
- existing_components=existing_components,
1279
- ssp_id=ssp_id,
1280
- progress=progress,
1281
- ):
1282
- new_components = create_objects_with_threads(
1283
- "components", list(missing_comps_mapping.values()), progress=progress
1284
- )
1285
- missing_comps_mapping = {component.description: component for component in new_components}
1286
- comp_mapping.update(missing_comps_mapping)
1287
- if assets_to_create := map_assets_to_components(
1288
- assets=[asset for asset in cloud_assets if asset.id == 0],
1289
- component_mapping=comp_mapping,
1290
- ssp_id=ssp_id,
1291
- progress=progress,
1292
- ):
1293
- new_assets = create_objects_with_threads("assets", assets_to_create, progress=progress)
1294
- logger.info(f"Created {len(new_assets)}/{len(cloud_assets)} asset(s) in RegScale.")
1295
- else:
1296
- logger.info(f"[green]All {len(cloud_assets)} Microsoft Defender resource(s) already exist in RegScale.")
1297
-
1298
-
1299
- def map_assets_to_components(
1300
- assets: list[Asset], component_mapping: dict[str, Component], ssp_id: int, progress: Progress
1301
- ) -> list[Asset]:
1302
- """
1303
- Function to map assets to components
1304
-
1305
- :param list[Asset] assets: List of assets to map
1306
- :param dict[str, Component] component_mapping: Dictionary of component titles and their corresponding component
1307
- :param int ssp_id: The RegScale SSP ID to add the assets to, used if no component is found to map to
1308
- :param Progress progress: Progress object to track progress
1309
- :return: List of assets with updated parentIds and parentModules
1310
- :rtype: list[Asset]
1311
- """
1312
- updated_assets = []
1313
- if assets:
1314
- mapping_assets = progress.add_task(
1315
- f"[#f8b737]Mapping {len(assets)} asset(s) to RegScale components...", total=len(assets)
1316
- )
1317
- for asset in assets:
1318
- if asset_type := asset.extra_data.get("type"):
1319
- if component := component_mapping.get(asset_type):
1320
- asset.extra_data["componentId"] = component.id
1321
- asset.parentId = ssp_id
1322
- asset.parentModule = "securityplans"
1323
- updated_assets.append(asset)
1324
- progress.update(mapping_assets, advance=1)
1325
- logger.info(f"Updated parentIds and parentModules for {len(assets)} asset(s).")
1326
- return updated_assets
1327
-
1328
-
1329
- def map_missing_components(components: set, existing_components: list, ssp_id: int, progress: Progress) -> dict:
1330
- """
1331
- Function to create missing components in RegScale
1332
-
1333
- :param set components: Set of expected components to create
1334
- :param list existing_components: List of existing components
1335
- :param int ssp_id: The RegScale SSP ID to add the components to
1336
- :param Progress progress: Progress object to track progress
1337
- :return: Dictionary of component titles and their corresponding component objects
1338
- :rtype: dict
1339
- """
1340
- from regscale.models.regscale_models import ComponentType, ComponentStatus
1341
-
1342
- missing_components = components - {component.title for component in existing_components}
1343
- component_mapping = {}
1344
- if missing_components:
1345
- mapping_components = progress.add_task(
1346
- f"[#ef5d23]Mapping {len(missing_components)} missing component(s)...", total=len(missing_components)
1347
- )
1348
- for component in missing_components:
1349
- component_obj = Component(
1350
- id=0,
1351
- title=component,
1352
- description=component,
1353
- componentType=ComponentType.Software.value,
1354
- status=ComponentStatus.Active.value,
1355
- securityPlansId=ssp_id,
1356
- )
1357
- component_mapping[component] = component_obj
1358
- progress.update(mapping_components, advance=1)
1359
- logger.info(f"Mapped {len(component_mapping)}/{len(missing_components)} missing component(s).")
1360
- return component_mapping
1361
-
1362
-
1363
- def create_objects_with_threads(object_name: str, objects: list, progress: Progress) -> list:
1364
- """
1365
- Create a list of objects in RegScale using threads
1366
-
1367
- :param str object_name: Type of object to create
1368
- :param list objects: A list of objects to create
1369
- :param Progress progress: Progress object to track progress
1370
- :rtype: List of created objects
1371
- :rtype: list
1372
- """
1373
- from regscale.integrations.variables import ScannerVariables
1374
-
1375
- created_objects = []
1376
- created_mappings = []
1377
- failed_count = 0
1378
- asset_component_ids = {
1379
- obj.otherTrackingNumber: obj.extra_data["componentId"]
1380
- for obj in objects
1381
- if isinstance(obj, Asset) and obj.extra_data.get("componentId")
1382
- }
1383
- with ThreadPoolExecutor(max_workers=ScannerVariables.threadMaxWorkers) as executor:
1384
- futures = [executor.submit(obj.create) for obj in objects]
1385
- create_task = progress.add_task(f"[#21a5bb]Creating {len(objects)} {object_name}...", total=len(objects))
1386
- for future in as_completed(futures):
1387
- try:
1388
- if future.result():
1389
- res = future.result()
1390
- if isinstance(res, Asset) and res.otherTrackingNumber in asset_component_ids:
1391
- from regscale.models.regscale_models import AssetMapping
1392
-
1393
- new_mapping = AssetMapping(
1394
- assetId=res.id, componentId=asset_component_ids[res.otherTrackingNumber]
1395
- ).create()
1396
- created_mappings.append(new_mapping)
1397
- elif isinstance(res, Component):
1398
- from regscale.models.regscale_models import ComponentMapping
1399
-
1400
- new_mapping = ComponentMapping(securityPlanId=res.securityPlansId, componentId=res.id).create()
1401
- created_mappings.append(new_mapping)
1402
- created_objects.append(res)
1403
- else:
1404
- failed_count += 1
1405
- except Exception as e:
1406
- logger.error(f"Failed to create {object_name[:-1]}: {e}")
1407
- failed_count += 1
1408
- progress.update(create_task, advance=1)
1409
- logger.info(
1410
- f"Created {len(created_objects)}/{len(objects)} {object_name}, {len(created_mappings)} mappings, and failed "
1411
- f"to create {failed_count} {object_name}."
1412
- )
1413
- return created_objects
1414
-
1415
-
1416
868
  def export_resources(parent_id: int, parent_module: str, query_name: str, no_upload: bool, all_queries: bool) -> None:
1417
869
  """
1418
870
  Export data from Microsoft Defender for Cloud queries and save them to a .csv file
@@ -1425,21 +877,11 @@ def export_resources(parent_id: int, parent_module: str, query_name: str, no_upl
1425
877
  :rtype: None
1426
878
  """
1427
879
  app = check_license()
1428
- api = Api()
1429
880
  # check if RegScale token is valid:
1430
881
  if not is_valid(app=app):
1431
882
  error_and_exit(LOGIN_ERROR)
1432
- token = check_token(api=api, system="cloud")
1433
- headers = {"Content-Type": APP_JSON, "Authorization": token}
1434
- url = f"https://management.azure.com/subscriptions/{api.config['azureCloudSubscriptionId']}/providers/Microsoft.ResourceGraph/queries?api-version=2024-04-01"
1435
- logger.info("Fetching saved queries from Azure Resource Graph...")
1436
- response = api.get(url=url, headers=headers)
1437
- logger.info(f"Azure API response status: {response.status_code}")
1438
- if response.raise_for_status():
1439
- response.raise_for_status()
1440
- logger.info("Parsing Azure API response...")
1441
- cloud_queries = response.json().get("value", [])
1442
- logger.info(f"Found {len(cloud_queries)} saved queries in Azure")
883
+ defender_api = DefenderApi(system="cloud")
884
+ cloud_queries = defender_api.fetch_queries_from_azure()
1443
885
  # Add user feedback if no queries are found
1444
886
  if not cloud_queries:
1445
887
  logger.warning("No saved queries found in Azure. Please create at least one query to use this export function.")
@@ -1448,8 +890,7 @@ def export_resources(parent_id: int, parent_module: str, query_name: str, no_upl
1448
890
  logger.info(f"Exporting all {len(cloud_queries)} queries...")
1449
891
  for query in cloud_queries:
1450
892
  fetch_save_and_upload_query(
1451
- api=api,
1452
- headers=headers,
893
+ defender_api=defender_api,
1453
894
  query=query,
1454
895
  parent_id=parent_id,
1455
896
  parent_module=parent_module,
@@ -1458,7 +899,11 @@ def export_resources(parent_id: int, parent_module: str, query_name: str, no_upl
1458
899
  else:
1459
900
  query = prompt_user_for_query_selection(queries=cloud_queries, query_name=query_name)
1460
901
  fetch_save_and_upload_query(
1461
- api=api, headers=headers, query=query, parent_id=parent_id, parent_module=parent_module, no_upload=no_upload
902
+ defender_api=defender_api,
903
+ query=query,
904
+ parent_id=parent_id,
905
+ parent_module=parent_module,
906
+ no_upload=no_upload,
1462
907
  )
1463
908
 
1464
909
 
@@ -1478,21 +923,20 @@ def prompt_user_for_query_selection(queries: list, query_name: Optional[str] = N
1478
923
 
1479
924
 
1480
925
  def fetch_save_and_upload_query(
1481
- api: Api, headers: dict, query: dict, parent_id: int, parent_module: str, no_upload: bool
926
+ defender_api: DefenderApi, query: dict, parent_id: int, parent_module: str, no_upload: bool
1482
927
  ) -> None:
1483
928
  """
1484
929
  Function to fetch Microsoft Defender queries from Azure and save them to a .xlsx file
1485
930
 
1486
- :param Api api: The API object, used to call Microsoft Defender
1487
- :param dict headers: The headers to use for the request
931
+ :param DefenderApi defender_api: The Defender API object, used to call Microsoft Defender
1488
932
  :param dict query: The query object to parse and run
1489
933
  :param int parent_id: The RegScale ID to upload the results to
1490
934
  :param str parent_module: The RegScale module to upload the results to
1491
935
  :param bool no_upload: Flag to skip uploading the exported .csv file to RegScale
1492
936
  :rtype: None
1493
937
  """
1494
- api.logger.info(f"Exporting data from Microsoft Defender for Cloud query: {query['name']}...")
1495
- data = fetch_and_run_query(api=api, headers=headers, query=query)
938
+ logger.info(f"Exporting data from Microsoft Defender for Cloud query: {query['name']}...")
939
+ data = defender_api.fetch_and_run_query(query=query)
1496
940
  todays_date = get_current_datetime(dt_format="%Y%m%d")
1497
941
  file_path = Path(f"./artifacts/{query['name']}_{todays_date}.csv")
1498
942
  save_data_to(file=file_path, data=data, transpose_data=False)
@@ -1500,24 +944,6 @@ def fetch_save_and_upload_query(
1500
944
  file_name=file_path,
1501
945
  parent_id=parent_id,
1502
946
  parent_module=parent_module,
1503
- api=api,
947
+ api=defender_api.api,
1504
948
  ):
1505
- api.logger.info(f"Successfully uploaded {file_path.name} to {parent_module} #{parent_id} in RegScale.")
1506
-
1507
-
1508
- def fetch_and_run_query(api: Api, headers: dict, query: dict) -> list[dict]:
1509
- """
1510
- Function to fetch Microsoft Defender queries from Azure and run them
1511
-
1512
- :param Api api: The API object, used to call Microsoft Defender
1513
- :param dict headers: The headers to use for the request
1514
- :param dict query: The query object to parse and run
1515
- :return: list of Microsoft Defender resources by using the query
1516
- :rtype: list[dict]
1517
- """
1518
- url = f"https://management.azure.com/subscriptions/{query['subscriptionId']}/resourceGroups/{query['resourceGroup']}/providers/Microsoft.ResourceGraph/queries/{query['name']}?api-version=2024-04-01"
1519
- response = api.get(url=url, headers=headers)
1520
- if response.raise_for_status():
1521
- response.raise_for_status()
1522
- query = response.json().get("properties", {}).get("query")
1523
- return fetch_resources_from_azure(api=api, headers=headers, query=query)
949
+ logger.info(f"Successfully uploaded {file_path.name} to {parent_module} #{parent_id} in RegScale.")