IP.Tools.psm1

## Running A Build Will Compile This To A Single PSM1 File Containing All Module Code ##

## If Importing Module Source Directly, This Will Dynamically Build Root Module ##

# Get list of private functions and public functions to import, in order.
$Private = @(Get-ChildItem -Path $PSScriptRoot\private -Recurse -Filter "*.ps1") | Sort-Object Name
$Public = @(Get-ChildItem -Path $PSScriptRoot\public -Recurse -Filter "*.ps1") | Sort-Object Name

$AllMask = [Math]::Pow(2, 32)
New-Variable -Name AllMask -Value $AllMask -Scope Script -Force

Class IpNetwork {

  [ipaddress]$IpAddress
  [ipaddress]$IpNetmask

  IpNetwork([string]$CIDR) {
    ($IP, $MaskLen) = $CIDR.Split('/')
    $IP = Get-IpNetworkBase -CIDR $CIDR
    Write-Verbose "IP: $($IP); Mask: $($MaskLen)"
    $this.IpAddress = [ipaddress]($IP)
    $this.IpNetmask = [ipaddress](Convert-MaskLenToIp($MaskLen))
  }

  IpNetwork([IpAddress]$IP, [IpAddress] $Mask) {
    Write-Verbose "IP: $($IP); Mask: $($Mask)"
    $IP = Get-IpNetworkBase -IP $IP -Mask $Mask
    $this.IpAddress = $IP
    $this.IpNetmask = $Mask
  }

  [system.net.ipaddress]GetStartAddress() {
    return Get-IpNetworkStartIp -Network $this
  }

  [system.net.ipaddress]GetEndAddress() {
    return Get-IpNetworkEndIP -Network $this
  }
}

# Dot source the private function files.
foreach ($ImportItem in $Private) {
  try {
    . $ImportItem.FullName
    Write-Verbose -Message ("Imported private function {0}" -f $ImportItem.FullName)
  }
  catch {
    Write-Error -Message ("Failed to import private function {0}: {1}" -f $ImportItem.FullName, $_)
  }
}

# Dot source the public function files.
foreach ($ImportItem in $Public) {
  try {
    . $ImportItem.FullName
    Write-Verbose -Message ("Imported public function {0}" -f $ImportItem.FullName)
  }
  catch {
    Write-Error -Message ("Failed to import public function {0}: {1}" -f $ImportItem.FullName, $_)
  }
}

# Export the public functions.
Export-ModuleMember -Function $Public.BaseName
# Private Function Example - Replace With Your Function
function Add-PrivateFunction {

  [CmdletBinding()]

  Param (
    # Your parameters go here...
  )

  # Your function code goes here...
  Write-Output "Your private function ran!"

}

function Get-IpBogon {
  <#
    .SYNOPSIS
        Return a list of IP Bogons with [ipaddress] objects representing the IP and Mask
    .DESCRIPTION
        Return a list of IP Bogons (Internet non-routable addresses) with [ipaddress] objects representing both IP and Mask.

        IP Bogons are Internet non-routable addresses that are either reserved for Private use (RFC1918), or are otherwise not available.
    .INPUTS
        None
    .OUTPUTS
        Array of [ipaddress] objects representing IP Bogons.
    .EXAMPLE
        PS> $Bogons = Get-IpBogon.ps1
    .LINK
        https://github.com/jberkers42/ip.tools
  #>

  [CmdletBinding()]

  $Bogons = @()
  $Bogons += '0.0.0.0/8'          # RFC1122 "This" network
  $Bogons += '10.0.0.0/8'         # RFC1918 Private-use networks
  $Bogons += '100.64.0.0/10'      # RFC6598 Carrier-grade NAT
  $Bogons += '127.0.0.0/8'        # RFC1122 IPv4 Loopback
  $Bogons += '169.254.0.0/16'     # RFC3927 IPv4 Link local
  $Bogons += '172.16.0.0/12'      # RFC1918 Private-use networks
  $Bogons += '192.0.0.0/24'       # RFC5736 IETF protocol assignments
  $Bogons += '192.0.2.0/24'       # RFC5737 TEST-NET-1
  $Bogons += '192.168.0.0/16'     # RFC1918 Private-use networks
  $Bogons += '198.18.0.0/15'      # RFC2544 Network interconnect device benchmark testing
  $Bogons += '198.51.100.0/24'    # RFC5737 TEST-NET-2
  $Bogons += '203.0.113.0/24'     # RFC5737 TEST-NET-3
  $Bogons += '224.0.0.0/4'        # RFC1112 Multicast
  $Bogons += '240.0.0.0/4'        # Reserved for future use

  $BogonIps = @()

  foreach ($Bogon in $Bogons) {

      $BogonIp = [IpNetwork]::new($Bogon)

      $BogonIPs += $BogonIp
  }

  Write-Output $BogonIps

}

