Framework/Core/SVT/Services/SQLDatabase.ps1

using namespace Microsoft.Azure.Commands.Sql.Auditing.Model
using namespace Microsoft.Azure.Commands.Sql.ServerUpgrade.Model
using namespace Microsoft.Azure.Commands.Sql.TransparentDataEncryption.Model
using namespace Microsoft.Azure.Commands.Sql.ThreatDetection.Model

Set-StrictMode -Version Latest
class SQLDatabase: SVTBase
{
    hidden [PSObject] $ResourceObject;
    hidden [PSObject[]] $SqlDatabases = $null;
    hidden [PSObject[]] $SqlFirewallDetails = $null;

    SQLDatabase([string] $subscriptionId, [string] $resourceGroupName, [string] $resourceName):
        Base($subscriptionId, $resourceGroupName, $resourceName)
    {
        $this.GetResourceObject();
    }

    SQLDatabase([string] $subscriptionId, [SVTResource] $svtResource):
        Base($subscriptionId, $svtResource)
    {
        $this.GetResourceObject();
    }

    hidden [PSObject] GetResourceObject()
    {
        if (-not $this.ResourceObject) {
            $this.ResourceObject =   Get-AzureRmResource -ResourceName $this.ResourceContext.ResourceName -ResourceGroupName $this.ResourceContext.ResourceGroupName

            if(-not $this.ResourceObject)
            {
                throw ([SuppressedException]::new(("Resource '{0}' not found under Resource Group '{1}'" -f ($this.ResourceContext.ResourceName), ($this.ResourceContext.ResourceGroupName)), [SuppressedExceptionType]::InvalidOperation))
            }
        }
        return $this.ResourceObject;
    }

    [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls)
    {
        if(-not $this.SqlDatabases)
        {
            try
            {
                $this.SqlDatabases = @();
                $this.SqlDatabases += Get-AzureRmSqlDatabase -ResourceGroupName $this.ResourceContext.ResourceGroupName -ServerName $this.ResourceContext.ResourceName -ErrorAction Stop |
                                Where-Object { $_.DatabaseName -ne "master" }
            }
            catch
            {
                $this.EvaluationError($_);
            }
        }

        $result = @();

        # Filter control if there are no databases
        if($this.SqlDatabases.Count -eq 0)
        {
            $result += $controls | Where-Object { $_.Tags -notcontains "SqlDatabase" };
        }
        else
        {
            $result += $controls;
        }

        return $result;
    }

