Skip to content

Commit de96ec9

Browse files
committed
New feature:
- includes executed programs in timeline
1 parent 4631ab3 commit de96ec9

File tree

6 files changed

+192
-6
lines changed

6 files changed

+192
-6
lines changed

CustomLibs/NTUSER_parsing.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from CustomLibs import ShadowCopies
2+
from CustomLibs import time_conversion as TC
3+
import os
4+
import shutil
5+
from Registry import Registry
6+
import codecs
7+
8+
def decode_rot13(string):
9+
return codecs.decode(string, 'rot13')
10+
11+
12+
# copy locked NTUSER.DAT file
13+
def copy_locked_NTUSER(user):
14+
try:
15+
ShadowCopies.create_shadow_copy() # create shadow copy
16+
shadow_copy_path = ShadowCopies.get_latest_shadow_copy() # get latest shadow copy
17+
NTUSER_source = os.path.join(shadow_copy_path, "Users", str(user), "NTUSER.DAT") # NTUSER.DAT source path
18+
destination_path = os.path.join(os.getcwd(), "NTUSER_copy") # destination to copy NTUSER to
19+
20+
shutil.copy(NTUSER_source, destination_path) # copy the NTUSER.DAT file
21+
22+
# delete shadow copy
23+
shadow_ID = ShadowCopies.get_latest_shadow_copy_id() # get shadow copy ID
24+
ShadowCopies.delete_shadow_copy(shadow_ID) # delete shadow copy
25+
except Exception as e:
26+
print("Error: Make sure you are running as administrator.")
27+
28+
# copy NTUSER.DAT file from mounted drive
29+
def copy_mounted_NTUSER(drive, user):
30+
NTUSER_path = f"{drive}[root]\\Users\\{user}\\NTUSER.DAT"
31+
destination_path = os.path.join(os.getcwd(), "NTUSER_copy")
32+
shutil.copy(NTUSER_path, destination_path)
33+
34+
def find_lnk_guid_path(user_assist_key):
35+
# Search for the GUID containing LNK files (".yax" in ROT13)
36+
for GUID in user_assist_key.subkeys():
37+
for Count in GUID.subkeys():
38+
for value in Count.values():
39+
if value.name().endswith(".yax"):
40+
clean_path = (user_assist_key.path()).split("Software", 1)[-1]
41+
return f"Software{clean_path}\\{GUID.name()}\\Count"
42+
return None
43+
44+
def decode_data(data):
45+
if len(data) >= 12:
46+
last_executed_filetime = int.from_bytes(data[60:68], byteorder="little")
47+
return last_executed_filetime
48+
return None
49+
50+
51+
def sanitize_name(name):
52+
if "{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}" in name:
53+
return name.replace("{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}", "{Common Programs}")
54+
elif "{9E3995AB-1F9C-4F13-B827-48B24B6C7174}" in name:
55+
return name.replace("{9E3995AB-1F9C-4F13-B827-48B24B6C7174}", "{User Pinned}")
56+
elif "{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}" in name:
57+
return name.replace("{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}", "{Programs}")
58+
else:
59+
return name
60+
61+
62+
# parse NTUSER.DAT information
63+
def get_user_assist(drive, user):
64+
lnk_guid_list = []
65+
66+
# create NTUSER.DAT copy
67+
if drive == "C:\\":
68+
if not os.path.exists("NTUSER_copy"):
69+
copy_locked_NTUSER(user)
70+
else:
71+
if not os.path.exists("NTUSER_copy"):
72+
copy_mounted_NTUSER(drive, user)
73+
74+
# open registry file and access user assist key
75+
reg = Registry.Registry("NTUSER_copy")
76+
user_assist_key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist"
77+
user_assist_key = reg.open(user_assist_key_path)
78+
79+
# get path with lnk files
80+
lnk_guid = find_lnk_guid_path(user_assist_key)
81+
if lnk_guid is None:
82+
return 0
83+
84+
# collect contents of LNK GUID folder
85+
lnk_guid = reg.open(lnk_guid)
86+
for value in lnk_guid.values():
87+
if (value.name()).endswith(".yax"):
88+
# name full path of program
89+
path = decode_rot13(value.name())
90+
91+
# sanitize names
92+
path = sanitize_name(path)
93+
94+
last_execution = TC.filetime_convert(decode_data(value.value())) # last execution date
95+
96+
# add data to list
97+
if path.endswith(".lnk"):
98+
path = path[:-4]
99+
lnk_guid_list.append(["Executed Program", path, last_execution])
100+
101+
os.remove("NTUSER_copy")
102+
return lnk_guid_list

