regscale-cli 6.21.1.0__py3-none-any.whl → 6.21.2.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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +7 -0
- regscale/integrations/commercial/__init__.py +8 -8
- regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
- regscale/integrations/commercial/microsoft_defender/__init__.py +0 -0
- regscale/integrations/commercial/{defender.py → microsoft_defender/defender.py} +38 -612
- regscale/integrations/commercial/microsoft_defender/defender_api.py +286 -0
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +80 -0
- regscale/integrations/commercial/microsoft_defender/defender_scanner.py +168 -0
- regscale/integrations/commercial/qualys/__init__.py +24 -86
- regscale/integrations/commercial/qualys/containers.py +2 -0
- regscale/integrations/commercial/qualys/scanner.py +7 -2
- regscale/integrations/commercial/sonarcloud.py +110 -71
- regscale/integrations/commercial/wizv2/click.py +4 -1
- regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
- regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +1402 -203
- regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
- regscale/integrations/commercial/wizv2/scanner.py +4 -4
- regscale/integrations/compliance_integration.py +212 -60
- regscale/integrations/public/fedramp/fedramp_five.py +92 -7
- regscale/integrations/scanner_integration.py +27 -4
- regscale/models/__init__.py +1 -1
- regscale/models/integration_models/cisa_kev_data.json +33 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/issue.py +29 -9
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +32 -27
- tests/regscale/test_authorization.py +0 -65
- tests/regscale/test_init.py +0 -96
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.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
|
-
|
|
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 =
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1495
|
-
data = fetch_and_run_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
|
-
|
|
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.")
|