Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-2022, windows-2025]
# windows-11-arm is a client SKU on ARM64 (Windows 10/11 audit profile, Sysmon64a).
os: [windows-2022, windows-2025, windows-11-arm]
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.os == 'windows-11-arm' }}
steps:
- uses: actions/checkout@v4
- name: Test with Windows PowerShell 5.1
Expand Down
26 changes: 23 additions & 3 deletions docs/what-gets-logged.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,33 @@
| Legacy override | `SCENoApplyLegacyAuditPolicy=1` so advanced audit policy wins | n/a |
| PowerShell | Script Block (incl. invocation start/stop), Module (`*`), Transcription to `C:\ProgramData\WinLogs\transcripts`; set for Windows PowerShell and PowerShell 7 | Microsoft-Windows-PowerShell/Operational 4103/4104, transcript files |
| Sysmon | sysmon-modular balanced config (process, network, DNS, image load, registry, WMI, pipes, ...) | Microsoft-Windows-Sysmon/Operational |
| Channels enabled / enlarged | Task Scheduler/Operational, DNS-Client/Operational (both usually off), Defender, WMI-Activity, BITS-Client, Firewall, TerminalServices, CodeIntegrity, System, Application | each channel |
| Channels enabled / enlarged | Task Scheduler, DNS-Client, DriverFrameworks-UserMode (usually off); Defender, WMI-Activity, BITS-Client, Firewall, TerminalServices, CodeIntegrity, NTLM, Security-Mitigations, PrintService, SmbClient Security, AppLocker (4 logs), Diagnosis-Scripted, PowerShellCore, System, Application | each channel |
| Sizes | Security, Sysmon, PowerShell: `-LogSizeMB` (default 1024). Others: up to 256 MB. Logs are never shrunk. | n/a |

Changes from the original audit policy: added **PNP Activity** (USB / device insertion, 6416) and set **Other Object Access Events** to Success and Failure (scheduled task creation 4698/4702).
## Client vs Server

`Install-WinLogs.ps1` picks an audit profile automatically (`-AuditProfile Auto|Workstation|Server`):

| | Windows 10 / 11 | Windows Server (incl. domain controllers) |
|---|---|---|
| Base policy (`audit.csv`) | yes | yes |
| Kerberos Authentication Service and Service Ticket Operations (4768/4769/4771...), Directory Service Access, Directory Service Replication (`audit-server.csv`) | no (these only generate events on a DC) | yes |
| Extra channels | n/a | Directory Service, DNS Server, DNS Server Audit, DFS Replication (only if the role is installed) |

## Audit policy changes from the original 2016 baseline
Added: PNP Activity (USB / device insertion, 6416), Group Membership (4627), and Other Object Access Events set to Success and Failure (scheduled task creation 4698/4702).
Turned on: SAM (4661 handle requests to the SAM), Other Policy Change Events, MPSSVC Rule-Level Policy Change (firewall rule changes, 4946-4948), DPAPI Activity (4692/4693).
Still off on purpose because they are very noisy: Handle Manipulation, Non Sensitive Privilege Use, Other Privilege Use, Filtering Platform Packet Drop, Detailed Directory Service Replication.

## Per-object auditing (SACLs)
The File System and Registry audit categories only log access to objects that carry an audit rule. `windows/sacls.psd1` lists what gets one (success audit for Everyone) and is meant to be edited:
- Registry: Run / RunOnce (native and WOW6432Node), Policies\Explorer\Run, Active Setup, Winlogon, Windows (AppInit_DLLs), Services, Session Manager, Image File Execution Options, SilentProcessExit, Lsa, SecurityProviders (WDigest), and the Defender policy key. Events: 4657 (value change), 4656/4663.
- Files: the all-users StartUp folder, `System32\Tasks`, `System32\drivers`, and the hosts file. Events: 4663 (and 4656).
- Per-user hives (HKCU Run keys) are not covered, since each user's hive is only loaded while that user is signed in.
- Tamper-protected keys (for example Defender) may refuse the rule; that is reported as a warning and the run continues.
- `-SkipObjectAuditing` leaves these out; `-Revert` removes exactly the rules it added.