CustomLibs/ShadowCopies.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import subprocess
2+
import re
3+
4+
# create shadow copy
5+
def create_shadow_copy():
6+
# Create a shadow copy
7+
result = subprocess.run("wmic shadowcopy call create Volume='C:\\'", shell=True, capture_output=True, text=True)
8+
9+
# get latest shadow copy
10+
def get_latest_shadow_copy():
11+
# run "vssadmin list shadows" command to list all shadow copies
12+
result = subprocess.run(['vssadmin', 'list', 'shadows'], stdout=subprocess.PIPE, text=True)
13+
14+
# find all 'Shadow Copy Volume' paths
15+
shadow_copy_paths = re.findall(r'Shadow Copy Volume: (\\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy\d+)',
16+
result.stdout)
17+
18+
if shadow_copy_paths:
19+
latest_shadow_copy = shadow_copy_paths[-1]
20+
return latest_shadow_copy
21+
else:
22+
return None
23+
24+
# get latest_shadow_copy_id
25+
def get_latest_shadow_copy_id():
26+
# Run "vssadmin list shadows" to list all shadow copies
27+
result = subprocess.run(['vssadmin', 'list', 'shadows'], stdout=subprocess.PIPE, text=True)
28+
29+
# Find all 'Shadow Copy ID' entries
30+
shadow_copy_ids = re.findall(r'Shadow Copy ID: ({[a-f0-9\-]+})', result.stdout)
31+
32+
if shadow_copy_ids:
33+
latest_shadow_copy_id = shadow_copy_ids[-1] # Get the last (most recent) Shadow Copy ID
34+
return latest_shadow_copy_id
35+
else:
36+
raise Exception("No shadow copies found.")
37+
38+
# delete shadow copy
39+
def delete_shadow_copy(shadow_copy_ID):
40+
# Run vssadmin delete shadows with the specific shadow copy path
41+
result = subprocess.run(
42+
['vssadmin', 'delete', 'shadows', '/Shadow=' + shadow_copy_ID, "/Quiet"],
43+
stdout=subprocess.PIPE,
44+
stderr=subprocess.PIPE,
45+
text=True
46+
)

CustomLibs/recent_parsing.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from CustomLibs import time_conversion as TC
22
import os
33
from construct import Struct, Int32ul, Int16ul, Int32sl, Bytes
4+
import shutil
45

