Framework/Core/FixControl/ControlSecurityFixes.ps1

Set-StrictMode -Version Latest 

class ControlSecurityFixes: AzCommandBase
{    
    hidden [string] $ParameterFilePath = "";
    hidden [string[]] $FolderPaths = @();
    [bool] $Force = $false;

    [FixControlConfig] $FixControlParam;

    ControlSecurityFixes([string] $subscriptionId, [InvocationInfo] $invocationContext, [FixControlConfig] $fixControlParam, [string] $parameterFilePath): 
        Base($subscriptionId, $invocationContext)
    { 
        if(-not $fixControlParam)
        {
            throw [System.ArgumentException] ("The argument 'fixControlParam' is null");
        }
        $this.FixControlParam = $fixControlParam;

        if(-not [string]::IsNullOrEmpty($parameterFilePath))
        {
            $this.ParameterFilePath = $parameterFilePath;
            $this.FolderPaths += Join-Path $([System.IO.Path]::GetDirectoryName($parameterFilePath)) "Services";
            $this.FolderPaths += Join-Path $PSScriptRoot "Services";
        }
        else
        {
            throw [System.ArgumentException] "The parameter 'parameterFilePath' is null or empty."
        }
    }

    [string] $ImpactText = "FixControlImpact";
    [string] $ControlsText = "Controls";
    [string] $TotalText = "Total";
    [string] $MarkerText = "--------";

    hidden [void] UpdateSummaryCount([PSObject[]] $summary, [ControlParam[]] $controls)
    {
        if($controls -and $controls.Count -ne 0)
        {
            $totalRow = $summary | Where-Object { $_.$($this.ImpactText) -eq $this.TotalText } | Select-Object -First 1;

            $controls | Group-Object { $_.$($this.ImpactText) } |
            ForEach-Object {
                $item = $_;
                $currentRow = $summary | Where-Object { $_.$($this.ImpactText) -eq $item.Name } | Select-Object -First 1;
                if($currentRow)
                {
                    $currentRow.$($this.ControlsText) += $item.Count;
                }

                if($totalRow)
                {
                    $totalRow.$($this.ControlsText) += $item.Count;
                }
            };
        }
    }

    hidden [void] PrintFixControlImpact()
    {
        $summary = @();
        [Enum]::GetNames([FixControlImpact]) |
        ForEach-Object {
            $row = [PSObject]::new();
            Add-Member -InputObject $row -Name $this.ImpactText -MemberType NoteProperty -Value $_.ToString()
            Add-Member -InputObject $row -Name $this.ControlsText -MemberType NoteProperty -Value 0
            $summary += $row;
        };

        $markerRow = [PSObject]::new();
        Add-Member -InputObject $markerRow -Name $this.ImpactText -MemberType NoteProperty -Value $this.MarkerText
        Add-Member -InputObject $markerRow -Name $this.ControlsText -MemberType NoteProperty -Value $this.MarkerText
        $summary += $markerRow;

        $totalRow = [PSObject]::new();
        Add-Member -InputObject $totalRow -Name $this.ImpactText -MemberType NoteProperty -Value $this.TotalText
        Add-Member -InputObject $totalRow -Name $this.ControlsText -MemberType NoteProperty -Value 0
        $summary += $totalRow;

        if($this.FixControlParam.SubscriptionControls.Count -ne 0)
        {
            $this.UpdateSummaryCount($summary, $this.FixControlParam.SubscriptionControls);            
        }

        if($this.FixControlParam.ResourceGroups.Count -ne 0)
        {
            $this.FixControlParam.ResourceGroups | ForEach-Object {
                $_.Resources | ForEach-Object {
                    $resource = $_.Controls;
                    $this.UpdateSummaryCount($summary, $_.Controls);            
                };
            };
        }

        $this.PublishCustomMessage(" `r`n" + ($summary | Format-Table | Out-String), [MessageType]::Info);
    }

