Skip to content

Audit log

The Audit Log tile

Show-AuditLogAvalonia (or the Audit Log tile of Start-HGPM) shows the audit log of HGPM: each change the domain accepted, from a console or a command, newest first. Who, when, from which computer, on which domain controller, what and on which GPO, OU or setting. It needs no right on the domain: only the read of the folder of the log.

Show-AuditLogAvalonia
Show-AuditLogAvalonia -Path \\files\gpo-audit                # another folder
Show-AuditLogAvalonia -Server dc.hardenbox.labo -Language fr-FR

The audit log

The audit log on Windows, the central share

The changes

Column
Time when, in the time of the computer (the log keeps UTC)
Account the account of the directory that made the change
Action what, by its name: GPO created, Link changed, Settings of a GPO saved, GPO approved...
GPO or object the GPO, else the OU, the domain or the configuration
Tier the folder of the central log it is in: Tier 0, 1, 2, Unassigned
Check whether the change is the one its account wrote: Verified, Altered, Tier differs, Owner differs, Not chained (below)
Computer, Domain from where, on which domain
  • The period: the last 24 hours, 7 days, 30 days (at first) or everything.
  • The domain and the action: those the log holds.
  • The search reads every text of a change, its details included: each word must be found (registry.pol tiers1).
  • The details of the selected change: everything the log keeps of it, as the domain controller, the GPO and its identifier, and its details (the options of a link changed, the file saved with its SHA-256 and its new version, the rights of the creators at an approval...).
  • Export to CSV...: the changes shown, as they are filtered, the time in UTC (ISO 8601), the details as JSON.

Where the log is

The folder is Audit.Path of the configuration of the domain: the window reads the domain once shown (-Server, else the domain of the computer). Without a domain, or when the configuration sets no folder, it is the folder Audit of the backups of the account, which only its console writes. Browse... and Refresh read another folder, or the same one again.

The log is on by default: Audit.Enabled = $false in the configuration of the domain stops it, the changes still made. See the audit log for what each line holds.

# The same changes in a script
Get-PSGPOAuditLog -After (Get-Date).AddDays(-7) -Action SaveGpoFile, ApproveGpo |
    Export-Csv .\audit.csv -NoTypeInformation

The central log, a folder per tier

A log the consoles share is on a share of a file server, Audit.Path of the configuration of the domain. Not SYSVOL: it is copied to every domain controller (DFSR), the log would grow on each of them, and two domain controllers adding to the same file would lose lines; Samba does not copy SYSVOL at all.

The share has a folder per tier, and the permissions of the roles:

Folder Writes there (its own file) Reads it
Tier0 the approvers of the workflow (Workflow.Approvers), Domain Admins Tier 0, the readers
Tier1 the groups of Audit.Tiers.Tier1 Tier 1, Tier 0, the readers
Tier2 the groups of Audit.Tiers.Tier2 Tier 2, Tier 0, the readers
Unassigned a user of the domain in no tier (Domain Users, not the computers) Tier 0, the readers
Archive the retention (Audit.Purgers) Tier 0, the readers
the share itself nobody listed by the groups of the tiers and the readers; any other account only goes through it, to Unassigned
  • Each account adds its changes to its own file: <tier>\<domain>-<yyyy-MM>-<account>-<computer>.jsonl. No two consoles write the same file. Its owner only appends to it: it neither writes over it, deletes it nor changes its permissions; nobody but the administrators does.
  • Tier 0 reads every tier, an account of Tier 1 or Tier 2 the changes of its tier only, the readers (Audit.Readers, the auditors) everything, without writing. The window shows what the account can read, and the tier of each change.
  • The tier of an account is read in its groups when the domain is read: an administrator of the domain or an approver is Tier 0, then the groups of Audit.Tiers.

1. The share

On a Windows file server, as an administrator:

Download windows/New-PSGPOShare.ps1

.\New-PSGPOShare.ps1 -Name gpo-audit                     # \\<server>\gpo-audit, D:\Shares\gpo-audit
.\New-PSGPOShare.ps1 -Name gpo-audit -Path E:\Shares\GPO-Audit -WhatIf
windows/New-PSGPOShare.ps1
#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Creates a share of HGPM on a Windows file server: the central audit log or the central backups.
.DESCRIPTION
    A folder and its SMB share. The share lets the authenticated users change files and the
    administrators do everything: the NTFS permissions decide, written by
    Initialize-PSGPOAuditShare (the audit log, a folder per tier) or Initialize-PSGPOBackupShare
    (the backups, Tier 0 only) from the roles of the configuration of the domain. Until then the
    folder is for the administrators only.

    Run it as an administrator of the file server (not a domain controller: SYSVOL is copied to
    every domain controller), once per share, then from a console of a domain administrator:

        Initialize-PSGPOAuditShare -Path \\<server>\gpo-audit
        Initialize-PSGPOBackupShare -Path \\<server>\gpo-backups

    and Audit.Path or Backup.Path in the Settings of the console. What is there is kept: the script
    can run again.