56
# Define the LNK header structure, which includes file attributes at offset 0x18
67
LNK_HEADER = Struct(
@@ -36,7 +37,7 @@ def get_recent_logs(drive, user):
3637
for file in os.listdir(recent_path):
3738
file_path = os.path.join(recent_path, file)
3839
m_time = TC.convert_unix_epoch_seconds(os.path.getmtime(file_path))
39-
# original_path = parse_lnk_path(file_path)
40+
4041
if os.path.isfile(file_path):
4142
if is_lnk_directory(file_path):
4243
recent_logs.append([f"Opened directory:", file, m_time])
@@ -56,6 +57,8 @@ def is_lnk_directory(lnk_path):
5657

5758
return is_directory
5859

60+
61+
'''
5962
def parse_lnk_path(lnk_path):
6063
with open(lnk_path, "rb") as f:
6164
lnk = LNK_HEADER.parse(f.read(76)) # Read and parse the header
@@ -69,3 +72,4 @@ def parse_lnk_path(lnk_path):
6972
original_path = path_bytes.decode("utf-16le", errors="ignore").rstrip("\x00")
7073
7174
return original_path
75+
'''

CustomLibs/recycle_bin_parsing.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def parse_i_file(file_path):
2929
metadata = [file_name, original_path, deletion_timestamp]
3030
return metadata
3131

32+
# Function to check if a string is mostly readable (ASCII characters only)
33+
def is_readable(s):
34+
# Check if all characters are ASCII and printable
35+
return all(32 <= ord(char) <= 126 for char in s)
36+
3237
# get $Recycle.Bin logs
3338
def get_recycle_logs(drive, user):
3439
RID = SAM_parsing.get_RID(drive, user)
@@ -51,4 +56,9 @@ def get_recycle_logs(drive, user):
5156
file_name, original_path, deletion_date = file_metadata
5257
recycle_logs.append(["Deleted file", original_path, deletion_date])
5358

59+
# remove unreadable content
60+
for log in recycle_logs:
61+
if not is_readable(log[1]):
62+
recycle_logs.remove(log)
63+
5464
return recycle_logs

config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
timezone = None
1+
timezone = "America/New_York"

main.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from CustomLibs import list_functions as LF
33
from CustomLibs import recent_parsing
44
from CustomLibs import recycle_bin_parsing
5+
from CustomLibs import NTUSER_parsing
56
import config
67
import psutil
78
import os
@@ -86,12 +87,25 @@ def main():
8687
drive_list = list_drives()
8788
drive = get_drive(drive_list)
8889
user = get_users(drive)
89-
recent_logs = recent_parsing.get_recent_logs(drive, user)
90-
recycle_logs = recycle_bin_parsing.get_recycle_logs(drive, user)
90+
91+
# gather logs
92+
try:
93+
recent_logs = recent_parsing.get_recent_logs(drive, user)
94+
except Exception:
95+
recent_logs = []
96+
try:
97+
recycle_logs = recycle_bin_parsing.get_recycle_logs(drive, user)
98+
except Exception:
99+
recycle_logs = []
100+
try:
101+
user_assist_logs = NTUSER_parsing.get_user_assist(drive, user)
102+
except Exception:
103+
user_assist_logs = []
91104

92105
# combine logs and sort
93106
all_logs = combine_logs(all_logs, recent_logs)
94107
all_logs = combine_logs(all_logs, recycle_logs)
108+
all_logs = combine_logs(all_logs, user_assist_logs)
95109
all_logs = sorted(all_logs, key=lambda x: x[2])
96110

97111
# get spacing
@@ -100,13 +114,23 @@ def main():
100114
for log in all_logs:
101115
if len(log[1]) > spacing:
102116
spacing = len(log[1])
117+
spacing += 10
118+
119+
# output logs to a file
120+
with open(f"{user} Activity Log.txt", 'w') as file:
121+
file.write(f"{'Activity':<{activity_spacing}}{'File':<{spacing}}Timestamp\n")
122+
file.write("-" * (activity_spacing + spacing + 25) + "\n")
123+
for log in all_logs:
124+
file.write(f"{log[0]:<{activity_spacing}}{log[1]:<{spacing}}{log[2]}\n")
103125

104126
# print logs
105-
print(f"{'Activity':<{spacing}}Timestamp")
106-
print("-" * (spacing + 25))
127+
print(f"{'Activity':<{activity_spacing}}{'File':<{spacing}}Timestamp")
128+
print("-" * (activity_spacing + spacing + 25))
107129
for log in all_logs:
108130
print(f"{log[0]:<{activity_spacing}}{log[1]:<{spacing}}{log[2]}")
109131

132+
print(f"\nLogs saved to '{user} Activity Log.txt'")
133+
110134

111135
if __name__ == "__main__":
112136
main()

0 commit comments

Comments
 (0)