Scripts/Get-AssetInventory.ps1

Param(
    [Parameter(Mandatory = $false, Position = 0)][switch]$Monitor,
    [Parameter(Mandatory = $true, Position = 1)][string[]]$Network,
    [Parameter(Mandatory = $false, Position = 2)][string]$Highlight
)

function Get-Credentials {
    $UserId = [Security.Principal.WindowsIdentity]::GetCurrent()
    $AdminId = [Security.Principal.WindowsBuiltInRole]::Administrator
    $CurrentUser = New-Object Security.Principal.WindowsPrincipal($UserId)
    $RunningAsAdmin = $CurrentUser.IsInRole($AdminId)
    if (-not $RunningAsAdmin) { 
        Write-Output "`n[x] This script requires administrator privileges.`n"
        break
    }
}

function Get-IpAddressRange {
    Param([Parameter(Mandatory)][string[]]$Network)

    function ConvertIpAddressTo-BinaryString {
        Param([IPAddress]$IpAddress)
        $Integer = $IpAddress.Address
        $ReverseIpAddress = [IPAddress][String]$Integer
        $BinaryString = [Convert]::toString($ReverseIpAddress.Address,2)
        return $BinaryString
    }

    function ConvertBinaryStringTo-IpAddress {
        Param($BinaryString)
        $Integer = [System.Convert]::ToInt64($BinaryString,2).ToString()
        $IpAddress = ([System.Net.IPAddress]$Integer).IpAddressToString
        return $IpAddress
    }

    $IpAddressRange = @()
    $Network |
    foreach {
        if ($_.Contains('/')) {
            $NetworkId = $_.Split('/')[0]
            $SubnetMask = $_.Split('/')[1]
            if ([ipaddress]$NetworkId -and ($SubnetMask -eq 32)) {
                $IpAddressRange += $NetworkId          
            } elseif ([ipaddress]$NetworkId -and ($SubnetMask -le 32)) {
                $Wildcard = 32 - $SubnetMask
                $NetworkIdBinary = ConvertIpAddressTo-BinaryString $NetworkId
                
                $NetworkIdIpAddressBinary = $NetworkIdBinary.SubString(0,$SubnetMask) + ('0' * $Wildcard)
                $BroadcastIpAddressBinary = $NetworkIdBinary.SubString(0,$SubnetMask) + ('1' * $Wildcard)
                
                $NetworkIdIpAddress = ConvertBinaryStringTo-IpAddress $NetworkIdIpAddressBinary
                $BroadcastIpAddress = ConvertBinaryStringTo-IpAddress $BroadcastIpAddressBinary
                
                $NetworkIdInt32 = [convert]::ToInt32($NetworkIdIpAddressBinary,2)
                $BroadcastIdInt32 = [convert]::ToInt32($BroadcastIpAddressBinary,2)

                $NetworkIdInt32..$BroadcastIdInt32 | 
                foreach {
                    $BinaryString = [convert]::ToString($_,2)
                    $Address = ConvertBinaryStringTo-IpAddress $BinaryString
                    if ($Address -ne $NetworkIdIpAddress -and $Address -ne $BroadcastIpAddress) {
                       $IpAddressRange += $Address
                    }
                }            
            }
        }
    }

    return $IpAddressRange
}

function Format-Color {
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)]$Input,
        [Parameter(Mandatory = $true, Position = 1)][string]$Value,
        [Parameter(Mandatory = $true, Position = 2)][string]$BackgroundColor,
        [Parameter(Mandatory = $true, Position = 3)][string]$ForegroundColor
    )

    $Lines = ($Input | Format-Table -AutoSize | Out-String) -replace "`r", "" -split "`n"
    foreach ($Line in $Lines) {
        foreach ($Pattern in $Value) { 
            if ($Line -match $Value) { $LineMatchesValue = $true } 
            else { $LineMatchesValue = $false }

            if ($LineMatchesValue) { Write-Host $Line -BackgroundColor $BackgroundColor -ForegroundColor $ForegroundColor } 
            else { Write-Host $Line }
        }
    }
}

