Skip to content

Commit a965cf9

Browse files
authored
Merge pull request #128 from rowingdude/devel_3.0.3
Devel_3.0.3
2 parents 29821c1 + cef6594 commit a965cf9

File tree

5 files changed

+165
-52
lines changed

5 files changed

+165
-52
lines changed

src/analyzeMFT/cli.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
11
import asyncio
2-
from optparse import OptionParser
2+
from optparse import OptionParser, OptionGroup
33
import sys
44
from .mft_analyzer import MftAnalyzer
55
from .constants import VERSION
66

77
async def main():
8-
parser = OptionParser(usage="usage: %prog -f <mft_file> -o <output.csv> [-d] [-H]",
8+
parser = OptionParser(usage="usage: %prog -f <mft_file> -o <output_file> [options]",
99
version=f"%prog {VERSION}")
1010
parser.add_option("-f", "--file", dest="filename",
1111
help="MFT file to analyze", metavar="FILE")
12-
parser.add_option("-o", "--output", dest="csvfile",
13-
help="Output CSV file", metavar="FILE")
12+
parser.add_option("-o", "--output", dest="output_file",
13+
help="Output file", metavar="FILE")
14+
15+
export_group = OptionGroup(parser, "Export Options")
16+
export_group.add_option("--csv", action="store_const", const="csv", dest="export_format",
17+
help="Export as CSV (default)")
18+
export_group.add_option("--json", action="store_const", const="json", dest="export_format",
19+
help="Export as JSON")
20+
export_group.add_option("--xml", action="store_const", const="xml", dest="export_format",
21+
help="Export as XML")
22+
export_group.add_option("--excel", action="store_const", const="excel", dest="export_format",
23+
help="Export as Excel")
24+
export_group.add_option("--body", action="store_const", const="body", dest="export_format",
25+
help="Export as body file (for mactime)")
26+
export_group.add_option("--timeline", action="store_const", const="timeline", dest="export_format",
27+
help="Export as TSK timeline")
28+
export_group.add_option("--l2t", action="store_const", const="l2t", dest="export_format",
29+
help="Export as log2timeline CSV")
30+
parser.add_option_group(export_group)
31+
1432
parser.add_option("-d", "--debug", action="store_true", dest="debug",
1533
help="Enable debug output", default=False)
1634
parser.add_option("-H", "--hash", action="store_true", dest="compute_hashes",
1735
help="Compute hashes (MD5, SHA256, SHA512, CRC32)", default=False)
1836

1937
(options, args) = parser.parse_args()
2038

21-
if not options.filename or not options.csvfile:
39+
if not options.filename or not options.output_file:
2240
parser.print_help()
2341
sys.exit(1)
2442

25-
analyzer = MftAnalyzer(options.filename, options.csvfile, options.debug, options.compute_hashes)
43+
if not options.export_format:
44+
options.export_format = "csv" # Default to CSV if no format specified
45+
46+
analyzer = MftAnalyzer(options.filename, options.output_file, options.debug, options.compute_hashes, options.export_format)
2647
await analyzer.analyze()
27-
print(f"Analysis complete. Results written to {options.csvfile}")
48+
print(f"Analysis complete. Results written to {options.output_file}")
2849

2950
if __name__ == "__main__":
3051
asyncio.run(main())

src/analyzeMFT/file_writers.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import csv
2+
import json
3+
import xml.etree.ElementTree as ET
4+
import asyncio
5+
from typing import List, Dict, Any
6+
from .mft_record import MftRecord
7+
8+
class FileWriters:
9+
@staticmethod
10+
async def write_csv(records: List[MftRecord], output_file: str) -> None:
11+
with open(output_file, 'w', newline='', encoding='utf-8') as csvfile:
12+
writer = csv.writer(csvfile)
13+
writer.writerow(CSV_HEADER)
14+
for record in records:
15+
writer.writerow(record.to_csv())
16+
await asyncio.sleep(0)
17+
18+
@staticmethod
19+
async def write_json(records: List[MftRecord], output_file: str) -> None:
20+
json_data = [record.__dict__ for record in records]
21+
with open(output_file, 'w', encoding='utf-8') as jsonfile:
22+
json.dump(json_data, jsonfile, indent=2, default=str)
23+
await asyncio.sleep(0)
24+
25+
@staticmethod
26+
async def write_xml(records: List[MftRecord], output_file: str) -> None:
27+
root = ET.Element("mft_records")
28+
for record in records:
29+
record_elem = ET.SubElement(root, "record")
30+
for key, value in record.__dict__.items():
31+
ET.SubElement(record_elem, key).text = str(value)
32+
tree = ET.ElementTree(root)
33+
tree.write(output_file, encoding='utf-8', xml_declaration=True)
34+
await asyncio.sleep(0)
35+
36+
@staticmethod
37+
async def write_excel(records: List[MftRecord], output_file: str) -> None:
38+
try:
39+
import openpyxl
40+
except ImportError:
41+
print("openpyxl is not installed. Please install it to use Excel export.")
42+
return
43+
44+
wb = openpyxl.Workbook()
45+
ws = wb.active
46+
ws.append(CSV_HEADER)
47+
for record in records:
48+
ws.append(record.to_csv())
49+
wb.save(output_file)
50+
await asyncio.sleep(0)
51+
52+
@staticmethod
53+
async def write_body(records: List[MftRecord], output_file: str) -> None:
54+
with open(output_file, 'w', encoding='utf-8') as bodyfile:
55+
for record in records:
56+
# Format: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime
57+
bodyfile.write(f"0|{record.filename}|{record.recordnum}|{record.flags:04o}|0|0|"
58+
f"{record.filesize}|{record.fn_times['atime'].unixtime}|"
59+
f"{record.fn_times['mtime'].unixtime}|{record.fn_times['ctime'].unixtime}|"
60+
f"{record.fn_times['crtime'].unixtime}\n")
61+
await asyncio.sleep(0)
62+
63+
@staticmethod
64+
async def write_timeline(records: List[MftRecord], output_file: str) -> None:
65+
with open(output_file, 'w', encoding='utf-8') as timeline:
66+
for record in records:
67+
# Format: Time|Source|Type|User|Host|Short|Desc|Version|Filename|Inode|Notes|Format|Extra
68+
timeline.write(f"{record.fn_times['crtime'].unixtime}|MFT|CREATE|||||{record.filename}|{record.recordnum}||||\n")
69+
timeline.write(f"{record.fn_times['mtime'].unixtime}|MFT|MODIFY|||||{record.filename}|{record.recordnum}||||\n")
70+
timeline.write(f"{record.fn_times['atime'].unixtime}|MFT|ACCESS|||||{record.filename}|{record.recordnum}||||\n")
71+
timeline.write(f"{record.fn_times['ctime'].unixtime}|MFT|CHANGE|||||{record.filename}|{record.recordnum}||||\n")
72+
await asyncio.sleep(0)
73+
74+
@staticmethod
75+
async def write_l2t(records: List[MftRecord], output_file: str) -> None:
76+
with open(output_file, 'w', newline='', encoding='utf-8') as l2tfile:
77+
writer = csv.writer(l2tfile)
78+
writer.writerow(['date', 'time', 'timezone', 'MACB', 'source', 'sourcetype', 'type', 'user', 'host', 'short', 'desc', 'version', 'filename', 'inode', 'notes', 'format', 'extra'])
79+
for record in records:
80+
for time_type, time_obj in record.fn_times.items():
81+
macb = 'M' if time_type == 'mtime' else 'A' if time_type == 'atime' else 'C' if time_type == 'ctime' else 'B'
82+
date_str = time_obj.dt.strftime('%m/%d/%Y') if time_obj.dt else ''
83+
time_str = time_obj.dt.strftime('%H:%M:%S') if time_obj.dt else ''
84+
writer.writerow([
85+
date_str, time_str, 'UTC', macb, 'MFT', 'FILESYSTEM', time_type, '', '', '',
86+
f"{record.filename} {time_type}", '', record.filename, record.recordnum, '', '', ''
87+
])
88+
await asyncio.sleep(0)

src/analyzeMFT/mft_analyzer.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
import io
44
import sys
55
import traceback
6+
from typing import Dict, Set, List, Optional, Any
67
from .constants import *
78
from .mft_record import MftRecord
89

910
class MftAnalyzer:
1011

11-
def __init__(self, mft_file, output_file, debug=False, compute_hashes=False):
12+
def __init__(self, mft_file: str, output_file: str, debug: bool = False, compute_hashes: bool = False, export_format: str = "csv") -> None:
1213
self.mft_file = mft_file
1314
self.output_file = output_file
1415
self.debug = debug
1516
self.compute_hashes = compute_hashes
16-
self.mft_records = {}
17+
self.export_format = export_format
18+
self.mft_records = []
1719
self.interrupt_flag = asyncio.Event()
1820
self.csv_writer = None
1921
self.csvfile = None
@@ -32,30 +34,19 @@ def __init__(self, mft_file, output_file, debug=False, compute_hashes=False):
3234
})
3335