    [MessageData[]] ImplementFix()
    {
        $this.PrintFixControlImpact();
        [MessageData[]] $messages = @();

        $this.PublishCustomMessage([MessageData]::new(" `nRunning this command will make changes to your Azure subscription/resource configurations in order to fix/remediate security controls. Please confirm that you have reviewed the proposed changes.", [MessageType]::Warning));
        $response = ""
        if($this.Force)
        {
            $response = "y";
            $this.PublishCustomMessage([MessageData]::new("User consent has been supressed by -Force parameter.", [MessageType]::Warning));
        }

        while($response -ne "y" -and $response -ne "n")
        {
            if(-not [string]::IsNullOrEmpty($response))
            {
                Write-Host "Please select appropriate option."
            }
            $response = Read-Host "Do you want to continue (Y/N)?"
            $response = $response.Trim()
        }
        if($response -eq "y")
        {
            $this.PublishCustomMessage("User has provided consent to implement the control fixes.");
            $this.PublishCustomMessage([Constants]::DoubleDashLine);
            $this.PublishCustomMessage("Started implementing control recommendations");
            $this.PublishCustomMessage("[SubscriptionId: $($this.FixControlParam.SubscriptionContext.SubscriptionId)] [SubscriptionName: $($this.FixControlParam.SubscriptionContext.SubscriptionName)]");
        
            # SSCore Fixes
            if($this.FixControlParam.SubscriptionControls.Count -ne 0)
            {
                $exceptionMessage = "Exception for subscription: [SubscriptionName: $($this.SubscriptionContext.SubscriptionName)] [SubscriptionId: $($this.SubscriptionContext.SubscriptionId)]"
                $wrapperObj = [ArrayWrapper]::new($this.FixControlParam.SubscriptionControls);
                $messages += $this.FixAllControls([SVTMapping]::SubscriptionMapping, $wrapperObj, [string] $exceptionMessage)
            }

            # Resource fixes
            if($this.FixControlParam.ResourceGroups.Count -ne 0)
            {
                $totalResources = 0;
                [int] $currentCount = 0;
                $this.FixControlParam.ResourceGroups | ForEach-Object { $totalResources += $_.Resources.Count };
                $this.FixControlParam.ResourceGroups | ForEach-Object {
                    $resourceGroup = $_;
                    $resourceGroup.Resources | ForEach-Object {
                        $resource = $_;
                        $exceptionMessage = "Exception for resource: [ResourceType: $($resource.ResourceTypeMapping.ResourceTypeName)] [ResourceGroupName: $($resourceGroup.ResourceGroupName)] [ResourceName: $($resource.ResourceName)]"
                        $currentCount += 1;
                        if($totalResources -gt 1)
                        {
                            $this.PublishCustomMessage(" `r`nFixing resource [$currentCount/$totalResources] ");
                        }

                        $messages += $this.FixAllControls($_.ResourceTypeMapping, @($resource, $resourceGroup.ResourceGroupName), [string] $exceptionMessage)
                    };
                };
            }
        }
        else
        {
            $this.PublishCustomMessage("The command execution aborted.")
        }

        return $messages;
    }

    [MessageData[]] FixAllControls([SubscriptionMapping] $typeMapping, [PSObject[]] $argumentList, [string] $exceptionMessage)
    {
        [MessageData[]] $messages = @();
        try 
        {
            if(-not $argumentList)
            {
                $argumentList = @();
            }

            foreach ($path in $this.FolderPaths) {
                $fileToLoad = Join-Path $path $typeMapping.FixFileName
                if(Test-Path -Path $fileToLoad)
                {
                    . $fileToLoad
                    break;
                }
            }
            
            $svtFixObject = $null;
            $fixClassName = $typeMapping.FixClassName;
            try
            {    
                $args = @();
                $args += $this.SubscriptionContext.SubscriptionId;
                $args += $argumentList;
                $svtFixObject = New-Object -TypeName $fixClassName -ArgumentList $args
            }
            catch
            {
                $this.PublishCustomMessage($exceptionMessage);
                if($_.Exception.InnerException)
                {
                    # Unwrapping the first layer of exception which is added by New-Object function
                    $this.CommandError($_.Exception.InnerException.ErrorRecord);
                }
                else
                {
                    $this.CommandError($_);
                }
            }

            if($svtFixObject)
            {
                $messages += $svtFixObject.FixAllControls();
            }

            # Register/Deregister all listeners to cleanup the memory
            [AzListenerHelper]::RegisterListeners();
        }
        catch 
        {
            $this.PublishCustomMessage($exceptionMessage);
            $this.CommandError($_);
        }
        return $messages;
    }
}