# STEP 1: #Provide the location of your download ARM template file: $ARMTemplateFilePath = "C:\ADFRoot\arm_template.json" #STEP 2: #Pick how you'd like the output: [bool]$SummaryOutput = $true [bool]$VerboseOutput = $false #STEP 3: #Run it. ############################################################################################# if(-not (Test-Path -Path $ARMTemplateFilePath)) { Write-Error "ARM template file not found. Please check the path provided." return } $Hr = "-------------------------------------------------------------------------------------------------------------------" Write-Host "" Write-Host $Hr Write-Host "Running checks for Data Factory ARM template:" Write-Host "" $ARMTemplateFilePath Write-Host "" #Parse template into ADF resource parts $ADF = Get-Content $ARMTemplateFilePath | ConvertFrom-Json $LinkedServices = $ADF.resources | Where-Object {$_.type -eq "Microsoft.DataFactory/factories/linkedServices"} $Datasets = $ADF.resources | Where-Object {$_.type -eq "Microsoft.DataFactory/factories/datasets"} $Pipelines = $ADF.resources | Where-Object {$_.type -eq "Microsoft.DataFactory/factories/pipelines"} $Activities = $Pipelines.properties.activities #regardless of pipeline $DataFlows = $ADF.resources | Where-Object {$_.type -eq "Microsoft.DataFactory/factories/dataflows"} $Triggers = $ADF.resources | Where-Object {$_.type -eq "Microsoft.DataFactory/factories/triggers"} #Output variables $CheckNumber = 0 $CheckDetail = "" $Severity = "" $CheckCounter = 0 $SummaryTable = @() $VerboseDetailTable = @() #String helper functions function CleanName { param ( [parameter(Mandatory = $true)] [String] $RawValue ) $CleanName = $RawValue.substring($RawValue.IndexOf("/")+1, $RawValue.LastIndexOf("'") - $RawValue.IndexOf("/")-1) return $CleanName } function CleanType { param ( [parameter(Mandatory = $true)] [String] $RawValue ) $CleanName = $RawValue.substring($RawValue.LastIndexOf("/")+1, $RawValue.Length - $RawValue.LastIndexOf("/")-1) return $CleanName } ############################################################################################# #Review resource dependants ############################################################################################# $ResourcesList = New-Object System.Collections.ArrayList($null) $DependantsList = New-Object System.Collections.ArrayList($null) #Get resources ForEach($Resource in $ADF.resources) { $ResourceName = CleanName -RawValue $Resource.name $ResourceType = CleanType -RawValue $Resource.type $CompleteResource = $ResourceType + "|" + $ResourceName if(-not ($ResourcesList -contains $CompleteResource)) { [void]$ResourcesList.Add($CompleteResource) } } #Get dependants ForEach($Resource in $ADF.resources)# | Where-Object {$_.type -ne "Microsoft.DataFactory/factories/triggers"}) { if($Resource.dependsOn.Count -eq 1) { $DependantName = CleanName -RawValue $Resource.dependsOn[0].ToString() $CompleteDependant = $DependantName.Replace('/','|') if(-not ($DependantsList -contains $CompleteDependant)) { [void]$DependantsList.Add($CompleteDependant) } } else { ForEach($Dependant in $Resource.dependsOn) { $DependantName = CleanName -RawValue $Dependant $CompleteDependant = $DependantName.Replace('/','|') if(-not ($DependantsList -contains $CompleteDependant)) { [void]$DependantsList.Add($CompleteDependant) } } } } #Get trigger dependants ForEach($Resource in $Triggers) { $ResourceName = CleanName -RawValue $Resource.name $ResourceType = CleanType -RawValue $Resource.type $CompleteResource = $ResourceType + "|" + $ResourceName if($Resource.dependsOn.count -ge 1) { if(-not ($DependantsList -contains $CompleteResource)) { [void]$DependantsList.Add($CompleteResource) } } } #Establish simple redundancy to use later $RedundantResources = $ResourcesList | Where-Object {$DependantsList -notcontains $_} ############################################################################################# #Check for pipeline without triggers ############################################################################################# $CheckNumber += 1 $CheckDetail = "Pipeline(s) without any triggers attached. Directly or indirectly." Write-Host "Running check... " $CheckDetail $Severity = "Medium" ForEach($RedundantResource in $RedundantResources | Where-Object {$_ -like "pipelines*"}) { $Parts = $RedundantResource.Split('|') $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Pipeline"; Name = $Parts[1]; CheckDetail = "Does not any triggers attached."; Severity = $Severity } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check pipeline with an impossible execution chain. ############################################################################################# $CheckNumber += 1 $CheckDetail = "Pipeline(s) with an impossible AND/OR activity execution chain." Write-Host "Running check... " $CheckDetail $Severity = "High" ForEach($Pipeline in $Pipelines) { $PipelineName = (CleanName -RawValue $Pipeline.name.ToString()) $ActivityFailureDependencies = New-Object System.Collections.ArrayList($null) $ActivitySuccessDependencies = New-Object System.Collections.ArrayList($null) #get upstream failure dependants ForEach($Activity in $Pipeline.properties.activities) { if($Activity.dependsOn.Count -gt 1) { ForEach($UpStreamActivity in $Activity.dependsOn) { if($UpStreamActivity.dependencyConditions.Contains('Failed')) { if(-not ($ActivityFailureDependencies -contains $UpStreamActivity.activity)) { [void]$ActivityFailureDependencies.Add($UpStreamActivity.activity) } } } } } #get downstream success dependants ForEach($ActivityDependant in $ActivityFailureDependencies) { ForEach($Activity in $Pipeline.properties.activities | Where-Object {$_.name -eq $ActivityDependant}) { if($Activity.dependsOn.Count -ge 1) { ForEach($DownStreamActivity in $Activity.dependsOn) { if($DownStreamActivity.dependencyConditions.Contains('Succeeded')) { if(-not ($ActivitySuccessDependencies -contains $DownStreamActivity.activity)) { [void]$ActivitySuccessDependencies.Add($DownStreamActivity.activity) } } } } } } #compare dependants - do they exist in both lists? $Problems = $ActivityFailureDependencies | Where-Object {$ActivitySuccessDependencies -contains $_} if($Problems.Count -gt 0) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Pipeline"; Name = $PipelineName; CheckDetail = "Has an impossible AND/OR activity execution chain."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for pipeline descriptions ############################################################################################# $CheckNumber += 1 $CheckDetail = "Pipeline(s) without a description value." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Pipeline in $Pipelines) { $PipelineName = (CleanName -RawValue $Pipeline.name.ToString()) $PipelineDescription = $Pipeline.properties.description if(([string]::IsNullOrEmpty($PipelineDescription))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Pipeline"; Name = $PipelineName; CheckDetail = "Does not have a description."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for pipelines not in folders ############################################################################################# $CheckNumber += 1 $CheckDetail = "Pipeline(s) not organised into folders." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Pipeline in $Pipelines) { $PipelineName = (CleanName -RawValue $Pipeline.name.ToString()) $PipelineFolder = $Pipeline.properties.folder.name if(([string]::IsNullOrEmpty($PipelineFolder))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Pipeline"; Name = $PipelineName; CheckDetail = "Not organised into a folder."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for pipelines without annotations ############################################################################################# $CheckNumber += 1 $CheckDetail = "Pipeline(s) without annotations." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Pipeline in $Pipelines) { $PipelineName = (CleanName -RawValue $Pipeline.name.ToString()) $PipelineAnnotations = $Pipeline.properties.annotations.Count if($PipelineAnnotations -le 0) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Pipeline"; Name = $PipelineName; CheckDetail = "Does not have any annotations."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for data flow descriptions ############################################################################################# $CheckNumber += 1 $CheckDetail = "Data Flow(s) without a description value." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($DataFlow in $DataFlows) { $DataFlowName = (CleanName -RawValue $DataFlow.name.ToString()) $DataFlowDescription = $DataFlow.properties.description if(([string]::IsNullOrEmpty($DataFlowDescription))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Data Flow"; Name = $DataFlowName; CheckDetail = "Does not have a description."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check activity timeout values ############################################################################################# $CheckNumber += 1 $CheckDetail = "Activitie(s) with timeout values still set to the service default value of 7 days." Write-Host "Running check... " $CheckDetail $Severity = "High" ForEach ($Activity in $Activities) { $timeout = $Activity.policy.timeout if(-not ([string]::IsNullOrEmpty($timeout))) { if($timeout -eq "7.00:00:00") { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Activity"; Name = $Activity.Name; CheckDetail = "Timeout policy still set to the service default value of 7 days."; Severity = $Severity } } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check activity descriptions ############################################################################################# $CheckNumber += 1 $CheckDetail = "Activitie(s) without a description value." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Activity in $Activities) { $ActivityDescription = $Activity.description if(([string]::IsNullOrEmpty($ActivityDescription))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Activity"; Name = $Activity.Name; CheckDetail = "Does not have a description."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check foreach activity batch size unset ############################################################################################# $CheckNumber += 1 $CheckDetail = "Activitie(s) ForEach iteration without a batch count value set." Write-Host "Running check... " $CheckDetail $Severity = "High" ForEach ($Activity in $Activities | Where-Object {$_.type -eq "ForEach"}) { [bool]$isSequential = $false #attribute may only exist if changed, assume not present in arm template if((-not [string]::IsNullOrEmpty($Activity.typeProperties.isSequential))) { $isSequential = $Activity.typeProperties.isSequential } $BatchCount = $Activity.typeProperties.batchCount if(!$isSequential) { if(([string]::IsNullOrEmpty($BatchCount))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Activity"; Name = $Activity.Name; CheckDetail = "ForEach does not have a batch count value set."; Severity = $Severity } } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check foreach activity batch size is less than the service maximum ############################################################################################# $CheckNumber += 1 $CheckDetail = "Activitie(s) ForEach iteration with a batch count size that is less than the service maximum." Write-Host "Running check... " $CheckDetail $Severity = "Medium" ForEach ($Activity in $Activities | Where-Object {$_.type -eq "ForEach"}) { [bool]$isSequential = $false #attribute may only exist if changed, assume not present in arm template if((-not [string]::IsNullOrEmpty($Activity.typeProperties.isSequential))) { $isSequential = $Activity.typeProperties.isSequential } $BatchCount = $Activity.typeProperties.batchCount if(!$isSequential) { if($BatchCount -lt 50) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Activity"; Name = $Activity.Name; CheckDetail = "ForEach has a batch size that is less than the service maximum."; Severity = $Severity } } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check linked service using key vault ############################################################################################# $CheckNumber += 1 $CheckDetail = "Linked Service(s) not using Azure Key Vault to store credentials." Write-Host "Running check... " $CheckDetail $Severity = "High" $LinkedServiceList = New-Object System.Collections.ArrayList($null) ForEach ($LinkedService in $LinkedServices | Where-Object {$_.properties.type -ne "AzureKeyVault"}) { $typeProperties = Get-Member -InputObject $LinkedService.properties.typeProperties -MemberType NoteProperty ForEach($typeProperty in $typeProperties) { $propValue = $LinkedService.properties.typeProperties | Select-Object -ExpandProperty $typeProperty.Name #handle linked services with multiple type properties if(([string]::IsNullOrEmpty($propValue.secretName))){ $LinkedServiceName = (CleanName -RawValue $LinkedService.name) if(-not ($LinkedServiceList -contains $LinkedServiceName)) { [void]$LinkedServiceList.Add($LinkedServiceName) #add linked service if secretName is missing } } if(-not([string]::IsNullOrEmpty($propValue.secretName))){ $LinkedServiceName = (CleanName -RawValue $LinkedService.name) [void]$LinkedServiceList.Remove($LinkedServiceName) #renove linked service if secretName is then found } } } $CheckCounter = $LinkedServiceList.Count $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 if($VerboseOutput) { ForEach ($LinkedServiceOutput in $LinkedServiceList) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Linked Service"; Name = $LinkedServiceOutput; CheckDetail = "Not using Key Vault to store credentials."; Severity = $Severity } } } ############################################################################################# #Check for linked services not in use ############################################################################################# $CheckNumber += 1 $CheckDetail = "Linked Service(s) not used by any other resource." Write-Host "Running check... " $CheckDetail $Severity = "Medium" ForEach($RedundantResource in $RedundantResources | Where-Object {$_ -like "linkedServices*"}) { $Parts = $RedundantResource.Split('|') $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Linked Service"; Name = $Parts[1]; CheckDetail = "Not used by any other resource."; Severity = $Severity } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check linked service descriptions ############################################################################################# $CheckNumber += 1 $CheckDetail = "Linked Service(s) without a description value." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($LinkedService in $LinkedServices) { $LinkedServiceName = (CleanName -RawValue $LinkedService.name.ToString()) $LinkedServiceDescription = $LinkedService.properties.description if(([string]::IsNullOrEmpty($LinkedServiceDescription))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Linked Service"; Name = $LinkedServiceName; CheckDetail = "Does not have a description."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for linked service without annotations ############################################################################################# $CheckNumber += 1 $CheckDetail = "Linked Service(s) without annotations." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Pipeline in $Pipelines) { $LinkedServiceName = (CleanName -RawValue $LinkedService.name.ToString()) $LinkedServiceAnnotations = $Pipeline.properties.annotations.Count if($LinkedServiceAnnotations -le 0) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Linked Service"; Name = $LinkedServiceName; CheckDetail = "Does not have any annotations."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for datasets not in use ############################################################################################# $CheckNumber += 1 $CheckDetail = "Dataset(s) not used by any other resource." Write-Host "Running check... " $CheckDetail $Severity = "Medium" ForEach($RedundantResource in $RedundantResources | Where-Object {$_ -like "datasets*"}) { $Parts = $RedundantResource.Split('|') $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Dataset"; Name = $Parts[1]; CheckDetail = "Not used by any other resource."; Severity = $Severity } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for dataset without description ############################################################################################# $CheckNumber += 1 $CheckDetail = "Dataset(s) without a description value." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Dataset in $Datasets) { $DatasetName = (CleanName -RawValue $Dataset.name.ToString()) $DatasetDescription = $Dataset.properties.description if(([string]::IsNullOrEmpty($DatasetDescription))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Dataset"; Name = $DatasetName; CheckDetail = "Does not have a description."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check dataset not in folders ############################################################################################# $CheckNumber += 1 $CheckDetail = "Dataset(s) not organised into folders." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Dataset in $Datasets) { $DatasetName = (CleanName -RawValue $Dataset.name.ToString()) $DatasetFolder = $Dataset.properties.folder.name if(([string]::IsNullOrEmpty($DatasetFolder))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Dataset"; Name = $DatasetName; CheckDetail = "Not organised into a folder."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for datasets without annotations ############################################################################################# $CheckNumber += 1 $CheckDetail = "Dataset(s) without annotations." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Dataset in $Datasets) { $DatasetName = (CleanName -RawValue $Dataset.name.ToString()) $DatasetAnnotations = $Dataset.properties.annotations.Count if($DatasetAnnotations -le 0) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Dataset"; Name = $DatasetName; CheckDetail = "Does not have any annotations."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for triggers not in use ############################################################################################# $CheckNumber += 1 $CheckDetail = "Trigger(s) not used by any other resource." Write-Host "Running check... " $CheckDetail $Severity = "Medium" ForEach($RedundantResource in $RedundantResources | Where-Object {$_ -like "triggers*"}) { $Parts = $RedundantResource.Split('|') $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Trigger"; Name = $Parts[1]; CheckDetail = "Not used by any other resource."; Severity = $Severity } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for trigger descriptions ############################################################################################# $CheckNumber += 1 $CheckDetail = "Trigger(s) without a description value." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Trigger in $Triggers) { $TriggerName = (CleanName -RawValue $Pipeline.name.ToString()) $TriggerDescription = $Trigger.properties.description if(([string]::IsNullOrEmpty($TriggerDescription))) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Trigger"; Name = $TriggerName; CheckDetail = "Does not have a description."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# #Check for trigger without annotations ############################################################################################# $CheckNumber += 1 $CheckDetail = "Trigger(s) without annotations." Write-Host "Running check... " $CheckDetail $Severity = "Low" ForEach ($Trigger in $Triggers) { $TriggerName = (CleanName -RawValue $Trigger.name.ToString()) $TriggerAnnotations = $Trigger.properties.annotations.Count if($TriggerAnnotations -le 0) { $CheckCounter += 1 if($VerboseOutput) { $VerboseDetailTable += [PSCustomObject]@{ Component = "Trigger"; Name = $TriggerName; CheckDetail = "Does not have any annotations."; Severity = $Severity } } } } $SummaryTable += [PSCustomObject]@{ IssueCount = $CheckCounter; CheckDetail = $CheckDetail; Severity = $Severity } $CheckCounter = 0 ############################################################################################# Write-Host "" Write-Host $Hr if($SummaryOutput) { Write-Host "" Write-Host "Results Summary:" Write-Host "" Write-Host "Checks ran against template:" $CheckNumber Write-Host "Checks with issues found:" ($SummaryTable | Where-Object {$_.IssueCount -ne 0}).Count.ToString() Write-Host "Total issue count:" ($SummaryTable | Measure-Object -Property IssueCount -Sum).Sum $SummaryTable | Where-Object {$_.IssueCount -ne 0} | Format-Table @{ Label = "Issue Count";Expression = {$_.IssueCount}; Alignment="Center"}, @{ Label = "Check Details";Expression = {$_.CheckDetail}}, @{ Label = "Severity" Expression = { switch ($_.Severity) { #https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#span-idtextformattingspanspan-idtextformattingspanspan-idtextformattingspantext-formatting 'Low' {$color = "92"; break } 'Medium' {$color = '93'; break } 'High' {$color = "31"; break } default {$color = "0"} } $e = [char]27 "$e[${color}m$($_.Severity)${e}[0m" } } Write-Host $Hr } if($VerboseOutput) { Write-Host "" Write-Host "Results Details:" $VerboseDetailTable | Format-Table @{ Label = "Component";Expression = {$_.Component}}, @{ Label = "Name";Expression = {$_.Name}}, @{ Label = "Check Detail";Expression = {$_.CheckDetail}}, @{ Label = "Severity" Expression = { switch ($_.Severity) { #https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#span-idtextformattingspanspan-idtextformattingspanspan-idtextformattingspantext-formatting 'Low' {$color = "92"; break } 'Medium' {$color = '93'; break } 'High' {$color = "31"; break } default {$color = "0"} } $e = [char]27 "$e[${color}m$($_.Severity)${e}[0m" } } Write-Host $Hr }