.PARAMETER Name
    Name of the share: gpo-audit, gpo-backups.
.PARAMETER Path
    The folder of the share; D:\Shares\<Name> by default.
.EXAMPLE
    .\New-PSGPOShare.ps1 -Name gpo-audit
.EXAMPLE
    .\New-PSGPOShare.ps1 -Name gpo-backups -Path E:\Shares\GPO-Backups -WhatIf
.LINK
    https://jm2k69.github.io/PSGPOTools/en/features/audit-log/
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [string]$Name = 'gpo-audit',
    [string]$Path
)

$ErrorActionPreference = 'Stop'
if (-not $Path) { $Path = "D:\Shares\$Name" }

if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
    if ($PSCmdlet.ShouldProcess($Path, 'Create the folder')) {
        $null = New-Item -ItemType Directory -Path $Path
        # The administrators and SYSTEM only, until Initialize-PSGPO...Share writes the roles.
        $acl = [System.Security.AccessControl.DirectorySecurity]::new()
        $acl.SetSecurityDescriptorSddlForm('D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)')
        Set-Acl -LiteralPath $Path -AclObject $acl
    }
}

if (Get-SmbShare -Name $Name -ErrorAction SilentlyContinue) {
    Write-Output "The share $Name exists: kept."
} elseif ($PSCmdlet.ShouldProcess("\\$env:COMPUTERNAME\$Name", 'Create the share')) {
    # Share permissions: change for the accounts of the domain, the NTFS permissions decide.
    $null = New-SmbShare -Name $Name -Path $Path -FullAccess 'BUILTIN\Administrators' -ChangeAccess 'NT AUTHORITY\Authenticated Users' `
        -FolderEnumerationMode AccessBased -Description "HGPM: $Name"
    Write-Output "Share \\$env:COMPUTERNAME\$Name created: $Path"
}

On a Samba server (a member server, or the domain controller of a lab), as root:

Download linux/New-PSGPOShare.sh

sudo ./New-PSGPOShare.sh                             # //<server>/gpo-audit, /srv/gpo-audit
sudo ./New-PSGPOShare.sh gpo-audit /srv/gpo-audit
linux/New-PSGPOShare.sh
#!/usr/bin/env bash
# Creates a share of HGPM on a Samba server (an AD domain controller or a member server): the
# central audit log (gpo-audit) or the central backups of the GPOs (gpo-backups). A folder, and a
# section of smb.conf whose permissions are the NT ACLs that Initialize-PSGPOAuditShare or
# Initialize-PSGPOBackupShare write (acl_xattr), not those of the file system.
#
#   sudo ./New-PSGPOShare.sh                                  # gpo-audit, /srv/gpo-audit
#   sudo ./New-PSGPOShare.sh gpo-backups /srv/gpo-backups
#
# Then, from a console with the Kerberos ticket of a domain administrator (kinit administrator):
#
#   Initialize-PSGPOAuditShare -Path //<server>/gpo-audit -Server <domain controller>
#   Initialize-PSGPOBackupShare -Path //<server>/gpo-backups -Server <domain controller>
#
# and Audit.Path or Backup.Path in the Settings of the console (the configuration of the domain).
# What is there is kept: the script can run again.
#
# https://jm2k69.github.io/PSGPOTools/en/features/audit-log/
set -euo pipefail

share="${1:-gpo-audit}"
folder="${2:-/srv/$share}"
config="$(smbd -b | awk '/CONFIGFILE:/ { print $2 }')"
config="${config:-/etc/samba/smb.conf}"

if [ "$(id -u)" -ne 0 ]; then
    echo "Run it as root: sudo $0 $*" >&2
    exit 1
fi

# The folder: every account of the domain reaches it, the NT ACLs decide what each does there.
# The sticky bit: an account cannot delete the file of another one, even from the server.
mkdir -p "$folder"
chmod 1777 "$folder"

# The share: NT ACLs kept in extended attributes, the POSIX ACLs left aside.
if grep -q "^\[$share\]" "$config"; then
    echo "[$share] is already in $config: kept."
else
    cp "$config" "$config.$(date +%Y%m%d-%H%M%S).bak"
    cat >> "$config" <<EOF

[$share]
    path = $folder
    read only = no
    vfs objects = acl_xattr
    acl_xattr:ignore system acls = yes
    map acl inherit = yes
    store dos attributes = yes
    comment = HGPM: $share
EOF
    echo "[$share] added to $config."
fi

testparm -s "$config" > /dev/null
smbcontrol all reload-config
echo "Share //$(hostname -f)/$share ready: $folder"

2. The roles, in the configuration of the domain

In Settings of the console (an administrator of the domain), or Configuration.psd1:

Audit = @{
    Path = '\\files.contoso.com\gpo-audit'
    Tiers = @{ Tier1 = 'GPO-Tier1-Admins'; Tier2 = 'GPO-Tier2-Admins' }
    Readers = 'GPO-Auditors'
}

3. The folders and their permissions

Initialize-PSGPOAuditShare makes the folders of the tiers and writes their permissions from the roles. Again each time the groups of the tiers change: the files are kept.

Initialize-PSGPOAuditShare -WhatIf
Initialize-PSGPOAuditShare                                   # Audit.Path of the configuration
Initialize-PSGPOAuditShare -Path //files.contoso.com/gpo-audit -Server dc.contoso.com    # Linux

4. The logs of the consoles, moved into it

The changes the consoles wrote before, in the folder Audit of the backups of their account:

Import-PSGPOAuditLog -Path "$HOME/PSGPOTools Backups/Audit" -WhatIf
Import-PSGPOAuditLog -Path "$HOME/PSGPOTools Backups/Audit" -Remove

Each change goes to the file of its account, computer and month, in the folder of the tier of its account; a change already there is not written twice, and -Remove deletes the files read.

A log you can trust

The log is evidence: a technician must not change what it did, nor write in the name of another.

  • Append only: the owner of a file has FILE_APPEND_DATA, not FILE_WRITE_DATA (0x12019d). Windows appends at the end of the file whatever the writer asks; Samba refuses to open it to write (smbclient put, del: access denied), but lets a handle opened to append write anywhere in it. On Linux the console appends with the Python bindings of Samba, not smbclient get and put (the owner cannot write the file, and each line copied the whole file).
  • Chained lines: each line starts with Seq (1, 2...) and PrevHash, the SHA-256 of the previous line of its file. A line changed or removed in the middle breaks the chain: the next line is Altered.
  • Checked when read: the Check column says Verified when the line follows the previous one, its tier is the folder of its file (Tier differs otherwise: a line of Tier 0 written in the folder of Tier 1), and its file belongs to its account (AccountSid, Owner differs otherwise), or to the account that imported it (ImportedBy). The lines written before 0.1.15 are Not chained.
  • Nothing lost: the change goes to the log of the system first, then to the share. A share that cannot be reached keeps the change in audit-waiting.jsonl, in the folder of the technical log, written with the next change (a warning says how many wait).
  • A file of its name someone else made (a technician of the same tier can create a file in its folder): the change goes to a file of a new name next to it, and a warning says so.
  • Initialize-PSGPOAuditShare makes the files already on the share append-only too (Samba does not pass the permissions of a folder to its files; Windows does).

The chain shows a line changed, not a whole file rewritten with a new chain: the Samba limit above needs a deliberate tool. The log of the system, sent to a SIEM, keeps a copy of each change out of reach of the technicians. Hardening says what was tested.

Retention and archives

The central log keeps Audit.RetentionMonths months (13 by default; 0 keeps everything): this month and the ones before it. Invoke-PSGPOAuditRetention puts the files of an older month, from every tier, in Archive\audit-<yyyy-MM>.zip (their folder in it), checks the archive holds them, then deletes them; a month already archived gets the files written since. The folder Archive has the permissions of the share: Tier 0 and the readers read it. The retention is written to the log itself (PurgeAuditLog).

Only the administrators of the domain and the purgers delete files of the share. Plan it as a purger, not as an administrator: put a dedicated account in a group of Audit.Purgers, then run Initialize-PSGPOAuditShare again. The purgers read every tier, delete the files of the old months and write the archives, with no other right in the domain.

A group managed service account: Windows keeps its password, the task stores none.

# Once, as an administrator of the domain (the KDS root key exists already when gMSAs are used)
New-ADServiceAccount -Name hgpm-purge -DNSHostName hgpm-purge.contoso.com -PrincipalsAllowedToRetrieveManagedPassword 'ADMIN-SERVER$'
Add-ADGroupMember -Identity 'GPO-Audit-Purgers' -Members 'hgpm-purge$'   # Audit.Purgers = 'GPO-Audit-Purgers'
Install-ADServiceAccount hgpm-purge                                       # on ADMIN-SERVER

Invoke-PSGPOAuditRetention -WhatIf
# Each month, as the gMSA: -LogonType Password with no password, Windows gets it
$action = New-ScheduledTaskAction -Execute 'pwsh.exe' -Argument '-NoProfile -Command "Import-Module HGPM; Invoke-PSGPOAuditRetention"'
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\hgpm-purge$' -LogonType Password
Register-ScheduledTask -TaskName 'HGPM audit retention' -Action $action -Principal $principal -Trigger (New-ScheduledTaskTrigger -Weekly -WeeksInterval 4 -DaysOfWeek Sunday -At 3am)

A dedicated account of the domain, in a group of Audit.Purgers; its keytab readable by the account that runs the retention only.

# Once, on a domain controller of Samba, as root
samba-tool user add hgpm-purge --random-password
samba-tool group addmembers GPO-Audit-Purgers hgpm-purge
samba-tool domain exportkeytab /root/hgpm-purge.keytab --principal=hgpm-purge
# Copied to the computer of the retention, for the account hgpm only
sudo install -o hgpm -g hgpm -m 600 hgpm-purge.keytab /etc/hgpm/hgpm-purge.keytab

# Each month
echo '0 3 1 * * hgpm kinit -k -t /etc/hgpm/hgpm-purge.keytab hgpm-purge && pwsh -NoProfile -Command "Import-Module HGPM; Invoke-PSGPOAuditRetention -Server dc.contoso.com"' | sudo tee /etc/cron.d/hgpm-audit-retention

The log of the system, for a SIEM

With Log.SystemLog (on by default), each change of the audit log and each error of the technical log also goes to the log of the system, where a SIEM collects it (the changes made with other tools: audit on the domain controllers):

Windows Linux
Where the event log Application, source HGPM syslog / journald, tag hgpm (logger)
A change Information, the event ID of its action user.notice, [<event ID>]
A warning, an error Warning 2000, Error 3000 user.warning, user.err
The posture (Test-PSGPOSecurityPosture -SystemLog) 4000 a summary (Information; Warning with a High finding), Warning 4001 each High finding [4000], [4001]
Once per computer Register-PSGPOEventSource, as an administrator nothing

The events of HGPM in the Event Viewer

The message says what, by whom, from where and on what, then the entry of the audit log as JSON:

HGPM audit: ApproveGpo by HARDENBOX\Administrator from dc on hardenbox.labo: T1 - Servers - Proxy - CN={AFE8...}
{"Format":"PSGPOTools.Audit/1","Time":"2026-10-24T15:44:02Z","Domain":"hardenbox.labo",...}
Event ID Action Event ID Action
1001 NewGpo 1023 SetBlockInheritance
1002 RenameGpo 1030 SetGpoPermission
1003 SetGpoStatus 1031 SetGpoSecurityFiltering
1004 RemoveGpo 1032 NewFilteringGroups
1005 RemoveDeletedGpoBackup 1040 SetWmiFilter
1006 RestoreGpo 1041 NewWmiFilter
1007 ImportGpo 1042 EditWmiFilter
1008 SaveGpoFile 1043 RemoveWmiFilter
1009 ImportPolicyRuleSet 1044 SetWmiFilterPermission
1010 PendingGpo 1050 NewPasswordPolicy
1011 ApproveGpo 1051 SetPasswordPolicy
1020 NewLink 1052 RemovePasswordPolicy
1021 SetLink 1060 SaveConfiguration
1022 RemoveLink 1061 InstallAdmx
1062 InitializeAuditShare 1063 InitializeBackupShare
1012 SetGpoComment 1064 PurgeAuditLog
1013 SaveStarterGpo 1014 RemoveStarterGpo
2000, 3000 a warning, an error
4000 the posture: a summary 4001 the posture: a High finding
Register-PSGPOEventSource                                                # Windows, once, as an administrator
Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'HGPM' } -MaxEvents 20
journalctl -t hgpm --since today                                         # Linux

The technical log

What goes wrong where a console runs, to find the cause afterwards: the errors of LDAP, of SYSVOL, of the windows, and the commands of the system (samba-tool, smbclient, python3, secedit, auditpol) that fail, with the command, its exit code and what it wrote. It is the log of the computer and of its user, not of the domain:

Where %LOCALAPPDATA%\PSGPOTools\Logs (Windows), ~/.local/share/PSGPOTools/Logs (Linux)
Files hgpm-<yyyy-MM-dd>.log, an entry per line, its details indented below it
What Log.Level: Error, Warning (by default), Information (the commands that fail), Verbose (every command)
How long Log.RetentionDays days (30), deleted when the console first writes there
2026-10-26T09:14:03.512Z ERROR       LDAP         LDAP connection to dc.hardenbox.labo:389 failed: ...
    Exception: System.DirectoryServices.Protocols.LdapException: The LDAP server is unavailable.
2026-10-26T09:15:40.077Z INFORMATION smbclient    Exit code 1.
    Command: smbclient //dc.hardenbox.labo/gpo-audit --use-kerberos=required -c 'ls'
    Error: NT_STATUS_ACCESS_DENIED listing \*

To find a problem: Log.Level = 'Verbose' in Settings, the problem again, then the file of the day.