diff --git a/README.md b/README.md index adc4e3f..f3a0328 100644 --- a/README.md +++ b/README.md @@ -90,31 +90,31 @@ Options: ``` ### [network-graph.js](./scripts/network-graph.js) -**RAM:** 3.80 GB +**RAM:** 3.85 GB Scan the network for devices and display as an ASCII tree. ``` -[home ~/]> run /scripts/network-graph.js -h -Running script with 1 thread(s), pid 138 and args: ["-h"]. +[home ~/]> run /scripts/network-graph.js --help +Running script with 1 thread(s), pid 138 and args: ["--help"]. /scripts/network-graph.js: Scan the network for devices and display as an ASCII tree: + home ├─ n00dles (ROOTED) - | └─ max-hardware (80|1) - | └─ neo-net (50|1) + | └─ max-hardware (80|1) + | └─ neo-net (50|1) ├─ foodnstuff (ROOTED) └─ sigma-cosmetics (ROOTED) -Usage: run network-graph.js - run network-graph.js [OPTIONS] TARGET +Usage: run network-graph.js [OPTIONS] run network-graph.js --help - TARGET Starting point to scan from, defaults to home - Options: - -d --depth=num Depth to scan for devices to, defaults to 3 - -v --verbose Displays "ROOTED" or the required hack level & ports: (level|port) - -h --help Display help message + -d --depth Depth to scan to, defaults to 3 + -f --filter Display path to single device + -s --start Point to start scan from, defaults to current machine + -v --verbose Displays the required hack level & ports needed to root: (level|port) + -h --help Display this help message ``` ### [node-manager.js](./scripts/node-manager.js) diff --git a/scripts/lib/arg-parser.js b/scripts/lib/arg-parser.js new file mode 100644 index 0000000..b58eaf0 --- /dev/null +++ b/scripts/lib/arg-parser.js @@ -0,0 +1,91 @@ +export class ArgError extends Error {} + +export class ArgParser { + /** + * Create a unix-like argument parser to extract flags from the argument list. Can also create help messages. + * @param name {string} - Script name + * @param desc {string} - Help text desciption + * @param examples {string[]} - Help text examples + * @param argList {name: string, desc: string, flags: string[], type: string, default: any}[] - Array of CLI arguments + */ + constructor(name, desc, examples, argList) { + this.name = name ?? 'example.js'; + this.description = desc ?? 'Example description'; + this.examples = [ + ...examples, + `[OPTIONS] ${argList.filter(arg => !arg.flags).map(arg => arg.name.toUpperCase())}`, + '--help' + ]; + this.argList = [ + ...argList, + {name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], type: 'bool'} + ]; + } + + /** + * Parse an array into an arguments dictionary using the configuration. + * @param args {string[]} - Array of arguments to be parsed + * @returns {object} - Dictionary of arguments with defaults applied + */ + parse(args) { + // Parse arguments + const queue = [...args], extra = []; + const parsed = this.argList.reduce((acc, arg) => ({...acc, [arg.name]: arg.default ?? (arg.type == 'bool' ? false : null)}), {}); + // Flags + while(queue.length) { + let parse = queue.splice(0, 1)[0]; + if(parse[0] == '-') { + // Check combined flags + if(parse[1] != '-' && parse.length > 2) { + parse = `-${parse[1]}`; + queue = parse.substring(1).split('').map(a => `-${a}`).concat(queue); + } + // Find & add flag + const arg = this.argList.find(arg => arg.flags && arg.flags.includes(parse)); + if(arg == null) throw new ArgError(`Unknown option: ${parse}`); + const value = arg.type == 'bool' ? true : parse.split('=')[1] || queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0]; + if(value == null) throw new ArgError(`Option missing value: ${arg.name}`); + parsed[arg.name] = value; + } else { + // Save for required parsing + extra.push(parse); + } + } + // Arguments + this.argList.filter(arg => !arg.flags).forEach(arg => { + if(!extra.length) throw new ArgError(`Argument missing: ${arg.name}`); + parsed[arg.name] = extra.splice(0, 1)[0]; + }); + // Extras + if(extra.length) parsed['extra'] = extra; + if(parsed['help']) throw new ArgError(); + return parsed; + } + + /** + * Create help message from the provided description, examples & argument list. + * @param message {string} - Message to display, defaults to the description + * @returns {string} - Help message + */ + help(msg) { + // Description + let message = '\n\n' + (msg ? msg : this.description); + // Usage + if(this.examples.length) message += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t'); + // Arguments + const req = this.argList.filter(a => !a.flags); + if(req.length) message += '\n\n\t' + req.map(arg => { + const padding = 3 - ~~(arg.name.length / 8); + return `${arg.name.toUpperCase()}${Array(padding).fill('\t').join('')} ${arg.desc}`; + }).join('\n\t'); + // Flags + const opts = this.argList.filter(a => a.flags); + if(opts.length) message += '\n\nOptions:\n\t' + opts.map(a => { + const flgs = a.flags.join(' '); + const padding = 3 - ~~(flgs.length / 8); + return `${flgs}${Array(padding).fill('\t').join('')} ${a.desc}`; + }).join('\n\t'); + // Print final message + return `${message}\n\n`; + } +} diff --git a/scripts/network-graph.js b/scripts/network-graph.js index 5e2342d..dcec446 100644 --- a/scripts/network-graph.js +++ b/scripts/network-graph.js @@ -1,122 +1,71 @@ -class ArgParser { - /** - * Create a unix-like argument parser to extract flags from the argument list. Can also create help messages. - * @param opts - {examples: string[], arguments: {key: string, alias: string, type: string, optional: boolean, desc: string}[], desc: string} - */ - constructor(opts) { - this.examples = opts.examples ?? []; - this.arguments = opts.args ?? []; - this.description = opts.desc; - } - - /** - * Parse the list for arguments & create a dictionary. - * @param args {any[]} - Array of arguments - * @returns Dictionary of matched flags + unmatched args under 'extra' - */ - parse(args) { - const req = this.arguments.filter(a => !a.optional && !a.skip); - const queue = [...args], parsed = {}, extra = []; - for(let i = 0; i < queue.length; i++) { - if(queue[i][0] != '-') { - extra.push(queue[i]); - continue; - } - let value = null, parse = queue[i].slice(queue[i][1] == '-' ? 2 : 1); - if(parse.indexOf('=')) { - const split = parse.split('='); - parse = split[0]; - value = split[1]; - } - let arg = this.arguments.find(a => a.key == parse) ?? this.arguments.find(a => a.alias == parse); - if(!arg) { - extra.push(queue[i]); - continue; - } - if(!value) { - value = arg.type == 'bool' ? true : queue[i + 1]; - if(arg.type != 'bool') i++; - } - parsed[arg.key] = value; - } - req.forEach((a, i) => parsed[a.key] = extra[i]); - extra.splice(0, req.length); - return {...parsed, extra}; - } - - /** - * Create a help message of the expected paramters & usage. - * @param msg {String} - Optional message to display with help - */ - help(msg) { - let message = '\n\n'; - message += msg ? msg : this.description; - if(this.examples.length) message += '\n\nUsage:\t' + this.examples.join('\n\t'); - const required = this.arguments.filter(a => !a.optional); - if(required.length) message += '\n\n\t' + required.map(a => { - const padding = 3 - ~~(a.key.length / 8); - return `${a.key}${Array(padding).fill('\t').join('')} ${a.desc}`; - }).join('\n\t'); - const optional = this.arguments.filter(a => a.optional); - if(optional.length) message += '\n\nOptions:\n\t' + optional.map(a => { - const flgs = `${a.alias ? `-${a.alias} ` : ''}--${a.key}${a.type && a.type != 'bool' ? `=${a.type}` : ''}`; - const padding = 3 - ~~(flgs.length / 8); - return `${flgs}${Array(padding).fill('\t').join('')} ${a.desc}`; - }).join('\n\t'); - return `${message}\n\n`; - } -} +import {ArgError, ArgParser} from './scripts/lib/arg-parser'; export async function main(ns) { + // Setup ns.disableLog('ALL'); - - // Initilize script arguments - const argParser = new ArgParser({ - desc: 'Scan the network for devices and display as an ASCII tree:\n ├─ n00dles (ROOTED)\n | └─ max-hardware (80|1)\n | └─ neo-net (50|1)\n ├─ foodnstuff (ROOTED)\n └─ sigma-cosmetics (ROOTED)', - examples: [ - 'run network-graph.js', - 'run network-graph.js [OPTIONS] TARGET', - 'run network-graph.js --help', - ], - args: [ - {key: 'TARGET', desc: 'Starting point to scan from, defaults to home'}, - {key: 'depth', alias: 'd', type: 'num', optional: true, desc: 'Depth to scan for devices to, defaults to 3'}, - {key: 'verbose', alias: 'v', type: 'bool', optional: true, desc: 'Displays "ROOTED" or the required hack level & ports: (level|port)'}, - {key: 'help', alias: 'h', type: 'bool', optional: true, desc: 'Display help message'}, - ] - }); - const args = argParser.parse(ns.args); - if(args['help']) return ns.tprint(argParser.help()); - const start = args['TARGET'] || 'home'; - const mDepth = args['depth'] || 3; + const argParser = new ArgParser('network-graph.js', 'Scan the network for devices and display as an ASCII tree:\n home\n ├─ n00dles (ROOTED)\n | └─ max-hardware (80|1)\n | └─ neo-net (50|1)\n ├─ foodnstuff (ROOTED)\n └─ sigma-cosmetics (ROOTED)', [], [ + {name: 'depth', desc: 'Depth to scan to, defaults to 3', flags: ['-d', '--depth'], default: Infinity, type: 'num'}, + {name: 'filter', desc: 'Display path to single device', flags: ['-f', '--filter'], type: 'string'}, + {name: 'start', desc: 'Point to start scan from, defaults to current machine', flags: ['-s', '--start'], default: ns.getHostname(), type: 'string'}, + {name: 'verbose', desc: 'Displays the required hack level & ports needed to root: (level|port)', flags: ['-v', '--verbose'], type: 'bool'}, + ]); + let args; + try { + args = argParser.parse(ns.args); + } catch(err) { + if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); + throw err; + } /** * Recursively search network & build a tree * @param host {string} - Point to scan from * @param depth {number} - Current scanning depth - * @param maxDepth {number} - Depth to scan to * @param blacklist {String[]} - Devices already discovered * @returns Dicionary of discovered devices */ - function scan(host, depth = 1, maxDepth = mDepth, blacklist = [host]) { - if(depth > maxDepth) return {}; + function scan(host, depth = 1, blacklist = [host]) { + if(depth >= args['depth']) return {}; const localTargets = ns.scan(host).filter(target => !blacklist.includes(target)); blacklist = blacklist.concat(localTargets); return localTargets.reduce((acc, target) => { const info = ns.getServer(target); - const verbose = args['verbose'] ? ` (${info.hasAdminRights ? 'ROOTED' : `${info.requiredHackingSkill}|${info.numOpenPortsRequired}`})` : ''; - const name = `${target}${verbose}`; - acc[name] = scan(target, depth + 1, maxDepth, blacklist); + const verb = args['verbose'] ? ` (${info.hasAdminRights ? 'ROOTED' : `${info.requiredHackingSkill}|${info.numOpenPortsRequired}`})` : ''; + const name = `${target}${verb}`; + acc[name] = scan(target, depth + 1, blacklist); return acc; }, {}); } /** - * Iterate tree & print to screen - * @param tree {Object} - Tree to parse - * @param spacer {String} - Spacer text for tree formatting + * Search tree for path to device. + * @param tree {object} - Tree to search + * @param find {string} - Device to search for + * @returns {object} - Path to device */ - function render(tree, spacer = '') { + function filter(tree, find) { + function filter(tree, find, path = []) { + return Object.keys(tree).flatMap(n => { + if(n.indexOf(find) == 0) return [...path, n]; + if(Object.keys(n).length) return filter(tree[n], find, [...path, n]); + return null; + }).filter(p => !!p); + } + let whitelist = filter(tree, find), acc = {}, next = acc; + while(whitelist.length) { + const n = whitelist.splice(0, 1); + next[n] = {}; + next = next[n]; + } + return acc; + } + + /** + * Iterate tree & print to screen + * @param tree {object} - Tree to parse + * @param spacer {string} - Spacer text for tree formatting + */ + function render(tree, spacer = ' ') { Object.keys(tree).forEach((key, i, arr) => { const last = i == arr.length - 1; const branch = last ? '└─ ' : '├─ '; @@ -125,10 +74,14 @@ export async function main(ns) { }); } - const network = scan(start); - render(network); + // Run + let found = scan(args['start'], args['verbose']); + if(args['filter']) found = filter(found, args['filter']); + ns.tprint(args['start']); + render(found); + ns.tprint(''); } export function autocomplete(data) { return [...data.servers]; -} +} \ No newline at end of file