#!/usr/bin/env python3 """Windows ShellBag artifact analysis agent. Parses ShellBag registry artifacts to reconstruct folder access history, directory browsing patterns, and evidence of accessed network shares. """ import os import sys import json import struct import hashlib import datetime from collections import defaultdict try: import Registry HAS_REGISTRY = True except ImportError: try: from regipy.registry import RegistryHive HAS_REGIPY = True HAS_REGISTRY = False except ImportError: HAS_REGISTRY = False HAS_REGIPY = False SHELLBAG_PATHS = { 'ntuser': [ 'Software\Microsoft\Windows\Shell\BagMRU', 'Software\Microsoft\Windows\Shell\Bags', 'Software\Microsoft\Windows\ShellNoRoam\BagMRU', 'Software\Microsoft\Windows\ShellNoRoam\Bags', ], 'usrclass': [ 'Local Settings\Software\Microsoft\Windows\Shell\BagMRU', 'Local Settings\Software\Microsoft\Windows\Shell\Bags', ], } def filetime_to_datetime(filetime): if not filetime or filetime == 0: return None try: epoch = datetime.datetime(1601, 1, 1) delta = datetime.timedelta(microseconds=filetime // 10) return (epoch + delta).isoformat() + 'Z' except (OverflowError, OSError): return None def parse_shell_item(data): if len(data) < 2: return None item_size = struct.unpack_from(' len(data): return None item_type = data[2] if len(data) > 2 else 0 result = {'size': item_size, 'type': hex(item_type)} if item_type == 0x1F: result['class'] = 'Root Folder (GUID)' if len(data) >= 18: guid = data[4:20].hex() result['guid'] = guid elif item_type in (0x31, 0x32, 0x35): result['class'] = 'File Entry' if len(data) > 14: file_size = struct.unpack_from(' name_offset: result['short_name'] = data[name_offset:name_end].decode('ascii', errors='replace') elif item_type in (0x41, 0x42, 0x46, 0x47): result['class'] = 'Network Location' if len(data) > 5: name_start = 5 name_end = data.find(b'', name_start) if name_end > name_start: result['network_path'] = data[name_start:name_end].decode('ascii', errors='replace') elif item_type == 0x71: result['class'] = 'Control Panel' else: result['class'] = 'Unknown' return result def parse_bagmru_value(data): items = [] offset = 0 while offset < len(data) - 2: item_size = struct.unpack_from('') print(f' regipy available: {HAS_REGIPY if not HAS_REGISTRY else False}') print(f' python-registry available: {HAS_REGISTRY}') sys.exit(0) print(f' [*] Analyzing: {target}') entries = analyze_shellbags_regipy(target) print(f'[*] ShellBag entries: {len(entries)}') print(' --- Folder Access History ---') for e in entries[:20]: name = e.get('short_name', e.get('network_path', e.get('guid', '?'))) print(f' [{e["class"]:20s}] {name}') findings = detect_suspicious_paths(entries) print(f' --- Suspicious Paths ({len(findings)}) ---') for f in findings[:10]: print(f' [{f["severity"]}] {f["indicator"]}: {f["path"]}')