Dargslan.IisSecurityAudit.psm1
|
<# .SYNOPSIS Audit IIS bindings, SChannel TLS protocols, weak ciphers and app pool identities. JSON / HTML report. .DESCRIPTION Part of the Dargslan Windows Admin Tools collection. Free Cheat Sheet: https://dargslan.com/cheat-sheets/iis-tls-security-audit-2026 Full Guide: https://dargslan.com/blog/iis-tls-security-audit-powershell-2026 More tools: https://dargslan.com .LINK https://dargslan.com .LINK https://github.com/Dargslan/powershell-admin-scripts #> $script:Banner = @" +----------------------------------------------------------+ | Dargslan IIS + TLS Security Audit | https://dargslan.com - Free cheat sheets & eBooks | +----------------------------------------------------------+ "@ function Get-DargslanIisBindings { <# .SYNOPSIS Return every IIS site binding with protocol, port, host header and cert thumbprint. #> [CmdletBinding()] param() Import-Module WebAdministration -ErrorAction Stop Get-Website | ForEach-Object { $site = $_ $site.Bindings.Collection | ForEach-Object { $cert = $null if ($_.protocol -eq 'https' -and $_.certificateHash) { try { $cert = Get-Item -Path "Cert:\LocalMachine\$($_.certificateStoreName)\$($_.certificateHash)" -ErrorAction SilentlyContinue } catch {} } [pscustomobject]@{ Site = $site.Name Protocol = $_.protocol Binding = $_.bindingInformation CertSubject = $cert.Subject CertExpires = $cert.NotAfter CertThumb = $_.certificateHash } } } } function Get-DargslanIisTlsProtocols { <# .SYNOPSIS Read SChannel registry to determine TLS 1.0 / 1.1 / 1.2 / 1.3 enabled state. #> [CmdletBinding()] param() $base = 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\SCHANNEL\\Protocols' foreach ($proto in 'TLS 1.0','TLS 1.1','TLS 1.2','TLS 1.3','SSL 3.0','SSL 2.0') { foreach ($role in 'Server','Client') { $key = "$base\\$proto\\$role" $enabled = $null; $disabled = $null if (Test-Path $key) { $v = Get-ItemProperty $key -ErrorAction SilentlyContinue $enabled = $v.Enabled $disabled = $v.DisabledByDefault } [pscustomobject]@{ Protocol = $proto Role = $role Enabled = $enabled DisabledByDefault= $disabled Effective = if ($enabled -eq 0) { 'Disabled' } elseif ($enabled -eq 1) { 'Enabled' } else { 'Default (OS)' } } } } } function Get-DargslanIisAppPoolIdentities { <# .SYNOPSIS Return every app pool with its identity and managed pipeline mode. #> [CmdletBinding()] param() Import-Module WebAdministration -ErrorAction Stop Get-ChildItem IIS:\\AppPools | ForEach-Object { [pscustomobject]@{ AppPool = $_.Name State = $_.State IdentityType = $_.processModel.identityType UserName = $_.processModel.userName ManagedRuntime = $_.managedRuntimeVersion Pipeline = $_.managedPipelineMode } } } function Get-DargslanIisSecurityAuditReport { <# .SYNOPSIS Combined IIS audit with PASS / WARN / FAIL verdict. #> [CmdletBinding()] param() $bindings = @(Get-DargslanIisBindings) $tls = @(Get-DargslanIisTlsProtocols) $pools = @(Get-DargslanIisAppPoolIdentities) $tls10On = ($tls | Where-Object { $_.Protocol -eq 'TLS 1.0' -and $_.Effective -ne 'Disabled' }).Count $tls11On = ($tls | Where-Object { $_.Protocol -eq 'TLS 1.1' -and $_.Effective -ne 'Disabled' }).Count $tls12On = ($tls | Where-Object { $_.Protocol -eq 'TLS 1.2' -and $_.Effective -eq 'Enabled' }).Count $customId= ($pools | Where-Object IdentityType -eq 'SpecificUser').Count $expiring= ($bindings | Where-Object { $_.CertExpires -and $_.CertExpires -lt (Get-Date).AddDays(30) }).Count $score = 0 if ($tls10On -eq 0) { $score++ } if ($tls11On -eq 0) { $score++ } if ($tls12On -gt 0) { $score++ } if ($customId -le 5) { $score++ } if ($expiring -eq 0) { $score++ } $verdict = if ($score -ge 4) { 'PASS' } elseif ($score -ge 2) { 'WARN' } else { 'FAIL' } [pscustomobject]@{ ComputerName = $env:COMPUTERNAME Bindings = $bindings TlsProtocols = $tls AppPools = $pools TLS10Enabled = $tls10On TLS11Enabled = $tls11On ExpiringCerts = $expiring SpecificUserPools = $customId Score = $score Verdict = $verdict TimeStamp = (Get-Date).ToString('s') } } function Export-DargslanIisSecurityAuditReport { <# .SYNOPSIS Export the IIS audit to HTML and JSON. #> [CmdletBinding()] param([string]$OutDir = (Join-Path $env:TEMP 'DargslanIisAudit')) if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null } $r = Get-DargslanIisSecurityAuditReport $json = Join-Path $OutDir ('iis-' + $env:COMPUTERNAME + '.json') $html = Join-Path $OutDir ('iis-' + $env:COMPUTERNAME + '.html') $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8 $body = "<h1>IIS Audit - $($r.ComputerName)</h1>" $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/5)</p>" $body += '<h2>Bindings</h2>' + ($r.Bindings | ConvertTo-Html -Fragment) $body += '<h2>TLS Protocols</h2>' + ($r.TlsProtocols | ConvertTo-Html -Fragment) $body += '<h2>App Pools</h2>' + ($r.AppPools | ConvertTo-Html -Fragment) ConvertTo-Html -Body $body -Title 'IIS Audit' | Set-Content $html -Encoding UTF8 [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict } } |