public/Test-AdfArmTemplate.ps1

<#
.SYNOPSIS
Runs validation agains Data Factory code in ARMTemplate in terms of best practices from the field.

.DESCRIPTION
Runs validation agains Data Factory code in ARMTemplate in terms of best practices from the field.
Author: Paul Andrew

.PARAMETER ARMTemplateFilePath
Provide the location of your ARM template file

.EXAMPLE
Test-AdfArmTemplate -ARMTemplateFilePath "$File"

#>

function Test-AdfArmTemplate {
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)] 
        [String] $ARMTemplateFilePath
        ,[bool] $SummaryOutput = $true 
        ,[bool] $VerboseOutput = $false
    )

    Set-StrictMode -Version 1


#############################################################################################
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
}

return $SummaryTable

}