Framework/Core/SVT/Services/VirtualMachine.ps1

using namespace Microsoft.Azure.Commands.Network.Models
using namespace Microsoft.Azure.Commands.Compute.Models

Set-StrictMode -Version Latest 
class VirtualMachine: SVTBase
{       
    hidden [PSVirtualMachine] $ResourceObject;
    hidden [PSNetworkInterface[]] $VMNICs = $null;

    VirtualMachine([string] $subscriptionId, [string] $resourceGroupName, [string] $resourceName): 
        Base($subscriptionId, $resourceGroupName, $resourceName) 
    { 
        $this.GetResourceObject();
    }
    
    VirtualMachine([string] $subscriptionId, [SVTResource] $svtResource): 
        Base($subscriptionId, $svtResource) 
    { 
        $this.GetResourceObject();
    }
     
    hidden [PSVirtualMachine] GetResourceObject()
    {
        if (-not $this.ResourceObject) {
            $this.ResourceObject = Get-AzureRmVM -ResourceGroupName $this.ResourceContext.ResourceGroupName -Name $this.ResourceContext.ResourceName -WarningAction SilentlyContinue

            if(-not $this.ResourceObject)
            {
                throw ("Resource '{0}' not found under Resource Group '{1}'" -f ($this.ResourceContext.ResourceName), ($this.ResourceContext.ResourceGroupName))
            }
        }
        return $this.ResourceObject;
    }

