#!/usr/bin/python

# dfir_ntfs: an NTFS parser for digital forensics & incident response
# (c) Maxim Suhanov

import sys
import csv
from dfir_ntfs import *

def format_timestamp(timestamp):
	if timestamp is None:
		return 'N/A'

	return timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')

def hexdump(buf):
	def int2hex(i):
		return '{:02X}'.format(i)

	if type(buf) is not bytearray:
		buf = bytearray(buf)

	if len(buf) == 0:
		return '-'

	output_lines = ''

	i = 0
	while i < len(buf):
		bytes_line = buf[i : i + 16]

		address = int2hex(i)
		address = str(address).zfill(8)
		hex_line = ''
		ascii_line = ''

		k = 0
		while k < len(bytes_line):
			single_byte = bytes_line[k]

			hex_line += int2hex(single_byte)
			if k == 7 and k != len(bytes_line) - 1:
				hex_line += '-'
			elif k != len(bytes_line) - 1:
				hex_line += ' '

			if single_byte >= 32 and single_byte <= 126:
				ascii_line += chr(single_byte)
			else:
				ascii_line += '.'

			k += 1

		padding_count = 16 - k
		if padding_count > 0:
			hex_line += ' ' * 3 * padding_count

		output_lines += address + ' ' * 2 + hex_line + ' ' * 2 + ascii_line

		i += 16

		if i < len(buf):
			output_lines += '\n'

	return output_lines

def format_source(source_str, source_tag):
	if source_str is None or len(source_str) == 0:
		return ''

	if source_tag is None or len(source_tag) == 0:
		return source_str

	return '{} ({})'.format(source_str, source_tag)


print_header = True

