Framework/Abstracts/CommandBase.ps1

using namespace System.Management.Automation
Set-StrictMode -Version Latest
# Base class for all classes being called from PS commands
# Provides functionality to fire important events at command call
class CommandBase: AzSKRoot {
    [string[]] $FilterTags = @();
    [bool] $DoNotOpenOutputFolder = $false;
    [bool] $Force = $false
    [bool] $IsLocalComplianceStoreEnabled = $false
    CommandBase([string] $subscriptionId, [InvocationInfo] $invocationContext):
    Base($subscriptionId) {
        [Helpers]::AbstractClass($this, [CommandBase]);
        if (-not $invocationContext) {
            throw [System.ArgumentException] ("The argument 'invocationContext' is null. Pass the `$PSCmdlet.MyInvocation from PowerShell command.");
        }
        $this.InvocationContext = $invocationContext;
        [PrivacyNotice]::ValidatePrivacyAcceptance()

        if($null -ne $this.InvocationContext.BoundParameters["DoNotOpenOutputFolder"])
        {
            $this.DoNotOpenOutputFolder = $this.InvocationContext.BoundParameters["DoNotOpenOutputFolder"];
        }
        if($null -ne $this.InvocationContext.BoundParameters["Force"])
        {
            $this.Force = $this.InvocationContext.BoundParameters["Force"];
        }        
        #Validate if command is getting run with correct Org Policy
        $IsTagSettingRequired=$this.ValidateOrgPolicyOnSubscription($this.Force)
        #Validate if policy url token is getting expired
        $onlinePolicyStoreUrl = [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl
        if([Helpers]::IsSASTokenUpdateRequired($onlinePolicyStoreUrl))
        {
            #Check if CA Setup Runbook URL token is valid and update it with local policy token
            $CASetupRunbookUrl = [ConfigurationManager]::GetAzSKConfigData().CASetupRunbookURL
            if(-not [Helpers]::IsSASTokenUpdateRequired($CASetupRunbookUrl))
            {
                [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl = [Helpers]::GetUriWithUpdatedSASToken($onlinePolicyStoreUrl,$CASetupRunbookUrl)                
                [AzSKSettings]::Update([ConfigurationManager]::GetAzSKSettings())
            }
            else
            {
                [EventBase]::PublishGenericCustomMessage("Org policy settings is getting expired. Please run installer(IWR) command to update with latest policy. ", [MessageType]::Warning);
            }
        }

         #Validate if command has AzSK component write permission
        $commandMetadata= $this.GetCommandMetadata()
        if(([Helpers]::CheckMember($commandMetadata,"HasAzSKComponentWritePermission")) -and  $commandMetadata.HasAzSKComponentWritePermission -and ($IsTagSettingRequired -or $this.Force))
        {
            #If command is running with Org-neutral Policy or switch Org policy, Set Org Policy tag on subscription
            $this.SetOrgPolicyTag($this.Force)
        }    

        $azskConfigComplianceFlag = [ConfigurationManager]::GetAzSKConfigData().StoreComplianceSummaryInUserSubscriptions;    
        $localSettingComplianceFlag = [ConfigurationManager]::GetAzSKSettings().StoreComplianceSummaryInUserSubscriptions;
        #return if feature is turned off at server config
        if($azskConfigComplianceFlag -or $localSettingComplianceFlag) 
        {
            $this.IsLocalComplianceStoreEnabled = $true
        }     
        #clear azsk storage instance
        [StorageHelper]::AzSKStorageHelperInstance = $null;

    }

    [void] CommandStarted() {
        $this.PublishAzSKRootEvent([AzSKRootEvent]::CommandStarted, $this.CheckModuleVersion());
    }

    [void] CommandError([System.Management.Automation.ErrorRecord] $exception) {
        [AzSKRootEventArgument] $arguments = $this.CreateRootEventArgumentObject();
        $arguments.ExceptionMessage = $exception;

        $this.PublishEvent([AzSKRootEvent]::CommandError, $arguments);
    }

    [void] CommandCompleted([MessageData[]] $messages) {
        $this.PublishAzSKRootEvent([AzSKRootEvent]::CommandCompleted, $messages);
    }

    [string] InvokeFunction([PSMethod] $methodToCall) {
        return $this.InvokeFunction($methodToCall, @());
    }

    [string] InvokeFunction([PSMethod] $methodToCall, [System.Object[]] $arguments) {
        if (-not $methodToCall) {
            throw [System.ArgumentException] ("The argument 'methodToCall' is null. Pass the reference of method to call. e.g.: [YourClass]::new().YourMethod");
        }

        # Reset cached context
        [Helpers]::ResetCurrentRMContext()

        $this.PublishRunIdentifier($this.InvocationContext);
        [AIOrgTelemetryHelper]::TrackCommandExecution("Command Started",
            @{"RunIdentifier" = $this.RunIdentifier}, @{}, $this.InvocationContext);
        $sw = [System.Diagnostics.Stopwatch]::StartNew();
        $isExecutionSuccessful = $true
        $this.CommandStarted();
        $this.PostCommandStartedAction();
        $methodResult = @();
        try {
           $methodResult = $methodToCall.Invoke($arguments);
        }
        catch {
            $isExecutionSuccessful = $true
            # Unwrapping the first layer of exception which is added by Invoke function
            [AIOrgTelemetryHelper]::TrackCommandExecution("Command Errored",
                @{"RunIdentifier" = $this.RunIdentifier; "ErrorRecord"= $_.Exception.InnerException.ErrorRecord},
                @{"TimeTakenInMs" = $sw.ElapsedMilliseconds; "SuccessCount" = 0},
                $this.InvocationContext);
            $this.CommandError($_.Exception.InnerException.ErrorRecord);
        }

        $this.CommandCompleted($methodResult);
        [AIOrgTelemetryHelper]::TrackCommandExecution("Command Completed",
            @{"RunIdentifier" = $this.RunIdentifier},
            @{"TimeTakenInMs" = $sw.ElapsedMilliseconds; "SuccessCount" = 1},
            $this.InvocationContext)
        $this.PostCommandCompletedAction($methodResult);

        $folderPath = $this.GetOutputFolderPath();

        #Generate PDF report
        $GeneratePDFReport = $this.InvocationContext.BoundParameters["GeneratePDF"];

        try {
            if (-not [string]::IsNullOrEmpty($folderpath)) {
                switch ($GeneratePDFReport) {
                    None {
                        # Do nothing
                    }
                    Landscape {
                        [AzSKPDFExtension]::GeneratePDF($folderpath, $this.SubscriptionContext, $this.InvocationContext, $true);
                    }
                    Portrait {
                        [AzSKPDFExtension]::GeneratePDF($folderpath, $this.SubscriptionContext, $this.InvocationContext, $false);
                    }
                }
            }
        }
        catch {
            # Unwrapping the first layer of exception which is added by Invoke function
            $this.CommandError($_);
        }

        $AttestControlParamFound = $this.InvocationContext.BoundParameters["AttestControls"];
        if($null -eq $AttestControlParamFound)
        {
            if((-not $this.DoNotOpenOutputFolder) -and (-not [string]::IsNullOrEmpty($folderPath)))
            {
                try
                {
                    Invoke-Item -Path $folderPath;
                }
                catch
                {
                    #ignore if any exception occurs
                }
            }
        }
        return $folderPath;

        # Call clear temp folder function.
    }

    [void] PostCommandStartedAction()
    {
        
    }

    [string] GetOutputFolderPath() {
        return [WriteFolderPath]::GetInstance().FolderPath;
    }


    [void] CheckModuleVersion() {
         
        $currentModuleVersion = [System.Version] $this.GetCurrentModuleVersion()
        $serverVersion = [System.Version] ([ConfigurationManager]::GetAzSKConfigData().GetLatestAzSKVersion($this.GetModuleName()));
        $currentModuleVersion = [System.Version] $this.GetCurrentModuleVersion() 
        if($currentModuleVersion -ne "0.0.0.0" -and $serverVersion -gt $this.GetCurrentModuleVersion()) {
            $this.RunningLatestPSModule = $false;
            $this.InvokeAutoUpdate()
            $this.PublishCustomMessage(([Constants]::VersionCheckMessage -f $serverVersion), [MessageType]::Warning);
            $this.PublishCustomMessage(([ConfigurationManager]::GetAzSKConfigData().InstallationCommand + "`r`n"), [MessageType]::Update);
            $this.PublishCustomMessage([Constants]::VersionWarningMessage, [MessageType]::Warning);

            $serverVersions = @()
            [ConfigurationManager]::GetAzSKConfigData().GetAzSKVersionList($this.GetModuleName()) | ForEach-Object { 
                #Take major and minor version and ignore build version for comparision
               $serverVersions+= [System.Version] ("$($_.Major)" +"." + "$($_.Minor)")
             }            
            $serverVersions =  $serverVersions | Select-Object -Unique
            $latestVersionList = $serverVersions | Where-Object {$_ -gt $currentModuleVersion}
            if(($latestVersionList | Measure-Object).Count -gt [ConfigurationManager]::GetAzSKConfigData().BackwardCompatibleVersionCount)
            {
                throw ([SuppressedException]::new(("Your version of AzSK is too old. Please update now!"),[SuppressedExceptionType]::Generic))
            }
        }
        
        $psGalleryVersion = [System.Version] ([ConfigurationManager]::GetAzSKConfigData().GetAzSKLatestPSGalleryVersion($this.GetModuleName()));            
        if($psGalleryVersion -ne $serverVersion)
        {
            $serverVersions = @()
            [ConfigurationManager]::GetAzSKConfigData().GetAzSKVersionList($this.GetModuleName()) | ForEach-Object { 
                #Take major and minor version and ignore build version for comparision
               $serverVersions+= [System.Version] ("$($_.Major)" +"." + "$($_.Minor)")
             }            
            $serverVersions =  $serverVersions | Select-Object -Unique
            $latestVersionAvailableFromGallery = $serverVersions | Where-Object {$_ -gt $serverVersion}
            if(($latestVersionAvailableFromGallery | Measure-Object).Count -gt [ConfigurationManager]::GetAzSKConfigData().BackwardCompatibleVersionCount)
            {
                $this.PublishCustomMessage("Your Org AzSK version[$serverVersion] is too old. Consider updating it to latest available version[$psGalleryVersion].",[MessageType]::Error);
            }
        }
        
    }

    [void] InvokeAutoUpdate()
    {
        $AutoUpdateSwitch= [ConfigurationManager]::GetAzSKSettings().AutoUpdateSwitch;
        $AutoUpdateCommand = [ConfigurationManager]::GetAzSKSettings().AutoUpdateCommand;

        if($AutoUpdateSwitch -ne [AutoUpdate]::On)
        {
            if($AutoUpdateSwitch -eq [AutoUpdate]::NotSet)
            {
                Write-Host "Auto-update for AzSK is currently not enabled for your machine. To set it, run the command below:" -ForegroundColor Yellow
                Write-Host "Set-AzSKPolicySettings -AutoUpdate On`n" -ForegroundColor Green
            }
            return;
        }

        #Step 1: Get the list of active running powershell prcesses including the current running PS Session
        $PSProcesses = Get-Process | Where-Object { ($_.Name -eq 'powershell' -or $_.Name -eq 'powershell_ise' -or $_.Name -eq 'powershelltoolsprocesshost')}

        $userChoice = ""
        if(($PSProcesses | Measure-Object).Count -ge 1)
        {            
            Write-Host "A new version of AzSK is available. Starting the auto-update workflow...`nTo prepare for auto-update, please:`n`t a) Save your work from all active PS sessions including the current one and`n`t b) Close all PS sessions other than the current one. " -ForegroundColor Cyan
        }

        #User choice that captures the decision to close the active PS Sessions
        $secondUserChoice =""
        $InvalidOption = $true;
        while($InvalidOption)
        {
            if([string]::IsNullOrWhiteSpace($userChoice) -or ($userChoice.Trim() -ne 'y' -and $userChoice.Trim() -ne 'n'))
            {
                $userChoice = Read-Host "Continue (Y/N)"
                if([string]::IsNullOrWhiteSpace($userChoice) -or ($userChoice.Trim() -ne 'y' -and $userChoice.Trim() -ne 'n'))
                {
                    Write-Host "Enter the valid option." -ForegroundColor Yellow
                }
                continue;
            }
            elseif($userChoice.Trim() -eq 'n')
            {
                $InvalidOption = $false;
            }
            elseif($userChoice.Trim() -eq 'y')
            {
                #Get the number of PS active sessions
                $PSProcesses = Get-Process | Where-Object { ($_.Name -eq 'powershell' -or $_.Name -eq 'powershell_ise' -or $_.Name -eq 'powershelltoolsprocesshost') -and $_.Id -ne $PID}
                if(($PSProcesses | Measure-Object).Count -gt 0)
                {
                    Write-Host "`nThe following other PS sessions are still active. Please save your work and close them. You can also use Task Manager to close these sessions." -ForegroundColor Yellow
                    Write-Host ($PSProcesses | Select-Object Id, ProcessName, Path | Out-String)
                    $secondUserChoice = Read-Host "Continue (Y/N)"
                }
                elseif(($PSProcesses | Measure-Object).Count -eq 0)
                {
                    Write-Host "`nThe current PS session will be closed now. Have you saved your work?" -ForegroundColor Yellow
                    $secondUserChoice = Read-Host "Continue (Y/N)"
                }
                if(-not [string]::IsNullOrWhiteSpace($secondUserChoice) -and `
                (($PSProcesses | Measure-Object).Count -eq 0 -and $secondUserChoice.Trim() -eq 'y') -or `
                $secondUserChoice.Trim() -eq 'n')
                {
                    $InvalidOption = $false;
                }
            }
        }
        #Check if the first user want to continue with auto-update using userChoice field and then check if user still wants to continue with auto-update after finding the active PS sessions.
        #In either case it is no it would exit the auto-update process
        if($userChoice.Trim() -eq "n" -or $secondUserChoice.Trim() -eq 'n')
        {            
            Write-Host "Exiting auto-update workflow. To disable auto-update permanently, run the command below:" -ForegroundColor Yellow
            Write-Host "Set-AzSKPolicySettings -AutoUpdate Off`n" -ForegroundColor Green
            return
        }
        $AzSKTemp = [Constants]::AzSKAppFolderPath + "\Temp\";
        try
        {
            $fileName = "au_" + $(get-date).ToUniversalTime().ToString("yyyyMMdd_HHmmss") + ".ps1";

            $autoUpdateContent = [ConfigurationHelper]::LoadOfflineConfigFile("ModuleAutoUpdate.ps1");
            if(-not (Test-Path -Path $AzSKTemp))
            {
                mkdir -Path $AzSKTemp -Force
            }
            Remove-Item -Path "$AzSKTemp\au_*" -Force -Recurse -ErrorAction SilentlyContinue

            $autoUpdateContent = $autoUpdateContent.Replace("##installurl##",$AutoUpdateCommand);
            $autoUpdateContent | Out-File "$AzSKTemp\$fileName" -Force

            Start-Process -WindowStyle Normal -FilePath "powershell.exe" -ArgumentList "$AzSKTemp\$fileName"
        }
        catch
        {
            $this.CommandError($_.Exception.InnerException.ErrorRecord);
        }
    }

    [void] CommandProgress([int] $totalItems, [int] $currentItem) {
        $this.CommandProgress($totalItems, $currentItem, 1);
    }

    [void] CommandProgress([int] $totalItems, [int] $currentItem, [int] $granularity) {
        if ($totalItems -gt 0) {
            # $granularity indicates the number of items after which percentage progress will be printed
            # Set the max granularity to total items
            if ($granularity -gt $totalItems) {
                $granularity = $totalItems;
            }

            # Conditions for posting progress: 0%, 100% and based on granularity
            if ($currentItem -eq 0 -or $currentItem -eq $totalItems -or (($currentItem % $granularity) -eq 0)) {
                $this.PublishCustomMessage("$([int](($currentItem / $totalItems) * 100))% Completed");
            }
        }
    }

    # Dummy function declaration to define the function signature
    [void] PostCommandCompletedAction([MessageData[]] $messages)
    { }
    
    [bool] ValidateOrgPolicyOnSubscription([bool] $Force)
    {
        $AzSKConfigData = [ConfigurationManager]::GetAzSKConfigData()
        $tagsOnSub =  [Helpers]::GetResourceGroupTags($AzSKConfigData.AzSKRGName)
        $IsTagSettingRequired = $false
        $commandMetadata= $this.GetCommandMetadata()
        if(([Helpers]::CheckMember($commandMetadata,"IsOrgPolicyMandatory")) -and  $commandMetadata.IsOrgPolicyMandatory)
        {
            if($tagsOnSub)
            {
                $SubOrgTag= $tagsOnSub.GetEnumerator() | Where-Object {$_.Name -like "AzSKOrgName*"}
                
                if(($SubOrgTag | Measure-Object).Count -gt 0)
                {
                    $OrgName =$SubOrgTag.Name.Split("_")[1]        
                    if(-not [string]::IsNullOrWhiteSpace($OrgName) -and  $OrgName -ne $AzSKConfigData.PolicyOrgName)
                    {
                        if($AzSKConfigData.PolicyOrgName -eq "org-neutral")
                        {
                            throw [SuppressedException]::new("The current subscription has been configured with DevOps kit policy for the '$OrgName' Org, However the DevOps kit command is running with a different ('$($AzSKConfigData.PolicyOrgName)') Org policy. `nPlease review FAQ at: https://aka.ms/devopskit/orgpolicy/faq and correct this condition depending upon which context(manual,CICD,CA scan) you are seeing this error. If FAQ does not help to resolve the issue, please contact your Org policy Owner ($($SubOrgTag.Value)).",[SuppressedExceptionType]::Generic)
                            
                        }
                        else
                        {    
                            if(-not $Force)
                            {
                                $this.PublishCustomMessage("Warning: The current subscription has been configured with DevOps kit policy for the '$OrgName' Org, However the DevOps kit command is running with a different ('$($AzSKConfigData.PolicyOrgName)') Org policy. `nPlease review FAQ at: https://aka.ms/devopskit/orgpolicy/faq and correct this condition depending upon which context(manual,CICD,CA scan) you are seeing this error. If FAQ does not help to resolve the issue, please contact your Org policy Owner ($($SubOrgTag.Value)).",[MessageType]::Warning);
                                $IsTagSettingRequired = $false
                            }                    
                        }
                    }              
                }
                elseif($AzSKConfigData.PolicyOrgName -ne "org-neutral"){                
                    $IsTagSettingRequired =$true            
                }             
            }
            else {
                $IsTagSettingRequired = $true
            }
        }
        return $IsTagSettingRequired    
    }

    [void] SetOrgPolicyTag([bool] $Force)
    {
        try
        {
            $AzSKConfigData = [ConfigurationManager]::GetAzSKConfigData()
            $tagsOnSub =  [Helpers]::GetResourceGroupTags($AzSKConfigData.AzSKRGName) 
            if($tagsOnSub)
            {
                $SubOrgTag= $tagsOnSub.GetEnumerator() | Where-Object {$_.Name -like "AzSKOrgName*"}            
                if(
                    (($SubOrgTag | Measure-Object).Count -eq 0 -and $AzSKConfigData.PolicyOrgName -ne "org-neutral") -or 
                    (($SubOrgTag | Measure-Object).Count -gt 0 -and $AzSKConfigData.PolicyOrgName -ne "org-neutral" -and $AzSKConfigData.PolicyOrgName -ne $SubOrgTag.Value -and $Force))
                {
                    if(($SubOrgTag | Measure-Object).Count -gt 0)
                    {
                        $SubOrgTag | ForEach-Object{
                            [Helpers]::SetResourceGroupTags($AzSKConfigData.AzSKRGName,@{$_.Name=$_.Value}, $true)               
                        }
                    }
                    $TagName = [Constants]::OrgPolicyTagPrefix +$AzSKConfigData.PolicyOrgName
                    $SupportMail = $AzSKConfigData.SupportDL
                    if(-not [string]::IsNullOrWhiteSpace($SupportMail) -and  [Constants]::SupportDL -eq $SupportMail)
                    {
                        $SupportMail = "Not Available"
                    }   
                    [Helpers]::SetResourceGroupTags($AzSKConfigData.AzSKRGName,@{$TagName=$SupportMail}, $false)                
                                    
                }
                                    
            }
        }
        catch{
            # Exception occurred during setting tag. This is kept blank intentionaly to avoid flow break
        }
    }
}