From 405bf79d5627c60461cc31321f8329672b4f82be Mon Sep 17 00:00:00 2001 From: brent s Date: Wed, 8 Nov 2017 17:00:11 -0500 Subject: [PATCH] adding hostscan --- aif/scripts/post/hostscan.py | 205 +++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100755 aif/scripts/post/hostscan.py diff --git a/aif/scripts/post/hostscan.py b/aif/scripts/post/hostscan.py new file mode 100755 index 0000000..e8eaf58 --- /dev/null +++ b/aif/scripts/post/hostscan.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 + +import argparse +import grp +import os +import pwd +import re +import subprocess +import sys + +# Defaults +#def_supported_keys = subprocess.run(['ssh', +# '-Q', +# 'key'], stdout = subprocess.PIPE).stdout.decode('utf-8').splitlines() +def_supported_keys = ['dsa', 'ecdsa', 'ed25519', 'rsa'] +def_mode = 'append' +def_syshostkeys = '/etc/ssh/ssh_known_hosts' +def_user = pwd.getpwuid(os.geteuid())[0] +def_grp = grp.getgrgid(os.getegid())[0] + + +class hostscanner(object): + def __init__(self, args): + self.args = args + if self.args['keytypes'] == ['all']: + self.args['keytypes'] = def_supported_keys + if self.args['system']: + if os.geteuid() != 0: + exit(('You have specified system-wide modification but ' + + 'are not running with root privileges! Exiting.')) + self.args['output'] = def_syshostkeys + if self.args['output'] != sys.stdout: + _pardir = os.path.dirname(os.path.abspath(os.path.expanduser(self.args['output']))) + if _pardir.startswith('/home'): + _octmode = 0o700 + else: + _octmode = 0o755 + os.makedirs(_pardir, mode = _octmode, exist_ok = True) + os.chown(_pardir, + pwd.getpwnam(self.args['chown_user'])[2], + grp.getgrnam(self.args['chown_grp'])[2]) + + def getHosts(self): + self.keys = {} + _hosts = os.path.abspath(os.path.expanduser(self.args['infile'])) + with open(_hosts, 'r') as f: + for l in f.readlines(): + l = l.strip() + if re.search('^\s*(#.*)?$', l, re.MULTILINE): + continue # Skip commented and blank lines + k = re.sub('^([0-9a-z-\.]+)\s*#.*$', + '\g<1>', + l.strip().lower(), + re.MULTILINE) + self.keys[k] = [] + return() + + def getKeys(self): + def parseType(k): + _newkey = re.sub('^ssh-', '', k).split('-')[0] + if _newkey == 'dss': + _newkey = 'dsa' + return(_newkey) + for h in list(self.keys.keys()): + _h = h.split(':') + if len(_h) == 1: + _host = _h[0] + _port = 22 + elif len(_h) == 2: + _host = _h[0] + _port = int(_h[1]) + _cmdline = ['ssh-keyscan', + '-t', ','.join(self.args['keytypes']), + '-p', str(_port), + _host] + if self.args['hash']: + #https://security.stackexchange.com/a/56283 + # verify via: + # SAMPLE ENTRY: |1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa ... + #key=$(echo F1E1KeoE/eEWhi10WpGv4OdiO6Y= | base64 -d | xxd -p) + #echo -n "192.168.1.61" | openssl sha1 -mac HMAC -macopt hexkey:${key} | awk '{print $2}' | xxd -r -p | base64 + _cmdline.insert(1, '-H') + _cmd = subprocess.run(_cmdline, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + if not re.match('\s*#.*', _cmd.stderr.decode('utf-8')): + _printerr = [] + for i in _cmd.stderr.decode('utf-8').splitlines(): + if i.strip() not in _printerr: + _printerr.append(i.strip()) + print('{0}: errors detected; skipping ({1})'.format(h, '\n'.join(_printerr))) + del(self.keys[h]) + continue + for l in _cmd.stdout.decode('utf-8').splitlines(): + _l = l.split() + _key = {'type': _l[1], + 'host': _l[0], + 'key': _l[2]} + if parseType(_key['type']) in self.args['keytypes']: + self.keys[h].append(_key) + return() + + def write(self): + for h in self.keys.keys(): + for i in self.keys[h]: + _s = '# Automatically added via hostscan.py\n{0} {1} {2}\n'.format(i['host'], + i['type'], + i['key']) + if self.args['output'] == sys.stdout: + print(_s, end = '') + else: + if self.args['writemode'] == 'append': + _wm = 'a' + else: + _wm = 'w' + with open(self.args['output'], _wm) as f: + f.write(_s) + os.chmod(self.args['output'], 0o644) + os.chown(self.args['output'], + pwd.getpwnam(self.args['chown_user'])[2], + grp.getgrnam(self.args['chown_grp'])[2]) + return() + +def parseArgs(): + def getTypes(t): + keytypes = t.split(',') + keytypes = [k.strip() for k in keytypes] + for k in keytypes: + if k not in ('all', *def_supported_keys): + raise argparse.ArgumentError('Must be one or more of the following: all, {0}'.format(', '.join(def_supported_keys))) + return(keytypes) + args = argparse.ArgumentParser(description = ('Scan a list of hosts and present their hostkeys in ' + + 'a format suitable for an SSH known_hosts file.')) + args.add_argument('-u', + '--user', + dest = 'chown_user', + default = def_user, + help = ('The username to chown the file to (if \033[1m{0}\033[0m is specified). ' + + 'Default: \033[1m{1}\033[0m').format('-o/--output', def_user)) + args.add_argument('-g', + '--group', + dest = 'chown_grp', + default = def_grp, + help = ('The group to chown the file to (if \033[1m{0}\033[0m is specified). ' + + 'Default: \033[1m{1}\033[0m').format('-o/--output', def_grp)) + args.add_argument('-H', + '--hash', + dest = 'hash', + action = 'store_true', + help = ('If specified, hash the hostkeys (see ssh-keyscan(1)\'s -H option for more info)')) + args.add_argument('-m', + '--mode', + dest = 'writemode', + default = def_mode, + choices = ['append', 'replace'], + help = ('If \033[1m{0}\033[0m is specified, the mode to use for the ' + + 'destination file. The default is \033[1m{1}\033[0m').format('-o/--output', def_mode)) + args.add_argument('-k', + '--keytypes', + dest = 'keytypes', + type = getTypes, + default = 'all', + help = ('A comma-separated list of key types to add (if supported by the target host). ' + + 'The default is to add all keys found. Must be one (or more) of: \033[1m{0}\033[0m').format(', '.join(def_supported_keys))) + args.add_argument('-o', + '--output', + default = sys.stdout, + metavar = 'OUTFILE', + dest = 'output', + help = ('If specified, write the hostkeys to \033[1m{0}\033[0m instead of ' + + '\033[1m{1}\033[0m (the default). ' + + 'Overrides \033[1m{2}\033[0m').format('OUTFILE', + 'stdout', + '-S/--system-wide')) + args.add_argument('-S', + '--system-wide', + dest = 'system', + action = 'store_true', + help = ('If specified, apply to the entire system (not just the ' + + 'specified/running user) via {0}. ' + + 'Requires \033[1m{1}\033[0m in /etc/ssh/ssh_config (usually ' + + 'enabled silently by default) and running with root ' + + 'privileges').format(def_syshostkeys, + 'GlobalKnownHostsFile {0}'.format(def_syshostkeys))) + args.add_argument(metavar = 'HOSTLIST_FILE', + dest = 'infile', + help = ('The path to the list of hosts. Can contain blank lines and/or comments. ' + + 'One host per line. Can be \033[1m{0}\033[0m (as long as it\'s resolvable), ' + + '\033[1m{1}\033[0m, or \033[1m{2}\033[0m. To specify an alternate port, ' + + 'add \033[1m{3}\033[0m to the end (e.g. ' + + '"some.host.tld:22")').format('hostname', + 'IP address', + 'FQDN', + ':')) + return(args) + +def main(): + args = vars(parseArgs().parse_args()) + scan = hostscanner(args) + scan.getHosts() + scan.getKeys() + scan.write() + +if __name__ == '__main__': + main()