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.
Files changed (34) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +7 -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 +1402 -203
  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 +212 -60
  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 +33 -3
  25. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  26. regscale/models/regscale_models/issue.py +29 -9
  27. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +1 -1
  28. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +32 -27
  29. tests/regscale/test_authorization.py +0 -65
  30. tests/regscale/test_init.py +0 -96
  31. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
  32. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
  33. {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
  34. {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