    hidden [PSNetworkInterface[]] GetVMNICObjects()
    {
        if (-not $this.VMNICs) 
        {
            $this.VMNICs = @();
            if($this.ResourceObject.NetworkProfile -and $this.ResourceObject.NetworkProfile.NetworkInterfaces)
            {
                $this.ResourceObject.NetworkProfile.NetworkInterfaces | 
                    ForEach-Object {          
                        $currentNic = Get-AzureRmResource -ResourceId $_.Id -ErrorAction SilentlyContinue
                        if($currentNic)
                        {
                            $nicResource = Get-AzureRmNetworkInterface -Name $currentNic.ResourceName `
                                                -ResourceGroupName $currentNic.ResourceGroupName `
                                                -ExpandResource NetworkSecurityGroup `
                                                -ErrorAction SilentlyContinue
                            if($nicResource)
                            {
                                $this.VMNICs += $nicResource;
                            }
                        }
                    }
            }            
        }
        return $this.VMNICs;
    }
    
    hidden [bool] IsLinuxVM()
    {
        return ($this.ResourceObject.OSProfile -and $this.ResourceObject.OSProfile.LinuxConfiguration);
    }
    hidden [ControlResult] CheckOSVersion([ControlResult] $controlResult)
    {
        $vmOSDetails = $this.ResourceObject.StorageProfile.ImageReference

        if($this.ResourceObject.StorageProfile -and $this.ResourceObject.StorageProfile.ImageReference)
        {
            $message = "";
            $verificationResult  = [VerificationResult]::Failed;

            if($vmOSDetails.Version -eq "latest")
            {
                $verificationResult  = [VerificationResult]::Passed
                $message = "Virtual Machine is running latest OS version"
            }
            else
            {
                $message = "Virtual Machine is not running latest OS version. Please upgrade the OS in order to comply."
            }
                
            $controlResult.AddMessage($verificationResult, $message, $vmOSDetails);    
        }
        else
        {
            $controlResult.AddMessage([MessageData]::new("We are not able to fetch the required data for the resource", [MessageType]::Error)); 
        }
        
        return $controlResult;
    }

    hidden [ControlResult] CheckOSAutoUpdateStatus([ControlResult] $controlResult)
    {        
        #TCP is not applicable for Linux.
        if($this.ResourceObject.OSProfile -and $this.ResourceObject.OSProfile.WindowsConfiguration)
        {
            $message = "";
            $verificationResult = [VerificationResult]::Failed;

            if($this.ResourceObject.OSProfile.WindowsConfiguration.EnableAutomaticUpdates -eq $true)
            {
                $verificationResult = [VerificationResult]::Passed;
                $message = "Automatic OS updates are enabled on Windows Virtual Machine";
            }
            else
            {
                $message = "Automatic OS updates are disabled on Windows Virtual Machine. Please enable OS automatic updates in order to comply.";
            }

            $controlResult.AddMessage($verificationResult, $message, $this.ResourceObject.OSProfile.WindowsConfiguration);    
        
        }
        elseif($this.IsLinuxVM())
        {
            $controlResult.AddMessage([VerificationResult]::Manual, "The control is not applicable on Linux Virtual Machine. It's good pratice to periodically update OS of Virtual Machine.", $this.ResourceObject.OSProfile.LinuxConfiguration); 
        }
        else
        {
            $controlResult.AddMessage([MessageData]::new("We are not able to fetch the required data for the resource", [MessageType]::Error)); 
        }
            
        return $controlResult;
    }

    hidden [ControlResult] CheckAntimalwareStatus([ControlResult] $controlResult)
    {
        #TCP is not applicable for Linux.
        if($this.ResourceObject.Extensions)
        {
            $antimalwareExt = $this.ResourceObject.Extensions | Where-Object { $_.VirtualMachineExtensionType -eq 'IaaSAntimalware'} | Select-Object -First 1
            if($antimalwareExt -and $antimalwareExt.Settings)
            {
                #create Antimalware data object
                $antimalwareData = @{
                    AntimalwareEnabled = $antimalwareExt.Settings.AntimalwareEnabled.Value;
                    RealtimeProtectionEnabled = $antimalwareExt.Settings.RealtimeProtectionEnabled.Value;
                    ScheduledScanSettings = @{
                        IsEnabled = $antimalwareExt.Settings.ScheduledScanSettings.isEnabled.Value
                        ScanType = $antimalwareExt.Settings.ScheduledScanSettings.scanType.Value
                        Day = $antimalwareExt.Settings.ScheduledScanSettings.day.Value
                        Time = $antimalwareExt.Settings.ScheduledScanSettings.time.Value
                    };
                };

                #$Exclusions = $antimalwareExt.Settings | where{ $_.Path -eq 'Exclusions'}
                #-and $scheduleScanSettings.isEnabled
                $message = "";
                $verificationResult = [VerificationResult]::Failed;

                if(($antimalwareData.AntimalwareEnabled -eq $true) -and 
                    ($antimalwareData.RealtimeProtectionEnabled -eq $true)-and 
                    ($antimalwareData.ScheduledScanSettings.IsEnabled -eq $true))
                {
                    $verificationResult = [VerificationResult]::Passed;
                    $message = "Antimalware extension is installed and is configured correctly"; 
                }
                else
                {
                    $verificationResult = [VerificationResult]::Failed
                    $message = "Antimalware extension is installed but is not configured correctly"; 
                }    

                $controlResult.AddMessage($verificationResult, $message, $antimalwareData);    
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Failed, "Antimalware extension is not installed");
            }
        }
        else
        {
            $controlResult.AddMessage([MessageData]::new("We are not able to fetch the required data for the resource", [MessageType]::Error)); 
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckNSGConfig([ControlResult] $controlResult)
    {
        $controlResult.VerificationResult = [VerificationResult]::Failed;
        $this.GetVMNICObjects() |
            ForEach-Object {          
                #Check NSGs applied at NIC level
                if($_.NetworkSecurityGroup)
                {
                    if($_.NetworkSecurityGroup.SecurityRules.Count -gt 0)
                    {
                        $controlResult.AddMessage("Validate NSG security rules applied to NIC - [$($_.Name)], Total - $($_.NetworkSecurityGroup.SecurityRules.Count)", $_.NetworkSecurityGroup.SecurityRules);              
                    }
                    if($_.NetworkSecurityGroup.DefaultSecurityRules.Count -gt 0)
                    {
                        $controlResult.AddMessage("Validate NSG default security rules applied to NIC - [$($_.Name)], Total - $($_.NetworkSecurityGroup.DefaultSecurityRules.Count)", $_.NetworkSecurityGroup.DefaultSecurityRules); 
                    }
                    $controlResult.VerificationResult = [VerificationResult]::Verify;
                }  
            
                #check NSGs applied at subnet level
                if($_.IpConfigurations)
                {
                    $_.IpConfigurations | 
                        ForEach-Object {                   
                            $subnetId = $_.Subnet.Id;        
                            $subnetName = $subnetId.Substring($subnetId.LastIndexOf("/") + 1);
                            #vnet id = trim '/subnets/' from subnet id
                            $vnetResource = Get-AzureRmResource -ResourceId $subnetId.Substring(0, $subnetId.IndexOf("/subnets/"))
                            if($vnetResource)
                            {
                                $vnetObject = Get-AzureRmVirtualNetwork -Name $vnetResource.Name -ResourceGroupName $vnetResource.ResourceGroupName
                                if($vnetObject)
                                {
                                    $subnetConfig = Get-AzureRmVirtualNetworkSubnetConfig -Name $subnetName -VirtualNetwork $vnetObject
                                    if($subnetConfig -and $subnetConfig.NetworkSecurityGroup -and $subnetConfig.NetworkSecurityGroup.Id)
                                    {
                                        $nsgResource = Get-AzureRmResource -ResourceId $subnetConfig.NetworkSecurityGroup.Id
                                        if($nsgResource)
                                        {
                                            $nsgObject = Get-AzureRmNetworkSecurityGroup -Name $nsgResource.Name -ResourceGroupName $nsgResource.ResourceGroupName
                                            if($nsgObject)
                                            {
                                                if($nsgObject.SecurityRules.Count -gt 0)
                                                {
                                                    $controlResult.AddMessage("Validate NSG security rules applied to Subnet - [$subnetName] in Virtual Network - [$($vnetResource.Name)]. Total - $($nsgObject.SecurityRules.Count)", $nsgObject.SecurityRules);                                       
                                                }
                                                
                                                if($nsgObject.DefaultSecurityRules.Count -gt 0)
                                                {
                                                    $controlResult.AddMessage("Validate NSG default security rules applied to Subnet - [$subnetName] in Virtual Network - [$($vnetResource.Name)]. Total - $($nsgObject.DefaultSecurityRules.Count)", $nsgObject.DefaultSecurityRules);
                                                }
                                                $controlResult.VerificationResult = [VerificationResult]::Verify;
                                            }
                                        }
                                    }
                                }
                            }
                        }           
                }            
            }

        if($controlResult.VerificationResult -ne [VerificationResult]::Verify)
        {
            $controlResult.AddMessage("No NSG is configured on Virtual Machine")                                                   
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckPublicIP([ControlResult] $controlResult)
    {    
        $publicIps = @();
        $this.GetVMNICObjects() | 
            ForEach-Object {
                $_.IpConfigurations | Where-Object { $_.PublicIpAddress } |
                    ForEach-Object {
                        $ipResource = Get-AzureRmResource -ResourceId $_.PublicIpAddress.Id 
                        if($ipResource)
                        {
                            $publicIpObject = Get-AzureRmPublicIpAddress -Name $ipResource.Name -ResourceGroupName $ipResource.ResourceGroupName
                            if($publicIpObject)
                            {
                                $_.PublicIpAddress = $publicIpObject;
                                $publicIps += $publicIpObject;
                            }
                        }
                    }
            }
         
        if($publicIps.Count -gt 0)
        {              
            $controlResult.AddMessage([VerificationResult]::Verify, "Validate Public IP(s) associated with Virtual Machine. Total - $($publicIps.Count)", $publicIps);  
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, "No Public IP is associated with Virtual Machine");
        }
        return $controlResult;
    }

    hidden [ControlResult] CheckDiskEncryption([ControlResult] $controlResult)
    {    
        #TCP is not applicable for Linux.
        if($this.ResourceObject.OSProfile -and $this.ResourceObject.OSProfile.WindowsConfiguration)
        {
            $diskEncryptionStatus = Get-AzureRmVMDiskEncryptionStatus -ResourceGroupName $this.ResourceContext.ResourceGroupName -VMName $this.ResourceContext.ResourceName
            $message = "";
            $verificationResult  = [VerificationResult]::Failed;

            #Need to convert the string values to Enum [Microsoft.Azure.Commands.Compute.Models.EncryptionStatus]
            #Enum type is not resolving here
            if(($diskEncryptionStatus.OsVolumeEncrypted -eq "NotEncrypted") -or ($diskEncryptionStatus.DataVolumesEncrypted -eq "NotEncrypted"))
            {
                $message = "All Virtual Machine disks (OS and Data disks) are not encrypted";
            }
            else
            {
                $verificationResult  = [VerificationResult]::Passed;
                $message = "All Virtual Machine disks (OS and Data disks) are encrypted";
            }

            $controlResult.AddMessage($verificationResult, $message, $diskEncryptionStatus);
        }
        elseif($this.IsLinuxVM())
        {
            $controlResult.AddMessage([VerificationResult]::Manual, "The control is not applicable on Linux Virtual Machine."); 
        }
        else
        {
            $controlResult.AddMessage([MessageData]::new("We are not able to fetch the required data for the resource", [MessageType]::Error)); 
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckASCStatus([ControlResult] $controlResult)
    {
        $isManual = $false;
        $result = $null 

        $uri = [System.String]::Format("{0}subscriptions/{1}/providers/microsoft.Security/securityStatuses?api-version=2015-06-01-preview", [WebRequestHelper]::AzureManagementUri, $this.SubscriptionContext.SubscriptionId)
                
        try 
        {     
            $result = [WebRequestHelper]::InvokeGetWebRequest($uri) 
        } 
        catch
        { 
            $isManual = $true;
        }       
        
        if(($result | Measure-Object).Count -gt 0)
        {
            $vmSecurityState = $result | Where-Object { $_.name -eq $this.ResourceContext.ResourceName -and $_.properties.type -eq 'VirtualMachine' } | 
                                            Select-Object -First 1;
            if($vmSecurityState) 
            {        
                $vmSecurityStateProperties = @{
                    SecurityState = $vmSecurityState.properties.securityState;
                    DetailedStatus = @{
                        Monitered = $vmSecurityState.properties.baselineScannerData.securityState;
                        SystemUpdates = $vmSecurityState.properties.patchScannerData.securityState;
                        EndpointProtection = $vmSecurityState.properties.antimalwareScannerData.securityState;
                        Vulnerabilities = $vmSecurityState.properties.vulnerabilityAssessmentScannerStatus.securityState;
                        DiskEncryption = $vmSecurityState.properties.encryptionDataState.securityState;
                    };
                };
                        
                if($vmSecurityState.properties.securityState -ne 'Healthy')
                {
                    $controlResult.VerificationResult = [VerificationResult]::Failed
                }
                else
                {
                    $controlResult.VerificationResult = [VerificationResult]::Passed
                }

                $controlResult.AddMessage("Security Center status for Virtual Machine [$($this.ResourceContext.ResourceName)] is: [$($vmSecurityState.properties.securityState)]", $vmSecurityStateProperties);
            }
            else
            {            
               $isManual = $true;
            }
        }
        else
        {            
            $isManual = $true;
        }

        if($isManual)
           {
            $controlResult.AddMessage([VerificationResult]::Manual, "We are not able to check Security Center status right now. Please validate manually.");
        }

        return $controlResult;
    }
    
    hidden [ControlResult] CheckVMDiagnostics([ControlResult] $controlResult)
    {        
        if($this.ResourceObject.Extensions)
        {
            $diagnosticExtensionType = if($this.IsLinuxVM()) { "LinuxDiagnostic" } else { "IaaSDiagnostics" }
            
            $diagExtension = $this.ResourceObject.Extensions | Where-Object { $_.VirtualMachineExtensionType -eq $diagnosticExtensionType } | Select-Object -First 1
            if($diagExtension)
            {
                $controlResult.AddMessage([VerificationResult]::Passed, "'$diagnosticExtensionType' extension is installed on Virtual Machine");
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Failed, "'$diagnosticExtensionType' extension is not installed on Virtual Machine");
            }
        }
        else
        {
            $controlResult.AddMessage([MessageData]::new("We are not able to fetch the required data for the resource", [MessageType]::Error)); 
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckOpenPorts([ControlResult] $controlResult)
    {    
        $isManual = $false
        $controlResult.AddMessage("Checking for Virtual Machine management ports RDP - $($this.ControlSettings.VirtualMachine.RDP_Port) and WinRM - $($this.ControlSettings.VirtualMachine.WinRM_Port)");
        $vulnerableNSGsWithRules = @();
        $this.GetVMNICObjects() | 
            ForEach-Object {
                $effectiveNSG = $null;
                try
                {
                    $effectiveNSG = Get-AzureRmEffectiveNetworkSecurityGroup -NetworkInterfaceName $_.Name -ResourceGroupName $_.ResourceGroupName -WarningAction SilentlyContinue -ErrorAction Stop
                }
                catch
                {
                    $isManual = $true    
                    $statusCode = ($_.Exception).InnerException.Response.StatusCode;
                    if($statusCode -eq [System.Net.HttpStatusCode]::BadRequest -or $statusCode -eq [System.Net.HttpStatusCode]::Forbidden)
                    {
                        $controlResult.AddMessage(($_.Exception).InnerException.Message);    
                    }
                    else
                    {
                        throw $_
                    }
                }

                if($effectiveNSG)
                {
                    $vulnerableRules = @();
                    $vulnerableRules += $effectiveNSG.EffectiveSecurityRules | 
                                            Where-Object { ($_.direction -eq "Inbound") -and
                                                            ($_.access -eq "Allow") -and 
                                                            ($_.destinationPortRange -like "*$($this.ControlSettings.VirtualMachine.RDP_Port)*" -or 
                                                                $_.destinationPortRange -like "*$($this.ControlSettings.VirtualMachine.WinRM_Port)*")}                                
                    if($vulnerableRules.Count -ne 0)
                    {
                        $vulnerableNSGsWithRules += @{
                            Association = $effectiveNSG.Association;
                            NetworkSecurityGroup = $effectiveNSG.NetworkSecurityGroup;
                            VulnerableRules = $vulnerableRules;
                            NicName = $_.Name
                        };
                    }
                }                
            }
        
        if($isManual)
        {
            $controlResult.AddMessage([VerificationResult]::Manual, "We are not able to check the NSG rules for some NICs. Please validate manually.");  
            if($vulnerableNSGsWithRules.Count -ne 0)
            {
                $controlResult.AddMessage([VerificationResult]::Manual, "Management ports are open on Virtual Machine. Please verify and remove the NSG rules in order to comply.", $vulnerableNSGsWithRules);
            }
        }
        else
        {
            if($vulnerableNSGsWithRules.Count -eq 0)
            {              
                $controlResult.AddMessage([VerificationResult]::Passed, "No management ports are open on Virtual Machine");  
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Failed, "Management ports are open on Virtual Machine. Please verify and remove the NSG rules in order to comply.", $vulnerableNSGsWithRules);
            }
        }
        
        return $controlResult;
    } 
}