This script will perform a normal IP-Scan then augment it with vendor names pulled from api.maclookup.app.
(The script is rate limited to not exceed the API limits, you can register for a free API key if you want to go faster and remove the :delay.)
In addition to the normal DNS, SNMP and Netbios name detection, it will also scrape names from the Neighbours table as well as Hostname, ClassID and Comment from the DHCP leases table.
*Updated 16-10-24* - Now sorts results, in as efficient a manor as I could manage.
*Updated 17-10-24* - Now also lists interface matched from the ARP table. This is especially useful when you're scanning a bridge with multiple VLANs on it.
*Updated 25-10-24* - Updated to selectively send additional octets to maclookup api for unusual vendors using larger mac address blocks.
*Updated 10-11-24* - Now supports optional API key use - Free registration here (I have no affiliation): https://my.maclookup.app/login
Tested down to ROS 7.10. Requires v7 minimum.
You should modify "scanInterface" and "scanDuration" to suit your network.
Add as a script named "ip-scan-vendor" then run from the CLI with:
Code: Select all
/system/script/run ip-scan-vendor
Code: Select all
#ip-scan-vendor v1.6
#ROS v7 and above
###---------------------
:local scanInterface "bridge"; #Interface to run scan on
:local scanDuration 30; #Seconds to run scan, 20 as a rough minimum, consider 60-120 for larger networks
:local addressRange ""; #Set here to scan a specific range
:local saveLog "no"; #Set yes to export a log file as well
:local apiKey ""; #Optional api key
#Speeds up vendor resolution slightly. Register for free here: https://my.maclookup.app/login
###---------------------
:if ([:len $apiKey] > 10) do={:put ("API key present: Rate limit disabled")}
:put ("Interface: ".$scanInterface)
:put ("Duration: ".$scanDuration."sec")
:put "Starting Scan, please wait.."
:put ""
:local scanResult
:if ([:len $addressRange] > 8) do={
:put ("Address Range: ".$addressRange)
:set scanResult [/tool/ip-scan interface=$scanInterface address-range=$addressRange as-value duration=$scanDuration]
} else={
:set scanResult [/tool/ip-scan interface=$scanInterface as-value duration=$scanDuration]
}
:local neighbours [/ip/neighbor/print as-value]
:local dhcpLeases [/ip/dhcp-server/lease/print detail as-value]
#Two calls to get dynamic and reservation lists, annoying it's not present in the above as-value results!
:local dhcpLeasesDynamic [/ip dhcp-server/lease/print as-value where dynamic=yes]
:local dhcpLeasesReservations [/ip dhcp-server/lease/print as-value where dynamic=no]
:local arpTable [/ip/arp/print as-value]
:local countTotal 0
:local countDynamic 0
:local countReservations 0
:local logString
#strPadTrim function
:local strPadTrim do={
:local str $string
:local tar $targetLen
:if ([:len $str] > $tar) do={:set str [:pick $str 0 $tar]}
:local cont [:len $str]
:while ($cont < $tar) do={
:set str ($str." ")
:set cont [:len $str]
}
:return $str
}
#char replace function
:local strReplace do={
:local encode
:for i from=0 to=([:len $string]-1) do={
:local char [:pick $string $i]
:if ($char=$find) do={
:set char $replace
}
:set encode ($encode.$char)
}
:return $encode
}
#Build multi array from each IP octet, let default array sorting organise entries.
:local multiSorted [ :toarray "" ]
:foreach line in=$scanResult do={
:local ipParts [:toarray [$strReplace string=[:tostr [($line->"address")]] find="." replace=","]]
:local pt0 ($ipParts->0); :if ([:len $pt0]=1) do={:set pt0 ("0".$pt0)}; :if ([:len $pt0]=2) do={:set pt0 ("0".$pt0)};
:local pt1 ($ipParts->1); :if ([:len $pt1]=1) do={:set pt1 ("0".$pt1)}; :if ([:len $pt1]=2) do={:set pt1 ("0".$pt1)};
:local pt2 ($ipParts->2); :if ([:len $pt2]=1) do={:set pt2 ("0".$pt2)}; :if ([:len $pt2]=2) do={:set pt2 ("0".$pt2)};
:local pt3 ($ipParts->3); :if ([:len $pt3]=1) do={:set pt3 ("0".$pt3)}; :if ([:len $pt3]=2) do={:set pt3 ("0".$pt3)};
:set ($multiSorted->$pt0->$pt1->$pt2->$pt3) $line
}
#Break sorted multi array back down to simple array
:local scanSorted [ :toarray "" ]
:local cont 0
:foreach line0 in=$multiSorted do={
:foreach line1 in=$line0 do={
:foreach line2 in=$line1 do={
:foreach line3 in=$line2 do={
:set ($scanSorted->$cont) "$line3"
:set cont ($cont+1)
}
}
}
}
#Display Loop
:foreach line in=$scanSorted do={
:local address ($line->"address")
:local addressTarLen 15
:local macAddress ($line->"mac-address")
:local macAddressTarLen 17
:local dns ($line->"dns")
:local netbios ($line->"netbios")
:local snmp ($line->"snmp")
:local time ($line->"time")
:local url ""
:local vendor ""
:local vendorTarLen 30
:local identity ""
:local dhcp ""
:local dhcpState " "
:local interface
:local interfaceTarLen 20
:local vendor
:local vmax 1
:local vend 8
:local keyParam
:if ([:len $apiKey] > 10) do={:set keyParam "?apiKey=$apiKey"}
:if ([:len $macAddress] = 17) do={
#Get Vendor
:local secBit [:pick $macAddress 1 2 ]
:if ( ($secBit = "2") or ($secBit = "6") or ($secBit = "A") or ($secBit = "E") ) do={:set vendor "*LOCAL PRIVATE*"} else={
:set vendor "IEEE Registration Authority"
:while ( ($vendor="IEEE Registration Authority") and ($vmax < 4) ) do={
:set url ("https://api.maclookup.app/v2/macs/".[:pick $macAddress 0 $vend]."/company/name".$keyParam)
:local vendorResult; :do {:set vendorResult [/tool fetch url=$url as-value output=user]} on-error={}
:if (($vendorResult->"status")="finished") do={:set vendor ($vendorResult->"data")}
:if ([:len $apiKey] < 10) do={:delay 0.5;}; #keep inside api rate limit
:set vend ($vend+3)
:set vmax ($vmax+1)
}
}
}
:if ($vendor="IEEE Registration Authority") do={:set $vendor ""}
:if ([:len $address] > 6) do={
#Neighbour Name
:foreach neighbour in=$neighbours do={
:if ( ($macAddress=($neighbour->"mac-address")) or ($address=($neighbour->"address")) ) do={
:if ($macAddress=($neighbour->"mac-address")) do={:set identity ($neighbour->"identity")}
}
}
#ARP interfaces
:foreach arp in=$arpTable do={
:if ( ($macAddress=($arp->"mac-address")) or ($address=($arp->"address")) ) do={
:if ([:len ($arp->"interface")] > 0) do={:set interface ($arp->"interface")}
}
}
#DHCP Names
:foreach lease in=$dhcpLeases do={
:if ( ($macAddress=($lease->"mac-address")) or ($address=($lease->"address")) ) do={
:if ([:len ($lease->"host-name")] > 0) do={:set dhcp ($dhcp.($lease->"host-name")." ")}
:if ([:len ($lease->"class-id")] > 0) do={:set dhcp ($dhcp.($lease->"class-id")." ")}
:if ([:len ($lease->"comment")] > 0) do={:set dhcp ($dhcp.($lease->"comment")." ")}
}
}
#DHCP Dynamic
:foreach lease in=$dhcpLeasesDynamic do={
:if ( ($macAddress=($lease->"mac-address")) or ($address=($lease->"address")) ) do={
:set dhcpState "D"
:set countDynamic ($countDynamic + 1)
}
}
#DHCP Reservations
:foreach lease in=$dhcpLeasesReservations do={
:if ( ($macAddress=($lease->"mac-address")) or ($address=($lease->"address")) ) do={
:set dhcpState "R"
:set countReservations ($countReservations + 1)
}
}
}
:set countTotal ($countTotal + 1)
:local space1 ""; :if ([:len $snmp] > 0) do={:set space1 " "}
:local space2 ""; :if ([:len $dns] > 0) do={:set space2 " "}
:local space3 ""; :if ([:len $netbios] > 0) do={:set space3 " "}
:local space4 ""; :if ([:len $identity] > 0) do={:set space4 " "}
:local names ($snmp.$space1.$dns.$space2.$netbios.$space3.$identity.$space4.$dhcp)
:set address [$strPadTrim string=$address targetLen=$addressTarLen]
:set macAddress [$strPadTrim string=$macAddress targetLen=$macAddressTarLen]
:set interface [$strPadTrim string=$interface targetLen=$interfaceTarLen]
:set vendor [$strPadTrim string=$vendor targetLen=$vendorTarLen]
:put ("IP: $address $dhcpState| MAC: $macAddress | ARP: $interface | Vendor: $vendor | Names: $names");
:if ($saveLog = "yes") do={:set logString ($logString."IP: $address $dhcpState| MAC: $macAddress | ARP: $interface | Vendor: $vendor | Names: $names\r\n")}
}
:put ""
:put ("Found Total: $countTotal | DHCP Dynamic: $countDynamic | DHCP Reservations: $countReservations")
#Export Log
:if ($saveLog = "yes") do={
# get time
:local ts [/system clock get time]
:set ts ([:pick $ts 0 2]."-".[:pick $ts 3 5]."-".[:pick $ts 6 8])
# get Date
:local ds [/system clock get date]
#file
:local filename ("IP-Scan_".[/system/identity get value-name=name]."_".$scanInterface."_".$ds."_".$ts)
:global logExport $logString
:local writeScript ":put (\$logExport)"
/execute $writeScript file=($filename)
#clean global var
/system/script/environment remove logExport
:put ("Log File: $filename")
}