Sure, here is the flow. Actually 2 flows.
One that presents an API url that is called by the script that runs the nmap local scan so that it updates things when the scan finishes. It reads the XML file and combines it into the existing network
variable. I initially created that from a spreadsheet that I've been using for years to track the devices on my network manually. It hen sends the data to uibuilder (the second part of the flow), splits the variable object and sends each device to MQTT.
The second part takes the data and dumps it to uibuilder which displays it as an editable table, If the user updates the table, it sends back the changed record which is merged into the networks variable again. A cache is included so that when a new page is loaded, the latest device table is automatically sent through.
In addition to the network variable, there is also a schema object variable. schema.network
contains the table schema for bootstrap-vue's table component to use along with some additional metadata for formatting and the like.
[{"id":"e97f9cf0.f86dd","type":"inject","z":"d0860be6.7951b8","name":"","topic":"","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":75,"y":320,"wires":[["e16eeebb.792f8"]],"l":false},{"id":"e16eeebb.792f8","type":"file in","z":"d0860be6.7951b8","name":"","filename":"/tmp/nmap.xml","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":320,"y":320,"wires":[["75a84cfb.fcccb4"]]},{"id":"75a84cfb.fcccb4","type":"xml","z":"d0860be6.7951b8","name":"","property":"payload","attr":"","chr":"","x":470,"y":320,"wires":[["35baaad9.7a1f06"]]},{"id":"35baaad9.7a1f06","type":"function","z":"d0860be6.7951b8","name":"Update networks global var","func":"const network = global.get('network','file')\n\nconst newnetwork = {}\n\n// arr.forEach(callback(currentValue [, index [, array]])[, thisArg])\nmsg.payload.nmaprun.host.forEach( (host) => {\n let mac = ''; vendor = ''\n \n // Should only be for localhost because nmap doesn't report the mac address for that\n if (! host.address[1]) {\n if ( host.address[0].$.addr !== '192.168.1.191' ) {\n node.warn(host)\n return\n } else {\n mac = '28:D2:44:69:FD:D9'\n vendor = 'Lenovo'\n }\n } else {\n mac = host.address[1].$.addr.toUpperCase()\n vendor = host.address[1].$.vendor\n }\n \n // Create the entry if it doesn't exist\n if ( ! network.hasOwnProperty(mac) ) {\n // ID = mac for anything coming from nmap\n network[mac] = { id: mac, 'mac': mac }\n newnetwork[mac] = host\n }\n \n // Record latest IP address\n network[mac].ipaddr = host.address[0].$.addr\n // and vendor\n network[mac].vendor = vendor\n // Add a JS timestamp for when this was run \n network[mac].updated = new Date(Number.parseInt(msg.payload.nmaprun.$.start, 10)*1000).toLocaleString()\n network[mac].updatedBy = 'nmap'\n // Add host name if available\n if ( host.hostnames[0].hostname ) network[mac].host = host.hostnames[0].hostname[0].$.name\n // Add latency to host if available\n // srtt is in text in microseconds so convert to milliseconds. /1000 again to get to seconds \n if ( host.times ) network[mac].srtt = Number.parseInt(host.times[0].$.srtt, 10)/1000\n\n})\n\n// Vuetify Table\n// network.schema = [\n// {\"value\": \"mac\", text: 'MAC', dataType: 'string', eg: \"D8:6C:63:5E:41:5F\"},\n// {\"value\": \"ipaddr\", text: 'IP', dataType: 'string', eg: \"192.168.1.1\"},\n// {\"value\": \"vendor\", text: 'Vendor', dataType: 'string', eg: \"Expressif\"},\n// {\"value\": \"updated\", text: 'Updated', dataType: 'Date', eg: 1581869007000},\n// {\"value\": \"updatedBy\", text: 'By', dataType: 'string', eg: 'nmap'},\n// {\"value\": \"srtt\", text: 'Latency', dataType: 'integer', eg: 97.269},\n// {\"value\": \"host\", text: 'Host Name', dataType: 'string', eg: 'pi2.knightnet.co.uk'},\n// {\"value\": \"hostname\", text: 'Host Descr', dataType: 'string', eg: 'D1M04 - Access Point'},\n// {\"value\": \"dhcp\", text: 'DHCP', dataType: 'string', eg: 'Yes-Fixed'},\n// {\"value\": \"description\", text: 'Description', dataType: 'string', eg: \"Some Text\"}\n// ]\n\n/** bootstrap-vue table format. https://bootstrap-vue.js.org/docs/components/table#field-definition-reference\n * BV native: key, label, headerTitle\n * uibuilder: primary, dataType, format, editable, eg\n * SEE COMMENT FOR DETAILS\n **/\n//network.schema = [\nglobal.set('schemas.network', {\n id : {\"key\": \"id\", primary: true, label: 'ID', dataType: 'string', \n thClass: 'd-none', tdClass: 'd-none', // Don't display this field in tables\n eg: \"D8:6C:63:5E:41:5F\"},\n mac : {\"key\": \"mac\", label: 'MAC', dataType: 'string', \n eg: \"D8:6C:63:5E:41:5F\"},\n ipaddr : {\"key\": \"ipaddr\", label: 'IP', dataType: 'text', eg: \"192.168.1.1\"},\n vendor : {\"key\": \"vendor\", label: 'Vendor', dataType: 'text', eg: \"Expressif\"},\n updated : {\"key\": \"updated\", label: 'Updated', dataType: 'date', eg: 1581869007000},\n updatedBy : {\"key\": \"updatedBy\", label: 'By', dataType: 'text', eg: 'nmap'},\n srtt : {\"key\": \"srtt\", label: 'Latency', dataType: 'number', eg: 97.269},\n host : {\"key\": \"host\", label: 'Host Name', dataType: 'text', eg: 'pi2.knightnet.co.uk'},\n hostname : {\"key\": \"hostname\", label: 'Host Descr', dataType: 'text', editable: true,\n description: 'Device Description', eg: 'D1M04 - Access Point'},\n dhcp : {\"key\": \"dhcp\", label: 'DHCP', dataType: 'text', editable: true, \n description: 'Is IP address set from DHCP?', eg: 'Yes-Fixed'},\n description: {\"key\": \"description\", label: 'Description', dataType: 'text', editable: true, \n eg: \"Some Text\"},\n}, 'file')\n\nglobal.set('network', network, 'file')\n\nreturn [{topic: 'network', payload: network}, {topic: 'new-network', payload: newnetwork}];\n","outputs":2,"noerr":0,"x":680,"y":320,"wires":[["99ae73de.4727a","965955f1.964818","36544259.7c139e"],["a4b7b52c.4cc688"]],"inputLabels":["nmap XML converted to JSON"],"outputLabels":["Device master","Newly added devices"],"info":"## Data Schema\n\nObject of objects. Each object's property is a row column.\n\nIf passed as an object of object, MUST be turned into an array for sending to bootstrap-vue's b-table component.\n\nThe column/field schema is written to the global variable: `schema.devices`.\n\n## Column Schema\n\nObject of objects, 1 row for each column in the data array\n\n### bootstrap-vue related\n\n* `key` {string}: column ID\n* `label` {string}: Used in table and edit dialog \n* `headerTitle` {string}: Added to each columns title attrib in the col headers\n\n### uibuilder specific\n\n* `id` {string}: Unique identifier for the row\n* `primary` {boolean}: If true, this column is the primary key that uniquely identifies a data table row\n* `dataType` {string}: HTML 5 `<input>` tag type attribute.\n* `editable` {boolean}: Is this column editable?\n* `eg` {string}: Gives an example of the data in this column\n* `format`\n* `description`\n"},{"id":"99ae73de.4727a","type":"debug","z":"d0860be6.7951b8","name":"Network Device Count","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"$count($keys(payload))","targetType":"jsonata","x":1111,"y":340,"wires":[]},{"id":"a4b7b52c.4cc688","type":"debug","z":"d0860be6.7951b8","name":"New Network Devices","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":960,"y":400,"wires":[]},{"id":"f3e31d57.9735c","type":"uibuilder","z":"d0860be6.7951b8","name":"","topic":"","url":"admin","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":330,"y":560,"wires":[["176ef11c.d3b67f"],["d6ce7d82.f7ae3"]]},{"id":"54ffbc11.b95ab4","type":"link in","z":"d0860be6.7951b8","name":"admin-in","links":["d6ce7d82.f7ae3","965955f1.964818"],"x":75,"y":560,"wires":[["baaed2ef.366e9","796970ad.c231a"]]},{"id":"d6ce7d82.f7ae3","type":"link out","z":"d0860be6.7951b8","name":"admin-control-out","links":["54ffbc11.b95ab4"],"x":495,"y":600,"wires":[]},{"id":"baaed2ef.366e9","type":"function","z":"d0860be6.7951b8","name":"Cache","func":"\nif ( msg.uibuilderCtrl === 'ready for content' && msg.cacheControl === 'REPLAY' && msg.from === 'client' ) {\n /** When a client loads or reloads, send the latest data \n * NOTE that we send data/schema as an array of objects not an object\n * because that is what bootstrap-vue's b-table component requires\n */\n return {\n topic: 'network',\n payload: Object.values(global.get('network','file')),\n // Include the schema so that tables will rend correctly\n schema: Object.values(global.get('schemas.network','file')),\n // Only send to the required client\n _socketId: msg._socketId,\n }\n} else {\n /** Send the input msg to all connected clients */\n // Add the schema if required so that tables will rend correctly\n if (msg.topic === 'network') msg.schema = Object.values(global.get('schemas.network','file'))\n return msg\n}","outputs":1,"noerr":0,"x":170,"y":560,"wires":[["f3e31d57.9735c"]]},{"id":"796970ad.c231a","type":"debug","z":"d0860be6.7951b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":170,"y":520,"wires":[]},{"id":"965955f1.964818","type":"link out","z":"d0860be6.7951b8","name":"network-out","links":["54ffbc11.b95ab4"],"x":930,"y":340,"wires":[],"inputLabels":["devices object"],"l":true},{"id":"176ef11c.d3b67f","type":"function","z":"d0860be6.7951b8","name":"Upd network global var","func":"const network = global.get('network', 'file')\n\ndelete network[0]\n\nconst mac = msg.payload.mac\n\ndelete msg.payload._idx\n\nnetwork[mac] = msg.payload\n\nglobal.set('network', network, 'file')\n\nreturn msg","outputs":1,"noerr":0,"x":580,"y":540,"wires":[[]]},{"id":"36544259.7c139e","type":"split","z":"d0860be6.7951b8","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":910,"y":280,"wires":[["23cf5dd4.ee91f2"]],"info":"Split the devices object into a msg for each device for sending to MQTT."},{"id":"bf65f0bf.66113","type":"comment","z":"d0860be6.7951b8","name":"Update Current Network List","info":"Updated by a cron job to run:\n\n sudo /home/home/nrmain/system/nmap_scan.sh\n \nSee `sudo crontab -l` for details","x":160,"y":240,"wires":[]},{"id":"b60f6c72.590b6","type":"comment","z":"d0860be6.7951b8","name":"Show Current Network Device List","info":"","x":200,"y":480,"wires":[]},{"id":"185ba1a6.6563de","type":"http in","z":"d0860be6.7951b8","name":"","url":"/localnetscan","method":"get","upload":false,"swaggerDoc":"","x":130,"y":280,"wires":[["e16eeebb.792f8","740868b4.37e748"]],"info":"API to receive data from executions of imapfilter running on the same device"},{"id":"740868b4.37e748","type":"http response","z":"d0860be6.7951b8","name":"","statusCode":"","headers":{},"x":310,"y":280,"wires":[]},{"id":"5d9cdc1.68ae724","type":"mqtt out","z":"d0860be6.7951b8","name":"nrmain-local","topic":"","qos":"","retain":"","broker":"3784c9f0.57bab6","x":1210,"y":280,"wires":[]},{"id":"23cf5dd4.ee91f2","type":"change","z":"d0860be6.7951b8","name":"topic & retain","rules":[{"t":"set","p":"topic","pt":"msg","to":"'network/'&payload.id","tot":"jsonata"},{"t":"set","p":"retain","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1050,"y":280,"wires":[["5d9cdc1.68ae724"]]},{"id":"3784c9f0.57bab6","type":"mqtt-broker","z":"","name":"nrmain-local","broker":"localhost","port":"1883","clientid":"nrmain-local","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"services/nrmain","birthQos":"0","birthRetain":"true","birthPayload":"Online","closeTopic":"services/nrmain","closeQos":"0","closeRetain":"true","closePayload":"Offline","willTopic":"services/nrmain","willQos":"0","willRetain":"true","willPayload":"Offline"}]
uibuilder code. Very much a work in progress ...
Urm, too big to fit in one post so continued in the next ...