BurpSuiteDeploy.psm1

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13

# $ExecutionContext.SessionState.Module.OnRemove = {
# [DeploymentCache]::Deployments = @()
# [ScanConfigurationCache]::ScanConfigurations = @()
# [SiteTreeCache]::SiteTree = $null
# }
class DeploymentCache {
    static [object[]] $Deployments = @()

    static [object] Get([string]$resourceId) {
        return @([DeploymentCache]::Deployments | Where-Object {$_.ResourceId -eq $resourceId})[0]
    }

    static [object] Get() {
        return [DeploymentCache]::Deployments
    }

    static [void] Set([object]$deployment) {
        [DeploymentCache]::Deployments += $deployment
    }

    static [void] Init() {
        [DeploymentCache]::Deployments = @()
    }
}
class ScanConfigurationCache {
    static [object[]] $ScanConfigurations = @()

    static [object] Get([string]$name) {
        return @([ScanConfigurationCache]::ScanConfigurations | Where-Object { $_.name -eq $name })[0]
    }

    static [void] Reload() {
        [ScanConfigurationCache]::Init()
    }

    static [void] Init() {
        [ScanConfigurationCache]::ScanConfigurations = @(Get-BurpSuiteScanConfiguration)
    }
}
class ScheduleItemCache {
    static [object[]] $ScheduleItems

    static [object] Get([string]$siteId) {
        return @([ScheduleItemCache]::ScheduleItems | Where-Object { $_.site.id -eq $siteId })
    }

    static [void] Reload() {
        [ScheduleItemCache]::Init()
    }

    static [void] Init() {
        [ScheduleItemCache]::ScheduleItems = @(Get-BurpSuiteScheduleItem -Fields id, schedule, site)
    }
}
class SiteTreeCache {
    static [object] $SiteTree

    static [object] Get([string]$parentId, [string]$name, [string]$type) {
        if ($type -eq 'Folders') {
            return @([SiteTreeCache]::SiteTree.Folders | Where-Object { ($_.name -eq $name) -and ($_.parent_id -eq $parentId) })[0]
        }
        return @([SiteTreeCache]::SiteTree.Sites | Where-Object { ($_.name -eq $name) -and ($_.parent_id -eq $parentId) })[0]
    }

    static [object] Get([string]$id, [string]$type) {
        if ($type -eq 'Folders') {
            return @([SiteTreeCache]::SiteTree.Folders | Where-Object { $_.id -eq $id })[0]
        }
        return @([SiteTreeCache]::SiteTree.Sites | Where-Object { $_.id -eq $id })[0]
    }

    static [void] Reload() {
        [SiteTreeCache]::Init()
    }

    static [void] Init() {
        [SiteTreeCache]::SiteTree = Get-BurpSuiteSiteTree
    }
}
class Deployment {
    [string] $Id
    [string] $ResourceId
    [string] $ProvisioningState
    [object] $Properties
    [string] $ProvisioningError
}
class Resource {
    [string] $ResourceId
    [string] $ResourceType
    [string] $Name
    [object] $Properties
    [string[]] $DependsOn
}
class ProvisioningState {
    static [string] $Succeeded = 'Succeeded'
    static [string] $Error = 'Error'
}
class Util {
    static [object] GetResourceId([string]$name, [string]$type) {
        $resourceId = @($type.TrimEnd("/"), $name.TrimStart("/")) -join "/"
        return $resourceId
    }

    static [object] GetResourceId([string]$name, [string]$type, [string]$parentType) {
        $resourceId = @($parentType.TrimEnd("/"), (($name.TrimStart("/")) -split "/")[0], $type.TrimEnd("/"), (($name.TrimStart("/")) -split "/")[-1]) -join "/"
        return $resourceId
    }

    static [object] GetResourceType([string]$type) {
        $resourceType = $type.TrimEnd("/")
        return $resourceType
    }

    static [object] GetResourceType([string]$type, [string]$parentType) {
        $resourceType = @($parentType.TrimEnd("/"), $type.TrimStart("/")) -join "/"
        return $resourceType
    }