function Remove-IpBogon {
  #
  <#
    .SYNOPSIS
      Remove any Bogon IPs from the list of provided IPs.
    .DESCRIPTION
      Remove any Bogon IPs from the list of provided IPs. Each IP is expected to be either a dotted quad notation string representation of the address, or an [ipaddress] object.
    .INPUTS
      [IpAddress] or [string] values to filter
    .OUTPUTS
      Array of [ipaddress] objects that have had IP Bogons removed.
    .EXAMPLE
      PS> $IPs = Get-IpFromSomeSource
      PS> $IPs | Remove-IpBogon

      Obtain a list of IPs from some source (like a SIEM), and filter out the Bogon addresses
    .LINK
        https://github.com/jberkers42/ip.tools
  #>

[cmdletbinding(SupportsShouldProcess)]
  param (
      [Parameter(Mandatory=$true,
          ValueFromPipeLine=$true)]
      [ipaddress] $Address
  )

  Begin {
      $Me = $MyInvocation.MyCommand.Name

      Write-Verbose $Me
  }

  Process {

    if ($PSCmdlet.ShouldProcess($Address, "Remove IP if Bogon")) {
      if (!(Test-IpBogon -Address $Address)) {
        Write-Output $Address
    }
  }

  }

  End {

  }

}

function Test-IpBogon {

  [cmdletbinding()]
  param (
      [Parameter(Mandatory=$true,
          ValueFromPipeLine=$true)]
      [ipaddress] $Address
  )

  Begin {
      $Me = $MyInvocation.MyCommand.Name

      Write-Verbose $Me

      $IpBogons = Get-IpBogon
  }

  Process {

      $IsBogon = $false

      foreach ($Bogon in $IpBogons) {
          if (Test-IpInNetwork -Address $Address -Network $Bogon) {
              $IsBogon = $true
          }
      }

      Write-Output $IsBogon

  }

  End {
      Clear-Variable 'IpBogons'
  }

}

function Convert-IpToMaskLen {
  <#
    .SYNOPSIS
        Convert a Subnet Mask to PrefixLength
    .DESCRIPTION
        Convert a Subnet Mask to a Prefix Length
    .EXAMPLE
        PS C:\> Convert-IpToMaskLen 255.255.0.0
        16

        This example counts the relevant network bits of the dotted SubnetMask 255.255.0.0.
    .INPUTS
        [string]
    .OUTPUTS
        [string]
  #>


  [CmdletBinding()]

  param (
    # SubnetMask to convert
    [Parameter(Mandatory)]
    [System.Net.IpAddress]$SubnetMask
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose "$($Me): $($SubnetMask)"
  }

  Process {
    $Octets = $SubnetMask.IPAddressToString.Split('.')
    Write-Verbose "$($Me): Octets: $($Octets)"
    foreach($Octet in $Octets) {
      while(0 -ne $Octet){
        $Octet = ($Octet -shl 1) -band [byte]::MaxValue
        $result++
      }
    }

    $TestMask = Convert-MaskLenToIp -MaskLen $result

    if ($TestMask -ne $SubnetMask) {
      throw "Invalid Netmask Supplied"
    } else {
      $result -as [string]
    }
  }

  End {

  }

}