Notes
- File System and Registry auditing only produce events for objects that have a SACL; the policy alone does not audit every file.
- Filtering Platform Connection (5156/5158) is very noisy and overlaps Sysmon event 3. Remove that row from `audit.csv` if logs roll too quickly.
- Transcripts are plain files that malware running as admin can delete. Fine for a lab, not evidence-grade.
- Everything is recorded in `C:\ProgramData\WinLogs\state.json`; `-Revert` restores it.
37 changes: 37 additions & 0 deletions tests/Test-WinLogs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ $repo = Split-Path $PSScriptRoot -Parent
$install = Join-Path $repo 'windows\Install-WinLogs.ps1'
$prep = Join-Path $repo 'windows\Prepare-Offline.ps1'
$state = Join-Path $env:ProgramData 'WinLogs\state.json'
. (Join-Path $repo 'windows\WinLogs.Acl.ps1')
$failed = @()

# Surface problems as GitHub Actions annotations (readable without downloading the full log).
Expand Down Expand Up @@ -44,6 +45,12 @@ Write-Host '== Baseline'
$cmdKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit'
$before = Reg $cmdKey 'ProcessCreationIncludeCmdLine_Enabled'
$auditBefore = Audit 'Process Creation'
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
$startup = Join-Path $env:ProgramData 'Microsoft\Windows\Start Menu\Programs\StartUp'
$runSaclBefore = Get-AuditRuleCount $runKey
$startupSaclBefore = Get-AuditRuleCount $startup
$isServer = (Get-CimInstance Win32_OperatingSystem).ProductType -ne 1
Write-Host " (server profile expected: $isServer)"