    static [object] GetResourceName([string]$name) {
        $resourceName = (($name.TrimStart("/")) -split "/")[-1]
        return $resourceName
    }
}
# Idea from http://stackoverflow.com/questions/7468707/deep-copy-a-dictionary-hashtable-in-powershell
# borrowed from http://stackoverflow.com/questions/8982782/does-anyone-have-a-dependency-graph-and-topological-sorting-code-snippet-for-pow
function _cloneObject {
    [cmdletbinding()]
    param(
        [object] $InputObject
    )

    $memoryStream = new-object IO.MemoryStream

    $binaryFormatter = new-object Runtime.Serialization.Formatters.Binary.BinaryFormatter
    $binaryFormatter.Serialize($memoryStream, $InputObject)

    $memoryStream.Position = 0

    $binaryFormatter.Deserialize($memoryStream)
}

function _convertToHashtable {
    [OutputType([System.Collections.Hashtable])]
    [cmdletbinding()]
    param(
        [object]$InputObject
    )

    $ht = @{}

    $InputObject | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | ForEach-Object {
        $ht.$_ = $InputObject.$_
    }

    $ht
}

function _createTempFile {
    [cmdletbinding()]
    param(
        [object] $InputObject
    )

    $tempFile = New-TemporaryFile
    if (-not ([string]::IsNullOrEmpty($InputObject))) {
        Out-File -NoNewline -InputObject $InputObject -FilePath $tempFile
    }
    $tempFile
}

function _sortDeployment {
    [OutputType([System.Object[]])]
    [cmdletbinding()]
    param(
        [object[]] $Resources
    )

    $order = @{}

    foreach ($resource in $Resources) {
        if ($resource.dependsOn) {
            if(-not $order.ContainsKey($resource.ResourceId)) {
                $order.add($resource.ResourceId, $resource.dependsOn)
            }
        }
    }

    if($order.Keys.Count -gt 0) {
        $deployOrder = _sortTopologically $order
        _sortWithCustomList -InputObject $Resources -Property ResourceId -CustomList $deployOrder
    } else {
        $Resources
    }
}

# Thanks to http://stackoverflow.com/questions/8982782/does-anyone-have-a-dependency-graph-and-topological-sorting-code-snippet-for-pow
# Input is a hashtable of @{ID = @(Depended,On,IDs);...}
function _sortTopologically {
    [OutputType([System.Collections.ArrayList])]
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [hashtable] $EdgeList
    )

    # Make sure we can use HashSet
    Add-Type -AssemblyName System.Core

    # Clone it so as to not alter original
    $currentEdgeList = [hashtable] (_cloneObject $EdgeList)

    # algorithm from http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
    $topologicallySortedElements = New-Object System.Collections.ArrayList
    $setOfAllNodesWithNoIncomingEdges = New-Object System.Collections.Queue

    $fasterEdgeList = @{}

    # Keep track of all nodes in case they put it in as an edge destination but not source
    $allTheNodes = New-Object -TypeName System.Collections.Generic.HashSet[object] -ArgumentList (, [object[]] $currentEdgeList.Keys)

    foreach ($currentNode in $currentEdgeList.Keys) {
        $currentDestinationNodes = [array] $currentEdgeList[$currentNode]
        if ($currentDestinationNodes.Length -eq 0) {
            $setOfAllNodesWithNoIncomingEdges.Enqueue($currentNode)
        }

        foreach ($currentDestinationNode in $currentDestinationNodes) {
            if (!$allTheNodes.Contains($currentDestinationNode)) {
                [void] $allTheNodes.Add($currentDestinationNode)
            }
        }

        # Take this time to convert them to a HashSet for faster operation
        $currentDestinationNodes = New-Object -TypeName System.Collections.Generic.HashSet[object] -ArgumentList (, [object[]] $currentDestinationNodes )
        [void] $fasterEdgeList.Add($currentNode, $currentDestinationNodes)
    }

    # Now let's reconcile by adding empty dependencies for source nodes they didn't tell us about
    foreach ($currentNode in $allTheNodes) {
        if (!$currentEdgeList.ContainsKey($currentNode)) {
            [void] $currentEdgeList.Add($currentNode, (New-Object -TypeName System.Collections.Generic.HashSet[object]))
            $setOfAllNodesWithNoIncomingEdges.Enqueue($currentNode)
        }
    }

    $currentEdgeList = $fasterEdgeList

    while ($setOfAllNodesWithNoIncomingEdges.Count -gt 0) {
        $currentNode = $setOfAllNodesWithNoIncomingEdges.Dequeue()
        [void] $currentEdgeList.Remove($currentNode)
        [void] $topologicallySortedElements.Add($currentNode)

        foreach ($currentEdgeSourceNode in $currentEdgeList.Keys) {
            $currentNodeDestinations = $currentEdgeList[$currentEdgeSourceNode]
            if ($currentNodeDestinations.Contains($currentNode)) {
                [void] $currentNodeDestinations.Remove($currentNode)

                if ($currentNodeDestinations.Count -eq 0) {
                    [void] $setOfAllNodesWithNoIncomingEdges.Enqueue($currentEdgeSourceNode)
                }
            }
        }
    }

    if ($currentEdgeList.Count -gt 0) {
        throw "Graph has at least one cycle!"
    }

    return $topologicallySortedElements
}