function Convert-MaskLenToIp {
  <#
    .SYNOPSIS
      Convert MaskLen from CIDR Notation (IP/MaskLen) to an IP Address object
    .DESCRIPTION
      Convert MaskLen from CIDR Notation (IP/MaskLen) to an IP Address object
    .PARAMETER MaskLen
      Integer value representing the mask length
    .INPUTS
      Integer Mask Lenth
    .OUTPUTS
      [ipaddress] object representing the Netmask
    .EXAMPLE
      Pass a single MaskLen as a parameter

      PS> Convert-MaskLenToIp -MaskLen 24

      AddressFamily : InterNetwork
      ScopeId :
      IsIPv6Multicast : False
      IsIPv6LinkLocal : False
      IsIPv6SiteLocal : False
      IsIPv6Teredo : False
      IsIPv6UniqueLocal : False
      IsIPv4MappedToIPv6 : False
      Address : 16777215
      IPAddressToString : 255.255.255.0

    .EXAMPLE
      Use Pipeline to convert MaskLen to IP Address

      PS> 27 | Convert-MaskLenToIp

      AddressFamily : InterNetwork
      ScopeId :
      IsIPv6Multicast : False
      IsIPv6LinkLocal : False
      IsIPv6SiteLocal : False
      IsIPv6Teredo : False
      IsIPv6UniqueLocal : False
      IsIPv4MappedToIPv6 : False
      Address : 3774873599
      IPAddressToString : 255.255.255.224

    .LINK
      https://github.com/jberkers42/ip.tools
  #>

  [CmdletBinding()]
  [OutputType([ipaddress])]

  param(
    [Parameter(Mandatory=$true,
      ValueFromPipeLine=$true)]
    [ValidateRange([int]0, [int]32)]
    [int]$MaskLen
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me
  }

  Process {

    [ipaddress]($AllMask - ($AllMask -shr $MaskLen))

  }

  End {

  }

}

function Get-IpNetworkBase {
  <#
    .SYNOPSIS
      Get the Base Network address for the supplied IP/Mask
    .DESCRIPTION
      Get the Base Network address for the supplied IP/Mask
    .PARAMETER CIDR
      [string] containing CIDR notation for network address
    .PARAMETER IP
      [IPAddress] object or Dotted Quad notation string representing the IP address
    .PARAMETER Mask
      [IpNetwork] Object or Dotted Quad notation string representing the Netmask
    .INPUTS
      CIDR Strings
    .OUTPUTS
      [IpAddress] Object
    .EXAMPLE
      PS> (Get-IpNetworkBase -CIDR 192.168.100.20/24).IPAddressToString
      192.168.100.0


    .EXAMPLE
      Use Pipeline to convert MaskLen to IP Address

      PS> 27 | Convert-MaskLenToIp


    .LINK
      https://github.com/jberkers42/ip.tools
  #>

  [CmdletBinding(DefaultParameterSetName = 'CIDR')]
  [OutputType([IpAddress])]
  param(
    [Parameter(ParameterSetName='CIDR',
      Mandatory=$true,
      Position = 0,
      ValueFromPipeline=$true)]
    [string] $CIDR,

    [Parameter(ParameterSetName='IpMask',
      Mandatory=$true,
      Position = 0)]
    [IpAddress] $IP,

    [Parameter(ParameterSetName='IpMask',
      Mandatory=$true,
      Position = 1)]
    [IpAddress] $Mask
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me
  }

  Process {
    If ($PSCmdlet.ParameterSetName -eq "CIDR") {
      ([IpAddress]$IP, $MaskLen) = $CIDR -split '/'
      [IpAddress]$Mask = Convert-MaskLenToIp $MaskLen
    }
    Write-Verbose "IP: $($IP); Mask: $($Mask);"
    [IpAddress]$Net = ($IP.Address -band $Mask.Address)
    return $Net
  }

  End {

  }

}