Write-Host '== -WhatIf changes nothing'
& $install -OfflinePath $offline -Force -WhatIf | Out-Null
Expand All @@ -62,6 +69,15 @@ Check 'Transcription policy' ((Reg "$psw\Transcription" 'EnableTranscript
Check 'PowerShell 7 policy mirrored' ((Reg 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging' 'EnableScriptBlockLogging') -eq 1)
Check 'Process Creation audited (Success)' ((Audit 'Process Creation') -match 'Success')
Check 'Security log >= 1 GB' ((Get-WinEvent -ListLog Security).MaximumSizeInBytes -ge 1GB)
Check 'Group Membership audited' ((Audit 'Group Membership') -match 'Success')
Check 'SAM audited (Success and Failure)' ((Audit 'SAM') -match 'Success and Failure')
if ($isServer) {
Check 'Kerberos Service Ticket audited (server overlay)' ((Audit 'Kerberos Service Ticket Operations') -match 'Success and Failure')
} else {
Check 'Kerberos not audited on client' ((Audit 'Kerberos Service Ticket Operations') -match 'No Auditing')
}
Check 'SACL on Run key' ((Get-AuditRuleCount $runKey) -gt $runSaclBefore)
Check 'SACL on StartUp folder' ((Get-AuditRuleCount $startup) -gt $startupSaclBefore)
Check 'Task Scheduler log enabled' ((Get-WinEvent -ListLog 'Microsoft-Windows-TaskScheduler/Operational').IsEnabled)

Write-Host '== Sysmon'
Expand All @@ -85,6 +101,25 @@ if ($svc) {
Check 'Sysmon event 1 recorded' ($sys1.Count -gt 0)
}

Write-Host '== Object auditing produces events'
$valName = 'WinLogsTest' + [guid]::NewGuid().ToString('N').Substring(0, 8)
$fileName = Join-Path $startup "$valName.txt"
New-ItemProperty -Path $runKey -Name $valName -Value 'C:\Windows\System32\cmd.exe' -PropertyType String -Force | Out-Null
Set-Content -Path $fileName -Value 'test'
function Wait-Event($id, $pattern) {
for ($i = 0; $i -lt 10; $i++) {
$hit = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = $id } -MaxEvents 500 -ErrorAction SilentlyContinue |
Where-Object { $_.Message -match [regex]::Escape($pattern) })
if ($hit.Count) { return $true }
Start-Sleep -Seconds 2
}
return $false
}
Check '4657 registry value change on Run key' (Wait-Event 4657 $valName)
Check '4663 file write in StartUp folder' (Wait-Event 4663 "$valName.txt")
Remove-ItemProperty -Path $runKey -Name $valName -ErrorAction SilentlyContinue
Remove-Item $fileName -Force -ErrorAction SilentlyContinue

Write-Host '== Re-run is idempotent'
& $install -OfflinePath $offline -Force
Check 'second run succeeded' $true
Expand All @@ -95,6 +130,8 @@ Check 'state.json removed' (-not (Test-Path $state))
Check 'cmdline auditing restored' ((Reg $cmdKey 'ProcessCreationIncludeCmdLine_Enabled') -eq $before)
Check 'PowerShell policy removed' ($null -eq (Reg "$psw\ScriptBlockLogging" 'EnableScriptBlockLogging'))
Check 'audit policy restored' ((Audit 'Process Creation') -eq $auditBefore)
Check 'Run key SACL removed' ((Get-AuditRuleCount $runKey) -eq $runSaclBefore)
Check 'StartUp SACL removed' ((Get-AuditRuleCount $startup) -eq $startupSaclBefore)
Check 'Sysmon service removed' ($null -eq (Get-Service -Name 'Sysmon*' -ErrorAction SilentlyContinue))

if ($failed.Count) { Write-Host "`n$($failed.Count) failed: $($failed -join '; ')" -ForegroundColor Red; exit 1 }
Expand Down
137 changes: 130 additions & 7 deletions windows/Install-WinLogs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
Run from an elevated prompt. Intended for disposable lab machines only.

What it does:
* Advanced audit policy (auditpol /restore from audit.csv)
* Advanced audit policy (auditpol /restore from audit.csv, plus audit-server.csv on Server)
* Per-object audit rules (SACLs) (persistence registry keys and folders, see sacls.psd1)
* Command line in process events (4688)
* PowerShell Script Block, Module and Transcription logging (Windows PowerShell + PowerShell 7)
* Larger event logs, and enables useful channels that are off by default
Expand All @@ -29,6 +30,14 @@
Optional SHA-256 the Sysmon.zip must match (downloaded or offline). The Authenticode signature of the
extracted binary is always checked.

.PARAMETER AuditProfile
Auto (default), Workstation or Server. Server adds the domain-controller categories (Kerberos,
Directory Service) from audit-server.csv. Auto picks Server on any Windows Server SKU.

.PARAMETER SkipObjectAuditing
Do not add per-object audit rules (SACLs, see sacls.psd1). Without those rules the File System and
Registry audit categories log almost nothing.

.PARAMETER LogSizeMB
Maximum size for the big channels (Security, Sysmon, PowerShell). Default 1024.

Expand All @@ -53,6 +62,8 @@ param(
[switch]$SkipSysmon,
[string]$SysmonConfigPath,
[string]$SysmonSha256,
[ValidateSet('Auto', 'Workstation', 'Server')][string]$AuditProfile = 'Auto',
[switch]$SkipObjectAuditing,
[ValidateRange(64, 65536)][int]$LogSizeMB = 1024,
[switch]$Revert,
[switch]$KeepSysmon,
Expand All @@ -63,6 +74,7 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$Root = $PSScriptRoot
. (Join-Path $PSScriptRoot 'WinLogs.Acl.ps1')
$DataDir = Join-Path $env:ProgramData 'WinLogs'
$StateFile = Join-Path $DataDir 'state.json'
$TranscriptDir = Join-Path $DataDir 'transcripts'
Expand All @@ -85,6 +97,7 @@ function New-State {
registry = @() # {path,name,existed,type,value}
registryKeys = @() # keys we created
logs = @() # {name,enabled,maxBytes}
sacls = @() # {kind,path,rights,inherit}
sysmon = $null # {installedByUs,mode}
}
}
Expand Down Expand Up @@ -146,6 +159,73 @@ function Confirm-Lab {
if ($answer -cne 'YES') { throw 'Aborted.' }
}

# --- audit helpers ----------------------------------------------------------------------------

function Resolve-AuditProfile {
if ($AuditProfile -ne 'Auto') { return $AuditProfile }
# ProductType: 1 = workstation (Windows 10/11), 2 = domain controller, 3 = server
if ((Get-CimInstance Win32_OperatingSystem).ProductType -eq 1) { return 'Workstation' }
return 'Server'
}

function New-AuditRule([string]$Kind, [string]$Rights, [string]$Inherit) {
$everyone = New-Object Security.Principal.SecurityIdentifier('S-1-1-0')
$inh = [Security.AccessControl.InheritanceFlags]$Inherit
$prop = [Security.AccessControl.PropagationFlags]::None
$flag = [Security.AccessControl.AuditFlags]::Success
if ($Kind -eq 'Registry') {
return New-Object Security.AccessControl.RegistryAuditRule($everyone, [Security.AccessControl.RegistryRights]$Rights, $inh, $prop, $flag)
}
return New-Object Security.AccessControl.FileSystemAuditRule($everyone, [Security.AccessControl.FileSystemRights]$Rights, $inh, $prop, $flag)
}

# Reads a key's / file's SACL, lets $Change modify it, writes it back (see WinLogs.Acl.ps1).
function Edit-ObjectAudit([string]$Kind, [string]$Path, [scriptblock]$Change) {
if ($Kind -eq 'Registry') {
$key = Open-AuditKey $Path
try {
$sec = $key.GetAccessControl([Security.AccessControl.AccessControlSections]::Audit)
& $Change $sec
$key.SetAccessControl($sec)
} finally { $key.Close() }
} else {
Enable-SecurityPrivilege
$acl = Get-Acl -LiteralPath $Path -Audit
& $Change $acl
Set-Acl -LiteralPath $Path -AclObject $acl
}
}

# Adds a success-audit rule for Everyone (a SACL entry) and records it so -Revert can remove it.
function Add-ObjectAudit($State, [string]$Kind, [string]$Path, [string]$Rights, [string]$Inherit) {
$Path = [Environment]::ExpandEnvironmentVariables($Path)
if (-not (Test-Path -LiteralPath $Path)) { Write-Verbose "Audit target not present, skipping: $Path"; return }
if ($PSCmdlet.ShouldProcess($Path, "Add audit rule ($Rights)")) {
try {
$rule = New-AuditRule $Kind $Rights $Inherit
Edit-ObjectAudit $Kind $Path { param($sec) $sec.AddAuditRule($rule) }
if (-not (@($State.sacls) | Where-Object { $_.path -eq $Path -and $_.rights -eq $Rights })) {
$State.sacls += [ordered]@{ kind = $Kind; path = $Path; rights = $Rights; inherit = $Inherit }
}
} catch { Write-Warn "Could not set audit rule on '$Path': $($_.Exception.Message)" }
}
}

function Remove-ObjectAudit($Entry) {
if (-not (Test-Path -LiteralPath $Entry.path)) { return }
if ($PSCmdlet.ShouldProcess($Entry.path, 'Remove audit rule')) {
try {
$rule = New-AuditRule $Entry.kind $Entry.rights $Entry.inherit
Edit-ObjectAudit $Entry.kind $Entry.path { param($sec) [void]$sec.RemoveAuditRuleSpecific($rule) }
} catch { Write-Warn "Could not remove audit rule from '$($Entry.path)': $($_.Exception.Message)" }
}
}

function Invoke-AuditRestore([string]$Csv) {
& auditpol.exe /restore /file:"$Csv" | Out-Null
if ($LASTEXITCODE -ne 0) { throw "auditpol /restore failed for $Csv" }
}

# --- revert -----------------------------------------------------------------------------------

function Invoke-Revert {
Expand All @@ -161,6 +241,11 @@ function Invoke-Revert {
}
}

if ($state.PSObject.Properties['sacls']) {
Write-Step 'Removing per-object audit rules'
foreach ($e in @($state.sacls)) { Remove-ObjectAudit $e }
}

Write-Step 'Restoring registry values'
foreach ($e in @($state.registry)) {
if (-not $PSCmdlet.ShouldProcess("$($e.path)\$($e.name)", 'Restore')) { continue }
Expand Down Expand Up @@ -303,7 +388,7 @@ if ($Revert) { Invoke-Revert; return }

$os = Get-CimInstance Win32_OperatingSystem
if ([version]$os.Version -lt [version]'10.0') { throw "Unsupported OS: $($os.Caption). Windows 10 or later is required." }
Write-Host "WinLogs on $($os.Caption) (build $($os.BuildNumber))"
Write-Host "WinLogs on $($os.Caption) (build $($os.BuildNumber)), audit profile: $(Resolve-AuditProfile)"
Confirm-Lab

$state = $null
Expand All @@ -314,6 +399,8 @@ if ($prior) {
$state.registry = @($state.registry)
$state.registryKeys = @($state.registryKeys)
$state.logs = @($state.logs)
if (-not $state.PSObject.Properties['sacls']) { $state | Add-Member -NotePropertyName sacls -NotePropertyValue @() }
$state.sacls = @($state.sacls)
} else { $state = New-State }

# 1. Advanced audit policy
Expand All @@ -328,8 +415,28 @@ if ($PSCmdlet.ShouldProcess('audit policy', "auditpol /restore $auditCsv")) {
if ($LASTEXITCODE -ne 0) { throw 'auditpol /backup failed.' }
$state.auditBackup = $backup
}
& auditpol.exe /restore /file:"$auditCsv" | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'auditpol /restore failed.' }
Invoke-AuditRestore $auditCsv
}
$profileName = Resolve-AuditProfile
if ($profileName -eq 'Server') {
Write-Step 'Adding Server / domain controller audit categories'
if ($PSCmdlet.ShouldProcess('audit policy', 'auditpol /restore audit-server.csv')) {
Invoke-AuditRestore (Join-Path $Root 'audit-server.csv')
}
}

# 1b. Per-object audit rules (without these, File System / Registry auditing logs nearly nothing)
if ($SkipObjectAuditing) {
Write-Warn 'Skipping per-object audit rules (-SkipObjectAuditing).'
} else {
Write-Step 'Adding per-object audit rules (sacls.psd1)'
Enable-SecurityPrivilege
$sacl = Import-PowerShellDataFile -Path (Join-Path $Root 'sacls.psd1')
foreach ($r in $sacl.Registry) { Add-ObjectAudit $state 'Registry' $r.Path $r.Rights 'ContainerInherit' }
foreach ($f in $sacl.Files) {
$inh = if ($f.ContainsKey('Inherit')) { $f.Inherit } else { 'None' }
Add-ObjectAudit $state 'File' $f.Path $f.Rights $inh
}
}

# 2. Registry: command line auditing, legacy audit override, PowerShell logging
Expand Down Expand Up @@ -362,7 +469,7 @@ if ($SkipSysmon) {

# 4. Event logs: sizes, and channels that are off by default
Write-Step 'Sizing and enabling event logs'
$big = 'Security', 'Microsoft-Windows-Sysmon/Operational', 'Microsoft-Windows-PowerShell/Operational', 'Windows PowerShell'
$big = 'Security', 'Microsoft-Windows-Sysmon/Operational', 'Microsoft-Windows-PowerShell/Operational', 'Windows PowerShell', 'PowerShellCore/Operational'
foreach ($n in $big) { Set-TrackedLog $state $n $LogSizeMB $true }
$medium = 'System', 'Application',
'Microsoft-Windows-Windows Defender/Operational',
Expand All @@ -371,10 +478,26 @@ $medium = 'System', 'Application',
'Microsoft-Windows-Windows Firewall With Advanced Security/Firewall',
'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational',
'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational',
'Microsoft-Windows-CodeIntegrity/Operational'
'Microsoft-Windows-CodeIntegrity/Operational',
'Microsoft-Windows-NTLM/Operational',
'Microsoft-Windows-Security-Mitigations/KernelMode',
'Microsoft-Windows-Security-Mitigations/UserMode',
'Microsoft-Windows-PrintService/Admin',
'Microsoft-Windows-PrintService/Operational',
'Microsoft-Windows-SmbClient/Security',
'Microsoft-Windows-AppLocker/EXE and DLL',
'Microsoft-Windows-AppLocker/MSI and Script',
'Microsoft-Windows-AppLocker/Packaged app-Deployment',
'Microsoft-Windows-AppLocker/Packaged app-Execution',
'Microsoft-Windows-Diagnosis-Scripted/Operational'
if ($profileName -eq 'Server') {
# Present only when the role is installed; Set-TrackedLog skips channels that do not exist.
$medium += 'Directory Service', 'DNS Server', 'Microsoft-Windows-DNSServer/Audit', 'DFS Replication'
}
foreach ($n in $medium) { Set-TrackedLog $state $n ([math]::Min(256, $LogSizeMB)) $true }
# Off by default on most builds; valuable for persistence and DNS visibility.
foreach ($n in 'Microsoft-Windows-TaskScheduler/Operational', 'Microsoft-Windows-DNS-Client/Operational') {
foreach ($n in 'Microsoft-Windows-TaskScheduler/Operational', 'Microsoft-Windows-DNS-Client/Operational',
'Microsoft-Windows-DriverFrameworks-UserMode/Operational') {
Set-TrackedLog $state $n ([math]::Min(256, $LogSizeMB)) $true
}

Expand Down
Loading
Loading