├── readme pics ├── webdev.jpg ├── response.jpg └── firevueout.jpg ├── .gitignore ├── README.md └── firevue.py /readme pics/webdev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightflix/firevue/HEAD/readme pics/webdev.jpg -------------------------------------------------------------------------------- /readme pics/response.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightflix/firevue/HEAD/readme pics/response.jpg -------------------------------------------------------------------------------- /readme pics/firevueout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightflix/firevue/HEAD/readme pics/firevueout.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firevue 2 | A hirevue question cracker 3 | 4 | ## *Update (15-11-2023): Many users reported that this script does not work anymore. Hirevue may have likely patched this.* 5 | 6 | ### Requirements 7 | 1. Python 3 8 | 2. Mozilla Firefox (Google Chrome might work but I haven't tested it). 9 | 3. Hirevue interview invite link 10 | 11 | ### Steps to run this code 12 | 1. Download the repository or the python file `firevue.py`. 13 | 2. Move the python file in any new directory (folder). This could be anywhere on the system. 14 | 3. Open the valid hirevue invite link in Firefox. If you don't see your name on the page, click the continue or start button and the page showing your name should be next. This will not the start the interview. But it's an important step in capturing the questions. 15 | 4. Open the web development console (`CTRL+SHIFT+I` or `CMD+ALT+I`). Click on the network tab. 16 | 5. Reload the current page so it can capture all web traffic. 17 | 6. Right click on any entry in the console and save all as HAR. Save the HAR file into the same folder that contains the python script `firevue.py`. 18 | Firevue output 19 | 20 | 7. On Windows, press (Windows key + R) and type `cmd`, then hit enter. This will open a black command prompt window. For Linux/MacOS, open the terminal. 21 | 8. In the terminal, type `cd`, followed by a space. 22 | 9. On Windows/macOS, drag the folder onto the command prompt window you just opened. You'll see the name of that folder appear. The command now should look like `cd C:\the\path\to\the\folder`. Hit enter. 23 | 10. Type `python3 firevue.py`, then hit enter. If "python3" is not a recognized command then either 1) run the command "python" or 2) go to where python is installed and either 1) rename "python.exe" to "python3.exe" or 2) copy and paste python.exe into the same directory and rename the copy "python3.exe" 24 | 11. The script should automatically detect the HAR file and try to do its magic. Follow the instructions on-screen. Eventually it will display your interview questions like shown below: 25 | Firevue output 26 | 27 | 12. Optional: If you want to manually decode encoded text that you find in the HAR file, you can use the `-d` option. Use `python3 firevue.py -h` or `python3 firevue.py --help` to check usage. 28 | 29 | ### FAQs 30 | 31 | #### 1. What does this script do and how does it work? 32 | This script reads web responses that were sent by Hirevue after you clicked the interview link. To my surprise, Hirevue sends all interview-related data including questions to your browser before the interview even starts. This means our browser already has the questions beforehand and displays them one at a time as you go through the interview process. The questions though were encoded with some trivial algorithm which I could easily reverse engineer. 33 | 34 | Encrypted questions as seen in Hirevue response 35 | 36 | I exploited this flaw and wrote python code to analyse the response, decrypt the "encrypted" questions and reveal them in plaintext so you can take all the time you need to prepare and give your best answers. 37 | 38 | #### 2. I don't have Python installed. How do I install it? 39 | - Windows users: https://www.tutorialspoint.com/how-to-install-python-in-windows 40 | - MacOS users: It's preinstalled. Just run the code using the above steps. 41 | - Linux users: Yes 42 | 43 | #### 3. I followed the above steps and it still doesn't work. 44 | There are a multitude of reasons why this may not work. Each interview can be slightly different in the way they're structured in the delivery process which could mean the questions may be in a different location in the received response, or the encoding might be different, or even worse - Hirevue may have fixed this. The only way I could have made my code more compatible was if I could test this on multiple interview links from different interviewers/companies. This isn't possible at the moment unless applicants send me their interview links for testing. 45 | 46 | #### 4. Why didn't I use the python requests library to automate all of this from just the interview link? 47 | I didn't want to access the interview page hundreds of times just to test my code, have them think I'm sus and increase my chances of failing the interview. 48 | 49 | #### 5. Is this safe to run? 50 | Yes, this script is 100% safe, runs locally and only reads the HAR file you've placed in its directory. You may be violating Hirevue's terms and conditions. I haven't read the fine print so I can't tell. 51 | -------------------------------------------------------------------------------- /firevue.py: -------------------------------------------------------------------------------- 1 | #Simple tool to extract and decode hirevue questions from a browser HAR file 2 | import json, glob, argparse 3 | 4 | class color: 5 | BLUE = '\033[94m' 6 | GREEN = '\033[92m' 7 | YELLOW = '\033[93m' 8 | BOLD = '\033[1m' 9 | END = '\033[0m' 10 | 11 | def decoder_plaintext(ciphertext): 12 | #uno reverse card 13 | return ciphertext 14 | 15 | def decoder_original(ciphertext): 16 | #if questions couldn't be extracted 17 | if ciphertext == None: 18 | return ciphertext 19 | 20 | #convert string to character array 21 | chars = list(ciphertext) 22 | 23 | #first half of ciphertext array 24 | first_half = chars[:len(chars)//2] 25 | 26 | #second half of ciphertext array 27 | second_half = chars[len(chars)//2:] 28 | result = [] 29 | 30 | for i in range(0, len(first_half)): 31 | 32 | #take one char from the first half and another char from the second half and add to resultant array. 33 | result.append(second_half[i]) 34 | result.append(first_half[i]) 35 | 36 | #if length of string is odd, it can't be evenly divided leaving out an additional character in the second half from being printed in loop, so we append that separately. 37 | if(len(chars) % 2 == 1): 38 | result.append(second_half[-1]) 39 | 40 | #convert array/list back to string and return. 41 | return "".join(result) 42 | 43 | def harParse(): 44 | 45 | har_file = glob.glob('*.har') 46 | 47 | if len(har_file) != 1: 48 | raise ValueError("[x] Error: HAR file does not exist in current directory or there is more than one HAR file in current directory!") 49 | 50 | #pick the only har file in current directory. 51 | har_filename = har_file[0] 52 | 53 | print(har_file[0]) 54 | 55 | #load json from the HAR file 56 | har_file = json.load(open(har_filename, encoding="utf-8")) 57 | 58 | try: 59 | entries = har_file.get('log').get('entries') 60 | except AttributeError: 61 | raise AttributeError("[x] Error: Logs or Entries not found") 62 | 63 | #iterate though each request made by the browser 64 | try: 65 | for entry in entries: 66 | 67 | #in each entry, get inside the response object and then into content. 68 | content = entry.get('response').get('content') 69 | 70 | #questions are normally in the content object with the key "text" 71 | if "text" in content and "\"questions\":" in content.get("text") and "interviewerId" in content.get("text"): 72 | if not content.get("mimeType") == "application/json": 73 | print("[-] Warning: json mimetype not found, attempting to fetch info") 74 | 75 | #the questions are again encoded in json to pythonise that. 76 | return json.loads(content["text"]) 77 | 78 | except TypeError: 79 | raise TypeError("[x] Error: Entries object isn't an iterable."+" Current Type:"+str(type(entries))) 80 | 81 | #no questions found :( 82 | return False 83 | 84 | def getQuestions(obj, decoder_func): 85 | 86 | question_list = obj.get('questions') 87 | if question_list == None: 88 | print("[x] Not available") 89 | else: 90 | 91 | i = 0 92 | for q in question_list: 93 | if q.get('type') != "mindx-assessment": 94 | print(color.GREEN+"\n - Question "+str(i+1)+": Prep Time (sec): "+str(q.get('prepTimeSeconds'))+", Max Answer Duration (sec): "+str(q.get('maxDuration'))+", Answer type: "+str(q.get('type'))+color.END+"\n") 95 | 96 | # send each question into the decoder that converts it back to plaintext. 97 | print(decoder_func(q.get('text'))) 98 | 99 | #if questions is of multiple choice type, print options. 100 | if str(q.get('type')) == "multiple-selection": 101 | for option in q.get('options'): 102 | print(" [ ]",decoder_func(option)) 103 | 104 | if str(q.get('type')) == "multiple-choice": 105 | for option in q.get('options'): 106 | print(" [ ]",decoder_func(option)) 107 | 108 | print("\n -------------------------------") 109 | i += 1 110 | 111 | def getInfo(content, decoder_func): 112 | 113 | print(color.BOLD+"\n[-] Interview Details\n"+color.END) 114 | 115 | print(color.GREEN+" - Interviewer: "+color.END+str(content.get('interviewer'))) 116 | print(color.GREEN+" - Role: "+color.END+str(content.get('position'))) 117 | # print(color.GREEN+" - Total number of questions (includes games section): "+color.END+str(content.get('questionCount'))) 118 | print(color.GREEN+" - Number of retries allowed: "+color.END+str(content.get('retryAllowance'))) 119 | print(color.GREEN+" - Interview Duration: "+color.END+str(content.get('interviewDurationMinutes'))+" mins") 120 | print(color.GREEN+" - Estimated Duration: "+color.END+str(content.get('estimatedMinutesToComplete'))+" mins") 121 | print(color.GREEN+" - Invite Date: "+color.END+str(content.get('interviewUses')[0].get('invitedDate'))) 122 | 123 | print(color.BOLD+"\n[-] All possible Interview Questions"+color.END) 124 | getQuestions(content, decoder_func) 125 | 126 | print(color.BOLD+"\n[-] Interview Sections"+color.END) 127 | section_list = content.get('sections') 128 | if section_list == None: 129 | print("[x] Not available") 130 | else: 131 | print("[-] Questions in this interview: ") 132 | i = 0 133 | for s in section_list: 134 | print(color.BLUE+"\n - Section "+str(i+1)+": "+str(s.get('name'))+color.END+"\n"+str(s.get('instructions'))+"\n\n -------------------------------") 135 | getQuestions(s, decoder_func) 136 | i += 1 137 | 138 | if __name__ == "__main__": 139 | 140 | cmdlineParse = argparse.ArgumentParser(description="This utility scans through a HAR file (JSON), automatically searches for interview data and displays them in decoded form. This is the default behaviour if no optional argument is specified. See below for manual decoding of text (for advanced users only).") 141 | 142 | cmdlineParse.add_argument('-d','--decode', type=str, help="Specify encoded text from the HAR file to be decoded, within double quotes. This mode does not read files and only uses the decode function of this script. Warning: Decoding may be incorrect if string supplied is incomplete or there are additional characters. \n\nExample usage: \npython firevue.py -d \"[String including \\\" as seen in file]\"", dest="encoded_string") 143 | args = cmdlineParse.parse_args() 144 | 145 | if args.encoded_string: 146 | print(color.YELLOW+"\n--== Firevue: Decode Mode ==--"+color.END) 147 | 148 | es = args.encoded_string 149 | 150 | if(es[:1] != "\"" or es[-1:] != "\""): 151 | print("[x] Error: Decoding failed. String incomplete or there are additional characters. String must begin and end with \\\" inside double quotes.\n") 152 | exit(0) 153 | 154 | print("\n"+decoder_original(es[1:-1])+"\n") 155 | exit(0) 156 | 157 | #parse result contains the interview details and all questions. 158 | try: 159 | parse_result = harParse() 160 | except ValueError as e: 161 | print(str(e)) 162 | exit() 163 | 164 | if parse_result == False: 165 | print("\n[x] ERROR: Incorrect HAR parsing, exiting.") 166 | exit() 167 | 168 | print(color.YELLOW+"\n--== Firevue: A Hirevue Question Cracker v1.1 ==--"+color.END) 169 | 170 | #fetch values and print them neatly 171 | decoders = [decoder_plaintext, decoder_original] 172 | for decoder in decoders: 173 | getInfo(parse_result, decoder) 174 | print("\n\n*** If the questions are readable, enter \"Y\" to finish, otherwise enter \"N\" to try again. ***\n") 175 | response = input().lower() 176 | if response == "y": 177 | break 178 | 179 | # getInfo(parse_result) 180 | 181 | # with open("parsed_result.json","w") as f: 182 | # json.dump(parse_result, f, indent=4) 183 | 184 | print(color.BLUE+"\n[-] Good luck! :D\n"+color.END) 185 | --------------------------------------------------------------------------------