    hidden [ControlResult] CheckSqlServerVersionUpgrade([ControlResult] $controlResult)
    {
        $upgradeStatus = Get-AzureRmSqlServerUpgrade -ResourceGroupName  $this.ResourceContext.ResourceGroupName -ServerName $this.ResourceContext.ResourceName -ErrorAction Stop

        $controlResult.AddMessage([MessageData]::new("Current status of SQL Database server upgrade -",
                                                     $upgradeStatus));

        if ($upgradeStatus.Status -eq [ServerUpgradeStatus]::Completed)
        {
            $controlResult.VerificationResult = [VerificationResult]::Passed
        }
        else
        {
            $controlResult.VerificationResult = [VerificationResult]::Failed
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckSqlServerAuditing([ControlResult] $controlResult)
    {
        $serverAudit = Get-AzureRmSqlServerAuditing -ResourceGroupName $this.ResourceContext.ResourceGroupName -ServerName $this.ResourceContext.ResourceName -ErrorAction Stop

        $controlResult.AddMessage([MessageData]::new("Current audit status for SQL server [$($this.ResourceContext.ResourceName)]:", $serverAudit))

        if($null -ne $serverAudit){
                $isCompliant = (($serverAudit.AuditState -eq [AuditStateType]::Enabled) `
                               -and ($serverAudit.RetentionInDays -eq $this.ControlSettings.SqlServer.AuditRetentionPeriod_Min -or $serverAudit.RetentionInDays -eq $this.ControlSettings.SqlServer.AuditRetentionPeriod_Forever))

                if ($isCompliant){
                           $controlResult.VerificationResult = [VerificationResult]::Passed
                    }
                else{
                        $controlResult.EnableFixControl = $true;
                        $controlResult.AddMessage([VerificationResult]::Failed,
                                                  "Audit settings are either disabled OR not retaining logs for at least $($this.ControlSettings.SqlServer.AuditRetentionPeriod_Min) days for SQL server - [$($this.ResourceContext.ResourceName)]");
                    }

        }
        else{
            $controlResult.AddMessage("Unable to get audit details for SQL server [$($this.ResourceContext.ResourceName)]");
        }

        return $controlResult;
    }

    hidden [ControlResult[]] CheckSqlDatabaseAuditing([ControlResult] $controlResult)
    {
        [ControlResult[]] $resultControlResultList = @()

        if(($this.SqlDatabases | Measure-Object ).Count -eq 0)
        {
            $controlResult.AddMessage([MessageData]::new("No database found on SQL Server ["+ $this.ResourceContext.ResourceName +"]"));
            #Passing the status as there is no database found on the SQL Server
            $controlResult.VerificationResult = [VerificationResult]::Passed;
            $resultControlResultList += $controlResult
        }
        else
        {
            $this.SqlDatabases |
            ForEach-Object {
                $dbName = $_.DatabaseName;
                #[ControlResult] $childControlResult = $this.CreateControlResult($dbName);
                [ControlResult] $childControlResult = $this.CreateChildControlResult($dbName, $controlResult);

                $dbAuditPolicy = Get-AzureRmSqlDatabaseAuditing `
                                    -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                    -ServerName $this.ResourceContext.ResourceName `
                                    -DatabaseName $dbName `
                                    -ErrorAction Stop

                if($null -ne $dbAuditPolicy){

                        $childControlResult.AddMessage([MessageData]::new("Current audit details for database [$($dbName)]:", $dbAuditPolicy));

                        $isCompliant = (($dbAuditPolicy.AuditState -eq [AuditStateType]::Enabled) `
                                                                            -and ($dbAuditPolicy.RetentionInDays -eq $this.ControlSettings.SqlServer.AuditRetentionPeriod_Min -or $dbAuditPolicy.RetentionInDays -eq $this.ControlSettings.SqlServer.AuditRetentionPeriod_Forever))

                            if($isCompliant){
                                $childControlResult.VerificationResult = [VerificationResult]::Passed
                            }
                            else{
                                $childControlResult.EnableFixControl = $true;
                                $childControlResult.AddMessage([VerificationResult]::Failed,
                                "Audit settings are either disabled OR not retaining logs for at least $($this.ControlSettings.SqlServer.AuditRetentionPeriod_Min) days for SQL database - [$($dbName)]");
                            }
                    }
                else{
                            $childControlResult.AddMessage("Unable to get database audit details for SQL database [$($dbName)]");
                    }
                $resultControlResultList += $childControlResult
            }
        }

        return $resultControlResultList;
    }

    hidden [ControlResult[]] CheckSqlDatabaseTDE([ControlResult] $controlResult)
    {
        [ControlResult[]] $resultControlResultList = @()

        if(($this.SqlDatabases | Measure-Object ).Count -eq 0)
        {
            $controlResult.AddMessage([MessageData]::new("No database found on SQL Server - ["+ $this.ResourceContext.ResourceName +"]"));
            #Since there is no database found we are passing this control
            $controlResult.VerificationResult = [VerificationResult]::Passed;
            $resultControlResultList += $controlResult
        }
        else
        {
            $atleastOneFailed = $false
            $this.SqlDatabases | ForEach-Object {
                $dbName = $_.DatabaseName;
                [ControlResult] $childControlResult = $this.CreateChildControlResult($dbName, $controlResult);
                try {
                    $tdeStatus = Get-AzureRmSqlDatabaseTransparentDataEncryption `
                    -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                    -ServerName $this.ResourceContext.ResourceName `
                    -DatabaseName $dbName `
                    -ErrorAction Stop

                    $childControlResult.AddMessage([MessageData]::new("Current TDE Status for - ["+ $dbName +"]",
                                                    ($tdeStatus )));

                    if($tdeStatus.State -eq [TransparentDataEncryptionStateType]::Enabled){
                            $childControlResult.VerificationResult = [VerificationResult]::Passed;
                    }
                    else
                    {
                        $atleastOneFailed = $true
                        $childControlResult.VerificationResult = [VerificationResult]::Failed;
                        $childControlResult.EnableFixControl = $true;
                    }
                }
                catch {
                    $atleastOneFailed = $true
                    $childControlResult.VerificationResult = [VerificationResult]::Error;
                }
                $resultControlResultList += $childControlResult
            } #End of ForEach-Object
            if($atleastOneFailed) {
                $controlResult.VerificationResult = [VerificationResult]::Failed;
            }else{
                $controlResult.VerificationResult = [VerificationResult]::Passed;
            }
            $resultControlResultList += $controlResult
        }
        return  $resultControlResultList;
    }

    hidden [ControlResult] CheckSqlServerADAdmin([ControlResult] $controlResult)
    {
        $adAdmin = Get-AzureRmSqlServerActiveDirectoryAdministrator -ResourceGroup $this.ResourceContext.ResourceGroupName -Server $this.ResourceContext.ResourceName.ToLower() -ErrorAction Stop

        $controlResult.AddMessage([MessageData]::new("Current status of Active Directory Admin for ["+ $this.ResourceContext.ResourceName +"] is"));

        if(($adAdmin | Measure-Object).Count -gt 0){
                $controlResult.VerificationResult = [VerificationResult]::Passed
                $controlResult.AddMessage([MessageData]::new("Active Directory admins are assigned on SQL Server - ["+ $this.ResourceContext.ResourceName +"]",
                                                            ($adAdmin)));
        }
        else{
            $controlResult.VerificationResult = [VerificationResult]::Failed
            $controlResult.AddMessage([MessageData]::new("No Active Directory admin is assigned on SQL Server - ["+ $this.ResourceContext.ResourceName +"]"));
            $controlResult.EnableFixControl = $true;
        }
        return $controlResult
    }

    hidden [ControlResult] CheckSqlServerThreatDetection([ControlResult] $controlResult)
    {
        $isCompliant = $false

        #First check if the server auditing is enabled, without which TD does not work
        $serverAudit = Get-AzureRmSqlServerAuditing -ResourceGroupName $this.ResourceContext.ResourceGroupName -ServerName $this.ResourceContext.ResourceName.ToLower() -ErrorAction Stop

        if($null -ne $serverAudit){
            #Check if Audit is Enabled
                if($serverAudit.AuditState -eq [AuditStateType]::Enabled){
                        $serverThreat = Get-AzureRmSqlServerThreatDetectionPolicy `
                                    -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                    -ServerName $this.ResourceContext.ResourceName.ToLower() `
                                    -ErrorAction Stop

                        $controlResult.AddMessage([MessageData]::new("Current threat detection status for SQL server ["+ $this.ResourceContext.ResourceName +"] is",
                                                            ($serverThreat)));

                        $excludedTypeCount = ($serverThreat.ExcludedDetectionTypes | Measure-Object ).Count

                        if($excludedTypeCount -gt 0){
                            $controlResult.AddMessage([MessageData]::new("All the required audit event types are not enabled for SQL Server - ["+ $this.ResourceContext.ResourceName +"]"));
                        }

                        $isCompliant =  (($serverThreat.ThreatDetectionState -eq [ThreatDetectionStateType]::Enabled) `
                                    -and ($excludedTypeCount -eq 0) `
                                    -and (($serverThreat.EmailAdmins  -eq $True) -or ($null -ne $serverThreat.NotificationRecipientsEmails)))
                        if ($isCompliant) {
                            $controlResult.VerificationResult = [VerificationResult]::Passed
                        }
                        else{
                            $controlResult.EnableFixControl = $true;
                            $controlResult.VerificationResult = [VerificationResult]::Failed
                        }
                            return $controlResult
                        }
                else{
                    $controlResult.EnableFixControl = $true;
                    $controlResult.AddMessage([VerificationResult]::Failed, "Auditing is not enabled for SQL server ["+ $this.ResourceContext.ResourceName +"]. Threat detection requires auditing enabled.");
                    return $controlResult
                }
        }
        else{
            $controlResult.AddMessage("Unable to get audit details for SQL server [$($this.ResourceContext.ResourceName)]. Threat detection requires auditing enabled.");
            return $controlResult
        }

    }

    hidden [ControlResult[]] CheckSqlDatabaseThreatDetection([ControlResult] $controlResult)
    {
        [ControlResult[]] $resultControlResultList = @()

        if(($this.SqlDatabases | Measure-Object ).Count -eq 0)
        {
            $controlResult.AddMessage([MessageData]::new("No database found on SQL Server - ["+ $this.ResourceContext.ResourceName +"]"));
            #Since there is no database found we are passing this control
            $controlResult.VerificationResult = [VerificationResult]::Passed;
            $resultControlResultList += $controlResult
        }
        else
        {
            #Checking TD and auditing at server level. If it is enabled on server level then it by default applies on DB level
            $serverTDEnabled = $false
            $serverTDEnabled = $this.IsServerThreatDetectionEnabled()

            $this.SqlDatabases | ForEach-Object {
                $dbName = $_.DatabaseName;
                [ControlResult] $childControlResult = $this.CreateChildControlResult($dbName, $controlResult);

                if($serverTDEnabled){
                    $childControlResult.AddMessage([VerificationResult]::Passed, "Threat detection is enabled at the server level.")
                }
                else{
                    $isAuditCompliant = $false
                    #First checking if the database auditing is enabled. TD requires auditing enabled.
                    $dbAuditPolicy = Get-AzureRmSqlDatabaseAuditing `
                                -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                -ServerName $this.ResourceContext.ResourceName `
                                -DatabaseName $dbName `
                                -ErrorAction Stop

                    if($null -ne $dbAuditPolicy){
                            $isAuditCompliant = ($dbAuditPolicy.AuditState -eq 'Enabled')
                        }

                    if($isAuditCompliant){
                        $dbThreat = Get-AzureRmSqlDatabaseThreatDetectionPolicy `
                                -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                -ServerName $this.ResourceContext.ResourceName `
                                -DatabaseName $dbName `
                                -ErrorAction Stop

                        $childControlResult.AddMessage([MessageData]::new("Current threat detection state for SQL database - ["+ $dbName +"].",
                                                                    ($dbThreat)));

                        if(($dbThreat.ThreatDetectionState -eq [ThreatDetectionStateType]::Enabled) `
                            -and ($dbThreat.ExcludedDetectionTypes.Count -eq 0) `
                            -and (($dbThreat.EmailAdmins -eq $True) -or ($dbThreat.NotificationRecipientsEmails.Length -gt 0)))
                        {
                            $isCompliant = $True
                        }
                        else{
                            $isCompliant = $false
                        }

                        if($isCompliant) {
                            $childControlResult.VerificationResult = [VerificationResult]::Passed;
                        }
                        else {
                            $childControlResult.EnableFixControl = $true;
                            $childControlResult.AddMessage([VerificationResult]::Failed, "Threat detection is not correctly enabled on SQL database ["+ $dbName +"].")
                        }
                }
                else{
                    $childControlResult.EnableFixControl = $true;
                    $childControlResult.AddMessage([VerificationResult]::Failed, "Auditing is not enabled on SQL database ["+ $dbName +"]. Threat detection requires auditing enabled.")
                }
                }

                $resultControlResultList += $childControlResult

            }
        }

        return  $resultControlResultList;
    }

    hidden [ControlResult] CheckSqlDatabaseFirewallEnabled([ControlResult] $controlResult)
    {
        $firewallDtls = $this.GetSqlServerFirewallRules()
        if(($firewallDtls | Measure-Object ).Count -gt 0){
            $controlResult.AddMessage("Firewall is enabled for [$($this.ResourceContext.ResourceName)].");
            $controlResult.VerificationResult = [VerificationResult]::Passed
        }
        else{
            $controlResult.AddMessage("Firewall is not enabled for [$($this.ResourceContext.ResourceName)].");
            $controlResult.VerificationResult = [VerificationResult]::Failed
        }
        return $controlResult
    }

    hidden [ControlResult] CheckSqlDatabaseFirewallIPAddressRange([ControlResult] $controlResult)
    {
        #Current function will check firewall ip address ranges, if firewall is enabled. When it is enabled, it allows any traffic from services within your Azure subscription to pass through.
        #Default record will be there with Start IP address as 0.0.0.0 and End Ip address as 0.0.0.0
        $firewallDtls = $this.GetSqlServerFirewallRules()
        if(($firewallDtls | Measure-Object ).Count -gt 0)
        {
            $firewallDtlsForAzure = $firewallDtls | Where-Object { $_.FirewallRuleName -ne "AllowAllWindowsAzureIps" }
            if(($firewallDtlsForAzure | Measure-Object ).Count -eq 0)
            {
                $controlResult.AddMessage([VerificationResult]::Passed, "No custom firewall rules found.");
                return $controlResult
            }

            $controlResult.AddMessage([MessageData]::new("Current firewall settings for - ["+ $this.ResourceContext.ResourceName +"]",
                                                            $firewallDtlsForAzure));

            $anyToAnyRule =  $firewallDtlsForAzure | Where-Object { $_.StartIpAddress -eq $this.ControlSettings.IPRangeStartIP -and $_.EndIpAddress -eq  $this.ControlSettings.IPRangeEndIP}
            if (($anyToAnyRule | Measure-Object).Count -gt 0)
            {
                $controlResult.AddMessage([VerificationResult]::Failed,
                                            [MessageData]::new("Firewall rule covering all IPs (Start IP address: $($this.ControlSettings.IPRangeStartIP) To End IP Address: $($this.ControlSettings.IPRangeEndIP)) is defined."));
            }
            else
            {
                $controlResult.VerificationResult = [VerificationResult]::Verify
            }
            $controlResult.SetStateData("Firewall IP addresses", $firewallDtls);
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, "No custom firewall rules found.");
        }
        return $controlResult
    }

    hidden [ControlResult] CheckSqlServerFirewallAccessAzureService([ControlResult] $controlResult)
    {
        $firewallDtls = $this.GetSqlServerFirewallRules()
        if(($firewallDtls | Measure-Object ).Count -gt 0)
        {
            $firewallDtls = $firewallDtls | Where-Object { $_.FirewallRuleName -eq "AllowAllWindowsAzureIps" }
            if(($firewallDtls | Measure-Object ).Count -gt 0)
            {
                $controlResult.AddMessage([VerificationResult]::Verify,
                                            [MessageData]::new("Azure services are allowed to access the server ["+ $this.ResourceContext.ResourceName +"]"));
            }    
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Azure services are not allowed to access the server ["+ $this.ResourceContext.ResourceName +"]"));
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, "No custom firewall rules found.");
        }
        return $controlResult
    }

