Mini Shell
"""
A module for working with the Windows Event log system.
.. versionadded:: 3006.0
"""
# https://docs.microsoft.com/en-us/windows/win32/eventlog/event-logging
import collections
import logging
import salt.utils.platform
import salt.utils.stringutils
from salt.exceptions import CommandExecutionError
try:
import pywintypes
import win32evtlog
import win32evtlogutil
import winerror
# Only windows needs this dependency at runtime
import xmltodict
IMPORT_STATUS = True
except ImportError:
IMPORT_STATUS = False
log = logging.getLogger(__name__)
__virtualname__ = "win_event"
def __virtual__():
"""
Load only on minions running on Windows.
"""
if not salt.utils.platform.is_windows():
return False, "win_event: Must be on Windows"
if not IMPORT_STATUS:
return False, "win_event: Missing PyWin32"
return __virtualname__
def _to_bytes(data, encoding="utf-8", encode_keys=False):
"""
Convert string objects to byte objects.
.. warning::
This function will destroy the data object and objects that data links
to.
Args:
data (object): The string object to encode
encoding(str): The encoding type
encode_keys(bool): If false key strings will not be turned into bytes
Returns:
(object): An object with the new encoding
"""
if isinstance(data, dict):
new_dict = {}
# recursively check every item in the dict
for key in data:
item = _to_bytes(data[key], encoding)
if encode_keys:
# keys that are strings most be made into bytes
key = _to_bytes(key, encoding)
new_dict[key] = item
data = new_dict
elif isinstance(data, list):
new_list = []
# recursively check every item in the list
for item in data:
new_list.append(_to_bytes(item, encoding))
data = new_list
elif isinstance(data, tuple):
new_list = []
# recursively check every item in the tuple
for item in data:
new_list.append(_to_bytes(item, encoding))
data = tuple(new_list)
elif isinstance(data, str):
# encode string data to bytes
data = data.encode(encoding)
return data
def _raw_time(time):
"""
Will make a pywintypes.datetime into a TimeTuple.
Args:
time (ob): A datetime object
Returns:
TimeTuple: A TimeTuple
"""
TimeTuple = collections.namedtuple(
"TimeTuple", "year, month, day, hour, minute, second"
)
return TimeTuple(
time.year, time.month, time.day, time.hour, time.minute, time.second
)
def _make_event_dict(event):
"""
Will make a PyEventLogRecord into a dictionary
Args:
event (PyEventLogRecord): An event to convert to a dictionary
Returns:
dict: A dictionary containing the event information
"""
# keys of all the parts of a Event supported by the API
event_parts = (
"closingRecordNumber",
"computerName",
"data",
"eventCategory",
"eventID",
"eventType",
"recordNumber",
"reserved",
"reservedFlags",
"sid",
"sourceName",
"stringInserts",
"timeGenerated",
"timeWritten",
)
event_dict = {}
for event_part in event_parts:
# get object value and add it to the event dict
event_dict[event_part] = getattr(
event, event_part[0].upper() + event_part[1:], None
)
# format items
event_dict["eventID"] = winerror.HRESULT_CODE(event_dict["eventID"])
if event_dict["sid"] is not None:
event_dict["sid"] = event_dict["sid"].GetSidIdentifierAuthority()
event_dict["timeGenerated"] = _raw_time(event_dict["timeGenerated"])
event_dict["timeWritten"] = _raw_time(event_dict["timeWritten"])
return _to_bytes(event_dict)
def _get_handle(log_name):
"""
Will try to open a PyHANDLE to the Event System
Args:
log_name (str): The name of the log to open
Returns:
PyHANDLE: A handle to the event log
"""
# TODO: upgrade windows token
# "log close" can fail if this is not done
try:
return win32evtlog.OpenEventLog(None, log_name)
except pywintypes.error as exc:
raise FileNotFoundError(
f"Failed to open log: {log_name}\nError: {exc.strerror}"
)
def _close_handle(handle):
"""
Will close the handle to the event log
Args:
handle (PyHANDLE): The handle to the event log to close
"""
# TODO: downgrade windows token
win32evtlog.CloseEventLog(handle)
def _event_generator(log_name):
"""
Get all log events one by one. Events are not ordered
Args:
log_name(str): The name of the log to retrieve
Yields:
dict: A dictionary object for each event
"""
# Get events from the local machine (None)
handle = _get_handle(log_name)
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
while True:
# get list of some of the events
events = win32evtlog.ReadEventLog(handle, flags, 0)
if not events:
# event log was updated and events are not ready to be given yet
# rather than wait just return
break
for event in events:
yield _make_event_dict(event)
_close_handle(handle)
def _event_generator_with_time(log_name):
"""
Sorts the results of the event generator
Args:
log_name (str): The name of the log to retrieve
Yields:
dict: A dictionary object for each event
"""
# keys time
time_parts = (
"year",
"month",
"day",
"hour",
"minute",
"second",
)
for event in _event_generator(log_name):
event_info = {}
for part in event:
event_info[part] = event[part]
for spot, key in enumerate(time_parts):
event_info[key] = event["timeGenerated"][spot]
yield event, event_info
def _event_generator_filter(log_name, all_requirements=True, **kwargs):
"""
Will find events that meet the requirements in the filter. Can be any item
in the return for the event.
Args:
log_name (str): The name of the log to retrieve
all_requirements (bool): Should the results match all requirements.
``True`` matches all requirements. ``False`` matches any
requirement.
Kwargs:
eventID (int): The event ID number
eventType (int): The event type number. Valid options and their
corresponding meaning are:
- 0 : Success
- 1 : Error
- 2 : Warning
- 4 : Information
- 8 : Audit Success
- 10 : Audit Failure
year (int): The year
month (int): The month
day (int): The day of the month
hour (int): The hour
minute (int): The minute
second (int): The second
eventCategory (int): The event category number
sid (sid): The SID of the user that created the event
sourceName (str): The name of the event source
Yields:
dict: A dictionary object for each event
CLI Example:
.. code-block:: python
# Return all events from the Security log with an ID of 1100
_event_generator_filter("Security", eventID=1100)
# Return all events from the System log with an Error (1) event type
_event_generator_filter("System", eventType=1)
# Return all events from System log with an Error (1) type, source is Service Control Manager, and data is netprofm
_event_generator_filter("System", eventType=1, sourceName="Service Control Manager", data="netprofm")
"""
for event, info in _event_generator_with_time(log_name):
if all_requirements:
# all keys need to match each other
for key in kwargs:
# ignore kwargs built-ins
if key.startswith("__"):
continue
# ignore function parameters
if key in ["log_name", "all_arguments"]:
continue
# Try to handle bytestrings
if isinstance(info[key], bytes):
# try utf-8 first
try:
log.trace(
"utf-8: Does %s == %s",
repr(kwargs[key]),
repr(info[key].decode("utf-8")),
)
if kwargs[key] != info[key].decode("utf-8"):
# try utf-16 and strip null bytes
try:
log.trace(
"utf-16: Does %s == %s",
repr(kwargs[key]),
repr(info[key].decode("utf-16").strip("\x00")),
)
if kwargs[key] != info[key].decode("utf-16").strip(
"\x00"
):
break
except UnicodeDecodeError:
log.trace("Failed to decode (utf-16): %s", info[key])
break
except UnicodeDecodeError:
log.trace("Failed to decode (utf-8): %s", info[key])
break
elif kwargs[key] != info[key]:
break
else:
yield info
else:
# just a single key pair needs to match
for key in kwargs:
# ignore kwargs built-ins
if key.startswith("__"):
continue
# ignore function parameters
if key in ["log_name", "all_arguments"]:
continue
# Try to handle bytestrings
if isinstance(info[key], bytes):
# try utf-8 first
try:
log.trace(
"utf-8: Does %s == %s",
repr(kwargs[key]),
repr(info[key].decode("utf-8")),
)
if kwargs[key] == info[key].decode("utf-8"):
yield info
except UnicodeDecodeError:
log.trace("Failed to decode (utf-8): %s", info[key])
# try utf-16 and strip null bytes
try:
log.trace(
"utf-16: Does %s == %s",
repr(kwargs[key]),
repr(info[key].decode("utf-16").strip("\x00")),
)
if kwargs[key] == info[key].decode("utf-16").strip("\x00"):
yield info
except UnicodeDecodeError:
log.trace("Failed to decode (utf-16): %s", info[key])
break
elif kwargs[key] == info[key]:
yield info
def get(log_name):
"""
Get events from the specified log. Get a list of available logs using the
:py:func:`win_event.get_log_names <salt.modules.win_event.get_log_names>`
function.
.. warning::
Running this command on a log with thousands of events, such as the
``Applications`` log, can take a long time.
Args:
log_name(str): The name of the log to retrieve.
Returns
tuple: A tuple of events as dictionaries
CLI Example:
.. code-block:: bash
salt '*' win_event.get Application
"""
return tuple(_event_generator(log_name))
def query(log_name, query_text=None, records=20, latest=True, raw=False):
"""
Query a log for a specific event_id. Return the top number of records
specified. Use the
:py:func:`win_event.get_log_names <salt.modules.win_event.get_log_names>`
to see a list of available logs on the system.
.. Note::
You can use the Windows Event Viewer to create the XPath query for the
``query_text`` parameter. Click on ``Filter Current Log``, configure the
filter, then click on the XML tab. Copy the text between the two
``<Select>`` tags. This will be the contents of the ``query_text``
parameter. You will have to convert some codes. For example, ``>``
becomes ``>``, ``<`` becomes ``<``. Additionally, you'll need to
put spaces between comparison operators. For example: ``this >= that``.
Args:
log_name (str): The name of the log to query
query_text (str): The filter to apply to the log
records (int): The number of records to return
latest (bool): ``True`` will return the newest events. ``False`` will
return the oldest events. Default is ``True``
raw (bool): ``True`` will return the raw xml results. ``False`` will
return the xml converted to a dictionary. Default is ``False``
Returns:
list: A list of dict objects that contain information about the event
CLI Example:
.. code-block:: bash
# Return the 20 most recent events from the Application log with an event ID of 22
salt '*' win_event.query Application "*[System[(EventID=22)]]"
# Return the 20 most recent events from the Application log with an event ID of 22
# Return raw xml
salt '*' win_event.query Application "*[System[(EventID=22)]]" raw=True
# Return the 20 oldest events from the Application log with an event ID of 22
salt '*' win_event.query Application "*[System[(EventID=22)]]" latest=False
# Return the 20 most recent Critical (1) events from the Application log in the last 12 hours
salt '*" win_event.query Application "*[System[(Level=1) and TimeCreated[timediff(@SystemTime) <= 43200000]]]"
# Return the 5 most recent Error (2) events from the application log
salt '*" win_event.query Application "*[System[(Level=2)]]" records=5
# Return the 20 most recent Warning (3) events from the Windows PowerShell log where the Event Source is PowerShell
salt '*" win_event.query "Windows PowerShell" "*[System[Provider[@Name='PowerShell'] and (Level=3)]]"
# Return the 20 most recent Information (0 or 4) events from the Microsoft-Windows-PowerShell/Operational on 2022-08-24 with an Event ID of 4103
salt '*" win_event.query "Microsoft-Windows-PowerShell/Operational" "*[System[(Level=4 or Level=0) and (EventID=4103) and TimeCreated[@SystemTime >= '2022-08-24T06:00:00.000Z']]]"
# Return the 20 most recent Information (0 or 4) events from the Microsoft-Windows-PowerShell/Operational within the last hour
salt '*" win_event.query "Microsoft-Windows-PowerShell/Operational" "*[System[(Level=4 or Level=0) and TimeCreated[timediff(@SystemTime) <= 3600000]]]"
"""
if not isinstance(latest, bool):
raise CommandExecutionError("latest must be a boolean")
direction = win32evtlog.EvtQueryReverseDirection
if not latest:
direction = win32evtlog.EvtQueryForwardDirection
results = win32evtlog.EvtQuery(log_name, direction, query_text, None)
event_list = []
for evt in win32evtlog.EvtNext(results, records):
if raw:
res = win32evtlog.EvtRender(evt, 1)
else:
res = xmltodict.parse(win32evtlog.EvtRender(evt, 1))
event_list.append(res)
return event_list
def get_filtered(log_name, all_requirements=True, **kwargs):
"""
Will find events that match the fields and values specified in the kwargs.
Kwargs can be any item in the return for the event.
.. warning::
Running this command on a log with thousands of events, such as the
``Applications`` log, can take a long time.
Args:
log_name (str): The name of the log to retrieve
all_requirements (bool): ``True`` matches all requirements. ``False``
matches any requirement. Default is ``True``
Kwargs:
eventID (int): The event ID number
eventType (int): The event type number. Valid options and their
corresponding meaning are:
- 0 : Success
- 1 : Error
- 2 : Warning
- 4 : Information
- 8 : Audit Success
- 10 : Audit Failure
year (int): The year
month (int): The month
day (int): The day of the month
hour (int): The hour
minute (int): The minute
second (int): The second
eventCategory (int): The event category number
sid (sid): The SID of the user that created the event
sourceName (str): The name of the event source
Returns:
tuple: A tuple of dicts of each filtered event
CLI Example:
.. code-block:: bash
# Return all events from the Security log with an ID of 1100
salt "*" win_event.get_filtered Security eventID=1100
# Return all events from the System log with an Error (1) event type
salt "*" win_event.get_filtered System eventType=1
# Return all events from System log with an Error (1) type, source is Service Control Manager, and data is netprofm
salt "*" win_event.get_filtered System eventType=1 sourceName="Service Control Manager" data="netprofm"
# Return events from the System log that match any of the kwargs below
salt "*" win_event.get_filtered System eventType=1 sourceName="Service Control Manager" data="netprofm" all_requirements=False
"""
return tuple(_event_generator_filter(log_name, all_requirements, **kwargs))
def get_log_names():
"""
Get a list of event logs available on the system
Returns:
list: A list of event logs available on the system
CLI Example:
.. code-block:: bash
salt "*" win_event.get_log_names
"""
h = win32evtlog.EvtOpenChannelEnum(None)
log_names = []
while win32evtlog.EvtNextChannelPath(h) is not None:
log_names.append(win32evtlog.EvtNextChannelPath(h))
return log_names
def add(
log_name,
event_id,
event_category=0,
event_type=None,
event_strings=None,
event_data=None,
event_sid=None,
):
"""
Adds an event to the application event log.
Args:
log_name (str): The name of the application or source
event_id (int): The event ID
event_category (int): The event category
event_type (str): The event category. Must be one of:
- Success
- Error
- Warning
- Information
- AuditSuccess
- AuditFailure
event_strings (list): A list of strings
event_data (bytes): Event data. Strings will be converted to bytes
event_sid (sid): The SID for the event
Raises:
CommandExecutionError: event_id is not an integer
CommandExecutionError: event_category is not an integer
CommandExecutionError: event_type is not one of the valid event types
CommandExecutionError: event_strings is not a list or string
CLI Example:
.. code-block:: bash
# A simple Application event log warning entry
salt '*' win_event.add Application 1234 12 Warning
# A more complex System event log information entry
salt '*' win_event.add System 1234 12 Information "['Event string data 1', 'Event string data 2']" "Some event data"
# Log to the System Event log with the source "Service Control Manager"
salt '*' win_event.add "Service Control Manager" 1234 12 Warning "['Event string data 1', 'Event string data 2']" "Some event data"
# Log to the PowerShell event log with the source "PowerShell (PowerShell)"
salt-call --local win_event.add "PowerShell" 6969 12 Warning
"""
try:
event_id = int(event_id)
except TypeError:
raise CommandExecutionError("event_id must be an integer")
try:
event_category = int(event_category)
except TypeError:
raise CommandExecutionError("event_category must be an integer")
event_types = {
"Success": 0x0000,
"Error": 0x0001,
"Warning": 0x0002,
"Information": 0x0004,
"AuditSuccess": 0x0008,
"AuditFailure": 0x0010,
0x0000: "Success",
0x0001: "Error",
0x0002: "Warning",
0x0004: "Information",
0x0008: "AuditSuccess",
0x0010: "AuditFailure",
}
if event_type is None:
event_type = event_types["Error"]
elif event_type not in event_types:
msg = f"Incorrect event type: {event_type}"
raise CommandExecutionError(msg)
else:
event_type = event_types[event_type]
if event_strings is not None:
if isinstance(event_strings, str):
event_strings = [event_strings]
elif not isinstance(event_strings, list):
raise CommandExecutionError("event_strings must be a list")
if event_data is not None:
event_data = salt.utils.stringutils.to_bytes(event_data)
# https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-reporteventw
win32evtlogutil.ReportEvent(
appName=log_name,
eventID=int(event_id),
eventCategory=int(event_category),
eventType=event_type,
strings=event_strings,
data=event_data,
sid=event_sid,
)
def clear(log_name, backup=None):
"""
Clears the specified event log.
.. note::
A clear log event will be added to the log after it is cleared.
Args:
log_name (str): The name of the log to clear
backup (str): Path to backup file
CLI Example:
.. code-block:: bash
salt "*" win_event.clear Application
"""
handle = _get_handle(log_name)
win32evtlog.ClearEventLog(handle, backup)
_close_handle(handle)
def count(log_name):
"""
Gets the number of events in the specified.
Args:
log_name (str): The name of the log
Returns:
int: The number of events the log contains
CLI Example:
.. code-block:: bash
salt "*" win_event.count Application
"""
handle = _get_handle(log_name)
number_of_events = win32evtlog.GetNumberOfEventLogRecords(handle)
_close_handle(handle)
return number_of_events
Zerion Mini Shell 1.0