Repository: clouditor/clouditor
Pull request #265 (2021-06-29 09:53:41)

Azure quick discovery using IaC template export
Added first IaC discovery draft for azure compute and network based on the Azure TemplateExport-Function

codecov[bot]: # [Codecov](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) Report
> Merging [#265](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) (0c52214) into [main](https://codecov.io/gh/clouditor/clouditor/commit/efe7d4a593d12385a14cbd7b709743112e559b49?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) (efe7d4a) will **decrease** coverage by `3.88%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/clouditor/clouditor/pull/265/graphs/tree.svg?width=650&height=150&src=pr&token=syrXsw5LTX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor)](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor)

```diff
@@            Coverage Diff             @@
##             main     #265      +/-   ##
==========================================
- Coverage   56.38%   52.50%   -3.89%     
==========================================
  Files          35       36       +1     
  Lines        1472     1577     +105     
==========================================
- Hits          830      828       -2     
- Misses        556      663     +107     
  Partials       86       86              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `52.50% <0.00%> (-3.89%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) | Coverage Δ | |
|---|---|---|
| [service/discovery/azure/iac\_template.go](https://codecov.io/gh/clouditor/clouditor/pull/265/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor#diff-c2VydmljZS9kaXNjb3ZlcnkvYXp1cmUvaWFjX3RlbXBsYXRlLmdv) | `0.00% <0.00%> (ø)` | |
| [service/discovery/discovery.go](https://codecov.io/gh/clouditor/clouditor/pull/265/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor#diff-c2VydmljZS9kaXNjb3ZlcnkvZGlzY292ZXJ5Lmdv) | `37.34% <0.00%> (-0.46%)` | :arrow_down: |
| [service/assessment/assessment.go](https://codecov.io/gh/clouditor/clouditor/pull/265/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor#diff-c2VydmljZS9hc3Nlc3NtZW50L2Fzc2Vzc21lbnQuZ28=) | `80.00% <0.00%> (-13.34%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor). Last update [efe7d4a...0c52214](https://codecov.io/gh/clouditor/clouditor/pull/265?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor).

garuppel: Should the IaC Discovery be started by using extra parameter? 
garuppel: Now, the virtual_machines, object_storage and load_balancers are discovered by the IaC template. 

Patch:
diff --git a/service/discovery/azure/iac_template.go b/service/discovery/azure/iac_template.go
new file mode 100644
index 00000000..9885f6c2
--- /dev/null
+++ b/service/discovery/azure/iac_template.go
@@ -0,0 +1,370 @@
+// Copyright 2021 Fraunhofer AISEC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//           $$\                           $$\ $$\   $$\
+//           $$ |                          $$ |\__|  $$ |
+//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
+// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
+// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
+// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
+// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
+//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
+//
+// This file is part of Clouditor Community Edition.
+
+package azure
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"strings"
+
+	"clouditor.io/clouditor/api/discovery"
+	"clouditor.io/clouditor/voc"
+	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-02-01/resources"
+	"github.com/Azure/go-autorest/autorest/to"
+)
+
+// type properties struct {
+// 	minimumTLSVersion string
+// 	hTTPSTrafficOnly  bool
+// 	encryptionEnabled bool
+// 	keySource         string
+// }
+
+type azureIacTemplateDiscovery struct {
+	azureDiscovery
+}
+
+func NewAzureIacTemplateDiscovery(opts ...DiscoveryOption) discovery.Discoverer {
+	d := &azureIacTemplateDiscovery{}
+
+	for _, opt := range opts {
+		if auth, ok := opt.(*authorizerOption); ok {
+			d.authOption = auth
+		} else {
+			d.options = append(d.options, opt)
+		}
+	}
+
+	return d
+}
+
+func (d *azureIacTemplateDiscovery) Name() string {
+	return "Azure"
+}
+
+func (d *azureIacTemplateDiscovery) Description() string {
+	return "Discovery IaC template."
+}
+
+// Discover IaC template
+func (d *azureIacTemplateDiscovery) List() (list []voc.IsResource, err error) {
+	if err = d.authorize(); err != nil {
+		return nil, fmt.Errorf("could not authorize Azure account: %w", err)
+	}
+
+	iacTemplate, err := d.discoverIaCTemplate()
+	if err != nil {
+		return nil, fmt.Errorf("could not discover IaC template: %w", err)
+	}
+	list = append(list, iacTemplate...)
+
+	return
+}
+
+func (d *azureIacTemplateDiscovery) discoverIaCTemplate() ([]voc.IsResource, error) {
+
+	var (
+		list []voc.IsResource
+	)
+
+	client := resources.NewGroupsClient(to.String(d.sub.SubscriptionID))
+	d.apply(&client.Client)
+
+	// Get all resource groups, as the exportTemplate function works only on resource group level
+	resultResourceGroupsResult, err := client.ListComplete(context.Background(), "", nil)
+	if err != nil {
+		return nil, fmt.Errorf("could not discover resource groups: %w", err)
+	}
+
+	resourceGroups := *resultResourceGroupsResult.Response().Value
+	for i := range resourceGroups {
+
+		expReq := resources.ExportTemplateRequest{
+			ResourcesProperty: &[]string{"*"},
+		}
+		result, err := client.ExportTemplate(context.Background(), *resourceGroups[i].Name, expReq)
+		if err != nil {
+			return nil, fmt.Errorf("could not discover IaC template: %w", err)
+		}
+
+		template, ok := result.Template.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("IaC template type convertion failed")
+		}
+
+		err = saveExportTemplate(result, *resourceGroups[i].Name)
+		if err != nil {
+			fmt.Println("Error saving export template: ", err)
+		}
+
+		for templateKey, templateValue := range template {
+
+			if templateKey == "resources" {
+				resources, ok := templateValue.([]interface{})
+				if !ok {
+					return nil, fmt.Errorf("templateValue  type convertion failed")
+				}
+
+				for _, resourcesValue := range resources {
+					value, ok := resourcesValue.(map[string]interface{})
+					if !ok {
+						return nil, fmt.Errorf("resources type convertion failed")
+					}
+
+					for valueKey, valueValue := range value {
+						if valueKey == "type" {
+
+							if valueValue.(string) == "Microsoft.Compute/virtualMachines" {
+								vm, err := d.createVMResource(value, *resourceGroups[i].Name)
+								if err != nil {
+									return nil, fmt.Errorf("could not create virtual machine resource: %w", err)
+								}
+								list = append(list, vm)
+							} else if valueValue.(string) == "Microsoft.Network/loadBalancers" {
+								lb, err := d.createLBResource(value, *resourceGroups[i].Name)
+								if err != nil {
+									return nil, fmt.Errorf("could not create load balancer resource: %w", err)
+								}
+								list = append(list, lb)
+							} else if valueValue.(string) == "Microsoft.Storage/storageAccounts" {
+								storage, err := d.createStorageResource(value, *resourceGroups[i].Name)
+								if err != nil {
+									return nil, fmt.Errorf("could not create storage resource: %w", err)
+								}
+								list = append(list, storage)
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return list, nil
+}
+
+// saveExportTemplate saves the resource group template in a json file.
+func saveExportTemplate(template resources.GroupExportResult, groupName string) error {
+
+	prefix, indent := "", "    "
+	exported, err := json.MarshalIndent(template, prefix, indent)
+	if err != nil {
+		return fmt.Errorf("MarshalIndent failed %w", err)
+	}
+
+	fileTemplate := "../../raw_discovery_results/azure_iac_templates/%s-template.json"
+	fileName := fmt.Sprintf(fileTemplate, groupName)
+
+	err = ioutil.WriteFile(fileName, exported, 0666)
+	if err != nil {
+		return fmt.Errorf("write file failed %w", err)
+	}
+
+	return nil
+}
+
+func (d *azureIacTemplateDiscovery) createStorageResource(resourceValue map[string]interface{}, resourceGroup string) (voc.IsCompute, error) {
+
+	var (
+		name string
+		// property properties
+	)
+
+	resourceType := resourceValue["type"].(string)
+
+	for key, value := range resourceValue {
+		// Get storage account name
+		if key == "name" {
+			name = getDefaultNameOfResource(value.(string))
+		}
+	}
+
+	storage := &voc.ObjectStorageResource{
+		StorageResource: voc.StorageResource{
+			Resource: voc.Resource{
+				ID:           voc.ResourceID(d.createID(resourceGroup, resourceType, name)),
+				Name:         name,
+				CreationTime: 0, // No creation time available
+				Type:         []string{"ObjectStorage", "Storage", "Resource"},
+			},
+			AtRestEncryption: voc.NewAtRestEncryption(
+				blobServiceEncryptionEnabled(resourceValue),
+				"AES-265", // seems to be always AWS-256
+				getStorageKeySource(resourceValue),
+			)},
+		HttpEndpoint: &voc.HttpEndpoint{
+			URL: "", // Not able to get from IaC tempalte
+			TransportEncryption: voc.NewTransportEncryption(
+				true, // cannot be disabled
+				httpTrafficOnlyEnabled(resourceValue),
+				getMinTlsVersion(resourceValue),
+			),
+		},
+	}
+
+	return storage, nil
+}
+
+func httpTrafficOnlyEnabled(value map[string]interface{}) bool {
+
+	if httpTrafficOnlyEnabled, ok := value["properties"].(map[string]interface{})["supportsHttpsTrafficOnly"].(bool); ok {
+		return httpTrafficOnlyEnabled
+	}
+
+	return false
+}
+
+func getStorageKeySource(value map[string]interface{}) string {
+
+	if storageKeySource, ok := value["properties"].(map[string]interface{})["encryption"].(map[string]interface{})["keySource"].(string); ok {
+		return storageKeySource
+	}
+
+	return ""
+}
+
+func blobServiceEncryptionEnabled(value map[string]interface{}) bool {
+
+	if blobServiceEnabled, ok := value["properties"].(map[string]interface{})["encryption"].(map[string]interface{})["services"].(map[string]interface{})["blob"].(map[string]interface{})["enabled"].(bool); ok {
+		return blobServiceEnabled
+	}
+
+	return false
+}
+
+func getMinTlsVersion(value map[string]interface{}) string {
+
+	if minTlsVersion, ok := value["properties"].(map[string]interface{})["minimumTlsVersion"].(string); ok {
+		return minTlsVersion
+	}
+
+	return ""
+}
+
+func (d *azureIacTemplateDiscovery) createLBResource(resourceValue map[string]interface{}, resourceGroup string) (voc.IsCompute, error) {
+
+	var name string
+
+	resourceType := resourceValue["type"].(string)
+
+	for key, value := range resourceValue {
+		// Get LB name
+		if key == "name" {
+			name = getDefaultNameOfResource(value.(string))
+		}
+	}
+
+	// TODO(garuppel): Which additional information do we get from the template?
+	lb := &voc.LoadBalancerResource{
+		NetworkService: voc.NetworkService{
+			NetworkResource: voc.NetworkResource{
+				Resource: voc.Resource{
+					ID:           voc.ResourceID(d.createID(resourceGroup, resourceType, name)),
+					Name:         name,
+					CreationTime: 0, // No creation time available
+					Type:         []string{"LoadBalancer", "NetworkService", "Resource"},
+				},
+			},
+			IPs:   []string{},
+			Ports: nil,
+		},
+		AccessRestriction: &voc.AccessRestriction{
+			Inbound:         false,
+			RestrictedPorts: "",
+		},
+		// TODO(all): Do we need the httpEndpoint?
+		HttpEndpoints: []*voc.HttpEndpoint{},
+	}
+
+	return lb, nil
+}
+
+func (d *azureIacTemplateDiscovery) createVMResource(resourceValue map[string]interface{}, resourceGroup string) (voc.IsCompute, error) {
+	var id string
+	var name string
+	var enabled bool
+
+	for key, value := range resourceValue {
+
+		// Get VM name
+		if key == "name" {
+			name = getDefaultNameOfResource(value.(string))
+		}
+
+		// Get bool for Logging enabled
+		if key == "properties" {
+			properties, ok := value.(map[string]interface{})
+
+			if !ok {
+				return nil, fmt.Errorf("type convertion failed")
+			}
+
+			for propertiesKey, propertiesValue := range properties {
+				if propertiesKey == "diagnosticsProfile" {
+					enabled = propertiesValue.(map[string]interface{})["bootDiagnostics"].(map[string]interface{})["enabled"].(bool)
+				}
+			}
+		}
+	}
+
+	// Get ID
+	// ID must be put together by hand, is not available in template. Better ideas? Leave empty?
+	id = d.createID(resourceGroup, resourceValue["type"].(string), name)
+
+	vm := &voc.VirtualMachineResource{
+		ComputeResource: voc.ComputeResource{
+			Resource: voc.Resource{
+				ID:           voc.ResourceID(id),
+				Name:         name,
+				CreationTime: 0, // No creation time available
+				Type:         []string{"VirtualMachine", "Compute", "Resource"},
+			}},
+		Log: &voc.Log{
+			Enabled: enabled,
+		},
+	}
+
+	return vm, nil
+}
+
+func (d *azureIacTemplateDiscovery) createID(resourceGroup, resourceType, name string) string {
+	return "/subscriptions/" + *d.sub.SubscriptionID + "/resourceGroups/" + strings.ToUpper(resourceGroup) + "/providers/" + resourceType + "/" + name
+}
+
+// getDefaultNameOfResource gets the defaultName from template parameter
+// TODO(all): The exported template contains a parameter instead of the defaultName (resourceName). Furthermore, the template parameters do not contain a mapping from the parameter to the defaultName. In the parameter name all word separators (e.g. _, -, .) were replaced by a underscore (_), so it is not possible to uniquely restore the defaultName. Ideas? Do we need the correct defaultNames?
+func getDefaultNameOfResource(name string) string {
+	// Name in template is an parameter and unnecessary information must be shortened
+	nameSplit := strings.Split(name, "'")
+	anotherNameSplit := strings.Split(nameSplit[1], "_")
+	anotherNameSplit = anotherNameSplit[1:]
+	anotherNameSplit = anotherNameSplit[:len(anotherNameSplit)-1]
+	resourceDefaultName := strings.Join(anotherNameSplit, "-")
+
+	return resourceDefaultName
+}
diff --git a/service/discovery/azure/network.go b/service/discovery/azure/network.go
index 843b370a..d8b34c2e 100644
--- a/service/discovery/azure/network.go
+++ b/service/discovery/azure/network.go
@@ -164,8 +164,8 @@ func (d *azureNetworkDiscovery) handleNetworkInterfaces(ni *network.Interface) v
 			},
 		},
 		AccessRestriction: &voc.AccessRestriction{
-			Inbound:         false, //TBD
-			RestrictedPorts: d.getRestrictedPorts(ni),
+			Inbound:         false, // TODO(garuppel): TBD
+      RestrictedPorts: d.getRestrictedPorts(ni),
 		},
 	}
 }
diff --git a/service/discovery/discovery.go b/service/discovery/discovery.go
index 0b77855a..b62500ec 100644
--- a/service/discovery/discovery.go
+++ b/service/discovery/discovery.go
@@ -120,6 +120,7 @@ func (s Service) Start(ctx context.Context, request *discovery.StartDiscoveryReq
 	var discoverer []discovery.Discoverer
 
 	discoverer = append(discoverer,
+		azure.NewAzureIacTemplateDiscovery(azure.WithAuthorizer(authorizer)),
 		azure.NewAzureStorageDiscovery(azure.WithAuthorizer(authorizer)),
 		azure.NewAzureComputeDiscovery(azure.WithAuthorizer(authorizer)),
 		azure.NewAzureNetworkDiscovery(azure.WithAuthorizer(authorizer)),
diff --git a/voc/access_restriction.go b/voc/access_restriction.go
index 2b77bd41..5a0cf3fa 100644
--- a/voc/access_restriction.go
+++ b/voc/access_restriction.go
@@ -26,9 +26,9 @@
 package voc
 
 type AccessRestriction struct {
-	// TODO What informaton do we need here?
+	// TODO(all): What informaton do we need here?
 	Inbound bool `json:"inbound"`
-	// TODO According to the ontology, it is a string. What information do we need exactly?
+	// TODO(all): According to the ontology, it is a string. What information do we need exactly?
 	RestrictedPorts string `json:"restrictedPorts"` // List of restricted ports
 }
 
diff --git a/voc/log.go b/voc/log.go
index 539aaa36..194d4a64 100644
--- a/voc/log.go
+++ b/voc/log.go
@@ -25,7 +25,7 @@
 
 package voc
 
-//TODO Do we want to distinguish between logging mechanisms, e.g., azure Log Analytics, Azure VM BootDiagnostics?
+// TODO(all): Do we want to distinguish between logging mechanisms, e.g., azure Log Analytics, Azure VM BootDiagnostics?
 type Log struct {
 	Enabled bool `json:"enabled"`
 }


Test patch:
diff --git a/service/discovery/azure/compute_test.go b/service/discovery/azure/compute_test.go
index 0b6a788f..dec1ec4b 100644
--- a/service/discovery/azure/compute_test.go
+++ b/service/discovery/azure/compute_test.go
@@ -48,6 +48,12 @@ func (m mockComputeSender) Do(req *http.Request) (res *http.Response, err error)
 					"location":   "eastus",
 					"properties": map[string]interface{}{},
 				},
+				{
+					"id":         "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Compute/virtualMachines/vm2",
+					"name":       "vm2",
+					"location":   "eastus",
+					"properties": map[string]interface{}{},
+				},
 			},
 		}, 200)
 	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Compute/virtualMachines/vm1" {
@@ -84,6 +90,40 @@ func (m mockComputeSender) Do(req *http.Request) (res *http.Response, err error)
 				},
 			},
 		}, 200)