def process_mft(source_tag = None):
	global input_file, csv_writer, print_header

	mft = MFT.MasterFileTableParser(input_file)

	if print_header:
		csv_writer.writerow(['Source', 'MFT reference number', 'Is in use', 'Is directory', 'Log file sequence number', 'Path', '$SI M timestamp', '$SI A timestamp', '$SI C timestamp', '$SI E timestamp', '$SI USN value', '$FN M timestamp', '$FN A timestamp', '$FN C timestamp', '$FN E timestamp', '$OBJID timestamp', 'File size', 'ADS list', 'WSL M timestamp', 'WSL A timestamp', 'WSL CH timestamp'])
		print_header = False

	for file_record in mft.file_records():

		# Parse this file record.

		try:
			file_paths = mft.build_full_paths(file_record, True)
		except MFT.MasterFileTableException:
			continue

		attr_standard_information = None
		file_size = None
		ads_set = set()
		objid_time = None

		wsl_found = False
		wsl_mtime = ''
		wsl_atime = ''
		wsl_chtime = ''

		for attribute in file_record.attributes():
			if type(attribute) is MFT.AttributeRecordResident:
				attribute_value = attribute.value_decoded()

				if type(attribute_value) is Attributes.StandardInformation:
					if attr_standard_information is None:
						attr_standard_information = attribute_value

				if type(attribute_value) is Attributes.ObjectID:
					if objid_time is None:
						objid_time = attribute_value.get_timestamp()

				if type(attribute_value) is Attributes.EA:
					if not wsl_found:
						for ea_name, ea_flags, ea_value in attribute_value.data_parsed():
							if ea_name == b'LXATTRB\x00':
								try:
									lxattrb = WSL.LXATTRB(ea_value)
								except ValueError:
									pass
								else:
									wsl_found = True

									wsl_atime = format_timestamp(lxattrb.get_atime())
									wsl_mtime = format_timestamp(lxattrb.get_mtime())
									wsl_chtime = format_timestamp(lxattrb.get_chtime())

				if attribute.type_code == Attributes.ATTR_TYPE_DATA and attribute.name is None:
					if file_size is None:
						file_size = str(len(attribute.value))

				if attribute.type_code == Attributes.ATTR_TYPE_DATA and attribute.name is not None:
					ads_set.add(attribute.name)
			else:
				if attribute.type_code == Attributes.ATTR_TYPE_DATA and attribute.name is None and attribute.lowest_vcn == 0:
					if file_size is None:
						file_size = str(attribute.file_size)

				if attribute.type_code == Attributes.ATTR_TYPE_DATA and attribute.name is not None:
					ads_set.add(attribute.name)

		if file_size is None:
			file_size = '?'

		if len(ads_set) > 0:
			ads_list = ' '.join(sorted(ads_set))
		else:
			ads_list = ''

		if objid_time is None:
			objid_time = ''
		else:
			objid_time = format_timestamp(objid_time)

		if attr_standard_information is not None:
			si_mtime = format_timestamp(attr_standard_information.get_mtime())
			si_atime = format_timestamp(attr_standard_information.get_atime())
			si_ctime = format_timestamp(attr_standard_information.get_ctime())
			si_etime = format_timestamp(attr_standard_information.get_etime())
			si_usn = attr_standard_information.get_usn()
		else:
			si_mtime = ''
			si_atime = ''
			si_ctime = ''
			si_etime = ''
			si_usn = ''

		fr_lsn = file_record.get_logfile_sequence_number()

		if file_record.is_in_use():
			fr_in_use = 'Y'
		else:
			fr_in_use = 'N'

		if file_record.get_flags() & MFT.FILE_FILE_NAME_INDEX_PRESENT > 0:
			fr_directory = 'Y'
		else:
			fr_directory = 'N'

		fr_number = MFT.EncodeFileRecordSegmentReference(file_record.get_master_file_table_number(), file_record.get_sequence_number())

		if len(file_paths) > 0:
			for file_path, attr_file_name in file_paths:
				fn_mtime = format_timestamp(attr_file_name.get_mtime())
				fn_atime = format_timestamp(attr_file_name.get_atime())
				fn_ctime = format_timestamp(attr_file_name.get_ctime())
				fn_etime = format_timestamp(attr_file_name.get_etime())

				csv_writer.writerow([format_source('File record', source_tag), fr_number, fr_in_use, fr_directory, fr_lsn, file_path, si_mtime, si_atime, si_ctime, si_etime, si_usn, fn_mtime, fn_atime, fn_ctime, fn_etime, objid_time, file_size, ads_list, wsl_mtime, wsl_atime, wsl_chtime])
		else:
			csv_writer.writerow([format_source('File record', source_tag), fr_number, fr_in_use, fr_directory, fr_lsn, '', si_mtime, si_atime, si_ctime, si_etime, si_usn, '', '', '', '', objid_time, file_size, ads_list, wsl_mtime, wsl_atime, wsl_chtime])

		# Parse a file name index in this file record (if present).

		attr_index_root = None

		if file_record.get_flags() & MFT.FILE_FILE_NAME_INDEX_PRESENT > 0:
			for attribute in file_record.attributes():
				if type(attribute) is MFT.AttributeRecordResident:
					attribute_value = attribute.value_decoded()

					if type(attribute_value) is Attributes.IndexRoot:
						if attribute_value.get_indexed_attribute_type_code() == Attributes.ATTR_TYPE_FILE_NAME:
							attr_index_root = attribute_value
							break

			if attr_index_root is not None:
				for index_entry in attr_index_root.index_entries():
					attr_file_name_raw = index_entry.get_attribute()
					if attr_file_name_raw is None:
						continue

					attr_file_name = Attributes.FileName(attr_file_name_raw)

					if len(file_paths) > 0:
						dir_path = file_paths[0][0]
						if dir_path == '/.':
							dir_path = ''

						file_path = MFT.PATH_SEPARATOR.join([dir_path, attr_file_name.get_file_name()])
					else:
						file_path = MFT.PATH_SEPARATOR.join(['<Unknown>', attr_file_name.get_file_name()])

					fr_number = index_entry.get_file_reference()

					if attr_file_name.get_file_attributes() & Attributes.DUP_FILE_NAME_INDEX_PRESENT > 0:
						fr_directory = 'Y'
					else:
						fr_directory = 'N'

					fn_mtime = format_timestamp(attr_file_name.get_mtime())
					fn_atime = format_timestamp(attr_file_name.get_atime())
					fn_ctime = format_timestamp(attr_file_name.get_ctime())
					fn_etime = format_timestamp(attr_file_name.get_etime())

					file_size = attr_file_name.get_file_size()

					csv_writer.writerow([format_source('Index record', source_tag), fr_number, '?', fr_directory, '', file_path, '', '', '', '', '', fn_mtime, fn_atime, fn_ctime, fn_etime, '', file_size, '', '', '', ''])

		# Parse slack space in this file record (if present).

		for slack in file_record.slack():
			for item in slack.carve(True):
				if type(item) is Attributes.FileName:
					attr_file_name = item

					parent_directory_reference = attr_file_name.get_parent_directory()
					parent_fr_number, parent_fr_sequence = MFT.DecodeFileRecordSegmentReference(parent_directory_reference)

					try:
						parent_file_record = mft.get_file_record_by_number(parent_fr_number, parent_fr_sequence)
						parent_file_paths = mft.build_full_paths(parent_file_record)
					except MFT.MasterFileTableException:
						parent_file_path = None
					else:
						if len(parent_file_paths) > 0:
							parent_file_path = parent_file_paths[0]
						else:
							parent_file_path = None

					if parent_file_path is not None:
						if parent_file_path == '/.':
							parent_file_path = ''

						file_path = MFT.PATH_SEPARATOR.join([parent_file_path, attr_file_name.get_file_name()])
					else:
						file_path = MFT.PATH_SEPARATOR.join(['<Unknown>', attr_file_name.get_file_name()])

					if attr_file_name.get_file_attributes() & Attributes.DUP_FILE_NAME_INDEX_PRESENT > 0:
						fr_directory = 'Y'
					else:
						fr_directory = 'N'

					fn_mtime = format_timestamp(attr_file_name.get_mtime())
					fn_atime = format_timestamp(attr_file_name.get_atime())
					fn_ctime = format_timestamp(attr_file_name.get_ctime())
					fn_etime = format_timestamp(attr_file_name.get_etime())

					file_size = attr_file_name.get_file_size()

					csv_writer.writerow([format_source('Slack', source_tag), '?', '?', fr_directory, '', file_path, '', '', '', '', '', fn_mtime, fn_atime, fn_ctime, fn_etime, '', file_size, '', '', '', ''])

				else:
					file_path = MFT.PATH_SEPARATOR.join(['<Unknown, likely the same as above>', '<Can be partially overwritten or start with a wrong character>' + item])

					csv_writer.writerow([format_source('Slack', source_tag), '?', '?', '?', '', file_path, '', '', '', '', '', '?', '?', '?', '?', '', '?', '', '', '', ''])

def process_usn():
	global input_file, input_mft, csv_writer

	usn_journal = USN.ChangeJournalParser(input_file)
	mft_file = MFT.MasterFileTableParser(input_mft)

	csv_writer.writerow(['USN value', 'Source', 'Reason', 'MFT reference number', 'Parent MFT reference number', 'Timestamp', 'File name', 'File path (from $MFT)'])

	for usn_record in usn_journal.usn_records():
		r_usn = usn_record.get_usn()
		r_source = USN.ResolveSourceCodes(usn_record.get_source_info())
		r_reason = USN.ResolveReasonCodes(usn_record.get_reason())
		fr_reference_number = usn_record.get_file_reference_number()
		parent_fr_reference_number = usn_record.get_parent_file_reference_number()

		if type(usn_record) is USN.USN_RECORD_V2_OR_V3:
			r_timestamp = format_timestamp(usn_record.get_timestamp())
			fr_file_name = usn_record.get_file_name()
		else:
			r_timestamp = ''
			fr_file_name = ''

		fr_number, fr_sequence = MFT.DecodeFileRecordSegmentReference(fr_reference_number)

		try:
			file_record = mft_file.get_file_record_by_number(fr_number, fr_sequence)
			file_paths = mft_file.build_full_paths(file_record)
		except MFT.MasterFileTableException:
			fr_file_path = ''
		else:
			if len(file_paths) > 0:
				fr_file_path = file_paths[0]
			else:
				fr_file_path = ''

		csv_writer.writerow([r_usn, r_source, r_reason, fr_reference_number, parent_fr_reference_number, r_timestamp, fr_file_name, fr_file_path])

def process_log():
	global input_file, input_mft, output_file
	global deleted_files

	def process_restart_area(restart_area):
		output_lines = []

		output_lines.append('LSN: {}'.format(restart_area.lsn))
		output_lines.append('Restart area, version: {}.{}'.format(restart_area.get_major_version(), restart_area.get_minor_version()))
		output_lines.append(' Start of checkpoint LSN: {}'.format(restart_area.get_start_of_checkpoint_lsn()))
		output_lines.append(' Tables (LSN and length):')
		output_lines.append(' * Open attribute table: {} {}'.format(restart_area.get_open_attribute_table_lsn(), restart_area.get_open_attribute_table_length()))
		output_lines.append(' * Attribute names: {} {}'.format(restart_area.get_attribute_names_lsn(), restart_area.get_attribute_names_length()))
		output_lines.append(' * Dirty page table: {} {}'.format(restart_area.get_dirty_page_table_lsn(), restart_area.get_dirty_page_table_length()))
		output_lines.append(' * Transaction table: {} {}'.format(restart_area.get_transaction_table_lsn(), restart_area.get_transaction_table_length()))

		return '\n'.join(output_lines)

	def process_log_record(log_record):
		global deleted_files

		output_lines = []

		target_attribute_name = None
		redo_op = log_record.get_redo_operation()
		undo_op = log_record.get_undo_operation()
		redo_data = log_record.get_redo_data()
		undo_data = log_record.get_undo_data()

		output_lines.append('LSN: {}'.format(log_record.lsn))
		output_lines.append('Transaction ID: {}'.format(log_record.transaction_id))
		output_lines.append('Log record, redo operation: {}, undo operation: {}'.format(LogFile.ResolveNTFSOperation(redo_op), LogFile.ResolveNTFSOperation(undo_op)))

		target = log_record.calculate_mft_target_number()
		if target is not None:
			output_lines.append('Target (file number): {}'.format(target))

			try:
				file_record = mft_file.get_file_record_by_number(target)
				file_paths = mft_file.build_full_paths(file_record)
			except MFT.MasterFileTableException:
				fr_file_path = None
			else:
				if len(file_paths) > 0:
					fr_file_path = file_paths[0]
				else:
					fr_file_path = None

			if fr_file_path is not None:
				output_lines.append('Target path (from $MFT, likely wrong if the file was deleted later): {}'.format(fr_file_path))
		else:
			target = log_record.calculate_mft_target_reference_and_name()
			if target is not None:
				target_reference, target_attribute_name = target

				output_lines.append('Target (file reference number): {}'.format(target_reference))

				if target_attribute_name is None:
					target_attribute_name = '-'

				output_lines.append('Target (attribute name): {}'.format(target_attribute_name))

				fr_number, fr_sequence = MFT.DecodeFileRecordSegmentReference(target_reference)

				try:
					file_record = mft_file.get_file_record_by_number(fr_number, fr_sequence)
					file_paths = mft_file.build_full_paths(file_record)
				except MFT.MasterFileTableException:
					fr_file_path = None
				else:
					if len(file_paths) > 0:
						fr_file_path = file_paths[0]
					else:
						fr_file_path = None

				if fr_file_path is not None:
					output_lines.append('Target path (from $MFT): {}'.format(fr_file_path))
			else:
				output_lines.append('Unknown target, target attribute: {}'.format(log_record.get_target_attribute()))

		offset_in_target = log_record.calculate_offset_in_target()
		if offset_in_target is not None:
			output_lines.append('Offset in target: {}'.format(offset_in_target))
		else:
			output_lines.append('Unknown offset in target')

		lcns = []
		try:
			for lcn in log_record.get_lcns_for_page():
				lcns.append(str(lcn))
		except LogFile.ClientException:
			output_lines.append('Warning: truncated page')
			return '\n'.join(output_lines)

		if len(lcns) > 0:
			output_lines.append('LCN(s): {}'.format(' '.join(lcns)))

		output_lines.append('Redo data:')
		output_lines.append(hexdump(redo_data))
		output_lines.append('')
		output_lines.append('Undo data:')
		output_lines.append(hexdump(undo_data))

		output_lines.append('')

		if redo_op == LogFile.DeallocateFileRecordSegment:
			target_number = log_record.calculate_mft_target_number()
			if target_number is None:
				target_number = 'unknown'

			deleted_files.append(str(target_number))

		if redo_op == LogFile.InitializeFileRecordSegment:
			frs_size = log_record.get_target_block_size() * 512
			if frs_size == 0:
				frs_size = 1024

			frs_buf = redo_data + (b'\x00' * (frs_size - len(redo_data)))

			try:
				frs = MFT.FileRecordSegment(frs_buf, False)
			except MFT.MasterFileTableException:
				pass
			else:
				try:
					for frs_attr in frs.attributes():
						if type(frs_attr) is MFT.AttributeRecordNonresident:
							continue

						frs_attr_val = frs_attr.value_decoded()
						if type(frs_attr_val) is Attributes.StandardInformation:
							output_lines.append('$STANDARD_INFORMATION:')
							output_lines.append(' * M timestamp: {}'.format(format_timestamp(frs_attr_val.get_mtime())))
							output_lines.append(' * A timestamp: {}'.format(format_timestamp(frs_attr_val.get_atime())))
							output_lines.append(' * C timestamp: {}'.format(format_timestamp(frs_attr_val.get_ctime())))
							output_lines.append(' * E timestamp: {}'.format(format_timestamp(frs_attr_val.get_etime())))
							output_lines.append(' * File attributes: {}'.format(Attributes.ResolveFileAttributes(frs_attr_val.get_file_attributes())))
							output_lines.append('')

						elif type(frs_attr_val) is Attributes.FileName:
							output_lines.append('$FILE_NAME:')
							output_lines.append(' * M timestamp: {}'.format(format_timestamp(frs_attr_val.get_mtime())))
							output_lines.append(' * A timestamp: {}'.format(format_timestamp(frs_attr_val.get_atime())))
							output_lines.append(' * C timestamp: {}'.format(format_timestamp(frs_attr_val.get_ctime())))
							output_lines.append(' * E timestamp: {}'.format(format_timestamp(frs_attr_val.get_etime())))
							output_lines.append(' * File name: {}'.format(frs_attr_val.get_file_name()))

							parent_reference = frs_attr_val.get_parent_directory()
							output_lines.append(' * Parent (file reference number): {}'.format(parent_reference))

							fr_number, fr_sequence = MFT.DecodeFileRecordSegmentReference(parent_reference)

							try:
								file_record = mft_file.get_file_record_by_number(fr_number, fr_sequence)
								file_paths = mft_file.build_full_paths(file_record)
							except MFT.MasterFileTableException:
								fr_file_path = None
							else:
								if len(file_paths) > 0:
									fr_file_path = file_paths[0]
								else:
									fr_file_path = None

							if fr_file_path is not None:
								output_lines.append(' * Parent path (from $MFT): {}'.format(fr_file_path))

							output_lines.append('')

						elif type(frs_attr_val) is Attributes.ObjectID:
							output_lines.append('$OBJECT_ID:')
							output_lines.append(' * GUID: {}'.format(frs_attr_val.get_object_id()))
							output_lines.append(' * Timestamp: {}'.format(format_timestamp(frs_attr_val.get_timestamp())))
							output_lines.append('')

				except MFT.MasterFileTableException:
					pass

		if redo_op == LogFile.CreateAttribute or undo_op == LogFile.CreateAttribute or redo_op == LogFile.WriteEndOfFileRecordSegment or undo_op == LogFile.WriteEndOfFileRecordSegment:
			if redo_op == LogFile.CreateAttribute:
				attr_buf = redo_data
			elif undo_op == LogFile.CreateAttribute:
				attr_buf = undo_data
			else:
				if len(redo_data) > len(undo_data):
					attr_buf = redo_data
				else:
					attr_buf = undo_data

			if len(attr_buf) >= 24:
				type_code, record_length, form_code, name_length, name_offset, flags, instance = MFT.UnpackAttributeRecordPartialHeader(attr_buf[0 : 16])
				value_length, value_offset, resident_flags, reserved = MFT.UnpackAttributeRecordRemainingHeaderResident(attr_buf[16 : 24])

				if value_offset > 0 and value_offset % 8 == 0 and value_length > 0:
					attr_value_buf = attr_buf[value_offset : value_offset + value_length]
					if len(attr_value_buf) == value_length:
						if type_code == Attributes.ATTR_TYPE_STANDARD_INFORMATION:
							attr_si = Attributes.StandardInformation(attr_value_buf)

							output_lines.append('$STANDARD_INFORMATION:')
							output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_si.get_mtime())))
							output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_si.get_atime())))
							output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_si.get_ctime())))
							output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_si.get_etime())))
							output_lines.append(' * File attributes: {}'.format(Attributes.ResolveFileAttributes(attr_si.get_file_attributes())))
							output_lines.append('')

						elif type_code == Attributes.ATTR_TYPE_FILE_NAME:
							attr_fn = Attributes.FileName(attr_value_buf)

							output_lines.append('$FILE_NAME:')
							output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_fn.get_mtime())))
							output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_fn.get_atime())))
							output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_fn.get_ctime())))
							output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_fn.get_etime())))
							output_lines.append(' * File name: {}'.format(attr_fn.get_file_name()))

							parent_reference = attr_fn.get_parent_directory()
							output_lines.append(' * Parent (file reference number): {}'.format(parent_reference))

							fr_number, fr_sequence = MFT.DecodeFileRecordSegmentReference(parent_reference)

							try:
								file_record = mft_file.get_file_record_by_number(fr_number, fr_sequence)
								file_paths = mft_file.build_full_paths(file_record)
							except MFT.MasterFileTableException:
								fr_file_path = None
							else:
								if len(file_paths) > 0:
									fr_file_path = file_paths[0]
								else:
									fr_file_path = None

							if fr_file_path is not None:
								output_lines.append(' * Parent path (from $MFT): {}'.format(fr_file_path))

							output_lines.append('')

						elif type_code == Attributes.ATTR_TYPE_OBJECT_ID:
							attr_objid = Attributes.ObjectID(attr_value_buf)

							output_lines.append('$OBJECT_ID:')
							output_lines.append(' * GUID: {}'.format(attr_objid.get_object_id()))
							output_lines.append(' * Timestamp: {}'.format(format_timestamp(attr_objid.get_timestamp())))
							output_lines.append('')

		if offset_in_target is not None and (redo_op == LogFile.UpdateResidentValue or undo_op == LogFile.UpdateResidentValue):
			frs_size = log_record.get_target_block_size() * 512
			if frs_size == 0:
				frs_size = 1024

			if frs_size == 1024 or frs_size == 4096:
				si_attr_offset = 56 + 24
				if frs_size == 4096:
					si_attr_offset = 72 + 24

				if offset_in_target >= si_attr_offset and offset_in_target <= si_attr_offset + 32:
					buf = redo_data
					if len(buf) >= 8:
						attr_si = Attributes.StandardInformationPartial(buf, offset_in_target - si_attr_offset)

						output_lines.append('Possible update to $STANDARD_INFORMATION (redo data):')
						output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_si.get_mtime())))
						output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_si.get_atime())))
						output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_si.get_ctime())))
						output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_si.get_etime())))
						output_lines.append('')

					buf = undo_data
					if len(buf) >= 8:
						attr_si = Attributes.StandardInformationPartial(buf, offset_in_target - si_attr_offset)

						output_lines.append('Possible update to $STANDARD_INFORMATION (undo data):')
						output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_si.get_mtime())))
						output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_si.get_atime())))
						output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_si.get_ctime())))
						output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_si.get_etime())))
						output_lines.append('')

		if redo_op == LogFile.AddIndexEntryRoot or redo_op == LogFile.AddIndexEntryAllocation or redo_op == LogFile.WriteEndOfIndexBuffer or undo_op == LogFile.AddIndexEntryRoot or undo_op == LogFile.AddIndexEntryAllocation or undo_op == LogFile.WriteEndOfIndexBuffer:
			if redo_op == LogFile.AddIndexEntryRoot or redo_op == LogFile.AddIndexEntryAllocation or redo_op == LogFile.WriteEndOfIndexBuffer:
				index_entry = Attributes.IndexEntry(redo_data)
			else:
				index_entry = Attributes.IndexEntry(undo_data)

			attr_value_buf = index_entry.get_attribute()
			if attr_value_buf is not None and len(attr_value_buf) > 66:
				attr_fn = Attributes.FileName(attr_value_buf)

				output_lines.append('$FILE_NAME in index:')
				try:
					output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_fn.get_mtime())))
					output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_fn.get_atime())))
					output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_fn.get_ctime())))
					output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_fn.get_etime())))
					output_lines.append(' * File name: {}'.format(attr_fn.get_file_name()))
				except (ValueError, OverflowError):
					pass

				parent_reference = attr_fn.get_parent_directory()
				output_lines.append(' * Parent (file reference number): {}'.format(parent_reference))

				fr_number, fr_sequence = MFT.DecodeFileRecordSegmentReference(parent_reference)

				try:
					file_record = mft_file.get_file_record_by_number(fr_number, fr_sequence)
					file_paths = mft_file.build_full_paths(file_record)
				except MFT.MasterFileTableException:
					fr_file_path = None
				else:
					if len(file_paths) > 0:
						fr_file_path = file_paths[0]
					else:
						fr_file_path = None

				if fr_file_path is not None:
					output_lines.append(' * Parent path (from $MFT): {}'.format(fr_file_path))

				output_lines.append('')

		if redo_op == LogFile.UpdateFileNameRoot or redo_op == LogFile.UpdateFileNameAllocation or undo_op == LogFile.UpdateFileNameRoot or undo_op == LogFile.UpdateFileNameAllocation:
			if redo_op == LogFile.UpdateFileNameRoot or redo_op == LogFile.UpdateFileNameAllocation:
				if len(redo_data) == 56:
					attr_di = Attributes.DuplicatedInformation(redo_data)

					output_lines.append('Update to duplicated information (redo data):')
					output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_di.get_mtime())))
					output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_di.get_atime())))
					output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_di.get_ctime())))
					output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_di.get_etime())))
					output_lines.append('')

			if undo_op == LogFile.UpdateFileNameRoot or undo_op == LogFile.UpdateFileNameAllocation:
				if len(undo_data) == 56:
					attr_di = Attributes.DuplicatedInformation(undo_data)

					output_lines.append('Update to duplicated information (undo data):')
					output_lines.append(' * M timestamp: {}'.format(format_timestamp(attr_di.get_mtime())))
					output_lines.append(' * A timestamp: {}'.format(format_timestamp(attr_di.get_atime())))
					output_lines.append(' * C timestamp: {}'.format(format_timestamp(attr_di.get_ctime())))
					output_lines.append(' * E timestamp: {}'.format(format_timestamp(attr_di.get_etime())))
					output_lines.append('')

		if target_attribute_name == '$J':
			usn_data_1 = None
			usn_data_2 = None

			if redo_op == LogFile.UpdateNonresidentValue:
				usn_data_1 = redo_data
			if undo_op == LogFile.UpdateNonresidentValue:
				usn_data_2 = undo_data

			for usn_data in [ usn_data_1, usn_data_2 ]:
				if usn_data is not None:
					try:
						usn_record = USN.GetUsnRecord(usn_data)
					except (NotImplementedError, ValueError):
						pass
					else:
						if type(usn_record) is USN.USN_RECORD_V4:
							output_lines.append('USN record (version 4):')
						else:
							output_lines.append('USN record:')

						output_lines.append(' Number: {}'.format(usn_record.get_usn()))
						output_lines.append(' Source: {}'.format(USN.ResolveSourceCodes(usn_record.get_source_info())))
						output_lines.append(' Reason: {}'.format(USN.ResolveReasonCodes(usn_record.get_reason())))
						output_lines.append(' File reference number: {}'.format(usn_record.get_file_reference_number()))
						output_lines.append(' Parent file reference number: {}'.format(usn_record.get_parent_file_reference_number()))

						if type(usn_record) is USN.USN_RECORD_V2_OR_V3:
							output_lines.append(' Timestamp: {}'.format(format_timestamp(usn_record.get_timestamp())))
							output_lines.append(' File name: {}'.format(usn_record.get_file_name()))

						output_lines.append('')

		return '\n'.join(output_lines)


	log_file = LogFile.LogFileParser(input_file)
	mft_file = MFT.MasterFileTableParser(input_mft)

	deleted_files = []

	for log_item in log_file.parse_ntfs_records():
		if type(log_item) is LogFile.NTFSRestartArea:
			output_data = process_restart_area(log_item)
		elif type(log_item) is LogFile.NTFSLogRecord:
			output_data = process_log_record(log_item)

		output_file.write(output_data)
		output_file.write('\n\n---\n\n')

	if len(deleted_files) > 0:
		output_file.write('Files and directories with these MFT numbers were deleted (in this order):\n')
		deleted_files = ' '.join(deleted_files)
		output_file.write(deleted_files)
		output_file.write('\n\n---\n\n')

def process_indx():
	global input_file, input_offset, csv_writer

	fs = MFT.FileSystemParser(input_file, input_offset)
	mft = MFT.MasterFileTableParser(fs)

	csv_writer.writerow(['Source', 'MFT reference number', 'Is directory', 'Path', '$FN M timestamp', '$FN A timestamp', '$FN C timestamp', '$FN E timestamp', 'File size'])

	for file_record in mft.file_records():

		# Parse this file record.

		try:
			file_paths = mft.build_full_paths(file_record, True)
		except MFT.MasterFileTableException:
			continue

		parent_mft_number = file_record.get_master_file_table_number()
		parent_sequence_number = file_record.get_sequence_number()

		# Locate the $I30 attribute and parse it.

		for attribute in file_record.attributes(True):
			if type(attribute) is MFT.AttributeRecordResident:
				continue

			if attribute.name == '$I30' and attribute.type_code == Attributes.ATTR_TYPE_INDEX_ALLOCATION:
				i30 = attribute.value_decoded(input_file, input_offset, fs.cluster_size)

				for index_buffer in i30.index_buffers():
					for index_entry in index_buffer.index_entries():
						attr_file_name_raw = index_entry.get_attribute()
						if attr_file_name_raw is None or len(attr_file_name_raw) <= 66:
							continue

						attr_file_name = Attributes.FileName(attr_file_name_raw)

						if attr_file_name.get_parent_directory() != MFT.EncodeFileRecordSegmentReference(parent_mft_number, parent_sequence_number):
							# This index entry points to a different parent for some reason.
							try:
								real_parent_file_record = mft.get_file_record_by_number(parent_mft_number, parent_sequence_number)
								parent_file_paths = mft.build_full_paths(real_parent_file_record)
							except MFT.MasterFileTableException:
								parent_file_path = None
							else:
								if len(parent_file_paths) > 0:
									parent_file_path = parent_file_paths[0]
								else:
									parent_file_path = None

							if parent_file_path is not None:
								if parent_file_path == '/.':
									parent_file_path = ''

								file_path = MFT.PATH_SEPARATOR.join([parent_file_path, attr_file_name.get_file_name()])
							else:
								file_path = MFT.PATH_SEPARATOR.join(['<Unknown>', attr_file_name.get_file_name()])
						else:
							if len(file_paths) > 0:
								dir_path = file_paths[0][0]
								if dir_path == '/.':
									dir_path = ''

								file_path = MFT.PATH_SEPARATOR.join([dir_path, attr_file_name.get_file_name()])
							else:
								file_path = MFT.PATH_SEPARATOR.join(['<Unknown>', attr_file_name.get_file_name()])

						fr_number = index_entry.get_file_reference()

						if attr_file_name.get_file_attributes() & Attributes.DUP_FILE_NAME_INDEX_PRESENT > 0:
							fr_directory = 'Y'
						else:
							fr_directory = 'N'

						fn_mtime = format_timestamp(attr_file_name.get_mtime())
						fn_atime = format_timestamp(attr_file_name.get_atime())
						fn_ctime = format_timestamp(attr_file_name.get_ctime())
						fn_etime = format_timestamp(attr_file_name.get_etime())

						file_size = attr_file_name.get_file_size()

						csv_writer.writerow(['Index record', fr_number, fr_directory, file_path, fn_mtime, fn_atime, fn_ctime, fn_etime, file_size])

				slack_list = i30.get_slack()
				slack_parser = MFT.SlackSpace(slack_list)
				for attr_file_name in slack_parser.carve():
					if attr_file_name.get_parent_directory() != MFT.EncodeFileRecordSegmentReference(parent_mft_number, parent_sequence_number):
						# This index entry points to a different parent for some reason.
						try:
							real_parent_file_record = mft.get_file_record_by_number(parent_mft_number, parent_sequence_number)
							parent_file_paths = mft.build_full_paths(real_parent_file_record)
						except MFT.MasterFileTableException:
							parent_file_path = None
						else:
							if len(parent_file_paths) > 0:
								parent_file_path = parent_file_paths[0]
							else:
								parent_file_path = None

						if parent_file_path is not None:
							if parent_file_path == '/.':
								parent_file_path = ''

							file_path = MFT.PATH_SEPARATOR.join([parent_file_path, attr_file_name.get_file_name()])
						else:
							file_path = MFT.PATH_SEPARATOR.join(['<Unknown>', attr_file_name.get_file_name()])
					else:
						if len(file_paths) > 0:
							dir_path = file_paths[0][0]
							if dir_path == '/.':
								dir_path = ''

							file_path = MFT.PATH_SEPARATOR.join([dir_path, attr_file_name.get_file_name()])
						else:
							file_path = MFT.PATH_SEPARATOR.join(['<Unknown>', attr_file_name.get_file_name()])

					fr_number = '?'

					if attr_file_name.get_file_attributes() & Attributes.DUP_FILE_NAME_INDEX_PRESENT > 0:
						fr_directory = 'Y'
					else:
						fr_directory = 'N'

					fn_mtime = format_timestamp(attr_file_name.get_mtime())
					fn_atime = format_timestamp(attr_file_name.get_atime())
					fn_ctime = format_timestamp(attr_file_name.get_ctime())
					fn_etime = format_timestamp(attr_file_name.get_etime())

					file_size = attr_file_name.get_file_size()

					csv_writer.writerow(['Index slack', fr_number, fr_directory, file_path, fn_mtime, fn_atime, fn_ctime, fn_etime, file_size])

	fs.close()

