├── .travis.yml ├── .gitignore ├── pics ├── window.png └── the_matrix.gif ├── nodescan.service ├── lib ├── mac.js ├── getHostInfo.js ├── is_sudo.js ├── arpscan.js ├── getHostName.js ├── page.js ├── nmap.js └── database.js ├── package.json ├── README.md └── bin └── server.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | old 4 | *network_db.json 5 | -------------------------------------------------------------------------------- /pics/window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllGloryToTheHypnotoad/nodescan/HEAD/pics/window.png -------------------------------------------------------------------------------- /pics/the_matrix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllGloryToTheHypnotoad/nodescan/HEAD/pics/the_matrix.gif -------------------------------------------------------------------------------- /nodescan.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | ExecStart=/usr/local/bin/nodescan -d eth0 -l /var/run 3 | Restart=always 4 | StandardOutput=syslog 5 | StandardError=syslog 6 | SyslogIdentifier=nodescan 7 | User=root 8 | Group=root 9 | Environment=NODE_ENV=production 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /lib/mac.js: -------------------------------------------------------------------------------- 1 | // Mac addr lookup 2 | 3 | var request = require('request'); 4 | 5 | // get vendor name from mac addr, arp-scan/nmap don't always get the correct thing 6 | // or anything at all 7 | // input: mac (mac addr) 8 | // output: vender string 9 | function macVendorLookup(mac){ 10 | var ret = ''; 11 | return new Promise(function(resolve,reject){ 12 | request('http://api.macvendors.com/'+mac, function (error, response, body) { 13 | if (!error && response.statusCode == 200) { 14 | ret = body; 15 | } 16 | resolve(ret); 17 | }); 18 | }); 19 | } 20 | 21 | module.exports = macVendorLookup; -------------------------------------------------------------------------------- /lib/getHostInfo.js: -------------------------------------------------------------------------------- 1 | // Use os specific programs to determine host info 2 | 3 | var os = require('os'); // OS access 4 | 5 | // arp-scan and nmap don't get localhost info 6 | 7 | function getHostInfo(){ 8 | var localhostInfo = {hostname: os.hostname(), vendor: '', timestamp: new Date(), status:'up', tcp: {}, udp: {}}; 9 | // get local info 10 | var ifaces = os.networkInterfaces(); 11 | for(var i in ifaces){ 12 | var eth = ifaces[i]; 13 | for(var info in eth){ 14 | var dev = eth[info]; 15 | if(dev.family === 'IPv4' && dev.internal === false) 16 | localhostInfo.ip = dev.address; 17 | localhostInfo.mac = dev.mac.toUpperCase(); 18 | if(dev.family === 'IPv6' && dev.internal === false) 19 | localhostInfo.ipv6 = dev.address; 20 | } 21 | } 22 | return localhostInfo; 23 | } 24 | 25 | module.exports = getHostInfo; 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/is_sudo.js: -------------------------------------------------------------------------------- 1 | // determine if running as root 2 | var spawn = require('child_process'); 3 | var debug = require('debug')('kevin:is_sudo'); // debugging 4 | /* 5 | simple example: 6 | 7 | is_sudo().then(function(response){ 8 | if( response === false ) { 9 | console.log('Sorry, you have to have root privileges, use sudo or run as root'); 10 | process.exit(); 11 | } 12 | }); 13 | 14 | 15 | output: bool (true - root/sudo, false - some other user) 16 | */ 17 | function is_sudo(){ 18 | var ans = false; 19 | return new Promise(function(response,reject){ 20 | spawn.exec('whoami', function (err, stdout, stderr){ 21 | if (err) { 22 | console.log("is_sudo error: " + 23 | err.code + stderr); 24 | } 25 | var usr = stdout.replace('\n',''); 26 | if(usr === 'root') ans = true; 27 | debug('user is: '+usr); 28 | response(ans); 29 | }); 30 | }); 31 | } 32 | 33 | 34 | module.exports = is_sudo; 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodescan", 3 | "version": "0.7.1", 4 | "description": "Local network scanner with web interface", 5 | "bin": { 6 | "nodescan": "./bin/server.js" 7 | }, 8 | "main": "bin/server.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 0", 11 | "debug": "sudo DEBUG=* ./bin/server.js -d en0", 12 | "start": "sudo ./bin/server.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/walchko/nodescan.git" 17 | }, 18 | "keywords": [ 19 | "arp-scan", 20 | "network", 21 | "scanner", 22 | "nmap", 23 | "mapper" 24 | ], 25 | "author": "Kevin J. Walchko", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/walchko/nodescan/issues" 29 | }, 30 | "homepage": "https://github.com/walchko/nodescan#readme", 31 | "dependencies": { 32 | "commander": "^2.9.0", 33 | "debug": "^2.2.0", 34 | "http": "0.0.0", 35 | "request": "^2.67.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/arpscan.js: -------------------------------------------------------------------------------- 1 | // Use arp-scan to find hosts on the network 2 | var spawn = require('child_process'); 3 | 4 | /* 5 | Uses arp-scan to detect hosts on the local network 6 | 7 | arp-scan -l I en0 8 | 9 | where: 10 | 11 | -l use local host info to determine network parameters 12 | -I use the network interface 13 | 14 | input: ops {dev: iface } 15 | output: array of hosts: [{ip,mac,vendor},{ip,mac,vendor}, ...] 16 | */ 17 | function scan(ops){ 18 | var ans = []; 19 | return new Promise(function(response,reject){ 20 | spawn.exec('arp-scan -l -I ' + ops.dev, function (err, stdout, stderr){ 21 | if (err) { 22 | console.log("child processes failed with error code: " + 23 | err.code + stderr); 24 | } 25 | var net = stdout.split('\n'); 26 | for(var i=2;i'; 6 | else return ''; 7 | } 8 | 9 | // Build each row of the table 10 | function line(a,b,c,d){ 11 | // return '' + a + '' + b + '' + c + '' + d + 'TCP'; 12 | return '' + a + '' + b + '' + c + '' + d + ''; 13 | } 14 | 15 | 16 | // build the table of network info 17 | // input: info (a list of hosts sorted by ip addr) 18 | // output: html table 19 | function table(info){ 20 | // if nothing yet ... tell user still scanning network 21 | if (info.length === 0) return '