function Get-IpNetworkEndIP {
  [CmdletBinding(DefaultParameterSetName = 'CIDR')]
  [OutputType([ipaddress])]
  param (
      [Parameter(ParameterSetName='CIDR',
      Mandatory=$true,
      ValueFromPipeline=$true)]
      [string]$CIDR,

      [Parameter(ParameterSetName='IpMask',
        Mandatory=$true,
        Position = 0)]
      [string] $IP,

      [Parameter(ParameterSetName='IpMask',
        Mandatory=$true,
        Position = 1)]
      [string] $Mask,

      [Parameter(ParameterSetName='Network',
        Mandatory=$true,
        Position = 1)]
      [IpNetwork] $Network
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me
  }

  Process {
    Switch ($PSCmdlet.ParameterSetName) {
      "CIDR" {
        Write-Verbose "$($Me): Invoked with CIDR"
        $Network = [IpNetwork]::new($CIDR)
      }
      "IpMask" {
        Write-Verbose "$($Me): Invoked with IP/Mask"
        $Network = [IpNetwork]::new($IP, $Mask)
      }
      "Network" {
        Write-Verbose "$($Me): Invoked with IpNetwork Object"

      }
    }

    $NetworkIP = $Network.IPAddress.GetAddressBytes()
    Write-Verbose "$($Me): NetworkIP: $($NetworkIP)"
    [Array]::Reverse($NetworkIP)
    Write-Verbose "$($Me): NetworkIP (reversed): $($NetworkIP)"
    $NetworkIP = ([ipaddress]($NetworkIP -join ".")).Address

    $MaskLen = Convert-IpToMaskLen -SubnetMask $Network.IpNetmask

    $NumIPs = ([System.Math]::Pow(2,(32 -$MaskLen)))

    $EndIP = $NetworkIP + $NumIPs - 1

    # Convert to Double
    If (($EndIP.GetType()).Name -ine "double") {
      $EndIP = [Convert]::ToDouble($EndIP)
    }

    $EndIP = [ipaddress]$EndIP

    Return $EndIP
  }

  End {

  }
}

function Get-IpNetworkStartIP {
  [CmdletBinding(DefaultParameterSetName = 'CIDR')]
  [OutputType([ipaddress])]
  param (
      [Parameter(ParameterSetName='CIDR',
      Mandatory=$true,
      ValueFromPipeline=$true,
      Position = 1)]
      [String]$CIDR,

      [Parameter(ParameterSetName='IpMask',
        Mandatory=$true,
        Position = 0)]
      [string] $IP,

      [Parameter(ParameterSetName='IpMask',
        Mandatory=$true,
        Position = 1)]
      [string] $Mask,

      [Parameter(ParameterSetName='Network',
        Mandatory=$true,
        ValueFromPipeline=$true,
        Position = 1)]
      [IpNetwork] $Network,

      [Parameter(Mandatory=$false)]
      [switch]$ExcludeNetwork
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me
  }

  Process {
    Switch ($PSCmdlet.ParameterSetName) {
      "CIDR" {
        Write-Verbose "$($Me): Invoked with CIDR"
        $Network = [IpNetwork]::new($CIDR)
      }
      "IpMask" {
        Write-Verbose "$($Me): Invoked with IP/Mask"
        $Network = [IpNetwork]::new($IP, $Mask)
      }
      "Network" {
        Write-Verbose "$($Me): Invoked with IpNetwork Object"

      }
    }

    $NetworkIP = $Network.IPAddress.GetAddressBytes()
    [Array]::Reverse($NetworkIP)
    $NetworkIP = ([ipaddress]($NetworkIP -join ".")).Address

    if($ExcludeNetwork) {
      $StartIP = $NetworkIP + 1
    } else {
      $StartIP = $NetworkIP
    }

    # Convert to Double
    If (($StartIP.GetType()).Name -ine "double") {
      $StartIP = [Convert]::ToDouble($StartIP)
    }

    $StartIP = [ipaddress]$StartIP

    Return $StartIP
  }

  End {

  }

}