# Thanks to http://stackoverflow.com/questions/8982782/does-anyone-have-a-dependency-graph-and-topological-sorting-code-snippet-for-pow
# Input is a hashtable of @{ID = @(Depended,On,IDs);...}
function _sortWithCustomList {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    Param (
        [parameter(ValueFromPipeline=$true)]
        [PSObject]
        $InputObject,

        [parameter(Position=1)]
        [String]
        $Property,

        [parameter()]
        [Object[]]
        $CustomList
    )

    begin {
        # convert customList (array) to hash
        $hash = @{}
        $rank = 0
        $customList | Select-Object -Unique | ForEach-Object {
            $key = $_
            $hash.Add($key, $rank)
            $rank++
        }

        # create script block for sorting
        # items not in custom list will be last in sort order
        $sortOrder = {
            $key = if ($Property) { $_.$Property } else { $_ }
            $rank = $hash[$key]
            if ($null -ne $rank) {
                $rank
            } else {
                [System.Double]::PositiveInfinity
            }
        }

        # create a place to collect objects from pipeline
        # (I don't know how to match behavior of Sort's InputObject parameter)
        $objects = @()
    }

    process {
        $objects += $InputObject
    }

    end {
        $objects | Sort-Object -Property $sortOrder
    }
}

function _tryGetProperty {
    param(
        [object] $InputObject,
        [string] $PropertyName
    )

    if ((@($InputObject.PSObject.Properties.Match($PropertyName)).Count -gt 0) -and ($null -ne $InputObject.$PropertyName)) {
        return $InputObject.$PropertyName
    }

    return $null
}
function _testIsExpression {
    [cmdletbinding()]
    param(
        [object] $InputString
    )

    $InputString -match '^\[.+\]$'
}