3436

35-
async def analyze(self):
37+
async def analyze(self) -> None:
3638
try:
37-
self.csvfile = io.open(self.output_file, 'w', newline='', encoding='utf-8')
38-
self.csv_writer = csv.writer(self.csvfile)
39-
header = CSV_HEADER.copy()
40-
if self.compute_hashes:
41-
header.extend(['MD5', 'SHA256', 'SHA512', 'CRC32'])
42-
self.csv_writer.writerow(header)
43-
44-
self.handle_interrupt()
4539
await self.process_mft()
46-
40+
await self.write_output()
4741
except Exception as e:
4842
print(f"An unexpected error occurred: {e}")
4943
if self.debug:
5044
traceback.print_exc()
5145
finally:
52-
await self.write_remaining_records()
5346
self.print_statistics()
54-
if self.csvfile:
55-
self.csvfile.close()
56-
print(f"Analysis complete. Results written to {self.output_file}")
5747

58-
async def process_mft(self):
48+
49+
async def process_mft(self) -> None:
5950
try:
6051
with open(self.mft_file, 'rb') as f:
6152
while not self.interrupt_flag.is_set():
@@ -100,7 +91,7 @@ async def process_mft(self):
10091
if self.debug:
10192
traceback.print_exc()
10293

103-
def handle_interrupt(self):
94+
def handle_interrupt(self) -> None:
10495
if sys.platform == "win32":
10596
# Windows-specific interrupt handling
10697
import win32api
@@ -120,7 +111,7 @@ def unix_handler():
120111
getattr(signal, signame),
121112
unix_handler)
122113

