Port rulegen.py to py3 and logging

This commit is contained in:
xeals 2025-03-13 12:19:30 +11:00
parent 622dd925bf
commit 57aa4f8a00
Signed by: xeals
SSH Key Fingerprint: SHA256:a4XbrSGLzdf9/fVgzuMqfLCU/p8A0g8smg0njUqVyXM

View File

@ -12,19 +12,23 @@
#
# Please see the attached LICENSE file for additional licensing information.
import sys
import re
import time
import operator
import enchant
from optparse import OptionParser, OptionGroup
from collections import Counter
import subprocess
from optparse import OptionParser, OptionGroup
import enchant
import multiprocessing
import operator
import re
import subprocess
import sys
import time
import logging
logging.VERBOSE = 15
logging.addLevelName(logging.VERBOSE, "VERBOSE")
logging.Logger.verbose = lambda self, msg, *args, **kwargs: \
self.log(logging.VERBOSE, msg, *args, **kwargs)
LOGGER = logging.getLogger("rulegen")
VERSION = "0.0.4"
@ -61,8 +65,6 @@ class RuleGen:
self.brute_rules = False
# Debugging options
self.verbose = False
self.debug = False
self.word = None # Custom word to use.
self.quiet = False
@ -75,10 +77,10 @@ class RuleGen:
########################################################################
# Preanalysis Password Patterns
self.password_pattern = dict()
self.password_pattern["insertion"] = re.compile('^[^a-z]*(?P<password>.+?)[^a-z]*$', re.IGNORECASE)
self.password_pattern["email"] = re.compile('^(?P<password>.+?)@[A-Z0-9.-]+\.[A-Z]{2,4}', re.IGNORECASE)
self.password_pattern["alldigits"] = re.compile('^(\d+)$', re.IGNORECASE)
self.password_pattern["allspecial"]= re.compile('^([^a-z0-9]+)$', re.IGNORECASE)
self.password_pattern["insertion"] = re.compile(r'^[^a-z]*(?P<password>.+?)[^a-z]*$', re.IGNORECASE)
self.password_pattern["email"] = re.compile(r'^(?P<password>.+?)@[A-Z0-9.-]+\.[A-Z]{2,4}', re.IGNORECASE)
self.password_pattern["alldigits"] = re.compile(r'^(\d+)$', re.IGNORECASE)
self.password_pattern["allspecial"]= re.compile(r'^([^a-z0-9]+)$', re.IGNORECASE)
########################################################################
# Hashcat Rules Engine
@ -172,9 +174,9 @@ class RuleGen:
matrix = []
# Generate and populate the initial matrix
for i in xrange(len(password) + 1):
for i in range(len(password) + 1):
matrix.append([])
for j in xrange(len(word) + 1):
for j in range(len(word) + 1):
if i == 0:
matrix[i].append(j)
elif j == 0:
@ -183,8 +185,8 @@ class RuleGen:
matrix[i].append(0)
# Calculate edit distance for each substring
for i in xrange(1,len(password) + 1):
for j in xrange(1,len(word) + 1):
for i in range(1,len(password) + 1):
for j in range(1,len(word) + 1):
if password[i-1] == word[j-1]:
matrix[i][j] = matrix[i-1][j-1]
else:
@ -205,7 +207,7 @@ class RuleGen:
if not s1:
return len(s2)
previous_row = xrange(len(s2) + 1)
previous_row = range(len(s2) + 1)
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
@ -219,11 +221,11 @@ class RuleGen:
def levenshtein_print(self,matrix,word,password):
""" Print word X password matrix """
print " %s" % " ".join(list(word))
print(" %s" % " ".join(list(word)))
for i,row in enumerate(matrix):
if i == 0: print " ",
else: print password[i-1],
print " ".join("%2d" % col for col in row)
if i == 0: print(" "),
else: print(password[i-1]),
print(" ".join("%2d" % col for col in row))
def generate_levenshtein_rules(self, word, password):
""" Generates levenshtein rules. Returns a list of lists of levenshtein rules. """
@ -254,7 +256,7 @@ class RuleGen:
cost = matrix[i][j]
# Calculate minimum cost of each operation
cost_delete = cost_insert = cost_equal_or_replace = sys.maxint
cost_delete = cost_insert = cost_equal_or_replace = sys.maxsize
if i > 0: cost_insert = matrix[i-1][j]
if j > 0: cost_delete = matrix[i][j-1]
if i > 0 and j > 0: cost_equal_or_replace = matrix[i-1][j-1]
@ -285,7 +287,7 @@ class RuleGen:
def generate_words(self,password):
""" Generate source word candidates."""
if self.debug: print "[*] Generating source words for %s" % password
LOGGER.debug("[*] Generating source words for %s" % password)
words = list()
words_collection = list()
@ -319,8 +321,8 @@ class RuleGen:
suggestions.append(suggestion)
if len(suggestions) != len(set(suggestions)):
print sorted(suggestions)
print sorted(set(suggestions))
print(sorted(suggestions))
print(sorted(set(suggestions)))
for suggestion in suggestions:
@ -346,23 +348,20 @@ class RuleGen:
best_found_distance = word["distance"]
elif word["distance"] > best_found_distance:
if self.verbose:
print "[-] %s => {edit distance suboptimal: %d (%d)} => %s" % \
(word["suggestion"], word["distance"], best_found_distance, word["password"])
LOGGER.verbose("[-] %s => {edit distance suboptimal: %d (%d)} => %s" % \
(word["suggestion"], word["distance"], best_found_distance, word["password"]))
break
# Filter words with too big edit distance
if word["distance"] <= self.max_word_dist:
if self.debug:
print "[+] %s => {edit distance: %d (%d)} = > %s" % \
(word["suggestion"], word["distance"],best_found_distance, word["password"])
LOGGER.debug("[+] %s => {edit distance: %d (%d)} = > %s" % \
(word["suggestion"], word["distance"],best_found_distance, word["password"]))
words_collection.append(word)
else:
if self.verbose:
print "[-] %s => {max distance exceeded: %d (%d)} => %s" % \
(word["suggestion"], word["distance"], self.max_word_dist, word["password"])
LOGGER.verbose("[-] %s => {max distance exceeded: %d (%d)} => %s" % \
(word["suggestion"], word["distance"], self.max_word_dist, word["password"]))
if self.max_words:
words_collection = words_collection[:self.max_words]
@ -399,7 +398,7 @@ class RuleGen:
else: preanalysis_password += c
password = preanalysis_password
if self.debug: "[*] Preanalysis Password: %s" % password
LOGGER.debug("[*] Preanalysis Password: %s" % password)
return self.enchant.suggest(password)
@ -433,9 +432,9 @@ class RuleGen:
hashcat_rule = self.generate_advanced_hashcat_rules(suggestion, lev_rule, password)
if hashcat_rule == None:
print "[!] Processing FAILED: %s => ;( => %s" % (suggestion,password)
print " Sorry about that, please report this failure to"
print " the developer: iphelix [at] thesprawl.org"
LOGGER.warning("[!] Processing FAILED: %s => ;( => %s" % (suggestion,password))
LOGGER.warning(" Sorry about that, please report this failure to")
LOGGER.warning(" the developer: iphelix [at] thesprawl.org")
else:
hashcat_rules.append(hashcat_rule)
@ -453,9 +452,8 @@ class RuleGen:
best_found_rule_length = rule_length
elif rule_length > best_found_rule_length:
if self.verbose:
print "[-] %s => {best rule length exceeded: %d (%d)} => %s" % \
(suggestion, rule_length, best_found_rule_length, password)
LOGGER.verbose("[-] %s => {best rule length exceeded: %d (%d)} => %s" % \
(suggestion, rule_length, best_found_rule_length, password))
break
if rule_length <= self.max_rule_len:
@ -467,7 +465,7 @@ class RuleGen:
""" Generate basic hashcat rules using only basic insert,delete,replace rules. """
hashcat_rules = []
if self.debug: print "[*] Simple Processing %s => %s" % (word,password)
LOGGER.debug("[*] Simple Processing %s => %s" % (word,password))
# Dynamically apply rules to the source word
# NOTE: Special case were word == password this would work as well.
@ -475,7 +473,7 @@ class RuleGen:
for (op,p,w) in rules:
if self.debug: print "\t[*] Simple Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules))
LOGGER.debug("\t[*] Simple Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules)))
if op == 'insert':
hashcat_rules.append("i%s%s" % (self.int_to_hashcat(p),password[p]))
@ -489,20 +487,20 @@ class RuleGen:
hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p),password[p]))
word_rules = self.hashcat_rule['o'](word_rules,p,password[p])
if self.debug: print "\t[*] Simple Processing Ended: %s => %s => %s" % (word_rules, " ".join(hashcat_rules),password)
LOGGER.debug("\t[*] Simple Processing Ended: %s => %s => %s" % (word_rules, " ".join(hashcat_rules),password))
# Check if rules result in the correct password
if word_rules == password:
return hashcat_rules
else:
if self.debug: print "[!] Simple Processing FAILED: %s => %s => %s (%s)" % (word," ".join(hashcat_rules),password,word_rules)
LOGGER.debug("[!] Simple Processing FAILED: %s => %s => %s (%s)" % (word," ".join(hashcat_rules),password,word_rules))
return None
def generate_advanced_hashcat_rules(self,word,rules,password):
""" Generate advanced hashcat rules using full range of available rules. """
hashcat_rules = []
if self.debug: print "[*] Advanced Processing %s => %s" % (word,password)
LOGGER.debug("[*] Advanced Processing %s => %s" % (word,password))
# Dynamically apply and store rules in word_rules variable.
# NOTE: Special case where word == password this would work as well.
@ -514,7 +512,7 @@ class RuleGen:
for i,(op,p,w) in enumerate(rules):
if self.debug: print "\t[*] Advanced Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules))
LOGGER.debug("\t[*] Advanced Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules)))
if op == 'insert':
hashcat_rules.append("i%s%s" % (self.int_to_hashcat(p),password[p]))
@ -538,7 +536,7 @@ class RuleGen:
# This rule was made obsolete by a prior global replacement
if word_rules[p] == password[p]:
if self.debug: print "\t[*] Advanced Processing Obsolete Rule: %s - %s" % (word_rules, " ".join(hashcat_rules))
LOGGER.debug("\t[*] Advanced Processing Obsolete Rule: %s - %s" % (word_rules, " ".join(hashcat_rules)))
# Swapping rules
elif p < len(password)-1 and p < len(word_rules)-1 and word_rules[p] == password[p+1] and word_rules[p+1] == password[p]:
@ -649,7 +647,7 @@ class RuleGen:
hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p),password[p]))
word_rules = self.hashcat_rule['o'](word_rules,p,password[p])
if self.debug: print "\t[*] Advanced Processing Ended: %s %s" % (word_rules, " ".join(hashcat_rules))
LOGGER.debug("\t[*] Advanced Processing Ended: %s %s" % (word_rules, " ".join(hashcat_rules)))
########################################################################
# Prefix rules
@ -718,7 +716,7 @@ class RuleGen:
if word_rules == password:
return hashcat_rules
else:
if self.debug: print "[!] Advanced Processing FAILED: %s => %s => %s (%s)" % (word," ".join(hashcat_rules),password,word_rules)
LOGGER.debug("[!] Advanced Processing FAILED: %s => %s => %s (%s)" % (word," ".join(hashcat_rules),password,word_rules))
return None
@ -727,21 +725,21 @@ class RuleGen:
# Skip all numeric passwords
if password.isdigit():
if self.verbose and not self.quiet: print "[!] %s => {skipping numeric} => %s" % (password,password)
if not self.quiet: LOGGER.verbose("[!] %s => {skipping numeric} => %s" % (password,password))
self.numeric_stats_total += 1
return False
# Skip passwords with less than 25% of alpha character
# TODO: Make random word detection more reliable based on word entropy.
elif len([c for c in password if c.isalpha()]) < len(password)/4:
if self.verbose and not self.quiet:print "[!] %s => {skipping alpha less than 25%%} => %s" % (password,password)
if not self.quiet: LOGGER.verbose("[!] %s => {skipping alpha less than 25%%} => %s" % (password,password))
self.special_stats_total += 1
return False
# Only check english ascii passwords for now
# TODO: Add support for more languages.
elif [c for c in password if ord(c) < 32 or ord(c) > 126]:
if self.verbose and not self.quiet: print "[!] %s => {skipping non ascii english} => %s" % (password,password)
if not self.quiet: LOGGER.verbose("[!] %s => {skipping non ascii english} => %s" % (password,password))
self.foreign_stats_total += 1
return False
@ -751,7 +749,7 @@ class RuleGen:
def analyze_password(self,password, rules_queue=multiprocessing.Queue(), words_queue=multiprocessing.Queue()):
""" Analyze a single password. """
if self.verbose: print "[*] Analyzing password: %s" % password
LOGGER.verbose("[*] Analyzing password: %s" % password)
words = []
@ -799,21 +797,20 @@ class RuleGen:
best_found_rule_length = rule_length
elif rule_length > best_found_rule_length:
if self.verbose:
print "[-] %s => {best rule length exceeded: %d (%d)} => %s" % \
(word["suggestion"], rule_length, best_found_rule_length, password)
LOGGER.verbose("[-] %s => {best rule length exceeded: %d (%d)} => %s" % \
(word["suggestion"], rule_length, best_found_rule_length, password))
break
if rule_length <= self.max_rule_len:
hashcat_rule_str = " ".join(hashcat_rule + word["pre_rule"] or [':'])
if self.verbose: print "[+] %s => %s => %s" % (word["suggestion"], hashcat_rule_str, password)
LOGGER.verbose("[+] %s => %s => %s" % (word["suggestion"], hashcat_rule_str, password))
rules_queue.put(hashcat_rule_str)
def password_worker(self,i, passwords_queue, rules_queue, words_queue):
if self.debug: print "[*] Password analysis worker [%d] started." % i
LOGGER.debug("[*] Password analysis worker [%d] started." % i)
try:
while True:
password = passwords_queue.get()
@ -823,16 +820,16 @@ class RuleGen:
self.analyze_password(password, rules_queue, words_queue)
except (KeyboardInterrupt, SystemExit):
if self.debug: print "[*] Password analysis worker [%d] terminated." % i
LOGGER.debug("[*] Password analysis worker [%d] terminated." % i)
if self.debug: print "[*] Password analysis worker [%d] stopped." % i
LOGGER.debug("[*] Password analysis worker [%d] stopped." % i)
def rule_worker(self, rules_queue, output_rules_filename):
""" Worker to store generated rules. """
print "[*] Saving rules to %s" % output_rules_filename
LOGGER.info("[*] Saving rules to %s" % output_rules_filename)
f = open(output_rules_filename, 'w')
if self.debug: print "[*] Rule worker started."
LOGGER.debug("[*] Rule worker started.")
try:
while True:
rule = rules_queue.get()
@ -844,17 +841,17 @@ class RuleGen:
f.flush()
except (KeyboardInterrupt, SystemExit):
if self.debug: print "[*] Rule worker terminated."
LOGGER.debug("[*] Rule worker terminated.")
f.close()
if self.debug: print "[*] Rule worker stopped."
LOGGER.debug("[*] Rule worker stopped.")
def word_worker(self, words_queue, output_words_filename):
""" Worker to store generated rules. """
print "[*] Saving words to %s" % output_words_filename
LOGGER.info("[*] Saving words to %s" % output_words_filename)
f = open(output_words_filename, 'w')
if self.debug: print "[*] Word worker started."
LOGGER.debug("[*] Word worker started.")
try:
while True:
word = words_queue.get()
@ -866,17 +863,17 @@ class RuleGen:
f.flush()
except (KeyboardInterrupt, SystemExit):
if self.debug: print "[*] Word worker terminated."
LOGGER.debug("[*] Word worker terminated.")
f.close()
if self.debug: print "[*] Word worker stopped."
LOGGER.debug("[*] Word worker stopped.")
# Analyze passwords file
def analyze_passwords_file(self,passwords_file):
def analyze_passwords_file(self,passwords_file, encoding):
""" Analyze provided passwords file. """
print "[*] Analyzing passwords file: %s:" % passwords_file
print "[*] Press Ctrl-C to end execution and generate statistical analysis."
LOGGER.info("[*] Analyzing passwords file: %s:" % passwords_file)
LOGGER.info("[*] Press Ctrl-C to end execution and generate statistical analysis.")
# Setup queues
passwords_queue = multiprocessing.Queue(self.threads)
@ -891,7 +888,7 @@ class RuleGen:
# Continue with the main thread
f = open(passwords_file,'r')
f = open(passwords_file,'r', encoding=encoding)
password_count = 0
analysis_start = time.time()
@ -904,8 +901,8 @@ class RuleGen:
# Provide analysis time feedback to the user
if not self.quiet and password_count != 0 and password_count % 5000 == 0:
segment_time = time.time() - segment_start
print "[*] Processed %d passwords in %.2f seconds at the rate of %.2f p/sec" % \
(password_count, segment_start - analysis_start, 5000/segment_time )
LOGGER.info("[*] Processed %d passwords in %.2f seconds at the rate of %.2f p/sec" % \
(password_count, segment_start - analysis_start, 5000/segment_time ))
segment_start = time.time()
password_count += 1
@ -915,7 +912,7 @@ class RuleGen:
passwords_queue.put(password)
except (KeyboardInterrupt, SystemExit):
print "\n[!] Rulegen was interrupted."
LOGGER.warning("\n[!] Rulegen was interrupted.")
else:
# Signal workers to stop.
@ -933,15 +930,15 @@ class RuleGen:
f.close()
analysis_time = time.time() - analysis_start
print "[*] Finished processing %d passwords in %.2f seconds at the rate of %.2f p/sec" % (password_count, analysis_time, float(password_count)/analysis_time )
LOGGER.info("[*] Finished processing %d passwords in %.2f seconds at the rate of %.2f p/sec" % (password_count, analysis_time, float(password_count)/analysis_time ))
print "[*] Generating statistics for [%s] rules and words." % self.basename
print "[-] Skipped %d all numeric passwords (%0.2f%%)" % \
(self.numeric_stats_total, float(self.numeric_stats_total)*100.0/float(password_count))
print "[-] Skipped %d passwords with less than 25%% alpha characters (%0.2f%%)" % \
(self.special_stats_total, float(self.special_stats_total)*100.0/float(password_count))
print "[-] Skipped %d passwords with non ascii characters (%0.2f%%)" % \
(self.foreign_stats_total, float(self.foreign_stats_total)*100.0/float(password_count))
LOGGER.info("[*] Generating statistics for [%s] rules and words." % self.basename)
LOGGER.info("[-] Skipped %d all numeric passwords (%0.2f%%)" % \
(self.numeric_stats_total, float(self.numeric_stats_total)*100.0/float(password_count)))
LOGGER.info("[-] Skipped %d passwords with less than 25%% alpha characters (%0.2f%%)" % \
(self.special_stats_total, float(self.special_stats_total)*100.0/float(password_count)))
LOGGER.info("[-] Skipped %d passwords with non ascii characters (%0.2f%%)" % \
(self.foreign_stats_total, float(self.foreign_stats_total)*100.0/float(password_count)))
# TODO: Counter breaks on large files. uniq -c | sort -rn is still the most
# optimal way.
@ -950,11 +947,11 @@ class RuleGen:
rules_counter = Counter(rules_file)
rule_counter_total = sum(rules_counter.values())
print "\n[*] Top 10 rules"
LOGGER.info("[*] Top 10 rules")
rules_i = 0
for (rule, count) in rules_counter.most_common():
rules_sorted_file.write(rule)
if rules_i < 10: print "[+] %s - %d (%0.2f%%)" % (rule.rstrip('\r\n'), count, count*100/rule_counter_total)
if rules_i < 10: LOGGER.info("[+] %s - %d (%0.2f%%)" % (rule.rstrip('\r\n'), count, count*100/rule_counter_total))
rules_i += 1
rules_file.close()
@ -966,11 +963,11 @@ class RuleGen:
words_counter = Counter(words_file)
word_counter_total = sum(rules_counter.values())
print "\n[*] Top 10 words"
LOGGER.info("[*] Top 10 words")
words_i = 0
for (word, count) in words_counter.most_common():
words_sorted_file.write(word)
if words_i < 10: print "[+] %s - %d (%0.2f%%)" % (word.rstrip('\r\n'), count, count*100/word_counter_total)
if words_i < 10: LOGGER.info("[+] %s - %d (%0.2f%%)" % (word.rstrip('\r\n'), count, count*100/word_counter_total))
words_i += 1
words_file.close()
@ -993,19 +990,19 @@ class RuleGen:
if out == password:
hashcat_rules_str = " ".join(rules or [':'])
if self.verbose: print "[+] %s => %s => %s" % (word, hashcat_rules_str, password)
LOGGER.verbose("[+] %s => %s => %s" % (word, hashcat_rules_str, password))
else:
print "[!] Hashcat Verification FAILED: %s => %s => %s (%s)" % (word," ".join(rules or [':']),password,out)
LOGGER.error("[!] Hashcat Verification FAILED: %s => %s => %s (%s)" % (word," ".join(rules or [':']),password,out))
if __name__ == "__main__":
header = " _ \n"
header += " RuleGen %s | |\n" % VERSION
header += " _ __ __ _ ___| | _\n"
header += " | '_ \ / _` |/ __| |/ /\n"
header += " | '_ \\ / _` |/ __| |/ /\n"
header += " | |_) | (_| | (__| < \n"
header += " | .__/ \__,_|\___|_|\_\\\n"
header += " | .__/ \\__,_|\\___|_|\\_\\\n"
header += " | | \n"
header += " |_| iphelix@thesprawl.org\n"
header += "\n"
@ -1017,6 +1014,7 @@ if __name__ == "__main__":
parser.add_option("-w","--wordlist", help="Use a custom wordlist for rule analysis.", metavar="wiki.dict")
parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.")
parser.add_option("--threads", type="int", default=multiprocessing.cpu_count(), help="Parallel threads to use for processing.")
parser.add_option("-e", "--encoding", help="Input file encoding.", default="utf-8")
wordtune = OptionGroup(parser, "Fine tune source word generation:")
wordtune.add_option("--maxworddist", help="Maximum word edit distance (Levenshtein)", type="int", default=10, metavar="10")
@ -1037,7 +1035,7 @@ if __name__ == "__main__":
spelltune.add_option("--providers", help="Comma-separated list of provider engines", default="aspell,myspell", metavar="aspell,myspell")
parser.add_option_group(spelltune)
debug = OptionGroup(parser, "Debuggin options:")
debug = OptionGroup(parser, "Debugging options:")
debug.add_option("-v","--verbose", help="Show verbose information.", action="store_true", default=False)
debug.add_option("-d","--debug", help="Debug rules.", action="store_true", default=False)
debug.add_option("--password", help="Process the last argument as a password not a file.", action="store_true", default=False)
@ -1049,7 +1047,7 @@ if __name__ == "__main__":
# Print program header
if not options.quiet:
print header
print (header)
if len(args) < 1:
parser.error("no passwords file specified")
@ -1069,23 +1067,25 @@ if __name__ == "__main__":
rulegen.more_rules=options.morerules
rulegen.simple_rules=options.simplerules
rulegen.brute_rules=options.bruterules
if rulegen.brute_rules: print "[!] Bruteforcing reversal and rotation rules. (slower)"
if rulegen.brute_rules: LOGGER.info("[!] Bruteforcing reversal and rotation rules. (slower)")
# Debugging options
rulegen.word = options.word
rulegen.verbose=options.verbose
rulegen.debug = options.debug
rulegen.hashcat = options.hashcat
rulegen.quiet = options.quiet
logging.basicConfig(level=logging.DEBUG if options.debug
else logging.VERBOSE if options.verbose
else logging.INFO)
# Custom wordlist
if not options.word:
if options.wordlist: rulegen.load_custom_wordlist(options.wordlist)
print "[*] Using Enchant '%s' module. For best results please install" % rulegen.enchant.provider.name
print " '%s' module language dictionaries." % rulegen.enchant.provider.name
LOGGER.info("[*] Using Enchant '%s' module. For best results please install" % rulegen.enchant.provider.name)
LOGGER.info(" '%s' module language dictionaries." % rulegen.enchant.provider.name)
# Analyze a single password or several passwords in a file
if options.password:
rulegen.analyze_password(args[0])
else:
rulegen.analyze_passwords_file(args[0])
rulegen.analyze_passwords_file(args[0], options.encoding)