Still scanning network

'; 22 | 23 | // ok, found something, so put it in a table 24 | var t = ''; 25 | for(var i=0;i'; 68 | page += table(info); 69 | 70 | // page += ''; 74 | page +=''; 75 | return page; 76 | }; -------------------------------------------------------------------------------- /lib/nmap.js: -------------------------------------------------------------------------------- 1 | // Use nmap to scan ports and network 2 | var debug = require('debug')('kevin:nmap'); // debugging 3 | var spawn = require('child_process'); // command line 4 | 5 | // Empty class holding the various nmap commands 6 | var Nmap = function(){} 7 | 8 | /* 9 | Use nmap to scan a network for hosts and return their info 10 | 11 | nmap -sn 192.168.1.1/24 12 | 13 | Starting Nmap 7.00 ( https://nmap.org ) at 2016-01-05 11:15 MST 14 | Nmap scan report for 192.168.1.1 15 | Host is up (0.0041s latency). 16 | MAC Address: 6C:70:9F:CE:DA:85 (Apple) 17 | Nmap scan report for 192.168.1.2 18 | Host is up (0.0041s latency). 19 | MAC Address: C8:2A:14:1F:18:69 (Apple) 20 | Nmap scan report for 192.168.1.6 21 | Host is up (0.0066s latency). 22 | MAC Address: 34:12:98:03:B3:B8 (Apple) 23 | 24 | input: opts {range: 192.168.1.1/24 } 25 | output: array of hosts: [{ip,mac,vendor},{ip,mac,vendor}, ...] 26 | */ 27 | Nmap.prototype.scanNetwork = function(opts){ 28 | return new Promise( function(resolve,reject){ 29 | var ans = []; 30 | // debug('nmap start -----------------------------------------'); 31 | opts = opts || {range: '192.168.1.1/24'}; 32 | var cmd = 'nmap -sn ' + opts.range; 33 | spawn.exec(cmd, function (err, stdout, stderr){ 34 | if (err) { 35 | debug(opts.host + ', error code: ' + err.code + stderr); 36 | reject( ans ); // return empty array 37 | } 38 | var info = stdout.split('\n'); 39 | // there is always an empty line and summary at the end, so only go to length-2 40 | var i = 0; 41 | while(i < info.length-3){ 42 | var host = {}; 43 | var line = info[i].split(' '); 44 | if(line[0] === 'Nmap'){ 45 | host.ip = line[4]; 46 | line = info[i+2].split(' '); 47 | if(line[0] === 'MAC'){ 48 | host.mac = line[2]; 49 | host.vender = info[i+2].split('(')[1].replace(')',''); // some strings are multiple words with spaces in them 50 | ans.push(host); // only add if we get everything 51 | i += 3; 52 | } 53 | else i++; 54 | } 55 | else i++; 56 | } 57 | resolve( ans ); 58 | }); 59 | }); 60 | } 61 | 62 | 63 | /** 64 | Use promise to scan and return the nmap response, an array of dicts 65 | 66 | input: opts {ports: '1-100', host: '10.10.1.1'} 67 | output: [{"port":"53","protocol":"domain"}] or [] 68 | */ 69 | Nmap.prototype.scanPorts = function(opts){ 70 | return new Promise( function(resolve,reject){ 71 | var ans = []; 72 | // debug('nmap start -----------------------------------------'); 73 | opts = opts || {ports:'1-3000', host: '192.168.1.1'}; 74 | var cmd = 'nmap -p' + opts.ports + ' ' + opts.host + ' | grep tcp'; 75 | spawn.exec(cmd, function (err, stdout, stderr){ 76 | if (err) { 77 | debug(opts.host + ', error code: ' + err.code + stderr); 78 | reject( ans ); // return empty array 79 | } 80 | var ports = stdout.split('\n'); 81 | ans.push( opts.host ); 82 | // there is always an empty line at the end, so only go to length-1 83 | for(var i=0;i Http server port number, default: 8888 53 | -u, --update [seconds] update time for arp-scan, default: 60 sec 54 | 55 | 56 | ## Setup 57 | 58 | For RPi, install this in `/etc/systemd/system/nodescan.service`, this will ensure it runs at start up. 59 | 60 | [Service] 61 | ExecStart=/usr/local/bin/nodescan -d eth0 -l /var/run 62 | Restart=always 63 | StandardOutput=syslog 64 | StandardError=syslog 65 | SyslogIdentifier=nodescan 66 | User=root 67 | Group=root 68 | Environment=NODE_ENV=production 69 | 70 | [Install] 71 | WantedBy=multi-user.target 72 | 73 | Then do: 74 | 75 | sudo systemctl enable nodescan.service 76 | sudo systemctl start nodescan.service 77 | 78 | Now use a browser to go to `:8888` and see the results. 79 | 80 | 81 | You can also use `sudo systemctl start|stop|status nodescan.service` to start, stop, or find the current status of the server. 82 | 83 | ## To Do 84 | 85 | * Scan hosts for open ports (easy) and figure out a smart way to put that on the web page (harder) 86 | * Save/recover network database from file, [having issues reading file back in] 87 | * Do I need to show the mac addr? Is there a better way to do that? 88 | * Maybe put a json interface? 89 | * Turn on/off web interface, then just use json to get info? 90 | * Add tests 91 | 92 | ## Change Log 93 | 94 | | Version | Date | Comments | 95 | |---------|-----------|----------| 96 | | 0.7.1 | 5 Nov 16 | Fixed file error | 97 | | 0.7.0 | 17 Jul 16 | Fixed readme | 98 | | 0.6.0 | 9 Jan 16 | Fixed MAC/IP issues with changing addresses, ensure root/sudo privileges, user define file save location | 99 | | 0.5.0 | 9 Jan 16 | Clean-up and fixes | 100 | | 0.4.0 | 8 Jan 16 | Clean-up and fixes | 101 | | 0.3.0 | 6 Jan 16 | Clean-up and fixes, still have a file error to fix | 102 | | 0.2.0 | 3 Jan 16 | Clean-up and fixes | 103 | | 0.1.0 | 1 Jan 16 | Initial commit | 104 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var debug = require('debug')('kevin:main'); // debugging 4 | //var chalk = require('chalk'); // colors 5 | var program = require('commander'); // CLI access 6 | var http = require('http'); // http-server 7 | 8 | // local packages 9 | var localhostInfo = require('../lib/getHostInfo.js'); 10 | var page = require('../lib/page.js'); // render a webpage 11 | var arpscan = require('../lib/arpscan.js'); // perform the arp scan 12 | var is_sudo = require('../lib/is_sudo.js'); // check for root privileges 13 | var db = require('../lib/database.js').DataBase; // data base 14 | var os = require('os'); // need for finding home directory 15 | 16 | // grab info from npm package 17 | var pck = require('../package.json'); 18 | 19 | // check privileges 20 | is_sudo().then(function(response){ 21 | if( response === false ) { 22 | console.log('Sorry, you have to have root privileges, use sudo or run as root'); 23 | console.log(response); 24 | process.exit(); 25 | } 26 | }); 27 | 28 | program 29 | .version(pck.version) 30 | .description(pck.description) 31 | .usage(pck.name + ' [options]') 32 | .option('-d, --dev [interface]','network interface to use for scan, default: en1', 'en1') 33 | .option('-i, --invert','invert webpage colors for darker screen, default: white background') 34 | .option('-l, --loc [location]','save file location, default location: ~', os.homedir()+'/') 35 | .option('-p, --port ','Http server port number, default: 8888',parseInt,8888) 36 | .option('-u, --update [seconds]','update time for arp-scan, default: 60 sec', parseInt, 60) 37 | .parse(process.argv); 38 | 39 | console.log('Starting nodescan on interface: '+program.dev+' every '+program.update); 40 | 41 | // console.log('main pass: '+program.invert); 42 | 43 | var options = {'dev': program.dev}; 44 | var scan = []; 45 | 46 | // ----- not using nmap yet ---------- 47 | // var nmap = require('../lib/nmap.js').Nmap; 48 | // 49 | // nmap.scanNetwork().then(function(res){ 50 | // for(i in res)debug(res[i]); 51 | // }); 52 | 53 | 54 | 55 | /* 56 | Iterate through scan and add new mac addresses into db dictionary 57 | I am having a hell of time with this damn non-blocking shit, it may take a reload or two 58 | to get the vendor name right. 59 | 60 | pi@hypnotoad ~ $ avahi-resolve-address 192.168.1.8 61 | 192.168.1.8 calculon.local 62 | 63 | [kevin@Tardis archeyjs]$ dig +short -x 192.168.1.3 -p 5353 @224.0.0.251 64 | Tardis.local. 65 | 66 | Flow: 67 | 1. reset host up flag to down 68 | 2. iterate through scan results to see if new host found 69 | - if host exists, mark it as up 70 | - if host doesn't exist, add to db 71 | 3. iterate through db and update vendor id if needed (should only have to do once) 72 | 4. iterate through db and update hostname if needed (should only have to do once) 73 | - linux uses avahi tools 74 | - osx uses dig 75 | 5. iterate through db and scan host for open tcp/udp ports 76 | 77 | */ 78 | 79 | var saveFile = program.loc+'network_db.json' 80 | 81 | function mapNetwork(){ 82 | debug('network scan start ...'); 83 | arpscan(options).then(function(response){ 84 | scan = response; 85 | debug('Scan done: '+scan.length+' hosts found'); 86 | db.update( scan ); // pops hosts off scan, so it is 0 87 | db.write(saveFile); 88 | }); 89 | } 90 | 91 | var hostinfo = localhostInfo(); 92 | // console.log(hostinfo); 93 | 94 | db.read(saveFile); 95 | db.push( hostinfo ); 96 | mapNetwork(); 97 | 98 | setInterval(function(){ 99 | mapNetwork(); 100 | }, program.update*1000); 101 | 102 | 103 | 104 | // Simple REST server 105 | var server = http.createServer(function(req, res){ 106 | var path = req.url; 107 | // debug( 'path: ' + path ); 108 | 109 | if ( path == '/' ){ 110 | if (req.method == 'GET') { 111 | res.writeHead(200,{'Content-Type': 'text/html'}); 112 | res.write(page(db.getSortedList(),program.invert)); 113 | // debug(db.get()); 114 | res.end(); 115 | } 116 | else if (req.method == 'PUT') { 117 | req.on('data', function(chunk) { 118 | debug( chunk.toString() ); 119 | }); 120 | } 121 | } 122 | // return error 123 | else { 124 | // force users to / 125 | debug("Wrong path " + path) 126 | res.writeHead(404, "Not Found", {'Content-Type': 'text/html'}); 127 | res.write("Wrong path: use http://localhost:"+program.port+"\n"); 128 | res.end(); 129 | } 130 | }); 131 | 132 | // start web server 133 | server.listen(program.port); 134 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | // Network database (db) 2 | var debug = require('debug')('kevin:database'); // debugging 3 | var getMac = require('../lib/mac.js'); // get vendor names from mac addr 4 | var nmap = require('../lib/nmap.js'); // use nmap to scan ports 5 | var fs = require('fs'); // get access to filing system 6 | var getHostName = require('../lib/getHostName.js'); // use os tools to get host name 7 | var spawn = require('child_process'); 8 | 9 | /* 10 | private database 11 | 12 | [ { ip: '192.168.1.2', 13 | vendor: 'APPLE, INC.', 14 | hostname: 'Dalek.local.\n', 15 | timestamp: Tue Jan 05 2016 09:26:48 GMT-0700 (MST), 16 | mac: 'C8:2A:14:1F:18:69', 17 | status: 'down', 18 | tcp: {}, 19 | udp: {} }, 20 | { ip: '192.168.1.6', 21 | vendor: 'APPLE, INC.', 22 | hostname: 'Airport-New.local.\n', 23 | timestamp: Tue Jan 05 2016 09:26:48 GMT-0700 (MST), 24 | mac: '34:12:98:03:B3:B8', 25 | status: 'down', 26 | tcp: {}, 27 | udp: {} } ] 28 | */ 29 | var db = {}; 30 | 31 | // Instantiate as: var obj = new DataBase() 32 | // This class is an abstract which could be replaced with a real database if 33 | // needed because of large number of hosts on network. 34 | var DataBase = function(){} 35 | 36 | // DataBase.prototype.getdb() = function(){return db;} 37 | 38 | // return an array sorted by IP addr 39 | DataBase.prototype.getSortedList = function(){ 40 | var s = []; 41 | Object.keys(db).forEach(function(key){ 42 | s.push(db[key]); 43 | }); 44 | s.sort(function(a,b){ 45 | var aa = parseInt(a.ip.split('.')[3]); 46 | var bb = parseInt(b.ip.split('.')[3]); 47 | return aa - bb; 48 | }); 49 | // console.log(s); 50 | return s; 51 | } 52 | 53 | // update the db with a scan 54 | // input: scan (an array of found hosts: [{ip,mac,name},{...]) 55 | DataBase.prototype.update = function(scan){ 56 | // clear status flag 57 | Object.keys(db).forEach(function(key){ 58 | db[key].status='down'; 59 | }); 60 | 61 | // pull hosts out of the scan, so scan will be empty at the end of function 62 | // add host to db if mac not found, otherwise just mark it as up 63 | while(scan.length > 0){ 64 | var host = scan.pop(); 65 | if( host['mac'] in db ){ 66 | host['mac'].status = 'up'; 67 | 68 | // sometimes I get an initially wrong mac/ip matchup, this is to 69 | // clear it out if the ip addresses don't match, reset them them to 70 | // the latest 71 | if(host.ip === db[host.mac].ip); 72 | else{ 73 | db[host.mac].ip = host.ip; 74 | db[host.mac].hostname = ''; 75 | db[host.mac].vendor = ''; 76 | } 77 | } 78 | else { 79 | // db[ host['mac'] ] = {'ip': host['ip'], 'vendor': host['vendor']}; 80 | db[ host['mac'] ] = {'ip': host['ip'], 'vendor': '', 'hostname': '', 'timestamp': new Date(), 'mac': host['mac'], 'status':'up', 'tcp': {}, 'udp': {}}; 81 | } 82 | } 83 | 84 | // if vendor is empty, then get it 85 | Object.keys(db).forEach(function(key){ 86 | // only search if vendor is empty 87 | if(db[key].vendor === ''){ 88 | getMac(key).then(function(response){ db[key]['vendor']=response; }); 89 | } 90 | }); 91 | 92 | // if hostname is empty then get it 93 | Object.keys(db).forEach(function(key){ 94 | if(db[key].hostname === ''){ 95 | getHostName( db[key].ip ).then( function (response){ db[key].hostname = response; } ); 96 | } 97 | }); 98 | ; 99 | } 100 | 101 | // used to add localhost to db 102 | DataBase.prototype.push = function(host){ 103 | db[host['mac']] = host; 104 | } 105 | 106 | // is this really useful for my use cases? 107 | DataBase.prototype.getHost = function(mac){ 108 | return db[mac]; 109 | } 110 | 111 | // write to a file 112 | DataBase.prototype.write = function(file){ 113 | var s = JSON.stringify(db); 114 | fs.writeFileSync(file, s); 115 | } 116 | 117 | // read db from a file 118 | DataBase.prototype.read = function(file){ 119 | try{ 120 | fs.statSync(file); // file exists 121 | var p = require(file); 122 | if(Object.keys(obj).length > 0) db = p; // not empty 123 | } 124 | catch(err){ 125 | debug('Error: '+file+' not found'); 126 | fs.writeFileSync(file,JSON.stringify({})); 127 | debug('Error: created empty '+file); 128 | // debug(err); 129 | return; 130 | } 131 | } 132 | 133 | // print out db to console 134 | DataBase.prototype.console = function(){ 135 | console.log(JSON.stringify(db, null, 4)); 136 | } 137 | 138 | // get copy of entire database 139 | DataBase.prototype.get = function(){ 140 | return db; 141 | } 142 | 143 | //module.exports = DataBase; 144 | module.exports.DataBase = new DataBase(); 145 | --------------------------------------------------------------------------------