Framework/Abstracts/AzSVTBase.ps1

class AzSVTBase: SVTBase{

    AzSVTBase()
    {

    }

    AzSVTBase([string] $subscriptionId):
        Base($subscriptionId)
    {
        $this.CreateInstance();
    }
    AzSVTBase([string] $subscriptionId, [SVTResource] $svtResource):
    Base($subscriptionId)
    {        
        $this.CreateInstance($svtResource);
    }
     #Create instance for subscription scan
     hidden [void] CreateInstance()
     {
         [Helpers]::AbstractClass($this, [SVTBase]);
 
         $this.LoadSvtConfig([SVTMapping]::SubscriptionMapping.JsonFileName);
         $this.ResourceId = $this.SubscriptionContext.Scope;    
     }
   
    #Add PreviewBaselineControls
    hidden [bool] CheckBaselineControl($controlId)
    {
        if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"BaselineControls.ResourceTypeControlIdMappingList"))
        {
          $baselineControl = $this.ControlSettings.BaselineControls.ResourceTypeControlIdMappingList | Where-Object {$_.ControlIds -contains $controlId}
           if(($baselineControl | Measure-Object).Count -gt 0 )
            {
                return $true
            }
        }

        if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"BaselineControls.SubscriptionControlIdList"))
        {
          $baselineControl = $this.ControlSettings.BaselineControls.SubscriptionControlIdList | Where-Object {$_ -eq $controlId}
           if(($baselineControl | Measure-Object).Count -gt 0 )
            {
                return $true
            }
        }
        return $false
    }
    hidden [bool] CheckPreviewBaselineControl($controlId)
    {
        if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"PreviewBaselineControls.ResourceTypeControlIdMappingList"))
        {
          $PreviewBaselineControls = $this.ControlSettings.PreviewBaselineControls.ResourceTypeControlIdMappingList | Where-Object {$_.ControlIds -contains $controlId}
           if(($PreviewBaselineControls | Measure-Object).Count -gt 0 )
            {
                return $true
            }
        }

        if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"PreviewBaselineControls.SubscriptionControlIdList"))
        {
          $PreviewBaselineControls = $this.ControlSettings.PreviewBaselineControls.SubscriptionControlIdList | Where-Object {$_ -eq $controlId}
           if(($PreviewBaselineControls | Measure-Object).Count -gt 0 )
            {
                return $true
            }
        }
        return $false
    }

    hidden [void] GetResourceId()
    {

        try {
            if ([FeatureFlightingManager]::GetFeatureStatus("EnableResourceGroupTagTelemetry","*") -eq $true -and $this.ResourceId -and $this.ResourceContext -and $this.ResourceTags.Count -eq 0) {
                
                    $tags = (Get-AzResourceGroup -Name $this.ResourceContext.ResourceGroupName).Tags
                    if( $tags -and ($tags | Measure-Object).Count -gt 0)
                    {
                        $this.ResourceTags = $tags
                    }            
            }   
        } catch {
            # flow shouldn't break if there are errors in fetching tags eg. locked resource groups. <TODO: Add exception telemetry>
        }
    }

    hidden [ControlResult] CheckPolicyCompliance([ControlItem] $controlItem, [ControlResult] $controlResult)
    {
        $initiativeName = [ConfigurationManager]::GetAzSKConfigData().AzSKInitiativeName
        $defnResourceId = $this.ResourceId + $controlItem.PolicyDefnResourceIdSuffix
        $policyState = Get-AzPolicyState -ResourceId $defnResourceId -Filter "PolicyDefinitionId eq '/providers/microsoft.authorization/policydefinitions/$($controlItem.PolicyDefinitionGuid)' and PolicySetDefinitionName eq '$initiativeName'"
        if($policyState)
        {
            $policyStateObject = $policyState | Select-Object ResourceId, PolicyAssignmentId, PolicyDefinitionId, PolicyAssignmentScope, PolicyDefinitionAction, PolicySetDefinitionName, IsCompliant
            if($policyState.IsCompliant)
            {
                $controlResult.AddMessage([VerificationResult]::Passed,
                                            [MessageData]::new("Policy compliance data:", $policyStateObject));
            }
            else
            { 
                #$controlResult.EnableFixControl = $true;
                $controlResult.AddMessage([VerificationResult]::Failed,
                                            [MessageData]::new("Policy compliance data:", $policyStateObject));
            }
            return $controlResult;
        }
        return $null;
    }
    # Policy compliance methods end
    hidden [ControlResult] CheckDiagnosticsSettings([ControlResult] $controlResult)
    {
        $diagnostics = $Null
        try
        {
            $diagnostics = Get-AzDiagnosticSetting -ResourceId $this.ResourceId -ErrorAction Stop -WarningAction SilentlyContinue
        }
        catch
        {
            if([Helpers]::CheckMember($_.Exception, "Response") -and ($_.Exception).Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound)
            {
                $controlResult.AddMessage([VerificationResult]::Failed, "Diagnostics setting is disabled for resource - [$($this.ResourceContext.ResourceName)].");
                return $controlResult
            }
            else
            {
                $this.PublishException($_);
            }
        }
        if($Null -ne $diagnostics -and ($diagnostics.Logs | Measure-Object).Count -ne 0)
        {
            $nonCompliantLogs = $diagnostics.Logs |
                                Where-Object { -not ($_.Enabled -and
                                            ($_.RetentionPolicy.Days -eq $this.ControlSettings.Diagnostics_RetentionPeriod_Forever -or
                                            $_.RetentionPolicy.Days -ge $this.ControlSettings.Diagnostics_RetentionPeriod_Min))};

            $selectedDiagnosticsProps = $diagnostics | Select-Object -Property Logs, Metrics, StorageAccountId, EventHubName, Name;

            if(($nonCompliantLogs | Measure-Object).Count -eq 0)
            {
                $controlResult.AddMessage([VerificationResult]::Passed,
                    "Diagnostics settings are correctly configured for resource - [$($this.ResourceContext.ResourceName)]",
                    $selectedDiagnosticsProps);
            }
            else
            {
                $failStateDiagnostics = $nonCompliantLogs | Select-Object -Property Logs, Metrics, StorageAccountId, EventHubName, Name;
                $controlResult.SetStateData("Non compliant resources are:", $failStateDiagnostics);
                $controlResult.AddMessage([VerificationResult]::Failed,
                    "Diagnostics settings are either disabled OR not retaining logs for at least $($this.ControlSettings.Diagnostics_RetentionPeriod_Min) days for resource - [$($this.ResourceContext.ResourceName)]",
                    $selectedDiagnosticsProps);
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Failed, "Diagnostics setting is disabled for resource - [$($this.ResourceContext.ResourceName)].");
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult)
    {
        $accessList = [RoleAssignmentHelper]::GetAzSKRoleAssignmentByScope($this.ResourceId, $false, $true);
        return $this.CheckRBACAccess($controlResult, $accessList)
    }

    hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult, [PSObject] $accessList)
    {
        $resourceAccessList = $accessList | Where-Object { $_.Scope -eq $this.ResourceId };

        $controlResult.VerificationResult = [VerificationResult]::Verify;

        if(($resourceAccessList | Measure-Object).Count -ne 0)
        {
            $controlResult.SetStateData("Identities having RBAC access at resource level", ($resourceAccessList | Select-Object -Property ObjectId,RoleDefinitionId,RoleDefinitionName,Scope));

            $controlResult.AddMessage("Validate that the following identities have explicitly provided with RBAC access to resource - [$($this.ResourceContext.ResourceName)]");
            $controlResult.AddMessage([MessageData]::new($this.CreateRBACCountMessage($resourceAccessList), $resourceAccessList));
        }
        else
        {
            $controlResult.AddMessage("No identities have been explicitly provided with RBAC access to resource - [$($this.ResourceContext.ResourceName)]");
        }

        $inheritedAccessList = $accessList | Where-Object { $_.Scope -ne $this.ResourceId };

        if(($inheritedAccessList | Measure-Object).Count -ne 0)
        {
            $controlResult.AddMessage("Note: " + $this.CreateRBACCountMessage($inheritedAccessList) + " have inherited RBAC access to resource. It's good practice to keep the RBAC access to minimum.");
        }
        else
        {
            $controlResult.AddMessage("No identities have inherited RBAC access to resource");
        }

        return $controlResult;
    }

    hidden [string] CreateRBACCountMessage([array] $resourceAccessList)
    {
        $nonNullObjectTypes = $resourceAccessList | Where-Object { -not [string]::IsNullOrEmpty($_.ObjectType) };
        if(($nonNullObjectTypes | Measure-Object).Count -eq 0)
        {
            return "$($resourceAccessList.Count) identities";
        }
        else
        {
            $countBreakupString = [string]::Join(", ",
                                    ($nonNullObjectTypes |
                                        Group-Object -Property ObjectType -NoElement |
                                        ForEach-Object { "$($_.Name): $($_.Count)" }
                                    ));
            return "$($resourceAccessList.Count) identities ($countBreakupString)";
        }
    }

    hidden [bool] CheckMetricAlertConfiguration([PSObject[]] $metricSettings, [ControlResult] $controlResult, [string] $extendedResourceName)
    {
        $result = $false;
        if($metricSettings -and $metricSettings.Count -ne 0)
        {
            $resId = $this.ResourceId + $extendedResourceName;
            $resIdMessageString = "";
            if(-not [string]::IsNullOrWhiteSpace($extendedResourceName))
            {
                $resIdMessageString = "for nested resource [$extendedResourceName]";
            }

            $resourceAlerts = @()
            # get classic alerts
            $resourceAlerts += (Get-AzAlertRule -ResourceGroup $this.ResourceContext.ResourceGroupName -WarningAction SilentlyContinue) |
                                Where-Object { $_.Condition -and $_.Condition.DataSource } |
                                Where-Object { $_.Condition.DataSource.ResourceUri -eq $resId };

            # get non-classic alerts
            try
            {
                $apiURL = "https://management.azure.com/subscriptions/{0}/providers/Microsoft.Insights/metricAlerts?api-version=2018-03-01&`$filter=targetResource eq '{1}'" -f $($this.SubscriptionContext.SubscriptionId), $resId
                $v2Alerts = [WebRequestHelper]::InvokeGetWebRequest($apiURL) 
                if(($v2Alerts | Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($v2Alerts[0],"id"))
                {
                    $v2Alerts |  ForEach-Object {
                        if([Helpers]::CheckMember($_,"properties"))
                        {
    
                            $alert = '{
                                  "Condition": {
                                                    "DataSource": {
                                                                       "MetricName": ""
                                                                   },
                                                    "OperatorProperty": "",
                                                    "Threshold": "" ,
                                                    "TimeAggregation": "",
                                                    "WindowSize": ""
                                                },
                                  "Actions" : null,
                                  "Description" : "",
                                  "IsEnabled": "",
                                  "Name" : "",
                                  "Type" : "",
                                  "AlertType" : "V2Alert"
                            }'
 | ConvertFrom-Json
                            if([Helpers]::CheckMember($_,"properties.criteria.allOf"))
                            {
                                $alert.Condition.DataSource.MetricName = $_.properties.criteria.allOf.metricName
                                $alert.Condition.OperatorProperty = $_.properties.criteria.allOf.operator
                                $alert.Condition.Threshold = [int] $_.properties.criteria.allOf.threshold
                                $alert.Condition.TimeAggregation = $_.properties.criteria.allOf.timeAggregation
                            }
                            $alert.Condition.WindowSize = ([Xml.XmlConvert]::ToTimeSpan("$($_.properties.windowSize)")).ToString()
                            $alert.Actions = [System.Collections.Generic.List[Microsoft.Azure.Management.Monitor.Models.RuleAction]]::new()
                            if([Helpers]::CheckMember($_.properties,"Actions.actionGroupId"))
                            {
                                $actionGroupTemp = $_.properties.Actions.actionGroupId.Split("/")
                                $actionGroup = Get-AzActionGroup -ResourceGroupName $actionGroupTemp[4] -Name $actionGroupTemp[-1] -WarningAction SilentlyContinue
                                if($actionGroup.EmailReceivers.Status -eq [Microsoft.Azure.Management.Monitor.Models.ReceiverStatus]::Enabled)
                                {
                                    if([Helpers]::CheckMember($actionGroup,"EmailReceivers.EmailAddress"))
                                    {
                                        $alert.Actions.Add($(New-AzAlertRuleEmail -SendToServiceOwner -CustomEmail $actionGroup.EmailReceivers.EmailAddress  -WarningAction SilentlyContinue));
                                    }
                                    else
                                    {
                                        $alert.Actions.Add($(New-AzAlertRuleEmail -SendToServiceOwner -WarningAction SilentlyContinue));
                                    }    
                                }
                            }                
                            $alert.Description = $_.properties.description
                            $alert.IsEnabled = $_.properties.enabled
                            $alert.Name = $_.name
                            $alert.Type = $_.type
                            if(($alert|Measure-Object).Count -gt 0)
                            {
                               $resourceAlerts += $alert 
                            }
                        }
                    }
                }   
            }
            catch
            {
                $this.PublishException($_);
            }

            $nonConfiguredMetrices = @();
            $misConfiguredMetrices = @();

            $metricSettings    |
            ForEach-Object {
                $currentMetric = $_;
                $matchedMetrices = @();
                $matchedMetrices += $resourceAlerts |
                                    Where-Object { $_.Condition.DataSource.MetricName -eq $currentMetric.Condition.DataSource.MetricName }

                if($matchedMetrices.Count -eq 0)
                {
                    $nonConfiguredMetrices += $currentMetric;
                }
                else
                {
                    $misConfigured = @();
                    #$controlResult.AddMessage("Metric object", $matchedMetrices);
                    $matchedMetrices | ForEach-Object {
                        if([Helpers]::CompareObject($currentMetric, $_))
                        {
                            #$this.ControlSettings.MetricAlert.Actions
                            if(($_.Actions.GetType().GetMembers() | Where-Object { $_.MemberType -eq [System.Reflection.MemberTypes]::Property -and $_.Name -eq "Count" } | Measure-Object).Count -ne 0)
                            {
                                $isActionConfigured = $false;
                                foreach ($action in $_.Actions) {
                                    if([Helpers]::CompareObject($this.ControlSettings.MetricAlert.Actions, $action))
                                    {
                                        $isActionConfigured = $true;
                                        break;
                                    }
                                }

                                if(-not $isActionConfigured)
                                {
                                    $misConfigured += $_;
                                }
                            }
                            else
                            {
                                if(-not [Helpers]::CompareObject($this.ControlSettings.MetricAlert.Actions, $_.Actions))
                                {
                                    $misConfigured += $_;
                                }
                            }
                        }
                        else
                        {
                            $misConfigured += $_;
                        }
                    };

                    if($misConfigured.Count -eq $matchedMetrices.Count)
                    {
                        $misConfiguredMetrices += $misConfigured;
                    }
                }
            }

            $controlResult.AddMessage("Following metric alerts must be configured $resIdMessageString with settings mentioned below:", $metricSettings);
            $controlResult.VerificationResult = [VerificationResult]::Failed;

            if($nonConfiguredMetrices.Count -ne 0)
            {
                $controlResult.AddMessage("Following metric alerts are not configured $($resIdMessageString):", $nonConfiguredMetrices);
            }

            if($misConfiguredMetrices.Count -ne 0)
            {
                $controlResult.AddMessage("Following metric alerts are not correctly configured $resIdMessageString. Please update the metric settings in order to comply.", $misConfiguredMetrices);
            }

            if($nonConfiguredMetrices.Count -eq 0 -and $misConfiguredMetrices.Count -eq 0)
            {
                $result = $true;
                $controlResult.AddMessage([VerificationResult]::Passed , "All mandatory metric alerts are correctly configured $resIdMessageString.");
            }
        }
        else
        {
            throw [System.ArgumentException] ("The argument 'metricSettings' is null or empty");
        }

        return $result;
    }
    
    hidden [void] GetDataFromSubscriptionReport($singleControlResult)
    {   
    try
     {
         $azskConfig = [ConfigurationManager]::GetAzSKConfigData();    
         $settingStoreComplianceSummaryInUserSubscriptions = [ConfigurationManager]::GetAzSKSettings().StoreComplianceSummaryInUserSubscriptions;
         #return if feature is turned off at server config
         if(-not $azskConfig.StoreComplianceSummaryInUserSubscriptions -and -not $settingStoreComplianceSummaryInUserSubscriptions) {return;}

            if(($this.ComplianceStateData | Measure-Object).Count -gt 0)
         {
             $ResourceData = @();
             $PersistedControlScanResult=@();                                
         
             #$ResourceScanResult=$ResourceData.ResourceScanResult
             [ControlResult[]] $controlsResults = @();
             $singleControlResult.ControlResults | ForEach-Object {
                 $currentControl=$_
                 $partsToHash = $singleControlResult.ControlItem.Id;
                 if(-not [string]::IsNullOrWhiteSpace($currentControl.ChildResourceName))
                 {
                     $partsToHash = $partsToHash + ":" + $currentControl.ChildResourceName;
                 }
                 $rowKey = [Helpers]::ComputeHash($partsToHash.ToLower());

                 $matchedControlResult = $this.ComplianceStateData | Where-Object { $_.RowKey -eq $rowKey}

                 # initialize default values
                 $currentControl.FirstScannedOn = [DateTime]::UtcNow
                 if($currentControl.ActualVerificationResult -ne [VerificationResult]::Passed)
                 {
                     $currentControl.FirstFailedOn = [DateTime]::UtcNow
                 }
                 if($null -ne $matchedControlResult -and ($matchedControlResult | Measure-Object).Count -gt 0)
                 {
                     $currentControl.UserComments = $matchedControlResult.UserComments
                     $currentControl.FirstFailedOn = [datetime] $matchedControlResult.FirstFailedOn
                     $currentControl.FirstScannedOn = [datetime] $matchedControlResult.FirstScannedOn                        
                 }

                 $scanFromDays = [System.DateTime]::UtcNow.Subtract($currentControl.FirstScannedOn)

                 $currentControl.MaximumAllowedGraceDays = $this.CalculateGraceInDays($singleControlResult);

                 # Setting isControlInGrace Flag
                 if($scanFromDays.Days -le $currentControl.MaximumAllowedGraceDays)
                 {
                     $currentControl.IsControlInGrace = $true
                 }
                 else
                 {
                     $currentControl.IsControlInGrace = $false
                 }
                 
                 $controlsResults+=$currentControl
             }
             $singleControlResult.ControlResults=$controlsResults 
         }
     }
     catch
     {
       $this.PublishException($_);
     }
    }

}