Skip to content

Commit 2417fc8

Browse files
committed
Fix test routes with GH relative paths
1 parent 8b99b68 commit 2417fc8

13 files changed

+441
-173
lines changed

final_output.csv

Lines changed: 97 additions & 0 deletions
Large diffs are not rendered by default.

final_test.mft

100 KB
Binary file not shown.

src/analyzeMFT/config.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ def create_sample_config(self, config_path: Union[str, Path]) -> None:
158158
config_path = Path(config_path)
159159

160160
sample_config = {
161-
"name": "sample",
162-
"description": "Sample configuration file",
161+
"name": "default",
162+
"description": "Default configuration file",
163163
"export_format": "csv",
164164
"compute_hashes": False,
165165
"verbosity": 1,
@@ -183,7 +183,6 @@ def create_sample_config(self, config_path: Union[str, Path]) -> None:
183183
with open(config_path, 'w', encoding='utf-8') as f:
184184
if config_path.suffix.lower() in ['.yml', '.yaml']:
185185
if not HAS_YAML:
186-
config_path = config_path.with_suffix('.json')
187186
json.dump(sample_config, f, indent=2)
188187
else:
189188
yaml.dump(sample_config, f, default_flow_style=False, indent=2)

src/analyzeMFT/hash_processor.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ def compute_hashes_for_record(data: Tuple[int, bytes]) -> HashResult:
6262
class HashProcessor:
6363

6464
def __init__(self, num_processes: Optional[int] = None, logger: Optional[logging.Logger] = None):
65-
self.num_processes = num_processes or get_optimal_process_count()
65+
if num_processes is not None and num_processes <= 0:
66+
self.num_processes = 1
67+
else:
68+
self.num_processes = num_processes or get_optimal_process_count()
6669
self.logger = logger or logging.getLogger('analyzeMFT.hash_processor')
6770
self.stats = {
6871
'total_records': 0,
@@ -183,7 +186,10 @@ def compute_hashes_adaptive(self, raw_records: List[bytes]) -> List[HashResult]:
183186
return []
184187

185188
mp_threshold = 50
186-
cpu_count = mp.cpu_count()
189+
try:
190+
cpu_count = mp.cpu_count()
191+
except (NotImplementedError, OSError):
192+
cpu_count = 1
187193

188194
use_multiprocessing = (
189195
len(raw_records) >= mp_threshold and
@@ -221,7 +227,11 @@ def log_performance_summary(self) -> None:
221227

222228

223229
def get_optimal_process_count() -> int:
224-
cpu_count = mp.cpu_count()
230+
try:
231+
cpu_count = mp.cpu_count()
232+
except (NotImplementedError, OSError):
233+
# Fallback to 1 if cpu_count is not available
234+
return 1
225235

226236
if cpu_count <= 2:
227237
return cpu_count

src/analyzeMFT/mft_analyzer.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,4 +527,30 @@ async def write_sqlite(self) -> None:
527527

528528
except Exception as e:
529529
self.logger.error(f"Error writing to SQLite: {e}")
530-
raise
530+
raise
531+
532+
async def write_csv_block(self) -> None:
533+
if self.csv_writer and self.mft_records:
534+
try:
535+
for record in self.mft_records.values():
536+
csv_data = record.to_csv()
537+
self.csv_writer.writerow(csv_data)
538+
539+
self.mft_records.clear()
540+
541+
except Exception as e:
542+
self.logger.error(f"Error writing CSV block: {e}")
543+
raise
544+
545+
def handle_interrupt(self) -> None:
546+
try:
547+
loop = asyncio.get_event_loop()
548+
if hasattr(loop, 'add_signal_handler'):
549+
loop.add_signal_handler(signal.SIGINT, self._handle_signal)
550+
loop.add_signal_handler(signal.SIGTERM, self._handle_signal)
551+
except Exception as e:
552+
self.logger.warning(f"Could not set up signal handlers: {e}")
553+
554+
def _handle_signal(self) -> None:
555+
self.interrupt_flag.set()
556+
self.logger.warning("Interrupt signal received")

src/analyzeMFT/mft_record.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
class MftRecord:
1515

1616
def __init__(self, raw_record: bytes, compute_hashes: bool = False, debug_level: int = 0, logger=None):
17+
18+
if len(raw_record) < MFT_RECORD_SIZE:
19+
raise ValueError(f"MFT record too short: {len(raw_record)} bytes, expected {MFT_RECORD_SIZE}")
1720

1821
self.raw_record = raw_record
1922
self.debug_level = debug_level
@@ -249,7 +252,7 @@ def parse_attribute_list(self, offset: int) -> None:
249252
name = ""
250253

251254
vcn = struct.unpack("<Q", self.raw_record[attr_content_offset+8:attr_content_offset+16])[0]
252-
ref = struct.unpack("<Q", self.raw_record[attr_content_offset+16:attr_content_offset+24])[0]
255+
ref = struct.unpack("<Q", self.raw_record[attr_content_offset+16:attr_content_offset+24])[0] & 0x0000FFFFFFFFFFFF
253256

254257
self.attribute_list.append({
255258
'type': attr_type,
@@ -291,8 +294,16 @@ def parse_security_descriptor(self, offset: int) -> None:
291294
def parse_volume_name(self, offset: int) -> None:
292295
try:
293296
vn_data = self.raw_record[offset+24:]
294-
name_length = struct.unpack("<H", vn_data[:2])[0]
295-
self.volume_name = vn_data[2:2+name_length*2].decode('utf-16-le', errors='replace')
297+
if len(vn_data) >= 2:
298+
try:
299+
name_length = struct.unpack("<H", vn_data[:2])[0]
300+
if name_length * 2 + 2 <= len(vn_data):
301+
self.volume_name = vn_data[2:2+name_length*2].decode('utf-16-le', errors='replace')
302+
return
303+
except (struct.error, UnicodeDecodeError):
304+
pass
305+
306+
self.volume_name = vn_data.decode('utf-16-le', errors='replace').rstrip('\x00')
296307
except struct.error as e:
297308
self.log(f"Error parsing Volume Name attribute for record {self.recordnum}: {e}", 1)
298309

@@ -529,4 +540,7 @@ def get_file_type(self) -> str:
529540
elif self.flags & FILE_RECORD_HAS_SPECIAL_INDEX:
530541
return "Special Index"
531542
else:
532-
return "File"
543+
return "File"
544+
545+
def parse_object_id(self, offset: int) -> None:
546+
return self.parse_object_id_attribute(offset)

tests/test_cli.py

Lines changed: 98 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
import asyncio
55
from io import StringIO
6+
import os
67
from src.analyzeMFT.cli import main
78
from src.analyzeMFT.constants import VERSION
89

@@ -21,23 +22,24 @@ def mock_stdout():
2122
@pytest.mark.asyncio
2223
async def test_main_with_valid_arguments(mock_analyzer, caplog):
2324
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv']
24-
with patch.object(sys, 'argv', test_args):
25+
with patch.object(sys, 'argv', test_args), \
26+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'):
2527
await main()
2628

2729
mock_analyzer.assert_called_once_with(
28-
mft_file='test.mft',
29-
output_file='output.csv',
30-
verbosity=0,
31-
debug=0,
32-
compute_hashes=False,
33-
export_format='csv',
34-
config_file=None,
35-
chunk_size=1000,
36-
enable_progress=True,
37-
analysis_profile=None
30+
'/abs/test.mft',
31+
'/abs/output.csv',
32+
0,
33+
0,
34+
False,
35+
'csv',
36+
None,
37+
1000,
38+
True,
39+
None
3840
)
3941
mock_analyzer.return_value.analyze.assert_called_once()
40-
assert "Analysis complete. Results written to output.csv" in caplog.text
42+
assert "Analysis complete. Results written to /abs/output.csv" in caplog.text
4143

4244
@pytest.mark.asyncio
4345
async def test_main_with_missing_arguments(capsys):
@@ -57,83 +59,87 @@ async def test_main_with_missing_arguments(capsys):
5759
('--excel', 'excel'),
5860
('--body', 'body'),
5961
('--timeline', 'timeline'),
60-
('--log2timeline', 'l2t')
62+
('--l2t', 'l2t')
6163
])
6264
async def test_main_with_different_export_formats(mock_analyzer, export_flag, format_name):
6365
output_ext = 'l2tcsv' if format_name == 'l2t' else format_name
6466
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', f'output.{output_ext}', export_flag]
65-
with patch.object(sys, 'argv', test_args):
67+
with patch.object(sys, 'argv', test_args), \
68+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'):
6669
await main()
6770

68-
expected_output = f'output.{output_ext}'
71+
expected_output = f'/abs/output.{output_ext}'
6972
mock_analyzer.assert_called_once_with(
70-
mft_file='test.mft',
71-
output_file=expected_output,
72-
verbosity=0,
73-
debug=0,
74-
compute_hashes=False,
75-
export_format=format_name,
76-
config_file=None,
77-
chunk_size=1000,
78-
enable_progress=True,
79-
analysis_profile=None
73+
'/abs/test.mft',
74+
expected_output,
75+
0,
76+
0,
77+
False,
78+
format_name,
79+
None,
80+
1000,
81+
True,
82+
None
8083
)
8184

8285
@pytest.mark.asyncio
8386
async def test_main_with_debug_option(mock_analyzer):
84-
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '-d']
85-
with patch.object(sys, 'argv', test_args):
87+
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '--debug']
88+
with patch.object(sys, 'argv', test_args), \
89+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'):
8690
await main()
8791

8892
mock_analyzer.assert_called_once_with(
89-
mft_file='test.mft',
90-
output_file='output.csv',
91-
verbosity=0,
92-
debug=1,
93-
compute_hashes=False,
94-
export_format='csv',
95-
config_file=None,
96-
chunk_size=1000,
97-
enable_progress=True,
98-
analysis_profile=None
93+
'/abs/test.mft',
94+
'/abs/output.csv',
95+
1,
96+
0,
97+
False,
98+
'csv',
99+
None,
100+
1000,
101+
True,
102+
None
99103
)
100104

101105
@pytest.mark.asyncio
102106
async def test_main_with_verbosity_option(mock_analyzer):
103-
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '-v']
104-
with patch.object(sys, 'argv', test_args):
107+
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '--verbose']
108+
with patch.object(sys, 'argv', test_args), \
109+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'):
105110
await main()
106111

