Skip to content
Merged
4 changes: 4 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ reviews:
- Every logging setting lives in LoggingBaseline.Settings.ps1 with a
plain-language Purpose (and Risk where volume/stability matters).
Flag settings hardcoded in the other scripts.
- Shared helpers live in WinLogKit.Common.ps1 (host probes, registry
reads, selection model). Flag a helper duplicated across scripts;
the generated Intune pack embedding its own is the intended
exception.
- No external module dependencies, no third party agents (no Sysmon).
- Never introduce CrashOnAuditFail, "do not overwrite" log retention,
global object access auditing, blanket SACLs, service restarts or
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ releases are tagged `vX.Y.Z` and published with a zip + SHA256 checksum.
## Unreleased

### Changed
- **`WinLogKit.Common.ps1`.** The helpers that Enable, Test, the WELA
check, the coverage report and the fleet generators each carried their
own copy of (admin check, host role and OS type, registry reads, the
auditpol and SMB audit-state readers, selection-CSV loading and the tier
logic) now live in
one dot-sourced file next to the settings table. Behaviour is unchanged
except that every generated artefact now describes its source the same
way (`Core tier [+ HighVolume] [+ Optional]` or `baseline file X.csv`).
The self-checks fail if a function is defined in more than one file.
- **Selection CSVs are checked before use.** A `-BaselineFile` that is not a
selection CSV (missing `ItemType`, `Id` or `Selected` columns), has an
empty ItemType or Id, or lists the same item twice now stops the run with
a message naming the problem, instead of an obscure error or a silent
select-nothing. So does a CSV whose rows match nothing in the settings
table (Test would otherwise report everything NOT APPLICABLE and exit 0);
rows for items this kit version does not know are warned about and
ignored, so an older CSV still works.
- **Docs cut.** README reduced to one screen and reused as the site home
page (MkDocs snippet include, one copy of the text). The site goes from
13 pages to 10: the WEC Collector page absorbs the WEF section of
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ is what tunes the presets. Open an issue with the *Field report* template.
plain-language purpose and, where it matters, a risk note. Scripts,
presets, packs and docs derive from it; never hard-code a setting
anywhere else.
- **Shared helpers live in `WinLogKit.Common.ps1`** (host probes, registry
reads, the audit policy reader, the selection model). A function
defined in two kit files fails the self-checks. The Intune pack
generator embeds its own helpers on purpose: the generated scripts must
run alone.
- **Windows PowerShell 5.1 compatible, no external modules, no agents.**
The design intent is a kit that runs on a bare server with nothing
installed. PowerShell 7 is fully supported (CI tests every change on
Expand Down
110 changes: 15 additions & 95 deletions Enable-LoggingBaseline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -100,40 +100,14 @@ if ([string]::IsNullOrEmpty($BaselineDir)) { $BaselineDir = Join-Path $PSScriptR
if ([string]::IsNullOrEmpty($LogDir)) { $LogDir = Join-Path $PSScriptRoot 'Logs' }

. (Join-Path $PSScriptRoot 'LoggingBaseline.Settings.ps1')
. (Join-Path $PSScriptRoot 'WinLogKit.Common.ps1')

# ---------------------------------------------------------------- helpers ---

function Test-IsAdmin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal $id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-DomainRole {
# Win32_ComputerSystem.DomainRole: 0/1 standalone, 2/3 member, 4/5 domain controller
$role = (Get-CimInstance -ClassName Win32_ComputerSystem).DomainRole
if ($role -ge 4) { return 'DomainController' }
if ($role -ge 2) { return 'Member' }
return 'Standalone'
}

function Get-OsType {
# Win32_OperatingSystem.ProductType: 1 workstation, 2 domain controller, 3 server
$pt = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType
if ($pt -eq 1) { return 'Workstation' }
if ($pt -eq 2) { return 'Domain Controller' }
return 'Server'
}

# Registry access uses the .NET API throughout, not *-ItemProperty, because
# one required value is literally named '*' and the ItemProperty cmdlets
# treat that as a wildcard.
function ConvertTo-NetRegPath { param([string]$Path) $Path -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' }

function Get-RegValue {
param([string]$Path, [string]$Name)
[Microsoft.Win32.Registry]::GetValue((ConvertTo-NetRegPath $Path), $Name, $null)
}

# Host probes, registry reads and the selection model come from
# WinLogKit.Common.ps1. The registry writers live here because this is
# the one script that writes; they use the .NET API for the reason noted
# there (a required value is literally named '*').
function Set-RegValue {
param([string]$Path, [string]$Name, $Value, [string]$Kind)
$kindEnum = [Microsoft.Win32.RegistryValueKind]::$Kind
Expand All @@ -149,17 +123,6 @@ function Remove-RegValue {
}
}

function Get-AuditPolicyByGuid {
# One auditpol call for everything; returns hashtable GUID -> inclusion setting text.
$map = @{}
$csv = auditpol /get /category:* /r | Where-Object { $_ -match '\S' } | ConvertFrom-Csv
foreach ($row in $csv) {
$guid = ($row.'Subcategory GUID' -replace '[{}]', '').ToUpper()
$map[$guid] = $row.'Inclusion Setting'
}
return $map
}

function Get-DesiredInclusion {
param([bool]$Success, [bool]$Failure)
if ($Success -and $Failure) { return 'Success and Failure' }
Expand All @@ -168,24 +131,6 @@ function Get-DesiredInclusion {
return 'No Auditing'
}

# Current state of the Server 2025+ SMB signing/encryption audit settings.
# Returns a hashtable Id -> current bool; items missing from the hashtable are
# unsupported on this OS (the properties only exist on Server 2025 / Win11 24H2+).
function Get-SmbAuditState {
$state = @{}
$srv = $null; $cli = $null
try { $srv = Get-SmbServerConfiguration -ErrorAction Stop } catch { $srv = $null }
try { $cli = Get-SmbClientConfiguration -ErrorAction Stop } catch { $cli = $null }
foreach ($item in $script:BaselineSmbAuditSettings) {
$cfg = $srv
if ($item.Side -eq 'Client') { $cfg = $cli }
if ($null -ne $cfg -and ($cfg.PSObject.Properties.Name -contains $item.Id)) {
$state[$item.Id] = [bool]$cfg.($item.Id)
}
}
return $state
}

function Set-SmbAuditSetting {
param([hashtable]$Item)
$setParams = @{ $Item.Id = $Item.Value; Force = $true }
Expand Down Expand Up @@ -217,37 +162,20 @@ function Add-Result {
Write-Host ('[{0,-15}] {1,-9} {2} {3}' -f $Action, $Area, $Item, $Detail) -ForegroundColor $colour
}

function Test-TierSelected {
param([string]$Tier)
if ($Tier -eq 'Core') { return $true }
if ($Tier -eq 'HighVolume') { return [bool]$IncludeHighVolume }
if ($Tier -eq 'Optional') { return [bool]$IncludeOptional }
return $false
}

# Selection map from a New-LoggingBaseline.ps1 CSV: "ITEMTYPE|ID" -> bool.
$script:Selection = $null
function Import-BaselineSelection {
param([string]$Path)
$map = @{}
foreach ($row in (Import-Csv $Path)) {
$map[("$($row.ItemType)|$($row.Id)").ToUpper()] = ("$($row.Selected)".Trim() -match '^(Y|YES|TRUE|1)$')
}
return $map
}

# One decision point for every item: baseline file wins when present,
# otherwise the tier switches decide. Returns Apply | PendingDecision |
# Excluded | NotListed.
# otherwise the tier switches decide (WinLogKit.Common.ps1 resolves both
# into $script:Selection). Returns Apply | PendingDecision | Excluded |
# NotListed.
$script:Selection = Resolve-BaselineSelection -BaselineFile $BaselineFile -IncludeHighVolume $IncludeHighVolume -IncludeOptional $IncludeOptional
function Get-ItemDecision {
param([string]$Tier, [string]$ItemType, [string]$Id)
if ($null -ne $script:Selection) {
if ($null -ne $script:Selection.Map) {
$key = ("$ItemType|$Id").ToUpper()
if (-not $script:Selection.ContainsKey($key)) { return 'NotListed' }
if ($script:Selection[$key]) { return 'Apply' }
if (-not $script:Selection.Map.ContainsKey($key)) { return 'NotListed' }
if ($script:Selection.Map[$key]) { return 'Apply' }
return 'Excluded'
}
if (Test-TierSelected $Tier) { return 'Apply' }
if (Test-ItemSelected $script:Selection $ItemType $Id $Tier) { return 'Apply' }
return 'PendingDecision'
}

Expand All @@ -273,18 +201,10 @@ try {
$baselineJson = Join-Path $BaselineDir 'LoggingBaseline-FirstRun.json'
$auditBackup = Join-Path $BaselineDir 'auditpol-backup.csv'

if (-not [string]::IsNullOrEmpty($BaselineFile)) {
if (-not (Test-Path $BaselineFile)) {
Write-Error "Baseline file not found: $BaselineFile (build one with New-LoggingBaseline.ps1)"
exit 1
}
$script:Selection = Import-BaselineSelection -Path $BaselineFile
}

Write-Host ''
Write-Host "Host profile : $(Get-OsType), $domainRole"
if ($null -ne $script:Selection) {
Write-Host "Baseline file : $BaselineFile ($(@($script:Selection.Values | Where-Object { $_ }).Count) items selected; tier switches ignored)"
if ($null -ne $script:Selection.Map) {
Write-Host "Baseline file : $BaselineFile ($(@($script:Selection.Map.Values | Where-Object { $_ }).Count) items selected; tier switches ignored)"
} else {
Write-Host "Tiers selected : Core$(if ($IncludeHighVolume) {' + HighVolume'})$(if ($IncludeOptional) {' + Optional'})"
}
Expand Down
36 changes: 7 additions & 29 deletions Export-AttackCoverage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,48 +64,27 @@ $ErrorActionPreference = 'Stop'
if ([string]::IsNullOrEmpty($OutDir)) { $OutDir = Join-Path $PSScriptRoot 'Results' }

. (Join-Path $PSScriptRoot 'LoggingBaseline.Settings.ps1')
. (Join-Path $PSScriptRoot 'WinLogKit.Common.ps1')

# ---------------------------------------------- resolve the selection sets ---

$selection = $null
if (-not [string]::IsNullOrEmpty($BaselineFile)) {
if (-not (Test-Path $BaselineFile)) {
Write-Error "Baseline file not found: $BaselineFile"
exit 1
}
$selection = @{}
foreach ($row in (Import-Csv $BaselineFile)) {
$selection[("$($row.ItemType)|$($row.Id)").ToUpper()] = ("$($row.Selected)".Trim() -match '^(Y|YES|TRUE|1)$')
}
}

function Test-ItemOn {
param([string]$ItemType, [string]$Id, [string]$Tier)
if ($null -ne $selection) {
$key = ("$ItemType|$Id").ToUpper()
return ($selection.ContainsKey($key) -and $selection[$key])
}
if ($Tier -eq 'Core') { return $true }
if ($Tier -eq 'HighVolume') { return [bool]$IncludeHighVolume }
if ($Tier -eq 'Optional') { return [bool]$IncludeOptional }
return $false
}
$sel = Resolve-BaselineSelection -BaselineFile $BaselineFile -IncludeHighVolume $IncludeHighVolume -IncludeOptional $IncludeOptional

$subcatSelected = @{}; $subcatKnownByGuid = @{}; $subcatNameByGuid = @{}
foreach ($sub in $script:BaselineAuditSubcategories) {
$g = $sub.Guid.ToUpper()
$subcatKnownByGuid[$g] = $true
$subcatNameByGuid[$g] = $sub.Name
if (Test-ItemOn 'AuditPolicy' $sub.Guid $sub.Tier) { $subcatSelected[$g] = $true }
if (Test-ItemSelected $sel 'AuditPolicy' $sub.Guid $sub.Tier) { $subcatSelected[$g] = $true }
}
$channelSelected = @{}; $channelKnown = @{}
foreach ($ch in $script:BaselineChannels) {
$channelKnown[$ch.Name] = $true
if (Test-ItemOn 'Channel' $ch.Name $ch.Tier) { $channelSelected[$ch.Name] = $true }
if (Test-ItemSelected $sel 'Channel' $ch.Name $ch.Tier) { $channelSelected[$ch.Name] = $true }
}
$regSelected = @{}
foreach ($rs in $script:BaselineRegistrySettings) {
if (Test-ItemOn 'Registry' $rs.Id $rs.Tier) { $regSelected[$rs.Id] = $true }
if (Test-ItemSelected $sel 'Registry' $rs.Id $rs.Tier) { $regSelected[$rs.Id] = $true }
}

function Test-Prereq {
Expand All @@ -121,8 +100,7 @@ function Test-Prereq {
return $false
}

$sourceDesc = "Core tier$(if ($IncludeHighVolume) {' + HighVolume'})$(if ($IncludeOptional) {' + Optional'})"
if ($null -ne $selection) { $sourceDesc = "baseline file $(Split-Path $BaselineFile -Leaf)" }
$sourceDesc = $sel.Description

$detail = New-Object System.Collections.Generic.List[object]

Expand All @@ -134,7 +112,7 @@ if ($UseOssem) {
$subcatSelectedByName = @{}; $subcatKnownByName = @{}
foreach ($sub in $script:BaselineAuditSubcategories) {
$subcatKnownByName[$sub.Name] = $true
if (Test-ItemOn 'AuditPolicy' $sub.Guid $sub.Tier) { $subcatSelectedByName[$sub.Name] = $true }
if (Test-ItemSelected $sel 'AuditPolicy' $sub.Guid $sub.Tier) { $subcatSelectedByName[$sub.Name] = $true }
}
foreach ($r in (Import-Csv $snapshot)) {
$status = ''; $via = ''
Expand Down
5 changes: 1 addition & 4 deletions Invoke-WELACheck.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrEmpty($EvidenceDir)) { $EvidenceDir = Join-Path $PSScriptRoot 'Evidence' }

function Test-IsAdmin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal $id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
. (Join-Path $PSScriptRoot 'WinLogKit.Common.ps1')

if (-not (Test-IsAdmin)) {
Write-Error 'Run as local Administrator - WELA audit-settings reads the audit policy via auditpol.'
Expand Down
34 changes: 6 additions & 28 deletions New-GpoPack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -66,33 +66,11 @@ $ErrorActionPreference = 'Stop'
if ([string]::IsNullOrEmpty($OutDir)) { $OutDir = Join-Path $PSScriptRoot 'GPO' }

. (Join-Path $PSScriptRoot 'LoggingBaseline.Settings.ps1')
. (Join-Path $PSScriptRoot 'WinLogKit.Common.ps1')

$selection = $null
if (-not [string]::IsNullOrEmpty($BaselineFile)) {
if (-not (Test-Path $BaselineFile)) {
Write-Error "Baseline file not found: $BaselineFile (build one with New-LoggingBaseline.ps1 or use a preset)"
exit 1
}
$selection = @{}
foreach ($row in (Import-Csv $BaselineFile)) {
$selection[("$($row.ItemType)|$($row.Id)").ToUpper()] = ("$($row.Selected)".Trim() -match '^(Y|YES|TRUE|1)$')
}
}

function Test-ItemOn {
param([string]$ItemType, [string]$Id, [string]$Tier)
if ($null -ne $selection) {
$key = ("$ItemType|$Id").ToUpper()
return ($selection.ContainsKey($key) -and $selection[$key])
}
if ($Tier -eq 'Core') { return $true }
if ($Tier -eq 'HighVolume') { return [bool]$IncludeHighVolume }
if ($Tier -eq 'Optional') { return [bool]$IncludeOptional }
return $false
}
$sel = Resolve-BaselineSelection -BaselineFile $BaselineFile -IncludeHighVolume $IncludeHighVolume -IncludeOptional $IncludeOptional

$sourceDesc = "Core tier$(if ($IncludeHighVolume) {' + HighVolume'})$(if ($IncludeOptional) {' + Optional'})"
if ($null -ne $selection) { $sourceDesc = "baseline file $(Split-Path $BaselineFile -Leaf)" }
$sourceDesc = $sel.Description

New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
$outDirFull = (Resolve-Path $OutDir).Path
Expand All @@ -104,7 +82,7 @@ $auditLines = New-Object System.Collections.Generic.List[string]
$auditLines.Add('Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value')
$auditCount = 0
foreach ($sub in $script:BaselineAuditSubcategories) {
if (-not (Test-ItemOn 'AuditPolicy' $sub.Guid $sub.Tier)) { continue }
if (-not (Test-ItemSelected $sel 'AuditPolicy' $sub.Guid $sub.Tier)) { continue }
$value = 0
if ($sub.Success) { $value += 1 }
if ($sub.Failure) { $value += 2 }
Expand All @@ -127,7 +105,7 @@ $regEntries = New-Object System.Collections.Generic.List[string]
$skipped = New-Object System.Collections.Generic.List[string]
$regCount = 0
foreach ($rs in $script:BaselineRegistrySettings) {
if (-not (Test-ItemOn 'Registry' $rs.Id $rs.Tier)) { continue }
if (-not (Test-ItemSelected $sel 'Registry' $rs.Id $rs.Tier)) { continue }
if ($rs.Path -notmatch $policyPathPattern) {
$skipped.Add("$($rs.Path)\$($rs.Name) (GPO Security Options territory - set in GPMC, not a registry.pol value)")
continue
Expand Down Expand Up @@ -162,7 +140,7 @@ if ($auditCount -lt $totalAudit) {
Write-Host ("PARTIAL SELECTION: audit.csv covers {0} of {1} kit subcategories. Apply semantics for the others depend on the tool " -f $auditCount, $totalAudit) -ForegroundColor Yellow
Write-Host 'and existing policy (LGPO /ac and GPO application may not preserve unlisted subcategories). After applying, ALWAYS verify' -ForegroundColor Yellow
$verifyArgs = ''
if ($null -ne $selection) { $verifyArgs = " -BaselineFile `"$BaselineFile`"" }
if ($null -ne $sel.Map) { $verifyArgs = " -BaselineFile `"$BaselineFile`"" }
else {
if ($IncludeHighVolume) { $verifyArgs += ' -IncludeHighVolume' }
if ($IncludeOptional) { $verifyArgs += ' -IncludeOptional' }
Expand Down
Loading