class JobsForm(FlaskForm):
    name = StringField('Job Name', validators=[DataRequired()])
    customer_id = StringField('Customer ID (unused)', validators=[DataRequired()])
    customer_name = StringField('Customer Name (unused)')
    submit = SubmitField('Next')

    def validate_job(self, name):
        job = Jobs.query.filter_by(name = name.data).first()
        if job:
            raise ValidationError('That job name is taken. Please choose a different one.') class JobsNewHashFileForm(FlaskForm):
    name = StringField('Hashfile Name') # While required we may dynamically create this based on file upload
    file_type = SelectField('Hash Format', choices=[('', '--SELECT--'), 
                                                    ('hash_only', '$hash'), 
                                                    ('user_hash', '$user:$hash'), 
                                                    ('shadow', 'Linux/Unix Shadow File'),
                                                    ('pwdump', 'pwdump()'), 
                                                    ('NetNTLM', 'NetNTLMv1, NetNTLMv1+ESS or NetNTLMv2'), 
                                                    ('kerberos', 'Kerberos')], validators=[DataRequired()])
    hash_type = SelectField('Hash Type', choices=[  ('', '--SELECT--'),
                                                    ('0', '(0) MD5'),
                                                    ('300', '(300) MySQL4.1/MySQL5'),
                                                    ('500', '(500) md5crypt, MD5 (Unix), Cisco-IOS'),
                                                    ('1000', '(1000) NTLM '),
                                                    ('1800', '(1800) sha512crypt (Unix)'),
                                                    ('2100', '(2100) DCC2, MS Cache2'),
                                                    ('3200', '(3200) bcrypt $2*$, Blowfish (unix)'),
                                                    ('5500', '(5500) NetNTLMv1 / NetNTLMv1+ESS'),
                                                    ('5600', '(5600) NetNTLMv2'),
                                                    ('7500', '(7500) Kerberos 5 AS-REQ Pre-Auth etype 23'),
                                                    ('13100', '(13100) Kerberos 5 TGS-REP etype 23'),
                                                    ('18200', '(18200) Kerberos 5 AS-REP etype 23'),
                                                    ('19600', '(19600) Kerberos 5 TGS-REP etype 17 (AES128-CTS-HMAC-SHA1-96)'),
                                                    ('19700', '(19700) Kerberos 5 TGS-REP etype 18 (AES256-CTS-HMAC-SHA1-96)'),
                                                    ('19800', '(19800) Kerberos 5, etype 17, Pre-Auth'),
                                                    ('19900', '(19900) Kerberos 5, etype 18, Pre-Auth')], validators=[DataRequired()])
    hashfilehashes = TextAreaField('Hashes')
    hashfile = FileField('Upload Hashfile')
    submit = SubmitField('Next') class JobsNotificationsForm(FlaskForm):
    job_completion = SelectField('Notify when Job completes', choices=[('none', 'No'),
                                                                        ('email', 'Send Email'),
                                                                        ('push', 'Send Push Notification')], validators=[DataRequired()])
    hash_completion = SelectField('Notify when specific hashes crack', choices=[('none', 'No'),
                                                                        ('email', 'Send Email'),
                                                                        ('push', 'Send Push Notification')], validators=[DataRequired()])
    submit = SubmitField('Next') class JobSummaryForm(FlaskForm):
    submit = SubmitField('Complete') class HashNotifications(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    owner_id = db.Column(db.Integer, nullable=False)
    hash_id = db.Column(db.Integer, nullable=False)
    method = db.Column(db.String(6), nullable=False)    # email, push class JobNotifications(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    owner_id = db.Column(db.Integer, nullable=False)
    job_id = db.Column(db.Integer, nullable=False)
    method = db.Column(db.String(6), nullable=False)    # email, push class Jobs(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    status = db.Column(db.String(20), nullable=False)           # Running, Paused, Completed, Queued, Canceled, Ready, Incomplete
    started_at = db.Column(db.DateTime, nullable=True)          # These defaults should be changed
    ended_at = db.Column(db.DateTime, nullable=True)            # These defaults should be changed
    hashfile_id = db.Column(db.Integer, nullable=True)
    customer_id = db.Column(db.Integer, db.ForeignKey('customers.id'), nullable=False)
    owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) class Customers(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(40), nullable=False) class Hashfiles(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(256), nullable=False)        # can probably be reduced
    uploaded_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    runtime = db.Column(db.Integer, default=0)
    customer_id = db.Column(db.Integer, nullable=False)
    owner_id = db.Column(db.Integer, nullable=False) class Users(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(20), nullable=False)
    last_name = db.Column(db.String(20), nullable=False)
    email_address = db.Column(db.String(50), unique=True, nullable=False)
    password = db.Column(db.String(60), nullable=False)
    admin = db.Column(db.Boolean, nullable=False, default=False)
    pushover_app_id = db.Column(db.String(50), nullable=True)
    pushover_user_key = db.Column(db.String(50), nullable=True)
    wordlists = db.relationship('Wordlists', backref='tbd', lazy=True)
    rules = db.relationship('Rules', backref='owner', lazy=True)
    jobs = db.relationship('Jobs', backref='owner', lazy=True)
    tasks = db.relationship('Tasks', backref='owner', lazy=True)
    taskgroups = db.relationship('TaskGroups', backref='owner', lazy=True)

    def get_reset_token(self, expires_sec=1800):
        s = Serializer(Config.SECRET_KEY, expires_sec)
        return s.dumps({'user_id': self.id}).decode('utf-8')

    @staticmethod
    def verify_reset_token(token):
        s = Serializer(Config.SECRET_KEY)
        try:
            user_id = s.loads(token)['user_id']
        except:
            return None     
        return Users.query.get(user_id) class HashfileHashes(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    hash_id = db.Column(db.Integer, nullable=False, index=True)
    username = db.Column(db.String(256), nullable=True, default=None, index=True)
    hashfile_id = db.Column(db.Integer, nullable=False) class Hashes(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    sub_ciphertext = db.Column(db.String(32), nullable=False, index=True)
    ciphertext = db.Column(db.String(16383), nullable=False) # Setting this to max value for now. If we run into this being a limitation in the future we can revisit changing thist to TEXT or BLOB. https://sheeri.org/max-varchar-size/
    hash_type = db.Column(db.Integer, nullable=False, index=True)
    cracked = db.Column(db.Boolean, nullable=False)
    plaintext = db.Column(db.String(256), index=True) class JobTasks(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    job_id = db.Column(db.Integer, nullable=False)
    task_id = db.Column(db.Integer, nullable=False)
    command = db.Column(db.String(1024))                  
    status = db.Column(db.String(50), nullable=False)       # Running, Paused, Not Started, Completed, Queued, Canceled, Importing
    agent_id = db.Column(db.Integer, db.ForeignKey('agents.id')) class Tasks(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    hc_attackmode = db.Column(db.String(25), nullable=False) # dictionary, mask, bruteforce, combinator
    owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    wl_id = db.Column(db.Integer)
    rule_id = db.Column(db.Integer)
    hc_mask = db.Column(db.String(50)) class TaskGroups(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    tasks = db.Column(db.String(256), nullable=False) def save_file(path, form_file):
    random_hex = secrets.token_hex(8)
    file_name = random_hex + os.path.split(form_file.filename)[0] + '.txt'
    file_path = os.path.join(current_app.root_path, path, file_name)
    form_file.save(file_path)
    return file_path def get_filehash(filepath):
    sha256_hash = hashlib.sha256()
    with open(filepath,"rb") as f:
        # Read and update hash string value in blocks of 4K
        for byte_block in iter(lambda: f.read(4096),b""):
            sha256_hash.update(byte_block)
    return sha256_hash.hexdigest() def import_hashfilehashes(hashfile_id, hashfile_path, file_type, hash_type):
    # Open file
    file = open(hashfile_path, 'r')
    lines = file.readlines()

    # for line in file, 
    for line in lines:
        # If line is empty:
        if len(line) > 0:
            if file_type == 'hash_only':
                hash_id = import_hash_only(line=line.rstrip(), hash_type=hash_type)
                if hash_type == '2100':
                    username = line.split('#')[1]
                else:
                    username = None
            elif file_type == 'user_hash':
                hash_id = import_hash_only(line=line.split(':',1)[1].rstrip(), hash_type=hash_type)
                username = line.split(':')[0]
            elif file_type == 'shadow':
                hash_id= import_hash_only(line=line.split(':')[1], hash_type=hash_type)
                username = line.split(':')[0]
            elif file_type == 'pwdump':
                # do we let user select LM so that we crack those instead of NTLM?
                # First extracting usernames so we can filter out machine accounts
                if re.search("\$$", line.split(':')[0]):
                #if '$' in line.split(':')[0]:
                    continue
                else:
                    hash_id = import_hash_only(line=line.split(':')[3].lower(), hash_type='1000')
                    username = line.split(':')[0]
            elif file_type == 'kerberos':
                hash_id = import_hash_only(line=line.lower().rstrip(), hash_type=hash_type)
                if hash_type == '18200':
                    username = line.split('$')[3].split(':')[0]
                else:
                    username = line.split('$')[3]
            elif file_type == 'NetNTLM':
                # First extracting usernames so we can filter out machine accounts
                # 5600, domain is case sensitve. Hashcat returns username in upper case.
                if re.search("\$$", line.split(':')[0]):
                #if '$' in line.split(':')[0]:
                    continue
                else:
                    # uppercase uesrname in line
                    line_list = line.split(':')
                    # uppercase the username in line
                    line_list[0] = line_list[0].upper()
                    # lowercase the rest (except domain name) 3,4,5
                    line_list[3] = line_list[3].lower()
                    line_list[4] = line_list[4].lower()
                    line_list[5] = line_list[5].lower()
                    line = ':'.join(line_list)
                    hash_id = import_hash_only(line=line.rstrip(), hash_type=hash_type)
                    username = line.split(':')[0] 
            else:
                return False
            if username == None:
                hashfilehashes = HashfileHashes(hash_id=hash_id, hashfile_id=hashfile_id)
            else:
                hashfilehashes = HashfileHashes(hash_id=hash_id, username=username.encode('latin-1').hex(), hashfile_id=hashfile_id)
            db.session.add(hashfilehashes)
            db.session.commit() 

    return True def build_hashcat_command(job_id, task_id):
    # this function builds the main hashcat cmd we use to crack.
    hc_binpath = '@HASHCATBINPATH@'
    task = Tasks.query.get(task_id)
    job = Jobs.query.get(job_id)
    rules_file = Rules.query.get(task.rule_id)
    hashfilehashes_single_entry = HashfileHashes.query.filter_by(hashfile_id = job.hashfile_id).first()
    hashes_single_entry = Hashes.query.get(hashfilehashes_single_entry.hash_id)
    hash_type = hashes_single_entry.hash_type
    attackmode = task.hc_attackmode
    mask = task.hc_mask

    if attackmode == 'combinator':
        print('unsupported combinator')
    else:
        wordlist = Wordlists.query.get(task.wl_id)

    target_file = 'control/hashes/hashfile_' + str(job.id) + '_' + str(task.id) + '.txt'
    crack_file = 'control/outfiles/hc_cracked_' + str(job.id) + '_' + str(task.id) + '.txt'
    if wordlist:
        relative_wordlist_path = 'control/wordlists/' + wordlist.path.split('/')[-1]
    else:
        relative_wordlist_path = ''
    if rules_file:
        relative_rules_path = 'control/rules/' + rules_file.path.split('/')[-1]
    else:
        relative_rules_path = ''

    session = secrets.token_hex(4)

    if attackmode == 'bruteforce':
        cmd = hc_binpath + ' -O -w 3 ' + ' --session ' + session + ' -m ' + str(hash_type) + ' --potfile-disable' + ' --status --status-timer=15' + ' --outfile-format 1,3' + ' --outfile ' + crack_file + ' ' + ' -a 3 ' + target_file
    elif attackmode == 'maskmode':
        cmd = hc_binpath + ' -O -w 3 ' + ' --session ' + session + ' -m ' + str(hash_type) + ' --potfile-disable' + ' --status --status-timer=15' + ' --outfile-format 1,3' + ' --outfile ' + crack_file + ' ' + ' -a 3 ' + target_file + ' ' + mask
    elif attackmode == 'dictionary':
        if isinstance(task.rule_id, int):
            cmd = hc_binpath + ' -O -w 3 ' + ' --session ' + session + ' -m ' + str(hash_type) + ' --potfile-disable' + ' --status --status-timer=15' + ' --outfile-format 1,3' + ' --outfile ' + crack_file + ' ' + ' -r ' + relative_rules_path + ' ' + target_file + ' ' + relative_wordlist_path
        else:
            cmd = hc_binpath + ' -O -w 3 ' + ' --session ' + session + ' -m ' + str(hash_type) + ' --potfile-disable' + ' --status --status-timer=15' + ' --outfile-format 1,3' + ' --outfile ' + crack_file + ' ' + target_file + ' ' + relative_wordlist_path
    elif attackmode == 'combinator':
      cmd = hc_binpath + ' -O -w 3 ' + ' --session ' + session + ' -m ' + str(hash_type) + ' --potfile-disable' + ' --status --status-timer=15' + ' --outfile-format 1,3' + ' --outfile ' + crack_file + ' ' + ' -a 1 ' + target_file + ' ' + wordlist_one.path + ' ' + ' ' + wordlist_two.path + ' ' + relative_rules_path

    return cmd def validate_hashfile(hashfile_path, file_type, hash_type):

    file = open(hashfile_path, 'r')
    lines = file.readlines()
    line_number = 0

    # Do a whole file check if file_type is NetNTLM
    # If duplicate usernames exists return error
    if file_type == 'NetNTLM':
        list_of_username_and_computers = []
        for line in lines:
            username_computer = (line.split(':')[0] + ':' + line.split(':')[2]).lower()
            if username_computer in list_of_username_and_computers:
                return 'Error: Duplicate usernames / computer found in hashfiles (' + str(username_computer) + '). Please only submit unique usernames / computer.'
            else:
                list_of_username_and_computers.append(username_computer)

    line_number = 0
    # for line in file, 
    for line in lines:
        line_number += 1
        # TODO
        # Skip entries that are just newlines
        if len(line) > 50000:
            return 'Error line ' + str(line_number) + ' is too long. Line length: ' + str(len(line)) + '. Max length is 50,000 chars.'
        if len(line) > 0:

            # Check file types & hash types
            if file_type == 'hash_only':
                if ':' in line:
                    return 'Error line ' + str(line_number) + ' contains a : character. File should be hashes only. No usernames'
                if hash_type == '0' or hash_type == '1000':
                    if len(line.rstrip()) != 32:
                        return 'Error line ' + str(line_number) + ' has an invalid number of characters (' + str(len(line.rstrip())) + ') should be 32'
                if hash_type == '300':
                    if len(line.rstrip()) != 40:
                        return 'Error line ' + str(line_number) + ' has an invalid number of characters (' + str(len(line.rstrip())) + ') should be 40'
                if hash_type == '500':
                    if '$1$' not in line:
                        return 'Error line ' + str(line_number) + ' appears to not be a valid md5Crypt hash'
                if hash_type == '2100':
                    if '$' not in line:
                        return 'Error line ' + str(line_number) + ' is missing a $ character. DCC2 Hashes should have these.'
                    dollar_cnt = 0
                    hash_cnt = 0
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                        if char == '#':
                            hash_cnt += 1
                    if dollar_cnt != 2:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: DCC2 MS Cache'
                    if hash_cnt != 2:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: DCC2 MS Cache'
                if hash_type == '1800':
                    dollar_cnt = 0
                    for char in line:
                        if char == '$':
                            dollar_cnt+=1
                    if dollar_cnt != 3:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Sha512 Crypt.'
                    if '$6$' not in line:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Sha512 Crypt.'                        
                if hash_type == '3200':
                    if '$' not in line:
                        return 'Error line ' + str(line_number) + ' is missing a $ character. bcrypt Hashes should have these.'
                    dollar_cnt = 0
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 3:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: bcrypt'
                       
            if file_type == 'shadow':
                if ':' not in line:
                    return 'Error line ' + str(line_number) + ' is missing a : character. shadow file should include usernames.'
                if hash_type == '1800':
                    dollar_cnt = 0
                    for char in line:
                        if char == '$':
                            dollar_cnt+=1
                    if dollar_cnt != 3:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Sha512 Crypt from a shadow file.'
                    if '$6$' not in line:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Sha512 Crypt from a shadow file.'

            elif file_type == 'pwdump':
                if ':' not in line:
                    return 'Error line ' + str(line_number) + ' is missing a : character. Pwdump file should include usernames.'
                # This is slow af :(
                colon_cnt = 0
                for char in line:
                    if char == ':':
                        colon_cnt += 1
                if colon_cnt < 6:
                    return 'Error line ' + str(line_number) + '. File does not appear to be be in a pwdump format.' 
                if hash_type == '1000':
                    if len(line.split(':')[3]) != 32:
                        return 'Error line ' + str(line_number) + ' has an invalid number of characters (' + str(len(line.rstrip())) + ') should be 32'
                else:
                    return 'Sorry. The only Hash Type we support for PWDump files is NTLM'   
            elif file_type == 'kerberos':
                if '$' not in line:
                    return 'Error line ' + str(line_number) + ' is missing a $ character. kerberos file should include these.'  
                if len(line) > 16384:
                    return 'Error line ' + str(line_number) + ' is too long. Max char length is 16384. If you need long please submit an issue on GitHub'
                dollar_cnt = 0
                if hash_type == '7500':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 6:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, AS-REQ Pre-Auth (1)'
                    if line.split('$')[1] != 'krb5pa':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, AS-REQ Pre-Auth (2)'
                    if line.split('$')[2] != '23':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, AS-REQ Pre-Auth (3)'
                elif hash_type == '13100':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 7 and dollar_cnt != 8:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, TGS-REP (1)'
                    if line.split('$')[1] != 'krb5tgs':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, TGS-REP (2)'
                    if line.split('$')[2] != '23':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, TGS-REP (3)'                    
                elif hash_type == '18200':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 4 and dollar_cnt != 5:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, AS-REP (1)'
                    if line.split('$')[1] != 'krb5asrep':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, AS-REP (2)'
                    if line.split('$')[2] != '23':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 23, AS-REP (3)'                      
                elif hash_type == '19600':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 6 and dollar_cnt != 7:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 17, TGS-REP (AES128-CTS-HMAC-SHA1-96) (1)'
                    if line.split('$')[1] != 'krb5tgs':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 17, TGS-REP (AES128-CTS-HMAC-SHA1-96) (2)'
                    if line.split('$')[2] != '17':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 17, TGS-REP (AES128-CTS-HMAC-SHA1-96) (3)'                     
                elif hash_type == '19700':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 6 and dollar_cnt != 7:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 18, TGS-REP (AES256-CTS-HMAC-SHA1-96) (1)'
                    if line.split('$')[1] != 'krb5tgs':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 18, TGS-REP (AES256-CTS-HMAC-SHA1-96) (2)'
                    if line.split('$')[2] != '18':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 18, TGS-REP (AES256-CTS-HMAC-SHA1-96) (3)'      
                elif hash_type == '19800':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 5 and dollar_cnt != 6:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 17, Pre-Auth (1)'
                    if line.split('$')[1] != 'krb5pa':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 17, Pre-Auth (2)'
                    if line.split('$')[2] != '17':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 17, Pre-Auth (3)'  
                elif hash_type == '19900':
                    # This is slow af :(
                    for char in line:
                        if char == '$':
                            dollar_cnt += 1
                    if dollar_cnt != 5 and dollar_cnt != 6:
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 18, Pre-Auth (1)'
                    if line.split('$')[1] != 'krb5pa':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 18, Pre-Auth (2)'
                    if line.split('$')[2] != '18':
                        return 'Error line ' + str(line_number) + '. Doesnt appear to be of the type: Kerberos 5, etype 18, Pre-Auth (3)'  
                else:
                    return 'Sorry. The only suppported Hash Types are: 7500, 13100, 18200, 19600, 19700, 19800 and 19900.'

            elif file_type == 'NetNTLM':
                # Excellent oppertunity to unique sort usernames and return error for duplicates
                if ':' not in line:
                    return 'Error line ' + str(line_number) + ' is missing a : character. NetNTLM file should include usernames.'
                # This is slow af :(
                colon_cnt = 0
                for char in line:
                    if char == ':':
                        colon_cnt += 1
                if colon_cnt < 5:
                    return 'Error line ' + str(line_number) + '. File does not appear to be be in a NetNTLM format.'    

    return False def data_retention_cleanup():
from flask import Blueprint, render_template, redirect, abort, flash, url_for, current_app, request
from flask_login import login_required, current_user
from sqlalchemy.sql.elements import Null
from hashview.jobs.forms import JobsForm, JobsNewHashFileForm, JobsNotificationsForm, JobSummaryForm
from hashview.models import HashNotifications, JobNotifications, Jobs, Customers, Hashfiles, Users, HashfileHashes, Hashes, JobTasks, Tasks, TaskGroups
from hashview.utils.utils import save_file, get_filehash, import_hashfilehashes, build_hashcat_command, validate_hashfile
from hashview import db
import os
import time
import secrets
import json
            return redirect(str(hashfile.id))

        elif jobsNewHashFileForm.hashfilehashes.data:
            # User submitted copied/pasted hashes
            # Going to have to save a file manually instead of using save_file since save_file requires form data to be passed and we're not collecting that object for this tab

            if len(jobsNewHashFileForm.name.data) == 0:
                flash('You must assign a name to the hashfile', 'danger')
                return redirect(url_for('jobs.jobs_assigned_hashfile', job_id=job_id))

            random_hex = secrets.token_hex(8)
            hashfile_path = 'hashview/control/tmp/' + random_hex

            hashfilehashes_file = open(hashfile_path, 'w+') 

            hashfilehashes_file.write(jobsNewHashFileForm.hashfilehashes.data)
            hashfilehashes_file.close()

            has_problem = validate_hashfile(hashfile_path, jobsNewHashFileForm.file_type.data, jobsNewHashFileForm.hash_type.data)

            if has_problem:
                flash(has_problem, 'danger')
                return redirect(url_for('jobs.jobs_assigned_hashfile', job_id=job_id))
            else:
                hashfile = Hashfiles(name=jobsNewHashFileForm.name.data, customer_id=job.customer_id, owner_id=current_user.id)
                db.session.add(hashfile)
                db.session.commit()            


                if not import_hashfilehashes(   hashfile_id=hashfile.id, 
                                                hashfile_path=hashfile_path, 
                                                file_type=jobsNewHashFileForm.file_type.data, 
                                                hash_type=jobsNewHashFileForm.hash_type.data
                                                ):
                    return ('Something went wrong')

                job.hashfile_id = hashfile.id
                db.session.commit()

                return redirect(str(hashfile.id))
        
    elif request.method == 'POST' and request.form['hashfile_id']:
        job.hashfile_id = request.form['hashfile_id']
        db.session.commit()
        #return redirect("/jobs/" + str(job.id)+"/tasks")
        return redirect("/jobs/" + str(job.id)+"/notifications")

    else:
        for error in jobsNewHashFileForm.name.errors:
            print(str(error))
        for error in jobsNewHashFileForm.file_type.errors:        
            print(str(error))
        for error in jobsNewHashFileForm.hash_type.errors:        
            print(str(error))
        for error in jobsNewHashFileForm.hashfile.errors:        
            print(str(error))
        for error in jobsNewHashFileForm.hashfilehashes.errors:        
            print(str(error))
        for error in jobsNewHashFileForm.submit.errors:        
            print(str(error))
        
    return render_template('jobs_assigned_hashfiles.html', title='Jobs Assigned Hashfiles', hashfiles=hashfiles, job=job, jobsNewHashFileForm=jobsNewHashFileForm, hashfile_cracked_rate=hashfile_cracked_rate)

@jobs.route("/jobs/<int:job_id>/assigned_hashfile/<int:hashfile_id>", methods=['GET'])
@login_required
def jobs_assigned_hashfile_cracked(job_id, hashfile_id):
    job = Jobs.query.get(job_id)
    hashfile = Hashfiles.query.get(hashfile_id)
    # Can be optimized to only return the hash and plaintext
    cracked_hashfiles_hashes = db.session.query(Hashes, HashfileHashes).join(HashfileHashes, Hashes.id==HashfileHashes.hash_id).filter(Hashes.cracked == '1').filter(HashfileHashes.hashfile_id==hashfile.id).all()
    cracked_hashfiles_hashes_cnt = db.session.query(Hashes).join(HashfileHashes, Hashes.id == HashfileHashes.hash_id).filter(Hashes.cracked == '1').filter(HashfileHashes.hashfile_id==hashfile.id).count()
    if cracked_hashfiles_hashes_cnt > 0:
        flash(str(cracked_hashfiles_hashes_cnt) + " instacracked Hashes!", 'success')
    # Oppertunity for either a stored procedure or for some fancy queries.
 
    return render_template('jobs_assigned_hashfiles_cracked.html', title='Jobs Assigned Hashfiles Cracked', hashfile=hashfile, job=job, cracked_hashfiles_hashes=cracked_hashfiles_hashes)

@jobs.route("/jobs/<int:job_id>/tasks", methods=['GET'])
@login_required
def jobs_list_tasks(job_id):
    job = Jobs.query.get(job_id)
    tasks = Tasks.query.all()
    job_tasks = JobTasks.query.filter_by(job_id=job_id)
    task_groups = TaskGroups.query.all()
    # Right now we're doing nested loops in the template, this could probably be solved with a left/join select

    return render_template('jobs_assigned_tasks.html', title='Jobs Assigned Tasks', job=job, tasks=tasks, job_tasks=job_tasks, task_groups=task_groups)

@jobs.route("/jobs/<int:job_id>/assign_task/<int:task_id>", methods=['GET'])
@login_required
def jobs_assigned_task(job_id, task_id):
    
    exists = JobTasks.query.filter_by(job_id=job_id, task_id=task_id).first()
    if exists:
        flash('Task already assigned to the job.', 'warning')
    else:
        job_task = JobTasks(job_id=job_id, task_id=task_id, status='Not Started')
        db.session.add(job_task)
        db.session.commit()

    return redirect("/jobs/"+str(job_id)+"/tasks")

@jobs.route("/jobs/<int:job_id>/assign_task_group/<int:task_group_id>", methods=['GET'])
@login_required
def jobs_assign_task_group(job_id, task_group_id):
    job = Jobs.query.get(job_id)
    task_group = TaskGroups.query.get(task_group_id)

    for task_group_entry in json.loads(task_group.tasks):
        job_task = JobTasks(job_id=job_id, task_id=task_group_entry, status='Not Started')
        db.session.add(job_task)
        db.session.commit()
    
    return redirect("/jobs/" + str(job_id) + "/tasks")

@jobs.route("/jobs/<int:job_id>/move_task_up/<int:task_id>", methods=['GET'])
@login_required
def jobs_move_task_up(job_id, task_id):
    job = Jobs.query.get(job_id)
    job_tasks = JobTasks.query.filter_by(job_id=job_id).all()

    tasks = Tasks.query.all()