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 ($this.IsResourceScan)
        {
            #Resource Scan so check only for resource level base line controls
            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
                }
            }
        }
        else
        {
            #Subscription Scan so check only for Subscription level base line controls
            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 ($this.IsResourceScan)
        {
            #Resource Scan so check only for resource level preview base line controls
            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
                }
            }
        }
        else
        {
            #Subscription Scan so check only for Subscription level preview base line controls
            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 [bool] CheckForControlExclusion($controlId)
    {
        $TenantId = ([ContextHelper]::GetCurrentRMContext()).Tenant.Id
        if (($null -ne $this.ControlSettings) `
                                            -and [Helpers]::CheckMember($this.ControlSettings, "ControlsToExcludeFromScan.TenantIds") `
                                            -and ($this.ControlSettings.ControlsToExcludeFromScan.TenantIds -contains $TenantId) `
                                            -and [Helpers]::CheckMember($this.ControlSettings, "ControlsToExcludeFromScan.ControlIds"))
        {
            if($this.ControlSettings.ControlsToExcludeFromScan.ControlIds -contains $controlId){
                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>
        }
    }

    <# TODO: Remove this block if new logic for policy complaince is working seemlessly
         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;
         }
    #>


    hidden [void] CheckPolicyCompliance([ControlItem] $controlItem, [ControlResult] $controlResult)
 {
        try
        {
            $controlResult.PolicyState = [PolicyState]::new()
            $initiativeName = [ConfigurationManager]::GetAzSKConfigData().AzSKInitiativeName
            $securityCenterInitiativeName = [ConfigurationManager]::GetAzSKConfigData().AzSKSecurityCenterInitiativeName.Replace("{0}", $this.SubscriptionContext.SubscriptionId)
            $defnResourceId = $this.ResourceId + $controlItem.PolicyDefnResourceIdSuffix
            $policyState = Get-AzPolicyState -ResourceId $defnResourceId -Filter "((PolicyDefinitionId eq '/providers/microsoft.authorization/policydefinitions/$($controlItem.PolicyDefinitionGuid)') or (PolicyDefinitionId eq '/subscriptions/$($this.SubscriptionContext.SubscriptionId)/providers/microsoft.authorization/policydefinitions/$($controlItem.PolicyDefinitionGuid)')) and (PolicySetDefinitionName eq '$initiativeName' or PolicySetDefinitionName eq '$securityCenterInitiativeName')" -ErrorAction Stop
            if ($policyState)
            {
                $groupResultByComplianceState = $policyState | Group-Object -Property ComplianceState
                if (($groupResultByComplianceState | Measure-Object).Count -eq 1)
                {
                    # Select first when multiple assignment are found for the same definition at subscription scope
                    $policyState = $policyState | Select-Object -First 1
                    $policyStateObject = $policyState | Select-Object ResourceId, PolicyAssignmentId, PolicyAssignmentScope, PolicyDefinitionAction, IsCompliant
                    if ($policyState.IsCompliant)
                    {
                        $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Passed;
                        $controlResult.PolicyState.DataObject = $policyStateObject;
                    }
                    else
                    {
                        $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Failed;
                        $controlResult.PolicyState.DataObject = $policyStateObject;
                    }

                }
                else
                {
                    $policyStateObject = @()
                    $policyState | ForEach-Object {
                        $AssignmentDetails = "" | Select-Object PolicyAssignmentId, PolicyAssignmentScope, PolicyDefinitionAction, IsCompliant, Parameters
                        $assignmentdetails.PolicyAssignmentId = $_.PolicyAssignmentId
                        $assignmentdetails.PolicyAssignmentScope = $_.PolicyAssignmentScope
                        $assignmentdetails.PolicyDefinitionAction = $_.PolicyDefinitionAction
                        $assignmentdetails.IsCompliant = $_.IsCompliant

                        $assignment = Get-AzPolicyAssignment -Id $($_.PolicyAssignmentId) -ErrorAction SilentlyContinue
                        if ($assignment)
                        {
                            $assignmentdetails.Parameters = $assignment.parameters
                        }
                        
                        $policyStateObject += $assignmentdetails
                        
                    }

                    # Mark policy verification result as Verify if control is found to be both compliance and non-compliant
                    $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Verify
                    $controlResult.PolicyState.DataObject = $policyStateObject;
                    
                }
            }
            else
            {
                #Check if definition is created in portal for this respective control
                $definition = Get-AzPolicyDefinition -Name $($controlItem.PolicyDefinitionGuid) -ErrorAction Stop
                $assignment = $null

                # TODO: Move this to a common place, where it is called only once
                $initiative = @()
                # Get azsk policy initiative
                $azskpolicyinitiative = Get-AzPolicySetDefinition -Name $initiativeName -ErrorAction Stop
                if ($azskpolicyinitiative)
                {
                    $assignment = Get-AzPolicyAssignment -PolicyDefinitionId $($azskpolicyinitiative.PolicySetDefinitionId) -ErrorAction Stop
                    $initiative += $azskpolicyinitiative
                }
                # Get security center initiative
                $initiative += Get-AzPolicySetDefinition -Name $securityCenterInitiativeName -ErrorAction Stop

                # Definition is present, and compliance result not found
                if ($definition)
                {
                    # Definition is present, and is added to the initiative
                    if ( ($initiative | Measure-Object).Count -gt 0 `
                            -and [Helpers]::CheckMember($initiative[0], "Properties.policyDefinitions.policyDefinitionId") `
                            -and $initiative.Properties.policyDefinitions.policyDefinitionId -contains $definition.PolicyDefinitionId)
                    {
                        # Assignment is present; compliance state not found for this resource
                        if ($assignment)
                        {
                            $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::NoResponse
                        }
                        # Assignment not found
                        else
                        {
                            $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::AssignmentNotFound
                        }
                    }
                    # Definition is present, and is not added to the initiative
                    else
                    {
                        $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::DefinitionNotInInitiative
                    }
                    
                }
                # Definition is not present
                else
                {
                    $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::DefinitionNotFound
                }
            }
        }
        catch
        {
            $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Error
            if ([Helpers]::CheckMember($_, "Exception.Message"))
            {
                $ErrorDetails = "" | Select-Object OuterMessage
                $ErrorDetails.OuterMessage = $_.Exception.Message
                $controlResult.PolicyState.DataObject = $ErrorDetails
            }
            else
            {
                $ErrorDetails = "" | Select-Object StackTrace
                $ErrorDetails.StackTrace = $_
                $controlResult.PolicyState.DataObject = $ErrorDetails
            }
        }
    }
    # 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)
        {
            $resIdMessageString = "";
            if (-not [string]::IsNullOrWhiteSpace($extendedResourceName))
            {
                $resIdMessageString = "for nested resource [$extendedResourceName]";
            }

            $resourceGrpAlerts = @()
            $resourceAlerts = @()
            $resourceGrpAlerts += Get-AzMetricAlertRuleV2 -ResourceGroup $this.ResourceContext.ResourceGroupName -WarningAction SilentlyContinue
            $resourceAlerts += $resourceGrpAlerts |  Where-Object { ($_.Scopes -eq $this.ResourceId) -and ( $_.Enabled -eq '$true' ) }
             
            $alertsConfiguration = @();
            $nonConfiguredMetrices = @();
            $misConfiguredMetrices = @();

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

                if ($matchedMetrices.Count -eq 0)
                {
                    $nonConfiguredMetrices += $currentMetric;
                }
                else
                {
                    $misConfigured = @();

                    $matchedMetrices | ForEach-Object {
                        if (($_.Criteria | Measure-Object).Count -gt 0 )
                        {

                            $condition = New-Object -TypeName PSObject

                            Add-Member -InputObject $condition -Name "OperatorProperty" -MemberType NoteProperty -Value $_.Criteria.OperatorProperty
                            Add-Member -InputObject $condition -Name "Threshold" -MemberType NoteProperty -Value $_.Criteria.Threshold
                            Add-Member -InputObject $condition -Name "TimeAggregation" -MemberType NoteProperty -Value $_.Criteria.TimeAggregation
                            Add-Member -InputObject $condition -Name "WindowSize" -MemberType NoteProperty -Value  $_.WindowSize.ToString()
                            $obj = [PSCustomObject]@{MetricName = $_.Criteria.MetricName }
                            Add-Member -InputObject $condition -Name "DataSource" -MemberType NoteProperty -Value $obj
                                
                            $alert = New-Object -TypeName PSObject        
                            Add-Member -InputObject $alert -Name "Condition" -MemberType NoteProperty -Value $condition

                            $actions = @();
                            if ([Helpers]::CheckMember($_, "Actions.actionGroupId"))
                            {
                                $_.Actions | ForEach-Object {
                                    $actionGroupTemp = $_.actionGroupId.Split("/")
                                    $actionGroup = Get-AzActionGroup -ResourceGroupName $actionGroupTemp[4] -Name $actionGroupTemp[-1] -WarningAction SilentlyContinue
                                    if ([Helpers]::CheckMember($actionGroup, "EmailReceivers.Status"))
                                    {
                                        if ($actionGroup.EmailReceivers.Status -eq [Microsoft.Azure.Management.Monitor.Models.ReceiverStatus]::Enabled)
                                        {
                                            if ([Helpers]::CheckMember($actionGroup, "EmailReceivers.EmailAddress"))
                                            {
                                                $actions += $actionGroup
                                            }
                                        }
                                    }    
                                }
                            }        
                            Add-Member -InputObject $alert -Name "Actions" -MemberType NoteProperty -Value $actions
                            Add-Member -InputObject $alert -Name "AlertName" -MemberType NoteProperty -Value $_.Name
                            Add-Member -InputObject $alert -Name "AlertType" -MemberType NoteProperty -Value $_.Type
   
                            if (($alert | Measure-Object).Count -gt 0)
                            {
                                $alertsConfiguration += $alert 
                            }
                        }
                    }
                }

                if (($alertsConfiguration | Measure-Object).Count -gt 0)
                {
                    $alertsConfiguration | ForEach-Object {
                        if ([Helpers]::CompareObject($currentMetric.Condition, $_.Condition))
                        {
                            $isActionConfigured = $false;
                            if (($_.Actions | Measure-Object).Count -gt 0 )
                            {
                                $isActionConfigured = $true;
                            }
                            if (-not $isActionConfigured)
                            {
                                $misConfigured += $_.Condition;
                            }
                        }
                        else
                        {
                            $misConfigured += $_.Condition
                        }
                    };

                    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
        {
         #populating MaximumAllowedGraceDays irrespective of whether compliance state is enabled/not
            $singleControlResult.ControlResults | ForEach-Object {
                $currentControl = $_
                $currentControl.MaximumAllowedGraceDays = $this.CalculateGraceInDays($singleControlResult);
            }
        
         if ($this.IsLocalComplianceStoreEnabled)
            {
                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)

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

}