    hidden [ControlResult[]]  CheckSqlServerDataMaskingPolicy([ControlResult] $controlResult)
    {

        [ControlResult[]] $resultControlResultList = @()

        if(($this.SqlDatabases | Measure-Object ).Count -eq 0)
        {
            $controlResult.AddMessage([MessageData]::new("No database found on SQL Server ["+ $this.ResourceContext.ResourceName +"]"));
            #Passing the status as there is no database found on the SQL Server
            $controlResult.VerificationResult = [VerificationResult]::Passed;
            $resultControlResultList += $controlResult
        }
        else
        {
            $atleastOneFailed = $false

            $this.SqlDatabases |
            ForEach-Object {
                $dbName = $_.DatabaseName;
                [ControlResult] $childControlResult = $this.CreateChildControlResult($dbName, $controlResult);

                try
                {
                    $dbMaskingPolicy = Get-AzureRmSqlDatabaseDataMaskingPolicy `
                                    -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                    -ServerName $this.ResourceContext.ResourceName `
                                    -DatabaseName $dbName

                    if($null -ne $dbMaskingPolicy){

                         $childControlResult.AddMessage([MessageData]::new("Current masking details for database [$($dbName)]:", $dbMaskingPolicy));

                         if($dbMaskingPolicy.DataMaskingState -eq 'Enabled'){
                                $atleastOneFailed = $true
                                   $childControlResult.VerificationResult = [VerificationResult]::Verify
                                $childControlResult.AddMessage([VerificationResult]::Verify,"SQL database data masking is enabled.");
                            }
                            else
                            {
                                $atleastOneFailed = $false
                                $childControlResult.VerificationResult = [VerificationResult]::Manual
                                $childControlResult.AddMessage([VerificationResult]::Manual, "SQL Database data masking is not enabled");
                            }
                        }
                        else{
                             $childControlResult.AddMessage("Unable to get Database masking details for [$($dbName)]");
                        }
                    $childControlResult.SetStateData("Database masking details for [$($dbName)]", $dbMaskingPolicy);
                    $resultControlResultList += $childControlResult
                }
                catch {
                    $atleastOneFailed = $true
                    $childControlResult.VerificationResult = [VerificationResult]::Error;
                }
            }

            if($atleastOneFailed) {
                $controlResult.VerificationResult = [VerificationResult]::Verify;
            }
            else{
                $controlResult.VerificationResult = [VerificationResult]::Manual;
            }
            $resultControlResultList += $controlResult
        }

        return $resultControlResultList;
    }