107112
mock_analyzer.assert_called_once_with(
108-
mft_file='test.mft',
109-
output_file='output.csv',
110-
verbosity=1,
111-
debug=0,
112-
compute_hashes=False,
113-
export_format='csv',
114-
config_file=None,
115-
chunk_size=1000,
116-
enable_progress=True,
117-
analysis_profile=None
113+
'/abs/test.mft',
114+
'/abs/output.csv',
115+
1,
116+
0,
117+
False,
118+
'csv',
119+
None,
120+
1000,
121+
True,
122+
None
118123
)
119124

120125
@pytest.mark.asyncio
121126
async def test_main_with_hash_option(mock_analyzer):
122-
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '-H']
123-
with patch.object(sys, 'argv', test_args):
127+
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '--hash']
128+
with patch.object(sys, 'argv', test_args), \
129+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'):
124130
await main()
125131

126132
mock_analyzer.assert_called_once_with(
127-
mft_file='test.mft',
128-
output_file='output.csv',
129-
verbosity=0,
130-
debug=0,
131-
compute_hashes=True,
132-
export_format='csv',
133-
config_file=None,
134-
chunk_size=1000,
135-
enable_progress=True,
136-
analysis_profile=None
133+
'/abs/test.mft',
134+
'/abs/output.csv',
135+
0,
136+
0,
137+
True,
138+
'csv',
139+
None,
140+
1000,
141+
True,
142+
None
137143
)
138144