function New-IpNetwork {
  <#
    .SYNOPSIS
      Create a new IpNetwork object with the specified CIDR or IP and Mask parameters
    .DESCRIPTION
      Create a new IpNetwork object with the specified CIDR or IP and Mask parameters
    .PARAMETER CIDR
      [string] containing CIDR notation for network
    .PARAMETER IP
      [IPAddress] object or Dotted Quad notation string representing the IP address
    .PARAMETER Mask
      [IpNetwork] Object or Dotted Quad notation string representing the Netmask
    .INPUTS
      CIDR Strings
    .OUTPUTS
      [IpNetwork] Object
    .EXAMPLE
      PS> $Network =
      PS> Test-IpInNetwork -Address 192.168.100.20 -Network


    .EXAMPLE
      Use Pipeline to convert MaskLen to IP Address

      PS> 27 | Convert-MaskLenToIp


    .LINK
      https://github.com/jberkers42/ip.tools
  #>

  [CmdletBinding(DefaultParameterSetName = 'CIDR', SupportsShouldProcess)]
  param(
    [Parameter(ParameterSetName='CIDR',
      Mandatory=$true,
      Position = 0,
      ValueFromPipeline=$true)]
    [string] $CIDR,

    [Parameter(ParameterSetName='IpMask',
      Mandatory=$true,
      Position = 0)]
    [string] $IP,

    [Parameter(ParameterSetName='IpMask',
      Mandatory=$true,
      Position = 1)]
    [string] $Mask
  )

  Begin {
    $Me = $MyInvocation.MyCommand.Name

    Write-Verbose $Me
  }

  Process {
    if ($PSCmdlet.ShouldProcess("IpNetwork", "Return new IP Network from CIDR or IP and Mask")) {
      Switch ($PSCmdlet.ParameterSetName) {
        "CIDR" {
          return [IpNetwork]::new($CIDR)
        }
        "IpMask" {
          return [IpNetwork]::new($IP, $Mask)
        }
      }
    }
  }

  End {

  }

}

function Test-IpInNetwork {
  <#
    .SYNOPSIS
      Test if an IP address exists in the specified network
    .DESCRIPTION
      Test if an IP address exists in the specified network
    .PARAMETER Address
      [IPAddress] object or Dotted Quad notation string representing the IP address to test
    .PARAMETER Network
      [IpNetwork] Object consisting of IpAddress and IpNetmask to check if IP belongs. If supplied as string, will be converted to IpNetwork before testing.
    .INPUTS
      IP Addresses and networks.
    .OUTPUTS
      [bool] result of test
    .EXAMPLE
      PS> $Network = New-IpNetwork "192.168.100.0/24"
      PS> Test-IpInNetwork -Address 192.168.100.20 -Network $Network


    .EXAMPLE
      Use Pipeline to convert MaskLen to IP Address

      PS> 27 | Convert-MaskLenToIp


    .LINK
      https://github.com/jberkers42/ip.tools
  #>

  [cmdletbinding()]
  param (
      [Parameter(Mandatory=$true,
          ValueFromPipeLine=$true)]
      [ipaddress] $Address,

      [Parameter(Mandatory=$true,
          ValueFromPipeline=$true)]
      [ValidateScript({
        $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique
        if ($TypeName -eq 'System.String' -and $_ -match "(\d{1,3}\.){3}\d{1,3}\/\d{1,2}") {
          Write-Verbose "Convert From String: $_"
          New-IpNetwork $_
        } elseif ($TypeName -eq 'IpNetwork') {
          Write-Verbose "Taken as IpNetwork Object $_"
          $_
        }
      })]
      $Network
  )

  Begin {
      $Me = $MyInvocation.MyCommand.Name

      Write-Verbose $Me

  }

  Process {

    Write-Verbose "Network To Test: $Network"

    if ($Network.GetType().Name -eq 'String') {
      $Network = New-IpNetwork -CIDR $Network
    }

    if ($Network.IpAddress.Address -eq ($Address.Address -band $Network.IpNetmask.Address)) {
        Write-Output $true
    } else {
        Write-Output $false
    }
  }

  End {

  }
}