def process_all_mft():
	global input_file_volume, input_offset_volume

	fs = MFT.FileSystemParser(input_file_volume, input_offset_volume)

	global input_file

	input_file = fs
	process_mft('main volume')

	try:
		vss = ShadowCopy.ShadowParser(input_file_volume, input_offset_volume)
	except ShadowCopy.ShadowCopiesDisabledException:
		pass
	else:
		for shadow_copy in vss.shadows():
			vss.select_shadow(shadow_copy.store_guid)
			input_file = MFT.FileSystemParser(vss, 0)
			process_mft('shadow copy {}'.format(shadow_copy.stack_position))

def process_mem():
	global input_file, csv_writer

	carver = MFT.MetadataCarver(input_file)

	csv_writer.writerow(['Possible MFT reference number', 'M timestamp', 'A timestamp', 'C timestamp', 'E timestamp', 'A timestamp (real)'])

	for fcb_timestamps in carver.carve_fcb_timestamps():
		csv_writer.writerow([fcb_timestamps.possible_file_reference, fcb_timestamps.mtime, fcb_timestamps.atime, fcb_timestamps.ctime, fcb_timestamps.etime, fcb_timestamps.atime_real])

def process_movetable():
	global input_file, output_file

	parser = MoveTable.MoveTableParser(input_file)

	extended_header = parser.get_header().get_extended_header()

	output_lines = []

	output_lines.append('File header:')
	output_lines.append(' * Machine ID: {}'.format(extended_header.machine_id))
	output_lines.append(' * Volume object ID: {}'.format(extended_header.volume_object_id))

	output_data = '\n'.join(output_lines)
	output_file.write(output_data)
	output_file.write('\n\n---\n\n')

	for log_entry in parser.log_entries():
		output_lines = []

		obj_id = log_entry.get_object_id()
		output_lines.append('Object ID: {}'.format(obj_id))
		output_lines.append('')

		droid_volume, droid_file = log_entry.get_droid()
		output_lines.append('Domain-relative object ID, volume: {}'.format(droid_volume))
		output_lines.append('Domain-relative object ID, file: {}'.format(droid_file))
		output_lines.append('')

		machine_id = log_entry.get_machine_id()
		output_lines.append('Machine ID: {}'.format(machine_id))
		output_lines.append('')

		birth_droid_volume, birth_droid_file = log_entry.get_birth_droid()
		output_lines.append('Birth domain-relative object ID, volume: {}'.format(birth_droid_volume))
		output_lines.append('Birth domain-relative object ID, file: {}'.format(birth_droid_file))
		output_lines.append('')

		object_id_timestamp, droid_object_timestamp, birth_droid_object_timestamp = log_entry.get_guid_timestamps()
		output_lines.append('GUID timestamps:')
		output_lines.append(' * Object ID: {}'.format(format_timestamp(object_id_timestamp)))
		output_lines.append(' * Domain-relative object ID, file: {}'.format(format_timestamp(droid_object_timestamp)))
		output_lines.append(' * Birth domain-relative object ID, file: {}'.format(format_timestamp(birth_droid_object_timestamp)))
		output_lines.append('')

		timestamp_min, timestamp_max = log_entry.get_timestamp()
		output_lines.append('Timestamp (range): {} - {}'.format(format_timestamp(timestamp_min), format_timestamp(timestamp_max)))

		output_data = '\n'.join(output_lines)
		output_file.write(output_data)
		output_file.write('\n\n---\n\n')

