diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 496943c..f351794 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,5 +22,13 @@ jobs: run: | .\dist\ms_teams_parser.exe -f ".\forensicsim-data\jane_doe_old_teams\IndexedDB\https_teams.microsoft.com_0.indexeddb.leveldb" -o "jane_doe.json" .\dist\ms_teams_parser.exe -f ".\forensicsim-data\john_doe_old_teams\IndexedDB\https_teams.microsoft.com_0.indexeddb.leveldb" -o "john_doe.json" + - name: Test calling script 📞 + run: | + python utils/dump_leveldb.py --help + python utils/dump_localstorage.py --help + python utils/dump_sessionstorage.py --help +# python utils/populate_teams.py --help +# python utils/populate_teams_2.py --help +# python utils/populate_skype.py --help # - name: Calculate diff 👽 # run: git diff --no-index --word-diff expected_output/john_doe.json current_output.json diff --git a/main.spec b/main.spec index 2e97c9d..564da30 100644 --- a/main.spec +++ b/main.spec @@ -5,7 +5,7 @@ block_cipher = None a = Analysis(['utils\\main.py'], binaries=[], - datas=[('c:/hostedtoolcache/windows/python/3.9.13/x64/lib/site-packages/pyfiglet', 'pyfiglet')], + datas=[], hiddenimports=[], hookspath=[], runtime_hooks=[], diff --git a/requirements.txt b/requirements.txt index 80e6bab..1448a7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -chardet~=4.0.0 -pyfiglet~=0.8.post1 -colorama~=0.4.4 beautifulsoup4~=4.9.3 +chardet~=4.0.0 click~=8.0.1 +colorama~=0.4.4 +pause~=0.3 +pyautogui~=0.9.54 +pywinauto~=0.6.8 diff --git a/utils/consts.py b/utils/consts.py new file mode 100644 index 0000000..6fd20ac --- /dev/null +++ b/utils/consts.py @@ -0,0 +1,35 @@ +XTRACT_HEADER = """ + _____ _ _ +| ___|__ _ __ ___ _ __ ___(_) ___ ___ (_)_ __ ___ +| |_ / _ \| '__/ _ \ '_ \/ __| |/ __/ __| | | '_ ` _ \\ +| _| (_) | | | __/ | | \__ \ | (__\__ \_| | | | | | | +|_| \___/|_| \___|_| |_|___/_|\___|___(_)_|_| |_| |_| +__ ___ _ _____ _ +\ \/ / |_ _ __ __ _ ___| |_ |_ _|__ ___ | | + \ /| __| '__/ _` |/ __| __| | |/ _ \ / _ \| | + / \| |_| | | (_| | (__| |_ | | (_) | (_) | | +/_/\_\\\\__|_| \__,_|\___|\__| |_|\___/ \___/|_| +""" +UTIL_HEADER = """ + _____ _ _ _ _ _ _ _ +| ___|__ _ __ ___ _ __ ___(_) ___ ___ (_)_ __ ___ | | | | |_(_) | +| |_ / _ \| '__/ _ \ '_ \/ __| |/ __/ __| | | '_ ` _ \ | | | | __| | | +| _| (_) | | | __/ | | \__ \ | (__\__ \_| | | | | | | | |_| | |_| | | +|_| \___/|_| \___|_| |_|___/_|\___|___(_)_|_| |_| |_| \___/ \__|_|_| + +""" + +DUMP_HEADER = """ + _____ _ _ +| ___|__ _ __ ___ _ __ ___(_) ___ ___ (_)_ __ ___ +| |_ / _ \| '__/ _ \ '_ \/ __| |/ __/ __| | | '_ ` _ \ +| _| (_) | | | __/ | | \__ \ | (__\__ \_| | | | | | | +|_| \___/|_| \___|_| |_|___/_|\___|___(_)_|_| |_| |_| + + ____ _____ _ +| _ \ _ _ _ __ ___ _ __ |_ _|__ ___ | | +| | | | | | | '_ ` _ \| '_ \ | |/ _ \ / _ \| | +| |_| | |_| | | | | | | |_) | | | (_) | (_) | | +|____/ \__,_|_| |_| |_| .__/ |_|\___/ \___/|_| + |_| +""" diff --git a/utils/dump_leveldb.py b/utils/dump_leveldb.py index 1c2bccf..cc986ab 100644 --- a/utils/dump_leveldb.py +++ b/utils/dump_leveldb.py @@ -24,56 +24,45 @@ from pathlib import Path -import argparse -import pyfiglet -import pyfiglet.fonts +import click -import shared +from consts import DUMP_HEADER +from shared import parse_db, write_results_to_json -def process_db(filepath, output_path): +def process_db(input_path, output_path): # Do some basic error handling - if not filepath.endswith("leveldb"): - raise Exception("Expected a leveldb folder. Path: {}".format(filepath)) - - p = Path(filepath) - if not p.exists(): - raise Exception("Given file path does not exists. Path: {}".format(filepath)) - - if not p.is_dir(): - raise Exception("Given file path is not a folder. Path: {}".format(filepath)) + if not input_path.parts[-1].endswith(".leveldb"): + raise ValueError(f"Expected a leveldb folder. Path: {input_path}") # convert the database to a python list with nested dictionaries - extracted_values = shared.parse_db(filepath, True) + extracted_values = parse_db(input_path, do_not_filter=True) # write the output to a json file - shared.write_results_to_json(extracted_values, output_path) - - -def run(args): - process_db(args.filepath, args.outputpath) - - -def parse_cmdline(): - description = "Forensics.im Dump Tool" - parser = argparse.ArgumentParser(description=description) - required_group = parser.add_argument_group("required arguments") - required_group.add_argument( - "-f", "--filepath", required=True, help="File path to the IndexedDB." - ) - required_group.add_argument( - "-o", "--outputpath", required=True, help="File path to the processed output." - ) - args = parser.parse_args() - return args - - -def cli(): - header = pyfiglet.figlet_format("Forensics.im Dump Tool") - print(header) - args = parse_cmdline() - run(args) + write_results_to_json(extracted_values, output_path) + + +@click.command() +@click.option( + "-f", + "--filepath", + type=click.Path( + exists=True, readable=True, writable=False, dir_okay=True, path_type=Path + ), + required=True, + help="File path to the IndexedDB.", +) +@click.option( + "-o", + "--outputpath", + type=click.Path(writable=True, path_type=Path), + required=True, + help="File path to the processed output.", +) +def process_cmd(filepath, outputpath): + click.echo(DUMP_HEADER) + process_db(filepath, outputpath) if __name__ == "__main__": - cli() + process_cmd() diff --git a/utils/dump_localstorage.py b/utils/dump_localstorage.py index 900016d..7c89a73 100644 --- a/utils/dump_localstorage.py +++ b/utils/dump_localstorage.py @@ -24,54 +24,38 @@ from pathlib import Path -import argparse -import pyfiglet -import pyfiglet.fonts - -import shared - - -def process_db(filepath, output_path): - # Do some basic error handling - - p = Path(filepath) - if not p.exists(): - raise Exception("Given file path does not exists. Path: {}".format(filepath)) - - if not p.is_dir(): - raise Exception("Given file path is not a folder. Path: {}".format(filepath)) - - # convert the database to a python list with nested dictionaries - extracted_values = shared.parse_localstorage(p) - - # write the output to a json file - shared.write_results_to_json(extracted_values, output_path) - - -def run(args): - process_db(args.filepath, args.outputpath) - - -def parse_cmdline(): - description = "Forensics.im Dump Local Storage" - parser = argparse.ArgumentParser(description=description) - required_group = parser.add_argument_group("required arguments") - required_group.add_argument( - "-f", "--filepath", required=True, help="File path to the IndexedDB." - ) - required_group.add_argument( - "-o", "--outputpath", required=True, help="File path to the processed output." - ) - args = parser.parse_args() - return args - - -def cli(): - header = pyfiglet.figlet_format("Forensics.im Dump Tool") - print(header) - args = parse_cmdline() - run(args) +import click + +from shared import parse_localstorage, write_results_to_json +from consts import DUMP_HEADER + + +def process_db(filepath: Path, output_path: Path): + extracted_values = parse_localstorage(filepath) + write_results_to_json(extracted_values, output_path) + + +@click.command() +@click.option( + "-f", + "--filepath", + type=click.Path( + exists=True, readable=True, writable=False, dir_okay=True, path_type=Path + ), + required=True, + help="File path to the IndexedDB.", +) +@click.option( + "-o", + "--outputpath", + type=click.Path(writable=True, path_type=Path), + required=True, + help="File path to the processed output.", +) +def process_cmd(filepath: Path, outputpath: Path): + click.echo(DUMP_HEADER) + process_db(filepath, outputpath) if __name__ == "__main__": - cli() + process_cmd() diff --git a/utils/dump_sessionstorage.py b/utils/dump_sessionstorage.py index 64a37d0..14a7154 100644 --- a/utils/dump_sessionstorage.py +++ b/utils/dump_sessionstorage.py @@ -24,55 +24,38 @@ from pathlib import Path -import argparse -import pyfiglet -import pyfiglet.fonts - -import shared - - -def process_db(filepath, output_path): - # Do some basic error handling - - p = Path(filepath) - if not p.exists(): - raise Exception("Given file path does not exists. Path: {}".format(filepath)) - - if not p.is_dir(): - raise Exception("Given file path is not a folder. Path: {}".format(filepath)) - - # convert the database to a python list with nested dictionaries - # - extracted_values = shared.parse_sessionstorage(p) - - # write the output to a json file - shared.write_results_to_json(extracted_values, output_path) - - -def run(args): - process_db(args.filepath, args.outputpath) - - -def parse_cmdline(): - description = "Forensics.im Dump Session Storage" - parser = argparse.ArgumentParser(description=description) - required_group = parser.add_argument_group("required arguments") - required_group.add_argument( - "-f", "--filepath", required=True, help="File path to the IndexedDB." - ) - required_group.add_argument( - "-o", "--outputpath", required=True, help="File path to the processed output." - ) - args = parser.parse_args() - return args - - -def cli(): - header = pyfiglet.figlet_format("Forensics.im Dump Tool") - print(header) - args = parse_cmdline() - run(args) +import click + +from consts import DUMP_HEADER +from shared import parse_sessionstorage, write_results_to_json + + +def process_db(input_path: Path, output_path: Path): + extracted_values = parse_sessionstorage(input_path) + write_results_to_json(extracted_values, output_path) + + +@click.command() +@click.option( + "-f", + "--filepath", + type=click.Path( + exists=True, readable=True, writable=False, dir_okay=True, path_type=Path + ), + required=True, + help="File path to the IndexedDB.", +) +@click.option( + "-o", + "--outputpath", + type=click.Path(writable=True, path_type=Path), + required=True, + help="File path to the processed output.", +) +def process_cmd(filepath, outputpath): + click.echo(DUMP_HEADER) + process_db(filepath, outputpath) if __name__ == "__main__": - cli() + process_cmd() diff --git a/utils/main.py b/utils/main.py index 94901b9..ef42c54 100644 --- a/utils/main.py +++ b/utils/main.py @@ -22,17 +22,15 @@ SOFTWARE. """ -import argparse import json from datetime import datetime from pathlib import Path -import pyfiglet -import pyfiglet.fonts +import click from bs4 import BeautifulSoup -import shared -import sys +from shared import parse_db, write_results_to_json +from consts import XTRACT_HEADER MESSAGE_TYPES = { "messages": { @@ -340,54 +338,36 @@ def deduplicate(records, key): return distinct_records -def process_db(filepath, output_path): - # Do some basic error handling - if not filepath.endswith("leveldb"): - raise Exception("Expected a leveldb folder. Path: {}".format(filepath)) +def process_db(input_path: Path, output_path: Path): + if not input_path.parts[-1].endswith(".leveldb"): + raise ValueError(f"Expected a leveldb folder. Path: {input_path}") - p = Path(filepath) - if not p.exists(): - raise Exception("Given file path does not exists. Path: {}".format(filepath)) - - if not p.is_dir(): - raise Exception("Given file path is not a folder. Path: {}".format(filepath)) - - # convert the database to a python list with nested dictionaries - - extracted_values = shared.parse_db(filepath) - - # parse records + extracted_values = parse_db(input_path) parsed_records = parse_records(extracted_values) - - # write the output to a json file - shared.write_results_to_json(parsed_records, output_path) - - -def run(args): - process_db(args.filepath, args.outputpath) - - -def parse_cmdline(): - description = "Forensics.im Xtract Tool" - parser = argparse.ArgumentParser(description=description) - required_group = parser.add_argument_group("required arguments") - required_group.add_argument( - "-f", "--filepath", required=True, help="File path to the IndexedDB." - ) - required_group.add_argument( - "-o", "--outputpath", required=True, help="File path to the processed output." - ) - args = parser.parse_args() - return args - - -def cli(): - header = pyfiglet.figlet_format("Forensics.im Xtract Tool") - print(header) - args = parse_cmdline() - run(args) - sys.exit(0) + write_results_to_json(parsed_records, output_path) + + +@click.command() +@click.option( + "-f", + "--filepath", + type=click.Path( + exists=True, readable=True, writable=False, dir_okay=True, path_type=Path + ), + required=True, + help="File path to the IndexedDB.", +) +@click.option( + "-o", + "--outputpath", + type=click.Path(writable=True, path_type=Path), + required=True, + help="File path to the processed output.", +) +def process_cmd(filepath, outputpath): + click.echo(XTRACT_HEADER) + process_db(filepath, outputpath) if __name__ == "__main__": - cli() + process_cmd() diff --git a/utils/populate_skype.py b/utils/populate_skype.py index 95d168a..fd0e086 100644 --- a/utils/populate_skype.py +++ b/utils/populate_skype.py @@ -5,10 +5,12 @@ import click import pause -import pyfiglet + from pywinauto import Desktop, keyboard from pywinauto.application import Application +from consts import UTIL_HEADER + logging.basicConfig( format="%(asctime)s %(message)s", datefmt="%m/%d/%Y %I:%M:%S %p", @@ -142,8 +144,7 @@ def populate_data_skype(all_data_to_populate, account): ) @click.option("--account", "-a", required=True, default="0", help="Account to populate") def cli(filepath, account): - header = pyfiglet.figlet_format("Forensics.im Util") - click.echo(header) + click.echo(UTIL_HEADER) with click.open_file(filepath, encoding="utf-8") as f: data = json.load(f) populate_data_skype(data, account) diff --git a/utils/populate_teams.py b/utils/populate_teams.py index 43dc211..85cf8bd 100644 --- a/utils/populate_teams.py +++ b/utils/populate_teams.py @@ -9,7 +9,7 @@ import pause import pyautogui from pywinauto import keyboard -import pyfiglet +from consts import UTIL_HEADER # Avoid the default link as it would update Teams on startup os.startfile("C:/Users/forensics/AppData/Local/Microsoft/Teams/current/Teams.exe") @@ -201,8 +201,7 @@ def populate_data_teams(all_data_to_populate, account): ) @click.option("--account", "-a", required=True, default="0", help="Account to populate") def cli(filepath, account): - header = pyfiglet.figlet_format("Forensics.im Util") - click.echo(header) + click.echo(UTIL_HEADER) with click.open_file(filepath, encoding="utf-8") as f: data = json.load(f) populate_data_teams(data, account) diff --git a/utils/populate_teams_2.py b/utils/populate_teams_2.py index cc86b56..e6120dd 100644 --- a/utils/populate_teams_2.py +++ b/utils/populate_teams_2.py @@ -7,9 +7,10 @@ import click import pause import pyautogui -import pyfiglet + from pywinauto import keyboard +from consts import UTIL_HEADER # Teams could be started from script, but requires change owner permissions. Better to launch Teams 2.0 first and # then set the focus to the application. # os.startfile("C:/Program Files/WindowsApps/MicrosoftTeams_21197.1103.908.5982_x64__8wekyb3d8bbwe/msteams.exe") @@ -155,8 +156,7 @@ def populate_data_teams(all_data_to_populate, account): ) @click.option("--account", "-a", required=True, default="0", help="Account to populate") def cli(filepath, account): - header = pyfiglet.figlet_format("Forensics.im Util") - click.echo(header) + click.echo(UTIL_HEADER) with click.open_file(filepath, encoding="utf-8") as f: data = json.load(f) populate_data_teams(data, account)