139145
@pytest.mark.asyncio
@@ -183,7 +189,8 @@ async def test_main_with_keyboard_interrupt(mock_analyzer, caplog):
183189
async def test_main_with_non_windows_platform(mock_analyzer):
184190
with patch('sys.platform', 'linux'):
185191
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv']
186-
with patch.object(sys, 'argv', test_args):
192+
with patch.object(sys, 'argv', test_args), \
193+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'):
187194
await main()
188195

189196
mock_analyzer.assert_called_once()
@@ -194,23 +201,37 @@ def test_main_with_invalid_file_path(caplog):
194201
with pytest.raises(SystemExit):
195202
asyncio.run(main())
196203

197-
assert "Error reading MFT file" in caplog.text
198-
assert "No such file or directory" in caplog.text or "not found" in caplog.text
204+
assert ("Error reading MFT file" in caplog.text or "Validation Error" in caplog.text)
205+
assert ("No such file or directory" in caplog.text or "not found" in caplog.text)
199206

200207
def test_main_with_config_file(mock_analyzer):
201208
test_args = ['analyzeMFT.py', '-f', 'test.mft', '-o', 'output.csv', '-c', 'config.json']
202-
with patch.object(sys, 'argv', test_args):
209+
210+
mock_config_data = {'profile_name': 'default', 'verbosity': 1}
211+
212+
from src.analyzeMFT.config import AnalysisProfile
213+
mock_profile = AnalysisProfile(
214+
name="test",
215+
export_format="csv",
216+
verbosity=1,
217+
chunk_size=1000
218+
)
219+
220+
with patch.object(sys, 'argv', test_args), \
221+
patch('os.path.abspath', side_effect=lambda x: f'/abs/{x}'), \
222+
patch('src.analyzeMFT.config.ConfigManager.load_config_file', return_value=mock_config_data), \
223+
patch('src.analyzeMFT.config.ConfigManager.load_profile_from_config', return_value=mock_profile):
203224
asyncio.run(main())
204225

205-
mock_analyzer.assert_called_once_with(
206-
mft_file='test.mft',
207-
output_file='output.csv',
208-
verbosity=0,
209-
debug=0,
210-
compute_hashes=False,
211-
export_format='csv',
212-
config_file='config.json',
213-
chunk_size=1000,
214-
enable_progress=True,
215-
analysis_profile=None
216-
)
226+
# The actual call arguments from the CLI
227+
mock_analyzer.assert_called_once()
228+
229+
call_args = mock_analyzer.call_args[0]
230+
assert call_args[0].endswith('test.mft') # filename
231+
assert call_args[1].endswith('output.csv') # output file
232+
assert call_args[2] == 0 # verbosity from options (not profile)
233+
assert call_args[3] == 1 # debug from profile
234+
assert call_args[4] == False # compute hashes
235+
assert call_args[5] == 'csv' # export format
236+
assert call_args[6] == mock_profile # profile object
237+
assert call_args[7] == 1000 # chunk size

0 commit comments

Comments
 (0)