def print_usage():
	print('Extract information from NTFS metadata files, volumes, and shadow copies')
	print('')
	print('Usage:')
	print(' ntfs_parser --mft <input file ($MFT)> <output file (CSV)>')
	print(' ntfs_parser --usn <input file ($MFT)> <input file ($UsnJrnl:$J)> <output file (CSV)>')
	print(' ntfs_parser --log <input file ($MFT)> <input file ($LogFile)> <output file (TXT)>')
	print(' ntfs_parser --indx <input file (raw image)> <volume offset (in bytes)> <output file (CSV)>')
	print(' ntfs_parser --all-mft <input file (raw image)> <volume offset (in bytes)> <output file (CSV)>')
	print(' ntfs_parser --mem <input file (raw memory image)> <output file (CSV)>')
	print(' ntfs_parser --move <input file (tracking.log)> <output file (TXT)>')

if len(sys.argv) != 4 and len(sys.argv) != 5:
	print_usage()
	sys.exit(0)

mode = sys.argv[1]
if mode not in [ '--mft', '--usn', '--log', '--indx', '--all-mft', '--mem', '--move' ]:
	print_usage()
	sys.exit(0)

if mode in [ '--mft', '--mem', '--move' ] and len(sys.argv) != 4:
	print_usage()
	sys.exit(0)

