1#
2# Copyright (c) 2012-2014, Ashok P. Nadkarni
3# All rights reserved.
4#
5# See the file LICENSE for license
6
7# Event log handling for Vista and later
8
9namespace eval twapi {
10    variable _evt;              # See _evt_init
11
12    # System event fields in order returned by _evt_decode_event_system_fields
13    twapi::record evt_system_fields  {
14        -providername -providerguid -eventid -qualifiers -level -task
15        -opcode -keywordmask -timecreated -eventrecordid -activityid
16        -relatedactivityid -pid -tid -channel
17        -computer -sid -version
18    }
19
20    proc _evt_init {} {
21        variable _evt
22
23        # Various structures that we maintain / cache for efficiency as they
24        # are commonly used are kept in the _evt array with the following keys:
25
26        # system_render_context_handle - is the handle to a rendering
27        #    context for the system portion of an event
28        set _evt(system_render_context_handle) [evt_render_context_system]
29
30        # user_render_context_handle - is the handle to a rendering
31        #    context for the user data portion of an event
32        set _evt(user_render_context_handle) [evt_render_context_user]
33
34        # render_buffer - is NULL or holds a pointer to the buffer used to
35        #    retrieve values so does not have to be reallocated every time.
36        set _evt(render_buffer) NULL
37
38        # publisher_handles - caches publisher names to their meta information.
39        #    This is a dictionary indexed with nested keys -
40        #     publisher, session, lcid. TBD - need a mechanism to clear ?
41        set _evt(publisher_handles) [dict create]
42
43        # -levelname - dict of publisher name / level number to level names
44        set _evt(-levelname) {}
45
46        # -taskname - dict of publisher name / task number to task name
47        set _evt(-taskname) {}
48
49        # -opcodename - dict of publisher name / opcode number to opcode name
50        set _evt(-opcodename) {}
51
52        # No-op the proc once init is done
53        proc _evt_init {} {}
54    }
55}
56
57# TBD - document
58proc twapi::evt_local_session {} {
59    return NULL
60}
61
62# TBD - document
63proc twapi::evt_local_session? {hsess} {
64    return [pointer_null? $hsess]
65}
66
67# TBD - document
68proc twapi::evt_open_session {server args} {
69    array set opts [parseargs args {
70        user.arg
71        domain.arg
72        password.arg
73        {authtype.arg 0}
74    } -nulldefault -maxleftover 0]
75
76    if {![string is integer -strict $opts(authtype)]} {
77        set opts(authtype) [dict get {default 0 negotiate 1 kerberos 2 ntlm 3} [string tolower $opts(authtype)]]
78    }
79
80    return [EvtOpenSession 1 [list $server $opts(user) $opts(domain) $opts(password) $opts(authtype)] 0 0]
81}
82
83# TBD - document
84proc twapi::evt_close_session {hsess} {
85    if {![evt_local_session? $hsess]} {
86        evt_close $hsess
87    }
88}
89
90proc twapi::evt_channels {{hevtsess NULL}} {
91    # TBD - document hevtsess
92    set chnames {}
93    set hevt [EvtOpenChannelEnum $hevtsess 0]
94    trap {
95        while {[set chname [EvtNextChannelPath $hevt]] ne ""} {
96            lappend chnames $chname
97        }
98    } finally {
99        evt_close $hevt
100    }
101
102    return $chnames
103}
104
105proc twapi::evt_clear_log {chanpath args} {
106    # TBD - document -session
107    array set opts [parseargs args {
108        {session.arg NULL}
109        {backup.arg ""}
110    } -maxleftover 0]
111
112    return [EvtClearLog $opts(session) $chanpath [_evt_normalize_path $opts(backup)] 0]
113}
114
115# TBD - document
116proc twapi::evt_archive_exported_log {logpath args} {
117    array set opts [parseargs args {
118        {session.arg NULL}
119        {lcid.int 0}
120    } -maxleftover 0]
121
122    return [EvtArchiveExportedLog $opts(session) [_evt_normalize_path $logpath] $opts(lcid) 0]
123}
124
125proc twapi::evt_export_log {outfile args} {
126    # TBD - document -session
127    array set opts [parseargs args {
128        {session.arg NULL}
129        file.arg
130        channel.arg
131        {query.arg *}
132        {ignorequeryerrors 0 0x1000}
133    } -maxleftover 0]
134
135    if {([info exists opts(file)] && [info exists opts(channel)]) ||
136        ! ([info exists opts(file)] || [info exists opts(channel)])} {
137        error "Exactly one of -file or -channel must be specified."
138    }
139
140    if {[info exists opts(file)]} {
141        set path [_evt_normalize_path $opts(file)]
142        incr opts(ignorequeryerrors) 2
143    } else {
144        set path $opts(channel)
145        incr opts(ignorequeryerrors) 1
146    }
147
148    return [EvtExportLog $opts(session) $path $opts(query) [_evt_normalize_path $outfile] $opts(ignorequeryerrors)]
149}
150
151# TBD - document
152proc twapi::evt_create_bookmark {{mark ""}} {
153    return [EvtCreateBookmark $mark]
154}
155
156# TBD - document
157proc twapi::evt_render_context_xpaths {xpaths} {
158    return [EvtCreateRenderContext $xpaths 0]
159}
160
161# TBD - document
162proc twapi::evt_render_context_system {} {
163    return [EvtCreateRenderContext {} 1]
164}
165
166# TBD - document
167proc twapi::evt_render_context_user {} {
168    return [EvtCreateRenderContext {} 2]
169}
170
171# TBD - document
172proc twapi::evt_open_channel_config {chanpath args} {
173    array set opts [parseargs args {
174        {session.arg NULL}
175    } -maxleftover 0]
176
177    return [EvtOpenChannelConfig $opts(session) $chanpath 0]
178}
179
180# TBD - document
181proc twapi::evt_get_channel_config {hevt args} {
182    set result {}
183    foreach opt $args {
184        lappend result $opt \
185            [EvtGetChannelConfigProperty $hevt \
186                 [_evt_map_channel_config_property $hevt $propid]]
187    }
188    return $result
189}
190
191# TBD - document
192proc twapi::evt_set_channel_config {hevt propid val} {
193    return [EvtSetChannelConfigProperty $hevt [_evt_map_channel_config_property $propid 0 $val]]
194}
195
196
197# TBD - document
198proc twapi::_evt_map_channel_config_property {propid} {
199    if {[string is integer -strict $propid]} {
200        return $propid
201    }
202
203    # Note: values are from winevt.h, Win7 SDK has typos for last few
204    return [dict get {
205        -enabled                  0
206        -isolation                1
207        -type                     2
208        -owningpublisher          3
209        -classiceventlog          4
210        -access                   5
211        -loggingretention         6
212        -loggingautobackup        7
213        -loggingmaxsize           8
214        -logginglogfilepath       9
215        -publishinglevel          10
216        -publishingkeywords       11
217        -publishingcontrolguid    12
218        -publishingbuffersize     13
219        -publishingminbuffers     14
220        -publishingmaxbuffers     15
221        -publishinglatency        16
222        -publishingclocktype      17
223        -publishingsidtype        18
224        -publisherlist            19
225        -publishingfilemax        20
226    } $propid]
227}
228
229# TBD - document
230proc twapi::evt_event_info {hevt args} {
231    set result {}
232    foreach opt $args {
233        lappend result $opt [EvtGetEventInfo $hevt \
234                                 [dict get {-queryids 0 -path 1} $opt]]
235    }
236    return $result
237}
238
239
240# TBD - document
241proc twapi::evt_event_metadata_property {hevt args} {
242    set result {}
243    foreach opt $args {
244        lappend result $opt \
245            [EvtGetEventMetadataProperty $hevt \
246                 [dict get {
247                     -id 0 -version 1 -channel 2 -level 3
248                     -opcode 4 -task 5 -keyword 6 -messageid 7 -template 8
249                 } $opt]]
250    }
251    return $result
252}
253
254
255# TBD - document
256proc twapi::evt_open_log_info {args} {
257    array set opts [parseargs args {
258        {session.arg NULL}
259        file.arg
260        channel.arg
261    } -maxleftover 0]
262
263    if {([info exists opts(file)] && [info exists opts(channel)]) ||
264        ! ([info exists opts(file)] || [info exists opts(channel)])} {
265        error "Exactly one of -file or -channel must be specified."
266    }
267
268    if {[info exists opts(file)]} {
269        set path [_evt_normalize_path $opts(file)]
270        set flags 0x2
271    } else {
272        set path $opts(channel)
273        set flags 0x1
274    }
275
276    return [EvtOpenLog $opts(session) $path $flags]
277}
278
279# TBD - document
280proc twapi::evt_log_info {hevt args} {
281    set result {}
282    foreach opt $args {
283        lappend result $opt  [EvtGetLogInfo $hevt [dict get {
284            -creationtime 0 -lastaccesstime 1 -lastwritetime 2
285            -filesize 3 -attributes 4 -numberoflogrecords 5
286            -oldestrecordnumber 6 -full 7
287        } $opt]]
288    }
289    return $result
290}
291
292# TBD - document
293proc twapi::evt_publisher_metadata_property {hpub args} {
294    set result {}
295    foreach opt $args {
296        set val [EvtGetPublisherMetadataProperty $hpub [dict get {
297            -publisherguid 0  -resourcefilepath 1 -parameterfilepath 2
298            -messagefilepath 3 -helplink 4 -publishermessageid 5
299            -channelreferences 6 -levels 12 -tasks 16
300            -opcodes 21 -keywords 25
301        } $opt] 0]
302        if {$opt ni {-channelreferences -levels -tasks -opcodes -keywords}} {
303            lappend result $opt $val
304            continue
305        }
306        set n [EvtGetObjectArraySize $val]
307        set val2 {}
308        for {set i 0} {$i < $n} {incr i} {
309            set rec {}
310            foreach {opt2 iopt} [dict get {
311                -channelreferences { -channelreferencepath 7
312                    -channelreferenceindex 8 -channelreferenceid 9
313                    -channelreferenceflags 10 -channelreferencemessageid 11}
314                -levels { -levelname 13 -levelvalue 14 -levelmessageid 15 }
315                -tasks { -taskname 17 -taskeventguid 18 -taskvalue 19
316                    -taskmessageid 20}
317                -opcodes {-opcodename 22 -opcodevalue 23 -opcodemessageid 24}
318                -keywords {-keywordname 26 -keywordvalue 27
319                    -keywordmessageid 28}
320            } $opt] {
321                lappend rec $opt2 [EvtGetObjectArrayProperty $val $iopt $i]
322            }
323            lappend val2 $rec
324        }
325
326        evt_close $val
327        lappend result $opt $val2
328    }
329    return $result
330}
331
332# TBD - document
333proc twapi::evt_query_info {hq args} {
334    set result {}
335    foreach opt $args {
336        lappend result $opt  [EvtGetQueryInfo $hq [dict get {
337            -names 1 statuses 2
338        } $opt]]
339    }
340    return $result
341}
342
343# TBD - document
344proc twapi::evt_object_array_size {hevt} {
345    return [EvtGetObjectArraySize $hevt]
346}
347
348# TBD - document
349proc twapi::evt_object_array_property {hevt index args} {
350    set result {}
351
352    foreach opt $args {
353        lappend result $opt \
354            [EvtGetObjectArrayProperty $hevt [dict get {
355                -channelreferencepath 7
356                -channelreferenceindex 8 -channelreferenceid 9
357                -channelreferenceflags 10 -channelreferencemessageid 11
358                -levelname 13 -levelvalue 14 -levelmessageid 15
359                -taskname 17 -taskeventguid 18 -taskvalue 19
360                -taskmessageid 20 -opcodename 22
361                -opcodevalue 23 -opcodemessageid 24
362                -keywordname 26 -keywordvalue 27 -keywordmessageid 28
363            }] $index]
364    }
365    return $result
366}
367
368proc twapi::evt_publishers {{hsess NULL}} {
369    set pubs {}
370    set hevt [EvtOpenPublisherEnum $hsess 0]
371    trap {
372        while {[set pub [EvtNextPublisherId $hevt]] ne ""} {
373            lappend pubs $pub
374        }
375    } finally {
376        evt_close $hevt
377    }
378
379    return $pubs
380}
381
382# TBD - document
383proc twapi::evt_open_publisher_metadata {pub args} {
384    array set opts [parseargs args {
385        {session.arg NULL}
386        logfile.arg
387        lcid.int
388    } -nulldefault -maxleftover 0]
389
390    return [EvtOpenPublisherMetadata $opts(session) $pub $opts(logfile) $opts(lcid) 0]
391}
392
393# TBD - document
394proc twapi::evt_publisher_events_metadata {hpub args} {
395    set henum [EvtOpenEventMetadataEnum $hpub]
396
397    # It is faster to build a list and then have Tcl shimmer to a dict when
398    # required
399    set meta {}
400    trap {
401        while {[set hmeta [EvtNextEventMetadata $henum 0]] ne ""} {
402            lappend meta [evt_event_metadata_property $hmeta {*}$args]
403            evt_close $hmeta
404        }
405    } finally {
406        evt_close $henum
407    }
408
409    return $meta
410}
411
412proc twapi::evt_query {args} {
413    array set opts [parseargs args {
414        {session.arg NULL}
415        file.arg
416        channel.arg
417        {query.arg *}
418        {ignorequeryerrors 0 0x1000}
419        {direction.sym forward {forward 0x100 reverse 0x200 backward 0x200}}
420    } -maxleftover 0]
421
422    if {([info exists opts(file)] && [info exists opts(channel)]) ||
423        ! ([info exists opts(file)] || [info exists opts(channel)])} {
424        error "Exactly one of -file or -channel must be specified."
425    }
426
427    set flags $opts(ignorequeryerrors)
428    incr flags $opts(direction)
429
430    if {[info exists opts(file)]} {
431        set path [_evt_normalize_path $opts(file)]
432        incr flags 0x2
433    } else {
434        set path $opts(channel)
435        incr flags 0x1
436    }
437
438    return [EvtQuery $opts(session) $path $opts(query) $flags]
439}
440
441proc twapi::evt_next {hresultset args} {
442    array set opts [parseargs args {
443        {timeout.int -1}
444        {count.int 1}
445        {status.arg}
446    } -maxleftover 0]
447
448    if {[info exists opts(status)]} {
449        upvar 1 $opts(status) status
450        return [EvtNext $hresultset $opts(count) $opts(timeout) 0 status]
451    } else {
452        return [EvtNext $hresultset $opts(count) $opts(timeout) 0]
453    }
454}
455
456twapi::proc* twapi::_evt_decode_event_system_fields {hevt} {
457    _evt_init
458} {
459    variable _evt
460    set _evt(render_buffer) [Twapi_EvtRenderValues $_evt(system_render_context_handle) $hevt $_evt(render_buffer)]
461    set rec [Twapi_ExtractEVT_RENDER_VALUES $_evt(render_buffer)]
462    return [evt_system_fields set $rec \
463                -providername [atomize [evt_system_fields -providername $rec]] \
464                -providerguid [atomize [evt_system_fields -providerguid $rec]] \
465                -channel [atomize [evt_system_fields -channel $rec]] \
466                -computer [atomize [evt_system_fields -computer $rec]]]
467}
468
469# TBD - document. Returns a list of user data values
470twapi::proc* twapi::evt_decode_event_userdata {hevt} {
471    _evt_init
472} {
473    variable _evt
474    set _evt(render_buffer) [Twapi_EvtRenderValues $_evt(user_render_context_handle) $hevt $_evt(render_buffer)]
475    return [Twapi_ExtractEVT_RENDER_VALUES $_evt(render_buffer)]
476}
477
478twapi::proc* twapi::evt_decode_events {hevts args} {
479    _evt_init
480} {
481    variable _evt
482
483    array set opts [parseargs args {
484        {values.arg NULL}
485        {session.arg NULL}
486        {logfile.arg ""}
487        {lcid.int 0}
488        ignorestring.arg
489        message
490        levelname
491        taskname
492        opcodename
493        keywords
494        xml
495    } -ignoreunknown -hyphenated]
496
497    # SAME ORDER AS _evt_decode_event_system_fields
498    set decoded_fields [evt_system_fields]
499    set decoded_events {}
500
501    # ORDER MUST BE SAME AS order in which values are appended below
502    foreach opt {-levelname -taskname -opcodename -keywords -xml -message} {
503        if {$opts($opt)} {
504            lappend decoded_fields $opt
505        }
506    }
507
508    foreach hevt $hevts {
509        set decoded [_evt_decode_event_system_fields $hevt]
510        # Get publisher from hevt
511        set publisher [evt_system_fields -providername $decoded]
512
513        if {! [dict exists $_evt(publisher_handles) $publisher $opts(-session) $opts(-lcid)]} {
514            if {[catch {
515                dict set _evt(publisher_handles) $publisher $opts(-session) $opts(-lcid) [EvtOpenPublisherMetadata $opts(-session) $publisher $opts(-logfile) $opts(-lcid) 0]
516            }]} {
517                # TBD - debug log
518                dict set _evt(publisher_handles) $publisher $opts(-session) $opts(-lcid) NULL
519            }
520        }
521        set hpub [dict get $_evt(publisher_handles) $publisher $opts(-session) $opts(-lcid)]
522
523        # See if cached values are present for -levelname -taskname
524        # and -opcodename. TBD - can -keywords be added to this ?
525        foreach {intopt opt callflag} {-level -levelname 2 -task -taskname 3 -opcode -opcodename 4} {
526            if {$opts($opt)} {
527                set ival [evt_system_fields $intopt $decoded]
528                if {[dict exists $_evt($opt) $publisher $ival]} {
529                    lappend decoded [dict get $_evt($opt) $publisher $ival]
530                } else {
531                    # Not cached. Look it up. Value of 0 -> null so
532                    # just use ignorestring if specified.
533                    if {$ival == 0 && [info exists opts(-ignorestring)]} {
534                        set optval $opts(-ignorestring)
535                    } else {
536                        if {[info exists opts(-ignorestring)]} {
537                            if {[EvtFormatMessage $hpub $hevt 0 $opts(-values) $callflag optval]} {
538                                dict set _evt($opt) $publisher $ival $optval
539                            } else {
540                                # Note result not cached if not found since
541                                # ignorestring may be different on every call
542                                set optval $opts(-ignorestring)
543                            }
544                        } else {
545                            # -ignorestring not specified so
546                            # will raise error if not found
547                            set optval [EvtFormatMessage $hpub $hevt 0 $opts(-values) $callflag]
548                            dict set _evt($opt) $publisher $ival [atomize $optval]
549                        }
550                    }
551                    lappend decoded $optval
552                }
553            }
554        }
555
556        # Non-cached fields
557        # ORDER MUST BE SAME AS decoded_fields ABOVE
558        foreach {opt callflag} {
559            -keywords 5
560            -xml 9
561        } {
562            if {$opts($opt)} {
563                if {[info exists opts(-ignorestring)]} {
564                    if {! [EvtFormatMessage $hpub $hevt 0 $opts(-values) $callflag optval]} {
565                        set optval $opts(-ignorestring)
566                    }
567                } else {
568                    set optval [EvtFormatMessage $hpub $hevt 0 $opts(-values) $callflag]
569                }
570                lappend decoded $optval
571            }
572        }
573
574        # We treat -message differently because on failure we want
575        # to extract the user data. -ignorestring is not used for this
576        # unless user data extraction also fails
577        if {$opts(-message)} {
578            if {[EvtFormatMessage $hpub $hevt 0 $opts(-values) 1 message]} {
579                lappend decoded $message
580            } else {
581                # TBD - make sure we have a test for this case.
582                # TBD - log
583                if {[catch {
584                    lappend decoded "Message for event could not be found. Event contained user data: [join [evt_decode_event_userdata $hevt] ,]"
585                } message]} {
586                    if {[info exists opts(-ignorestring)]} {
587                        lappend decoded $opts(-ignorestring)
588                    } else {
589                        error $message
590                    }
591                }
592            }
593        }
594
595        lappend decoded_events $decoded
596    }
597
598    return [list $decoded_fields $decoded_events]
599}
600
601proc twapi::evt_decode_event {hevt args} {
602    return [recordarray index [evt_decode_events [list $hevt] {*}$args] 0 -format dict]
603}
604
605# TBD - document
606proc twapi::evt_format_publisher_message {hpub msgid args} {
607
608    array set opts [parseargs args {
609        {values.arg NULL}
610    } -maxleftover 0]
611
612    return [EvtFormatMessage $hpub NULL $msgid $opts(values) 8]
613}
614
615# TBD - document
616# Where is this used?
617proc twapi::evt_free_EVT_VARIANT_ARRAY {p} {
618    evt_free $p
619}
620
621# TBD - document
622# Where is this used?
623proc twapi::evt_free_EVT_RENDER_VALUES {p} {
624    evt_free $p
625}
626
627# TBD - document
628proc twapi::evt_seek {hresults pos args} {
629    array set opts [parseargs args {
630        {origin.arg first {first last current}}
631        bookmark.arg
632        {strict 0 0x10000}
633    } -maxleftover 0]
634
635    if {[info exists opts(bookmark)]} {
636        set flags 4
637    } else {
638        set flags [lsearch -exact {first last current} $opts(origin)]
639        incr flags;             # 1 -> first, 2 -> last, 3 -> current
640        set opts(bookmark) NULL
641    }
642
643    incr flags $opts(strict)
644
645    EvtSeek $hresults $pos $opts(bookmark) 0 $flags
646}
647
648proc twapi::evt_subscribe {path args} {
649    # TBD - document -session and -bookmark and -strict
650    array set opts [parseargs args {
651        {session.arg NULL}
652        {query.arg *}
653        bookmark.arg
654        includeexisting
655        {ignorequeryerrors 0 0x1000}
656        {strict 0 0x10000}
657    } -maxleftover 0]
658
659    set flags [expr {$opts(ignorequeryerrors) | $opts(strict)}]
660    if {[info exists opts(bookmark)]} {
661        set flags [expr {$flags | 3}]
662        set bookmark $opts(origin)
663    } else {
664        set bookmark NULL
665        if {$opts(includeexisting)} {
666            set flags [expr {$flags | 2}]
667        } else {
668            set flags [expr {$flags | 1}]
669        }
670    }
671
672    set hevent [lindex [CreateEvent [_make_secattr {} 0] 0 0 ""] 0]
673    if {[catch {
674        EvtSubscribe $opts(session) $hevent $path $opts(query) $bookmark $flags
675    } hsubscribe]} {
676        set erinfo $::errorInfo
677        set ercode $::errorCode
678        CloseHandle $hevent
679        error $hsubscribe $erinfo $ercode
680    }
681
682    return [list $hsubscribe $hevent]
683}
684
685proc twapi::_evt_normalize_path {path} {
686    # Do not want to rely on [file normalize] returning "" for ""
687    if {$path eq ""} {
688        return ""
689    } else {
690        return [file nativename [file normalize $path]]
691    }
692}
693
694proc twapi::_evt_dump {args} {
695    array set opts [parseargs args {
696        {outfd.arg stdout}
697        count.int
698    } -ignoreunknown]
699
700    set hq [evt_query {*}$args]
701    trap {
702        while {[llength [set hevts [evt_next $hq]]]} {
703            trap {
704                foreach ev [recordarray getlist [evt_decode_events $hevts -message -ignorestring None.] -format dict] {
705                    if {[info exists opts(count)] &&
706                        [incr opts(count) -1] < 0} {
707                        return
708                    }
709                    puts $opts(outfd) "[dict get $ev -timecreated] [dict get $ev -eventrecordid] [dict get $ev -providername]: [dict get $ev -eventrecordid] [dict get $ev -message]"
710                }
711            } finally {
712                evt_close {*}$hevts
713            }
714        }
715    } finally {
716        evt_close $hq
717    }
718}
719