This is basically how I collect physical user logons along with reboots, etc. I dump this shit into a database and deliver via a web-page so folks can do dated searches by workstation or user ID. Works beautifully. You will need the Win32api libraries. I am running this on a Win 2019 server from Python3.7, which I am locked into as I am compiled against WSGI. This focuses on Windows 10 workstations since we moved to the latest and greatest so it will need adjusted for older versions. That will change the game a bit so look up 528/538 if that is the case. Read through the code for ideas on how best to use this in your world.
Python
x
354
354
1
import win32evtlog
2
import win32evtlogutil
3
import win32security
4
import win32net
5
import win32con
6
import winerror
7
import datetime
8
9
'''
10
User logon collections running on Windows 2019 server collecting records on
11
Windows 10 workstations.
12
'''
13
14
def logon_types(evt_type, evt_id):
15
'''
16
This has been fairly solid and I have validated against my
17
workstation usage. If your org allows screen savers add in
18
4801 activated and 4802 deactivated.
19
'''
20
logon_type = None
21
22
if evt_id == 4624:
23
24
# Successful user log on event. I skip evt_type 7 as it is collected in 4800/4801.
25
if evt_type in [2, 11]:
26
value_type='user_logon' # Interactive/cached logon.
27
28
# I skip 4635 as it adds too many unneeded entries.
29
if evt_id == 4647:
30
31
# Successful user log off event.
32
logon_type='user_logoff' # Interactive log off.
33
34
if evt_id == 4800:
35
36
logon_type='wkst_lock' # workstation lock invoked.
37
38
if evt_id == 4801:
39
40
logon_type='wkst_unlock' # workstation unlock invoked.
41
42
return value_type
43
44
def get_logon_events(server, run_delta):
45
'''Get user logons from event log.'''
46
47
format = '%a %b %d %H:%M:%S %Y'
48
print('Received %s, %s' % (server,run_delta))
49
50
rows = []
51
try:
52
53
# ==========================================
54
# START ON SECURITY LOG...
55
# ==========================================
56
57
log='Security'
58
59
#id_filter = [528,534,4611,4624,4634,4647,4672,4800,4801]
60
id_filter = [4800,4801,4624,4634,4647]
61
62
flags = win32evtlog.EVENTLOG_BACKWARDS_READ|win32evtlog.EVENTLOG_SEQUENTIAL_READ
63
64
#This dict converts the event type into a human readable form
65
evt_dict={win32con.EVENTLOG_AUDIT_FAILURE:'EVENTLOG_AUDIT_FAILURE',\
66
win32con.EVENTLOG_AUDIT_SUCCESS:'EVENTLOG_AUDIT_SUCCESS',\
67
win32con.EVENTLOG_INFORMATION_TYPE:'EVENTLOG_INFORMATION_TYPE',\
68
win32con.EVENTLOG_WARNING_TYPE:'EVENTLOG_WARNING_TYPE',\
69
win32con.EVENTLOG_ERROR_TYPE:'EVENTLOG_ERROR_TYPE'}
70
71
#open event log
72
hand=win32evtlog.OpenEventLog(server,log)
73
events=1
74
75
_break = False
76
77
while events:
78
79
if _break: break
80
81
events=win32evtlog.ReadEventLog(hand,flags,0)
82
83
for ev_obj in events:
84
try:
85
86
event_time = ev_obj.TimeGenerated.Format()
87
event_dt = datetime.datetime.strptime(event_time, format)
88
if event_dt < run_delta:
89
_break = True
90
break
91
92
evt_id = str(winerror.HRESULT_CODE(ev_obj.EventID)).strip().lower()
93
evt_id = int(evt_id)
94
if evt_id not in id_filter: continue
95
96
# Get the data elements.
97
cat = str(ev_obj.EventCategory)
98
src = str(ev_obj.SourceName)
99
record = str(ev_obj.RecordNumber)
100
101
evt_type = str(evt_dict[ev_obj.EventType]).strip().lower()
102
103
if 'EVENTLOG_AUDIT_SUCCESS' not in evt_type.upper():
104
continue
105
106
msg = win32evtlogutil.SafeFormatMessage(ev_obj)
107
if not msg: continue
108
ascii_msg = msg.encode('ascii','ignore').decode().strip().lower()
109
110
# Do some filtering if you're picking up noise, like service accounts, test, whatever.
111
user_id = '-'
112
msg_parts = ascii_msg.split(',')
113
action = 0
114
ltype = 0
115
if evt_id == 4624:
116
# LOG ON
117
domain_name = msg_parts[6].strip().lower()
118
user_id = msg_parts[5].strip().lower()
119
ltype = int(msg_parts[8])
120
if 'winlogon.exe' not in msg_parts[17].lower() and 'svchost.exe' not in msg_parts[17].lower(): continue
121
122
elif evt_id == 4634:
123
124
domain_name = msg_parts[-3].strip().lower()
125
user_id = msg_parts[-4].strip().lower()
126
127
ltype = int(msg_parts[-1].replace("'.>",''))
128
129
elif evt_id == 4647:
130
131
domain_name = msg_parts[-2].strip().lower()
132
user_id = msg_parts[-3].strip().lower()
133
134
elif evt_id in [4800,4801]:
135
domain_name = msg_parts[2].strip().lower()
136
user_id = msg_parts[1].strip().lower()
137
ltype = int(msg_parts[4].replace("'.>",''))
138
else:
139
continue
140
141
logon_key = logon_types(ltype, evt_id)
142
if not logon_key: continue
143
144
# Do some filtering if you're picking up noise, like service accounts, test, whatever.
145
if 'MYDOMAIN' not in domain_name.lower():
146
domain_name = 'MYDOMAIN' # CHANGE TO YOUR DOMAIN OR REMOVE THIS!
147
148
if not user_id: continue
149
if '$' in user_id: continue
150
if 'SYSTEM' in user_id.upper(): continue
151
if 'ANONYMOUS LOGON' in user_id.upper(): continue
152
if '-' in user_id.upper(): continue
153
if '%%' in user_id.upper(): continue
154
if 'TEST' in user_id.upper(): continue
155
if 'IMAUSER' in user_id.upper(): continue
156
if 'SERVICE_' in user_id.upper(): continue # Filter service accounts however yours are defined.
157
158
#print(ascii_msg) # Raw message.
159
#print('****', event_dt, evt_id, domain_name, user_id, ltype, logon_key) # Parts.
160
161
# This data gets inserted into a database and needs to be in this format for me.
162
row = {'date':event_dt,'wkst':server,'evt_id':evt_id,'user_id':user_id,'domain':domain_name,'desc':logon_key}
163
if row not in rows: rows.append(row)
164
165
except:
166
pass # Junk, ignore.
167
168
win32evtlog.CloseEventLog(hand) # Clean up...
169
del(hand)
170
except:
171
pass # Can't access, ignore.
172
173
print('%s processed ok, record count %s' % (server,len(rows)))
174
return rows
175
176
def get_shutdown_events(server, run_delta):
177
'''Get user logons from event log.'''
178
179
format = '%a %b %d %H:%M:%S %Y'
180
print('Received %s, %s' % (server,run_delta))
181
182
rows = []
183
try:
184
# ==========================================
185
# START ON SYSTEM LOG...
186
# ==========================================
187
log='System'
188
id_filter = [6005,6006,6008,1074]
189
190
flags = win32evtlog.EVENTLOG_BACKWARDS_READ|win32evtlog.EVENTLOG_SEQUENTIAL_READ
191
192
#This dict converts the event type into a human readable form
193
evt_dict={win32con.EVENTLOG_AUDIT_FAILURE:'EVENTLOG_AUDIT_FAILURE',\
194
win32con.EVENTLOG_AUDIT_SUCCESS:'EVENTLOG_AUDIT_SUCCESS',\
195
win32con.EVENTLOG_INFORMATION_TYPE:'EVENTLOG_INFORMATION_TYPE',\
196
win32con.EVENTLOG_WARNING_TYPE:'EVENTLOG_WARNING_TYPE',\
197
win32con.EVENTLOG_ERROR_TYPE:'EVENTLOG_ERROR_TYPE'}
198
199
#open event log
200
hand=win32evtlog.OpenEventLog(server,log)
201
events=1
202
203
_break = False
204
205
while events:
206
207
if _break: break
208
209
events=win32evtlog.ReadEventLog(hand,flags,0)
210
211
for ev_obj in events:
212
try:
213
reason_key = 'power_off'
214
215
event_time = ev_obj.TimeGenerated.Format()
216
event_dt = datetime.datetime.strptime(event_time, format)
217
if event_dt < run_delta:
218
_break = True
219
break
220
221
# Get the data elements.
222
cat = str(ev_obj.EventCategory)
223
src = str(ev_obj.SourceName)
224
record = str(ev_obj.RecordNumber)
225
evt_id = int(winerror.HRESULT_CODE(ev_obj.EventID))
226
227
if evt_id not in id_filter: continue
228
229
msg = win32evtlogutil.SafeFormatMessage(ev_obj)
230
if not msg: continue
231
232
ascii_msg = msg.encode('ascii','ignore').decode().strip().lower()
233
#Out.debug(ascii_msg)
234
235
if evt_id == 6006:
236
reason_key = 'power_off'
237
elif evt_id == 6008:
238
reason_key = 'power_off'
239
if evt_id == 6005:
240
reason_key = 'power_on'
241
if evt_id == 1074:
242
if 'explorer.exe' not in ascii_msg: continue
243
reason_key = 'restart'
244
245
ascii_msg = ascii_msg.replace(".>",'').replace("'",'')
246
msg_parts = ascii_msg.split(',')
247
248
user_id = '-'
249
if '\\' in msg_parts[-1]:
250
domain_name = msg_parts[-1].split('\\')[0].strip()
251
user_id = msg_parts[-1].split('\\')[-1]
252
else:
253
user_id = msg_parts[-1]
254
255
if 'SOME_DOMAIN' not in domain_name.lower():
256
domain_name = '-'
257
258
user_id = user_id.strip()
259
if user_id.startswith('{'): continue
260
if user_id.startswith('svc'): continue
261
if '-' in user_id: continue
262
263
if 'system' in user_id.lower(): user_id = '-'
264
265
row = {'date':event_dt,'wkst':server,'evt_id':evt_id,'user_id':user_id,'domain':domain_name,'desc':reason_key}
266
if row not in rows: rows.append(row)
267
268
except:
269
pass # Junk, ignore.
270
271
win32evtlog.CloseEventLog(hand) # Clean up...
272
del(hand)
273
except:
274
pass # Not accessable, ignore.
275
276
print('%s processed ok, record count %s' % (server,len(rows)))
277
return rows
278
279
def save_rows(records):
280
if not records:
281
print('No records received.')
282
return False
283
print('Save records here')
284
return True
285
286
if __name__=='__main__':
287
try:
288
ALL_RECORDS = []
289
period_in_hours = 24 # I collect 24 hours of log data each run.
290
291
'''
292
This is a working example only so you might not want to put into production
293
until you have done some cleanup and changes/error handling. This is not the code I run in production,
294
just bits and pieces to provide an example. The big thing to pay attention to are the event IDs,
295
the event type, and the splits to get the details you need. This only focuses on a physical standard
296
user.
297
298
In my production code I load up a collection of successfully pinged user workstations from AD that
299
I store daily. I divide the full list into three separate lists and store on disk. Each night I run
300
three separate logon collection scripts reading in its respective list of computers. about 670 per script.
301
Each script runs threaded, about 40 threads, collecting 24 hours of records for each machine. I get
302
all 2000 machines in less than 4 hours this way and only hit each machine once per night. We used to have a
303
distributed method that wold get all 2000 in less than an hour, but I am scaling back as our current staff
304
do not want to support in-house systems code/environments.
305
306
If you have a lot of machines threaded will speed things up.
307
I offset the tasks by two hours, starting the first at 6 pm. If your machines are not
308
returning all the records you expect to see it is likely the logs are filling up and rotating
309
what you want before you get to it. You may want to change tasks and collections to 8 hours
310
if that is the case.
311
'''
312
works = ['computer1', 'computer2', 'etc...']
313
print('Total wkst count: %s' % len(works))
314
315
# Here is an example on how to devide your master list.
316
works.sort()
317
318
list_count = 3
319
limit = round(len(works) / list_count)
320
321
work_lists = [works[n:n+limit] for n in range(0, len(works), limit)]
322
323
works_group = work_lists[0] # Get the first list to process.
324
325
print('Processing count: %s' % len(works_group))
326
327
# Set the delta for log collection.
328
run_time_dt = datetime.datetime.now()
329
run_delta = run_time_dt - datetime.timedelta(hours=period_in_hours)
330
331
# Loop the workstations and do the collections. I merge this data...
332
for wrk in works_group:
333
# Do logons.
334
recs = get_logon_events(wrk, run_delta)
335
ALL_RECORDS.extend(recs)
336
337
# Do shutdowns.
338
recs = get_shutdown_events(wrk, run_delta)
339
ALL_RECORDS.extend(recs)
340
341
# Save however you please or generate a report.
342
if ALL_RECORDS:
343
print('Saving %s records' % len(ALL_RECORDS))
344
saved = save_rows(ALL_RECORDS)
345
if saved:
346
print('All records saved successfully.')
347
else:
348
print('Could not save records.')
349
else:
350
print('No records to save.')
351
352
print('Finished')
353
except:
354
print('Failed in main!')