diff --git a/changelog/68354.fixed.md b/changelog/68354.fixed.md new file mode 100644 index 000000000000..1146852475e8 --- /dev/null +++ b/changelog/68354.fixed.md @@ -0,0 +1 @@ +Windows LGPO / audit policy: Advanced audit policy is now read and applied through the Windows security API (AuditQuerySystemPolicy / AuditSetSystemPolicy) instead of parsing auditpol.exe output, so behavior no longer depends on the system locale. diff --git a/salt/modules/win_auditpol.py b/salt/modules/win_auditpol.py index d0cc906ff218..de6aab3a8404 100644 --- a/salt/modules/win_auditpol.py +++ b/salt/modules/win_auditpol.py @@ -9,7 +9,12 @@ .. versionadded:: 2019.2.1 This module allows you to view and modify the audit settings as they are applied -on the machine. The audit settings are broken down into nine categories: +on the machine. Implementation uses the ``auditpol`` execution utility +(``__utils__['auditpol']``), which reads and writes policy through Windows +``advapi32`` audit APIs with English subcategory names, independent of the host +display language. + +The audit settings are broken down into nine categories: - Account Logon - Account Management @@ -95,11 +100,13 @@ def get_settings(category="All"): Returns: dict: A dictionary containing all subcategories for the specified - category along with their current configuration + category along with their current configuration (English names and + value labels). Raises: KeyError: On invalid category CommandExecutionError: If an error is encountered retrieving the settings + from the underlying Windows API. CLI Example: @@ -128,6 +135,7 @@ def get_setting(name): Raises: KeyError: On invalid setting name CommandExecutionError: If an error is encountered retrieving the settings + from the underlying Windows API. CLI Example: @@ -162,6 +170,7 @@ def set_setting(name, value): Raises: KeyError: On invalid ``name`` or ``value`` CommandExecutionError: If an error is encountered modifying the setting + (for example insufficient privilege for ``AuditSetSystemPolicy``). CLI Example: diff --git a/salt/modules/win_lgpo.py b/salt/modules/win_lgpo.py index 1454778cc2b0..8911ca86992e 100644 --- a/salt/modules/win_lgpo.py +++ b/salt/modules/win_lgpo.py @@ -318,9 +318,9 @@ class _policy_info: AdvAudit Mechanism ------------------ - The Advanced Audit Policies are configured using a combination of the - auditpol command-line utility and modifying the audit.csv file in two - locations. The value of this key is a dict with the following make-up: + The Advanced Audit Policies are configured using the Windows security APIs + (via Salt's ``auditpol`` execution utility) and modifying the audit.csv file + in two locations. The value of this key is a dict with the following make-up: ====== =================================== Key Value @@ -5375,6 +5375,15 @@ def _get_advaudit_defaults(option=None): configurable policies as keys. The values are used to create/modify the ``audit.csv`` file. The first entry is `fieldnames` used to create the header for the csv file. The rest of the entries are the audit policy names. + + Row templates are built from ``__utils__['auditpol.get_advaudit_policy_rows']()``, + which uses Windows ``AuditQuerySystemPolicy`` and English metadata (not + ``auditpol /backup``), so defaults stay consistent on non-English Windows. + Those templates are still used to **create or update** the machine's + ``audit.csv`` files (see ``_advaudit_check_csv`` / ``_set_advaudit_file_data``); + only the source of the default *content* changed, not LGPO's use of + ``audit.csv`` on disk. + Sample data follows: { @@ -5413,8 +5422,9 @@ def _get_advaudit_defaults(option=None): } .. note:: - `Auditpol Name` designates the value to use when setting the value with - the auditpol command + ``Auditpol Name`` is the English subcategory string passed to + ``__utils__['auditpol.set_setting']``, which applies policy via + ``AuditSetSystemPolicy`` (not ``auditpol.exe``). Args: option (str): The item from the dictionary to return. If ``None`` the @@ -5427,11 +5437,10 @@ def _get_advaudit_defaults(option=None): if "lgpo.audit_defaults" not in __context__: # Get available setting names and GUIDs # This is used to get the fieldnames and GUIDs for individual policies - log.debug("Loading auditpol defaults into __context__") - dump = __utils__["auditpol.get_auditpol_dump"]() - reader = csv.DictReader(dump) - audit_defaults = {"fieldnames": reader.fieldnames} - for row in reader: + log.debug("Loading advanced audit defaults into __context__") + rows = __utils__["auditpol.get_advaudit_policy_rows"]() + audit_defaults = {"fieldnames": list(rows[0].keys())} + for row in rows: row["Machine Name"] = "" row["Auditpol Name"] = row["Subcategory"] # Special handling for snowflake scenarios where the audit.csv names @@ -5643,7 +5652,10 @@ def _set_advaudit_pol_data(option, value): """ Helper function that updates the current applied settings to match what has just been set in the audit.csv files. We're doing it this way instead of - running `gpupdate` + running `gpupdate`. + + Calls ``__utils__['auditpol.set_setting']``, which uses Windows + ``AuditSetSystemPolicy`` (not ``auditpol.exe``). Args: option (str): The name of the option to set @@ -5673,7 +5685,8 @@ def _set_advaudit_value(option, value): C:\\Windows\\Security\\Audit\\audit.csv C:\\Windows\\System32\\GroupPolicy\\Machine\\Microsoft\\Windows NT\\Audit\\audit.csv - Then it applies those settings using ``auditpol`` + Then it applies those settings using ``__utils__['auditpol.set_setting']`` + (native ``AuditSetSystemPolicy``). After that, it updates ``__context__`` with the new setting diff --git a/salt/utils/win_lgpo_auditpol.py b/salt/utils/win_lgpo_auditpol.py index 47f0d8e8912e..7dce5191af13 100644 --- a/salt/utils/win_lgpo_auditpol.py +++ b/salt/utils/win_lgpo_auditpol.py @@ -9,6 +9,11 @@ .. versionadded:: 2018.3.4 .. versionadded:: 2019.2.1 +Audit policy is read and written using the Windows ``advapi32`` APIs +(``AuditQuerySystemPolicy`` / ``AuditSetSystemPolicy``) with a static +subcategory GUID to English name map, so behavior is consistent regardless of +the host OS display language. + This util allows you to view and modify the audit settings as they are applied on the machine. The audit settings are broken down into nine categories: @@ -25,6 +30,9 @@ The ``get_settings`` function will return the subcategories for all nine of the above categories in one dictionary along with their auditing status. +Subcategory names are **canonical English** strings (they match the names Salt +and LGPO use, not the host OS localized ``auditpol`` display names). + To modify a setting you only need to specify the subcategory name and the value you wish to set. Valid settings are: @@ -33,6 +41,15 @@ - Failure - Success and Failure +The module constant ``settings`` maps those English labels to the integer +**auditing bitmask** passed to ``AuditSetSystemPolicy`` (``0``–``3``, aligned +with LGPO ``audit.csv`` ``Setting Value``). Execution modules should keep +using the string labels; callers should not rely on ``settings`` values being +``auditpol.exe`` switch strings. + +LGPO loads defaults via :func:`get_advaudit_policy_rows`; :func:`get_auditpol_dump` +serializes the same data as UTF-8 CSV lines for backward compatibility. + Usage: .. code-block:: python @@ -59,17 +76,24 @@ value='No Auditing') """ +from __future__ import annotations + +import contextlib +import csv +import ctypes +import io import logging -import re -import tempfile +import struct +import uuid +from ctypes import wintypes as w +from types import SimpleNamespace -import salt.modules.cmdmod -import salt.utils.files import salt.utils.platform from salt.exceptions import CommandExecutionError log = logging.getLogger(__name__) __virtualname__ = "auditpol" +__context__ = {} categories = [ "Account Logon", @@ -83,16 +107,491 @@ "System", ] +# English value label -> bitmask for AuditSetSystemPolicy (matches LGPO 0–3) settings = { - "No Auditing": "/success:disable /failure:disable", - "Success": "/success:enable /failure:disable", - "Failure": "/success:disable /failure:enable", - "Success and Failure": "/success:enable /failure:enable", + "No Auditing": 0, + "Success": 1, + "Failure": 2, + "Success and Failure": 3, } +_AUDIT_NONE = 0 +_AUDIT_SUCCESS = 1 +_AUDIT_FAILURE = 2 + +_TOKEN_QUERY = 0x0008 +_TOKEN_ADJUST_PRIVILEGES = 0x0020 +_SE_PRIVILEGE_ENABLED = 0x00000002 + +_FIELDNAMES = [ + "Machine Name", + "Policy Target", + "Subcategory", + "Subcategory GUID", + "Inclusion Setting", + "Exclusion Setting", + "Setting Value", +] + + +class _GUID(ctypes.Structure): + """Win32 ``GUID`` layout (``Data1``..``Data4``) for ``Audit*`` APIs.""" + + _fields_ = [ + ("Data1", w.DWORD), + ("Data2", w.WORD), + ("Data3", w.WORD), + ("Data4", w.BYTE * 8), + ] + + +class _AUDIT_POLICY_INFORMATION(ctypes.Structure): + """``AUDIT_POLICY_INFORMATION`` from ntsecapi / advapi32 (per subcategory).""" + + _fields_ = [ + ("AuditSubcategoryGuid", _GUID), + ("AuditingInformation", w.ULONG), + ("AuditCategoryGuid", _GUID), + ] + + +class _LUID(ctypes.Structure): + """Locally unique identifier (used with ``LookupPrivilegeValueW``).""" + + _fields_ = [("LowPart", w.DWORD), ("HighPart", w.LONG)] + + +class _LUID_AND_ATTRIBUTES(ctypes.Structure): + """One privilege entry for ``TOKEN_PRIVILEGES``.""" + + _fields_ = [("Luid", _LUID), ("Attributes", w.DWORD)] + + +class _TOKEN_PRIVILEGES(ctypes.Structure): + """``TOKEN_PRIVILEGES`` with a single ``LUID_AND_ATTRIBUTES`` (fixed size).""" + + _fields_ = [("PrivilegeCount", w.DWORD), ("Privileges", _LUID_AND_ATTRIBUTES * 1)] + + +def _load_advapi32(): + """ + Bind advapi32/kernel32 entry points used for audit policy and token + privileges. ``use_last_error=True`` so ``ctypes.get_last_error()`` matches + Win32 failures after each call. + """ + advapi32_dll = ctypes.WinDLL("advapi32", use_last_error=True) + kernel32_dll = ctypes.WinDLL("kernel32", use_last_error=True) + + # AuditQuerySystemPolicy — ntsecapi.h; ppAuditPolicy is heap-allocated; free + # with AuditFree. Local names match Win32 exports for easier MSDN lookup. + AuditQuerySystemPolicy = advapi32_dll.AuditQuerySystemPolicy + AuditQuerySystemPolicy.argtypes = [ + ctypes.POINTER(_GUID), + w.ULONG, + ctypes.POINTER(ctypes.POINTER(_AUDIT_POLICY_INFORMATION)), + ] + AuditQuerySystemPolicy.restype = w.BOOL + + # AuditSetSystemPolicy — category GUID member ignored per MSDN. + AuditSetSystemPolicy = advapi32_dll.AuditSetSystemPolicy + AuditSetSystemPolicy.argtypes = [ + ctypes.POINTER(_AUDIT_POLICY_INFORMATION), + w.ULONG, + ] + AuditSetSystemPolicy.restype = w.BOOL + + AuditFree = advapi32_dll.AuditFree + AuditFree.argtypes = [w.LPVOID] + AuditFree.restype = None + + OpenProcessToken = advapi32_dll.OpenProcessToken + OpenProcessToken.argtypes = [w.HANDLE, w.DWORD, ctypes.POINTER(w.HANDLE)] + OpenProcessToken.restype = w.BOOL + + LookupPrivilegeValueW = advapi32_dll.LookupPrivilegeValueW + LookupPrivilegeValueW.argtypes = [ + w.LPCWSTR, + w.LPCWSTR, + ctypes.POINTER(_LUID), + ] + LookupPrivilegeValueW.restype = w.BOOL + + AdjustTokenPrivileges = advapi32_dll.AdjustTokenPrivileges + AdjustTokenPrivileges.argtypes = [ + w.HANDLE, + w.BOOL, + ctypes.POINTER(_TOKEN_PRIVILEGES), + w.DWORD, + ctypes.c_void_p, + ctypes.c_void_p, + ] + AdjustTokenPrivileges.restype = w.BOOL + + GetCurrentProcess = kernel32_dll.GetCurrentProcess + GetCurrentProcess.argtypes = [] + GetCurrentProcess.restype = w.HANDLE + + CloseHandle = kernel32_dll.CloseHandle + CloseHandle.argtypes = [w.HANDLE] + CloseHandle.restype = w.BOOL + + return SimpleNamespace( + AuditQuerySystemPolicy=AuditQuerySystemPolicy, + AuditSetSystemPolicy=AuditSetSystemPolicy, + AuditFree=AuditFree, + OpenProcessToken=OpenProcessToken, + LookupPrivilegeValueW=LookupPrivilegeValueW, + AdjustTokenPrivileges=AdjustTokenPrivileges, + GetCurrentProcess=GetCurrentProcess, + CloseHandle=CloseHandle, + ) + + +_API = None + + +def _api(): + """ + Lazy singleton of :func:`_load_advapi32` bindings (``_API`` cache). + + Attributes use **PascalCase** names matching the Win32 exports (e.g. + ``AuditQuerySystemPolicy``) for parity with MSDN and headers. + """ + global _API + if _API is None: + _API = _load_advapi32() + return _API + + +def _uuid_to_guid(subcategory_uuid: uuid.UUID) -> _GUID: + """ + Pack a Python :class:`uuid.UUID` into the Win32 ``GUID`` memory layout. + + ``UUID.bytes_le`` matches how ``GUID`` fields are ordered in RAM for the + ``Data1``/``Data2``/``Data3``/``Data4`` split used by ``AuditQuerySystemPolicy``. + """ + uuid_bytes_le = subcategory_uuid.bytes_le + guid_struct = _GUID() + guid_struct.Data1, guid_struct.Data2, guid_struct.Data3 = struct.unpack( + " tuple[str, str]: + """Map ``AuditingInformation`` bitmask to (Inclusion Setting, Setting Value str).""" + success_on = bool(audit_mask & _AUDIT_SUCCESS) + failure_on = bool(audit_mask & _AUDIT_FAILURE) + if success_on and failure_on: + return "Success and Failure", "3" + if success_on: + return "Success", "1" + if failure_on: + return "Failure", "2" + return "No Auditing", "0" + + +@contextlib.contextmanager +def _enable_se_security_privilege(): + """ + Enable ``SeSecurityPrivilege`` on this process for the duration of the + ``with`` block (required for ``AuditSetSystemPolicy``). + """ + win32 = _api() + process_token_handle = w.HANDLE() + if not win32.OpenProcessToken( + win32.GetCurrentProcess(), + _TOKEN_ADJUST_PRIVILEGES | _TOKEN_QUERY, + ctypes.byref(process_token_handle), + ): + raise CommandExecutionError( + "OpenProcessToken failed", info={"errno": ctypes.get_last_error()} + ) + try: + security_privilege_luid = _LUID() + if not win32.LookupPrivilegeValueW( + None, "SeSecurityPrivilege", ctypes.byref(security_privilege_luid) + ): + raise CommandExecutionError( + "LookupPrivilegeValueW(SeSecurityPrivilege) failed", + info={"errno": ctypes.get_last_error()}, + ) + token_privileges = _TOKEN_PRIVILEGES() + token_privileges.PrivilegeCount = 1 + token_privileges.Privileges[0].Luid = security_privilege_luid + token_privileges.Privileges[0].Attributes = _SE_PRIVILEGE_ENABLED + if not win32.AdjustTokenPrivileges( + process_token_handle, + False, + ctypes.byref(token_privileges), + ctypes.sizeof(token_privileges), + None, + None, + ): + raise CommandExecutionError( + "AdjustTokenPrivileges failed", + info={"errno": ctypes.get_last_error()}, + ) + yield + finally: + win32.CloseHandle(process_token_handle) + + +def _query_system_policies(): + """ + Call ``AuditQuerySystemPolicy`` for every subcategory in + :data:`_AUDIT_SUBCATEGORY_METADATA`. + + Returns: + list: One tuple per row: + ``(category_name, subcategory_name, subcategory_uuid, audit_mask)`` — + ``audit_mask`` is the raw ``AuditingInformation`` ULONG (success/failure + bits per MSDN). + """ + metadata_rows = _AUDIT_SUBCATEGORY_METADATA + subcategory_count = len(metadata_rows) + + # Contiguous array of GUIDs — required; passing a single GUID pointer is a + # common marshaling mistake and yields ERROR_INVALID_PARAMETER. + guid_array_type = _GUID * subcategory_count + subcategory_guid_array = guid_array_type() + subcategory_uuids_ordered = [] + for index, (_category, _subcategory, guid_string) in enumerate(metadata_rows): + parsed_uuid = uuid.UUID(guid_string) + subcategory_uuids_ordered.append(parsed_uuid) + subcategory_guid_array[index] = _uuid_to_guid(parsed_uuid) + + # Output: pointer to heap array of AUDIT_POLICY_INFORMATION (same length). + allocated_policy_array_ptr = ctypes.POINTER(_AUDIT_POLICY_INFORMATION)() + win32 = _api() + if not win32.AuditQuerySystemPolicy( + subcategory_guid_array, + subcategory_count, + ctypes.byref(allocated_policy_array_ptr), + ): + err = ctypes.get_last_error() + raise CommandExecutionError( + "AuditQuerySystemPolicy failed", + info={"errno": err}, + ) + if not allocated_policy_array_ptr: + return [ + ( + metadata_rows[i][0], + metadata_rows[i][1], + subcategory_uuids_ordered[i], + 0, + ) + for i in range(subcategory_count) + ] + try: + results = [] + for index in range(subcategory_count): + policy_info_struct = allocated_policy_array_ptr[index] + results.append( + ( + metadata_rows[index][0], + metadata_rows[index][1], + subcategory_uuids_ordered[index], + int(policy_info_struct.AuditingInformation), + ) + ) + return results + finally: + win32.AuditFree(allocated_policy_array_ptr) + + +# (category, subcategory English name as in auditpol backup / set, GUID string) +# GUIDs align with Microsoft advanced audit subcategories (see e.g. PowerShell +# AuditPolicyDsc AuditPolicyResourceHelper). +_AUDIT_SUBCATEGORY_METADATA = [ + ("System", "Security State Change", "{0CCE9210-69AE-11D9-BED3-505054503030}"), + ("System", "Security System Extension", "{0CCE9211-69AE-11D9-BED3-505054503030}"), + ("System", "System Integrity", "{0CCE9212-69AE-11D9-BED3-505054503030}"), + ("System", "IPsec Driver", "{0CCE9213-69AE-11D9-BED3-505054503030}"), + ("System", "Other System Events", "{0CCE9214-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "Logon", "{0CCE9215-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "Logoff", "{0CCE9216-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "Account Lockout", "{0CCE9217-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "IPsec Main Mode", "{0CCE9218-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "IPsec Quick Mode", "{0CCE9219-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "IPsec Extended Mode", "{0CCE921A-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "Special Logon", "{0CCE921B-69AE-11D9-BED3-505054503030}"), + ( + "Logon/Logoff", + "Other Logon/Logoff Events", + "{0CCE921C-69AE-11D9-BED3-505054503030}", + ), + ("Logon/Logoff", "Network Policy Server", "{0CCE9243-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "User / Device Claims", "{0CCE9247-69AE-11D9-BED3-505054503030}"), + ("Logon/Logoff", "Group Membership", "{0CCE9249-69AE-11D9-BED3-505054503030}"), + ("Object Access", "File System", "{0CCE921D-69AE-11D9-BED3-505054503030}"), + ("Object Access", "Registry", "{0CCE921E-69AE-11D9-BED3-505054503030}"), + ("Object Access", "Kernel Object", "{0CCE921F-69AE-11D9-BED3-505054503030}"), + ("Object Access", "SAM", "{0CCE9220-69AE-11D9-BED3-505054503030}"), + ( + "Object Access", + "Certification Services", + "{0CCE9221-69AE-11D9-BED3-505054503030}", + ), + ( + "Object Access", + "Application Generated", + "{0CCE9222-69AE-11D9-BED3-505054503030}", + ), + ("Object Access", "Handle Manipulation", "{0CCE9223-69AE-11D9-BED3-505054503030}"), + ("Object Access", "File Share", "{0CCE9224-69AE-11D9-BED3-505054503030}"), + ( + "Object Access", + "Filtering Platform Packet Drop", + "{0CCE9225-69AE-11D9-BED3-505054503030}", + ), + ( + "Object Access", + "Filtering Platform Connection", + "{0CCE9226-69AE-11D9-BED3-505054503030}", + ), + ( + "Object Access", + "Other Object Access Events", + "{0CCE9227-69AE-11D9-BED3-505054503030}", + ), + ("Object Access", "Detailed File Share", "{0CCE9244-69AE-11D9-BED3-505054503030}"), + ("Object Access", "Removable Storage", "{0CCE9245-69AE-11D9-BED3-505054503030}"), + ( + "Object Access", + "Central Policy Staging", + "{0CCE9246-69AE-11D9-BED3-505054503030}", + ), + ( + "Privilege Use", + "Sensitive Privilege Use", + "{0CCE9228-69AE-11D9-BED3-505054503030}", + ), + ( + "Privilege Use", + "Non Sensitive Privilege Use", + "{0CCE9229-69AE-11D9-BED3-505054503030}", + ), + ( + "Privilege Use", + "Other Privilege Use Events", + "{0CCE922A-69AE-11D9-BED3-505054503030}", + ), + ("Detailed Tracking", "Process Creation", "{0CCE922B-69AE-11D9-BED3-505054503030}"), + ( + "Detailed Tracking", + "Process Termination", + "{0CCE922C-69AE-11D9-BED3-505054503030}", + ), + ("Detailed Tracking", "DPAPI Activity", "{0CCE922D-69AE-11D9-BED3-505054503030}"), + ("Detailed Tracking", "RPC Events", "{0CCE922E-69AE-11D9-BED3-505054503030}"), + ( + "Detailed Tracking", + "Plug and Play Events", + "{0CCE9248-69AE-11D9-BED3-505054503030}", + ), + ( + "Detailed Tracking", + "Token Right Adjusted Events", + "{0CCE924A-69AE-11D9-BED3-505054503030}", + ), + ("Policy Change", "Audit Policy Change", "{0CCE922F-69AE-11D9-BED3-505054503030}"), + ( + "Policy Change", + "Authentication Policy Change", + "{0CCE9230-69AE-11D9-BED3-505054503030}", + ), + ( + "Policy Change", + "Authorization Policy Change", + "{0CCE9231-69AE-11D9-BED3-505054503030}", + ), + ( + "Policy Change", + "MPSSVC Rule-Level Policy Change", + "{0CCE9232-69AE-11D9-BED3-505054503030}", + ), + ( + "Policy Change", + "Filtering Platform Policy Change", + "{0CCE9233-69AE-11D9-BED3-505054503030}", + ), + ( + "Policy Change", + "Other Policy Change Events", + "{0CCE9234-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Management", + "User Account Management", + "{0CCE9235-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Management", + "Computer Account Management", + "{0CCE9236-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Management", + "Security Group Management", + "{0CCE9237-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Management", + "Distribution Group Management", + "{0CCE9238-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Management", + "Application Group Management", + "{0CCE9239-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Management", + "Other Account Management Events", + "{0CCE923A-69AE-11D9-BED3-505054503030}", + ), + ("DS Access", "Directory Service Access", "{0CCE923B-69AE-11D9-BED3-505054503030}"), + ( + "DS Access", + "Directory Service Changes", + "{0CCE923C-69AE-11D9-BED3-505054503030}", + ), + ( + "DS Access", + "Directory Service Replication", + "{0CCE923D-69AE-11D9-BED3-505054503030}", + ), + ( + "DS Access", + "Detailed Directory Service Replication", + "{0CCE923E-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Logon", + "Credential Validation", + "{0CCE923F-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Logon", + "Kerberos Service Ticket Operations", + "{0CCE9240-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Logon", + "Other Account Logon Events", + "{0CCE9241-69AE-11D9-BED3-505054503030}", + ), + ( + "Account Logon", + "Kerberos Authentication Service", + "{0CCE9242-69AE-11D9-BED3-505054503030}", + ), +] + -# Although utils are often directly imported, it is also possible to use the -# loader. def __virtual__(): """ Only load if on a Windows system @@ -103,111 +602,106 @@ def __virtual__(): return __virtualname__ -def _auditpol_cmd(cmd): +def get_advaudit_policy_rows(): """ - Helper function for running the auditpol command + Return one row per advanced audit subcategory for LGPO and related code. - Args: - cmd (str): the auditpol command to run + Rows are built from ``AuditQuerySystemPolicy`` and the in-module GUID/name + table, so they do not depend on ``auditpol.exe`` or the host display + language. Returns: - list: A list containing each line of the return (splitlines) + list: A list of dictionaries, each with keys ``Machine Name``, + ``Policy Target``, ``Subcategory``, ``Subcategory GUID``, + ``Inclusion Setting``, ``Exclusion Setting``, and ``Setting Value``. + ``Machine Name`` is always ``""``; ``Policy Target`` is ``System``; + ``Subcategory`` is the English name used with :func:`set_setting`; + ``Subcategory GUID`` is braced uppercase; ``Setting Value`` is + ``"0"``–``"3"`` matching LGPO's ``audit.csv`` convention. Raises: - CommandExecutionError: If the command encounters an error + CommandExecutionError: If the Windows API call fails. """ - ret = salt.modules.cmdmod.run_all(cmd=f"auditpol {cmd}", python_shell=True) - if ret["retcode"] == 0: - return ret["stdout"].splitlines() - - msg = f"Error executing auditpol command: {cmd}\n" - msg += "\n".join(ret["stdout"]) - raise CommandExecutionError(msg) + rows_out = [] + for (_metadata_category, subcategory, guid_string), query_row in zip( + _AUDIT_SUBCATEGORY_METADATA, _query_system_policies() + ): + _, _, _, audit_mask = query_row + inclusion, setting_val = _mask_to_labels(audit_mask) + guid_braced = guid_string.upper() + rows_out.append( + { + "Machine Name": "", + "Policy Target": "System", + "Subcategory": subcategory, + "Subcategory GUID": guid_braced, + "Inclusion Setting": inclusion, + "Exclusion Setting": "", + "Setting Value": setting_val, + } + ) + return rows_out def get_settings(category="All"): """ - Get the current configuration for all audit settings specified in the - category + Get the current configuration for all audit settings in the given category. + + Reads effective policy via ``AuditQuerySystemPolicy`` (not ``auditpol + /get``), so results use English subcategory keys and English value labels + regardless of OS language. Args: category (str): - One of the nine categories to return. Can also be ``All`` to return - the settings for all categories. Valid options are: - - - Account Logon - - Account Management - - Detailed Tracking - - DS Access - - Logon/Logoff - - Object Access - - Policy Change - - Privilege Use - - System - - All - - Default value is ``All`` + One of the nine categories, or ``All`` / ``*`` for every + subcategory. Names are matched case-insensitively against + :data:`categories`. Returns: - dict: A dictionary containing all subcategories for the specified - category along with their current configuration + dict: Maps each **English** subcategory name to one of ``No Auditing``, + ``Success``, ``Failure``, or ``Success and Failure``. Raises: - KeyError: On invalid category - CommandExecutionError: If an error is encountered retrieving the settings - - Usage: - - .. code-block:: python - - import salt.utils.win_lgpo_auditpol - - # Get current state of all audit settings - salt.utils.win_lgpo_auditpol.get_settings() - - # Get the current state of all audit settings in the "Account Logon" - # category - salt.utils.win_lgpo_auditpol.get_settings(category="Account Logon") + KeyError: If ``category`` is not recognized. + CommandExecutionError: If the Windows API call fails. """ - # Parameter validation if category.lower() in ["all", "*"]: - category = "*" - elif category.lower() not in [x.lower() for x in categories]: - raise KeyError(f'Invalid category: "{category}"') - - cmd = f'/get /category:"{category}"' - results = _auditpol_cmd(cmd) + want = None + else: + want = None + for c in categories: + if c.lower() == category.lower(): + want = c + break + if want is None: + raise KeyError(f'Invalid category: "{category}"') ret = {} - # Skip the first 2 lines - for line in results[3:]: - if " " in line.strip(): - ret.update(dict(list(zip(*[iter(re.split(r"\s{2,}", line.strip()))] * 2)))) + for (category_name, subcategory_name, _subcategory_uuid), query_row in zip( + _AUDIT_SUBCATEGORY_METADATA, _query_system_policies() + ): + _, _, _, audit_mask = query_row + if want is not None and category_name != want: + continue + label, _setting_value_str = _mask_to_labels(audit_mask) + ret[subcategory_name] = label return ret def get_setting(name): """ - Get the current configuration for the named audit setting + Get the current configuration for a single subcategory. Args: - name (str): The name of the setting to retrieve + name (str): English subcategory name (matched case-insensitively). Returns: - str: The current configuration for the named setting + str: One of ``No Auditing``, ``Success``, ``Failure``, or + ``Success and Failure``. Raises: - KeyError: On invalid setting name - CommandExecutionError: If an error is encountered retrieving the settings - - Usage: - - .. code-block:: python - - import salt.utils.win_lgpo_auditpol - - # Get current state of the "Credential Validation" setting - salt.utils.win_lgpo_auditpol.get_setting(name='Credential Validation') + KeyError: If ``name`` is not a known subcategory. + CommandExecutionError: If querying the current policy fails. """ current_settings = get_settings(category="All") for setting in current_settings: @@ -217,89 +711,106 @@ def get_setting(name): def _get_valid_names(): + """ + Return lowercase English subcategory names valid for :func:`set_setting`. + + Cached on ``__context__['auditpol.valid_names']`` until cleared after a + successful :func:`set_setting`. + """ if "auditpol.valid_names" not in __context__: - settings = get_settings(category="All") - __context__["auditpol.valid_names"] = [k.lower() for k in settings] + settings_map = get_settings(category="All") + __context__["auditpol.valid_names"] = [k.lower() for k in settings_map] return __context__["auditpol.valid_names"] def set_setting(name, value): """ - Set the configuration for the named audit setting + Set the auditing bitmask for one subcategory. - Args: + Enables ``SeSecurityPrivilege`` on the current process token for the + duration of the ``AuditSetSystemPolicy`` call. + Args: name (str): - The name of the setting to configure - + English subcategory name (same strings as returned by + :func:`get_settings`). value (str): - The configuration for the named value. Valid options are: - - - No Auditing - - Success - - Failure - - Success and Failure + One of ``No Auditing``, ``Success``, ``Failure``, or + ``Success and Failure`` (matched case-insensitively). Returns: - bool: True if successful + bool: ``True`` on success. Raises: - KeyError: On invalid ``name`` or ``value`` - CommandExecutionError: If an error is encountered modifying the setting - - Usage: - - .. code-block:: python - - import salt.utils.win_lgpo_auditpol - - # Set the state of the "Credential Validation" setting to Success and - # Failure - salt.utils.win_lgpo_auditpol.set_setting(name='Credential Validation', - value='Success and Failure') - - # Set the state of the "Credential Validation" setting to No Auditing - salt.utils.win_lgpo_auditpol.set_setting(name='Credential Validation', - value='No Auditing') + KeyError: On invalid ``name`` or ``value``. + CommandExecutionError: If privilege adjustment or + ``AuditSetSystemPolicy`` fails. """ - # Input validation if name.lower() not in _get_valid_names(): raise KeyError(f"Invalid name: {name}") + audit_bitmask = None for setting in settings: if value.lower() == setting.lower(): - cmd = f'/set /subcategory:"{name}" {settings[setting]}' + audit_bitmask = int(settings[setting]) break - else: + if audit_bitmask is None: raise KeyError(f"Invalid setting value: {value}") - _auditpol_cmd(cmd) + resolved_subcategory_name = None + resolved_subcategory_uuid = None + for _meta_cat, meta_subcategory, guid_string in _AUDIT_SUBCATEGORY_METADATA: + if name.lower() == meta_subcategory.lower(): + resolved_subcategory_name = meta_subcategory + resolved_subcategory_uuid = uuid.UUID(guid_string) + break + if resolved_subcategory_uuid is None: + raise KeyError(f"Invalid name: {name}") + audit_policy_info = _AUDIT_POLICY_INFORMATION() + audit_policy_info.AuditSubcategoryGuid = _uuid_to_guid(resolved_subcategory_uuid) + audit_policy_info.AuditingInformation = audit_bitmask + audit_policy_info.AuditCategoryGuid = _GUID() + + win32 = _api() + with _enable_se_security_privilege(): + policy_count = 1 + if not win32.AuditSetSystemPolicy( + ctypes.byref(audit_policy_info), policy_count + ): + err = ctypes.get_last_error() + raise CommandExecutionError( + "AuditSetSystemPolicy failed", + info={ + "errno": err, + "name": resolved_subcategory_name, + "value": value, + }, + ) + + __context__.pop("auditpol.valid_names", None) return True def get_auditpol_dump(): """ - Gets the contents of an auditpol /backup. Used by the LGPO module to get - fieldnames and GUIDs for Advanced Audit policies. - - Returns: - list: A list of lines form the backup file - - Usage: + Return advanced audit policy rows as CSV **text lines** (UTF-8). - .. code-block:: python + This does not run ``auditpol /backup`` or read a temp file. It formats the + same data as :func:`get_advaudit_policy_rows` using :mod:`csv`, so each line + is a normal Unicode string (newlines ``\\n``). - import salt.utils.win_lgpo_auditpol + Returns: + list: Lines including a header row, suitable for passing to + :class:`csv.DictReader` if needed. - dump = salt.utils.win_lgpo_auditpol.get_auditpol_dump() + Raises: + CommandExecutionError: If building rows fails (e.g. API error from + :func:`get_advaudit_policy_rows`). """ - # Just get a temporary file name - # NamedTemporaryFile will delete the file it creates by default on Windows - with tempfile.NamedTemporaryFile(suffix=".csv") as tmp_file: - csv_file = tmp_file.name - - cmd = f"/backup /file:{csv_file}" - _auditpol_cmd(cmd) - - with salt.utils.files.fopen(csv_file) as fp: - return fp.readlines() + buf = io.StringIO(newline="") + writer = csv.DictWriter(buf, fieldnames=_FIELDNAMES, lineterminator="\n") + writer.writeheader() + for row in get_advaudit_policy_rows(): + writer.writerow(row) + buf.seek(0) + return buf.readlines() diff --git a/tests/pytests/unit/modules/win_lgpo/test_adv_audit.py b/tests/pytests/unit/modules/win_lgpo/test_adv_audit.py index c982ff6abb07..5628692e4574 100644 --- a/tests/pytests/unit/modules/win_lgpo/test_adv_audit.py +++ b/tests/pytests/unit/modules/win_lgpo/test_adv_audit.py @@ -32,6 +32,7 @@ def configure_loader_modules(tmp_path): "file.write": win_file.write, }, "__utils__": { + "auditpol.get_advaudit_policy_rows": auditpol.get_advaudit_policy_rows, "auditpol.get_auditpol_dump": auditpol.get_auditpol_dump, "auditpol.set_setting": auditpol.set_setting, }, @@ -136,7 +137,11 @@ def test_get_value(setting, expected): def test_get_defaults(): patch_context = patch.dict(win_lgpo.__context__, {}) patch_salt = patch.dict( - win_lgpo.__utils__, {"auditpol.get_auditpol_dump": auditpol.get_auditpol_dump} + win_lgpo.__utils__, + { + "auditpol.get_advaudit_policy_rows": auditpol.get_advaudit_policy_rows, + "auditpol.get_auditpol_dump": auditpol.get_auditpol_dump, + }, ) with patch_context, patch_salt: assert "Machine Name" in win_lgpo._get_advaudit_defaults("fieldnames") diff --git a/tests/pytests/unit/modules/win_lgpo/test_mechanisms.py b/tests/pytests/unit/modules/win_lgpo/test_mechanisms.py index 5ce9ce2a4fb1..0f3892a8bc2c 100644 --- a/tests/pytests/unit/modules/win_lgpo/test_mechanisms.py +++ b/tests/pytests/unit/modules/win_lgpo/test_mechanisms.py @@ -38,6 +38,7 @@ def configure_loader_modules(tmp_path): "cachedir": str(cachedir), }, "__utils__": { + "auditpol.get_advaudit_policy_rows": win_lgpo_auditpol.get_advaudit_policy_rows, "auditpol.get_auditpol_dump": win_lgpo_auditpol.get_auditpol_dump, "reg.read_value": win_reg.read_value, }, diff --git a/tests/pytests/unit/modules/win_lgpo/test_point_print_enabled.py b/tests/pytests/unit/modules/win_lgpo/test_point_print_enabled.py index 4d39311f8e4c..0ce3cdaad4a1 100644 --- a/tests/pytests/unit/modules/win_lgpo/test_point_print_enabled.py +++ b/tests/pytests/unit/modules/win_lgpo/test_point_print_enabled.py @@ -35,6 +35,7 @@ def configure_loader_modules(tmp_path): "cachedir": str(cachedir), }, "__utils__": { + "auditpol.get_advaudit_policy_rows": win_lgpo_auditpol.get_advaudit_policy_rows, "auditpol.get_auditpol_dump": win_lgpo_auditpol.get_auditpol_dump, "reg.read_value": win_reg.read_value, }, diff --git a/tests/pytests/unit/modules/win_lgpo/test_point_print_nc.py b/tests/pytests/unit/modules/win_lgpo/test_point_print_nc.py index 2196c7624c3a..a4103e57ac51 100644 --- a/tests/pytests/unit/modules/win_lgpo/test_point_print_nc.py +++ b/tests/pytests/unit/modules/win_lgpo/test_point_print_nc.py @@ -42,6 +42,7 @@ def configure_loader_modules(tmp_path): "cachedir": str(cachedir), }, "__utils__": { + "auditpol.get_advaudit_policy_rows": win_lgpo_auditpol.get_advaudit_policy_rows, "auditpol.get_auditpol_dump": win_lgpo_auditpol.get_auditpol_dump, "reg.read_value": win_reg.read_value, }, diff --git a/tests/pytests/unit/modules/win_lgpo/test_policy_resources.py b/tests/pytests/unit/modules/win_lgpo/test_policy_resources.py index 8d49468792a4..1c44064d71ec 100644 --- a/tests/pytests/unit/modules/win_lgpo/test_policy_resources.py +++ b/tests/pytests/unit/modules/win_lgpo/test_policy_resources.py @@ -35,6 +35,7 @@ def configure_loader_modules(tmp_path): "cachedir": str(cachedir), }, "__utils__": { + "auditpol.get_advaudit_policy_rows": win_lgpo_auditpol.get_advaudit_policy_rows, "auditpol.get_auditpol_dump": win_lgpo_auditpol.get_auditpol_dump, "reg.read_value": win_reg.read_value, }, diff --git a/tests/pytests/unit/utils/win_lgpo/test_auditpol.py b/tests/pytests/unit/utils/win_lgpo/test_auditpol.py index 85e53780efec..b851d64391d2 100644 --- a/tests/pytests/unit/utils/win_lgpo/test_auditpol.py +++ b/tests/pytests/unit/utils/win_lgpo/test_auditpol.py @@ -1,9 +1,10 @@ +import contextlib +import ctypes import random +from copy import copy import pytest -import salt.modules.cmdmod -import salt.utils.platform import salt.utils.win_lgpo_auditpol as win_lgpo_auditpol from tests.support.mock import MagicMock, patch @@ -18,12 +19,17 @@ def settings(): return ["No Auditing", "Success", "Failure", "Success and Failure"] +@pytest.fixture(autouse=True) +def reset_auditpol_api_cache(): + with patch.object(win_lgpo_auditpol, "_API", new=None): + yield + + @pytest.fixture def configure_loader_modules(): return { win_lgpo_auditpol: { "__context__": {}, - "__salt__": {"cmd.run_all": salt.modules.cmdmod.run_all}, } } @@ -52,21 +58,38 @@ def test_get_setting_invalid_name(): def test_set_setting(settings): + def noop_security_privilege(): + return contextlib.nullcontext() + names = ["Credential Validation", "IPsec Driver", "File System", "SAM"] - mock_set = MagicMock(return_value={"retcode": 0, "stdout": "Success"}) - with patch.object(salt.modules.cmdmod, "run_all", mock_set): + mock_set = MagicMock(return_value=True) + real_api = win_lgpo_auditpol._load_advapi32() + patched_api = copy(real_api) + patched_api.AuditSetSystemPolicy = mock_set + with patch.object(win_lgpo_auditpol, "_API", patched_api): with patch.object( win_lgpo_auditpol, - "_get_valid_names", - return_value=[k.lower() for k in names], + "_enable_se_security_privilege", + noop_security_privilege, ): - for name in names: - value = random.choice(settings) - win_lgpo_auditpol.set_setting(name=name, value=value) - switches = win_lgpo_auditpol.settings[value] - cmd = f'auditpol /set /subcategory:"{name}" {switches}' - mock_set.assert_called_once_with(cmd=cmd, python_shell=True) - mock_set.reset_mock() + with patch.object( + win_lgpo_auditpol, + "_get_valid_names", + return_value=[k.lower() for k in names], + ): + for name in names: + value = random.choice(settings) + win_lgpo_auditpol.set_setting(name=name, value=value) + mask = win_lgpo_auditpol.settings[value] + mock_set.assert_called_once() + args, _kwargs = mock_set.call_args + assert args[1] == 1 + policy_ptr = ctypes.cast( + args[0], + ctypes.POINTER(win_lgpo_auditpol._AUDIT_POLICY_INFORMATION), + ) + assert policy_ptr.contents.AuditingInformation == mask + mock_set.reset_mock() def test_set_setting_invalid_setting(): @@ -109,3 +132,10 @@ def test_get_auditpol_dump(): found = True break assert found is True + + +def test_get_advaudit_policy_rows_matches_fieldnames(): + rows = win_lgpo_auditpol.get_advaudit_policy_rows() + assert rows + expected = win_lgpo_auditpol._FIELDNAMES + assert list(rows[0].keys()) == expected