mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
Add query function, docs
Add query function that accepts XPath queries Add docs and CLI examples for all functions Fix issues with the get_filtered function
This commit is contained in:
parent
2902f2d8a3
commit
8660718fa7
5 changed files with 819 additions and 436 deletions
726
salt/modules/win_event.py
Normal file
726
salt/modules/win_event.py
Normal file
|
@ -0,0 +1,726 @@
|
|||
"""
|
||||
A module for working with the Windows Event log system.
|
||||
"""
|
||||
# https://docs.microsoft.com/en-us/windows/win32/eventlog/event-logging
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Python libs
|
||||
import collections
|
||||
import logging
|
||||
import xmltodict
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.utils.platform
|
||||
import salt.utils.stringutils
|
||||
from salt.exceptions import CommandExecutionError
|
||||
|
||||
# Import Third Party Libs
|
||||
try:
|
||||
import win32evtlog
|
||||
import win32evtlogutil
|
||||
import winerror
|
||||
import pywintypes
|
||||
IMPORT_STATUS = True
|
||||
except ImportError:
|
||||
IMPORT_STATUS = False
|
||||
|
||||
# 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_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",
|
||||
}
|
||||
|
||||
# keys time
|
||||
TIME_PARTS = ("year",
|
||||
"month",
|
||||
"day",
|
||||
"hour",
|
||||
"minute",
|
||||
"second",
|
||||
)
|
||||
TimeTuple = collections.namedtuple("TimeTuple", "year, month, day, hour, minute, second")
|
||||
|
||||
|
||||
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: Most 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
|
||||
"""
|
||||
|
||||
return TimeTuple._make((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
|
||||
"""
|
||||
|
||||
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:
|
||||
raise FileNotFoundError(
|
||||
"{0} log can not be found or access was denied!".format(log_name)
|
||||
)
|
||||
|
||||
|
||||
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_sorted(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
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
Can be any item in the return for the event. Common kwargs are:
|
||||
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_sorted(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_sorted(log_name):
|
||||
"""
|
||||
Make a list of events sorted by date.
|
||||
|
||||
.. 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:
|
||||
dict: A dictionary of events
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block::bash
|
||||
|
||||
# This command can take a long time
|
||||
salt "*" win_event.get_sorted Application
|
||||
"""
|
||||
|
||||
event_info = {event_part: collections.defaultdict(list) for event_part in EVENT_PARTS + TIME_PARTS}
|
||||
for event, info in _event_generator_sorted(log_name):
|
||||
for part in info:
|
||||
event_info[part][info.get(part)].append(event)
|
||||
|
||||
return event_info
|
||||
|
||||
|
||||
def get_filtered(log_name, all_requirements=True, **kwargs):
|
||||
"""
|
||||
Will find events that match the fields and values specified in the kwargs.
|
||||
|
||||
.. 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:
|
||||
Can be any item in the return for the event. Common kwargs are:
|
||||
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")
|
||||
|
||||
if event_type is None:
|
||||
event_type = EVENT_TYPES["Error"]
|
||||
elif event_type not in EVENT_TYPES:
|
||||
msg = "Incorrect event type: {}".format(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(log_name):
|
||||
"""
|
||||
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
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block::bash
|
||||
|
||||
salt "*" win_event.clear_log Application
|
||||
"""
|
||||
|
||||
handle = _get_handle(log_name)
|
||||
win32evtlog.ClearEventLog(handle, log_name)
|
||||
_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
|
|
@ -1,411 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Gives SaltStack access to Windows event log
|
||||
Charles McMarrow <cmcmarrow@saltstack.com>
|
||||
'''
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Python libs
|
||||
import logging
|
||||
import collections
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.utils.platform
|
||||
|
||||
# Import Third Party Libs
|
||||
try:
|
||||
import win32evtlog
|
||||
import win32evtlogutil
|
||||
import winerror
|
||||
import pywintypes
|
||||
IMPORT_STATUS = True
|
||||
except ImportError:
|
||||
IMPORT_STATUS = False
|
||||
|
||||
# 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',
|
||||
)
|
||||
|
||||
# keys time
|
||||
TIME_PARTS = ('year',
|
||||
'month',
|
||||
'day',
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
)
|
||||
TimeTuple = collections.namedtuple('TimeTuple', 'year, month, day, hour, minute, second')
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__virtualname__ = 'win_event_viewer'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Load only on minions running on Windows.
|
||||
'''
|
||||
|
||||
if not salt.utils.platform.is_windows() or not IMPORT_STATUS:
|
||||
return False, 'win_event_viewer: most be on windows'
|
||||
return __virtualname__
|
||||
|
||||
|
||||
def _change_str_to_bytes(data, encoding='utf-8', encode_keys=False):
|
||||
'''
|
||||
Convert string objects to byte objects.
|
||||
This function will destroy the data object and objects that data links to.
|
||||
|
||||
data
|
||||
object
|
||||
|
||||
encoding
|
||||
str
|
||||
|
||||
encode_keys
|
||||
bool
|
||||
if false key strings will not be turned into bytes
|
||||
|
||||
return
|
||||
new object
|
||||
'''
|
||||
|
||||
if isinstance(data, dict):
|
||||
new_dict = {}
|
||||
# recursively check every item in dict
|
||||
for key in data:
|
||||
item = _change_str_to_bytes(data[key], encoding)
|
||||
if encode_keys:
|
||||
# keys that are strings most be made into bytes
|
||||
key = _change_str_to_bytes(key, encoding)
|
||||
new_dict[key] = item
|
||||
data = new_dict
|
||||
elif isinstance(data, list):
|
||||
new_list = []
|
||||
# recursively check every item in list
|
||||
for item in data:
|
||||
new_list.append(_change_str_to_bytes(item, encoding))
|
||||
data = new_list
|
||||
elif isinstance(data, tuple):
|
||||
new_list = []
|
||||
# recursively check every item in list
|
||||
for item in data:
|
||||
new_list.append(_change_str_to_bytes(item, encoding))
|
||||
data = tuple(new_list)
|
||||
elif isinstance(data, str):
|
||||
# data is turning into bytes because if was a string
|
||||
data = data.encode(encoding)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _get_raw_time(time):
|
||||
'''
|
||||
Will make a pywintypes.datetime into a TimeTuple.
|
||||
|
||||
time
|
||||
pywintypes.datetime
|
||||
|
||||
return
|
||||
TimeTuple
|
||||
'''
|
||||
|
||||
return TimeTuple._make((time.year, time.month, time.day, time.hour, time.minute, time.second))
|
||||
|
||||
|
||||
def make_event_dict(event):
|
||||
'''
|
||||
Will make a PyEventLogRecord into a dict.
|
||||
|
||||
event
|
||||
PyEventLogRecord
|
||||
|
||||
return
|
||||
dict
|
||||
'''
|
||||
|
||||
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'] = _get_raw_time(event_dict['timeGenerated'])
|
||||
event_dict['timeWritten'] = _get_raw_time(event_dict['timeWritten'])
|
||||
|
||||
return _change_str_to_bytes(event_dict)
|
||||
|
||||
|
||||
def _get_event_handler(log_name, target_computer=None):
|
||||
'''
|
||||
Will try to open a PyHANDLE.
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
return
|
||||
PyHANDLE
|
||||
'''
|
||||
|
||||
# TODO: upgrade windows token
|
||||
# 'log close' can fail if this is not done
|
||||
try:
|
||||
return win32evtlog.OpenEventLog(target_computer, log_name)
|
||||
except pywintypes.error:
|
||||
raise FileNotFoundError('Log "{0}" of "{1}" can not be found or access was denied!'.format(log_name,
|
||||
target_computer))
|
||||
|
||||
|
||||
def _close_event_handler(handler):
|
||||
'''
|
||||
Will close the event handler.
|
||||
|
||||
handler
|
||||
PyHANDLE
|
||||
'''
|
||||
|
||||
# TODO: downgrade windows token
|
||||
win32evtlog.CloseEventLog(handler)
|
||||
|
||||
|
||||
def get_event_generator(log_name, target_computer=None, raw=False):
|
||||
'''
|
||||
Will get all log events one by one.
|
||||
Events are not in exact order.
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
raw
|
||||
bool
|
||||
True: PyEventLogRecord
|
||||
False: dict
|
||||
|
||||
return
|
||||
PyEventLogRecord or dict
|
||||
'''
|
||||
|
||||
handler = _get_event_handler(log_name, target_computer)
|
||||
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
|
||||
event_count = 0
|
||||
|
||||
while win32evtlog.GetNumberOfEventLogRecords(handler) > event_count:
|
||||
# get list of some of the events
|
||||
events = win32evtlog.ReadEventLog(handler, 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:
|
||||
event_count += 1
|
||||
if raw:
|
||||
yield event
|
||||
else:
|
||||
yield make_event_dict(event)
|
||||
_close_event_handler(handler)
|
||||
|
||||
|
||||
def get_events(log_name, target_computer=None, raw=False):
|
||||
'''
|
||||
Convert pywinypes.datetime into a TimeTuple.
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
raw
|
||||
bool
|
||||
True: PyEventLogRecord
|
||||
False: dict
|
||||
|
||||
return
|
||||
tuple
|
||||
'''
|
||||
|
||||
return tuple(get_event_generator(log_name, target_computer, raw))
|
||||
|
||||
|
||||
def get_event_sorted_by_info_generator(log_name, target_computer=None):
|
||||
'''
|
||||
Makes keys to event
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
return
|
||||
dict
|
||||
'''
|
||||
|
||||
for event in get_event_generator(log_name, target_computer):
|
||||
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 get_events_sorted_by_info(log_name, target_computer=None):
|
||||
'''
|
||||
Make dict of sorted events
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
return
|
||||
dict
|
||||
'''
|
||||
|
||||
event_info = {event_part: collections.defaultdict(list) for event_part in EVENT_PARTS + TIME_PARTS}
|
||||
for event, info in get_event_sorted_by_info_generator(log_name, target_computer):
|
||||
for part in info:
|
||||
event_info[part][info.get(part)].append(event)
|
||||
|
||||
return event_info
|
||||
|
||||
|
||||
def get_event_filter_generator(log_name, target_computer=None, all_requirements=True, **kwargs):
|
||||
'''
|
||||
Will find events that meet the requirements
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
all_requirements
|
||||
bool
|
||||
True: all requirements most be meet
|
||||
False: only a single requirement most be meet
|
||||
|
||||
kwargs
|
||||
requirements for the events
|
||||
|
||||
return
|
||||
dict
|
||||
'''
|
||||
|
||||
for event, info in get_event_sorted_by_info_generator(log_name, target_computer):
|
||||
if all_requirements:
|
||||
# all keys need to match each other
|
||||
for key in kwargs:
|
||||
if kwargs[key] != info[key]:
|
||||
break
|
||||
else:
|
||||
yield event
|
||||
else:
|
||||
# just a single key par needs to match
|
||||
if any([kwargs[key] == info[key] for key in kwargs]):
|
||||
yield event
|
||||
|
||||
|
||||
def get_events_filter(log_name, target_computer=None, all_requirements=True, **kwargs):
|
||||
'''
|
||||
Find events that meet the requirements.
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
all_requirements
|
||||
bool
|
||||
True: all requirements most be meet
|
||||
False: only a single requirement most be meet
|
||||
|
||||
kwargs
|
||||
requirements for the events
|
||||
|
||||
return
|
||||
list
|
||||
'''
|
||||
|
||||
return tuple(get_event_filter_generator(log_name, target_computer, all_requirements, **kwargs))
|
||||
|
||||
|
||||
def log_event(application_name, event_id, **kwargs):
|
||||
'''
|
||||
Adds event to application log.
|
||||
|
||||
application_name
|
||||
str
|
||||
|
||||
event_id
|
||||
int
|
||||
|
||||
kwargs
|
||||
parts of event
|
||||
'''
|
||||
|
||||
win32evtlogutil.ReportEvent(application_name, event_id, **kwargs)
|
||||
|
||||
|
||||
def clear_log(log_name, target_computer=None):
|
||||
'''
|
||||
Clears event log.
|
||||
A clear log event will be add it after the log was clear.
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
'''
|
||||
|
||||
handler = _get_event_handler(log_name, target_computer)
|
||||
win32evtlog.ClearEventLog(handler, log_name)
|
||||
_close_event_handler(handler)
|
||||
|
||||
|
||||
def get_number_of_events(log_name, target_computer=None):
|
||||
'''
|
||||
Gets the number of events in a log.
|
||||
|
||||
log_name
|
||||
str
|
||||
|
||||
target_computer
|
||||
None or str
|
||||
|
||||
return
|
||||
int
|
||||
'''
|
||||
|
||||
handler = _get_event_handler(log_name, target_computer)
|
||||
number_of_events = win32evtlog.GetNumberOfEventLogRecords(handler)
|
||||
_close_event_handler(handler)
|
||||
return number_of_events
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Salt Testing libs
|
||||
from tests.support.case import ModuleCase
|
||||
from tests.support.unit import skipIf
|
||||
from tests.support.helpers import destructiveTest
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.utils.platform
|
||||
|
||||
# All test will add to a log in some way.
|
||||
# Because of this they all most be destructive test.
|
||||
@destructiveTest
|
||||
@skipIf(not salt.utils.platform.is_windows(), 'Tests for only Windows')
|
||||
class WinEventViewerTest(ModuleCase):
|
||||
def test_get_number_of_events(self):
|
||||
'''
|
||||
test get_number_of_events
|
||||
'''
|
||||
|
||||
ret = self.run_function('win_event_viewer.get_number_of_events', ('System',), timeout=180)
|
||||
self.assertEqual(int(ret), abs(int(ret)))
|
15
tests/pytests/functional/modules/test_win_event.py
Normal file
15
tests/pytests/functional/modules/test_win_event.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import pytest
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def win_event(modules):
|
||||
return modules.win_event
|
||||
|
||||
|
||||
def test_get(win_event):
|
||||
events = win_event.get()
|
||||
assert events == {}
|
78
tests/pytests/unit/modules/test_win_event.py
Normal file
78
tests/pytests/unit/modules/test_win_event.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
import salt.modules.win_event as win_event
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skip_unless_on_windows,
|
||||
pytest.mark.destructive_test,
|
||||
]
|
||||
|
||||
|
||||
def test__to_bytes_utf8():
|
||||
data = {'key1': 'item1',
|
||||
'key2': [1, 2, 'item2'],
|
||||
'key3': 45,
|
||||
45: str}
|
||||
|
||||
new_data = win_event._to_bytes(data, 'utf-8', False)
|
||||
|
||||
assert 'key1' in new_data
|
||||
|
||||
assert new_data['key1'] == 'item1'.encode('utf-8')
|
||||
assert new_data['key2'][2] == 'item2'.encode('utf-8')
|
||||
|
||||
|
||||
def test__to_bytes_cp1252():
|
||||
data = {'key1': 'item1',
|
||||
'key2': [1, 2, 'item2'],
|
||||
'key3': 45,
|
||||
45: str}
|
||||
|
||||
new_data = win_event._to_bytes(data, 'CP1252', True)
|
||||
|
||||
assert b'key1' in new_data
|
||||
assert b'key2' in new_data
|
||||
assert b'key3' in new_data
|
||||
|
||||
assert new_data['key1'.encode('CP1252')] == 'item1'.encode('CP1252')
|
||||
assert new_data['key2'.encode('CP1252')][2] == 'item2'.encode('CP1252')
|
||||
|
||||
|
||||
def test__raw_time():
|
||||
raw_time = win_event._raw_time(datetime.datetime(2019, 7, 2, 10, 8, 19))
|
||||
assert raw_time == (2019, 7, 2, 10, 8, 19)
|
||||
|
||||
|
||||
def test_count():
|
||||
"""
|
||||
Test win_event.count
|
||||
"""
|
||||
ret = win_event.count("System")
|
||||
assert ret > 0
|
||||
|
||||
|
||||
def test_security():
|
||||
ret = win_event.get("Security")
|
||||
print(len(ret))
|
||||
assert len(ret) > 0
|
||||
|
||||
|
||||
def test_windows_powershell():
|
||||
ret = win_event.get("Windows PowerShell")
|
||||
print(len(ret))
|
||||
assert len(ret) > 0
|
||||
|
||||
|
||||
def test_operational():
|
||||
ret = win_event.get("Operational")
|
||||
print(len(ret))
|
||||
assert len(ret) > 0
|
||||
|
||||
|
||||
def test_query():
|
||||
|
||||
ret = win_event.query("Microsoft-Windows-TerminalServices-LocalSessionManager/Operational", 22)
|
||||
print(len(ret))
|
||||
assert len(ret) > 0
|
Loading…
Add table
Reference in a new issue