diff --git a/BitsParser.py b/BitsParser.py index d813cae..dc95c6a 100644 --- a/BitsParser.py +++ b/BitsParser.py @@ -57,6 +57,8 @@ def __init__(self, queue_dir, carve_db, carve_all, out_file, sid_lookup=True): self.carve_all_files = carve_all self.out_file = out_file self.sid_lookup = sid_lookup + # Set when a file could not be read, so the exit code reports it + self.failed = False self.sid_user_cache = {} self.visited_jobs = set() @@ -384,7 +386,7 @@ def output_jobs_properjson(self, file_path, jobs): sys.stdout = orig_stdout - def process_file(self, file_path): + def process_file(self, file_path, named_directly=False): """ Processes the given BITS file. Attempts to find/parse jobs. """ try: @@ -408,10 +410,17 @@ def process_file(self, file_path): else: jobs = self.load_non_qmgr_jobs(file_data) + # A directory also holds ESE logs and checkpoints, so only a file named directly is an error + elif named_directly: + print(f'{file_path} is not a recognized BITS database', file=sys.stderr) + self.failed = True + return + self.output_jobs_properjson(file_path, jobs) except Exception: print(f'Exception occurred processing file {file_path}: ' + traceback.format_exc(), file=sys.stderr) + self.failed = True def determine_directory_architecture(self, path): @@ -427,7 +436,7 @@ def run(self): # If the queue "directory" is a file, just process the file if os.path.isfile(self.queue_dir): - self.process_file(self.queue_dir) + self.process_file(self.queue_dir, named_directly=True) return # Determine if the directory appears to belong to a Windows 10 system or an older system for carving @@ -605,3 +614,4 @@ def parse(self): bits_parser = BitsParser(queue_dir, parsed_args.carvedb, parsed_args.carveall, parsed_args.output, sid_lookup=not parsed_args.no_sid_lookup) bits_parser.run() + sys.exit(1 if bits_parser.failed else 0) diff --git a/ct/README.md b/ct/README.md index 4e38e2a..6309d79 100644 --- a/ct/README.md +++ b/ct/README.md @@ -19,6 +19,9 @@ Work happens on `master`. Upstream is that has the XP job delimiter the PyPI release lacks. - **`--no-sid-lookup`.** Writes only `OwnerSID`, without resolving it against the accounts of the machine running BitsParser. Cyber Triage passes it. +- **Failure exit code.** Exits 1, without writing output, when a file named by + `-i` is not a recognized BITS database or cannot be parsed. Cyber Triage + reports that exit code as a host analysis issue. - **Pinned build.** `requirements-build.txt` pins PyInstaller and its dependencies, and `.github/workflows/ct_release.yml` builds the exe. diff --git a/ct/smoke_test.py b/ct/smoke_test.py index febe63a..90a1585 100644 --- a/ct/smoke_test.py +++ b/ct/smoke_test.py @@ -55,9 +55,11 @@ def main(): with open(out, encoding="utf-8") as file_object: document = json.load(file_object) print(f"zero-filled file: exit code {exit_code}, output {document}") - if exit_code != 0 or document != {"jobs": []}: + # Cyber Triage reports an unreadable database from the exit code + if exit_code != 1 or document is not None: failures.append( - f'a zero-filled file gave {document}, expected {{"jobs": []}}' + f"a zero-filled file gave exit code {exit_code} and {document}, " + "expected exit code 1 and no output" ) # pyi-archive_viewer, from the PyInstaller that runs this script.