123-
async def write_csv_block(self):
114+
async def write_csv_block(self) -> None:
124115
try:
125116
for record in self.mft_records.values():
126117
filepath = self.build_filepath(record)
@@ -143,11 +134,11 @@ async def write_csv_block(self):
143134
traceback.print_exc()
144135

145136

146-
async def write_remaining_records(self):
137+
async def write_remaining_records(self) -> None:
147138
await self.write_csv_block()
148139
self.mft_records.clear()
149140

150-
def build_filepath(self, record):
141+
def build_filepath(self, record: MftRecord) -> str:
151142
path_parts = []
152143
current_record = record
153144
max_depth = 255
@@ -179,7 +170,7 @@ def build_filepath(self, record):
179170

180171
return '\\'.join(path_parts)
181172

182-
def print_statistics(self):
173+
def print_statistics(self) -> None:
183174
print("\nMFT Analysis Statistics:")
184175
print(f"Total records processed: {self.stats['total_records']}")
185176
print(f"Active records: {self.stats['active_records']}")
@@ -191,3 +182,15 @@ def print_statistics(self):
191182
print(f"Unique SHA512 hashes: {len(self.stats['unique_sha512'])}")
192183
print(f"Unique CRC32 hashes: {len(self.stats['unique_crc32'])}")
193184

185+
186+
async def write_output(self) -> None:
187+
if self.export_format == "csv":
188+
await FileWriters.write_csv(self.mft_records, self.output_file)
189+
elif self.export_format == "json":
190+
await FileWriters.write_json(self.mft_records, self.output_file)
191+
elif self.export_format == "xml":
192+
await FileWriters.write_xml(self.mft_records, self.output_file)
193+
elif self.export_format == "excel":
194+
await FileWriters.write_excel(self.mft_records, self.output_file)
195+
else:
196+
print(f"Unsupported export format: {self.export_format}")

0 commit comments

Comments
 (0)