    hidden [bool] IsServerThreatDetectionEnabled(){
            $isCompliant = $false
            $serverAudit = Get-AzureRmSqlServerAuditing -ResourceGroupName $this.ResourceContext.ResourceGroupName -ServerName $this.ResourceContext.ResourceName -ErrorAction Stop
            if($null -ne $serverAudit){
                if($serverAudit.AuditState -eq 'Enabled'){
                            $serverThreat = Get-AzureRmSqlServerThreatDetectionPolicy `
                                            -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                            -ServerName $this.ResourceContext.ResourceName `
                                            -ErrorAction Stop
                            $excludedTypeCount = ($serverThreat.ExcludedDetectionTypes | Measure-Object ).Count
                            $isCompliant =  (($serverThreat.ThreatDetectionState -eq [ThreatDetectionStateType]::Enabled) `
                                            -and ($excludedTypeCount -eq 0) `
                                            -and (($serverThreat.EmailAdmins  -eq $True) -or ($null -ne $serverThreat.NotificationRecipientsEmails)))
                    }
                }
        return $isCompliant
}

    hidden     [PSObject[]] GetSqlServerFirewallRules()
    {
        if ($null -eq $this.SqlFirewallDetails) {
            $this.SqlFirewallDetails = Get-AzureRmSqlServerFirewallRule -ResourceGroupName $this.ResourceContext.ResourceGroupName  -ServerName $this.ResourceContext.ResourceName
        }
        return $this.SqlFirewallDetails;
    }
    
}