Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions BitsParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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)
3 changes: 3 additions & 0 deletions ct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions ct/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down