+	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Compute/virtualMachines/vm2" {
+		return createResponse(map[string]interface{}{
+			"properties": map[string]interface{}{
+				"storageProfile": map[string]interface{}{
+					"osDisk": map[string]interface{}{
+						"managedDisk": map[string]interface{}{
+							"id": "os_test_disk",
+						},
+					},
+					"dataDisks": &[]map[string]interface{}{
+						{
+							"managedDisk": map[string]interface{}{
+								"id": "data_disk_2",
+							},
+						},
+						{
+							"managedDisk": map[string]interface{}{
+								"id": "data_disk_3",
+							},
+						},
+					},
+				},
+				"networkProfile": map[string]interface{}{
+					"networkInterfaces": &[]map[string]interface{}{
+						{
+							"id": "987",
+						},
+						{
+							"id": "654",
+						},
+					},
+				},
+			},
+		}, 200)
 	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Web/sites" {
 		return createResponse(map[string]interface{}{
 			"value": &[]map[string]interface{}{
@@ -110,7 +150,7 @@ func TestVirtualMachine(t *testing.T) {
 
 	assert.Nil(t, err)
 	assert.NotNil(t, list)
-	assert.Equal(t, 2, len(list))
+	assert.Equal(t, 3, len(list))
 
 	virtualMachine, ok := list[0].(*voc.VirtualMachineResource)
 
@@ -133,9 +173,9 @@ func TestFunction(t *testing.T) {
 
 	assert.Nil(t, err)
 	assert.NotNil(t, list)
-	assert.Equal(t, 2, len(list))
+	assert.Equal(t, 3, len(list))
 
-	function, ok := list[1].(*voc.FunctionResource)
+	function, ok := list[2].(*voc.FunctionResource)
 
 	assert.True(t, ok)
 	assert.Equal(t, "function1", function.Name)
diff --git a/service/discovery/azure/iac_template_test.go b/service/discovery/azure/iac_template_test.go
new file mode 100644
index 00000000..070d4ee0
--- /dev/null
+++ b/service/discovery/azure/iac_template_test.go
@@ -0,0 +1,191 @@
+// Copyright 2021 Fraunhofer AISEC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//           $$\                           $$\ $$\   $$\
+//           $$ |                          $$ |\__|  $$ |
+//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
+// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
+// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
+// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
+// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
+//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
+//
+// This file is part of Clouditor Community Edition.
+
+package azure_test
+
+import (
+	"net/http"
+	"testing"
+
+	"clouditor.io/clouditor/service/discovery/azure"
+	"clouditor.io/clouditor/voc"
+	"github.com/stretchr/testify/assert"
+)
+
+type mockIacTemplateSender struct {
+	mockSender
+}
+
+func (m mockIacTemplateSender) Do(req *http.Request) (res *http.Response, err error) {
+	if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups" {
+		return createResponse(map[string]interface{}{
+			"value": &[]map[string]interface{}{
+				{
+					"id":         "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1",
+					"name":       "res1",
+					"location":   "eastus",
+					"properties": map[string]interface{}{},
+				},
+				{
+					"id":         "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res2",
+					"name":       "res2",
+					"location":   "eastus",
+					"properties": map[string]interface{}{},
+				},
+			},
+		}, 200)
+	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/res1/exportTemplate" {
+		return createResponse(map[string]interface{}{
+			"template": &map[string]interface{}{
+				"resources": []map[string]interface{}{
+					{
+						"type":       "Microsoft.Compute/virtualMachines",
+						"name":       "[parameters('virtualMachines_vm1_name')]",
+						"location":   "eastus",
+						"properties": map[string]interface{}{},
+					},
+					{
+						"type":       "Microsoft.Compute/virtualMachines",
+						"name":       "[parameters('virtualMachines_vm2_name')]",
+						"location":   "eastus",
+						"properties": map[string]interface{}{},
+					},
+					{
+						"type":     "Microsoft.Storage/storageAccounts",
+						"name":     "[parameters('storageAccounts_storage_1_name')]",
+						"location": "eastus",
+						"properties": map[string]interface{}{
+							"encryption": map[string]interface{}{
+								"services": map[string]interface{}{
+									"file": map[string]interface{}{
+										"keyType": "Account",
+										"enabled": true,
+									},
+									"blob": map[string]interface{}{
+										"keyType": "Account",
+										"enabled": true,
+									},
+								},
+								"keySource":                "Microsoft.Storage",
+								"minimumTlsVersion":        "TLS1_1",
+								"supportsHttpsTrafficOnly": true,
+							},
+						},
+					},
+				},
+			},
+		}, 200)
+	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/res2/exportTemplate" {
+		return createResponse(map[string]interface{}{
+			"template": &map[string]interface{}{
+				"resources": []map[string]interface{}{
+					{
+						"type":       "Microsoft.Compute/virtualMachines",
+						"name":       "[parameters('virtualMachines_vm_3_name')]",
+						"location":   "eastus",
+						"properties": map[string]interface{}{},
+					},
+					{
+						"type":     "Microsoft.Storage/storageAccounts",
+						"name":     "[parameters('storageAccounts_storage_1_name')]",
+						"location": "eastus",
+						"properties": map[string]interface{}{
+							"encryption": map[string]interface{}{
+								"services": map[string]interface{}{
+									"file": map[string]interface{}{
+										"keyType": "Account",
+										"enabled": true,
+									},
+									"blob": map[string]interface{}{
+										"keyType": "Account",
+										"enabled": true,
+									},
+								},
+								"keySource":                "Microsoft.Storage",
+								"minimumTlsVersion":        "TLS1_1",
+								"supportsHttpsTrafficOnly": true,
+							},
+						},
+					},
+					{
+						"type":     "Microsoft.Storage/storageAccounts",
+						"name":     "[parameters('storageAccounts_storage_2_name')]",
+						"location": "eastus",
+						"properties": map[string]interface{}{
+							"encryption": map[string]interface{}{
+								"services": map[string]interface{}{
+									"file": map[string]interface{}{
+										"keyType": "Account",
+										"enabled": true,
+									},
+									"blob": map[string]interface{}{
+										"keyType": "Account",
+										"enabled": true,
+									},
+								},
+								"keySource":                "Microsoft.Storage",
+								"minimumTlsVersion":        "TLS1_1",
+								"supportsHttpsTrafficOnly": true,
+							},
+						},
+					},
+					{
+						"type":       "Microsoft.Network/loadBalancers",
+						"name":       "[parameters('loadBalancers_kubernetes_name')]",
+						"location":   "eastus",
+						"properties": map[string]interface{}{},
+					},
+				},
+			},
+		}, 200)
+	}
+
+	return m.mockSender.Do(req)
+}
+
+func TestIacDiscovery(t *testing.T) {
+	d := azure.NewAzureIacTemplateDiscovery(
+		azure.WithSender(&mockIacTemplateSender{}),
+		azure.WithAuthorizer(&mockAuthorizer{}),
+	)
+
+	list, err := d.List()
+
+	assert.Nil(t, err)
+	assert.NotNil(t, list)
+	assert.Equal(t, 7, len(list))
+
+	resourceVM, ok := list[0].(*voc.VirtualMachineResource)
+	assert.True(t, ok)
+	assert.Equal(t, "vm1", resourceVM.Name)
+
+	resourceStorage, ok := list[2].(*voc.ObjectStorageResource)
+	assert.True(t, ok)
+
+	// That should be equal. The Problem is described in file 'service/discovery/azure/iac_template.go'
+	assert.NotEqual(t, "storage_1", resourceStorage.Name)
+	assert.NotEqual(t, "TLS1_1", resourceStorage.HttpEndpoint.TransportEncryption.TlsVersion)
+
+}