Functions/Invoke-PasmBlueprint.ps1

#Requires -Version 5.1
using namespace System.IO
using namespace System.Collections.Generic

function Invoke-PasmBlueprint {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo[]])]
    param (
        # Specify the path to the Yaml template.
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('file')]
        [ValidateNotNullOrEmpty()]
        [string[]]$FilePath = $($PWD, $('{0}.yml' -f [Pasm.Template.Name]::outline) -join [path]::DirectorySeparatorChar),

        # Specify the output file name.
        [Parameter(Mandatory = $false)]
        [Alias('out')]
        [ValidateNotNullOrEmpty()]
        [string[]]$OutputFileName = $('{0}.yml' -f [Pasm.Template.Name]::blueprint)
    )

    begin {
        try {
            Set-StrictMode -Version Latest

            # Load helper functions
            . $($PSScriptRoot, 'Helpers', 'Helpers.ps1' -join [path]::DirectorySeparatorChar)

            # Implicitly run the validator process.
            Invoke-PasmValidation -FilePath $filePath | Out-Null

            # Datetime variables
            $published = Get-AWSPublicIpAddressRange -OutputPublicationDate
            $now = [datetime]::Now.ToUniversalTime()
            $datetimeFormat = 'yyyyMMddHHmmss'

            # Validation that the number of parameters match
            if ($filePath.Length -ne $outputFileName.Length) {
                throw [InvalidOperationException]::new('The length of the ''FilePath'' and the length of the ''OutputFileName'' must be the same.')
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }

    process {
        try {
            $i = 0
            foreach ($file in $filePath) {
                # Load outline file
                $obj = Import-PasmFile -FilePath $file -Ordered

                # Create output file path
                $outputFilePath = $([path]::GetDirectoryName($file), $OutputFileName[$i] -join [path]::DirectorySeparatorChar)

                # If the blueprint file already exists, load it
                # The original blueprint file will be used to update the metadata section and resource ids
                # If the blueprint file does not yet exist, it cannot be loaded here
                $update = Test-Path -LiteralPath $outputFilePath
                if ($update) {
                    $dest = Import-PasmFile -FilePath $outputFilePath -Ordered
                }

                # Create metadata section
                $metadata = [ordered]@{}
                $metadata.UpdateNumber = if ($update) { [int]$dest.Metadata.UpdateNumber + 1 } else { 1 }
                $metadata.DeployNumber = if ($update) { [int]$dest.Metadata.DeployNumber } else { 0 }
                $metadata.CleanUpNumber = if ($update) { [int]$dest.Metadata.CleanUpNumber } else { 0 }
                $metadata.PublishedAt = $published
                $metadata.CreatedAt = if ($update) { ([datetime]$dest.Metadata.CreatedAt).ToUniversalTime() } else { $now }
                $metadata.UpdatedAt = $now
                $metadata.DeployedAt = if ($update) { ([datetime]$dest.Metadata.DeployedAt).ToUniversalTime() } else { [datetime]::UnixEpoch }
                $metadata.CleandAt = if ($update) { ([datetime]$dest.Metadata.CleandAt).ToUniversalTime() } else { [datetime]::UnixEpoch }

                # Rresource variables
                $resource = $obj.Resource
                if ($resource.Contains('SecurityGroup')) { $securityGroup = $obj.Resource.SecurityGroup }
                if ($resource.Contains('NetworkAcl')) { $networkAcl = $obj.Resource.NetworkAcl }
                if ($resource.Contains('PrefixList')) { $prefixList = $obj.Resource.PrefixList }

                # Create outer container
                $parent = [ordered]@{}
                $parent.Common = $obj.Common
                $parent.Resource = [ordered]@{}

                # Parse the outline file and convert the 'Rules' section to CIDR units: 'SecurityGroup'
                if ($resource.Contains('SecurityGroup')) {
                    $sgContainer = [list[object]]::new()

                    # Multiple resource definitions are allowed, so process them one by one
                    foreach ($sg in $securityGroup) {
                        $sgRulesContainer = [list[object]]::new()

                        $obj = [ordered]@{}
                        $obj.ResourceName = $sg.ResourceName
                        $obj.ResourceId = if ($update) { $dest.Resource.SecurityGroup.ResourceId } else { 'not-deployed' }
                        $obj.VpcId = $sg.VpcId
                        $obj.MaxEntry = if ($sg.Contains('MaxEntry')) { $sg.MaxEntry } else { 60 }
                        $obj.IPv4Entry = $null
                        $obj.IPv6Entry = $null
                        $obj.FlowDirection = if ($sg.Contains('FlowDirection')) { $sg.FlowDirection } else { 'Ingress' }
                        $obj.Description = $sg.Description

                        # Multiple rules definitions are allowed, so process them one by one
                        foreach ($rule in $sg.Rules) {
                            $sgRangesContainer = [list[object]]::new()

                            $o = [ordered]@{}
                            $o.Id = $rule.Id
                            $o.ServiceKey = $rule.ServiceKey
                            $o.Protocol = $rule.Protocol
                            $o.FromPort = $rule.FromPort
                            $o.ToPort = $rule.ToPort

                            # For each rule, send API to 'ip-ranges.json' to get the IP range
                            $num = 1
                            foreach ($r in $(Get-PasmAWSIpRange $rule -Resource SecurityGroup)) {
                                $range = [ordered]@{}
                                $range.RangeId = $num
                                $range.IpPrefix = $r.IpPrefix
                                $range.IpFormat = $r.IpAddressFormat
                                $range.Region = $r.Region
                                $range.Description = 'Service:{0} Region:{1} Published:{2} Created:{3} Updated:{4}' -f (
                                    $rule.ServiceKey,
                                    $r.Region,
                                    $published.ToString($datetimeFormat),
                                    $(if ($update) { ([datetime]$dest.Metadata.CreatedAt).ToUniversalTime().ToString($datetimeFormat) } else { $now.ToString($datetimeFormat) }),
                                    $now.ToString($datetimeFormat)
                                )
                                $sgRangesContainer.Add($range)
                                $num++
                            }
                            $o.Ranges = $sgRangesContainer
                            $sgRulesContainer.Add($o)
                        }
                        $obj.Rules = $sgRulesContainer

                        # Get the number of registered ipv4 and ipv6 addresses
                        $obj.IPv4Entry = @($obj.Rules.Ranges.Where( { $_.Ipformat -eq 'IPv4' } )).Length
                        $obj.IPv6Entry = @($obj.Rules.Ranges.Where( { $_.Ipformat -eq 'IPv6' } )).Length

                        # Validate the number of entries does not exceed the limit
                        Test-PasmMaxEntry -Entry $obj.IPv4Entry -MaxEntry $obj.MaxEntry -IpFormat 'IPv4' -ResourceType 'SecurityGroup'
                        Test-PasmMaxEntry -Entry $obj.IPv6Entry -MaxEntry $obj.MaxEntry -IpFormat 'IPv6' -ResourceType 'SecurityGroup'

                        $sgContainer.Add($obj)
                    }

                    $parent.Resource.SecurityGroup = $sgContainer
                }

                # Parse the outline file and convert the 'Rules' section to CIDR units: 'NetworkAcl'
                if ($resource.Contains('NetworkAcl')) {
                    $naclContainer = [list[object]]::new()

                    # Multiple resource definitions are allowed, so process them one by one
                    foreach ($nacl in $networkAcl) {
                        $naclRulesContainer = [list[object]]::new()

                        $obj = [ordered]@{}
                        $obj.ResourceName = $nacl.ResourceName
                        $obj.ResourceId = if ($update) { $dest.Resource.NetworkAcl.ResourceId } else { 'not-deployed' }
                        $obj.VpcId = $nacl.VpcId
                        $obj.MaxEntry = if ($nacl.Contains('MaxEntry')) { $nacl.MaxEntry } else { 20 }
                        $obj.IPv4Entry = $null
                        $obj.IPv6Entry = $null
                        $obj.FlowDirection = if ($nacl.Contains('FlowDirection')) { $nacl.FlowDirection } else { 'Ingress' }
                        $obj.EphemeralPort = if ($nacl.Contains('EphemeralPort')) { $nacl.EphemeralPort } else { 'Default' }

                        if ($nacl.Contains('AssociationSubnetId')) {
                            $obj.AssociationSubnetId = $nacl.AssociationSubnetId
                        }

                        $ruleNumber = $nacl.RuleNumber.StartNumber
                        $interval = $nacl.RuleNumber.Interval

                        # Multiple rules definitions are allowed, so process them one by one
                        foreach ($rule in $nacl.Rules) {
                            $naclRangesContainer = [list[object]]::new()

                            $o = [ordered]@{}
                            $o.Id = $rule.Id
                            $o.ServiceKey = $rule.ServiceKey
                            $o.Protocol = $rule.Protocol
                            $o.FromPort = $rule.FromPort
                            $o.ToPort = $rule.ToPort

                            # For each rule, send API to 'ip-ranges.json' to get the IP range
                            $num = 1
                            foreach ($r in $(Get-PasmAWSIpRange $rule -Resource NetworkAcl)) {
                                $range = [ordered]@{}
                                $range.RangeId = $num
                                $range.IpPrefix = $r.IpPrefix
                                $range.IpFormat = $r.IpAddressFormat
                                $range.Region = $r.Region
                                $range.RuleNumber = $ruleNumber

                                $naclRangesContainer.Add($range)
                                $ruleNumber = $ruleNumber + $interval
                                $num++
                            }
                            $o.Ranges = $naclRangesContainer
                            $naclRulesContainer.Add($o)
                        }
                        $obj.Rules = $naclRulesContainer

                        # Get the number of registered ipv4 and ipv6 addresses
                        $obj.IPv4Entry = @($obj.Rules.Ranges.Where( { $_.Ipformat -eq 'IPv4' } )).Length
                        $obj.IPv6Entry = @($obj.Rules.Ranges.Where( { $_.Ipformat -eq 'IPv6' } )).Length

                        # Validate the number of entries does not exceed the limit
                        Test-PasmMaxEntry -Entry $obj.IPv4Entry -MaxEntry $obj.MaxEntry -IpFormat 'IPv4' -ResourceType 'NetworkAcl'
                        Test-PasmMaxEntry -Entry $obj.IPv6Entry -MaxEntry $obj.MaxEntry -IpFormat 'IPv6' -ResourceType 'NetworkAcl'

                        $naclContainer.Add($obj)
                    }
                    $parent.Resource.NetworkAcl = $naclContainer
                }

                # Parse the outline file and convert the 'Rules' section to CIDR units: 'PrefixList'
                if ($resource.Contains('PrefixList')) {
                    $plContainer = [list[object]]::new()

                    # Multiple resource definitions are allowed, so process them one by one
                    foreach ($pl in $prefixList) {
                        $plRulesContainer = [list[object]]::new()

                        $obj = [ordered]@{}
                        $obj.ResourceName = $pl.ResourceName
                        $obj.ResourceId = if ($update) { $dest.Resource.PrefixList.ResourceId } else { 'not-deployed' }
                        $obj.VpcId = $pl.VpcId
                        $obj.MaxEntry = if ($pl.Contains('MaxEntry')) { $pl.MaxEntry } else { 1000 }
                        $obj.IPv4Entry = $null
                        $obj.IPv6Entry = $null
                        $obj.AddressFamily = if ($pl.Contains('AddressFamily')) { $pl.AddressFamily } else { 'IPv4' }

                        # Multiple rules definitions are allowed, so process them one by one
                        foreach ($rule in $pl.Rules) {
                            $plRangesContainer = [list[object]]::new()

                            $o = [ordered]@{}
                            $o.Id = $rule.Id
                            $o.ServiceKey = $rule.ServiceKey

                            # For each rule, send API to 'ip-ranges.json' to get the IP range
                            $num = 1
                            foreach ($r in $(Get-PasmAWSIpRange $rule -Resource PrefixList -AddressFamily $obj.AddressFamily)) {
                                $range = [ordered]@{}
                                $range.RangeId = $num
                                $range.IpPrefix = $r.IpPrefix
                                $range.IpFormat = $r.IpAddressFormat
                                $range.Region = $r.Region
                                $range.Description = 'Service:{0} Region:{1} Published:{2} Created:{3} Updated:{4}' -f (
                                    $rule.ServiceKey,
                                    $r.Region,
                                    $published.ToString($datetimeFormat),
                                    $(if ($update) { ([datetime]$dest.Metadata.CreatedAt).ToUniversalTime().ToString($datetimeFormat) } else { $now.ToString($datetimeFormat) }),
                                    $now.ToString($datetimeFormat)
                                )
                                $plRangesContainer.Add($range)
                                $num++
                            }
                            $o.Ranges = $plRangesContainer
                            $plRulesContainer.Add($o)
                        }
                        $obj.Rules = $plRulesContainer

                        # Get the number of registered ipv4 and ipv6 addresses
                        $obj.IPv4Entry = @($obj.Rules.Ranges.Where( { $_.IpFormat -eq 'IPv4' } )).Length
                        $obj.IPv6Entry = @($obj.Rules.Ranges.Where( { $_.IpFormat -eq 'IPv6' } )).Length

                        # Validate the number of entries does not exceed the limit
                        Test-PasmMaxEntry -Entry $obj.IPv4Entry -MaxEntry $obj.MaxEntry -IpFormat 'IPv4' -ResourceType 'PrefixList'
                        Test-PasmMaxEntry -Entry $obj.IPv6Entry -MaxEntry $obj.MaxEntry -IpFormat 'IPv6' -ResourceType 'PrefixList'

                        $plContainer.Add($obj)
                    }
                    $parent.Resource.PrefixList = $plContainer
                }

                # Add metadata section to object
                $parent.MetaData = $metadata

                # Converts the object to Yaml format and writes it to a file
                # If the file already exists, it will be overwritten
                $parent | ConvertTo-Yaml -OutFile $outputFilePath -Force
                $i++
                $PSCmdlet.WriteObject([fileinfo]::new($outputFilePath))
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }

    end {
        # Clean up processes, if any
    }

    <#
        .SYNOPSIS
        Get the ip ranges from 'ip-ranges.json' as described in the Yaml template, and create a blueprint.
 
        .DESCRIPTION
        Get the ip ranges from 'ip-ranges.json' as described in the Yaml template, and create a blueprint.
        See the following source for details: https://github.com/nekrassov01/Pasm/blob/main/src/Functions/Invoke-PasmBlueprint.ps1
 
        .EXAMPLE
        # Default input file path: ${PWD}/outline.yml, default output file name: 'blueprint.yml'
        Invoke-PasmBlueprint
 
        .EXAMPLE
        # Loading multiple files
        Invoke-PasmBlueprint -FilePath 'C:/Pasm/outline-sg.yml', 'C:/Pasm/outline-nacl.yml', 'C:/Pasm/outline-pl.yml' -OutputFileName 'blueprint-sg.yml', 'blueprint-nacl.yml', 'blueprint-pl.yml'
 
        .EXAMPLE
        # Loading multiple files from pipeline
        'C:/Pasm/outline-sg.yml', 'C:/Pasm/outline-nacl.yml', 'C:/Pasm/outline-pl.yml' | Invoke-PasmBlueprint -OutputFileName 'blueprint-sg.yml', 'blueprint-nacl.yml', 'blueprint-pl.yml'
 
        .LINK
        https://github.com/nekrassov01/Pasm
    #>

}