function _resolveExpression {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [cmdletbinding()]
    param(
        [object] $inputString,
        [hashtable] $variables,
        [object[]] $resources
    )

    if (-not (_testIsExpression -InputString $inputString)) {
        return $inputString
    }

    $safeCommands = @('variables', 'concat', 'resourceId', 'reference')

    $parsedString = (($inputString -replace '^\[', '') -replace '\]$', '')

    $expression = [scriptblock]::Create("return ($parsedString)")

    function variables([string]$name) { ($variables[$name]) }
    function concat([string[]]$arguments) { ($arguments -join '') }
    function resourceId([string[]]$segments) {
        $resourceId = (
            $segments |
            ForEach-Object {
                if (-not ([string]::IsNullOrEmpty($_))) {
                    $_.TrimEnd("/")
                }
            }
        ) -join '/'
        $resourceId
    }
    function reference([string]$resourceId) { $resources | Where-Object { $_.ResourceId -eq $resourceId } }

    $ast = [System.Management.Automation.Language.Parser]::ParseInput($parsedString, [ref]$null, [ref]$null)

    $commandsAst = $ast.FindAll( {
            ($args[0] -is [System.Management.Automation.Language.CommandAst]) `
                -and $args[0].GetCommandName() -notin $safeCommands
        }, $true)

    if ($commandsAst.Count -eq 0) {
        & $expression
    }
}
function Get-BurpSuiteResource {
    [CmdletBinding(HelpUri = 'https://github.com/juniinacio/BurpSuiteDeploy', ConfirmImpact = 'Low')]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript( { Test-Path -Path $_ -PathType Leaf -ErrorAction Stop })]
        [string[]] $TemplateFile
    )

    begin {
    }

    process {
        try {
            $resources = foreach($path in $TemplateFile) {
                $path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)

                $template = ConvertFrom-Json -InputObject (Get-Content -Path $path -Raw | Out-String)

                foreach($resource in $template.resources) {
                    [Resource]@{
                        ResourceId = [Util]::GetResourceId($resource.name, $resource.type)
                        ResourceType = [Util]::GetResourceType($resource.type)
                        Name = $resource.name
                        Properties = $resource.Properties
                        DependsOn = $resource.dependsOn
                    }

                    if ($null -ne (_tryGetProperty -InputObject $resource -PropertyName 'resources')) {
                        foreach($childResource in $resource.resources) {
                            [Resource]@{
                                ResourceId = [Util]::GetResourceId($childResource.name, $childResource.type, $resource.type)
                                ResourceType = [Util]::GetResourceType($childResource.type, $resource.type)
                                Name = [Util]::GetResourceName($childResource.name)
                                Properties = $childResource.Properties
                                DependsOn = $childResource.dependsOn
                            }
                        }
                    }
                }
            }

            if (-not $resources) {
                throw "No resources processed. Something went wrong."
            }

            _sortDeployment -Resources $resources
        } catch {
            throw
        }
    }

    end {
    }
}
function Invoke-BurpSuiteDeploy {
    [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://github.com/juniinacio/BurpSuiteDeploy', ConfirmImpact = 'Medium')]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript( { Test-Path -Path $_ -PathType Leaf -ErrorAction Stop })]
        [string[]] $TemplateFile,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Uri,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $APIKey
    )

    begin {
        try {
            Connect-BurpSuite -Uri $Uri -APIKey $APIKey
        } catch {
            throw
        }
    }

    process {
        try {
            if ($PSCmdlet.ShouldProcess("Deploy", $TemplateFile)) {

                $resources = Get-BurpSuiteResource -TemplateFile $TemplateFile

                $deployments = $resources | Invoke-BurpSuiteResource -Confirm:$false

                if (@($deployments).ProvisioningState -contains [ProvisioningState]::Error) {
                    Write-Error -Message "Provisioning of one or more resources completed with errors."
                }

                $deployments
            }
        } catch {
            throw
        }
    }

    end {
        try {
            Disconnect-BurpSuite
        } catch {
            throw
        }
    }
}
function Invoke-BurpSuiteResource {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [CmdletBinding(SupportsShouldProcess = $true,
        HelpUri = 'https://github.com/juniinacio/BurpSuiteDeploy',
        ConfirmImpact = 'Medium')]
    Param (
        [parameter(ValueFromPipeline = $True, Mandatory = $True)]
        [object]$InputObject
    )

    begin {
        [SiteTreeCache]::Init()
        [ScanConfigurationCache]::Init()
        [ScheduleItemCache]::Init()
    }

    process {
        try {
            Write-Verbose "Deploying resource $($InputObject.ResourceId)`..."

            if ($PSCmdlet.ShouldProcess("Deploy", $InputObject.ResourceId)) {
                switch ($InputObject.ResourceType) {
                    'BurpSuite/Sites' {
                        $resource = [SiteTreeCache]::Get(0, $InputObject.Name, 'Sites')
                        if ($null -eq $resource) {

                            Write-Verbose "Creating site $($InputObject.Name)`..."

                            $scanConfigurationIds = @()
                            foreach ($scanConfigurationId in $InputObject.Properties.scanConfigurationIds) {
                                if ((_testIsExpression -InputString $scanConfigurationId)) {
                                    $resolvedScanConfigurationId = _resolveExpression -inputString $scanConfigurationId -variables @{} -resources ([DeploymentCache]::Deployments)
                                    if ($null -eq $resolvedScanConfigurationId) {
                                        throw "Could not resolve dependency expression $scanConfigurationId`."
                                    }
                                    $scanConfigurationIds += $resolvedScanConfigurationId
                                } else {
                                    $scanConfigurationIds += $scanConfigurationId
                                }
                            }

                            $parameters = @{
                                ParentId             = "0"
                                Name                 = $InputObject.Name
                                Scope                = $InputObject.Properties.scope
                                ScanConfigurationIds = $scanConfigurationIds
                            }

                            if ($null -ne ($InputObject.Properties.emailRecipients)) {
                                $parameters.EmailRecipients = $InputObject.Properties.emailRecipients
                            }

                            if ($null -ne ($InputObject.Properties.applicationLogins)) {
                                if ($null -ne ($InputObject.Properties.applicationLogins.loginCredentials)) {
                                    $loginCredentials = @()

                                    foreach ($loginCredential in $InputObject.Properties.applicationLogins.loginCredentials) {
                                        $loginCredentials += [PSCustomObject]@{ Label = $loginCredential.Label; Credential = (New-Object System.Management.Automation.PSCredential ($loginCredential.Username, $(ConvertTo-SecureString $loginCredential.Password -AsPlainText -Force))) }
                                    }

                                    $parameters.LoginCredentials = $loginCredentials
                                }

                                if ($null -ne ($InputObject.Properties.applicationLogins.recordedLogins)) {
                                    $recordedLogins = @()

                                    foreach ($recordedLogin in $InputObject.Properties.applicationLogins.recordedLogins) {
                                        $recordedLogins += [PSCustomObject]@{ Label = $recordedLogin.Label; FilePath = (_createTempFile -InputObject $recordedLogin.script).FullName }
                                    }

                                    $parameters.RecordedLogins = $recordedLogins
                                }
                            }

                            $resource = New-BurpSuiteSite @parameters

                            [SiteTreeCache]::Reload()
                        } else {

                            Write-Verbose "Updating site $($InputObject.Name)`..."

                            if ($null -ne ($InputObject.Properties.scope)) {
                                Write-Verbose " Updating site scopes..."
                                Update-BurpSuiteSiteScope -SiteId $resource.Id -IncludedUrls $InputObject.Properties.scope.includedUrls -ExcludedUrls $InputObject.Properties.scope.excludedUrls
                                Start-Sleep -Seconds 1
                            }

                            if ($null -ne ($InputObject.Properties.scanConfigurationIds)) {
                                Write-Verbose " Updating scan configuration..."
                                $scanConfigurationIds = @()
                                foreach ($scanConfigurationId in $InputObject.Properties.scanConfigurationIds) {
                                    if ((_testIsExpression -InputString $scanConfigurationId)) {
                                        $resolvedScanConfigurationId = _resolveExpression -inputString $scanConfigurationId -variables @{} -resources ([DeploymentCache]::Deployments)
                                        if ($null -eq $resolvedScanConfigurationId) {
                                            throw "Could not resolve dependency expression $scanConfigurationId`."
                                        }
                                        $scanConfigurationIds += $resolvedScanConfigurationId
                                    } else {
                                        $scanConfigurationIds += $scanConfigurationId
                                    }
                                }
                                Update-BurpSuiteSiteScanConfiguration -Id $resource.Id -ScanConfigurationIds $scanConfigurationIds
                                Start-Sleep -Seconds 1
                            }

                            [SiteTreeCache]::Reload()
                            $resource = [SiteTreeCache]::Get(0, $InputObject.Name, 'Sites')

                            if ($null -ne ($InputObject.Properties.applicationLogins)) {
                                if ($null -ne ($InputObject.Properties.applicationLogins.loginCredentials)) {
                                    Write-Verbose " Updating application logins..."
                                    foreach ($loginCredential in $InputObject.Properties.applicationLogins.loginCredentials) {
                                        $appPass = ConvertTo-SecureString -String $loginCredential.password -AsPlainText -Force
                                        $appCredential = New-Object -TypeName PSCredential -ArgumentList $loginCredential.username, $appPass
                                        $appLogin = $resource.application_logins.login_credentials | Where-Object { $_.label -eq $loginCredential.label }
                                        if ($null -eq $appLogin) {
                                            New-BurpSuiteSiteLoginCredential -SiteId $resource.id -Label $loginCredential.label -Credential $appCredential | Out-Null
                                        } else {
                                            Update-BurpSuiteSiteLoginCredential -Id $appLogin.id -Credential $appCredential
                                        }
                                        Start-Sleep -Seconds 1
                                    }
                                }

                                if ($null -ne ($InputObject.Properties.applicationLogins.recordedLogins)) {
                                    Write-Verbose " Updating recorded logins..."
                                    foreach ($recordedLogin in $InputObject.Properties.applicationLogins.recordedLogins) {
                                        $appLogin = $resource.application_logins.recorded_logins | Where-Object { $_.label -eq $recordedLogin.label }
                                        if ($null -eq $appLogin) {
                                            New-BurpSuiteSiteRecordedLogin -SiteId $resource.id -Label $recordedLogin.label -FilePath (_createTempFile -InputObject $recordedLogin.script).FullName | Out-Null
                                        }
                                        Start-Sleep -Seconds 1
                                    }
                                }
                            }

                            if ($null -ne ($InputObject.Properties.emailRecipients)) {
                                Write-Verbose " Updating email recipients..."
                                foreach ($emailRecipient in $InputObject.Properties.emailRecipients) {
                                    $emailRec = $resource.email_recipients | Where-Object { $_.email -eq $emailRecipient.email }
                                    if ($null -eq $emailRec) {
                                        New-BurpSuiteSiteEmailRecipient -SiteId $resource.id -EmailRecipient $emailRecipient.email | Out-Null
                                    } else {
                                        Update-BurpSuiteSiteEmailRecipient -Id $emailRec.id -Email $emailRecipient.email
                                    }
                                    Start-Sleep -Seconds 1
                                }
                            }
                        }
                    }

                    'BurpSuite/Folders' {
                        $resource = [SiteTreeCache]::Get(0, $InputObject.Name, 'Folders')
                        if ($null -eq $resource) {
                            Write-Verbose "Creating folder $($InputObject.Name)`..."
                            $resource = New-BurpSuiteFolder -ParentId 0 -Name $InputObject.Name
                            [SiteTreeCache]::Reload()
                            Start-Sleep -Seconds 1
                        }
                    }

                    'BurpSuite/Folders/Sites' {
                        $parentResourceId = ($InputObject.ResourceId -split '/' | Select-Object -First 3) -join '/'
                        $parentResource = [DeploymentCache]::Get($parentResourceId)

                        if ($null -ne $parentResource) {
                            $resource = [SiteTreeCache]::Get($parentResource.Id, $InputObject.Name, 'Sites')
                            if ($null -eq $resource) {
                                Write-Verbose "Creating site $($InputObject.Name), parent id $($parentResource.Id)`..."

                                $scanConfigurationIds = @()
                                foreach ($scanConfigurationId in $InputObject.Properties.scanConfigurationIds) {
                                    if ((_testIsExpression -InputString $scanConfigurationId)) {
                                        $resolvedScanConfigurationId = _resolveExpression -inputString $scanConfigurationId -variables @{} -resources ([DeploymentCache]::Deployments)
                                        if ($null -eq $resolvedScanConfigurationId) {
                                            throw "Could not resolve dependency expression $scanConfigurationId`."
                                        }
                                        $scanConfigurationIds += $resolvedScanConfigurationId
                                    } else {
                                        $scanConfigurationIds += $scanConfigurationId
                                    }
                                }

                                $parameters = @{
                                    ParentId             = $parentResource.Id
                                    Name                 = $InputObject.Name
                                    Scope                = $InputObject.Properties.scope
                                    ScanConfigurationIds = $scanConfigurationIds
                                }

                                if ($null -ne ($InputObject.Properties.emailRecipients)) {
                                    $parameters.EmailRecipients = $InputObject.Properties.emailRecipients
                                }

                                if ($null -ne ($InputObject.Properties.applicationLogins)) {
                                    if ($null -ne ($InputObject.Properties.applicationLogins.loginCredentials)) {
                                        $loginCredentials = @()

                                        foreach ($loginCredential in $InputObject.Properties.applicationLogins.loginCredentials) {
                                            $loginCredentials += [PSCustomObject]@{ Label = $loginCredential.Label; Credential = (New-Object System.Management.Automation.PSCredential ($loginCredential.Username, $(ConvertTo-SecureString $loginCredential.Password -AsPlainText -Force))) }
                                        }

                                        $parameters.LoginCredentials = $loginCredentials
                                    }

                                    if ($null -ne ($InputObject.Properties.applicationLogins.recordedLogins)) {
                                        $recordedLogins = @()

                                        foreach ($recordedLogin in $InputObject.Properties.applicationLogins.recordedLogins) {
                                            $recordedLogins += [PSCustomObject]@{ Label = $recordedLogin.Label; FilePath = (_createTempFile -InputObject $recordedLogin.script).FullName }
                                        }

                                        $parameters.RecordedLogins = $recordedLogins
                                    }
                                }

                                $resource = New-BurpSuiteSite @parameters

                                [SiteTreeCache]::Reload()
                            } else {
                                Write-Verbose "Updating site $($InputObject.Name), parent id $($parentResource.Id)`..."

                                if ($null -ne ($InputObject.Properties.scope)) {
                                    Write-Verbose " Updating site scopes..."
                                    Update-BurpSuiteSiteScope -SiteId $resource.Id -IncludedUrls $InputObject.Properties.scope.includedUrls -ExcludedUrls $InputObject.Properties.scope.excludedUrls
                                    Start-Sleep -Seconds 1
                                }

                                if ($null -ne ($InputObject.Properties.scanConfigurationIds)) {
                                    Write-Verbose " Updating scan configurations..."
                                    $scanConfigurationIds = @()
                                    foreach ($scanConfigurationId in $InputObject.Properties.scanConfigurationIds) {
                                        if ((_testIsExpression -InputString $scanConfigurationId)) {
                                            $resolvedScanConfigurationId = _resolveExpression -inputString $scanConfigurationId -variables @{} -resources ([DeploymentCache]::Deployments)
                                            if ($null -eq $resolvedScanConfigurationId) {
                                                throw "Could not resolve dependency expression $scanConfigurationId`."
                                            }
                                            $scanConfigurationIds += $resolvedScanConfigurationId
                                        } else {
                                            $scanConfigurationIds += $scanConfigurationId
                                        }
                                    }
                                    Update-BurpSuiteSiteScanConfiguration -Id $resource.Id -ScanConfigurationIds $scanConfigurationIds
                                    Start-Sleep -Seconds 1
                                }

                                [SiteTreeCache]::Reload()
                                $resource = [SiteTreeCache]::Get($parentResource.Id, $InputObject.Name, 'Sites')

                                if ($null -ne ($InputObject.Properties.applicationLogins)) {
                                    if ($null -ne ($InputObject.Properties.applicationLogins.loginCredentials)) {
                                        Write-Verbose " Updating application logins..."
                                        foreach ($loginCredential in $InputObject.Properties.applicationLogins.loginCredentials) {
                                            $appPass = ConvertTo-SecureString -String $loginCredential.password -AsPlainText -Force
                                            $appCredential = New-Object -TypeName PSCredential -ArgumentList $loginCredential.username, $appPass
                                            $appLogin = $resource.application_logins.login_credentials | Where-Object { $_.label -eq $loginCredential.label }
                                            if ($null -eq $appLogin) {
                                                New-BurpSuiteSiteLoginCredential -SiteId $resource.id -Label $loginCredential.label -Credential $appCredential | Out-Null
                                            } else {
                                                Update-BurpSuiteSiteLoginCredential -Id $appLogin.id -Credential $appCredential
                                            }
                                            Start-Sleep -Seconds 1
                                        }
                                    }

                                    if ($null -ne ($InputObject.Properties.applicationLogins.recordedLogins)) {
                                        Write-Verbose " Updating recorded logins..."
                                        foreach ($recordedLogin in $InputObject.Properties.applicationLogins.recordedLogins) {
                                            $appLogin = $resource.application_logins.recorded_logins | Where-Object { $_.label -eq $recordedLogin.label }
                                            if ($null -eq $appLogin) {
                                                New-BurpSuiteSiteRecordedLogin -SiteId $resource.id -Label $recordedLogin.label -FilePath (_createTempFile -InputObject $recordedLogin.script).FullName | Out-Null
                                            }
                                            Start-Sleep -Seconds 1
                                        }
                                    }
                                }

                                if ($null -ne ($InputObject.Properties.emailRecipients)) {
                                    Write-Verbose " Updating email recipients..."
                                    foreach ($emailRecipient in $InputObject.Properties.emailRecipients) {
                                        $emailRec = $resource.email_recipients | Where-Object { $_.email -eq $emailRecipient.email }
                                        if ($null -eq $emailRec) {
                                            New-BurpSuiteSiteEmailRecipient -SiteId $resource.id -EmailRecipient $emailRecipient.email | Out-Null
                                        } else {
                                            Update-BurpSuiteSiteEmailRecipient -Id $emailRec.id -Email $emailRecipient.email
                                        }
                                        Start-Sleep -Seconds 1
                                    }
                                }
                            }
                        } else {
                            throw "Resource $($InputObject.ResourceId) parent could not be determined."
                        }
                    }

                    'BurpSuite/ScanConfigurations' {
                        $tempFile = _createTempFile -InputObject $InputObject.Properties.scanConfigurationFragmentJson

                        $resource = [ScanConfigurationCache]::Get($InputObject.Name)
                        if ($null -eq $resource) {
                            Write-Verbose "Creating scan configuration $($InputObject.Name)`..."
                            $resource = New-BurpSuiteScanConfiguration -Name $InputObject.Name -FilePath $tempFile.FullName
                            [ScanConfigurationCache]::Reload()
                        } else {
                            Write-Verbose "Updating scan configuration $($InputObject.Name)`..."
                            Update-BurpSuiteScanConfiguration -Id $resource.Id -FilePath $tempFile.FullName
                        }
                        Start-Sleep -Seconds 1
                    }

                    'BurpSuite/ScheduleItems' {
                        $siteId = $InputObject.Properties.siteId
                        if ((_testIsExpression -InputString $siteId)) {
                            $resolvedSiteId = _resolveExpression -inputString $siteId -variables @{} -resources ([DeploymentCache]::Deployments)
                            if ($null -eq $resolvedSiteId) {
                                throw "Could not resolve expression $siteId`."
                            }
                            $siteId = $resolvedSiteId
                        }

                        $site = [SiteTreeCache]::Get($siteId, 'Sites')
                        if ($null -eq $site) {
                            throw "Could not find site with resource id $siteId`."
                        }

                        $scanConfigurationIds = _tryGetProperty -InputObject $InputObject.Properties -PropertyName 'scanConfigurationIds'
                        if ($null -eq $scanConfigurationIds) {
                            $scanConfigurationIds = $site.scan_configurations.id
                        }

                        $resolvedScanConfigurationIds = @()
                        foreach ($scanConfigurationId in $scanConfigurationIds) {
                            if ((_testIsExpression -InputString $scanConfigurationId)) {
                                $resolvedScanConfigurationId = _resolveExpression -inputString $scanConfigurationId -variables @{} -resources ([DeploymentCache]::Deployments)
                                if ($null -eq $resolvedScanConfigurationId) {
                                    throw "Could not resolve expression $scanConfigurationId`."
                                }
                                $resolvedScanConfigurationIds += $resolvedScanConfigurationId
                            } else {
                                $resolvedScanConfigurationIds += $scanConfigurationId
                            }
                        }

                        $recurrenceRule = _tryGetProperty -InputObject $InputObject.Properties.schedule -PropertyName 'rRule'
                        if (-not ([string]::IsNullOrEmpty($recurrenceRule))) {
                            $resource = [ScheduleItemCache]::Get($siteId) | Where-Object { $_.schedule.rrule -eq $recurrenceRule } | Select-Object -First 1
                        } else {
                            $resource = $null
                        }

                        if ($null -eq $resource) {
                            Write-Verbose "Creating schedule item $($InputObject.Name), site $siteId`..."

                            $parameters = @{}

                            $recurrenceRule = _tryGetProperty -InputObject $InputObject.Properties.schedule -PropertyName 'rRule'
                            if (-not ([string]::IsNullOrEmpty($recurrenceRule))) {
                                $parameters.rrule = $recurrenceRule
                            }

                            $initialRunTime = _tryGetProperty -InputObject $InputObject.Properties.schedule -PropertyName 'initialRunTime'
                            if (-not ([string]::IsNullOrEmpty($initialRunTime))) {
                                $dateTimeNow = Get-Date
                                $initialRunTimeDate = [DateTime]::SpecifyKind($initialRunTime, [DateTimeKind]::Utc)
                                # Correct initial run time if date is in past
                                if ($initialRunTimeDate -lt $dateTimeNow) {
                                    $dateTimeTomorrow = $dateTimeNow.AddHours(24)
                                    $newInitialRunTimeDate = (Get-Date -Day $dateTimeTomorrow.Day -Month $dateTimeTomorrow.Month -Year $dateTimeTomorrow.Year -Hour $initialRunTimeDate.Hour -Minute $initialRunTimeDate.Minute -Second $initialRunTimeDate.Second)
                                    $newInitialRunTimeDate = [DateTime]::SpecifyKind($newInitialRunTimeDate, [DateTimeKind]::Utc)
                                    $initialRunTime = Get-Date -Date $newInitialRunTimeDate -Format o
                                }
                                $parameters.initialRunTime = $initialRunTime
                            }

                            $schedule = [PSCustomObject]$parameters

                            $resource = New-BurpSuiteScheduleItem -SiteId $siteId -ScanConfigurationIds $resolvedScanConfigurationIds -Schedule $schedule

                            [ScheduleItemCache]::Reload()
                        }

                        Start-Sleep -Seconds 1
                    }

                    default {
                        throw "Unknown resource type."
                    }
                }

                $deployment = [Deployment]@{
                    Id                = $resource.Id
                    ResourceId        = $InputObject.ResourceId
                    ProvisioningState = [ProvisioningState]::Succeeded
                    Properties        = $resource
                }
            }
        } catch {
            $deployment = [Deployment]@{
                Id                = $resource.Id
                ResourceId        = $InputObject.ResourceId
                ProvisioningState = [ProvisioningState]::Error
                ProvisioningError = $_.Exception.Message.ToString()
                Properties        = $resource
            }
        }

        [DeploymentCache]::Set($deployment)

        $deployment
    }

    end {
        [DeploymentCache]::Init()
    }
}