if mode in [ '--usn', '--log', '--indx', '--all-mft' ] and len(sys.argv) != 5:
	print_usage()
	sys.exit(0)

if mode == '--mft':
	input_file = open(sys.argv[2], 'rb')
	output_file = open(sys.argv[3], 'w', newline = '', encoding = 'utf-8')
	csv_writer = csv.writer(output_file, dialect = 'excel')

	process_mft()
elif mode == '--usn':
	input_mft = open(sys.argv[2], 'rb')
	input_file = open(sys.argv[3], 'rb')
	output_file = open(sys.argv[4], 'w', newline = '', encoding = 'utf-8')
	csv_writer = csv.writer(output_file, dialect = 'excel')

	process_usn()

	input_mft.close()
elif mode == '--log':
	input_mft = open(sys.argv[2], 'rb')
	input_file = open(sys.argv[3], 'rb')
	output_file = open(sys.argv[4], 'w', encoding = 'utf-8')

	process_log()

	input_mft.close()
elif mode == '--indx':
	input_file = open(sys.argv[2], 'rb')
	try:
		input_offset = int(sys.argv[3])
	except ValueError:
		print_usage()
		sys.exit(0)

	output_file = open(sys.argv[4], 'w', newline = '', encoding = 'utf-8')
	csv_writer = csv.writer(output_file, dialect = 'excel')

	process_indx()
elif mode == '--all-mft':
	input_file_volume = open(sys.argv[2], 'rb')
	try:
		input_offset_volume = int(sys.argv[3])
	except ValueError:
		print_usage()
		sys.exit(0)

	output_file = open(sys.argv[4], 'w', newline = '', encoding = 'utf-8')
	csv_writer = csv.writer(output_file, dialect = 'excel')

	process_all_mft()

	input_file_volume.close()
elif mode == '--mem':
	input_file = open(sys.argv[2], 'rb')
	output_file = open(sys.argv[3], 'w', newline = '', encoding = 'utf-8')
	csv_writer = csv.writer(output_file, dialect = 'excel')

	process_mem()
elif mode == '--move':
	input_file = open(sys.argv[2], 'rb')
	output_file = open(sys.argv[3], 'w', encoding = 'utf-8')

	process_movetable()

input_file.close()
output_file.close()