function Test-IpRangeIsSubnet {
  <#
    .SYNOPSIS
      Test if supplied IP Range is a valid Subnet
    .DESCRIPTION
      Test if supplied IP Range is a valid Subnet
    .PARAMETER StartAddress
      [IPAddress] object or Dotted Quad notation string representing the Start IP address of the range to test
    .PARAMETER EndAddress
      [IPAddress] object or Dotted Quad notation string representing the End IP address of the range to test
    .INPUTS
      IP Addresses
    .OUTPUTS
      [bool] False if the IP Range does not represent a valid subnet
      [ipaddress] representation of the Subnet Mask
    .EXAMPLE
      PS> $StartAddress = 192.168.1.0
      PS> $EndAddress = 192.168.1.15
      PS> Test-IpRangeIsSubnet -StartAddress $StartAddress -EndAddress $EndAddress

        AddressFamily : InterNetwork
        ScopeId :
        IsIPv6Multicast : False
        IsIPv6LinkLocal : False
        IsIPv6SiteLocal : False
        IsIPv6Teredo : False
        IsIPv6UniqueLocal : False
        IsIPv4MappedToIPv6 : False
        Address : 3774873599
        IPAddressToString : 255.255.255.224

    .LINK
      https://github.com/jberkers42/ip.tools
  #>

  [cmdletbinding()]
  param (
      [Parameter(Mandatory=$true,
          ValueFromPipeLine=$true)]
      [ipaddress] $StartAddress,

      [Parameter(Mandatory=$true,
          ValueFromPipeLine=$true)]
      [ipaddress] $EndAddress
  )

  Begin {
      $Me = $MyInvocation.MyCommand.Name

      Write-Verbose $Me
  }

  Process {

    Write-Verbose "Address Range To Test: $StartAddress - $EndAddress"

    $StartAddressBytes = $StartAddress.GetAddressBytes()
    $EndAddressBytes = $EndAddress.GetAddressBytes()

    $AddressDiffBytes = @()
    for (($octet = 0);  ($octet -lt $StartAddressBytes.Count); $octet++) {
      Write-verbose "Octet $octet"
      $AddressDiffBytes += $StartAddressBytes[$octet] -bxor $EndAddressBytes[$octet]
    }

    Write-Verbose "$($AddressDiffBytes -join '.')"
    [ipaddress]$AddressDiff = $AddressDiffBytes -join '.'

    Write-Verbose $AddressDiff

    [ipaddress]$MaskDiff = ($AllMask - 1) - $AddressDiff.Address

    Write-Verbose $MaskDiff

    # Reverse the bytes
    $Octets = $MaskDiff.IPAddressToString.Split('.')
    [Array]::Reverse($Octets)
    $Mask = [IpAddress]($Octets -join '.')

    if (Test-ValidMask ($Mask)) {
        Write-Output $Mask
    } else {
        Write-Output $false
    }
  }

  End {

  }
}

function Test-ValidMask {
  <#
    .SYNOPSIS
      Test if an IP address represents a valid network mask
    .DESCRIPTION
      Test if an IP address represents a valid network mask. I.E. the binary representation contains all 1's followed by all 0'
    .PARAMETER Address
      [IPAddress] object or Dotted Quad notation string representing the IP address to test
    .INPUTS
      IP Address
    .OUTPUTS
      [bool] result of test
    .EXAMPLE
      PS> $Mask = "255.255.192.0"
      PS> Test-ValidMask -Address $Mask

    .EXAMPLE
      Use Pipeline to convert MaskLen to IP Address

      PS> 27 | Convert-MaskLenToIp


    .LINK
      https://github.com/jberkers42/ip.tools
  #>

  [cmdletbinding()]
  param (
      [Parameter(Mandatory=$true,
          ValueFromPipeLine=$true)]
      [ipaddress] $Address
  )

  Begin {
      $Me = $MyInvocation.MyCommand.Name

      Write-Verbose $Me

  }

  Process {

    Write-Verbose "Mask To Test: $Address"

    # Convert Address to binary representation
    $binAddress = ($Address.GetAddressBytes() | ForEach-Object {[System.Convert]::ToString($_,2).PadLeft(8,'0')}) -join ''

    $AddressBits = $binAddress.ToCharArray()

    $Last = 1
    $ValidMask = $true
    Foreach ($bit in $AddressBits) {
      if ($bit -eq '1') {
        if ($Last -ne '1') {
          $ValidMask = $false
        }
      }
      $Last = $bit
    }

    Write-Output $ValidMask
  }

  End {

  }
}

Export-ModuleMember -Function Get-IpBogon, Remove-IpBogon, Test-IpBogon, Convert-IpToMaskLen, Convert-MaskLenToIp, Get-IpNetworkBase, Get-IpNetworkEndIP, Get-IpNetworkStartIP, New-IpNetwork, Test-IpInNetwork, Test-IpRangeIsSubnet, Test-ValidMask