scanners/nmap/parser/parser.js
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0
const xml2js = require('xml2js');
const { get, merge } = require('lodash');
async function parse(fileContent) {
const hosts = await parseResultFile(fileContent);
return transformToFindings(hosts);
}
function transformToFindings(hosts) {
const scriptFindings = transformNMAPScripts(hosts);
const portFindings = hosts.flatMap(({ openPorts = [], ...hostInfo }) => {
if(openPorts === null){
return [];
}
return openPorts.map(openPort => {
return {
name: openPort.service ? `Open Port: ${openPort.port} (${openPort.service})`: `Open Port: ${openPort.port}`,
description: `Port ${openPort.port} is ${openPort.state} using ${openPort.protocol} protocol.`,
category: 'Open Port',
location: `${openPort.protocol}://${getHostOrIp(hostInfo)}:${openPort.port}`,
osi_layer: 'NETWORK',
severity: 'INFORMATIONAL',
attributes: {
port: openPort.port,
state: openPort.state,
ip_addresses: hostInfo.ips,
mac_address: hostInfo.mac || null,
protocol: openPort.protocol,
hostname: hostInfo.hostname,
method: openPort.method,
operating_system: hostInfo.osNmap,
service: openPort.service,
serviceProduct: openPort.serviceProduct || null,
serviceVersion: openPort.serviceVersion || null,
scripts: openPort.scriptOutputs || null,
tunnel: openPort.tunnel || null,
},
};
});
});
const hostFindings = hosts.map(({ hostname, ips, osNmap }) => {
return {
name: `Host: ${getHostOrIp({ hostname, ips})}`,
category: 'Host',
description: 'Found a host',
location: hostname,
severity: 'INFORMATIONAL',
osi_layer: 'NETWORK',
attributes: {
ip_addresses: ips,
hostname: hostname,
operating_system: osNmap,
},
};
});
return [...portFindings, ...hostFindings, ...scriptFindings];
}
function getHostOrIp(hostInfo) {
if (hostInfo.hostname) {
return hostInfo.hostname;
}
if (hostInfo.ips && hostInfo.ips.length > 0) {
return hostInfo.ips[0];
}
return "unknown-address";
}
function transformNMAPScripts(hosts) {
let scriptFindings = [];
for(const host of hosts) {
if(host.scripts) {
for(const script of host.scripts) {
// Parse Script Results
const parseFunction = scriptParser[script.$.id];
if (parseFunction) {
scriptFindings = scriptFindings.concat(parseFunction(host, script));
}
}
}
}
return scriptFindings;
}
const scriptParser = {
"ftp-anon": parseFtpAnon,
"banner": parseBanner,
"smb-protocols": parseSmbProtocols,
}
function parseFtpAnon(host, script) {
return [merge(
{
name: "Anonymous FTP Login possible",
description: `Port ${host.openPorts[0].port} allows anonymous FTP login`,
severity: 'MEDIUM',
},
parseFtpCommon(host, script)
)]
}
function parseBanner(host, script) {
return [merge(
{
name: "Server banner found",
description: `Port ${host.openPorts[0].port} displays banner`,
severity: 'INFORMATIONAL',
attributes: {
banner: script.$.output || null,
},
},
host.openPorts[0].port === 21 ? parseFtpCommon(host, script) : parseCommon(host,script)
)]
}
function parseFtpCommon(host, script) {
return {
category: 'FTP',
location: `ftp://${getHostOrIp(host)}:${host.openPorts[0].port}`,
osi_layer: 'NETWORK',
attributes: {
script: script.$.id || null,
},
}
}
function parseCommon(host, script) {
return {
category: 'TCP',
location: `tcp://${getHostOrIp(host)}:${host.openPorts[0].port}`,
osi_layer: 'NETWORK',
attributes: {
script: script.$.id || null,
},
}
}
function parseSmbProtocols(host, script) {
// Parse SMB Script Results
console.log ("Found SMB Script Result: " + script.$.output);
//console.log (script);
var scriptFindings = [];
if(script.table && script.table[0] && script.table[0].elem) {
for(const elem of script.table[0].elem) {
console.log ("Found SMB SMB Protocol: " + elem);
//console.log (elem);
const smbVersion = elem.toString().includes("SMBv1") ? 1 : parseFloat(elem);
const attributes = {
hostname: host.hostname,
mac_address: host.mac || null,
ip_addresses: host.ips,
port: host.openPorts[0].port,
state: host.openPorts[0].state,
protocol: host.openPorts[0].protocol,
method: host.openPorts[0].method,
operating_system: host.osNmap || null,
service: host.openPorts[0].service,
serviceProduct: host.openPorts[0].serviceProduct || null,
serviceVersion: host.openPorts[0].serviceVersion || null,
scripts: elem || null,
smb_protocol_version: smbVersion,
}
if(elem.toString().includes("SMBv1")) {
scriptFindings.push({
name: "SMB Dangerous Protocol Version Finding SMBv1",
description: `Port ${host.openPorts[0].port} is ${host.openPorts[0].state} using SMB protocol with an old version: SMBv1`,
category: 'SMB',
location: `${host.openPorts[0].protocol}://${getHostOrIp(host)}:${host.openPorts[0].port}`,
osi_layer: 'NETWORK',
severity: 'HIGH',
attributes: attributes
});
}
else if(!isNaN(smbVersion)) {
if(smbVersion > 0 && smbVersion < 2) {
scriptFindings.push({
name: "SMB Dangerous Protocol Version Finding v"+smbVersion,
description: `Port ${host.openPorts[0].port} is ${host.openPorts[0].state} using SMB protocol with an old version: ` + smbVersion,
category: 'SMB',
location: `${host.openPorts[0].protocol}://${getHostOrIp(host)}:${host.openPorts[0].port}`,
osi_layer: 'NETWORK',
severity: 'MEDIUM',
attributes: attributes
});
}
else if(smbVersion >= 2 && smbVersion < 3) {
scriptFindings.push({
name: "SMB Protocol Version Finding v"+smbVersion,
description: `Port ${host.openPorts[0].port} is ${host.openPorts[0].state} using SMB protocol with an old version: `+ smbVersion,
category: 'SMB',
location: `${host.openPorts[0].protocol}://${getHostOrIp(host)}:${host.openPorts[0].port}`,
osi_layer: 'NETWORK',
severity: 'LOW',
attributes: attributes
});
}
else if(smbVersion >= 3) {
scriptFindings.push({
name: "SMB Protocol Version Finding v"+smbVersion,
description: `Port ${host.openPorts[0].port} is ${host.openPorts[0].state} using SMB protocol with version: ` + smbVersion,
category: 'SMB',
location: `${host.openPorts[0].protocol}://${getHostOrIp(host)}:${host.openPorts[0].port}`,
osi_layer: 'NETWORK',
severity: 'INFORMATIONAL',
attributes: attributes
});
}
}
}
}
return scriptFindings
}
/**
* Parses a given NMAP XML file to a smaller JSON representation with the following object:
* {
* hostname: null,
* ip: null,
* mac: null,
* openPorts: null,
* osNmap: null,
* scripts: null
* }
* @param {*} fileContent
*/
function parseResultFile(fileContent) {
return new Promise((resolve, reject) => {
xml2js.parseString(fileContent, (err, xmlInput) => {
if (err) {
reject(new Error('Error converting XML to JSON in xml2js: ' + err));
} else {
let tempHostList = [];
if (!xmlInput.nmaprun.host) {
resolve([]);
return;
}
xmlInput = xmlInput.nmaprun.host;
tempHostList = xmlInput.map(host => {
const newHost = {
hostname: null,
ip: null,
mac: null,
openPorts: null,
osNmap: null,
scripts: null
};
if (host.status && host.status?.[0]?.$?.state === 'down') {
return null;
}
// Get hostname
if (
host.hostnames &&
host.hostnames[0] !== '\r\n' &&
host.hostnames[0] !== '\n'
) {
newHost.hostname = host.hostnames[0].hostname[0].$.name;
}
const cleanAddresses = host.address.map(address => {
return {
type: address.$.addrtype,
address: address.$.addr,
vendor: address.$.vendor
};
});
newHost.mac = cleanAddresses.find((address) => address.type === "mac")?.address;
newHost.ips = cleanAddresses
.filter((address) => address.type.startsWith("ip"))
.map((address) => address.address);
// Get ports
if (host.ports && host.ports[0].port) {
const portList = host.ports[0].port;
const openPorts = portList.filter(port => {
return port.state[0].$.state !== 'closed';
});
newHost.openPorts = openPorts.map(portItem => {
// console.log(JSON.stringify(portItem, null, 4))
const port = parseInt(portItem.$.portid, 10);
const protocol = portItem.$.protocol;
const service = get(portItem, ["service",0,"$","name"]);
const serviceProduct = get(portItem, ["service",0,"$","product"]);
const serviceVersion = get(portItem, ["service",0,"$","version"]);
const tunnel = get(portItem, ["service",0,"$","tunnel"]);
const method = get(portItem, ["service",0,"$","method"]);
const product = get(portItem, ["service",0,"$","tunnel"]);
const state = portItem.state[0].$.state;
let scriptOutputs = null;
if (portItem.script) {
scriptOutputs = portItem.script.reduce(
(carry, { $: scriptRes }) => {
carry[scriptRes.id] = scriptRes.output;
return carry;
},
{}
);
}
let portObject = {};
if (port) portObject.port = port;
if (protocol) portObject.protocol = protocol;
if (service) portObject.service = service;
if (serviceProduct) portObject.serviceProduct = serviceProduct;
if (serviceVersion) portObject.serviceVersion = serviceVersion;
if (tunnel) portObject.tunnel = tunnel;
if (method) portObject.method = method;
if (product) portObject.product = product;
if (state) portObject.state = state;
if (scriptOutputs) portObject.scriptOutputs = scriptOutputs;
return portObject;
});
}
// Get Script Content
if(host.hostscript && host.hostscript[0].script) {
newHost.scripts = host.hostscript[0].script
}
// Get Script Content in case the script is of the port-rule type,
// and thus has the script under 'port' instead of 'hostscript'.
else if(host.ports && host.ports[0].port){
for (let i=0; i < host.ports[0].port.length; i++){
if ((host.ports[0].port)[i].script) {
newHost.scripts = host.ports[0].port[i].script
}
}
}
if (host.os && host.os[0].osmatch && host.os[0].osmatch[0].$.name) {
newHost.osNmap = host.os[0].osmatch[0].$.name;
}
return newHost;
}).filter(Boolean);
resolve(tempHostList);
}
});
});
}
module.exports.parse = parse;