Public/Resolve-NSPIpInfo.ps1
|
function Resolve-NSPIpInfo { <# .SYNOPSIS Looks up the country, ISP, ASN and hosting/proxy/mobile flags of IP addresses through ip-api.com, 100 per request. .DESCRIPTION Addresses are de-duplicated, ports and brackets stripped ("203.0.113.5:51234", "[2001:db8::1]:443"), and looked up through ip-api.com's batch endpoint, waiting out its rate limit (X-Rl / X-Ttl headers) between batches. Results are cached for the session, so the same address is never looked up twice; -NoCache looks them up again. Private and reserved addresses come back with Status 'fail' and ip-api's reason in Message. ip-api.com's free endpoint is plain HTTP and licensed for non-commercial use: the addresses cross the internet unencrypted. Check its terms before using it for client work. .PARAMETER IPAddress One or more addresses. Takes pipeline input, including objects with an IP, ClientIP or Query property. .PARAMETER NoCache Look the addresses up again even when this session already has them. .EXAMPLE Resolve-NSPIpInfo 203.0.113.5, 198.51.100.7 .EXAMPLE Get-NSPM365AuditSummary -Path .\audit.csv -GroupBy ClientIP | Resolve-NSPIpInfo | Where-Object Hosting #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('IP', 'ClientIP', 'Query', 'Name')] [AllowEmptyString()] [string[]]$IPAddress, [switch]$NoCache ) begin { $wanted = New-Object System.Collections.Generic.List[string] if ($null -eq $script:NSPIpInfoCache) { $script:NSPIpInfoCache = @{} } } process { foreach ($value in $IPAddress) { $ip = ConvertTo-NSPAuditIp $value if ($ip -and -not $wanted.Contains($ip)) { $wanted.Add($ip) } } } end { $lookup = @($wanted | Where-Object { $NoCache -or -not $script:NSPIpInfoCache.ContainsKey($_) }) for ($start = 0; $start -lt $lookup.Count; $start += 100) { $batch = @($lookup[$start..([math]::Min($start + 99, $lookup.Count - 1))]) Write-Verbose "ip-api.com: looking up $($batch.Count) address(es) ($($start + 1)-$($start + $batch.Count) of $($lookup.Count))." $body = ConvertTo-Json -InputObject $batch -Compress $response = Invoke-WebRequest -Method Post -Uri $script:NSPIpApiUri -Body $body -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop foreach ($result in (ConvertFrom-Json -InputObject $response.Content)) { $asn = if ($result.as -match '^AS(?<n>\d+)') { $Matches.n } else { '' } $script:NSPIpInfoCache[[string]$result.query] = [pscustomobject]@{ IP = [string]$result.query Status = [string]$result.status Message = [string]$result.message Country = [string]$result.country CountryCode = [string]$result.countryCode Region = [string]$result.regionName City = [string]$result.city Isp = [string]$result.isp Org = [string]$result.org Asn = $asn AsName = [string]$result.asname Mobile = [bool]$result.mobile Proxy = [bool]$result.proxy Hosting = [bool]$result.hosting } } # X-Rl: requests left in this window; X-Ttl: seconds until it resets. $left = @($response.Headers['X-Rl'])[0] $ttl = @($response.Headers['X-Ttl'])[0] if (($start + 100) -lt $lookup.Count -and "$left" -eq '0') { $wait = [int]$ttl + 1 Write-Host "ip-api.com rate limit reached - waiting $wait s." -ForegroundColor DarkGray Start-Sleep -Seconds $wait } } foreach ($ip in $wanted) { if ($script:NSPIpInfoCache.ContainsKey($ip)) { $script:NSPIpInfoCache[$ip] } else { [pscustomobject]@{ IP = $ip; Status = 'fail'; Message = 'No result returned'; Country = ''; CountryCode = ''; Region = ''; City = ''; Isp = ''; Org = ''; Asn = ''; AsName = ''; Mobile = $false; Proxy = $false; Hosting = $false } } } } } $script:NSPIpApiUri = 'http://ip-api.com/batch?fields=status,message,query,country,countryCode,regionName,city,isp,org,as,asname,mobile,proxy,hosting' |