Private/Invoke-ArpScan.ps1

function Invoke-ARPScan {
    <#
    .Synopsis
    Performs an ARP scan against a given range of IPv4 IP Addresses.
    Part of Posh-SecMod (https://github.com/darkoperator/Posh-SecMod/)
    Author: darkoperator
    .DESCRIPTION
    Performs an ARP scan against a given range of IPv4 IP Addresses.
    .EXAMPLE
    Invoke an ARP Scan against a range of IPs specified in CIDR Format
        PS C:\> Invoke-ARPScan -CIDR 172.20.10.1/24
        MAC Address
        --- -------
        14:10:9F:D5:1A:BF 172.20.10.2
        00:0C:29:93:10:B5 172.20.10.3
        00:0C:29:93:10:B5 172.20.10.15
    .LINK
    https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1
    #>

    param (
        [Parameter(Mandatory = $true,
            ParameterSetName = "Range",
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [string]$Range,

        [Parameter(Mandatory = $true,
            ParameterSetName = "CIDR",
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [string]$CIDR,

        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [string]$MaxThreads = 50
    )


    Begin {

        Clear-Arp

        function New-IPv4Range {
            <#
            .Synopsis
                Generates a list of IPv4 IP Addresses given a Start and End IP.
            .DESCRIPTION
                Generates a list of IPv4 IP Addresses given a Start and End IP.
            #>

            param(
                [Parameter(Mandatory = $true,
                    ValueFromPipelineByPropertyName = $true,
                    Position = 0)]
                $StartIP,

                [Parameter(Mandatory = $true,
                    ValueFromPipelineByPropertyName = $true,
                    Position = 2)]
                $EndIP
            )

            # created by Dr. Tobias Weltner, MVP PowerShell
            $ip1 = ([System.Net.IPAddress]$StartIP).GetAddressBytes()
            [Array]::Reverse($ip1)
            $ip1 = ([System.Net.IPAddress]($ip1 -join '.')).Address

            $ip2 = ([System.Net.IPAddress]$EndIP).GetAddressBytes()
            [Array]::Reverse($ip2)
            $ip2 = ([System.Net.IPAddress]($ip2 -join '.')).Address

            for ($x = $ip1; $x -le $ip2; $x++) {
                $ip = ([System.Net.IPAddress]$x).GetAddressBytes()
                [Array]::Reverse($ip)
                $ip -join '.'
            }
        }


        function New-IPv4RangeFromCIDR {
            <#
            .Synopsis
                Generates a list of IPv4 IP Addresses given a CIDR.
            .DESCRIPTION
                Generates a list of IPv4 IP Addresses given a CIDR.
            #>

            param(
                [Parameter(Mandatory = $true,
                    ValueFromPipelineByPropertyName = $true,
                    Position = 0)]
                $Network
            )
            # Extract the portions of the CIDR that will be needed
            $StrNetworkAddress = ($Network.split("/"))[0]
            [int]$NetworkLength = ($Network.split("/"))[1]
            $NetworkIP = ([System.Net.IPAddress]$StrNetworkAddress).GetAddressBytes()
            $IPLength = 32 - $NetworkLength
            [Array]::Reverse($NetworkIP)
            $NumberOfIPs = ([System.Math]::Pow(2, $IPLength)) - 1
            $NetworkIP = ([System.Net.IPAddress]($NetworkIP -join ".")).Address
            $StartIP = $NetworkIP + 1
            $EndIP = $NetworkIP + $NumberOfIPs
            # We make sure they are of type Double before conversion
            If ($EndIP -isnot [double]) {
                $EndIP = $EndIP -as [double]
            }
            If ($StartIP -isnot [double]) {
                $StartIP = $StartIP -as [double]
            }
            # We turn the start IP and end IP in to strings so they can be used.
            $StartIP = ([System.Net.IPAddress]$StartIP).IPAddressToString
            $EndIP = ([System.Net.IPAddress]$EndIP).IPAddressToString
            New-IPv4Range $StartIP $EndIP
        }

        $sign = @"
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
public static class NetUtils
{
    [System.Runtime.InteropServices.DllImport("iphlpapi.dll", ExactSpelling = true)]
    static extern int SendARP(int DestIP, int SrcIP, byte[] pMacAddr, ref int PhyAddrLen);
    public static string GetMacAddress(String addr)
    {
        try
                {
                    IPAddress IPaddr = IPAddress.Parse(addr);
 
                    byte[] mac = new byte[6];
 
                    int L = 6;
 
                    SendARP(BitConverter.ToInt32(IPaddr.GetAddressBytes(), 0), 0, mac, ref L);
 
                    String macAddr = BitConverter.ToString(mac, 0, L);
 
                    return (macAddr.Replace('-',':'));
                }
                catch (Exception ex)
                {
                    return (ex.Message);
                }
    }
}
"@

        try {
            Write-Verbose "Instanciating NetUtils"
            $IPHlp = Add-Type -TypeDefinition $sign -Language CSharp -PassThru
        }
        catch {
            Write-Verbose "NetUtils already instanciated"
        }

        # Manage if range is given
        if ($Range) {
            $rangeips = $Range.Split("-")
            $targets = New-IPv4Range -StartIP $rangeips[0] -EndIP $rangeips[1]
        }

        # Manage if CIDR is given
        if ($CIDR) {
            $targets = New-IPv4RangeFromCIDR -Network $CIDR
        }
    }
    Process {


        $scancode = {
            param($IPAddress, $IPHlp)
            $result = $IPHlp::GetMacAddress($IPAddress)
            if ($result) { New-Object psobject -Property @{Address = $IPAddress; MAC = $result }
            }
        } # end ScanCode var

        $jobs = @()



        $start = Get-Date
        Write-Verbose "Begin Scanning at $start"

        #Multithreading setup

        # create a pool of maxThread runspaces
        $pool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
        $pool.Open()

        $jobs = @()
        $ps = @()
        $wait = @()

        $i = 0

        # How many servers
        #$record_count = $targets.Length

        #Loop through the endpoints starting a background job for each endpoint
        foreach ($IPAddress in $targets) {
            # Show Progress
            #$record_progress = [int][Math]::Ceiling((($i / $record_count) * 100))
            # Write-Progress -Activity "Performing ARP Scan" -PercentComplete $record_progress -Status "Addresses Queried - $record_progress%" -Id 1;

            while ($($pool.GetAvailableRunspaces()) -le 0) {
                Start-Sleep -milliseconds 500
            }

            # create a "powershell pipeline runner"
            $ps += [powershell]::create()

            # assign our pool of 3 runspaces to use
            $ps[$i].runspacepool = $pool

            # command to run
            [void]$ps[$i].AddScript($scancode).AddParameter('IPaddress', $IPAddress).AddParameter('IPHlp', $IPHlp)
            #[void]$ps[$i].AddParameter()

            # start job
            $jobs += $ps[$i].BeginInvoke();

            # store wait handles for WaitForAll call
            $wait += $jobs[$i].AsyncWaitHandle

            $i++
        }

        Write-Verbose "Waiting for scanning threads to finish..."

        $waitTimeout = Get-Date

        while ($($jobs | Where-Object { $_.IsCompleted -eq $false }).count -gt 0 -or $($($(Get-Date) - $waitTimeout).totalSeconds) -gt 60) {
            Start-Sleep -milliseconds 500
        }

        # end async call
        for ($y = 0; $y -lt $i; $y++) {

            try {
                # complete async job
                $ScanResults += $ps[$y].EndInvoke($jobs[$y])

            }
            catch {

                Write-Warning "error: $_"
            }

            finally {
                $ps[$y].Dispose()
            }
        }

        $pool.Dispose()
    }

    end {
        $ScanResults
    }
}