function Get-AssetInventory {
    <#
        .SYNOPSIS
        Given an IP address range, returns information about computers discovered online.
        .PARAMETER Network
        Specifies the network ID in CIDR notation.
        .INPUTS
        None. You cannot pipe objects to Get-AssetInventory.
        .OUTPUTS
        System.Array. Get-AssetInventory returns an array of custom PS objects.
        .EXAMPLE
        ./Get-AssetInventory.ps1 -Network 192.168.2.0/24
        IpAddress MacAddress HostName SerialNumber UserName FirstSeen LastSeen
        --------- ---------- -------- ------------ -------- --------- --------
        192.168.2.1 - - - - 2020-12-31 17:44 2021-01-01 09:30
        192.168.2.3 - - - - 2021-01-01 09:14 2021-01-01 09:14
        192.168.2.57 - - - - 2020-12-31 17:44 2021-01-01 09:30
        192.168.2.60 - - - - 2021-01-01 09:33 2021-01-01 09:30
        192.168.2.75 aa:bb:cc:11:22:33 Windows T6UsW9N8 WINDOWS\Victor 2020-12-31 17:44 2021-01-01 09:30
        .LINK
        https://www.github.com/cyberphor/scripts/PowerShell/Get-AssetInventory.ps1
        .NOTES
        https://devblogs.microsoft.com/scripting/parallel-processing-with-jobs-in-powershell/
        https://stackoverflow.com/questions/8751187/how-to-capture-the-exception-raised-in-the-scriptblock-of-start-job
        https://ss64.com/ps/start-job.html
        https://codeandkeep.com/PowerShell-Get-Subnet-NetworkID/
        https://stackoverflow.com/questions/27613836/how-to-pass-multiple-objects-via-the-pipeline-between-two-functions-in-powershel
        https://info.sapien.com/index.php/scripting/scripting-how-tos/take-values-from-the-pipeline-in-powershell
        https://stackoverflow.com/questions/48946924/powershell-function-not-accepting-array-of-objects
        https://www.reddit.com/r/PowerShell/comments/6eyhpv/whats_the_quickest_way_to_ping_a_computer/
        https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/sort-ipv4-addresses-correctly
        https://www.sans.org/reading-room/whitepapers/critical/leveraging-asset-inventory-database-37507
        https://stackoverflow.com/questions/17696149/invoke-command-in-a-background-job
        https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-pscustomobject?view=powershell-7.1
        https://devblogs.microsoft.com/scripting/two-simple-powershell-methods-to-remove-the-last-letter-of-a-string/
        https://www.pluralsight.com/blog/tutorials/measure-powershell-scripts-speed
        https://stackoverflow.com/questions/34113755/need-to-make-a-powershell-script-faster/34114444
        https://gallery.technet.microsoft.com/scriptcenter/Fast-asynchronous-ping-IP-d0a5cf0e
        https://stackoverflow.com/questions/55971796/powershell-parameters-validation-and-positioning
        https://social.technet.microsoft.com/Forums/Lync/en-US/ff644fca-1b25-4c8a-9a8a-ce90eb024389/
            in-powershell-how-do-i-pass-startjob-arguments-to-a-script-using-param-style-arguments?forum=ITCG
    #>


    Get-Credentials 
    $IpAddressRange = Get-IpAddressRange -Network $Network

    $Database = './AssetInventory.csv'
    if (Test-Path $Database) { 
        $Inventory = Import-Csv $Database 
    } else { 
        New-Item -ItemType File -Name $Inventory -ErrorAction Ignore | Out-Null
    }

    Get-Event -SourceIdentifier "Ping-*" | Remove-Event -ErrorAction Ignore
    Get-EventSubscriber -SourceIdentifier "Ping-*" | Unregister-Event -ErrorAction Ignore

    $IpAddressRange | 
    foreach {
        [string]$Event = "Ping-" + $_
        New-Variable -Name $Event -Value (New-Object System.Net.NetworkInformation.Ping)
        Register-ObjectEvent -InputObject (Get-Variable $Event -ValueOnly) -EventName PingCompleted -SourceIdentifier $Event
        (Get-Variable $Event -ValueOnly).SendAsync($_,2000,$Event)
        Remove-Variable $Event
    }

    while ($Pending -lt $IpAddressRange.Count) {
        Wait-Event -SourceIdentifier "Ping-*" | Out-Null
        Start-Sleep -Milliseconds 10
        $Pending = (Get-Event -SourceIdentifier "Ping-*").Count
    }

    $Assets = @()
    Get-Event -SourceIdentifier "Ping-*" | 
    foreach { 
        if ($_.SourceEventArgs.Reply.Status -eq 'Success') {
            $Asset = New-Object -TypeName psobject
            $IpAddress = ($_.SourceEventArgs.Reply).Address.IpAddressToString
            Remove-Event $_.SourceIdentifier
            Unregister-Event $_.SourceIdentifier
            Add-Member -InputObject $Asset -MemberType NoteProperty -Name IpAddress -Value $IpAddress
            $Assets += $Asset

            Start-Job -Name "Query-$IpAddress" -ArgumentList $IpAddress -ScriptBlock {
                $Hostname = [System.Net.Dns]::GetHostEntryAsync($args[0]).Result.HostName
                $MacAddress, $SerialNumber, $UserName = '-', '-', '-'
                if ($Hostname -eq $null) {
                    $Hostname = '-'
                } else { 
                    $Query = Invoke-Command -ComputerName $Hostname -ArgumentList $args[0] -ErrorAction Ignore -ScriptBlock {
                        (Get-WmiObject -Class Win32_BIOS).SerialNumber
                        (Get-WmiObject -Class Win32_ComputerSystem).UserName
                         Get-WmiObject -Class Win32_NetworkAdapterConfiguration | 
                            Where-Object { $_.IpAddress -eq $args[0] } | 
                            Select -ExpandProperty MacAddress
                    }
                    if ($Query -ne $null) {
                        $MacAddress = $Query[2]
                        $SerialNumber = $Query[0]
                        $UserName = $Query[1]
                    }
                }
                return $Hostname, $MacAddress, $SerialNumber, $UserName
            } | Out-Null
        }
    }

    While ((Get-Job -Name "Query-*").State -ne 'Completed') { Start-Sleep -Milliseconds 10 }

    $Assets |
    foreach {
        $CurrentAsset = $_
        $Job = Receive-Job -Name "Query-$($CurrentAsset.IpAddress)"
        Add-Member -InputObject $CurrentAsset -MemberType NoteProperty -Name MacAddress -Value $Job[1]
        Add-Member -InputObject $CurrentAsset -MemberType NoteProperty -Name HostName -Value $Job[0]
        Add-Member -InputObject $CurrentAsset -MemberType NoteProperty -Name SerialNumber -Value $Job[2]
        Add-Member -InputObject $CurrentAsset -MemberType NoteProperty -Name UserName -Value $Job[3]
        Add-Member -InputObject $CurrentAsset -MemberType NoteProperty -Name FirstSeen -Value $(Get-Date -Format 'yyyy-MM-dd HH:mm')
        Add-Member -InputObject $CurrentAsset -MemberType NoteProperty -Name LastSeen -Value $(Get-Date -Format 'yyyy-MM-dd HH:mm')

        $OldAsset = $Inventory | Where-Object { $_.IpAddress -eq $CurrentAsset.IpAddress }
        if ($OldAsset) { $CurrentAsset.FirstSeen = $OldAsset.FirstSeen }
    }

    $Inventory |
    foreach {
        $IdleAsset = $_
        $Added = $Assets | Where-Object { $_.IpAddress -eq $IdleAsset.IpAddress }
        if (-not $Added) {
            $Assets += $IdleAsset
        }
    }

    Remove-Job -Name "Query-*"
    $Assets | Sort-Object { $_.IpAddress -as [Version] } | Export-Csv -NoTypeInformation $Database

    if ($Highlight) {
        $Assets | Sort-Object { $_.IpAddress -as [Version] } | Format-Color -Value $Highlight -BackgroundColor Red -ForegroundColor White
    } else {
        $Assets | Sort-Object { $_.IpAddress -as [Version] } | Format-Table -AutoSize
    }
}

if ($Monitor) {
    While ($true) {
        Clear-Host
        Get-AssetInventory -Network $Network
        Start-Sleep -Seconds 300
    }
} else { Get-AssetInventory -Network $Network }