diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/bolt.py b/bolt.py index b80337c..6296b1d 100644 --- a/bolt.py +++ b/bolt.py @@ -1,30 +1,7 @@ -from core.colors import green, yellow, end, run, good, info, bad, white, red - -lightning = '\033[93;5m⚡\033[0m' - - -def banner(): - print (''' - %s⚡ %sBOLT%s ⚡%s - ''' % (yellow, white, yellow, end)) +"""bolt""" - -banner() - -try: - import concurrent.futures - from pathlib import Path -except: - print ('%s Bolt is not compatible with python 2. Please run it with python 3.' % bad) - -try: - from fuzzywuzzy import fuzz, process -except: - import os - print ('%s fuzzywuzzy library is not installed, installing now.' % info) - os.system('pip3 install fuzzywuzzy') - print ('%s fuzzywuzzy has been installed, please restart Bolt.' % info) - quit() +import concurrent.futures +from pathlib import Path import argparse import json @@ -32,6 +9,7 @@ def banner(): import re import statistics +from fuzzywuzzy import fuzz, process from core.entropy import isRandom from core.datanize import datanize from core.prompt import prompt @@ -41,22 +19,44 @@ def banner(): from core.ranger import ranger from core.zetanize import zetanize from core.requester import requester -from core.utils import extractHeaders, strength, isProtected, stringToBinary, longestCommonSubstring +from core.utils import ( + extractHeaders, + strength, + isProtected, + stringToBinary, + longestCommonSubstring, +) + + +from core.colors import green, yellow, end, run, good, info, bad, white, red + +lightning = "\033[93;5m⚡\033[0m" + + +def banner(): + """banner""" + print( + f""" + {yellow}⚡ {white}BOLT{yellow} ⚡{end} + """ + ) + + +banner() parser = argparse.ArgumentParser() -parser.add_argument('-u', help='target url', dest='target') -parser.add_argument('-t', help='number of threads', dest='threads', type=int) -parser.add_argument('-l', help='levels to crawl', dest='level', type=int) -parser.add_argument('--delay', help='delay between requests', - dest='delay', type=int) -parser.add_argument('--timeout', help='http request timeout', - dest='timeout', type=int) -parser.add_argument('--headers', help='http headers', - dest='add_headers', nargs='?', const=True) +parser.add_argument("-u", help="target url", dest="target") +parser.add_argument("-t", help="number of threads", dest="threads", type=int) +parser.add_argument("-l", help="levels to crawl", dest="level", type=int) +parser.add_argument("--delay", help="delay between requests", dest="delay", type=int) +parser.add_argument("--timeout", help="http request timeout", dest="timeout", type=int) +parser.add_argument( + "--headers", help="http headers", dest="add_headers", nargs="?", const=True +) args = parser.parse_args() if not args.target: - print('\n' + parser.format_help().lower()) + print("\n" + parser.format_help().lower()) quit() if type(args.add_headers) == bool: @@ -77,75 +77,77 @@ def banner(): tokenDatabase = [] insecureForms = [] -print (' %s Phase: Crawling %s[%s1/6%s]%s' % - (lightning, green, end, green, end)) +print(" %s Phase: Crawling %s[%s1/6%s]%s" % (lightning, green, end, green, end)) dataset = photon(target, headers, level, threadCount) allForms = dataset[0] -print ('\r%s Crawled %i URL(s) and found %i form(s).%-10s' % - (info, dataset[1], len(allForms), ' ')) -print (' %s Phase: Evaluating %s[%s2/6%s]%s' % - (lightning, green, end, green, end)) +print( + "\r%s Crawled %i URL(s) and found %i form(s).%-10s" + % (info, dataset[1], len(allForms), " ") +) +print(" %s Phase: Evaluating %s[%s2/6%s]%s" % (lightning, green, end, green, end)) evaluate(allForms, weakTokens, tokenDatabase, allTokens, insecureForms) if weakTokens: - print ('%s Weak token(s) found' % good) + print("%s Weak token(s) found" % good) for weakToken in weakTokens: url = list(weakToken.keys())[0] token = list(weakToken.values())[0] - print ('%s %s %s' % (info, url, token)) + print("%s %s %s" % (info, url, token)) if insecureForms: - print ('%s Insecure form(s) found' % good) + print("%s Insecure form(s) found" % good) for insecureForm in insecureForms: url = list(insecureForm.keys())[0] - action = list(insecureForm.values())[0]['action'] - form = action.replace(target, '') + action = list(insecureForm.values())[0]["action"] + form = action.replace(target, "") if form: - print ('%s %s %s[%s%s%s]%s' % - (bad, url, green, end, form, green, end)) + print("%s %s %s[%s%s%s]%s" % (bad, url, green, end, form, green, end)) -print (' %s Phase: Comparing %s[%s3/6%s]%s' % - (lightning, green, end, green, end)) +print(" %s Phase: Comparing %s[%s3/6%s]%s" % (lightning, green, end, green, end)) uniqueTokens = set(allTokens) if len(uniqueTokens) < len(allTokens): - print ('%s Potential Replay Attack condition found' % good) - print ('%s Verifying and looking for the cause' % run) + print("%s Potential Replay Attack condition found" % good) + print("%s Verifying and looking for the cause" % run) replay = False for each in tokenDatabase: url, token = next(iter(each.keys())), next(iter(each.values())) for each2 in tokenDatabase: url2, token2 = next(iter(each2.keys())), next(iter(each2.values())) if token == token2 and url != url2: - print ('%s The same token was used on %s%s%s and %s%s%s' % - (good, green, url, end, green, url2, end)) + print( + "%s The same token was used on %s%s%s and %s%s%s" + % (good, green, url, end, green, url2, end) + ) replay = True if not replay: - print ('%s Further investigation shows that it was a false positive.') + print("%s Further investigation shows that it was a false positive.") -p = Path(__file__).parent.joinpath('db/hashes.json') -with p.open('r') as f: +p = Path(__file__).parent.joinpath("db/hashes.json") +with p.open("r", encoding="latin1") as f: hashPatterns = json.load(f) if not allTokens: - print ('%s No CSRF protection to test' % bad) + print(f"{bad} No CSRF protection to test") quit() aToken = allTokens[0] matches = [] for element in hashPatterns: - pattern = element['regex'] + pattern = element["regex"] if re.match(pattern, aToken): - for name in element['matches']: + for name in element["matches"]: matches.append(name) if matches: - print ('%s Token matches the pattern of following hash type(s):' % info) + print(f"{info} Token matches the pattern of following hash type(s):") for name in matches: - print (' %s>%s %s' % (yellow, end, name)) + print(f" {yellow}>{end} {name}") def fuzzy(tokens): + """fuzzy""" averages = [] + for token in tokens: sameTokenRemoved = False result = process.extract(token, tokens, scorer=fuzz.partial_ratio) @@ -158,23 +160,29 @@ def fuzzy(tokens): scores.append(score) average = statistics.mean(scores) averages.append(average) + return statistics.mean(averages) try: similarity = fuzzy(allTokens) - print ('%s Tokens are %s%i%%%s similar to each other on an average' % - (info, green, similarity, end)) + print( + "%s Tokens are %s%i%%%s similar to each other on an average" + % (info, green, similarity, end) + ) except statistics.StatisticsError: - print ('%s No CSRF protection to test' % bad) + print("%s No CSRF protection to test" % bad) quit() def staticParts(allTokens): + """staticParts""" strings = list(set(allTokens.copy())) commonSubstrings = {} + for theString in strings: strings.remove(theString) + for string in strings: commonSubstring = longestCommonSubstring(theString, string) if commonSubstring not in commonSubstrings: @@ -184,31 +192,32 @@ def staticParts(allTokens): commonSubstrings[commonSubstring].append(theString) if string not in commonSubstrings[commonSubstring]: commonSubstrings[commonSubstring].append(string) + return commonSubstrings result = {k: v for k, v in staticParts(allTokens).items() if v} if result: - print ('%s Common substring found' % info) - print (json.dumps(result, indent=4)) + print(f"{info} Common substring found") + print(json.dumps(result, indent=4)) simTokens = [] -print (' %s Phase: Observing %s[%s4/6%s]%s' % - (lightning, green, end, green, end)) -print ('%s 100 simultaneous requests are being made, please wait.' % info) +print(f" {lightning} Phase: Observing {green}[{end}4/6{green}]{end}") +print(f"{info} 100 simultaneous requests are being made, please wait.") def extractForms(url): + """extractForms""" response = requester(url, {}, headers, True, 0).text forms = zetanize(url, response) + for each in forms.values(): - localTokens = set() - inputs = each['inputs'] + inputs = each["inputs"] for inp in inputs: - value = inp['value'] - if value and re.match(r'^[\w\-_]+$', value): + value = inp["value"] + if value and re.match(r"^[\w\-_]+$", value): if strength(value) > 10: simTokens.append(value) @@ -221,137 +230,142 @@ def extractForms(url): break threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=30) -futures = (threadpool.submit(extractForms, goodCandidate) - for goodCandidate in [goodCandidate] * 30) -for i in concurrent.futures.as_completed(futures): +futures = ( + threadpool.submit(extractForms, goodCandidate) + for goodCandidate in [goodCandidate] * 30 +) + +for _ in concurrent.futures.as_completed(futures): pass if simTokens: if len(set(simTokens)) < len(simTokens): - print ('%s Same tokens were issued for simultaneous requests.' % good) + print(f"{good} Same tokens were issued for simultaneous requests.") else: - print (simTokens) + print(simTokens) else: - print ('%s Different tokens were issued for simultaneous requests.' % info) + print(f"{info} Different tokens were issued for simultaneous requests.") -print (' %s Phase: Testing %s[%s5/6%s]%s' % - (lightning, green, end, green, end)) +print(f" {lightning} Phase: Testing {green}[{end}5/6{green}]{end}") -parsed = '' +parsed = "" found = False -print ('%s Finding a suitable form for further testing. It may take a while.' % run) + +print(f"{run} Finding a suitable form for further testing. It may take a while.") for form_dict in allForms: for url, forms in form_dict.items(): parsed = datanize(forms, tolerate=True) if parsed: found = True break + if found: break if not parsed: - quit('%s No suitable form found for testing.' % bad) + quit(f"{bad} No suitable form found for testing.") origGET = parsed[0] origUrl = parsed[1] origData = parsed[2] -print ('%s Making a request with CSRF token for comparison.' % run) +print(f"{run} Making a request with CSRF token for comparison.") response = requester(origUrl, origData, headers, origGET, 0) originalCode = response.status_code originalLength = len(response.text) -print ('%s Status Code: %s' % (info, originalCode)) -print ('%s Content Length: %i' % (info, originalLength)) -print ('%s Checking if the resonse is dynamic.' % run) +print(f"{info} Status Code: {originalCode}") +print(f"{info} Content Length: {originalLength}") +print(f"{run} Checking if the response is dynamic.") + response = requester(origUrl, origData, headers, origGET, 0) secondLength = len(response.text) + if originalLength != secondLength: - print ('%s Response is dynamic.' % info) + print(f"{info} Response is dynamic.") tolerableDifference = abs(originalLength - secondLength) else: - print ('%s Response isn\'t dynamic.' % info) + print(f"{info} Response isn't dynamic.") tolerableDifference = 0 -print ('%s Emulating a mobile browser' % run) -print ('%s Making a request with mobile browser' % run) -headers['User-Agent'] = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows CE; PPC; 240x320)' +print(f"{run} Emulating a mobile browser") +print(f"{run} Making a request with mobile browser") +headers["User-Agent"] = "Mozilla/4.0 (compatible; MSIE 5.5; Windows CE; PPC; 240x320)" response = requester(origUrl, {}, headers, True, 0).text parsed = zetanize(origUrl, response) + if isProtected(parsed): - print ('%s CSRF protection is enabled for mobile browsers as well.' % bad) + print(f"{bad} CSRF protection is enabled for mobile browsers as well.") else: - print ('%s CSRF protection isn\'t enabled for mobile browsers.' % good) + print(f"{good} CSRF protection isn't enabled for mobile browsers.") -print ('%s Making a request without CSRF token parameter.' % run) +print(f"{run} Making a request without CSRF token parameter.") -data = tweaker(origData, 'remove') +data = tweaker(origData, "remove") response = requester(origUrl, data, headers, origGET, 0) if response.status_code == originalCode: - if str(originalCode)[0] in ['4', '5']: - print ('%s It didn\'t work' % bad) + if str(originalCode)[0] in ["4", "5"]: + print(f"{bad} It didn't work") else: difference = abs(originalLength - len(response.text)) if difference <= tolerableDifference: - print ('%s It worked!' % good) + print(f"{good} It worked!") else: - print ('%s It didn\'t work' % bad) + print(f"{bad} It didn't work") -print ('%s Making a request without CSRF token parameter value.' % run) -data = tweaker(origData, 'clear') +print(f"{run} Making a request without CSRF token parameter value.") +data = tweaker(origData, "clear") response = requester(origUrl, data, headers, origGET, 0) if response.status_code == originalCode: - if str(originalCode)[0] in ['4', '5']: - print ('%s It didn\'t work' % bad) + if str(originalCode)[0] in ["4", "5"]: + print(f"{bad} It didn't work") else: difference = abs(originalLength - len(response.text)) if difference <= tolerableDifference: - print ('%s It worked!' % good) + print(f"{good} It worked!") else: - print ('%s It didn\'t work' % bad) + print(f"{bad} It didn't work") seeds = ranger(allTokens) -print ('%s Checking if tokens are checked to a specific length' % run) +print(f"{run} Checking if tokens are checked to a specific length") for index in range(len(allTokens[0])): - data = tweaker(origData, 'replace', index=index, seeds=seeds) + data = tweaker(origData, "replace", index=index, seeds=seeds) response = requester(origUrl, data, headers, origGET, 0) if response.status_code == originalCode: - if str(originalCode)[0] in ['4', '5']: + if str(originalCode)[0] in ["4", "5"]: break else: difference = abs(originalLength - len(response.text)) if difference <= tolerableDifference: - print ('%s Last %i chars of token aren\'t being checked' % - (good, index + 1)) + print(f"{good} Last {index+1} chars of token aren't being checked") else: break -print ('%s Generating a fake token.' % run) +print(f"{run} Generating a fake token.") -data = tweaker(origData, 'generate', seeds=seeds) -print ('%s Making a request with the self generated token.' % run) +data = tweaker(origData, "generate", seeds=seeds) +print(f"{run} Making a request with the self generated token.") response = requester(origUrl, data, headers, origGET, 0) if response.status_code == originalCode: - if str(originalCode)[0] in ['4', '5']: - print ('%s It didn\'t work' % bad) + if str(originalCode)[0] in ["4", "5"]: + print(f"{bad} It didn't work") else: difference = abs(originalLength - len(response.text)) if difference <= tolerableDifference: - print ('%s It worked!' % good) + print(f"{good} It worked!") else: - print ('%s It didn\'t work' % bad) + print(f"{bad} It didn't work") -print (' %s Phase: Analysing %s[%s6/6%s]%s' % - (lightning, green, end, green, end)) +print(f" {lightning} Phase: Analysing {green}[{end}6/6{green}]{end}") -binary = stringToBinary(''.join(allTokens)) +binary = stringToBinary("".join(allTokens)) result = isRandom(binary) for name, result in result.items(): if not result: - print ('%s %s : %s%s%s' % (good, name, green, 'non-random', end)) + print(f"{good} {name} : {green}non-random{end}") else: - print ('%s %s : %s%s%s' % (bad, name, red, 'random', end)) + print(f"{bad} {name} : {red}random{end}") diff --git a/core/colors.py b/core/colors.py index 222f0af..da86a09 100644 --- a/core/colors.py +++ b/core/colors.py @@ -1,22 +1,23 @@ +"""colors""" import sys colors = True # Output should be colored machine = sys.platform # Detecting the os of current system -if machine.lower().startswith(('os', 'win', 'darwin', 'ios')): +if machine.lower().startswith(("os", "win", "darwin", "ios")): colors = False # Colors shouldn't be displayed in mac & windows if not colors: - end = red = white = green = yellow = run = bad = good = info = que = '' - lightning = '⚡' + end = red = white = green = yellow = run = bad = good = info = que = "" + lightning = "⚡" else: - white = '\033[97m' - green = '\033[92m' - red = '\033[91m' - yellow = '\033[93m' - end = '\033[0m' - back = '\033[7;91m' - info = '\033[93m[!]\033[0m' - que = '\033[94m[?]\033[0m' - bad = '\033[91m[-]\033[0m' - good = '\033[92m[+]\033[0m' - run = '\033[97m[~]\033[0m' - lightning = '\033[93;5m⚡\033[0m' + white = "\033[97m" + green = "\033[92m" + red = "\033[91m" + yellow = "\033[93m" + end = "\033[0m" + back = "\033[7;91m" + info = "\033[93m[!]\033[0m" + que = "\033[94m[?]\033[0m" + bad = "\033[91m[-]\033[0m" + good = "\033[92m[+]\033[0m" + run = "\033[97m[~]\033[0m" + lightning = "\033[93;5m⚡\033[0m" diff --git a/core/config.py b/core/config.py index 1b38588..e039230 100644 --- a/core/config.py +++ b/core/config.py @@ -1,15 +1,26 @@ -password = 'xXx!69!xXx' -email = 'testing@gmail.com' -strings = ['red', 'bob', 'admin', 'alex', 'testing', - 'test', 'lol', 'yes', 'dragon', 'bad'] -commonNames = ['csrf', 'auth', 'token', 'verify', 'hash'] -tokenPattern = r'^[\w\-_+=/]{14,256}$' +"""config""" +password = "xXx!69!xXx" +email = "testing@gmail.com" +strings = [ + "red", + "bob", + "admin", + "alex", + "testing", + "test", + "lol", + "yes", + "dragon", + "bad", +] +commonNames = ["csrf", "auth", "token", "verify", "hash"] +tokenPattern = r"^[\w\-_+=/]{14,256}$" headers = { # default headers - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip,deflate', - 'Connection': 'close', - 'DNT': '1', - 'Upgrade-Insecure-Requests': '1', + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip,deflate", + "Connection": "close", + "DNT": "1", + "Upgrade-Insecure-Requests": "1", } diff --git a/core/datanize.py b/core/datanize.py index 4607f77..0b7b764 100644 --- a/core/datanize.py +++ b/core/datanize.py @@ -1,3 +1,4 @@ +"""datanize""" import random import re @@ -10,25 +11,25 @@ def datanize(forms, tolerate=False): data = {} login = False protected = False - action = oneForm['action'] - method = oneForm['method'] - inputs = oneForm['inputs'] + action = oneForm["action"] + method = oneForm["method"] + inputs = oneForm["inputs"] for inp in inputs: - name = inp['name'] - kind = inp['type'] - value = inp['value'] + name = inp["name"] + kind = inp["type"] + value = inp["value"] if re.match(tokenPattern, value): protected = True - if kind == 'password': + if kind == "password": data[name] = password login = True - if kind == 'email': + if kind == "email": data[name] = email - if kind == 'text': + if kind == "text": data[name] = random.choice(strings) else: data[name] = value - if method == 'GET': + if method == "GET": GET = True else: GET = False diff --git a/core/entropy.py b/core/entropy.py index e88e559..8c0af08 100644 --- a/core/entropy.py +++ b/core/entropy.py @@ -1,57 +1,77 @@ #!/usr/bin/env python3 -import numpy as np from math import floor, log +from functools import reduce +import scipy.stats as sst + +import numpy as np import scipy.special as spc import scipy.fftpack as sff -import scipy.stats as sst -from functools import reduce -def sumi(x): return 2 * x - 1 +def sumi(x): + """sumi""" + return 2 * x - 1 -def su(x, y): return x + y +def su(x, y): + """su""" + return x + y -def sus(x): return (x - 0.5) ** 2 +def sus(x): + """sus""" + return (x - 0.5) ** 2 -def sq(x): return int(x) ** 2 +def sq(x): + """sq""" + return int(x) ** 2 -def logo(x): return x * np.log(x) +def logo(x): + """logo""" + return x * np.log(x) def pr(u, x): + """pr""" if u == 0: out = 1.0 * np.exp(-x) else: - out = 1.0 * x * np.exp(2*-x) * (2**-u) * spc.hyp1f1(u + 1, 2, x) + out = 1.0 * x * np.exp(2 * -x) * (2**-u) * spc.hyp1f1(u + 1, 2, x) return out def stringpart(binin, num): - blocks = [binin[xs * num:num + xs * num:] - for xs in range(floor(len(binin) / num))] + """stringpart""" + blocks = [ + binin[xs * num : num + xs * num :] for xs in range(floor(len(binin) / num)) + ] return blocks def randgen(num): - '''Spits out a stream of random numbers like '1001001' with the length num''' + """Spits out a stream of random numbers like '1001001' with the length num""" - rn = open('/dev/urandom', 'r') - random_chars = rn.read(num / 2) - stream = '' - for char in random_chars: - c = ord(char) - for i in range(0, 2): - stream += str(c >> i & 1) - return stream + with open("/dev/urandom", "r", encoding="latin1") as rn: + random_chars = rn.read(num / 2) + stream = "" + for char in random_chars: + c = ord(char) + for i in range(0, 2): + stream += str(c >> i & 1) + return stream def monobitfrequencytest(binin): - ''' The focus of the test is the proportion of zeroes and ones for the entire sequence. The purpose of this test is to determine whether that number of ones and zeros in a sequence are approximately the same as would be expected for a truly random sequence. The test assesses the closeness of the fraction of ones to 1/2, that is, the number of ones and zeroes in a sequence should be about the same.''' + """ + The focus of the test is the proportion of zeroes and ones for the entire sequence. + The purpose of this test is to determine whether that number of ones and zeros + in a sequence are approximately the same as would be expected for a truly random + sequence. The test assesses the closeness of the fraction of ones to 1/2, + that is, the number of ones and zeroes in a sequence should be about the same. + """ ss = [int(el) for el in binin] sc = list(map(sumi, ss)) @@ -62,10 +82,16 @@ def monobitfrequencytest(binin): def blockfrequencytest(binin, nu=20): - ''' The focus of the test is the proportion of zeroes and ones within M-bit blocks. The purpose of this test is to determine whether the frequency of ones is an M-bit block is approximately M/2.''' + """ + The focus of the test is the proportion of zeroes and ones within M-bit blocks. + The purpose of this test is to determine whether the frequency of ones is an + M-bit block is approximately M/2. + """ ss = [int(el) for el in binin] - tt = [1.0 * sum(ss[xs * nu:nu + xs * nu:]) / - nu for xs in range(floor(len(ss) / nu))] + tt = [ + 1.0 * sum(ss[xs * nu : nu + xs * nu :]) / nu + for xs in range(floor(len(ss) / nu)) + ] uu = list(map(sus, tt)) chisqr = 4 * nu * reduce(su, uu, 0) pval = spc.gammaincc(len(tt) / 2.0, chisqr / 2.0) @@ -73,110 +99,143 @@ def blockfrequencytest(binin, nu=20): def runstest(binin): - ''' The focus of this test is the total number of zero and one runs in the entire sequence, where a run is an uninterrupted sequence of identical bits. A run of length k means that a run consists of exactly k identical bits and is bounded before and after with a bit of the opposite value. The purpose of the runs test is to determine whether the number of runs of ones and zeros of various lengths is as expected for a random sequence. In particular, this test determines whether the oscillation between such substrings is too fast or too slow.''' + """ + The focus of this test is the total number of zero and one runs in the entire + sequence, where a run is an uninterrupted sequence of identical bits. A run of + length k means that a run consists of exactly k identical bits and is bounded + before and after with a bit of the opposite value. The purpose of the runs test + is to determine whether the number of runs of ones and zeros of various lengths + is as expected for a random sequence. In particular, this test determines + whether the oscillation between such substrings is too fast or too slow. + """ ss = [int(el) for el in binin] n = len(binin) pi = 1.0 * reduce(su, ss) / n - vobs = len(binin.replace('0', ' ').split()) + \ - len(binin.replace('1', ' ').split()) - pval = spc.erfc(abs(vobs-2*n*pi*(1-pi)) / - (2 * pi * (1 - pi) * np.sqrt(2*n))) + vobs = len(binin.replace("0", " ").split()) + len(binin.replace("1", " ").split()) + pval = spc.erfc( + abs(vobs - 2 * n * pi * (1 - pi)) / (2 * pi * (1 - pi) * np.sqrt(2 * n)) + ) return pval def longestrunones8(binin): - ''' The focus of the test is the longest run of ones within M-bit blocks. The purpose of this test is to determine whether the length of the longest run of ones within the tested sequence is consistent with the length of the longest run of ones that would be expected in a random sequence. Note that an irregularity in the expected length of the longest run of ones implies that there is also an irregularity in the expected length of the longest run of zeroes. Long runs of zeroes were not evaluated separately due to a concern about statistical independence among the tests.''' + """ + The focus of the test is the longest run of ones within M-bit blocks. + The purpose of this test is to determine whether the length of the longest + run of ones within the tested sequence is consistent with the length of + the longest run of ones that would be expected in a random sequence. + Note that an irregularity in the expected length of the longest run of + ones implies that there is also an irregularity in the expected length + of the longest run of zeroes. Long runs of zeroes were not evaluated + separately due to a concern about statistical independence among the + tests. + """ m = 8 k = 3 - pik = [0.2148, 0.3672, 0.2305, 0.1875] - blocks = [binin[xs*m:m+xs*m:] for xs in range(len(binin) / m)] + pikt = [0.2148, 0.3672, 0.2305, 0.1875] + blocks = [binin[xs * m : m + xs * m :] for xs in range(len(binin) / m)] n = len(blocks) # append the string 01 to guarantee the length of 1 - counts1 = [xs+'01' for xs in blocks] - counts = [xs.replace('0', ' ').split() - for xs in counts1] # split into all parts + counts1 = [xs + "01" for xs in blocks] + counts = [xs.replace("0", " ").split() for xs in counts1] # split into all parts counts2 = [list(map(len, xx)) for xx in counts] counts4 = [(4 if xx > 4 else xx) for xx in map(max, counts2)] freqs = [counts4.count(spi) for spi in [1, 2, 3, 4]] - chisqr1 = [(freqs[xx]-n*pik[xx])**2/(n*pik[xx]) for xx in range(4)] + chisqr1 = [(freqs[xx] - n * pikt[xx]) ** 2 / (n * pikt[xx]) for xx in range(4)] chisqr = reduce(su, chisqr1) pval = spc.gammaincc(k / 2.0, chisqr / 2.0) return pval def longestrunones128(binin): # not well tested yet + """longestrunones128""" if len(binin) > 128: m = 128 k = 5 n = len(binin) - pik = [0.1174, 0.2430, 0.2493, 0.1752, 0.1027, 0.1124] - blocks = [binin[xs * m:m + xs * m:] for xs in range(len(binin) / m)] + pikt = [0.1174, 0.2430, 0.2493, 0.1752, 0.1027, 0.1124] + blocks = [binin[xs * m : m + xs * m :] for xs in range(len(binin) / m)] n = len(blocks) - counts = [xs.replace('0', ' ').split() for xs in blocks] + counts = [xs.replace("0", " ").split() for xs in blocks] counts2 = [list(map(len, xx)) for xx in counts] counts3 = [(1 if xx < 1 else xx) for xx in map(max, counts2)] counts4 = [(4 if xx > 4 else xx) for xx in counts3] - chisqr1 = [(counts4[xx] - n * pik[xx]) ** 2 / (n * pik[xx]) - for xx in range(len(counts4))] + chisqr1 = [ + (counts4[xx] - n * pikt[xx]) ** 2 / (n * pikt[xx]) + for xx in range(len(counts4)) + ] chisqr = reduce(su, chisqr1) pval = spc.gammaincc(k / 2.0, chisqr / 2.0) else: - print('longestrunones128 failed, too few bits:', len(binin)) + print("longestrunones128 failed, too few bits:", len(binin)) pval = 0 return pval def longestrunones10000(binin): # not well tested yet - ''' The focus of the test is the longest run of ones within M-bit blocks. The purpose of this test is to determine whether the length of the longest run of ones within the tested sequence is consistent with the length of the longest run of ones that would be expected in a random sequence. Note that an irregularity in the expected length of the longest run of ones implies that there is also an irregularity in the expected length of the longest run of zeroes. Long runs of zeroes were not evaluated separately due to a concern about statistical independence among the tests.''' + """The focus of the test is the longest run of ones within M-bit blocks. The purpose of this test is to determine whether the length of the longest run of ones within the tested sequence is consistent with the length of the longest run of ones that would be expected in a random sequence. Note that an irregularity in the expected length of the longest run of ones implies that there is also an irregularity in the expected length of the longest run of zeroes. Long runs of zeroes were not evaluated separately due to a concern about statistical independence among the tests.""" if len(binin) > 128: m = 10000 k = 6 - pik = [0.0882, 0.2092, 0.2483, 0.1933, 0.1208, 0.0675, 0.0727] - blocks = [binin[xs * m:m + xs * m:] - for xs in range(floor(len(binin) / m))] + pikt = [0.0882, 0.2092, 0.2483, 0.1933, 0.1208, 0.0675, 0.0727] + blocks = [binin[xs * m : m + xs * m :] for xs in range(floor(len(binin) / m))] n = len(blocks) - counts = [xs.replace('0', ' ').split() for xs in blocks] + counts = [xs.replace("0", " ").split() for xs in blocks] counts2 = [list(map(len, xx)) for xx in counts] counts3 = [(10 if xx < 10 else xx) for xx in map(max, counts2)] counts4 = [(16 if xx > 16 else xx) for xx in counts3] freqs = [counts4.count(spi) for spi in [10, 11, 12, 13, 14, 15, 16]] - chisqr1 = [(freqs[xx] - n * pik[xx]) ** 2 / (n * pik[xx]) - for xx in range(len(freqs))] + chisqr1 = [ + (freqs[xx] - n * pikt[xx]) ** 2 / (n * pikt[xx]) for xx in range(len(freqs)) + ] chisqr = reduce(su, chisqr1) pval = spc.gammaincc(k / 2.0, chisqr / 2.0) else: - print('longestrunones10000 failed, too few bits:', len(binin)) + print("longestrunones10000 failed, too few bits:", len(binin)) pval = 0 return pval + # test 2.06 def spectraltest(binin): - '''The focus of this test is the peak heights in the discrete Fast Fourier Transform. The purpose of this test is to detect periodic features (i.e., repetitive patterns that are near each other) in the tested sequence that would indicate a deviation from the assumption of randomness. ''' + """ + The focus of this test is the peak heights in the discrete Fast Fourier Transform. + The purpose of this test is to detect periodic features (i.e., repetitive patterns + that are near each other) in the tested sequence that would indicate a deviation + from the assumption of randomness. + """ n = len(binin) ss = [int(el) for el in binin] sc = list(map(sumi, ss)) ft = sff.fft(sc) - af = abs(ft)[1:floor(n/2)+1:] - t = np.sqrt(np.log(1/0.05)*n) - n0 = 0.95*n/2 + af = abs(ft)[1 : floor(n / 2) + 1 :] + t = np.sqrt(np.log(1 / 0.05) * n) + n0 = 0.95 * n / 2 n1 = len(np.where(af < t)[0]) - d = (n1 - n0)/np.sqrt(n*0.95*0.05/4) - pval = spc.erfc(abs(d)/np.sqrt(2)) + d = (n1 - n0) / np.sqrt(n * 0.95 * 0.05 / 4) + pval = spc.erfc(abs(d) / np.sqrt(2)) return pval def nonoverlappingtemplatematchingtest(binin, mat="000000001", num=9): - ''' The focus of this test is the number of occurrences of pre-defined target substrings. The purpose of this test is to reject sequences that exhibit too many occurrences of a given non-periodic (aperiodic) pattern. For this test and for the Overlapping Template Matching test, an m-bit window is used to search for a specific m-bit pattern. If the pattern is not found, the window slides one bit position. For this test, when the pattern is found, the window is reset to the bit after the found pattern, and the search resumes.''' + """ + The focus of this test is the number of occurrences of pre-defined target substrings. + The purpose of this test is to reject sequences that exhibit too many occurrences of + a given non-periodic (aperiodic) pattern. For this test and for the Overlapping + Template Matching test, an m-bit window is used to search for a specific m-bit pattern. + If the pattern is not found, the window slides one bit position. For this test, + when the pattern is found, the window is reset to the bit after the found pattern, + and the search resumes.""" n = len(binin) m = len(mat) - M = floor(n/num) - blocks = [binin[xs*M:M+xs*M:] for xs in range(floor(n/M))] + M = floor(n / num) + blocks = [binin[xs * M : M + xs * M :] for xs in range(floor(n / M))] counts = [xx.count(mat) for xx in blocks] - avg = 1.0 * (M-m+1)/2 ** m - var = M*(2**-m - (2*m-1)*2**(-2*m)) + avg = 1.0 * (M - m + 1) / 2**m + var = M * (2**-m - (2 * m - 1) * 2 ** (-2 * m)) chisqr = reduce(su, [(xs - avg) ** 2 for xs in counts]) / var pval = spc.gammaincc(1.0 * len(blocks) / 2, chisqr / 2) return pval @@ -185,7 +244,7 @@ def nonoverlappingtemplatematchingtest(binin, mat="000000001", num=9): def occurances(string, sub): count = start = 0 while True: - start = string.find(sub, start)+1 + start = string.find(sub, start) + 1 if start > 0: count += 1 else: @@ -193,29 +252,45 @@ def occurances(string, sub): def overlappingtemplatematchingtest(binin, mat="111111111", num=1032, numi=9): - ''' The focus of this test is the number of pre-defined target substrings. The purpose of this test is to reject sequences that show deviations from the expected number of runs of ones of a given length. Note that when there is a deviation from the expected number of ones of a given length, there is also a deviation in the runs of zeroes. Runs of zeroes were not evaluated separately due to a concern about statistical independence among the tests. For this test and for the Non-overlapping Template Matching test, an m-bit window is used to search for a specific m-bit pattern. If the pattern is not found, the window slides one bit position. For this test, when the pattern is found, the window again slides one bit, and the search is resumed.''' + """ + The focus of this test is the number of pre-defined target substrings. + The purpose of this test is to reject sequences that show deviations + from the expected number of runs of ones of a given length. Note that + when there is a deviation from the expected number of ones of a given + length, there is also a deviation in the runs of zeroes. Runs of zeroes + were not evaluated separately due to a concern about statistical + independence among the tests. For this test and for the Non-overlapping + Template Matching test, an m-bit window is used to search for a specific + m-bit pattern. If the pattern is not found, the window slides one bit position. + For this test, when the pattern is found, the window again slides one bit, + and the search is resumed.""" n = len(binin) bign = int(n / num) m = len(mat) - lamda = 1.0 * (num - m + 1) / 2 ** m + lamda = 1.0 * (num - m + 1) / 2**m eta = 0.5 * lamda pi = [pr(i, eta) for i in range(numi)] pi.append(1 - reduce(su, pi)) v = [0 for x in range(numi + 1)] blocks = stringpart(binin, num) - blocklen = len(blocks[0]) counts = [occurances(i, mat) for i in blocks] counts2 = [(numi if xx > numi else xx) for xx in counts] for i in counts2: v[i] = v[i] + 1 - chisqr = reduce(su, [(v[i]-bign*pi[i]) ** 2 / (bign*pi[i]) - for i in range(numi + 1)]) - pval = spc.gammaincc(0.5*numi, 0.5*chisqr) + chisqr = reduce( + su, [(v[i] - bign * pi[i]) ** 2 / (bign * pi[i]) for i in range(numi + 1)] + ) + pval = spc.gammaincc(0.5 * numi, 0.5 * chisqr) return pval def maurersuniversalstatistictest(binin, l=6, q=640): - ''' The focus of this test is the number of bits between matching patterns. The purpose of the test is to detect whether or not the sequence can be significantly compressed without loss of information. An overly compressible sequence is considered to be non-random.''' + """ + The focus of this test is the number of bits between matching patterns. + The purpose of the test is to detect whether or not the sequence can be + significantly compressed without loss of information. + An overly compressible sequence is considered to be non-random. + """ ru = [ [0.7326495, 0.690], [1.5374383, 1.338], @@ -238,94 +313,119 @@ def maurersuniversalstatistictest(binin, l=6, q=640): k = len(blocks) - q states = [0 for x in range(2**l)] for x in range(q): - states[blocks[x]-1] = x+1 - sumi = 0.0 + states[blocks[x] - 1] = x + 1 + sumit = 0.0 for x in range(q, len(blocks)): - sumi += np.log2((x+1)-states[blocks[x]-1]) - states[blocks[x]-1] = x+1 - fn = sumi / k - c = 0.7-(0.8/l)+(4+(32.0/l))*((k**(-3.0/l))/15) - sigma = c*np.sqrt((ru[l-1][1])/k) - pval = spc.erfc(abs(fn-ru[l-1][0]) / (np.sqrt(2)*sigma)) + sumit += np.log2((x + 1) - states[blocks[x] - 1]) + states[blocks[x] - 1] = x + 1 + fn = sumit / k + c = 0.7 - (0.8 / l) + (4 + (32.0 / l)) * ((k ** (-3.0 / l)) / 15) + sigma = c * np.sqrt((ru[l - 1][1]) / k) + pval = spc.erfc(abs(fn - ru[l - 1][0]) / (np.sqrt(2) * sigma)) return pval def lempelzivcompressiontest1(binin): - ''' The focus of this test is the number of cumulatively distinct patterns (words) in the sequence. The purpose of the test is to determine how far the tested sequence can be compressed. The sequence is considered to be non-random if it can be significantly compressed. A random sequence will have a characteristic number of distinct patterns.''' + """ + The focus of this test is the number of cumulatively distinct patterns + (words) in the sequence. The purpose of the test is to determine + how far the tested sequence can be compressed. The sequence is + considered to be non-random if it can be significantly compressed. + A random sequence will have a characteristic number of distinct patterns. + """ i = 1 j = 0 n = len(binin) mu = 69586.25 sigma = 70.448718 words = [] - while (i+j) <= n: - tmp = binin[i:i+j:] + while (i + j) <= n: + tmp = binin[i : i + j :] if words.count(tmp) > 0: j += 1 else: words.append(tmp) - i += j+1 + i += j + 1 j = 0 wobs = len(words) - pval = 0.5*spc.erfc((mu-wobs)/np.sqrt(2.0*sigma)) + pval = 0.5 * spc.erfc((mu - wobs) / np.sqrt(2.0 * sigma)) return pval + # test 2.11 def serialtest(binin): + """ + The focus of this test is the frequency of each and every overlapping + m-bit pattern across the entire sequence. The purpose of this test is + to determine whether the number of occurrences of the 2m m-bit + overlapping patterns is approximately the same as would be expected + for a random sequence. The pattern can overlap. + """ m = int(log(len(binin), 2) - 3) - ''' The focus of this test is the frequency of each and every overlapping m-bit pattern across the entire sequence. The purpose of this test is to determine whether the number of occurrences of the 2m m-bit overlapping patterns is approximately the same as would be expected for a random sequence. The pattern can overlap.''' n = len(binin) - hbin = binin+binin[0:m-1:] - f1a = [hbin[xs:m+xs:] for xs in range(n)] + hbin = binin + binin[0 : m - 1 :] + f1a = [hbin[xs : m + xs :] for xs in range(n)] oo = set(f1a) - f1 = [f1a.count(xs)**2 for xs in oo] + f1 = [f1a.count(xs) ** 2 for xs in oo] f1 = list(map(f1a.count, oo)) - cou = f1a.count - f2a = [hbin[xs:m-1+xs:] for xs in range(n)] - f2 = [f2a.count(xs)**2 for xs in set(f2a)] - f3a = [hbin[xs:m-2+xs:] for xs in range(n)] - f3 = [f3a.count(xs)**2 for xs in set(f3a)] + f2a = [hbin[xs : m - 1 + xs :] for xs in range(n)] + f2 = [f2a.count(xs) ** 2 for xs in set(f2a)] + f3a = [hbin[xs : m - 2 + xs :] for xs in range(n)] + f3 = [f3a.count(xs) ** 2 for xs in set(f3a)] psim1 = 0 psim2 = 0 psim3 = 0 if m >= 0: suss = reduce(su, f1) - psim1 = 1.0 * 2 ** m * suss / n - n + psim1 = 1.0 * 2**m * suss / n - n if m >= 1: suss = reduce(su, f2) psim2 = 1.0 * 2 ** (m - 1) * suss / n - n if m >= 2: suss = reduce(su, f3) psim3 = 1.0 * 2 ** (m - 2) * suss / n - n - d1 = psim1-psim2 - d2 = psim1-2 * psim2 + psim3 + d1 = psim1 - psim2 + d2 = psim1 - 2 * psim2 + psim3 pval1 = spc.gammaincc(2 ** (m - 2), d1 / 2.0) pval2 = spc.gammaincc(2 ** (m - 3), d2 / 2.0) return [pval1, pval2] def cumultativesumstest(binin): - ''' The focus of this test is the maximal excursion (from zero) of the random walk defined by the cumulative sum of adjusted (-1, +1) digits in the sequence. The purpose of the test is to determine whether the cumulative sum of the partial sequences occurring in the tested sequence is too large or too small relative to the expected behavior of that cumulative sum for random sequences. This cumulative sum may be considered as a random walk. For a random sequence, the random walk should be near zero. For non-random sequences, the excursions of this random walk away from zero will be too large.''' + """ + The focus of this test is the maximal excursion (from zero) of the + random walk defined by the cumulative sum of adjusted (-1, +1) digits + in the sequence. The purpose of the test is to determine whether + the cumulative sum of the partial sequences occurring in the tested + sequence is too large or too small relative to the expected behavior + of that cumulative sum for random sequences. This cumulative sum + may be considered as a random walk. For a random sequence, the + random walk should be near zero. For non-random sequences, the + excursions of this random walk away from zero will be too large. + """ n = len(binin) ss = [int(el) for el in binin] sc = list(map(sumi, ss)) cs = np.cumsum(sc) z = max(abs(cs)) - ra = 0 start = int(np.floor(0.25 * np.floor(-n / z) + 1)) stop = int(np.floor(0.25 * np.floor(n / z) - 1)) pv1 = [] for k in range(start, stop + 1): - pv1.append(sst.norm.cdf((4 * k + 1) * z / np.sqrt(n)) - - sst.norm.cdf((4 * k - 1) * z / np.sqrt(n))) + pv1.append( + sst.norm.cdf((4 * k + 1) * z / np.sqrt(n)) + - sst.norm.cdf((4 * k - 1) * z / np.sqrt(n)) + ) start = int(np.floor(0.25 * np.floor(-n / z - 3))) stop = int(np.floor(0.25 * np.floor(n / z) - 1)) pv2 = [] for k in range(start, stop + 1): - pv2.append(sst.norm.cdf((4 * k + 3) * z / np.sqrt(n)) - - sst.norm.cdf((4 * k + 1) * z / np.sqrt(n))) + pv2.append( + sst.norm.cdf((4 * k + 3) * z / np.sqrt(n)) + - sst.norm.cdf((4 * k + 1) * z / np.sqrt(n)) + ) pval = 1 pval -= reduce(su, pv1) pval += reduce(su, pv2) @@ -334,23 +434,45 @@ def cumultativesumstest(binin): def cumultativesumstestreverse(binin): - '''The focus of this test is the maximal excursion (from zero) of the random walk defined by the cumulative sum of adjusted (-1, +1) digits in the sequence. The purpose of the test is to determine whether the cumulative sum of the partial sequences occurring in the tested sequence is too large or too small relative to the expected behavior of that cumulative sum for random sequences. This cumulative sum may be considered as a random walk. For a random sequence, the random walk should be near zero. For non-random sequences, the excursions of this random walk away from zero will be too large. ''' + """ + The focus of this test is the maximal excursion (from zero) + of the random walk defined by the cumulative sum of adjusted + (-1, +1) digits in the sequence. The purpose of the test is + to determine whether the cumulative sum of the partial + sequences occurring in the tested sequence is too large or + too small relative to the expected behavior of that cumulative + sum for random sequences. This cumulative sum may be considered + as a random walk. For a random sequence, the random walk should + be near zero. For non-random sequences, the excursions of this + random walk away from zero will be too large. + """ pval = cumultativesumstest(binin[::-1]) return pval def pik(k, x): + """pik""" if k == 0: - out = 1-1.0/(2*np.abs(x)) + out = 1 - 1.0 / (2 * np.abs(x)) elif k >= 5: - out = (1.0/(2*np.abs(x)))*(1-1.0/(2*np.abs(x)))**4 + out = (1.0 / (2 * np.abs(x))) * (1 - 1.0 / (2 * np.abs(x))) ** 4 else: - out = (1.0/(4*x*x))*(1-1.0/(2*np.abs(x)))**(k-1) + out = (1.0 / (4 * x * x)) * (1 - 1.0 / (2 * np.abs(x))) ** (k - 1) return out def randomexcursionstest(binin): - ''' The focus of this test is the number of cycles having exactly K visits in a cumulative sum random walk. The cumulative sum random walk is found if partial sums of the (0,1) sequence are adjusted to (-1, +1). A random excursion of a random walk consists of a sequence of n steps of unit length taken at random that begin at and return to the origin. The purpose of this test is to determine if the number of visits to a state within a random walk exceeds what one would expect for a random sequence.''' + """ + The focus of this test is the number of cycles having exactly + K visits in a cumulative sum random walk. The cumulative + sum random walk is found if partial sums of the (0,1) sequence + are adjusted to (-1, +1). A random excursion of a random walk + consists of a sequence of n steps of unit length taken at random + that begin at and return to the origin. The purpose of this + test is to determine if the number of visits to a state within + a random walk exceeds what one would expect for a random + sequence. + """ xvals = [-4, -3, -2, -1, 1, 2, 3, 4] ss = [int(el) for el in binin] sc = list(map(sumi, ss)) @@ -358,34 +480,40 @@ def randomexcursionstest(binin): cumsum = np.append(cumsum, 0) cumsum = np.append(0, cumsum) posi = np.where(cumsum == 0)[0] - cycles = ([cumsum[posi[x]:posi[x+1]+1] for x in range(len(posi)-1)]) + cycles = [cumsum[posi[x] : posi[x + 1] + 1] for x in range(len(posi) - 1)] j = len(cycles) sct = [] for ii in cycles: sct.append(([len(np.where(ii == xx)[0]) for xx in xvals])) sct = np.transpose(np.clip(sct, 0, 5)) - su = [] + sut = [] for ii in range(6): - su.append([(xx == ii).sum() for xx in sct]) - su = np.transpose(su) - pikt = ([([pik(uu, xx) for uu in range(6)]) for xx in xvals]) - # chitab=1.0*((su-j*pikt)**2)/(j*pikt) - chitab = np.sum(1.0*(np.array(su)-j*np.array(pikt)) - ** 2/(j*np.array(pikt)), axis=1) - pval = ([spc.gammaincc(2.5, cs/2.0) for cs in chitab]) + sut.append([(xx == ii).sum() for xx in sct]) + sut = np.transpose(sut) + pikt = [([pik(uu, xx) for uu in range(6)]) for xx in xvals] + chitab = np.sum( + 1.0 * (np.array(sut) - j * np.array(pikt)) ** 2 / (j * np.array(pikt)), axis=1 + ) + pval = [spc.gammaincc(2.5, cs / 2.0) for cs in chitab] return pval def getfreq(linn, nu): + """getfreq""" val = 0 - for (x, y) in linn: + for x, y in linn: if x == nu: val = y return val def randomexcursionsvarianttest(binin): - ''' The focus of this test is the number of times that a particular state occurs in a cumulative sum random walk. The purpose of this test is to detect deviations from the expected number of occurrences of various states in the random walk.''' + """ + The focus of this test is the number of times that a particular + state occurs in a cumulative sum random walk. The purpose of + this test is to detect deviations from the expected number of + occurrences of various states in the random walk. + """ ss = [int(el) for el in binin] sc = list(map(sumi, ss)) cs = np.cumsum(sc) @@ -397,18 +525,26 @@ def randomexcursionsvarianttest(binin): pval = [] for xs in range(-9, 9 + 1): if not xs == 0: - # pval.append([xs, spc.erfc(np.abs(getfreq(li, xs) - j) / np.sqrt(2 * j * (4 * np.abs(xs) - 2)))]) - pval.append(spc.erfc(np.abs(getfreq(li, xs) - j) / - np.sqrt(2 * j * (4 * np.abs(xs) - 2)))) + pval.append( + spc.erfc( + np.abs(getfreq(li, xs) - j) / np.sqrt(2 * j * (4 * np.abs(xs) - 2)) + ) + ) return pval def aproximateentropytest(binin, m=5): - ''' The focus of this test is the frequency of each and every overlapping m-bit pattern. The purpose of the test is to compare the frequency of overlapping blocks of two consecutive/adjacent lengths (m and m+1) against the expected result for a random sequence.''' + """ + The focus of this test is the frequency of each and every + overlapping m-bit pattern. The purpose of the test is + to compare the frequency of overlapping blocks of + two consecutive/adjacent lengths (m and m+1) against + the expected result for a random sequence. + """ n = len(binin) - f1a = [(binin + binin[0:m - 1:])[xs:m + xs:] for xs in range(n)] + f1a = [(binin + binin[0 : m - 1 :])[xs : m + xs :] for xs in range(n)] f1 = [[xs, f1a.count(xs)] for xs in sorted(set(f1a))] - f2a = [(binin + binin[0:m:])[xs:m + 1 + xs:] for xs in range(n)] + f2a = [(binin + binin[0:m:])[xs : m + 1 + xs :] for xs in range(n)] f2 = [[xs, f2a.count(xs)] for xs in sorted(set(f2a))] c1 = [1.0 * f1[xs][1] / n for xs in range(len(f1))] c2 = [1.0 * f2[xs][1] / n for xs in range(len(f2))] @@ -420,61 +556,75 @@ def aproximateentropytest(binin, m=5): return pval -def matrank(mat): # old function, does not work as advertized - gives the matrix rank, but not binary - u, s, v = np.linalg.svd(mat) +def matrank( + mat, +): # old function, does not work as advertized - gives the matrix rank, but not binary + """matrank""" + _, s, _ = np.linalg.svd(mat) rank = np.sum(s > 1e-10) return rank def mrank(matrix): # matrix rank as defined in the NIST specification + """mrank""" m = len(matrix) leni = len(matrix[0]) def proc(mat): for i in range(m): if mat[i][i] == 0: - for j in range(i+1, m): + for j in range(i + 1, m): if mat[j][i] == 1: mat[j], mat[i] = mat[i], mat[j] break + if mat[i][i] == 1: - for j in range(i+1, m): + for j in range(i + 1, m): if mat[j][i] == 1: mat[j] = [mat[i][x] ^ mat[j][x] for x in range(leni)] return mat + maa = proc(matrix) maa.reverse() mu = [i[::-1] for i in maa] - muu = proc(mu) ra = np.sum(np.sign([xx.sum() for xx in np.array(mu)])) return ra def binarymatrixranktest(binin, m=32, q=32): - ''' The focus of the test is the rank of disjoint sub-matrices of the entire sequence. The purpose of this test is to check for linear dependence among fixed length substrings of the original sequence.''' + """ + The focus of the test is the rank of disjoint sub-matrices + of the entire sequence. The purpose of this test is to + check for linear dependence among fixed length + substrings of the original sequence. + """ p1 = 1.0 for x in range(1, 50): - p1 *= 1-(1.0/(2**x)) - p2 = 2*p1 - p3 = 1-p1-p2 + p1 *= 1 - (1.0 / (2**x)) + + p2 = 2 * p1 + p3 = 1 - p1 - p2 n = len(binin) # the input string as numbers, to generate the dot product u = [int(el) for el in binin] - f1a = [u[xs*m:xs*m+m:] for xs in range(floor(n/m))] + f1a = [u[xs * m : xs * m + m :] for xs in range(floor(n / m))] n = len(f1a) - f2a = [f1a[xs*q:xs*q+q:] for xs in range(floor(n/q))] - # r=map(matrank,f2a) + f2a = [f1a[xs * q : xs * q + q :] for xs in range(floor(n / q))] r = list(map(mrank, f2a)) n = len(r) fm = r.count(m) - fm1 = r.count(m-1) - chisqr = ((fm-p1*n)**2)/(p1*n)+((fm1-p2*n)**2) / \ - (p2*n)+((n-fm-fm1-p3*n)**2)/(p3*n) - pval = np.exp(-0.5*chisqr) + fm1 = r.count(m - 1) + chisqr = ( + ((fm - p1 * n) ** 2) / (p1 * n) + + ((fm1 - p2 * n) ** 2) / (p2 * n) + + ((n - fm - fm1 - p3 * n) ** 2) / (p3 * n) + ) + pval = np.exp(-0.5 * chisqr) return pval def lincomplex(binin): + """lincomplex""" lenn = len(binin) c = b = np.zeros(lenn) c[0] = b[0] = 1 @@ -485,128 +635,159 @@ def lincomplex(binin): u = [int(el) for el in binin] p = 99 while n < lenn: - v = u[(n-l):n] # was n-l..n-1 + v = u[(n - l) : n] # was n-l..n-1 v.reverse() - cc = c[1:l+1] # was 2..l+1 - d = (u[n]+np.dot(v, cc)) % 2 + cc = c[1 : l + 1] # was 2..l+1 + d = (u[n] + np.dot(v, cc)) % 2 if d == 1: tmp = c p = np.zeros(lenn) for i in range(0, l): # was 1..l+1 if b[i] == 1: - p[i+n-m] = 1 - c = (c+p) % 2 - if l <= 0.5*n: # was if 2l <= n - l = n+1-l + p[i + n - m] = 1 + c = (c + p) % 2 + if l <= 0.5 * n: # was if 2l <= n + l = n + 1 - l m = n b = tmp n += 1 return l + # test 2.10 def linearcomplexitytest(binin, m=500): - ''' The focus of this test is the length of a generating feedback register. The purpose of this test is to determine whether or not the sequence is complex enough to be considered random. Random sequences are characterized by a longer feedback register. A short feedback register implies non-randomness.''' + """ + The focus of this test is the length of a generating feedback + register. The purpose of this test is to determine whether + or not the sequence is complex enough to be considered random. + Random sequences are characterized by a longer feedback register. + A short feedback register implies non-randomness. + """ k = 6 pi = [0.01047, 0.03125, 0.125, 0.5, 0.25, 0.0625, 0.020833] - avg = 0.5*m + (1.0/36)*(9 + (-1)**(m + 1)) - (m/3.0 + 2.0/9)/2**m + avg = 0.5 * m + (1.0 / 36) * (9 + (-1) ** (m + 1)) - (m / 3.0 + 2.0 / 9) / 2**m blocks = stringpart(binin, m) bign = len(blocks) - lc = ([lincomplex(chunk) for chunk in blocks]) - t = ([-1.0*(((-1)**m)*(chunk-avg)+2.0/9) for chunk in lc]) - vg = np.histogram(t, bins=[-9999999999, -2.5, - - 1.5, -0.5, 0.5, 1.5, 2.5, 9999999999])[0][::-1] - im = ([((vg[ii]-bign*pi[ii])**2)/(bign*pi[ii]) for ii in range(7)]) + lc = [lincomplex(chunk) for chunk in blocks] + t = [-1.0 * (((-1) ** m) * (chunk - avg) + 2.0 / 9) for chunk in lc] + vg = np.histogram( + t, bins=[-9999999999, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 9999999999] + )[0][::-1] + im = [((vg[ii] - bign * pi[ii]) ** 2) / (bign * pi[ii]) for ii in range(7)] chisqr = reduce(su, im) - pval = spc.gammaincc(k/2.0, chisqr/2.0) + pval = spc.gammaincc(k / 2.0, chisqr / 2.0) return pval def isRandom(bits): + """isRandom""" result = {} def adder(name, p): - if 'list' in str(type(p)): + if "list" in str(type(p)): count = 0 for i in p: - if 'nan' in str(i): + if "nan" in str(i): pass - elif 'e' in str(i): + elif "e" in str(i): pass elif i > 0.01: pass else: count += 1 - if count >= (len(p)/2): + if count >= (len(p) / 2): result[name] = False else: result[name] = True - elif 'e' in str(p): + elif "e" in str(p): pass elif p > 0.01: result[name] = True else: result[name] = False + try: - adder('Monobit frequency test', monobitfrequencytest(bits[:100])) + adder("Monobit frequency test", monobitfrequencytest(bits[:100])) except: pass + try: - adder('Block frequency test', blockfrequencytest(bits[:2000])) + adder("Block frequency test", blockfrequencytest(bits[:2000])) except: pass + try: - adder('Runs test', runstest(bits)) + adder("Runs test", runstest(bits)) except: pass + try: - adder('Spectral test', spectraltest(bits[:1024])) + adder("Spectral test", spectraltest(bits[:1024])) except: pass + try: - adder('Non-overlapping template matching test', - nonoverlappingtemplatematchingtest(bits[:1048576], '11111', 8)) + adder( + "Non-overlapping template matching test", + nonoverlappingtemplatematchingtest(bits[:1048576], "11111", 8), + ) except: pass + try: - adder('Overlapping template matching test', - overlappingtemplatematchingtest(bits[:998976], '0000001', 12, 5)) + adder( + "Overlapping template matching test", + overlappingtemplatematchingtest(bits[:998976], "0000001", 12, 5), + ) except: pass + try: - adder('Serial test', serialtest(bits[:500])) + adder("Serial test", serialtest(bits[:500])) except: pass + try: - adder('Cumultative sums test', cumultativesumstest(bits[:100])) + adder("Cumultative sums test", cumultativesumstest(bits[:100])) except: pass + try: - adder('Aproximate entropy test', aproximateentropytest(bits[:500], 5)) + adder("Aproximate entropy test", aproximateentropytest(bits[:500], 5)) except: pass + try: - adder('Random excursions variant test', - randomexcursionsvarianttest(bits[:1000000])) + adder( + "Random excursions variant test", + randomexcursionsvarianttest(bits[:1000000]), + ) except: pass + try: - adder('Linear complexity test', - linearcomplexitytest(bits[:1000000], 10)) + adder("Linear complexity test", linearcomplexitytest(bits[:1000000], 10)) except: pass + try: - adder('Longest runs test', longestrunones10000(bits)) + adder("Longest runs test", longestrunones10000(bits)) except: pass + try: - adder('Maurers universal statistic test', - maurersuniversalstatistictest(bits[:387840], 6, 640)) + adder( + "Maurers universal statistic test", + maurersuniversalstatistictest(bits[:387840], 6, 640), + ) except: pass + try: - adder('Random excursions test', randomexcursionstest(bits[:1000000])) + adder("Random excursions test", randomexcursionstest(bits[:1000000])) except: pass + return result diff --git a/core/evaluate.py b/core/evaluate.py index e5fb481..a692e52 100644 --- a/core/evaluate.py +++ b/core/evaluate.py @@ -1,31 +1,35 @@ +"""evaluate""" from re import match from core.utils import strength from core.config import commonNames def evaluate(dataset, weakTokens, tokenDatabase, allTokens, insecureForms): + """evaluate""" done = [] for i in dataset: for url, page in i.items(): localTokens = set() for each in page.values(): protected = False - action = each['action'] - method = each['method'] - inputs = each['inputs'] + action = each["action"] + inputs = each["inputs"] for inp in inputs: - name = inp['name'] - value = inp['value'] - if value and match(r'^[\w\-_]+$', value): + name = inp["name"] + value = inp["value"] + if value and match(r"^[\w\-_]+$", value): if strength(value) > 10: localTokens.add(value) protected = True break elif name.lower() in commonNames: weakTokens.append({url: {name: value}}) + if not protected and action not in done: done.append(done) insecureForms.append({url: each}) + for token in localTokens: allTokens.append(token) + tokenDatabase.append({url: localTokens}) diff --git a/core/photon.py b/core/photon.py index abc4e44..5d7d47e 100644 --- a/core/photon.py +++ b/core/photon.py @@ -1,4 +1,4 @@ -# Let's import what we need +"""photon""" from re import findall import concurrent.futures from urllib.parse import urlparse # for python3 @@ -15,48 +15,50 @@ def photon(seedUrl, headers, depth, threadCount): storage = set() # urls that belong to the target i.e. in-scope scheme = urlparse(seedUrl).scheme host = urlparse(seedUrl).netloc - main_url = scheme + '://' + host + main_url = scheme + "://" + host storage.add(seedUrl) def rec(url): processed.add(url) - urlPrint = (url + (' ' * 60))[:60] - print ('%s Parsing %-40s' % (run, urlPrint), end='\r') - url = getUrl(url, '', True) - params = getParams(url, '', True) - if '=' in url: + urlPrint = (url + (" " * 60))[:60] + print("%s Parsing %-40s" % (run, urlPrint), end="\r") + url = getUrl(url, "", True) + params = getParams(url, "", True) + if "=" in url: inps = [] for name, value in params.items(): - inps.append({'name': name, 'value': value}) - forms.append( - {url: {0: {'action': url, 'method': 'get', 'inputs': inps}}}) + inps.append({"name": name, "value": value}) + forms.append({url: {0: {"action": url, "method": "get", "inputs": inps}}}) + response = requester(url, params, headers, True, 0).text forms.append({url: zetanize(url, response)}) - matches = findall( - r'<[aA][^>]*?(href|HREF)=["\']{0,1}(.*?)["\']', response) + matches = findall(r'<[aA][^>]*?(href|HREF)=["\']{0,1}(.*?)["\']', response) + for link in matches: # iterate over the matches # remove everything after a "#" to deal with in-page anchors - link = link[1].split('#')[0].lstrip(' ') - if link[:4] == 'http': + link = link[1].split("#")[0].lstrip(" ") + if link[:4] == "http": if link.startswith(main_url): storage.add(link) - elif link[:2] == '//': - if link.split('/')[2].startswith(host): - storage.add(scheme + '://' + link) - elif link[:1] == '/': + elif link[:2] == "//": + if link.split("/")[2].startswith(host): + storage.add(scheme + "://" + link) + elif link[:1] == "/": storage.add(remove_file(url) + link) else: usable_url = remove_file(url) - if usable_url.endswith('/'): + if usable_url.endswith("/"): storage.add(usable_url + link) - elif link.startswith('/'): + elif link.startswith("/"): storage.add(usable_url + link) else: - storage.add(usable_url + '/' + link) - for x in range(depth): + storage.add(usable_url + "/" + link) + + for _ in range(depth): urls = storage - processed threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=10) futures = (threadpool.submit(rec, url) for url in urls) - for i in concurrent.futures.as_completed(futures): + for _ in concurrent.futures.as_completed(futures): pass + return [forms, len(processed)] diff --git a/core/prompt.py b/core/prompt.py index 84c3b09..2ab699d 100644 --- a/core/prompt.py +++ b/core/prompt.py @@ -1,10 +1,11 @@ +"""prompt""" import os import tempfile def prompt(default=None): - editor = 'nano' - with tempfile.NamedTemporaryFile(mode='r+') as tmpfile: + editor = "nano" + with tempfile.NamedTemporaryFile(mode="r+") as tmpfile: if default: tmpfile.write(default) tmpfile.flush() diff --git a/core/ranger.py b/core/ranger.py index 5175d05..1b3d973 100644 --- a/core/ranger.py +++ b/core/ranger.py @@ -1,10 +1,14 @@ +"""ranger""" + + def ranger(tokens): + """ranger""" digits = set() alphabets = set() for token in tokens: for char in token: - if char in '0123456789': + if char in "0123456789": digits.add(char) - elif char in 'abcdefghijklmnopqrstuvwxyz': + elif char in "abcdefghijklmnopqrstuvwxyz": alphabets.add(char) return [list(digits), list(alphabets)] diff --git a/core/requester.py b/core/requester.py index 78a825f..1a5e9ab 100644 --- a/core/requester.py +++ b/core/requester.py @@ -1,22 +1,35 @@ +"""requester""" import time import random import warnings import requests -warnings.filterwarnings('ignore') # Disable SSL related warnings +warnings.filterwarnings("ignore") # Disable SSL related warnings def requester(url, data, headers, GET, delay): time.sleep(delay) - user_agents = ['Mozilla/5.0 (X11; Linux i686; rv:60.0) Gecko/20100101 Firefox/60.0', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36' - 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991'] + user_agents = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.3", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.1", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.", + "Mozilla/5.0 (X11; Linux i686; rv:127.0) Gecko/20100101 Firefox/127.0", + "Mozilla/5.0 (X11; U; Linux 2.4.2-2 i586; en-US; m18) Gecko/20010131 Netscape6/6.01", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/25.0 Chrome/121.0.0.0 Safari/537.3", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19", + ] + if headers: - if 'User-Agent' not in headers: - headers['User-Agent'] = random.choice(user_agents) + if "User-Agent" not in headers: + headers["User-Agent"] = random.choice(user_agents) + if GET: response = requests.get( - url, params=data, headers=headers, verify=False) + url, params=data, headers=headers, verify=False, timeout=1 + ) else: - response = requests.post(url, data=data, headers=headers, verify=False) + response = requests.post( + url, data=data, headers=headers, verify=False, timeout=1 + ) + return response diff --git a/core/tweaker.py b/core/tweaker.py index 0b29a2a..0e20158 100644 --- a/core/tweaker.py +++ b/core/tweaker.py @@ -1,33 +1,41 @@ -from core.config import tokenPattern +"""tweaker""" import random import re +from core.config import tokenPattern def tweaker(data, strategy, index=0, seeds=[None, None]): + """tweaker""" digits = seeds[0] alphabets = seeds[1] newData = {} - if strategy == 'clear': + + if strategy == "clear": for name, value in data.items(): if re.match(tokenPattern, value): - value = '' + value = "" newData[name] = value return newData - elif strategy == 'remove': + + if strategy == "remove": for name, value in data.items(): if not re.match(tokenPattern, value): newData[name] = value - elif strategy == 'break': + return newData + + if strategy == "break": for name, value in data.items(): if re.match(tokenPattern, value): value = value[:index] - for i in index: + for _ in index: value += random.choice(digits + alphabets) newData[name] = value - elif strategy == 'generate': + return newData + + if strategy == "generate": for name, value in data.items(): if re.match(tokenPattern, value): - newToken = '' + newToken = "" for char in list(value): if char in digits: newToken += random.choice(digits) @@ -38,8 +46,11 @@ def tweaker(data, strategy, index=0, seeds=[None, None]): newData[name] = newToken else: newData[name] = value - elif strategy == 'replace': + return newData + + if strategy == "replace": for name, value in data.items(): if re.match(tokenPattern, value): value + return newData diff --git a/core/utils.py b/core/utils.py index 28eb82e..e235940 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,8 +1,10 @@ +"""utils""" import re from core.config import tokenPattern def longestCommonSubstring(s1, s2): + """longestCommonSubstring""" m = [[0] * (1 + len(s2)) for i in range(1 + len(s1))] longest, x_longest = 0, 0 for x in range(1, 1 + len(s1)): @@ -14,85 +16,98 @@ def longestCommonSubstring(s1, s2): x_longest = x else: m[x][y] = 0 - return s1[x_longest - longest: x_longest] + return s1[x_longest - longest : x_longest] def stringToBinary(string): - return ''.join(format(ord(x), 'b') for x in string) + """stringToBinary""" + return "".join(format(ord(x), "b") for x in string) def strength(string): - digits = re.findall(r'\d', string) - lowerAlphas = re.findall(r'[a-z]', string) - upperAlphas = re.findall(r'[A-Z]', string) + """strength""" + digits = re.findall(r"\d", string) + lowerAlphas = re.findall(r"[a-z]", string) + upperAlphas = re.findall(r"[A-Z]", string) entropy = len(set(digits + lowerAlphas + upperAlphas)) if not digits: - entropy = entropy/2 + entropy = entropy / 2 + return entropy def isProtected(parsed): + """isProtected""" protected = False parsedForms = list(parsed.values()) + for oneForm in parsedForms: - inputs = oneForm['inputs'] + inputs = oneForm["inputs"] for inp in inputs: - name = inp['name'] - kind = inp['type'] - value = inp['value'] + value = inp["value"] if re.match(tokenPattern, value): protected = True + return protected def extractHeaders(headers): - headers = headers.replace('\\n', '\n') + """extractHeaders""" + headers = headers.replace("\\n", "\n") sorted_headers = {} - matches = re.findall(r'(.*):\s(.*)', headers) + matches = re.findall(r"(.*):\s(.*)", headers) + for match in matches: header = match[0] value = match[1] try: - if value[-1] == ',': + if value[-1] == ",": value = value[:-1] sorted_headers[header] = value except IndexError: pass + return sorted_headers def getUrl(url, data, GET): + """getUrl""" if GET: - return url.split('?')[0] - else: - return url + return url.split("?")[0] + + return url def getParams(url, data, GET): + """getParams""" params = {} if GET: - if '=' in url: - data = url.split('?')[1] - if data[:1] == '?': + if "=" in url: + data = url.split("?")[1] + if data[:1] == "?": data = data[1:] else: - data = '' - parts = data.split('&') + data = "" + + parts = data.split("&") + for part in parts: - each = part.split('=') + each = part.split("=") try: params[each[0]] = each[1] except IndexError: params = None + return params def remove_file(url): - if url.count('/') > 2: - replacable = re.search(r'/[^/]*?$', url).group() - if replacable != '/': - return url.replace(replacable, '') + """remove_file""" + if url.count("/") > 2: + replacable = re.search(r"/[^/]*?$", url).group() + if replacable != "/": + return url.replace(replacable, "") else: return url - else: - return url + + return url diff --git a/core/zetanize.py b/core/zetanize.py index 5b1e1cc..692120f 100644 --- a/core/zetanize.py +++ b/core/zetanize.py @@ -1,51 +1,52 @@ +"""zetanize""" import re from urllib.parse import urlparse def zetanize(url, response): + """zetanize""" parsedUrl = urlparse(url) - mainUrl = parsedUrl.scheme + '://' + parsedUrl.netloc + mainUrl = parsedUrl.scheme + "://" + parsedUrl.netloc def e(string): - return string.encode('utf-8') + return string.encode("utf-8") def d(string): - return string.decode('utf-8') + return string.decode("utf-8") - response = re.sub(r'(?s)', '', response) + response = re.sub(r"(?s)", "", response) forms = {} - matches = re.findall(r'(?i)(?s)', response) + matches = re.findall(r"(?i)(?s)", response) num = 0 + for match in matches: page = re.search(r'(?i)action=[\'"](.*?)[\'"]', match) method = re.search(r'(?i)method=[\'"](.*?)[\'"]', match) forms[num] = {} action = d(e(page.group(1))) - if not action.startswith('http'): - if action.startswith('/'): + if not action.startswith("http"): + if action.startswith("/"): action = mainUrl + action else: - action = mainUrl + '/' + action - forms[num]['action'] = action.replace('&', '&') if page else '' - forms[num]['method'] = d( - e(method.group(1)).lower()) if method else 'get' - forms[num]['inputs'] = [] - inputs = re.findall(r'(?i)(?s)', response) + action = mainUrl + "/" + action + + forms[num]["action"] = action.replace("&", "&") if page else "" + forms[num]["method"] = d(e(method.group(1)).lower()) if method else "get" + forms[num]["inputs"] = [] + inputs = re.findall(r"(?i)(?s)", response) + for inp in inputs: inpName = re.search(r'(?i)name=[\'"](.*?)[\'"]', inp) - if inpName: + if inpName is not None: inpType = re.search(r'(?i)type=[\'"](.*?)[\'"]', inp) inpValue = re.search(r'(?i)value=[\'"](.*?)[\'"]', inp) inpName = d(e(inpName.group(1))) - inpType = d(e(inpType.group(1)))if inpType else '' - inpValue = d(e(inpValue.group(1))) if inpValue else '' - if inpType.lower() == 'submit' and inpValue == '': - inpValue = 'Submit Query' - inpDict = { - 'name': inpName, - 'type': inpType, - 'value': inpValue - } - forms[num]['inputs'].append(inpDict) + inpType = d(e(inpType.group(1))) if inpType else "" + inpValue = d(e(inpValue.group(1))) if inpValue else "" + if inpType.lower() == "submit" and inpValue == "": + inpValue = "Submit Query" + inpDict = {"name": inpName, "type": inpType, "value": inpValue} + forms[num]["inputs"].append(inpDict) num += 1 + return forms diff --git a/requirements.txt b/requirements.txt index 5c18100..60535e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -numpy -scipy -requests fuzzywuzzy +numpy python-Levenshtein +requests +scipy