Repository: clouditor/clouditor
Pull request #1147 (2023-07-03 08:30:54)

Add Azure SQL discovery

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/clouditor/clouditor/pull/1147?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) Report
> Merging [#1147](https://app.codecov.io/gh/clouditor/clouditor/pull/1147?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) (b238f85) into [main](https://app.codecov.io/gh/clouditor/clouditor/commit/bc975803f8147728466d77b18e5d8d48015326d7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) (bc97580) will **decrease** coverage by `0.03%`.
> The diff coverage is `82.75%`.

```diff
@@            Coverage Diff             @@
##             main    #1147      +/-   ##
==========================================
- Coverage   83.90%   83.88%   -0.03%     
==========================================
  Files          93       93              
  Lines        8766     8910     +144     
==========================================
+ Hits         7355     7474     +119     
- Misses       1080     1098      +18     
- Partials      331      338       +7     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `83.88% <82.75%> (-0.03%)` | :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://app.codecov.io/gh/clouditor/clouditor/pull/1147?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor) | Coverage Δ | |
|---|---|---|
| [service/discovery/azure/azure.go](https://app.codecov.io/gh/clouditor/clouditor/pull/1147?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor#diff-c2VydmljZS9kaXNjb3ZlcnkvYXp1cmUvYXp1cmUuZ28=) | `86.50% <ø> (ø)` | |
| [service/discovery/azure/storage.go](https://app.codecov.io/gh/clouditor/clouditor/pull/1147?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clouditor#diff-c2VydmljZS9kaXNjb3ZlcnkvYXp1cmUvc3RvcmFnZS5nbw==) | `85.65% <82.75%> (-1.22%)` | :arrow_down: |


oxisto: Would also be good to update https://github.com/clouditor/clouditor/tree/main/service/discovery/README.md

Patch:
diff --git a/go.mod b/go.mod
index 84132f14..c6765d57 100644
--- a/go.mod
+++ b/go.mod
@@ -174,6 +174,7 @@ require (
 )
 
 require (
+	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.1.0
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/go-ini/ini v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index af3fc0da..0d5908eb 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.11.0 h1:hxVDRUlzurR0SR71oRaZjn/qS6Ub2nVCPZ+AYxFR+IA=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.11.0/go.mod h1:ThM/jv2MXLB5PMseu0si9VcGF62a3963e7lMvPZ4zyU=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.1.0 h1:G2MvNS98bjXD7Vks+psbTU/uBiBH7gicij12Xc8q6lM=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.1.0/go.mod h1:f/IvRlQ/eFP31UXVUwh3BzTOOC2cEo6/u+7g9+KTzPk=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.3.0 h1:LcJtQjCXJUm1s7JpUHZvu+bpgURhCatxVNbGADXniX0=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.3.0/go.mod h1:+OgGVo0Httq7N5oayfvaLQ/Jq+2gJdqfp++Hyyl7Tws=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/subscription/armsubscription v1.1.0 h1:pYhaMoTHP/zYIJGDA1sWsfyTDjdglaoYjIFMOEcL+/U=
diff --git a/service/discovery/README.md b/service/discovery/README.md
index 8e44a020..ec6a4ffe 100644
--- a/service/discovery/README.md
+++ b/service/discovery/README.md
@@ -33,7 +33,7 @@
 | Resource          | ✅     | ✅   |
 | NetworkInterfaces | ✅     | ✅   |
 | ResourceLogging  | ✅     |    |
-| Backup  | ✅    |    |
+| Backups  | ❌ |    |
 
 #### Resource
 | Evidence    | Azure | AWS |
@@ -76,6 +76,7 @@
 | Resource          | ✅     | ✅   |
 | AtRestEncryption  | ✅     | ✅   |
 | Immutability      | ✅     | ❌   |
+| Backups      | ✅     |  ❌  |
 
 #### ManagedKeyEncryption
 | Evidence  | Azure | AWS |
@@ -133,6 +134,7 @@
 |--------------|-------|-----|
 | Storage      | ✅     | ✅   |
 | PublicAccess | ✅     | ❌   |
+| Backups  | ✅     |    |
 
 #### Storage
 | Evidence         | Azure | AWS |
@@ -141,7 +143,7 @@
 | AtRestEncryption | ✅     | ✅   |
 | Immutability     | ✅     | ❌   |
 | ResourceLogging  | ✅     |    |
-| Backup  | ✅     |    |
+| Backups  | ✅     |    |
 
 ### ObjectStorageService
 | Evidence       | Azure | AWS |
@@ -169,6 +171,7 @@
 | Evidence          | Azure | AWS |
 |-------------------|-------|-----|
 | Storage           | ✅     | ❌   |
+|Backups             |         |  ❌  |
 
 #### ManagedKeyEncryption
 | Evidence  | Azure | AWS |
@@ -183,23 +186,48 @@
 | Algorithm  | ❌     | ❌   |
 | KeyUrl     | ✅     | ✅   |
 
+### Database Storage
+| Evidence     | Azure | AWS |
+|--------------|-------|-----|
+| Storage      | ✅     | ❌   |
+| Parent       |  ✅    | ❌   |
+
+### Database Service
+| Evidence     | Azure | AWS |
+|--------------|-------|-----|
+| NetworkService      | ✅     | ❌   |
+| AnomalyDetection       |  ✅    | ❌   |
+
+#### Networkservice
+| Evidence             | Azure | AWS |
+|----------------------|-------|-----|
+| Networking           | ✅     | ✅   |
+| Authenticity         | ❌     | ❌   |
+| Compute              | ❌     | ❌   |  
+| TransportEncryption  | ❌     | ✅   |
+| Ips                  | ❌     | ❌   |
+| Ports                | ❌     | ❌   |
 </details>
 
 # Azure Backup
+<details>
+<summary>Expand</summary>
+
 There are 2 different backup solutions for different resources
 - Backup Vaults and
 - Recovery Services Vault.
 
 | Resource   | Backup Vaults | Recovery Services Vault |
 |------------|-------|-----|
-| Azure Virtual Machine | | ✅ |
-| Azure Storage (Files)| | ✅ |
-| Azure Backup Agent| | ✅ |
-| Azure Backup Server| | ✅ |
-| DPM| | ✅ |
-| SQL in Azure VM | | ✅ |
-| SAP HANA in Azure VM | | ✅ |
-| Azure Storage (Blobs) | ✅ | |
-| Azure disks | ✅ | |
-| Azure Database for PostgreSQL servers | ✅ | |
-| Kubernetes Services | ✅ | |
+| Azure Virtual Machine | | x |
+| Azure Storage (Files)| | x |
+| Azure Backup Agent| | x |
+| Azure Backup Server| | x |
+| DPM| | x |
+| SQL in Azure VM | | x |
+| SAP HANA in Azure VM | | x |
+| Azure Storage (Blobs) | x | |
+| Azure disks | x | |
+| Azure Database for PostgreSQL servers | x | |
+| Kubernetes Services | x | |
+</details>
diff --git a/service/discovery/azure/azure.go b/service/discovery/azure/azure.go
index 3f13c28f..68f5beb7 100644
--- a/service/discovery/azure/azure.go
+++ b/service/discovery/azure/azure.go
@@ -43,6 +43,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity"
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/subscription/armsubscription"
 
@@ -67,6 +68,8 @@ const (
 	Duration30Days = time.Duration(30 * time.Hour * 24)
 	Duration7Days  = time.Duration(7 * time.Hour * 24)
 
+	AES256 = "AES256"
+
 	RetentionPeriod90Days = 90
 )
 
@@ -136,6 +139,11 @@ type clients struct {
 	fileStorageClient   *armstorage.FileSharesClient
 	accountsClient      *armstorage.AccountsClient
 
+	// DB
+	databasesClient        *armsql.DatabasesClient
+	sqlServersClient       *armsql.ServersClient
+	threatProtectionClient *armsql.DatabaseAdvancedThreatProtectionSettingsClient
+
 	// Network
 	networkInterfacesClient     *armnetwork.InterfacesClient
 	loadBalancerClient          *armnetwork.LoadBalancersClient
diff --git a/service/discovery/azure/storage.go b/service/discovery/azure/storage.go
index e04e8433..0bf1b06c 100644
--- a/service/discovery/azure/storage.go
+++ b/service/discovery/azure/storage.go
@@ -37,6 +37,8 @@ import (
 	"clouditor.io/clouditor/voc"
 
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection"
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"
+
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
 )
@@ -104,9 +106,140 @@ func (d *azureStorageDiscovery) List() (list []voc.IsCloudResource, err error) {
 	}
 	list = append(list, storageAccounts...)
 
+	// Discover sql databases
+	dbs, err := d.discoverSqlServers()
+	if err != nil {
+		return nil, fmt.Errorf("could not discover sql databases: %w", err)
+	}
+	list = append(list, dbs...)
+
 	return
 }
 
+// discoverSqlServers discovers the sql server and databases
+func (d *azureStorageDiscovery) discoverSqlServers() ([]voc.IsCloudResource, error) {
+	var (
+		list []voc.IsCloudResource
+		err  error
+	)
+
+	// initialize SQL server client
+	if err := d.initSQLServersClient(); err != nil {
+		return nil, err
+	}
+
+	// Discover sql server
+	err = listPager(d.azureDiscovery,
+		d.clients.sqlServersClient.NewListPager,
+		d.clients.sqlServersClient.NewListByResourceGroupPager,
+		func(res armsql.ServersClientListResponse) []*armsql.Server {
+			return res.Value
+		},
+		func(res armsql.ServersClientListByResourceGroupResponse) []*armsql.Server {
+			return res.Value
+		},
+		func(server *armsql.Server) error {
+			db, err := d.handleSqlServer(server)
+			if err != nil {
+				return fmt.Errorf("could not handle sql database: %w", err)
+			}
+			log.Infof("Adding sql database '%s", *server.Name)
+			list = append(list, db...)
+
+			return nil
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	return list, nil
+}
+
+func (d *azureStorageDiscovery) handleSqlServer(server *armsql.Server) ([]voc.IsCloudResource, error) {
+	var (
+		dbStorage voc.IsCloudResource
+		dbService voc.IsCloudResource
+		list      []voc.IsCloudResource
+		err       error
+	)
+
+	// initialize SQL databases client
+	if err = d.initDatabasesClient(); err != nil {
+		return nil, err
+	}
+
+	// Get databases for given server
+	serverlistPager := d.clients.databasesClient.NewListByServerPager(resourceGroupName(util.Deref(server.ID)), *server.Name, &armsql.DatabasesClientListByServerOptions{})
+	for serverlistPager.More() {
+		pageResponse, err := serverlistPager.NextPage(context.TODO())
+		if err != nil {
+			err = fmt.Errorf("%s: %v", ErrGettingNextPage, err)
+			return nil, err
+		}
+
+		for _, value := range pageResponse.Value {
+			// Getting anomaly detection status
+			anomalyDetectionEnabeld, err := d.anomalyDetectionEnabled(server, value)
+			if err != nil {
+				log.Errorf("error getting anomaly detection info for database '%s': %v", *value.Name, err)
+			}
+
+			// Create database storage voc object
+			dbStorage = &voc.DatabaseStorage{
+				Storage: &voc.Storage{
+					Resource: discovery.NewResource(d,
+						voc.ResourceID(*value.ID),
+						*value.Name,
+						value.Properties.CreationDate,
+						voc.GeoLocation{
+							Region: *value.Location,
+						},
+						labels(value.Tags),
+						voc.DatabaseStorageType,
+						value),
+					AtRestEncryption: &voc.AtRestEncryption{
+						Enabled:   *value.Properties.IsInfraEncryptionEnabled,
+						Algorithm: AES256,
+					},
+					// TODO(all): Backups
+
+				},
+				Parent: []voc.ResourceID{voc.ResourceID(*server.ID)},
+			}
+
+			list = append(list, dbStorage)
+
+			// Create database service voc object
+			dbService = &voc.DatabaseService{
+				StorageService: &voc.StorageService{
+					NetworkService: &voc.NetworkService{
+						Networking: &voc.Networking{
+							Resource: discovery.NewResource(d,
+								voc.ResourceID(*value.ID),
+								*value.Name,
+								value.Properties.CreationDate,
+								voc.GeoLocation{
+									Region: *value.Location,
+								},
+								labels(value.Tags),
+								voc.DatabaseServiceType,
+								server,
+								value,
+							),
+						},
+						// TODO(all): TransportEncryption, HttpEndpoint
+					},
+				},
+				AnomalyDetection: &voc.AnomalyDetection{
+					Enabled: anomalyDetectionEnabeld,
+				},
+			}
+			list = append(list, dbService)
+		}
+	}
+	return list, nil
+}
+
 func (d *azureStorageDiscovery) discoverStorageAccounts() ([]voc.IsCloudResource, error) {
 	var storageResourcesList []voc.IsCloudResource
 
@@ -428,7 +561,7 @@ func storageAtRestEncryption(account *armstorage.Account) (enc voc.IsAtRestEncry
 	} else if util.Deref(account.Properties.Encryption.KeySource) == armstorage.KeySourceMicrosoftStorage {
 		enc = &voc.ManagedKeyEncryption{
 			AtRestEncryption: &voc.AtRestEncryption{
-				Algorithm: "AES256",
+				Algorithm: AES256,
 				Enabled:   true,
 			},
 		}
@@ -445,6 +578,30 @@ func storageAtRestEncryption(account *armstorage.Account) (enc voc.IsAtRestEncry
 	return enc, nil
 }
 
+// anomalyDetectionEnabled returns true if Azure Advanced Threat Protection is enabled for the database.
+func (d *azureStorageDiscovery) anomalyDetectionEnabled(server *armsql.Server, db *armsql.Database) (bool, error) {
+	// initialize threat protection client
+	if err := d.initThreatProtectionClient(); err != nil {
+		return false, err
+	}
+
+	listPager := d.clients.threatProtectionClient.NewListByDatabasePager(resourceGroupName(util.Deref(db.ID)), *server.Name, *db.Name, &armsql.DatabaseAdvancedThreatProtectionSettingsClientListByDatabaseOptions{})
+	for listPager.More() {
+		pageResponse, err := listPager.NextPage(context.TODO())
+		if err != nil {
+			err = fmt.Errorf("%s: %v", ErrGettingNextPage, err)
+			return false, err
+		}
+
+		for _, value := range pageResponse.Value {
+			if *value.Properties.State == armsql.AdvancedThreatProtectionStateEnabled {
+				return true, nil
+			}
+		}
+	}
+	return false, nil
+}
+
 // diskEncryptionSetName return the disk encryption set ID's name
 func diskEncryptionSetName(diskEncryptionSetID string) string {
 	if diskEncryptionSetID == "" {
@@ -522,3 +679,24 @@ func (d *azureStorageDiscovery) initBackupInstancesClient() (err error) {
 
 	return
 }
+
+// initDatabasesClient creates the client if not already exists
+func (d *azureStorageDiscovery) initDatabasesClient() (err error) {
+	d.clients.databasesClient, err = initClient(d.clients.databasesClient, d.azureDiscovery, armsql.NewDatabasesClient)
+
+	return
+}
+
+// initSQLServersClient creates the client if not already exists
+func (d *azureStorageDiscovery) initSQLServersClient() (err error) {
+	d.clients.sqlServersClient, err = initClient(d.clients.sqlServersClient, d.azureDiscovery, armsql.NewServersClient)
+
+	return
+}
+
+// initThreatProtectionClient creates the client if not already exists
+func (d *azureStorageDiscovery) initThreatProtectionClient() (err error) {
+	d.clients.threatProtectionClient, err = initClient(d.clients.threatProtectionClient, d.azureDiscovery, armsql.NewDatabaseAdvancedThreatProtectionSettingsClient)
+
+	return
+}
diff --git a/voc/database_service.go b/voc/database_service.go
index dd31d8ea..2dd77b99 100644
--- a/voc/database_service.go
+++ b/voc/database_service.go
@@ -32,4 +32,5 @@ var DatabaseServiceType = []string{"DatabaseService", "StorageService", "Network
 // DatabaseService is an entity in our Cloud ontology. This class represents a database service. For example, a postgres SQL server would be modelled as a database service (with a host and IP) and the individual tables or collections would be modelled as a DatabaseStorage entity.
 type DatabaseService struct {
 	*StorageService
+	AnomalyDetection *AnomalyDetection `json:"anomalyDetection"`
 }


Test patch:
diff --git a/service/discovery/azure/storage_test.go b/service/discovery/azure/storage_test.go
index b7a20e5e..2fb24ac9 100644
--- a/service/discovery/azure/storage_test.go
+++ b/service/discovery/azure/storage_test.go
@@ -40,6 +40,7 @@ import (
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity"
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"
 	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
 	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/util/json"
@@ -298,6 +299,41 @@ func (m mockStorageSender) Do(req *http.Request) (res *http.Response, err error)
 				},
 			},
 		}, 200)
+	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Sql/servers" {
+		return createResponse(req, map[string]interface{}{
+			"value": &[]map[string]interface{}{
+				{
+					"id":   "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1",
+					"name": "SQLServer1",
+				},
+			},
+		}, 200)
+	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases" {
+		return createResponse(req, map[string]interface{}{
+			"value": &[]map[string]interface{}{
+				{
+					"id":       "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1",
+					"name":     "SqlDatabase1",
+					"location": "eastus",
+					"properties": map[string]interface{}{
+						"isInfraEncryptionEnabled": true,
+					},
+				},
+			},
+		}, 200)
+	} else if req.URL.Path == "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1/advancedThreatProtectionSettings" {
+		return createResponse(req, map[string]interface{}{
+			"value": &[]map[string]interface{}{
+				{
+					"id":       "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1/advancedThreatProtectionSettings/Default",
+					"name":     "Default",
+					"location": "eastus",
+					"properties": map[string]interface{}{
+						"state": "Enabled",
+					},
+				},
+			},
+		}, 200)
 	}
 
 	return m.mockSender.Do(req)
@@ -390,7 +426,7 @@ func TestStorage(t *testing.T) {
 
 	assert.NoError(t, err)
 	assert.NotNil(t, list)
-	assert.Equal(t, 9, len(list))
+	assert.Equal(t, 11, len(list))
 	assert.NotEmpty(t, d.Name())
 }
 
@@ -713,7 +749,7 @@ func Test_azureStorageDiscovery_List(t *testing.T) {
 						Resource: &voc.Resource{
 							ID:           "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/res1/providers/Microsoft.DataProtection/backupVaults/backupAccount1/backupInstances/account1-account1-22222222-2222-2222-2222-222222222222",
 							Name:         "account1-account1-22222222-2222-2222-2222-222222222222",
-							ServiceID:    "11111111-1111-1111-1111-111111111111",
+							ServiceID:    testdata.MockCloudServiceID1,
 							CreationTime: 0,
 							Type:         voc.ObjectStorageType,
 							GeoLocation: voc.GeoLocation{
@@ -723,6 +759,50 @@ func Test_azureStorageDiscovery_List(t *testing.T) {
 						},
 					},
 				},
+				&voc.DatabaseStorage{
+					Storage: &voc.Storage{
+						Resource: &voc.Resource{
+							ID:           "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1",
+							ServiceID:    testdata.MockCloudServiceID1,
+							Name:         "SqlDatabase1",
+							CreationTime: 0,
+							Type:         voc.DatabaseStorageType,
+							GeoLocation: voc.GeoLocation{
+								Region: "eastus",
+							},
+							Labels: make(map[string]string),
+							Raw:    "{\"*armsql.Database\":[{\"id\":\"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1\",\"location\":\"eastus\",\"name\":\"SqlDatabase1\",\"properties\":{\"isInfraEncryptionEnabled\":true}}]}",
+						},
+						AtRestEncryption: &voc.AtRestEncryption{
+							Enabled:   true,
+							Algorithm: AES256,
+						},
+					},
+					Parent: []voc.ResourceID{"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1"},
+				},
+				&voc.DatabaseService{
+					StorageService: &voc.StorageService{
+						NetworkService: &voc.NetworkService{
+							Networking: &voc.Networking{
+								Resource: &voc.Resource{
+									ID:           "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1",
+									ServiceID:    testdata.MockCloudServiceID1,
+									Name:         "SqlDatabase1",
+									CreationTime: 0,
+									Type:         voc.DatabaseServiceType,
+									GeoLocation: voc.GeoLocation{
+										Region: "eastus",
+									},
+									Labels: make(map[string]string),
+									Raw:    "{\"*armsql.Database\":[{\"id\":\"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1\",\"location\":\"eastus\",\"name\":\"SqlDatabase1\",\"properties\":{\"isInfraEncryptionEnabled\":true}}],\"*armsql.Server\":[{\"id\":\"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1\",\"name\":\"SQLServer1\"}]}",
+								},
+							},
+						},
+					},
+					AnomalyDetection: &voc.AnomalyDetection{
+						Enabled: true,
+					},
+				},
 			},
 			wantErr: assert.NoError,
 		},
@@ -1646,3 +1726,196 @@ func Test_azureStorageDiscovery_discoverObjectStorages(t *testing.T) {
 		})
 	}
 }
+
+func Test_azureStorageDiscovery_handleSqlServer(t *testing.T) {
+	type fields struct {
+		azureDiscovery     *azureDiscovery
+		defenderProperties map[string]*defenderProperties
+	}
+	type args struct {
+		server *armsql.Server
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    []voc.IsCloudResource
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name: "error list pager",
+			fields: fields{
+				azureDiscovery: &azureDiscovery{
+					clients: clients{},
+				},
+			},
+			args: args{
+				server: &armsql.Server{
+					Location: util.Ref("eastus"),
+					ID:       util.Ref("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1"),
+					Name:     util.Ref("SQLServer1"),
+				},
+			},
+			want: nil,
+			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+				return assert.ErrorContains(t, err, "error getting next page: ")
+			},
+		},
+		{
+			name: "Happy path",
+			fields: fields{
+				azureDiscovery: NewMockAzureDiscovery(newMockStorageSender()),
+			},
+			args: args{
+				server: &armsql.Server{
+					Location: util.Ref("eastus"),
+					ID:       util.Ref("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1"),
+					Name:     util.Ref("SQLServer1"),
+				},
+			},
+			want: []voc.IsCloudResource{
+				&voc.DatabaseStorage{
+					Storage: &voc.Storage{
+						Resource: &voc.Resource{
+							ID:           "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1",
+							Name:         "SqlDatabase1",
+							ServiceID:    testdata.MockCloudServiceID1,
+							CreationTime: 0,
+							Type:         voc.DatabaseStorageType,
+							GeoLocation: voc.GeoLocation{
+								Region: "eastus",
+							},
+							Labels: make(map[string]string),
+							Raw:    "{\"*armsql.Database\":[{\"id\":\"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1\",\"location\":\"eastus\",\"name\":\"SqlDatabase1\",\"properties\":{\"isInfraEncryptionEnabled\":true}}]}",
+						},
+						AtRestEncryption: &voc.AtRestEncryption{
+							Enabled:   true,
+							Algorithm: AES256,
+						},
+					},
+					Parent: []voc.ResourceID{"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1"},
+				},
+				&voc.DatabaseService{
+					StorageService: &voc.StorageService{
+						NetworkService: &voc.NetworkService{
+							Networking: &voc.Networking{
+								Resource: &voc.Resource{
+									ID:           "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1",
+									ServiceID:    testdata.MockCloudServiceID1,
+									Name:         "SqlDatabase1",
+									CreationTime: 0,
+									Type:         voc.DatabaseServiceType,
+									GeoLocation: voc.GeoLocation{
+										Region: "eastus",
+									},
+									Labels: make(map[string]string),
+									Raw:    "{\"*armsql.Database\":[{\"id\":\"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1\",\"location\":\"eastus\",\"name\":\"SqlDatabase1\",\"properties\":{\"isInfraEncryptionEnabled\":true}}],\"*armsql.Server\":[{\"id\":\"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1\",\"location\":\"eastus\",\"name\":\"SQLServer1\"}]}",
+								},
+							},
+						},
+					},
+					AnomalyDetection: &voc.AnomalyDetection{
+						Enabled: true,
+					},
+				},
+			},
+			wantErr: assert.NoError,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := &azureStorageDiscovery{
+				azureDiscovery:     tt.fields.azureDiscovery,
+				defenderProperties: tt.fields.defenderProperties,
+			}
+
+			got, err := d.handleSqlServer(tt.args.server)
+			if !tt.wantErr(t, err, fmt.Sprintf("handleSqlServer(%v, %v)", tt.args.server, tt.args.server)) {
+				return
+			}
+
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func Test_azureStorageDiscovery_anomalyDetectionEnabled(t *testing.T) {
+	type fields struct {
+		azureDiscovery     *azureDiscovery
+		defenderProperties map[string]*defenderProperties
+	}
+	type args struct {
+		server *armsql.Server
+		db     *armsql.Database
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    bool
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name: "error list pager",
+			fields: fields{
+				azureDiscovery: &azureDiscovery{
+					clients: clients{},
+				},
+			},
+			args: args{
+				server: &armsql.Server{
+					Location: util.Ref("eastus"),
+					ID:       util.Ref("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1"),
+					Name:     util.Ref("SQLServer1"),
+				},
+				db: &armsql.Database{
+					ID:       util.Ref("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1"),
+					Name:     util.Ref("SqlDatabase1"),
+					Location: util.Ref("eastus"),
+					Properties: &armsql.DatabaseProperties{
+						IsInfraEncryptionEnabled: util.Ref(true),
+					},
+				},
+			},
+			want: false,
+			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+				return assert.ErrorContains(t, err, "error getting next page: ")
+			},
+		},
+		{
+			name: "Happy path",
+			fields: fields{
+				azureDiscovery: NewMockAzureDiscovery(newMockStorageSender()),
+			},
+			args: args{
+				server: &armsql.Server{
+					Location: util.Ref("eastus"),
+					ID:       util.Ref("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1"),
+					Name:     util.Ref("SQLServer1"),
+				},
+				db: &armsql.Database{
+					ID:       util.Ref("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/res1/providers/Microsoft.Sql/servers/SQLServer1/databases/SqlDatabase1"),
+					Name:     util.Ref("SqlDatabase1"),
+					Location: util.Ref("eastus"),
+					Properties: &armsql.DatabaseProperties{
+						IsInfraEncryptionEnabled: util.Ref(true),
+					},
+				},
+			},
+			want:    true,
+			wantErr: assert.NoError,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := &azureStorageDiscovery{
+				azureDiscovery:     tt.fields.azureDiscovery,
+				defenderProperties: tt.fields.defenderProperties,
+			}
+			got, err := d.anomalyDetectionEnabled(tt.args.server, tt.args.db)
+
+			tt.wantErr(t, err)
+			assert.Equal(t, got, tt.want)
+		})
+	}
+}