Well you could run it from an exec node but I prefer to run it from a script that runs from a CRON timer.
#! /usr/bin/env bash
# Fast scan the local network for live devices and record
# to /tmp/nmap.xml which can be used in Node-RED
#
# To run manually:
# sudo /home/home/nrmain/system/nmap_scan.sh
#
# To run via cron:
# sudo crontab -e
# 01,16,31,46 * * * * /home/home/nrmain/system/nmap_scan.sh
# Run the scan
nmap -sn --oX /tmp/nmap.xml --privileged -R --system-dns --webxml 192.168.1.0/24
# Make sure ownership & ACLs on the output are secure
chown root:home /tmp/nmap.xml
chmod --silent 640 /tmp/nmap.xml
# Trigger the Node-RED update
#curl --silent --output /dev/null 'http://localhost:1880/localnetscan' > /dev/null
curl 'http://localhost:1880/localnetscan'
This produces an XML file from the nmap command and then it triggers a flow in Node-RED using curl to an http-in node.
The Node-RED flow reads the XML file, references a global retained variable that is my master table & it updates that via a function node which also sends data out to MQTT (I've not included the mqtt-out node) and also sends to a uibuilder node (not included) on port #1. Port #2 of the function node outputs a copy of the master data to a log file (rather overkill).
[{"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","d102c6.9932fd38"]]},{"id":"75a84cfb.fcccb4","type":"xml","z":"d0860be6.7951b8","name":"","property":"payload","attr":"","chr":"","x":470,"y":320,"wires":[["35baaad9.7a1f06","9d2dadf4.f2eb5"]]},{"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\n/*\n(\n // Look for a mac address in reference array, \n * return the array object that matches \n $a := function($mac) {\n $filter($.other, function($val, $i, $array) {\n // Filter function to match mac addresses \n $val.mac.$lowercase() = $mac\n })\n };\n \n // Flatten the input data\n * Data comes from nmap: sudo nmap -sn --oX nmap.xml --privileged -R --system-dns --webxml 192.168.1.0/24\n \n payload.nmaprun.host.[(\n // Grab matching mac address data from lookup array \n $x := $a(address[1].\"$\".addr.$lowercase());\n /* Build the output \n {\n \"IP\": address[0].\"$\".addr,\n \"MAC\": address[1].\"$\".addr.$uppercase(),\n \"vendor\": address[1].\"$\".vendor,\n \"hostname\": hostnames.hostname.\"$\".name,\n // srtt is in microseconds so convert to milliseconds. /1000 again to get to seconds \n \"rtt\": $parseInteger(times.\"$\".srtt, '#0')/1000,\n \"description\": $x.description,\n \"name\": $x.hostname,\n \"dhcp\": $x.dhcp,\n \"refIP\": address[0].\"$\".addr = $x.ipaddr ? null : $x.ipaddr,\n // Add a JS timestamp for when this was run \n \"timestamp\": $parseInteger($$.payload.nmaprun.\"$\".start, '#0')*1000,\n \"y\": $x\n }\n )][0]\n)\n*/","outputs":2,"noerr":0,"x":680,"y":320,"wires":[["99ae73de.4727a","965955f1.964818","36544259.7c139e"],["5d34242a.d5290c"]],"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":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1120,"y":440,"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":"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":"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":"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":"5d34242a.d5290c","type":"file","z":"d0860be6.7951b8","name":"","filename":"/home/home/nrmain/data/newnetworkdevices.log","appendNewline":true,"createDir":false,"overwriteFile":"false","encoding":"none","x":1050,"y":400,"wires":[["a4b7b52c.4cc688"]]}]
This is a slightly complex way of doing things since you could just call the nmap command from an exec node driven by a repeating trigger node. However, I have a couple of scripts that I prefer to run outside Node-RED for performance and reliability & like the fact that you can then interface back into Node-RED.
In a similar way, I also run some IMAPFILTER scripts against my email accounts that automatically categorise/file emails and get rid of spam. IMAPFILTER uses a LUA script and this has the ability to output the stats data to a curl command - in this case, the temporary file is avoided altogether:
-- ...
-- Send to Node-RED via simple http-in/-out nodes
output, tblResult = osExecute('curl "http://localhost:1880/imapfilter?' ..
'account=' .. acct ..
'&removedFromInbox=' .. (sIbTotal - eIbTotal) ..
'&unreadInbox=' .. eIbUnseen ..
'" --silent --connect-timeout 0.1 --max-time 0.5')
print('OUTPUT: ', output, 'RESULT: ', dump(tblResult))
I'm sure that others can think of more ways to achieve the same ends - Node-RED is nothing if not flexible.