Framework/Core/SVT/Services/ERvNet.ps1

#using namespace Microsoft.Azure.Commands.ExpressRouteVirtualNetwork.Models
Set-StrictMode -Version Latest
class ERvNet : SVTIaasBase
{
    ERvNet([string] $subscriptionId, [SVTResource] $svtResource):
        Base($subscriptionId, $svtResource)
    {
    }

    hidden [ControlResult] CheckPublicIps([ControlResult] $controlResult)
    {
        if($null -ne $this.vNetNicsOutput )
        {
            $controlResult.AddMessage([MessageData]::new("Analyzing all the NICs configured in the VNet"));
            $publicIpCount = (($this.vNetNicsOutput | Where-Object {!([System.String]::IsNullOrWhiteSpace($_.PublicIpAddress))}) | Measure-Object).count
            if($publicIpCount -gt 0)
            {
                $publicIPList = @()
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Below Public IP(s) on the ERVnet"));
                $this.vNetNicsOutput | ForEach-Object{
                    Set-Variable -Name nic -Scope Local -Value $_
                    $publicIP = $nic | Select-Object NICName, VMName, PrimaryStatus, NetworkSecurityGroupName, PublicIpAddress, PrivateIpAddress
                    $publicIPList += $publicIP
                    $controlResult.AddMessage([MessageData]::new($publicIP));
                }
                $controlResult.SetStateData("Public IP(s) on the ERVnet", $publicIPList);
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No Public IP is configured in any NIC on the ERVnet"));
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No NICs found on the ERVNet"));
        }
        if(($this.vNetNicsWIssues | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Not able to validate following NICs:", $this.vNetNicsWIssues));
        }
        if(($this.vNetPIPIssues | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Not able to validate following IPConfigurations:", $this.vNetPIPIssues));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckIPForwardingforNICs([ControlResult] $controlResult)
    {
        if($null -ne $this.vNetNicsOutput)
        {
            [array] $vNetNicsIPFwed = $this.vNetNicsOutput | Where-Object { $_.EnableIPForwarding }

            if($null -ne $vNetNicsIPFwed -and ($vNetNicsIPFwed | Measure-Object).count -gt 0)
            {
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("IP Forwarding is enabled for below NIC(s) in ERVNet"));
                $controlResult.AddMessage([MessageData]::new(($vNetNicsIPFwed | Select-Object NICName, EnableIPForwarding)));
                $controlResult.SetStateData("IP Forwarding is enabled for NIC(s) in ERVNet", $vNetNicsIPFwed);
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("There are no NICs with EnableIPForwarding turned on the ERVNet"));
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No NICs found on the ERVNet"));
        }

        if(($this.vNetNicsWIssues | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Not able to validate following NICs:", $this.vNetNicsWIssues));
        }
        if(($this.vNetPIPIssues | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Not able to validate following IPConfigurations:", $this.vNetPIPIssues));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckNSGUseonGatewaySubnet([ControlResult] $controlResult)
    {
        $gateWaySubnet = $this.ResourceObject.Subnets | Where-Object { $_.Name -eq "GatewaySubnet" }
        if($null -ne $gateWaySubnet)
        {
            if($null -ne $gateWaySubnet.NetworkSecurityGroup -and -not [System.String]::IsNullOrWhiteSpace($gateWaySubnet.NetworkSecurityGroup.Id))
            {
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("NSG is configured on the Gateway Subnet of ERVNet", ($gateWaySubnet | Select-Object Name, NetworkSecurityGroupText)));
                $controlResult.SetStateData("Gateway subnet of ERVNet", ($gateWaySubnet | Select-Object Name, NetworkSecurityGroup));
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("There are no NSG's configured on the Gateway subnet of ERVNet"));
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No Gateway subnet found on the ERVNet"));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckVnetPeering([ControlResult] $controlResult)
    {
        $whiteListedRGs = $this.ControlSettings.ERvNet.WhiteListedRGs
        $whiteListedRemoteVirtualNetworkId = $this.ControlSettings.ERvNet.WhiteListedRemoteVirtualNetworkId
        
        $vnetPeerings = Get-AzVirtualNetworkPeering -VirtualNetworkName $this.ResourceContext.ResourceName -ResourceGroupName $this.ResourceContext.ResourceGroupName
        if($null -ne $vnetPeerings -and ($vnetPeerings|Measure-Object).count -gt 0)
        {
            $filteredVnetPeerings = @()
            # Filter whitelisted vNet peerings, if resource is in whitelisted RG
            if((-not [string]::IsNullOrEmpty($whiteListedRemoteVirtualNetworkId)) -and (($whiteListedRGs | Measure-Object).Count -gt 0) -and ($whiteListedRGs -contains $this.ResourceContext.ResourceGroupName))
            {
                $filteredVnetPeerings += $vnetPeerings | Where-Object { $_.RemoteVirtualNetwork.id -notlike $whiteListedRemoteVirtualNetworkId }
            }else{
                # All vNet peering are non-compliant, if resource is not in whitelisted RG
                $filteredVnetPeerings = $vnetPeerings
            }

            # If there is any non-compliant vNet peering fail the control
            if($null -ne $filteredVnetPeerings -and ($filteredVnetPeerings|Measure-Object).count -gt 0)
            {
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Below peering found on ERVNet", $vnetPeerings));
                $controlResult.SetStateData("Peering found on ERVNet", $vnetPeerings);
            }else{
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No additional VNet peerings found on ERVNet", $vnetPeerings));
            }

        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No VNet peerings found on ERVNet", $vnetPeerings));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckMultiNICVMUsed([ControlResult] $controlResult)
    {
        $VMNics = @()
        if($null -ne $this.vNetNicsOutput)
        {
            $vNetNicsMultiVM = $this.vNetNicsOutput | Group-Object VMId | Where-Object {-not [System.String]::IsNullOrWhiteSpace($_.Name) -and $_.Count -gt 1}

            $hasTCPPassed = $true
            if($null -ne $vNetNicsMultiVM)
            {
                $vNetNicsMultiVM | ForEach-Object{
                    $NICGroup = @()
                    $NICGroup += $_.Group

                    if($null -ne $NICGroup)
                    {
                        $NICGroup | ForEach-Object{
                            Set-Variable -Name tempNIC -Value $_
                            if($null -ne $tempNIC.IpConfigurations )
                            {
                                $tempIpConfigurations = [array]($tempNIC.IpConfigurations)
                                $tempIpConfigurations | ForEach-Object{
                                    Set-Variable -Name tempIPConfig -Value $_
                                    if($null -ne $tempIPConfig.properties.Subnet)
                                    {
                                        if(-not $tempIPConfig.properties.Subnet.Id.StartsWith($this.ResourceObject.Id,"CurrentCultureIgnoreCase"))
                                        {
                                            $hasTCPPassed = $false
                                        }
                                    }
                                }
                            }
                        }
                        $VMNics += $NICGroup
                    }
                }
            }

            $controlResult.AddMessage([MessageData]::new(($this.vNetNicsOutput | Group-Object VMId | Where-Object {-not [System.String]::IsNullOrWhiteSpace($_.Name) } | Select-Object @{Name="[Count of NICs]";Expression= {$_.Count}}, @{Name="[VM ResourceID]";Expression= {$_.Name}})));
            if(-not $hasTCPPassed)
            {
                $controlResult.SetStateData("VM NIC details", $VMNics);
                $controlResult.VerificationResult = [VerificationResult]::Failed;
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("There are no VMs with more than one NIC"));
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No NICs found on the ERVNet"));
        }

        if(($this.vNetNicsWIssues | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Not able to validate following NICs:", $this.vNetNicsWIssues));
        }
        if(($this.vNetPIPIssues | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Not able to validate following IPConfigurations:", $this.vNetPIPIssues));
        }

        return $controlResult;
    }

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

        $whiteListedRGs = $this.ControlSettings.ERvNet.WhiteListedRGs
        $whiteListedaddressPrefix =  $this.ControlSettings.ERvNet.WhiteListedaddressPrefix
        $whiteListednextHopType =  $this.ControlSettings.ERvNet.WhiteListednextHopType
      
        $subnetsWithUDRs = $this.ResourceObject.Subnets | Where-Object {$null -ne $_.RouteTable -and -not [System.String]::IsNullOrWhiteSpace($_.RouteTable.Id)}

        if($null -ne $subnetsWithUDRs -and ($subnetsWithUDRs | Measure-Object).count -gt 0)
        {
            $nonCompliantSubnetsWithUDRs = @()
            # Filter whitelisted UDR's, if resource is in whitelisted RG
            if(($whiteListedRGs | Measure-Object).Count -gt 0 -and ($whiteListedRGs -contains $this.ResourceContext.ResourceGroupName)){
                $subnetsWithUDRs | Foreach-Object {
                    $IsUDRPermitted = $true
                    try{
                        $routeTableResourceId = $_.RouteTable.Id
                        $routeTable = Get-AzResource -ResourceId $routeTableResourceId -ErrorAction SilentlyContinue
                        if($null -ne  $routeTable -and ($whiteListedRGs -contains $routeTable.ResourceGroupName) -and [Helpers]::CheckMember($routeTable,"Properties.routes")){
                            $routes =  $routeTable.Properties.routes
                            $routes | ForEach-Object {
                                $addressPrefix =  $_.properties.addressPrefix
                                $nextHopType  = $_.properties.nextHopType
                                if(-not($addressPrefix -eq $whiteListedaddressPrefix -and $nextHopType -eq $whiteListednextHopType)){
                                    $IsUDRPermitted = $false
                                }
                            }
                        }else{
                            $IsUDRPermitted = $false
                        }
                    }catch{
                        $IsUDRPermitted = $false
                    }
                    if(-not $IsUDRPermitted){
                        $nonCompliantSubnetsWithUDRs += $_
                    }
                }
            }else{
                # All UDR's are non-compliant, if resource is not in whitelisted RG
                $nonCompliantSubnetsWithUDRs = $subnetsWithUDRs
            }

            # If there is any non-compliant UDR fail the control
            if($null -ne $nonCompliantSubnetsWithUDRs -and ($nonCompliantSubnetsWithUDRs | Measure-Object).count -gt 0){
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new(($subnetsWithUDRs | Select-Object Name, RouteTableText)));
                $controlResult.SetStateData("UDRs found on any Subnet of ERVNet", $subnetsWithUDRs);
            }else{
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No additional UDRs found on any Subnet of ERVNet"));
            }

        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No UDRs found on any Subnet of ERVNet"));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckGatewayUsed([ControlResult] $controlResult)
    {
        $nonERvNetGateways = @()
        $hasTCPPassed = $true
        $gateways = Get-AzVirtualNetworkGateway -ResourceGroupName $this.ResourceContext.ResourceGroupName
        $count = 0
        if(($null -ne $gateways) -and (($gateways | Measure-Object).count -gt 0))
        {
            $gateways | ForEach-Object{
                Set-Variable -Name gateway -Scope Local -Value $_

                if($null -ne $gateway.IpConfigurations)
                {
                    $tempIpConfigurations =  [array]($gateway.IpConfigurations)
                    $tempIpConfigurations | ForEach-Object{
                        Set-Variable -Name tempIpConfig -Value $_
                        if($tempIpConfig.Subnet.Id.StartsWith($this.ResourceObject.Id,"CurrentCultureIgnoreCase"))
                        {
                            if($gateway.GatewayType -ne "ExpressRoute")
                            {
                                $nonERvNetGateway = New-Object System.Object
                                $nonERvNetGateway | Add-Member -type NoteProperty -name ResourceName -Value $gateway.Name
                                $nonERvNetGateway | Add-Member -type NoteProperty -name ResourceGroupName -Value $gateway.ResourceGroupName
                                $nonERvNetGateway | Add-Member -type NoteProperty -name GatewayType -Value $gateway.GatewayType
                                $nonERvNetGateway | Add-Member -type NoteProperty -name VPNType -Value $gateway.VpnType

                                $nonERvNetGateways += $nonERvNetGateway

                                $hasTCPPassed = $false
                            }
                            $controlResult.AddMessage([MessageData]::new("GateWay Name: " + $gateway.Name + " GatewayType: " + $gateway.GatewayType));
                            $count++
                        }
                    }
                }
            }
        }

        if($count -eq 0)
        {
            $controlResult.AddMessage([MessageData]::new("No gateways found"));
        }

        if(-not $hasTCPPassed)
        {
            $controlResult.SetStateData("Non Express Route gateways in ERVNet", $nonERvNetGateways);
            $controlResult.VerificationResult = [VerificationResult]::Failed;
        }
        else
        {
            $controlResult.VerificationResult = [VerificationResult]::Passed;
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckInternalLoadBalancers([ControlResult] $controlResult)
    {
        $invalidlbList = @()
        $hasTCPPassed = $true
        $ilbs = Get-AzLoadBalancer
        $count = 0

        if($null -ne $ilbs -and ($ilbs|Measure-Object).count -gt 0)
        {
            $ilbs | ForEach-Object {
                Set-Variable -Name ilb -Value $_ -Scope Local
                if($null -ne $ilb -and $null -ne $ilb.FrontendIpConfigurations)
                {
                    $ilb.FrontendIpConfigurations |ForEach-Object{
                        Set-Variable -Name frontEndIpConfig -Scope Local -Value $_

                        if($null -ne $frontEndIpConfig.Subnet)
                        {
                            if($frontEndIpConfig.Subnet.Id.StartsWith($this.ResourceObject.Id,"CurrentCultureIgnoreCase"))
                            {
                                if($null -ne $frontEndIpConfig.PublicIpAddress)
                                {
                                    $subParts = $frontEndIpConfig.PublicIpAddress.Id.Split('/')
                                    $publicIpResourceName = $subParts[$subParts.Length-1]
                                    $pubResourceName = Get-AzPublicIpAddress -Name $publicIpResourceName -ResourceGroupName $this.ResourceContext.ResourceGroupName
                                    $hasTCPPassed = $false

                                    $invalidlb = New-Object System.Object
                                    $invalidlb | Add-Member -type NoteProperty -name Name -Value $ilbs.Name
                                    $invalidlb | Add-Member -type NoteProperty -name IpAddress -Value  $pubResourceName.IpAddress

                                    $invalidlbList += $invalidlb
                                    $controlResult.AddMessage([MessageData]::new("ILB Name: " + $ilbs.Name + " PublicIP: " + $pubResourceName.IpAddress));
                                }

                                $controlResult.AddMessage([MessageData]::new("No public Ips found on ILB: " + $ilbs.Name));
                                $count++
                            }
                        }
                    }
                }
            }
        }

        if($count -eq 0)
        {
            $controlResult.AddMessage([MessageData]::new("No ILB found"));
        }
        if(-not $hasTCPPassed)
        {
            $controlResult.SetStateData("Non internal LBs in ERVNet", $invalidlbList);
            $controlResult.VerificationResult = [VerificationResult]::Failed;
        }
        else
        {
            $controlResult.VerificationResult = [VerificationResult]::Passed;
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckOnlyNetworkResourceExist([ControlResult] $controlResult)
    {
        $resources = [array](Get-AzResource -ResourceGroupName $this.ResourceContext.ResourceGroupName)

        if($null -ne $resources)
        {
            $nonApprovedResources = [array]($resources | Where-Object { -not $_.ResourceType.StartsWith("Microsoft.Network","CurrentCultureIgnoreCase")})
            if($null -ne $nonApprovedResources )
            {
                $controlResult.SetStateData("Non approved resources in ERVNet ResourceGroup", $nonApprovedResources);
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Other resource types found apart from Microsoft.Network\*. Below are the Resource IDs and Resource Types available under the ResourceGroup - ["+ $this.ResourceContext.ResourceGroupName +"]",($nonApprovedResources | Select-Object ResourceType, ResourceID)));
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No other resource types found apart from Microsoft.Network\* . Below are the Resource ID available under the ResourceGroup - ["+ $this.ResourceContext.ResourceGroupName +"]"));
            }

            $controlResult.AddMessage([MessageData]::new("Resources configured under ResourceGroup - ["+ $this.ResourceContext.ResourceGroupName +"]",($resources | Select-Object ResourceType, ResourceID)));
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No other resources found under the ResourceGroup - ["+ $this.ResourceContext.ResourceGroupName +"]"));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckResourceLockConfigured([ControlResult] $controlResult)
    {
        $locks = [array](Get-AzResourceLock -ResourceGroupName $this.ResourceContext.ResourceGroupName -AtScope)

        if($null -eq $locks -or $locks.Length -le 0)
        {
            $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("No resource locks are configured at the ResourceGroup scope for - ["+ $this.ResourceContext.ResourceName +"]"));
        }
        else
        {
            if(($locks | Where-Object {$_.Properties.Level -eq $this.ControlSettings.ERvNet.ResourceLockLevel } | Measure-Object).Count -gt 0)
            {
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Found resource locks configured at the ResourceGroup scope for - ["+ $this.ResourceContext.ResourceName +"]", $locks));
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("No *$($this.ControlSettings.ERvNet.ResourceLockLevel)* resource locks are configured at the ResourceGroup scope for - ["+ $this.ResourceContext.ResourceName +"]"));
            }
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckARMPolicyConfigured([ControlResult] $controlResult)
    {
        $controlSettings = $this.LoadServerConfigFile("Subscription.ARMPolicies.json");
        $output = @()
        $missingPolicies = @()
        $subscriptionId = $this.SubscriptionContext.SubscriptionId 
        $resourceGroupName = $this.ResourceContext.ResourceGroupName
        if($null -ne $controlSettings -and [Helpers]::CheckMember($controlSettings,"Policies"))
        {
            $policies = $controlSettings.Policies
            $enabledPolicies = @()
            $sdoPolicies = @()
            #Filter to get only enabled and sdo tagged policy
            $enabledPolicies += $policies | Where-Object {( ($_.tags.Trim().ToLower().Contains("sdo")) -and ($_.enabled) )}
            #Filter to get policy applicable for current ErvNet RG
            if(($enabledPolicies | Measure-Object).Count -gt 0){
                $enabledPolicies | ForEach-Object {
                    $ErvNetRGPatterns = ((($_.applicableForRGs | ForEach-Object {'^' + [regex]::escape($_) + '$' }) -join '|') ) -replace '[\\]',''
                    if(($this.ResourceContext.ResourceGroupName.ToLower() -imatch $ErvNetRGPatterns)){
                        $sdoPolicies += $_
                    }
                }
            }

            if(($sdoPolicies | Measure-Object).Count -gt 0)
            {
                $configuredPolicies = Get-AzPolicyAssignment -IncludeDescendent
                $sdoPolicies | ForEach-Object{
                    Set-Variable -Name pol -Scope Local -Value $_
                    Set-Variable -Name policyDefinitionName -Scope Local -Value $_.policyDefinitionName
                    Set-Variable -Name tags -Scope Local -Value $_.tags
                    $policyScope =  ( $_.scope -replace "subscriptionId",$subscriptionId ) -replace "resourceGroupName" , $resourceGroupName
                      
                    $foundPolicies = [array]($configuredPolicies | Where-Object {$_.Name -like $policyDefinitionName -and $_.properties.scope -eq $policyScope})
                
                    if($null -ne $foundPolicies)
                    {
                        if($foundPolicies.Length -gt 0)
                        {
                            $output += $pol
                        }
                        else{
                            $missingPolicies += $pol
                        }
                    }
                    else{
                        $missingPolicies += $pol
                    }
                    
                }

            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Passed,[MessageData]::new("No mandatory ARM policies required to be configured on the subscription because of ERNetwork."));
            }
            
        }
        if(($missingPolicies | Measure-Object).Count -le 0)
        {
            $controlResult.VerificationResult = [VerificationResult]::Passed;
        }
        else
        {
            $missingPolicies = $missingPolicies | select-object "policyDefinitionName"
            $controlResult.SetStateData("Missing mandatory policies", $missingPolicies);
            $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Following mandatory policies are missing which are demanded by the control tags:",$missingPolicies));
        }
        return $controlResult;
    }
}