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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to handle API calls to Microsoft Defender for Cloud
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from json import JSONDecodeError
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import Any, Literal, Optional
|
|
8
|
+
|
|
9
|
+
from requests import Response
|
|
10
|
+
|
|
11
|
+
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
|
+
|
|
15
|
+
logger = getLogger("regscale")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DefenderApi:
|
|
19
|
+
"""
|
|
20
|
+
Class to handle API calls to Microsoft Defender 365 or Microsoft Defender for Cloud
|
|
21
|
+
|
|
22
|
+
:param Literal["cloud", "365"] system: Which system to make API calls to, either cloud or 365
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, system: Literal["cloud", "365"]):
|
|
26
|
+
self.api: Api = Api()
|
|
27
|
+
self.config: dict = self.api.config
|
|
28
|
+
self.system: Literal["cloud", "365"] = system
|
|
29
|
+
self.headers: dict = self.set_headers()
|
|
30
|
+
self.decode_error: str = "JSON Decode error"
|
|
31
|
+
self.skip_token_key: str = "$skipToken"
|
|
32
|
+
|
|
33
|
+
def set_headers(self) -> dict:
|
|
34
|
+
"""
|
|
35
|
+
Function to set the headers for the API calls
|
|
36
|
+
"""
|
|
37
|
+
token = self.check_token()
|
|
38
|
+
return {"Content-Type": APP_JSON, "Authorization": token}
|
|
39
|
+
|
|
40
|
+
def get_token(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Function to get a token from Microsoft Azure and saves it to init.yaml
|
|
43
|
+
|
|
44
|
+
:return: JWT from Azure
|
|
45
|
+
:rtype: str
|
|
46
|
+
"""
|
|
47
|
+
# set the url and body for request
|
|
48
|
+
if self.system == "365":
|
|
49
|
+
url = f'https://login.windows.net/{self.config["azure365TenantId"]}/oauth2/token'
|
|
50
|
+
client_id = self.config["azure365ClientId"]
|
|
51
|
+
client_secret = self.config["azure365Secret"]
|
|
52
|
+
resource = "https://api.securitycenter.windows.com"
|
|
53
|
+
key = "azure365AccessToken"
|
|
54
|
+
elif self.system == "cloud":
|
|
55
|
+
url = f'https://login.microsoftonline.com/{self.config["azureCloudTenantId"]}/oauth2/token'
|
|
56
|
+
client_id = self.config["azureCloudClientId"]
|
|
57
|
+
client_secret = self.config["azureCloudSecret"]
|
|
58
|
+
resource = "https://management.azure.com"
|
|
59
|
+
key = "azureCloudAccessToken"
|
|
60
|
+
data = {
|
|
61
|
+
"resource": resource,
|
|
62
|
+
"client_id": client_id,
|
|
63
|
+
"client_secret": client_secret,
|
|
64
|
+
"grant_type": "client_credentials",
|
|
65
|
+
}
|
|
66
|
+
# get the data
|
|
67
|
+
response = self.api.post(
|
|
68
|
+
url=url,
|
|
69
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
70
|
+
data=data,
|
|
71
|
+
)
|
|
72
|
+
try:
|
|
73
|
+
return self._parse_and_save_token(response, key)
|
|
74
|
+
except KeyError as ex:
|
|
75
|
+
# notify user we weren't able to get a token and exit
|
|
76
|
+
error_and_exit(f"Didn't receive token from Azure.\n{ex}\n{response.text}")
|
|
77
|
+
except JSONDecodeError as ex:
|
|
78
|
+
# notify user we weren't able to get a token and exit
|
|
79
|
+
error_and_exit(f"Unable to authenticate with Azure.\n{ex}\n{response.text}")
|
|
80
|
+
|
|
81
|
+
def check_token(self, url: Optional[str] = None) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Function to check if current Azure token from init.yaml is valid, if not replace it
|
|
84
|
+
|
|
85
|
+
:param str url: The URL to use for authentication, defaults to None
|
|
86
|
+
:return: returns JWT for Microsoft 365 Defender or Microsoft Defender for Cloud depending on system provided
|
|
87
|
+
:rtype: str
|
|
88
|
+
"""
|
|
89
|
+
# set up variables for the provided system
|
|
90
|
+
if self.system == "cloud":
|
|
91
|
+
key = "azureCloudAccessToken"
|
|
92
|
+
elif self.system.lower() == "365":
|
|
93
|
+
key = "azure365AccessToken"
|
|
94
|
+
else:
|
|
95
|
+
error_and_exit(
|
|
96
|
+
f"{self.system.title()} is not supported, only Microsoft 365 Defender and Microsoft Defender for Cloud."
|
|
97
|
+
)
|
|
98
|
+
current_token = self.config[key]
|
|
99
|
+
# check the token if it isn't blank
|
|
100
|
+
if current_token and url:
|
|
101
|
+
# set the headers
|
|
102
|
+
header = {"Content-Type": APP_JSON, "Authorization": current_token}
|
|
103
|
+
# test current token by getting recommendations
|
|
104
|
+
token_pass = self.api.get(url=url, headers=header)
|
|
105
|
+
# check the status code
|
|
106
|
+
if getattr(token_pass, "status_code", 0) == 200:
|
|
107
|
+
# token still valid, return it
|
|
108
|
+
token = self.config[key]
|
|
109
|
+
logger.info(
|
|
110
|
+
"Current token for %s is still valid and will be used for future requests.",
|
|
111
|
+
self.system.title(),
|
|
112
|
+
)
|
|
113
|
+
elif getattr(token_pass, "status_code", 0) == 403:
|
|
114
|
+
# token doesn't have permissions, notify user and exit
|
|
115
|
+
error_and_exit(
|
|
116
|
+
"Incorrect permissions set for application. Cannot retrieve recommendations.\n"
|
|
117
|
+
+ f"{token_pass.status_code}: {token_pass.reason}\n{token_pass.text}"
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
# token is no longer valid, get a new one
|
|
121
|
+
token = self.get_token()
|
|
122
|
+
# token is empty, get a new token
|
|
123
|
+
else:
|
|
124
|
+
token = self.get_token()
|
|
125
|
+
return token
|
|
126
|
+
|
|
127
|
+
def _parse_and_save_token(self, response: Response, key: str) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Function to parse the token from the response and save it to init.yaml
|
|
130
|
+
|
|
131
|
+
:param Response response: Response from API call
|
|
132
|
+
:param str key: Key to use for init.yaml token update
|
|
133
|
+
:return: JWT from Azure for the provided system
|
|
134
|
+
:rtype: str
|
|
135
|
+
"""
|
|
136
|
+
# try to read the response and parse the token
|
|
137
|
+
res = response.json()
|
|
138
|
+
token = res["access_token"]
|
|
139
|
+
|
|
140
|
+
# add the token to init.yaml
|
|
141
|
+
self.config[key] = f"Bearer {token}"
|
|
142
|
+
|
|
143
|
+
# write the changes back to file
|
|
144
|
+
self.api.app.save_config(self.api.config) # type: ignore
|
|
145
|
+
|
|
146
|
+
# notify the user we were successful
|
|
147
|
+
logger.info(
|
|
148
|
+
f"Azure {self.system.title()} Login Successful! Init.yaml file was updated with the new access token."
|
|
149
|
+
)
|
|
150
|
+
# return the token string
|
|
151
|
+
return self.config[key]
|
|
152
|
+
|
|
153
|
+
def execute_resource_graph_query(
|
|
154
|
+
self, query: str = None, skip_token: Optional[str] = None, record_count: int = 0
|
|
155
|
+
) -> list[dict]:
|
|
156
|
+
"""
|
|
157
|
+
Function to fetch Microsoft Defender resources from Azure
|
|
158
|
+
|
|
159
|
+
:param str query: Query to use for the API call
|
|
160
|
+
:param Optional[str] skip_token: Token to skip results, used during pagination, defaults to None
|
|
161
|
+
:param int record_count: Number of records fetched, defaults to 0, used for logging during pagination
|
|
162
|
+
:return: list of Microsoft Defender resources
|
|
163
|
+
:rtype: list[dict]
|
|
164
|
+
"""
|
|
165
|
+
url = "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01"
|
|
166
|
+
if query:
|
|
167
|
+
payload: dict[str, Any] = {"query": query}
|
|
168
|
+
else:
|
|
169
|
+
payload: dict[str, Any] = {
|
|
170
|
+
"query": query,
|
|
171
|
+
"subscriptions": [self.config["azureCloudSubscriptionId"]],
|
|
172
|
+
}
|
|
173
|
+
if skip_token:
|
|
174
|
+
payload["options"] = {self.skip_token_key: skip_token}
|
|
175
|
+
logger.info("Retrieving more Microsoft Defender resources from Azure...")
|
|
176
|
+
else:
|
|
177
|
+
logger.info("Retrieving Microsoft Defender resources from Azure...")
|
|
178
|
+
response = self.api.post(url=url, headers=self.headers, json=payload)
|
|
179
|
+
if response.status_code != 200:
|
|
180
|
+
error_and_exit(
|
|
181
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
|
|
182
|
+
+ f"\n{response.text}",
|
|
183
|
+
)
|
|
184
|
+
try:
|
|
185
|
+
response_data = response.json()
|
|
186
|
+
total_records = response_data.get("totalRecords", 0)
|
|
187
|
+
count = response_data.get("count", len(response_data.get("data", [])))
|
|
188
|
+
logger.info(f"Received {count + record_count}/{total_records} items from Microsoft Defender.")
|
|
189
|
+
# try to get the values from the api response
|
|
190
|
+
defender_data = response_data["data"]
|
|
191
|
+
except JSONDecodeError:
|
|
192
|
+
# notify user if there was a json decode error from API response and exit
|
|
193
|
+
error_and_exit(self.decode_error)
|
|
194
|
+
except KeyError:
|
|
195
|
+
# notify user there was no data from API response and exit
|
|
196
|
+
error_and_exit(
|
|
197
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.reason}\n"
|
|
198
|
+
+ f"{response.text}"
|
|
199
|
+
)
|
|
200
|
+
# check if pagination is required to fetch all data from Microsoft Defender
|
|
201
|
+
skip_token = response_data.get(self.skip_token_key)
|
|
202
|
+
if response.status_code == 200 and skip_token:
|
|
203
|
+
# get the rest of the data
|
|
204
|
+
defender_data.extend(
|
|
205
|
+
self.execute_resource_graph_query(query=query, skip_token=skip_token, record_count=count + record_count)
|
|
206
|
+
)
|
|
207
|
+
# return the defender recommendations
|
|
208
|
+
return defender_data
|
|
209
|
+
|
|
210
|
+
def get_items_from_azure(self, url: str) -> list:
|
|
211
|
+
"""
|
|
212
|
+
Function to get data from Microsoft Defender returns the data as a list while handling pagination
|
|
213
|
+
|
|
214
|
+
:param str url: URL to use for the API call
|
|
215
|
+
:return: list of recommendations
|
|
216
|
+
:rtype: list
|
|
217
|
+
"""
|
|
218
|
+
# get the data via api call
|
|
219
|
+
response = self.api.get(url=url, headers=self.headers)
|
|
220
|
+
if response.status_code != 200:
|
|
221
|
+
error_and_exit(
|
|
222
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
|
|
223
|
+
+ f"\n{response.text}",
|
|
224
|
+
)
|
|
225
|
+
# try to read the response
|
|
226
|
+
try:
|
|
227
|
+
response_data = response.json()
|
|
228
|
+
# try to get the values from the api response
|
|
229
|
+
defender_data = response_data["value"]
|
|
230
|
+
except JSONDecodeError:
|
|
231
|
+
# notify user if there was a json decode error from API response and exit
|
|
232
|
+
error_and_exit(self.decode_error)
|
|
233
|
+
except KeyError:
|
|
234
|
+
# notify user there was no data from API response and exit
|
|
235
|
+
error_and_exit(
|
|
236
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.text}"
|
|
237
|
+
)
|
|
238
|
+
# check if pagination is required to fetch all data from Microsoft Defender
|
|
239
|
+
if next_link := response_data.get("nextLink"):
|
|
240
|
+
# get the rest of the data
|
|
241
|
+
defender_data.extend(self.get_items_from_azure(url=next_link))
|
|
242
|
+
# return the defender recommendations
|
|
243
|
+
return defender_data
|
|
244
|
+
|
|
245
|
+
def fetch_queries_from_azure(self) -> list[dict]:
|
|
246
|
+
"""
|
|
247
|
+
Function to fetch queries from Microsoft Defender for Cloud
|
|
248
|
+
"""
|
|
249
|
+
url = (
|
|
250
|
+
f"https://management.azure.com/subscriptions/{self.config['azureCloudSubscriptionId']}/"
|
|
251
|
+
"providers/Microsoft.ResourceGraph/queries?api-version=2024-04-01"
|
|
252
|
+
)
|
|
253
|
+
logger.info("Fetching saved queries from Azure Resource Graph...")
|
|
254
|
+
response = self.api.get(url=url, headers=self.headers)
|
|
255
|
+
logger.debug(f"Azure API response status: {response.status_code}")
|
|
256
|
+
if response.raise_for_status():
|
|
257
|
+
error_and_exit(
|
|
258
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
|
|
259
|
+
+ f"\n{response.text}",
|
|
260
|
+
)
|
|
261
|
+
logger.debug("Parsing Azure API response...")
|
|
262
|
+
cloud_queries = response.json().get("value", [])
|
|
263
|
+
logger.info(f"Found {len(cloud_queries)} saved queries in Azure Resource Graph.")
|
|
264
|
+
return cloud_queries
|
|
265
|
+
|
|
266
|
+
def fetch_and_run_query(self, query: dict) -> list[dict]:
|
|
267
|
+
"""
|
|
268
|
+
Function to fetch and run a query from Microsoft Defender for Cloud
|
|
269
|
+
|
|
270
|
+
:param dict query: Query to run in Azure Resource Graph
|
|
271
|
+
:return: Results from the query
|
|
272
|
+
:rtype: list[dict]
|
|
273
|
+
"""
|
|
274
|
+
url = (
|
|
275
|
+
f"https://management.azure.com/subscriptions/{query['subscriptionId']}/resourceGroups/"
|
|
276
|
+
f"{query['resourceGroup']}/providers/Microsoft.ResourceGraph/queries/{query['name']}"
|
|
277
|
+
"?api-version=2024-04-01"
|
|
278
|
+
)
|
|
279
|
+
response = self.api.get(url=url, headers=self.headers)
|
|
280
|
+
if response.raise_for_status():
|
|
281
|
+
error_and_exit(
|
|
282
|
+
f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
|
|
283
|
+
+ f"\n{response.text}",
|
|
284
|
+
)
|
|
285
|
+
query_string = response.json().get("properties", {}).get("query")
|
|
286
|
+
return self.execute_resource_graph_query(query=query_string)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to store constants for Microsoft Defender for Cloud
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
6
|
+
IDENTIFICATION_TYPE = "Vulnerability Assessment"
|
|
7
|
+
CLOUD_RECS = "Microsoft Defender for Cloud Recommendation"
|
|
8
|
+
APP_JSON = "application/json"
|
|
9
|
+
AFD_ENDPOINTS = "microsoft.cdn/profiles/afdendpoints"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
RESOURCES_QUERY = """
|
|
13
|
+
resources
|
|
14
|
+
| where subscriptionId == "{SUBSCRIPTION_ID}"
|
|
15
|
+
| extend resourceName = name,
|
|
16
|
+
resourceType = type,
|
|
17
|
+
resourceLocation = location,
|
|
18
|
+
resourceGroup = resourceGroup,
|
|
19
|
+
resourceId = id,
|
|
20
|
+
propertiesJson = parse_json(properties)
|
|
21
|
+
| extend ipAddress =
|
|
22
|
+
case(
|
|
23
|
+
resourceType =~ "microsoft.network/networkinterfaces", tostring(propertiesJson.ipConfigurations[0].properties.privateIPAddress),
|
|
24
|
+
resourceType =~ "microsoft.network/publicipaddresses", tostring(propertiesJson.ipAddress),
|
|
25
|
+
resourceType =~ "microsoft.compute/virtualmachines", tostring(propertiesJson.networkProfile.networkInterfaces[0].id),
|
|
26
|
+
""
|
|
27
|
+
)
|
|
28
|
+
| project resourceName, resourceType, resourceLocation, resourceGroup, resourceId, ipAddress, properties
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
CONTAINER_SCAN_QUERY = """
|
|
32
|
+
securityresources
|
|
33
|
+
| where type == 'microsoft.security/assessments'
|
|
34
|
+
| summarize by assessmentKey=name
|
|
35
|
+
| join kind=inner (
|
|
36
|
+
securityresources
|
|
37
|
+
| where type == 'microsoft.security/assessments/subassessments'
|
|
38
|
+
| extend assessmentKey = extract('.*assessments/(.+?)/.*', 1, id)
|
|
39
|
+
| where resourceGroup == '{RESOURCE_GROUP}'
|
|
40
|
+
) on assessmentKey
|
|
41
|
+
| project assessmentKey, subassessmentKey=name, id, parse_json(properties), resourceGroup, subscriptionId, tenantId
|
|
42
|
+
| extend description = properties.description,
|
|
43
|
+
displayName = properties.displayName,
|
|
44
|
+
resourceId = properties.resourceDetails.id,
|
|
45
|
+
tag = properties.additionalData.artifactDetails.tags,
|
|
46
|
+
resourceSource = properties.resourceDetails.source,
|
|
47
|
+
category = properties.category,
|
|
48
|
+
severity = properties.status.severity,
|
|
49
|
+
code = properties.status.code,
|
|
50
|
+
timeGenerated = properties.timeGenerated,
|
|
51
|
+
remediation = properties.remediation,
|
|
52
|
+
impact = properties.impact,
|
|
53
|
+
vulnId = properties.id,
|
|
54
|
+
additionalData = properties.additionalData
|
|
55
|
+
| where resourceId startswith "/subscriptions"
|
|
56
|
+
| order by ['id'] asc
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
DB_SCAN_QUERY = """
|
|
60
|
+
securityresources
|
|
61
|
+
| where type =~ "microsoft.security/assessments/subassessments"
|
|
62
|
+
| extend assessmentKey=extract(@"(?i)providers/Microsoft.Security/assessments/([^/]*)", 1, id), subAssessmentId=tostring(properties.id), parentResourceId= extract("(.+)/providers/Microsoft.Security", 1, id)
|
|
63
|
+
| extend resourceIdTemp = iff(properties.resourceDetails.id != "", properties.resourceDetails.id, extract("(.+)/providers/Microsoft.Security", 1, id))
|
|
64
|
+
| extend resourceId = iff(properties.resourceDetails.source =~ "OnPremiseSql", strcat(resourceIdTemp, "/servers/", properties.resourceDetails.serverName, "/databases/" , properties.resourceDetails.databaseName), resourceIdTemp)
|
|
65
|
+
| where assessmentKey =~ "{ASSESSMENT_KEY}"
|
|
66
|
+
| where subscriptionId == "{SUBSCRIPTION_ID}"
|
|
67
|
+
| project assessmentKey,
|
|
68
|
+
subAssessmentId,
|
|
69
|
+
resourceId,
|
|
70
|
+
name=properties.displayName,
|
|
71
|
+
description=properties.description,
|
|
72
|
+
severity=properties.status.severity,
|
|
73
|
+
status=properties.status.code,
|
|
74
|
+
cause=properties.status.cause,
|
|
75
|
+
category=properties.category,
|
|
76
|
+
impact=properties.impact,
|
|
77
|
+
remediation=properties.remediation,
|
|
78
|
+
benchmarks=properties.additionalData.benchmarks
|
|
79
|
+
| where status == "Unhealthy"
|
|
80
|
+
"""
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Defender for Cloud Scanner Integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Iterator
|
|
7
|
+
|
|
8
|
+
from regscale.integrations.commercial.microsoft_defender.defender_api import DefenderApi
|
|
9
|
+
from regscale.integrations.commercial.microsoft_defender.defender_constants import RESOURCES_QUERY, AFD_ENDPOINTS
|
|
10
|
+
from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding, ScannerIntegration
|
|
11
|
+
from regscale.models import IssueSeverity
|
|
12
|
+
from regscale.utils.string import generate_html_table_from_dict
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("regscale")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DefenderScanner(ScannerIntegration):
|
|
18
|
+
title = "Microsoft Defender for Cloud"
|
|
19
|
+
# Required fields from ScannerIntegration
|
|
20
|
+
asset_identifier_field = "otherTrackingNumber"
|
|
21
|
+
finding_severity_map = {
|
|
22
|
+
"Critical": IssueSeverity.Critical,
|
|
23
|
+
"High": IssueSeverity.High,
|
|
24
|
+
"Medium": IssueSeverity.Moderate,
|
|
25
|
+
"Low": IssueSeverity.Low,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def __init__(self, *args, **kwargs):
|
|
29
|
+
self.system = kwargs.pop("system", "cloud")
|
|
30
|
+
super().__init__(*args, **kwargs)
|
|
31
|
+
self.api = DefenderApi(system=self.system) # type: ignore
|
|
32
|
+
|
|
33
|
+
def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
|
|
34
|
+
"""
|
|
35
|
+
Fetches assets from Synqly
|
|
36
|
+
|
|
37
|
+
:yields: Iterator[IntegrationAsset]
|
|
38
|
+
"""
|
|
39
|
+
cloud_resources = self.api.execute_resource_graph_query(
|
|
40
|
+
query=RESOURCES_QUERY.format(SUBSCRIPTION_ID=self.api.config["azureCloudSubscriptionId"])
|
|
41
|
+
)
|
|
42
|
+
self.num_assets_to_process = len(cloud_resources)
|
|
43
|
+
for asset in cloud_resources:
|
|
44
|
+
yield self.parse_asset(asset)
|
|
45
|
+
|
|
46
|
+
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
47
|
+
"""
|
|
48
|
+
Fetches findings from the Synqly
|
|
49
|
+
|
|
50
|
+
:yields: Iterator[IntegrationFinding]
|
|
51
|
+
"""
|
|
52
|
+
integration_findings = kwargs.get("integration_findings")
|
|
53
|
+
for finding in integration_findings:
|
|
54
|
+
yield finding
|
|
55
|
+
|
|
56
|
+
def parse_asset(self, defender_asset: dict) -> IntegrationAsset:
|
|
57
|
+
"""
|
|
58
|
+
Function to map data to an Asset object
|
|
59
|
+
|
|
60
|
+
:param defender_asset: Data from Microsoft Defender for Cloud
|
|
61
|
+
:return: IntegrationAsset object that is parsed from the defender_asset
|
|
62
|
+
:rtype: IntegrationAsset
|
|
63
|
+
"""
|
|
64
|
+
asset_id = defender_asset.get("resourceId")
|
|
65
|
+
properties = defender_asset.get("properties", {})
|
|
66
|
+
resource_type = defender_asset.get("resourceType", "").lower()
|
|
67
|
+
try:
|
|
68
|
+
ip_mapping = {
|
|
69
|
+
"microsoft.network/networksecuritygroups": properties.get("securityRules", [{}])[0]
|
|
70
|
+
.get("properties", {})
|
|
71
|
+
.get("destinationAddressPrefix"),
|
|
72
|
+
"microsoft.network/virtualnetworks": properties.get("addressSpace", {}).get("addressPrefixes"),
|
|
73
|
+
"microsoft.app/managedenvironments": properties.get("staticIp"),
|
|
74
|
+
"microsoft.network/networkinterfaces": properties.get("ipConfigurations", [{}])[0]
|
|
75
|
+
.get("properties", {})
|
|
76
|
+
.get("privateIPAddress"),
|
|
77
|
+
}
|
|
78
|
+
except IndexError:
|
|
79
|
+
ip_mapping = {}
|
|
80
|
+
try:
|
|
81
|
+
fqdn_mapping = {
|
|
82
|
+
"microsoft.keyvault/vaults": properties.get("vaultUri"),
|
|
83
|
+
"microsoft.storage/storageaccounts": properties.get("primaryEndpoints", {}).get("blob"),
|
|
84
|
+
"microsoft.appconfiguration/configurationstores": properties.get("endpoint"),
|
|
85
|
+
"microsoft.dbforpostgresql/flexibleservers": properties.get("fullyQualifiedDomainName"),
|
|
86
|
+
AFD_ENDPOINTS: properties.get("hostName"),
|
|
87
|
+
"microsoft.containerregistry/registries": properties.get("loginServer"),
|
|
88
|
+
"microsoft.app/containerapps": properties.get("configuration", {}).get("ingress", {}).get("fqdn"),
|
|
89
|
+
"microsoft.network/privatednszones": defender_asset.get("name") or defender_asset.get("resourceName"),
|
|
90
|
+
"microsoft.cognitiveservices/accounts": properties.get("endpoint"),
|
|
91
|
+
}
|
|
92
|
+
except IndexError:
|
|
93
|
+
fqdn_mapping = {}
|
|
94
|
+
# pylint: disable=line-too-long
|
|
95
|
+
function_mapping = {
|
|
96
|
+
"microsoft.network/privateendpoints": "Private endpoint that links the private link and the nic together",
|
|
97
|
+
"microsoft.network/networkinterfaces": "Network Interface that connects to everything internal to the resource group",
|
|
98
|
+
"microsoft.network/privatednszones": "Dns zone that will connect to the private endpoint and network interfaces",
|
|
99
|
+
"microsoft.network/privatednszones/virtualnetworklinks": "Link for the Private DNS zone back to the vnet",
|
|
100
|
+
"microsoft.app/containerapps": "Application runner that houses the running Docker Container",
|
|
101
|
+
"microsoft.network/publicipaddresses": "Public ip address used for load balancing the container apps",
|
|
102
|
+
"microsoft.storage/storageaccounts": "Storage blob to house unstructured files uploaded to the platform",
|
|
103
|
+
"microsoft.network/networksecuritygroups": "Network protection for internal communications and load balancing",
|
|
104
|
+
"microsoft.network/networkwatchers/flowlogs": "Logs that determine the flow of traffic",
|
|
105
|
+
"microsoft.sql/servers/databases": "Database that houses application logs",
|
|
106
|
+
"microsoft.network/virtualnetworks": "Network Interface that determines what the valid IP range is for all internal resources",
|
|
107
|
+
"microsoft.portal/dashboards": "Dashboard that shows the status of the application and traffic",
|
|
108
|
+
"microsoft.dataprotection/backupvaults": "Azure Blob Storage Account backup location",
|
|
109
|
+
"microsoft.keyvault/vaults": "To securely store API keys, passwords, certificates, or cryptographic keys",
|
|
110
|
+
"microsoft.managedidentity/userassignedidentities": "Identity that connects all internal resources in the resource group",
|
|
111
|
+
"microsoft.app/managedenvironments": "Application environment to connect to the vnet",
|
|
112
|
+
"microsoft.sql/servers": "Server that will house the database for the application logs",
|
|
113
|
+
"microsoft.sql/servers/encryptionprotector": "Server encryption",
|
|
114
|
+
"microsoft.appconfiguration/configurationstores": "Configure, store, and retrieve parameters and settings. Store configuration for all system components in the environment",
|
|
115
|
+
"microsoft.insights/metricalerts": "Alerts that trigger when exceptions hit above 100",
|
|
116
|
+
"microsoft.insights/webtests": "Test to ensure the integrity of the app and alert when availability drops",
|
|
117
|
+
"microsoft.insights/components": "Insights and mapping for the data flow through the platform container application",
|
|
118
|
+
"microsoft.dbforpostgresql/flexibleservers": "Application Database for OpenAI and Automation containers",
|
|
119
|
+
"microsoft.network/loadbalancers": "Load Balancer that handles the load traffic for the containerapp",
|
|
120
|
+
"microsoft.insights/activitylogalerts": "Alert rule to send an email to the Action Group when the trigger event happens",
|
|
121
|
+
"microsoft.operationalinsights/workspaces": "Collection of Logs contained in a workspace",
|
|
122
|
+
"microsoft.insights/actiongroups": "Action Group to send Emails to when alerts trigger",
|
|
123
|
+
"microsoft.network/networkwatchers": "Monitor on the network to look for any suspecious activity",
|
|
124
|
+
"microsoft.app/managedenvironments/certificates": "Tls cert for the application environment",
|
|
125
|
+
"microsoft.authorization/roledefinitions": "Custom role definition",
|
|
126
|
+
"microsoft.alertsmanagement/actionrules": "Alert Processing Rule to show when to trigger",
|
|
127
|
+
"microsoft.network/frontdoorwebapplicationfirewallpolicies": "Waf protection policy that connects to the firewall and frontdoor",
|
|
128
|
+
"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)",
|
|
129
|
+
"microsoft.resourcegraph/queries": "Query to return all resources in the SaaS subscription in the resource graph",
|
|
130
|
+
"microsoft.network/firewallpolicies": "Firewall policy that connects to frontdoor and handles our traffic coming into the system",
|
|
131
|
+
AFD_ENDPOINTS: "Endpoint that all of the routes attach to",
|
|
132
|
+
"microsoft.containerregistry/registries": "House the Docker container image for ContainerApp pull",
|
|
133
|
+
"microsoft.operationalinsights/querypacks": "Log analytics query that loads default queries for running",
|
|
134
|
+
"microsoft.alertsmanagement/smartdetectoralertrules": "Failure Anomalies notifies you of an unusual rise in the rate of failed HTTP requests or dependency calls.",
|
|
135
|
+
}
|
|
136
|
+
# pylint: enable=line-too-long
|
|
137
|
+
from regscale.models import regscale_models
|
|
138
|
+
|
|
139
|
+
mapped_asset = IntegrationAsset(
|
|
140
|
+
name=defender_asset.get("resourceName", asset_id),
|
|
141
|
+
description=generate_html_table_from_dict(defender_asset),
|
|
142
|
+
other_tracking_number=asset_id,
|
|
143
|
+
azure_identifier=asset_id,
|
|
144
|
+
external_id=asset_id,
|
|
145
|
+
identifier=asset_id,
|
|
146
|
+
asset_type=regscale_models.AssetType.Other,
|
|
147
|
+
asset_category=regscale_models.AssetCategory.Software,
|
|
148
|
+
parent_id=self.plan_id,
|
|
149
|
+
parent_module=(
|
|
150
|
+
regscale_models.Component.get_module_slug()
|
|
151
|
+
if self.is_component
|
|
152
|
+
else regscale_models.SecurityPlan.get_module_slug()
|
|
153
|
+
),
|
|
154
|
+
status=regscale_models.AssetStatus.Active,
|
|
155
|
+
software_function=function_mapping.get(resource_type, properties.get("description")),
|
|
156
|
+
ip_address=defender_asset.get("ipAddress") or ip_mapping.get(resource_type, properties.get("ipAddress")),
|
|
157
|
+
is_public_facing=resource_type in ["microsoft.cdn/profiles", AFD_ENDPOINTS],
|
|
158
|
+
is_authenticated_scan=resource_type
|
|
159
|
+
not in ["microsoft.alertsmanagement/actionrules", "microsoft.alertsmanagement/smartdetectoralertrules"],
|
|
160
|
+
is_virtual=True,
|
|
161
|
+
baseline_configuration="Azure Hardening Guide",
|
|
162
|
+
component_names=[resource_type],
|
|
163
|
+
source_data=defender_asset,
|
|
164
|
+
)
|
|
165
|
+
if fqdn := fqdn_mapping.get(resource_type, properties.get("dnsSettings", {}).get("fqdn")):
|
|
166
|
+
mapped_asset.fqdn = fqdn
|
|
167
|
+
mapped_asset.description += f"<p>FQDN: {fqdn}</p>"
|
|
168
|
+
return mapped_asset
|