From e67bef2f760e14d09ea2f569f82524a4a557b4ac Mon Sep 17 00:00:00 2001 From: Zak Timson Date: Wed, 9 Mar 2022 19:06:14 +0000 Subject: [PATCH] Updated scripts to use ArgParser --- README.md | 122 ++++++++++++--------- scripts/auto-pwn.js | 158 --------------------------- scripts/crawler.js | 176 +++++++++++------------------- scripts/lib/arg-parser.js | 34 +++--- scripts/lib/utils.js | 23 ++++ scripts/miner.js | 131 ++++++---------------- scripts/network-graph.js | 75 +++++++------ scripts/node-manager.js | 124 ++++----------------- scripts/rootkit.js | 97 +++++++++++++++++ scripts/update.js | 221 +++++++++++++++++++++----------------- 10 files changed, 488 insertions(+), 673 deletions(-) delete mode 100644 scripts/auto-pwn.js create mode 100644 scripts/lib/utils.js create mode 100644 scripts/rootkit.js diff --git a/README.md b/README.md index f3a0328..7b37f3b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,17 @@ These scripts are for playing the [open source](https://github.com/danielyxie/bitburner) game [BitBurner](https://danielyxie.github.io/bitburner/) ## Table of Contents -[[_TOC_]] +- [BitBurner - Scripts](#bitburner-scripts) + - [Table of Contents](#table-of-contents) + - [Quick Start](#quick-start) + - [Scripts](#scripts) + - [bruteforce.js (WIP)](#bruteforcejs-wip) + - [crawler.js](#crawlerjs) + - [miner.js](#minerjs) + - [network-graph.js](#network-graphjs) + - [node-manager.js](#node-managerjs) + - [rootkit.js](#rootkitjs) + - [update.js](#updatejs) ## Quick Start ```bash @@ -18,39 +28,17 @@ run scripts/crawler.js /scripts/auto-pwn.js {{TARGET}} /scripts/miner.js ``` ## Scripts -### [auto-pwn.js](./scripts/auto-pwn.js) -**RAM:** 4.75 GB - -Automatically gain root on a target machine. Optionaly after being rooted, a file can be coppied & executed. -``` -[home ~/]> run scripts/auto-pwn.js --help -Running script with 1 thread(s), pid 161 and args: ["--help"]. -/scripts/auto-pwn.js: - -Automatically gain root on a target machine. Optionaly after being rooted, a file can be coppied & executed. - -Usage: run auto-pwn.js [TARGET] [SCRIPT] [ARGS]... - run auto-pwn.js --help - - TARGET Target machine to root. Defaults to localhost - SCRIPT Script to copy & execute - ARGS Aditional arguments for SCRIPT. Forward the target with "{{TARGET}}" - -Options: - -t --threads=num Set number of threads for script - -h --help Display help message -``` ### [bruteforce.js](./scripts/bruteforce.js) (WIP) Attacks target until security falls bellow threshold. Useful for throwing extra compute power & cracking a specific computer. -### [crawler.js](./scripts/crawler.js) (WIP) -**RAM:** 3.05 GB +### [crawler.js](./scripts/crawler.js) +**RAM:** 4.05 GB Search the network for targets to execute a script against. ``` [home ~/]> run scripts/crawler.js --help -Running script with 1 thread(s), pid 163 and args: ["--help"]. +Running script with 1 thread(s), pid 1 and args: ["--help"]. /scripts/crawler.js: Search the network for targets to execute a script against. @@ -59,34 +47,34 @@ Usage: run crawler.js [OPTIONS] SCRIPT [ARGS]... run crawler.js --help SCRIPT Script to copy & execute - ARGS Aditional arguments for SCRIPT. Forward the target with "{{TARGET}}" + ARGS Arguments for script. Forward the current target with: {{TARGET}} Options: - -d --depth=num Number of network hops. Defaults to 3 - -l --level=num Exclude targets with a high hacking level. Defaults to hack level, 0 to disable - -p --ports=num Exclute targets with too many closed ports - -t --threads=num Set number of threads for script - -h --help Display help message + -c --cpu Number of CPU threads to use with script + -d --depth Depth to scan to, defaults to 3 + -l --level Exclude targets with higher hack level, defaults to current hack level + -p --ports Exclute targets with too many closed ports + -h --help Display this help message ``` ### [miner.js](./scripts/miner.js) -**RAM:** 2.35 GB +**RAM:** 2.45 GB -Weaken, spoof & hack the target in a loop for money. +Weaken, Grow, Hack loop to "mine" target machine for money. ``` [home ~/]> run scripts/miner.js --help -Running script with 1 thread(s), pid 165 and args: ["--help"]. +Running script with 1 thread(s), pid 1 and args: ["--help"]. /scripts/miner.js: -Weaken, spoof & hack the target in a loop for money. +Weaken, Grow, Hack loop to "mine" target machine for money. Usage: run miner.js [TARGET] run miner.js --help - TARGET Target to mine. Defaults to localhost + TARGET Device to mine, defaults to current machine Options: - -h --help Display help message + -h --help Display this help message ``` ### [network-graph.js](./scripts/network-graph.js) @@ -95,7 +83,7 @@ Options: Scan the network for devices and display as an ASCII tree. ``` [home ~/]> run /scripts/network-graph.js --help -Running script with 1 thread(s), pid 138 and args: ["--help"]. +Running script with 1 thread(s), pid 1 and args: ["--help"]. /scripts/network-graph.js: Scan the network for devices and display as an ASCII tree: @@ -106,13 +94,15 @@ Scan the network for devices and display as an ASCII tree: ├─ foodnstuff (ROOTED) └─ sigma-cosmetics (ROOTED) -Usage: run network-graph.js [OPTIONS] +Usage: run network-graph.js [OPTIONS] [TARGET] run network-graph.js --help + TARGET Point to start scan from, defaults to current machine + Options: -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 + -f --filter Display devices matching name + -r --regex Display devices matching pattern -v --verbose Displays the required hack level & ports needed to root: (level|port) -h --help Display this help message ``` @@ -120,39 +110,65 @@ Options: ### [node-manager.js](./scripts/node-manager.js) **RAM:** 5.70 GB -Buy, upgrade & manage Hacknet nodes automatically. +Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates. ``` [home ~/]> run scripts/node-manager.js --help -Running script with 1 thread(s), pid 166 and args: ["--help"]. +Running script with 1 thread(s), pid 1 and args: ["--help"]. /scripts/node-manager.js: Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates. -Usage: run node-manager.js [OPTIONS] LIMIT - run node-manager.js --balance 1E6 4 +Usage: run node-manager.js [OPTIONS] [LIMIT] run node-manager.js --help - LIMIT Limit the number of nodes the manager will buy + LIMIT Limit the number of nodes the manager will buy, defaults to 8 Options: - -b --balance=num Prevent spending bellow this point - -h --help Display help message + -b --balance Prevent spending bellow point + -h --help Display this help message +``` + +### [rootkit.js](./scripts/rootkit.js) +**RAM:** 4.75 GB - 4.90 GB (depending on un-commented programs) + +Automatically gain root on a target machine. A file can also be uploaded & executed. + +Programs can be commented out to lower the cost of running. +``` +[home ~/]> run scripts/rootkit.js --help +Running script with 1 thread(s), pid 1 and args: ["--help"]. +/scripts/rootkit.js: + +Automatically gain root on a target machine. A file can also be uploaded & executed. + +Usage: run rootkit.js [OPTIONS] [TARGET] [SCRIPT] [ARGS]... + run rootkit.js --help + + TARGET Target machine to root, defaults to current machine + SCRIPT Script to copy & execute + ARGS Arguments for script. Forward the current target with: {{TARGET}} + +Options: + -c --cpu Number of CPU threads to use with script + -h --help Display this help message ``` ### [update.js](./scripts/update.js) -**RAM:** 2.60 GB +**RAM:** 2.95 GB -Automatically download the latest versions of all scripts using wget. +Download the latest script updates from the repository using wget. ``` [home ~/]> run scripts/update.js --help -Running script with 1 thread(s), pid 167 and args: ["--help"]. +Running script with 1 thread(s), pid 1 and args: ["--help"]. /scripts/update.js: -Automatically download the latest versions of all scripts using wget. +Download the latest script updates from the repository using wget. Usage: run update.js run update.js --help + TARGET Target device to update, defaults to current machine + Options: -h --help Display help message ``` diff --git a/scripts/auto-pwn.js b/scripts/auto-pwn.js deleted file mode 100644 index ef9f751..0000000 --- a/scripts/auto-pwn.js +++ /dev/null @@ -1,158 +0,0 @@ -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`; - } -} - -/** - * Pwn a target server will availible tools. Can also copy & execute a script after pwning. - */ -export async function main(ns) { - ns.disableLog('ALL'); - - /** - * Prints text and waits a random amount of time to emulate work being complete. - * @param text {String} - message to Display - * @param min {Number} - minimum amount of time to wait in seconds. Defaults to 1 second. - * @param max {Number} - maximum amount of time to wait in seconds. Defaults to 1 second. - */ - async function printWithDelay(text, min=1, max=1) { - ns.tprint(text); - await ns.sleep(~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000); - } - - // Initilize script arguments - const argParser = new ArgParser({ - desc: 'Automatically gain root on a target machine. Optionaly after being rooted, a file can be coppied & executed.', - examples: [ - 'run auto-pwn.js [TARGET] [SCRIPT] [ARGS]...', - 'run auto-pwn.js --help', - ], - args: [ - {key: 'TARGET', desc: 'Target machine to root. Defaults to localhost'}, - {key: 'SCRIPT', desc: 'Script to copy & execute'}, - {key: 'ARGS', skip: true, desc: 'Aditional arguments for SCRIPT. Forward the target with "{{TARGET}}"'}, - {key: 'threads', alias: 't', type: 'num', optional: true, desc: 'Set number of threads for script'}, - {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()); - - // Setup - const target = args['TARGET'] && args['TARGET'] != 'localhost' ? args['TARGET'] : ns.getHostname(); - const threads = args['threads'] || !args['SCRIPT'] ? 1 : ~~(ns.getServerMaxRam(target) / ns.getScriptRam(args['SCRIPT'], 'home')); - - // Banner - ns.tprint('==================================================='); - ns.tprint(`🧑‍💻 Pwning: ${target}`); - await printWithDelay('==================================================='); - - try { - // Open as many ports as possible - ns.brutessh(target); - await printWithDelay(`Attacking (SSH) ⚔️ ${target}:22`, 3, 5); - ns.ftpcrack(target); - await printWithDelay(`Attacking (FTP) ⚔️ ${target}:24`, 3, 5); - } catch { - } finally { - // Attempt to root - try { - ns.nuke(target) - await printWithDelay(`💀 Root Granted 💀`); - } catch { - await printWithDelay(`⚠️ Failed to Root ⚠️`); - ns.exit(); - } - } - - if(args['SCRIPT']) { - // Copy scripts - ns.tprint(''); - await printWithDelay('Copying script:'); - const speed = ~~(Math.random() * 100) / 10 - await ns.scp(args['SCRIPT'], target); - await printWithDelay(`${args['SCRIPT']} \t [==================>] 100% \t (${speed} MB/s)`); - - // Run scripts - ns.tprint(''); - ns.tprint('Executing:'); - ns.scriptKill(args['SCRIPT'], target); - await printWithDelay(`ssh -c "run ${args['SCRIPT']} ${args['extra'].join(' ')} -t ${threads}" root@${target}`); - const pid = ns.exec(args['SCRIPT'], target, threads, ...args['extra'].map(a => a == '{{TARGET}}' ? target : a)); - ns.tprint(''); - ns.tprint(pid != null ? '✅ Done!' : '⚠️ Failed to start ⚠️'); - ns.tprint(''); - } -} - -/** - * Autocomplete script arguments - * @param data - provided by API - */ -export function autocomplete(data) { - return [...data.servers, ...data.scripts]; -} diff --git a/scripts/crawler.js b/scripts/crawler.js index 6c6d5f5..bcf4efe 100644 --- a/scripts/crawler.js +++ b/scripts/crawler.js @@ -1,122 +1,72 @@ -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'; +/** + * Search the network for targets to execute a script against. + * @param ns {NS} - BitBurner API + */ export async function main(ns) { - ns.disableLog('ALL'); - - // Initilize script arguments - const argParser = new ArgParser({ - desc: 'Search the network for targets to execute a script against.', - examples: [ - 'run crawler.js [OPTIONS] SCRIPT [ARGS]...', - 'run crawler.js --help', - ], - args: [ - {key: 'SCRIPT', desc: 'Script to copy & execute'}, - {key: 'ARGS', skip: true, desc: 'Aditional arguments for SCRIPT. Forward the target with "{{TARGET}}"'}, - {key: 'depth', alias: 'd', type: 'num', optional: true, desc: 'Number of network hops. Defaults to 3'}, - {key: 'level', alias: 'l', type: 'num', optional: true, desc: 'Exclude targets with a high hacking level. Defaults to hack level, 0 to disable'}, - {key: 'ports', alias: 'p', type: 'num', optional: true, desc: 'Exclute targets with too many closed ports'}, - {key: 'threads', alias: 't', type: 'num', optional: true, desc: 'Set number of threads for script'}, - {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()); - if(!args['SCRIPT']) return ns.tprint(argParser.help('Missing SCRIPT')); - - // Setup - const depth = args['depth'] || 3; - const level = args['level'] || ns.getHackingLevel(); - const ports = args['ports'] || Infinity; - const threads = args['threads'] || 1; - - // Recursively search for targets - const targets = ns.scan().map(h => [h, 1]); - for(let i = 0; i < targets.length; i++) { - if(targets[i][1] < depth) ns.scan(targets[i][0]).forEach(h => { - if(h != 'home' && !targets.find(t => t[0] == h)) targets.push([h, targets[i][1] + 1]); - }); + // Setup + ns.disableLog('ALL'); + const argParser = new ArgParser('crawler.js', 'Search the network for targets to execute a script against.', null, [ + {name: 'script', desc: 'Script to copy & execute', type: 'string'}, + {name: 'args', desc: 'Arguments for script. Forward the current target with: {{TARGET}}', optional: true, extras: true, type: 'string'}, + {name: 'cpu', desc: 'Number of CPU threads to use with script', flags: ['-c', '--cpu'], default: 1, type: 'num'}, + {name: 'depth', desc: 'Depth to scan to, defaults to 3', flags: ['-d', '--depth'], default: Infinity, type: 'num'}, + {name: 'level', desc: 'Exclude targets with higher hack level, defaults to current hack level', flags: ['-l', '--level'], default: ns.getHackingLevel(), type: 'num'}, + {name: 'ports', desc: 'Exclute targets with too many closed ports', flags: ['-p', '--ports'], optional: true, default: Infinity, type: 'num'}, + ]); + let args; + try { + args = argParser.parse(ns.args); + } catch(err) { + if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); + throw err; } - // Execute script on each target - for(const target of targets) { - // Check target - if(level && level < ns.getServerRequiredHackingLevel(target[0]) || - (ports && ports < ns.getServerNumPortsRequired(target[0]))) return; + /** + * Recursively search network & build a tree + * @param host {string} - Point to scan from + * @param depth {number} - Current scanning depth + * @param blacklist {String[]} - Devices already discovered + * @returns Dicionary of discovered devices + */ + function scan(target = 'home', depth = 1, found = new Set()) { + if(found.size == 0) found.add(target); + ns.scan(target).filter(t => !found.has(t)).forEach(t => { + found.add(t); + scan(t, depth + 1, found); + }); + found.delete('home'); + return Array.from(found); + } - // Start script - ns.run(args['SCRIPT'], threads, ...args['extra'].map(a => a == '{{TARGET}}' ? target[0] : a)); + // Run + const targets = scan(); + let complete = 0, failed = 0, skipped = 0; + for(let target of targets) { + if(target == 'home') continue; + + if(args['level'] < ns.getServerRequiredHackingLevel(target) || args['ports'] < ns.getServerNumPortsRequired(target)) { + skipped++; + continue; + } + + const scriptArgs = args['args'].map(arg => arg == '{{TARGET}}' ? target : arg); + const pid = ns.run(args['script'], args['cpu'], ...scriptArgs); + if(pid == 0) { + failed++; + continue; + } // Wait for script to finish - // do { await ns.sleep(1000); } - // while(ns.scriptRunning(args['SCRIPT'], 'home')); - await ns.sleep(10000); + do { await ns.sleep(1000); } + while(ns.scriptRunning(args['script'], 'home')); + complete++; } + // Output report + ns.tprint('==================================================='); + ns.tprint(`Crawler Report: ${targets.length} Device${targets.length > 1 ? 's' : ''}`); + ns.tprint('==================================================='); + ns.tprint(`Complete: ${complete}\tFailed: ${failed}\tSkipped: ${skipped}`); + ns.tprint(''); } diff --git a/scripts/lib/arg-parser.js b/scripts/lib/arg-parser.js index b58eaf0..ad5ea1a 100644 --- a/scripts/lib/arg-parser.js +++ b/scripts/lib/arg-parser.js @@ -11,15 +11,10 @@ export class ArgParser { 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'} - ]; + this.examples = examples || [`${argList.find(arg => !!arg.flags) ? '[OPTIONS] ' : ''}${argList.filter(arg => !arg.flags).map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`]; + this.examples.push('--help'); + this.argList = argList || []; + this.argList.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], type: 'bool'}); } /** @@ -41,9 +36,11 @@ export class ArgParser { 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]; + const split = parse.split('='); + const arg = this.argList.find(arg => arg.flags && arg.flags.includes(split[0] || parse)); + if(arg == null) throw new ArgError(`Option unknown: ${parse}`); + if(arg.name == 'help') throw new ArgError('Help'); + const value = arg.type == 'bool' ? true : 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 { @@ -52,13 +49,14 @@ export class ArgParser { } } // 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]; + this.argList.filter(arg => !arg.flags && !arg.extras).forEach(arg => { + if(!arg.optional && !extra.length) throw new ArgError(`Argument missing: ${arg.name.toUpperCase()}`); + const value = extra.splice(0, 1)[0]; + if(value != null) parsed[arg.name] = value; }); // Extras - if(extra.length) parsed['extra'] = extra; - if(parsed['help']) throw new ArgError(); + const extraKey = this.argList.find(arg => arg.extras)?.name || 'extra'; + parsed[extraKey] = extra; return parsed; } @@ -69,7 +67,7 @@ export class ArgParser { */ help(msg) { // Description - let message = '\n\n' + (msg ? msg : this.description); + let message = '\n\n' + (msg && msg.toLowerCase() != 'help' ? msg : this.description); // Usage if(this.examples.length) message += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t'); // Arguments diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js new file mode 100644 index 0000000..d09ff2b --- /dev/null +++ b/scripts/lib/utils.js @@ -0,0 +1,23 @@ +/** + * Print a download bar to the terminal. + * @params ns {NS} - Bitburner API + * @params file - Filename to display with progress bar + */ +export async function downloadPrint(ns, file) { + const speed = ~~(Math.random() * 100) / 10; + const spacing = Array(5 - Math.floor((file.length) / 8)).fill('\t').join(''); + await slowPrint(ns, `${file}${spacing}[==================>] 100% \t (${speed} MB/s)`); +} + +/** + * Print text to the terminal & then delay for a random amount of time to emulate execution time. + * @params ns {NS} - Bitburner API + * @params message {string} - Text to display + * @params min {number} - minimum amount of time to wait after printing text + * @params max {number} - maximum amount of time to wait after printing text + */ +export async function slowPrint(ns, message, min = 0.5, max = 1.5) { + const time = ~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000; + ns.tprint(message); + await ns.sleep(time); +} diff --git a/scripts/miner.js b/scripts/miner.js index 5bd1049..4eb8efd 100644 --- a/scripts/miner.js +++ b/scripts/miner.js @@ -1,77 +1,29 @@ -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'; /** - * Hack a server for it's money. + * Weaken, Grow, Hack loop to "mine" target machine for money. + * @params ns {NS} - BitBurner API */ export async function main(ns) { + // Setup ns.disableLog('ALL'); + const historyLength = 15; + const messageHistory = Array(historyLength).fill(''); + let maxBalance, balance, minSecurity, security; + const argParser = new ArgParser('miner.js', 'Weaken, Grow, Hack loop to "mine" target machine for money.', null, [ + {name: 'target', desc: 'Device to mine, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'} + ]); + let args; + try { + args = argParser.parse(ns.args); + maxBalance = await ns.getServerMaxMoney(args['target']); + balance = await ns.getServerMoneyAvailable(args['target']); + minSecurity = await ns.getServerMinSecurityLevel(args['target']) + 2; + security = await ns.getServerSecurityLevel(args['target']); + } catch(err) { + if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); + throw err; + } /** * Print header with logs @@ -81,7 +33,7 @@ export async function main(ns) { const sec = `${Math.round(security)}/${minSecurity}`; ns.clearLog(); ns.print('==================================================='); - ns.print(`💎⛏️ Mining: ${target}`); + ns.print(`Mining: ${args['target']}`); ns.print('==================================================='); ns.print(`Security: ${sec}${sec.length < 6 ? '\t' : ''}\tBalance: $${Math.round(balance * 100) / 100}`); ns.print('==================================================='); @@ -90,50 +42,27 @@ export async function main(ns) { messageHistory.forEach(m => ns.print(m)); } - // Initilize script arguments - const argParser = new ArgParser({ - desc: 'Weaken, spoof & hack the target in a loop for money.', - examples: [ - 'run miner.js [TARGET]', - 'run miner.js --help', - ], - args: [ - {key: 'TARGET', desc: 'Target to mine. Defaults to localhost'}, - {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()); - - // Setup - const historyLength = 15; - const messageHistory = Array(historyLength).fill(''); - const target = args['TARGET'] && args['TARGET'] != 'localhost' ? args['TARGET'] : ns.getHostname(); - const minSecurity = ns.getServerMinSecurityLevel(target) + 2; - let orgBalance, balance, security; - log(); - while(true) { + do { // Update information - security = await ns.getServerSecurityLevel(target); - balance = await ns.getServerMoneyAvailable(target); - if(orgBalance == null) orgBalance = balance; + security = await ns.getServerSecurityLevel(args['target']); + balance = await ns.getServerMoneyAvailable(args['target']); // Pick step if(security > minSecurity) { // Weaken log('Attacking Security...'); - const w = await ns.weaken(target); + const w = await ns.weaken(args['target']); log(`Security: -${w}`); - } else if(balance <= orgBalance) { // Grow + } else if(balance < maxBalance) { // Grow log('Spoofing Balance...'); - const g = await ns.grow(target); + const g = await ns.grow(args['target']); log(`Balance: +$${Math.round((g * balance - balance) * 100) / 100}`); } else { // Hack log('Hacking Account...'); - const h = await ns.hack(target); + const h = await ns.hack(args['target']); log(`Balance: -$${h}`); } - } + } while(true); } export function autocomplete(data) { diff --git a/scripts/network-graph.js b/scripts/network-graph.js index dcec446..ac6a2f7 100644 --- a/scripts/network-graph.js +++ b/scripts/network-graph.js @@ -3,10 +3,11 @@ import {ArgError, ArgParser} from './scripts/lib/arg-parser'; export async function main(ns) { // Setup ns.disableLog('ALL'); - 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)', [], [ + 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)', null, [ + {name: 'target', desc: 'Point to start scan from, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'}, {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: 'filter', desc: 'Display devices matching name', flags: ['-f', '--filter'], type: 'string'}, + {name: 'regex', desc: 'Display devices matching pattern', flags: ['-r', '--regex'], type: 'string'}, {name: 'verbose', desc: 'Displays the required hack level & ports needed to root: (level|port)', flags: ['-v', '--verbose'], type: 'bool'}, ]); let args; @@ -17,6 +18,38 @@ export async function main(ns) { throw err; } + /** + * Prune tree down to devices that match name or pattern. + * @param tree {object} - Tree to search + * @param find {string} - Device name or pattern to search for + * @param regex {boolean} - True to use regex, false for raw check + * @returns {object} - Pruned tree + */ + function filter(tree, find, regex = false) { + const found = new Set(); + function buildWhitelist(tree, find, path = []) { + const keys = Object.keys(tree); + if(!keys.length) return; + Object.keys(tree).forEach(n => { + const matches = regex ? new RegExp(find).test(n) : n == find; + if(n == 'n00dles') console.log(n, find, matches); + if(matches) { + found.add(n); + path.forEach(p => found.add(p)); + } + buildWhitelist(tree[n], find, [...path, n]); + }) + } + function prune(tree, whitelist) { + Object.keys(tree).forEach(n => { + if(Object.keys(tree[n]).length) prune(tree[n], whitelist); + if(!whitelist.includes(n)) delete tree[n]; + }); + } + buildWhitelist(tree, find); + prune(tree, Array.from(found)); + } + /** * Recursively search network & build a tree * @param host {string} - Point to scan from @@ -25,9 +58,9 @@ export async function main(ns) { * @returns Dicionary of discovered devices */ function scan(host, depth = 1, blacklist = [host]) { - if(depth >= args['depth']) return {}; + if(depth > args['depth']) return {}; const localTargets = ns.scan(host).filter(target => !blacklist.includes(target)); - blacklist = blacklist.concat(localTargets); + blacklist = [...blacklist, ...localTargets]; return localTargets.reduce((acc, target) => { const info = ns.getServer(target); const verb = args['verbose'] ? ` (${info.hasAdminRights ? 'ROOTED' : `${info.requiredHackingSkill}|${info.numOpenPortsRequired}`})` : ''; @@ -37,29 +70,6 @@ export async function main(ns) { }, {}); } - /** - * 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 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 @@ -75,13 +85,14 @@ export async function main(ns) { } // Run - let found = scan(args['start'], args['verbose']); - if(args['filter']) found = filter(found, args['filter']); - ns.tprint(args['start']); + ns.tprint(args['target']); + const found = scan(args['target']); + if(args['regex']) filter(found, args['regex'], true); + else if(args['filter']) filter(found, args['filter']); render(found); ns.tprint(''); } export function autocomplete(data) { return [...data.servers]; -} \ No newline at end of file +} diff --git a/scripts/node-manager.js b/scripts/node-manager.js index d5e2da1..5f7cebf 100644 --- a/scripts/node-manager.js +++ b/scripts/node-manager.js @@ -1,78 +1,26 @@ -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'; /** - * Manages hacknet nodes, purchasing nodes to reach the desired amount. - * Upgrades (Level, RAM, Cores & Cache) will be automatically purchased. + * Buy, upgrade & manage Hacknet nodes automatically. + * @params ns {NS} - BitBurner API */ export async function main(ns) { + // Setup ns.disableLog('ALL'); + const historyLength = 17; + const messageHistory = Array(historyLength).fill(''); + let args, nodeCount = ns.hacknet.numNodes(); + const argParser = new ArgParser('node-manager.js', 'Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates.', null, [ + {name: 'limit', desc: 'Limit the number of nodes the manager will buy, defaults to 8', optional: true, default: 8, type: 'num'}, + {name: 'balance', desc: 'Prevent spending bellow point', flags: ['-b', '--balance'], type: 'num'} + ]); + + try { + args = argParser.parse(ns.args); + } catch(err) { + if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); + throw err; + } /** * Print header with logs @@ -81,48 +29,20 @@ export async function main(ns) { function log(message) { ns.clearLog(); ns.print('==================================================='); - ns.print(`🖥️ Node Manager: ${nodeCount}/${limit} Nodes`); + ns.print(`Node Manager: ${nodeCount}/${args['limit']} Nodes`); ns.print('==================================================='); if(message != null) messageHistory.push(message); messageHistory.splice(0, messageHistory.length - historyLength); messageHistory.forEach(m => ns.print(m)); } - // Initilize script arguments - const argParser = new ArgParser({ - desc: 'Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates.', - examples: [ - 'run node-manager.js [OPTIONS] LIMIT', - 'run node-manager.js --balance 1E6 4', - 'run node-manager.js --help', - ], - args: [ - {key: 'LIMIT', desc: 'Limit the number of nodes the manager will buy'}, - {key: 'balance', alias: 'b', type: 'num', optional: true, desc: 'Prevent spending bellow this point'}, - {key: 'help', alias: 'h', type: 'bool', optional: true, desc: 'Display help message'}, - ] - }); - const args = argParser.parse(ns.args); - - // Check arguments - if(args['help']) return ns.tprint(argParser.help()); - if(!args['LIMIT']) return ns.tprint(argParser.help('Missing LIMIT')); - if(isNaN(args['LIMIT'])) return ns.tprint(argParser.help('LIMIT must be a number')); - if(!!args['balance'] && isNaN(args['balance'])) return ns.tprint(argParser.help('LIMIT must be a number')); - - // Setup - const historyLength = 17; - const messageHistory = Array(historyLength).fill(''); - const limit = args['LIMIT']; - const savings = args['balance'] ?? 0 - let nodeCount = ns.hacknet.numNodes(); - + // Run log(); while(true) { const balance = ns.getServerMoneyAvailable('home'); // Check if we should buy a new node - if(nodeCount < limit && balance - ns.hacknet.getPurchaseNodeCost() >= savings) { + if(nodeCount < args['limit'] && balance - ns.hacknet.getPurchaseNodeCost() >= args['balance']) { nodeCount++; ns.hacknet.purchaseNode(); log(`Buying Node ${nodeCount}`); @@ -170,7 +90,7 @@ export async function main(ns) { }); // Apply the cheapest upgrade - if(nodes.length && balance - nodes[0].bestUpgrade.cost >= savings) { + if(nodes.length && balance - nodes[0].bestUpgrade.cost >= args['balance']) { const cost = Math.round(nodes[0].bestUpgrade.cost * 100) / 100; log(`Node ${nodes[0].index} - ${nodes[0].bestUpgrade.name} ${nodes[0][nodes[0].bestUpgrade.name] + 1} - $${cost}`); nodes[0].bestUpgrade.purchase(); diff --git a/scripts/rootkit.js b/scripts/rootkit.js new file mode 100644 index 0000000..70e1dc1 --- /dev/null +++ b/scripts/rootkit.js @@ -0,0 +1,97 @@ +import {ArgError, ArgParser} from './scripts/lib/arg-parser'; +import {downloadPrint, slowPrint} from './scripts/lib/utils'; + +/** + * Pwn a target server with availible tools. Additionally can copy & execute a script after pwning. + * @param ns {NS} - Bitburner API + */ +export async function main(ns) { + // Setup + ns.disableLog('ALL'); + const argParser = new ArgParser('rootkit.js', 'Automatically gain root on a target machine. A file can also be uploaded & executed.', null, [ + {name: 'target', desc: 'Target machine to root, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'}, + {name: 'script', desc: 'Script to copy & execute', optional: true, type: 'string'}, + {name: 'args', desc: 'Arguments for script. Forward the current target with: {{TARGET}}', optional: true, extras: true, type: 'string'}, + {name: 'cpu', desc: 'Number of CPU threads to use with script', flags: ['-c', '--cpu'], type: 'num'}, + ]); + let args; + try { + args = argParser.parse(ns.args); + if(args['script'] && !args['cpu']) args['cpu'] = ~~(ns.getServerMaxRam(args['target']) / ns.getScriptRam(args['script'], 'home')) || 1; + } catch(err) { + if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); + throw err; + } + + /** + * Detect import statements inside script & build a dependency tree. + * @params file {string} - Path to file to search + * @returns {string[]} - Array of required files + */ + async function dependencyFinder(file) { + const queue = [file], found = []; + while(queue.length) { + const imports = new RegExp(/from ["']\.(.+)["']/g); + const script = await ns.read(queue.splice(0, 1)[0]); + let match; + while((match = imports.exec(script)) != null) { + const path = `${match[1]}.js`; + queue.push(path); + found.push(path); + } + } + return found; + } + + // Banner + ns.tprint('==================================================='); + ns.tprint(`Rooting: ${args['target']}`); + await slowPrint(ns, '==================================================='); + + try { + // Run exploits + await slowPrint(ns, `Attacking over SSH (${args['target']}:22)...`, 1, 2); + ns.brutessh(args['target']); + await slowPrint(ns, `Attacking over FTP (${args['target']}:24)...`, 1, 2); + ns.ftpcrack(args['target']); + await slowPrint(ns, `Attacking over SMTP (${args['target']}:25)...`, 1, 2); + ns.relaysmtp(args['target']); + } catch { + } finally { + try { + // Attempt root + ns.tprint(''); + ns.nuke(args['target']) + ns.tprint(`Root: Success!`); + ns.tprint(''); + } catch { + ns.tprint(`Root: Failed`); + ns.tprint(''); + ns.exit(); + } + } + + if(args['script']) { + // Detect script dependencies & copy everything to target + await ns.sleep(0.5); + await slowPrint(ns, 'Copying files:'); + const deps = [...(await dependencyFinder(args['script'])), args['script']]; + for(let dep of deps) { + await ns.scp(dep, args['target']); + await downloadPrint(ns, dep); + } + + // Run script + ns.tprint(''); + await slowPrint(ns, `Executing with ${args['cpu']} thread${args['cpu'] > 1 ? 's' : ''}...`); + ns.scriptKill(args['script'], args['target']); + const pid = ns.exec(args['script'], args['target'], args['cpu'], ...args['args'] + .map(a => a == '{{TARGET}}' ? args['target'] : a)); + ns.tprint(!!pid ? 'Done!' : 'Failed to start'); + ns.tprint(''); + } +} + +export function autocomplete(data) { + return [...data.servers, ...data.scripts]; +} diff --git a/scripts/update.js b/scripts/update.js index f9b336d..ba1dc28 100644 --- a/scripts/update.js +++ b/scripts/update.js @@ -1,135 +1,164 @@ -class ArgParser { +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 opts - {examples: string[], arguments: {key: string, alias: string, type: string, optional: boolean, desc: string}[], desc: string} + * @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(opts) { - this.examples = opts.examples ?? []; - this.arguments = opts.args ?? []; - this.description = opts.desc; + constructor(name, desc, examples, argList) { + this.name = name ?? 'example.js'; + this.description = desc ?? 'Example description'; + this.examples = examples || [`${argList.find(arg => !!arg.flags) ? '[OPTIONS] ' : ''}${argList.filter(arg => !arg.flags).map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`]; + this.examples.push('--help'); + this.argList = argList || []; + this.argList.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], type: 'bool'}); } /** - * Parse the list for arguments & create a dictionary. - * @param args {any[]} - Array of arguments - * @returns Dictionary of matched flags + unmatched args under 'extra' + * 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) { - 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('=')) { + // 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 split = parse.split('='); - parse = split[0]; - value = split[1]; + const arg = this.argList.find(arg => arg.flags && arg.flags.includes(split[0] || parse)); + if(arg == null) throw new ArgError(`Option unknown: ${parse}`); + if(arg.name == 'help') throw new ArgError('Help'); + const value = arg.type == 'bool' ? true : 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); } - 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}; + // Arguments + this.argList.filter(arg => !arg.flags && !arg.extras).forEach(arg => { + if(!arg.optional && !extra.length) throw new ArgError(`Argument missing: ${arg.name.toUpperCase()}`); + const value = extra.splice(0, 1)[0]; + if(value != null) parsed[arg.name] = value; + }); + // Extras + const extraKey = this.argList.find(arg => arg.extras)?.name || 'extra'; + parsed[extraKey] = extra; + return parsed; } /** - * Create a help message of the expected paramters & usage. - * @param msg {String} - Optional message to display with help + * 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) { - 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}`; + // Description + let message = '\n\n' + (msg && msg.toLowerCase() != 'help' ? 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'); - 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}` : ''}`; + // 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`; } } +/** + * Print a download bar to the terminal. + * @params ns {NS} - Bitburner API + * @params file - Filename to display with progress bar + */ +export async function downloadPrint(ns, file) { + const speed = ~~(Math.random() * 100) / 10; + const spacing = Array(5 - Math.floor((file.length) / 8)).fill('\t').join(''); + await slowPrint(ns, `${file}${spacing}[==================>] 100% \t (${speed} MB/s)`); +} + +/** + * Print text to the terminal & then delay for a random amount of time to emulate execution time. + * @params ns {NS} - Bitburner API + * @params message {string} - Text to display + * @params min {number} - minimum amount of time to wait after printing text + * @params max {number} - maximum amount of time to wait after printing text + */ +export async function slowPrint(ns, message, min = 0.5, max = 1.5) { + const time = ~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000; + ns.tprint(message); + await ns.sleep(time); +} + /** * Automatically download all the scripts in the repository. + * @params ns {NS} - BitBurner API */ export async function main(ns) { - ns.disableLog('ALL'); - - /** - * Download a file from the repo with some fancy styling & deplays. - * @param file {String} - file name - */ - async function download(file) { - await ns.wget(`${src}${file}`, `${dest}${file}`); - const speed = ~~((Math.random() * 200) + 100) / 10; - ns.tprint(`${file} ${file.length <= 10 ? '\t' : ''}\t [==================>] 100% \t (${speed} MB/s)`); - } - - // Initilize script arguments - const argParser = new ArgParser({ - desc: 'Automatically download the latest versions of all scripts using wget.', - examples: [ - 'run update.js', - 'run update.js --help', - ], - args: [ - {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()); - // Setup - const src = 'https://gitlab.zakscode.com/ztimson/BitBurner/-/raw/develop/scripts/'; - const dest = '/scripts/'; + ns.disableLog('ALL'); + const updateFile = 'update.js'; + const argParser = new ArgParser(updateFile, 'Download the latest script updates from the repository using wget.', null, [ + {name: 'target', desc: 'Target device to update, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'} + ]); + const src = 'https://gitlab.zakscode.com/ztimson/BitBurner/-/raw/develop/scripts/'; + const dest = '/scripts2/'; const fileList = [ 'lib/arg-parser.js', - 'auto-pwn.js', + 'lib/utils.js', 'bruteforce.js', 'crawler.js', 'miner.js', 'network-graph.js', - 'node-manager.js' + 'node-manager.js', + 'rootkit.js' ]; - - // Update self & restart - if(!ns.args.length) { - ns.tprint("Updating self:"); - await ns.sleep(1000); - await download('update.js'); - await ns.sleep(500); + let args; + try { + args = argParser.parse(ns.args || []); + } catch(err) { + if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); + throw err; + } + + if(!ns.args.length) { // Update self & restart + await slowPrint(ns, 'Updating self:'); + await ns.wget(`${src}${updateFile}`, `${dest}${updateFile}`, args['target']); + await downloadPrint(ns, `${dest}${updateFile}`); + ns.tprint(''); + await slowPrint(ns, 'Restarting...'); + return ns.exec(`${dest}${updateFile}`, args['target'], 1, 1); + } else { // Update everything else + ns.tprint(''); + await slowPrint(ns, 'Downloading scripts:'); + for(let file in fileList) { + await ns.wget(`${src}${file}`, `${dest}${file}`, args['target']); + await downloadPrint(ns, `${dest}${file}`); + } + ns.tprint(''); + ns.tprint('Done!'); ns.tprint(''); - ns.tprint("Restarting..."); - await ns.sleep(2000); - return ns.run(`${dest}update.js`, 1, 1); } - - // Download each file - ns.tprint("Downloading scripts:"); - ns.tprint(''); - for(const file of fileList) { - await ns.sleep(500); - await download(file); - } - ns.tprint(''); - ns.tprint('✅ Done!'); - ns.tprint(''); }