diff --git a/BitsParser.py b/BitsParser.py index a2a83db..c90547f 100644 --- a/BitsParser.py +++ b/BitsParser.py @@ -328,6 +328,7 @@ def output_jobs(self, file_path, jobs): """Cleans up and outputs the parsed jobs from the qmgr database files""" # If an output file is specified, open it and use it instead of stdout + if self.out_file: orig_stdout = sys.stdout sys.stdout = open(self.out_file, "w") @@ -350,6 +351,20 @@ def output_jobs(self, file_path, jobs): sys.stdout = orig_stdout + def get_unique_records(self, jobs): + records = list() + for job in jobs: + # Skip incomplete carved jobs as they do not contain useful info + if job.is_carved() and not job.is_useful_for_analysis(): + continue + + # Output unique jobs + if job.hash not in self.visited_jobs: + records.append(job.job_dict) + self.visited_jobs.add(job.hash) + return records + + def process_file(self, file_path): """ Processes the given BITS file. Attempts to find/parse jobs. """ @@ -363,7 +378,7 @@ def process_file(self, file_path): # Parse as a qmgr database (support old and Win10 formats) jobs = [] if BitsParser.is_qmgr_database(file_data): - jobs = self.load_qmgr_jobs(file_data) + jobs = self.load_qmgr_jobs(file_path) elif BitsParser.is_qmgr10_database(file_data): jobs = self.load_qmgr10_jobs(file_data) @@ -374,7 +389,11 @@ def process_file(self, file_path): else: jobs = self.load_non_qmgr_jobs(file_data) - self.output_jobs(file_path, jobs) + if self.out_file and self.out_file.endswith(".csv"): + import csv_writer + csv_writer.write_csv(self.out_file, self.get_unique_records(jobs)) + else: + self.output_jobs(file_path, jobs) except Exception: print(f'Exception occurred processing file {file_path}: ' + traceback.format_exc(), file=sys.stderr) diff --git a/csv_writer.py b/csv_writer.py new file mode 100644 index 0000000..a566960 --- /dev/null +++ b/csv_writer.py @@ -0,0 +1,55 @@ +import csv +import os + + +DEFAULT_VALUES = ( + ('JobId', None), + ('JobName', None), + ('JobType', None), + ('JobPriority', None), + ('OwnerSID', None), + ('JobState', None), + ('CommandExecuted', None), + ('CommandArguments', None), + ('FileID', 0), + ('DestFile', None), + ('SourceURL', None), + ('TmpFile', None), + ('DownloadByteSize', -1), + ('TransferByteSize', -1), + ('VolumeGUID', None), + ('CreationTime', None), + ('ModifiedTime', None), + ('Carved', False) +) + +def flattener(job): + + def _f(index, file): + rv = {k: file.get(k, job.get(k, v)) for k, v in DEFAULT_VALUES} + rv['FileID'] = index + return rv + + files = job.get('Files', []) + + if files: + return [_f(index, f) for index, f in enumerate(files)] + + return [_f(0, {})] + + +def write_csv(filename, records): + """Write records to a CSV file.""" + if not len(records): + return + if os.path.isfile(filename): + csvfile = open(filename, "a+", newline='', encoding='utf-8') + else: + csvfile = open(filename, "w", newline='', encoding='utf-8') + + writer = csv.DictWriter(csvfile, fieldnames=[k for k, _ in DEFAULT_VALUES]) + writer.writeheader() + for r in records: + for sub_r in flattener(r): + writer.writerow(sub_r) + csvfile.close()