├── .gitignore ├── ADFSToolbox.psd1 ├── ADFSToolbox.psm1 ├── LICENSE ├── README.md ├── SECURITY.md ├── diagnosticsModule ├── AdfsDiagnosticsModule.psm1 ├── Private │ ├── AdfsConfiguration.ps1 │ ├── AdfsHealthChecks.ps1 │ ├── AdfsProxyHealthChecks.ps1 │ ├── CertificateChecks.ps1 │ ├── CertificateUtilities.ps1 │ ├── CommonHealthChecks.ps1 │ ├── Constants.ps1 │ ├── DataTypes.ps1 │ ├── HelperUtilities.ps1 │ └── TestUtilities.ps1 ├── Public │ ├── Export-AdfsDiagnosticsFile.ps1 │ ├── Get-AdfsServerConfiguration.ps1 │ ├── Get-AdfsServerTrace.ps1 │ ├── Get-AdfsSystemInformation.ps1 │ ├── Get-AdfsVersionEx.ps1 │ ├── Join-DiagnosticsFiles.ps1 │ ├── Receive-AdfsServerTrace.ps1 │ ├── Set-ADFSDiagTestMode.ps1 │ ├── Start-AdfsServerTrace.ps1 │ ├── Test-AdfsServerHealth.ps1 │ ├── Test-AdfsServerHealthSingleCheck.ps1 │ └── Test-AdfsServerToken.ps1 ├── README.md ├── Test │ ├── ADFSDiagnostics.Test.ps1 │ └── Private │ │ ├── AdfsHealthChecks.Test.ps1 │ │ ├── AdfsProxyHealthChecks.Test.ps1 │ │ ├── CommonHealthChecks.Test.ps1 │ │ ├── Data │ │ └── Diagnostics │ │ │ ├── ADFSDiagnosticsFile-20190115134922.json │ │ │ └── ADFSDiagnosticsFile-20190115134926.json │ │ ├── HelperUtilities.Test.ps1 │ │ └── JoinDiagnosticsFiles.Test.ps1 ├── build.ps1 ├── en-US │ └── about_ADFSDiagnostics.help.txt └── psakeBuild.ps1 ├── eventsModule ├── AdfsEventsModule.psm1 ├── README.md ├── TESTDETAILS.md └── Test.AdfsEventsModule.ps1 ├── serviceAccountModule ├── AdfsServiceAccountModule.psm1 ├── README.md └── Tests │ └── Test.ServiceAccount.ps1 ├── tlsModule ├── AdfsTlsModule.psm1 └── README.md └── widSyncModule ├── AdfsWidSyncModule.psm1 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | # Visual Studio Code 291 | .vscode/ 292 | -------------------------------------------------------------------------------- /ADFSToolbox.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/adfsToolbox/a5ee812e694883047762145d455e3aeabdbca777/ADFSToolbox.psd1 -------------------------------------------------------------------------------- /ADFSToolbox.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Version 4 2 | #Requires -RunAsAdministrator 3 | 4 | <# 5 | 6 | .SYNOPSIS 7 | Contains data gathering, health checks, and additional tools for AD FS server deployments. 8 | 9 | .DESCRIPTION 10 | 11 | ADFSToolbox is a Windows PowerShell module that contains various tools for managing ADFS 12 | 13 | 14 | .DISCLAIMER 15 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 16 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 17 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 18 | PARTICULAR PURPOSE. 19 | 20 | Copyright (c) Microsoft Corporation. All rights reserved. 21 | #> 22 | 23 | New-Variable -Name ModuleVersion -Value "1.0.13" 24 | 25 | $url = "https://api.github.com/repos/Microsoft/adfsToolbox/releases/latest" 26 | $oldProtocol = [Net.ServicePointManager]::SecurityProtocol 27 | # We switch to using TLS 1.2 because GitHub closes the connection if it uses 1.0 or 1.1 28 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 29 | try 30 | { 31 | $response = Invoke-WebRequest -URI $url | ConvertFrom-Json 32 | if ($response.name -ne $ModuleVersion) 33 | { 34 | Write-Host "There is a newer version available. Run 'Update-Module -Name ADFSToolbox' to update to the latest version" -BackgroundColor DarkYellow -ForegroundColor Black 35 | Write-Host "Alternatively, you can download it manually from https://github.com/Microsoft/adfsToolbox/releases/latest" -BackgroundColor DarkYellow -ForegroundColor Black 36 | } 37 | else 38 | { 39 | Write-Host "You have the latest version installed!" -BackgroundColor DarkYellow -ForegroundColor Black 40 | } 41 | } 42 | catch 43 | { 44 | # Github limits the number of unauthenticated API requests. To avoid this throwing an error we supress it here. 45 | Write-Host "Importing ADFSToolbox version $ModuleVersion" -BackgroundColor Yellow -ForegroundColor Black 46 | Write-Host "Unable to reach GitHub, please manually verify that you have the latest version by going to https://github.com/Microsoft/adfsToolbox/releases/latest" -BackgroundColor Yellow -ForegroundColor Black 47 | } 48 | 49 | [Net.ServicePointManager]::SecurityProtocol = $oldProtocol 50 | 51 | Export-ModuleMember -Variable ModuleVersion -Function * 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AD FS Toolbox 2 | 3 | ## Notice: This repository is no longer being actively maintained. ADFSToolbox is still being actively worked on and can be installed from [here](https://www.powershellgallery.com/packages/ADFSToolbox/). 4 | 5 | ### Details 6 | 7 | As of 7/31/2019, we have migrated the diagnosticsModule from PowerShell to C# into a new repository. As a consequence of this change, the new repository is not publically available; **however, we will still be making improvements to ADFSToolbox and releasing them [here](https://www.powershellgallery.com/packages/ADFSToolbox/)**. If you feel that we should invest the time to make the new repository open source, please direct your feedback to [here](https://adfshelp.microsoft.com/Feedback/ProvideFeedback) or respond to the issue [here](https://github.com/microsoft/adfsToolbox/issues/75). This repository will remain public, but will not be actively maintained. 8 | 9 | ## Notice: Additionally, the diagnostics module will no longer support AD FS 2.1 or lower. If you need to target AD FS versions 2.1 or lower please install version 1.0.13 of ADFSToolbox. 10 | 11 | ## Overview 12 | 13 | This repository contains tools for helping you manage your AD FS farm. The following tools are currently included: 14 | 15 | 1. __[Diagnostics Module](diagnosticsModule)__ - PowerShell module to do basic health checks against AD FS. Determines if AD FS is in a healthy state. 16 | 17 | 2. __[Events Module](eventsModule)__ - PowerShell module provides tools for gathering related ADFS events from the security, admin, and debug logs, across multiple servers. 18 | 19 | 3. __[Service Account Module](serviceAccount)__ - PowerShell module to change the AD FS service account. 20 | 21 | 4. __[WID Sync Module](widSync)__ - PowerShell module to force a full WID sync to an AD FS secondary node 22 | 23 | ## Getting Started 24 | 25 | ### Install through PowerShell Gallery (Recommended) for AD FS 3.0 26 | 27 | 1. Install the PowerShell Module 28 | 29 | In a PowerShell window, run the following: 30 | 31 | `Install-Module -Name ADFSToolbox -Force` 32 | 33 | 2. Import the PowerShell Module 34 | 35 | In a PowerShell window, run the following: 36 | 37 | `Import-Module ADFSToolbox -Force` 38 | 39 | 3. Run the cmdlet of your choice, with the required parameters (see individual tools for details) 40 | 41 | 42 | ### Install manually for AD FS 3.0 43 | 44 | 1. Launch an elevated PowerShell window on a machine that has internet access. 45 | 2. Install the PowerShell Module 46 | 47 | `Install-Module -Name ADFSToolbox -Force` 48 | 49 | 3. Copy the ADFSToolbox folder located `%SYSTEMDRIVE%\Program Files\WindowsPowerShell\Modules\` on your local machine to the same location on your AD FS or WAP machine. 50 | 51 | 4. Launch an elevated PowerShell window on your AD FS machine and run the following cmdlet to import the module. 52 | 53 | `Import-Module -Name ADFSToolbox -Force` 54 | 55 | ### Install through PowerShell Gallery (Recommended) for AD FS 2.1 or lower 56 | 57 | 1. Install the PowerShell Module 58 | 59 | In a PowerShell window, run the following: 60 | 61 | `Install-Module -Name ADFSToolbox -RequiredVersion 1.0.13 -Force` 62 | 63 | 2. Import the PowerShell Module 64 | 65 | In a PowerShell window, run the following: 66 | 67 | `Import-Module ADFSToolbox -RequiredVersion 1.0.13 -Force` 68 | 69 | 3. Run the cmdlet of your choice, with the required parameters (see individual tools for details) 70 | 71 | 72 | ### Install manually for AD FS 2.1 or lower 73 | 74 | 1. Launch an elevated PowerShell window on a machine that has internet access. 75 | 2. Install the PowerShell Module 76 | 77 | `Install-Module -Name ADFSToolbox -RequiredVersion 1.0.13 -Force` 78 | 79 | 3. Copy the ADFSToolbox folder located `%SYSTEMDRIVE%\Program Files\WindowsPowerShell\Modules\` on your local machine to the same location on your AD FS or WAP machine. 80 | 81 | 4. Launch an elevated PowerShell window on your AD FS machine and run the following cmdlet to import the module. 82 | 83 | `Import-Module -Name ADFSToolbox -RequiredVersion 1.0.13 -Force` 84 | 85 | 86 | ## Contributing 87 | 88 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 89 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 90 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 91 | 92 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 93 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 94 | provided by the bot. You will only need to do this once across all repos using our CLA. 95 | 96 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 97 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 98 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 99 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /diagnosticsModule/AdfsDiagnosticsModule.psm1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | Contains data gathering, health checks, and additional utilities for AD FS server deployments. 5 | 6 | .DESCRIPTION 7 | 8 | ADFSDiagnostics.psm1 is a Windows PowerShell module for diagnosing issues with ADFS 9 | 10 | 11 | .DISCLAIMER 12 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 13 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 14 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 15 | PARTICULAR PURPOSE. 16 | 17 | Copyright (c) Microsoft Corporation. All rights reserved. 18 | #> 19 | 20 | #Get public and private function definition files. 21 | Write-Debug "Importing public and private functions" 22 | 23 | $Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue) 24 | $Private = @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue) 25 | #Dot source the files 26 | foreach ($import in @($Public + $Private)) 27 | { 28 | try 29 | { 30 | . $import.fullname 31 | } 32 | catch 33 | { 34 | Write-Error -Message "Failed to import script $($import.fullname): $_" 35 | } 36 | } 37 | 38 | Export-ModuleMember -Function $Public.Basename 39 | -------------------------------------------------------------------------------- /diagnosticsModule/Private/AdfsConfiguration.ps1: -------------------------------------------------------------------------------- 1 | function AdfsConfiguration 2 | { 3 | $configurationOutput = New-Object PSObject; 4 | $ErrorActionPreference = "SilentlyContinue" 5 | 6 | # Get AD FS role 7 | $role = Get-AdfsRole; 8 | if($role -eq $adfsRoleSTS){ 9 | # If primary ADFS server 10 | if(IsAdfsSyncPrimaryRole){ 11 | $role = 1; 12 | } 13 | # Is secondary ADFS server 14 | else{ 15 | $role = 2; 16 | } 17 | } 18 | # Is WAP server 19 | elseif($role -eq $adfsRoleProxy){ 20 | $role = 3; 21 | } 22 | $configurationOutput | Add-Member NoteProperty -name "Role" -value $role -Force; 23 | 24 | # Get Major value of the operating system 25 | $MajorOsVersion = ([environment]::OSVersion.Version).Major; 26 | $configurationOutput | Add-Member NoteProperty -name "MajorOsVersion" -value $MajorOsVersion -Force; 27 | 28 | # Get Farm Behavior Level and ADFS Servers 29 | $adfsFarmInformation = Get-AdfsFarmInformation; 30 | [array]$adfsServers = $adfsFarmInformation.FarmNodes.FQDN; 31 | $configurationOutput | Add-Member NoteProperty -name "CurrentFarmBehavior" -value $adfsFarmInformation.CurrentFarmBehavior -Force; 32 | $configurationOutput | Add-Member NoteProperty -name "AdfsServers" -value $adfsServers -Force; 33 | 34 | #Get the connected WAP servers 35 | [array]$wapServers = (Get-WebApplicationProxyConfiguration).ConnectedServersName; 36 | $configurationOutput | Add-Member NoteProperty -name "WapServers" -value $wapServers -Force; 37 | 38 | # Get Operating system 39 | $operatingSystem = (Get-WmiObject -class Win32_OperatingSystem).Caption; 40 | $configurationOutput | Add-Member NoteProperty -name "OperatingSystem" -value $operatingSystem -Force; 41 | 42 | # Get Adfs Properties 43 | $adfsProperties = Get-AdfsProperties; 44 | 45 | # Get Database 46 | $database = $adfsProperties.ArtifactDbConnection; 47 | if($database.ToLower().Contains("wid") -or $database.ToLower().Contains("ssee")) 48 | { 49 | $database = "Windows Internal Database"; 50 | }else{ 51 | $database = "External SQL Server"; 52 | } 53 | $configurationOutput | Add-Member NoteProperty -name "Database" -value $database -Force; 54 | 55 | # Get Federation service name 56 | $configurationOutput | Add-Member NoteProperty -name "FederationServiceName" -value $adfsProperties.Hostname -Force; 57 | 58 | # Get Service account and Service type 59 | $serviceAccount = (gwmi win32_service -filter "name='adfssrv'").StartName; 60 | if($serviceAccount.EndsWith("$")){ 61 | $serviceType = "GMSA"; 62 | }else{ 63 | $serviceType = "Standard service account"; 64 | } 65 | $configurationOutput | Add-Member NoteProperty -name "ServiceAccount" -value $serviceAccount -Force; 66 | $configurationOutput | Add-Member NoteProperty -name "ServiceAccountType" -value $serviceType -Force; 67 | 68 | # Get Service account SPN 69 | if($null -ne $serviceAccount){ 70 | [array]$serviceAccountSPN = setspn -L $serviceAccount; 71 | } 72 | $configurationOutput | Add-Member NoteProperty -name "ServiceAccountSpn" -value $serviceAccountSPN -Force; 73 | 74 | # Get ADFS Global authentication policy 75 | $globalAuthenticationPolicyOuput = New-Object PSObject; 76 | $globalAuthenticationPolicy = Get-AdfsGlobalAuthenticationPolicy; 77 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "AdditionalAuthenticationProvider" -value $globalAuthenticationPolicy.AdditionalAuthenticationProvider -Force; 78 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "DeviceAuthenticationEnabled" -value $globalAuthenticationPolicy.DeviceAuthenticationEnabled -Force; 79 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "AllowAdditionalAuthenticationAsPrimary" -value $globalAuthenticationPolicy.AllowAdditionalAuthenticationAsPrimary -Force; 80 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "EnablePaginatedAuthenticationPages" -value $globalAuthenticationPolicy.EnablePaginatedAuthenticationPages -Force; 81 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "DeviceAuthenticationMethod" -value $globalAuthenticationPolicy.DeviceAuthenticationMethod -Force; 82 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "TreatDomainJoinedDevicesAsCompliant" -value $globalAuthenticationPolicy.TreatDomainJoinedDevicesAsCompliant -Force; 83 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "PrimaryIntranetAuthenticationProvider" -value $globalAuthenticationPolicy.PrimaryIntranetAuthenticationProvider -Force; 84 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "PrimaryExtranetAuthenticationProvider" -value $globalAuthenticationPolicy.PrimaryExtranetAuthenticationProvider -Force; 85 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "WindowsIntegratedFallbackEnabled" -value $globalAuthenticationPolicy.WindowsIntegratedFallbackEnabled -Force; 86 | try{ 87 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "ClientAuthenticationMethods" -value $globalAuthenticationPolicy.ClientAuthenticationMethods.ToString() -Force; 88 | } 89 | catch [Exception] 90 | { 91 | $globalAuthenticationPolicyOuput | Add-Member NoteProperty -name "ClientAuthenticationMethods" -value $globalAuthenticationPolicy.ClientAuthenticationMethods -Force; 92 | } 93 | $configurationOutput | Add-Member NoteProperty -name "AdfsGlobalAuthenticationPolicy" -value $globalAuthenticationPolicyOuput -Force; 94 | 95 | # Get ADFS SSL Certificates 96 | $sslCertificateHash = Get-AdfsSslCertificate | Select-Object CertificateHash; 97 | $sslCertificate = Get-ChildItem -Path Cert:\LocalMachine\my | Where-Object Thumbprint -contains $sslCertificateHash[0].CertificateHash | Select-Object Issuer, NotBefore, NotAfter, Thumbprint 98 | $configurationOutput | Add-Member NoteProperty -name "AdfsSslCertificate" -value $sslCertificate -Force; 99 | 100 | # Get ADFS Cerificates 101 | [array]$certificates = Get-AdfsCertificate -CertificateType "Token-Signing" | Select-Object -Property IsPrimary, CertificateType, @{Name="Certificate"; Expression={$_.Certificate | Select-Object Issuer, NotBefore, NotAfter, Thumbprint}} 102 | $configurationOutput | Add-Member NoteProperty -name "AdfsCertificate" -value $certificates -Force; 103 | 104 | # Get ADFS Relying party trust 105 | [array]$relyingPartyTrust = Get-AdfsRelyingPartyTrust | Select-Object -Property Name, Identifier, ProtocolProfile, AccessControlPolicyName, IssuanceAuthorizationRules, AdditionalAuthenticationRules, IssuanceTransformRules; 106 | $configurationOutput | Add-Member NoteProperty -name "AdfsRelyingPartyTrust" -value $relyingPartyTrust -Force; 107 | 108 | $ErrorActionPreference = "Continue" 109 | return $configurationOutput; 110 | } -------------------------------------------------------------------------------- /diagnosticsModule/Private/AdfsProxyHealthChecks.ps1: -------------------------------------------------------------------------------- 1 | Function TestIsAdfsProxyRunning 2 | { 3 | $testName = "TestIsAdfsProxyRunning"; 4 | $serviceStateOutputKey = "ADFSProxyServiceState"; 5 | $testResult = New-Object TestResult -ArgumentList($testName); 6 | 7 | try 8 | { 9 | $adfsProxyServiceState = Get-ServiceState($adfsProxyServiceName); 10 | if ($adfsProxyServiceState -ne "Running") 11 | { 12 | $testResult.Result = [ResultType]::Fail; 13 | $testResult.Detail = "Current state of $adfsProxyServiceName is: $adfsProxyServiceState"; 14 | } 15 | $testResult.Output = @{$serviceStateOutputKey = $adfsProxyServiceState}; 16 | 17 | return $testResult; 18 | } 19 | catch [Exception] 20 | { 21 | return Create-ErrorExceptionTestResult $testName $_.Exception 22 | } 23 | } 24 | 25 | Function TestSTSReachableFromProxy() 26 | { 27 | $testName = "TestSTSReachableFromProxy" 28 | $exceptionKey = "TestSTSReachableFromProxyException" 29 | try 30 | { 31 | $mexUrlTestResult = New-Object TestResult -ArgumentList($testName); 32 | $mexUrlTestResult.Output = @{$exceptionKey = "NONE"} 33 | 34 | $proxyInfo = gwmi -Class ProxyService -Namespace root\ADFS 35 | 36 | $stsHost = $proxyInfo.HostName + ":" + $proxyInfo.HostHttpsPort 37 | 38 | $mexUrl = "https://" + $stsHost + "/adfs/services/trust/mex"; 39 | $webClient = New-Object net.WebClient; 40 | try 41 | { 42 | $data = $webClient.DownloadData($mexUrl); 43 | #If the mex is successfully downloaded from proxy, then the test is deemed succesful 44 | } 45 | catch [Net.WebException] 46 | { 47 | $exceptionEncoded = [System.Web.HttpUtility]::HtmlEncode($_.Exception.ToString()); 48 | $mexUrlTestResult.Result = [ResultType]::Fail; 49 | $mexUrlTestResult.Detail = $exceptionEncoded; 50 | $mexUrlTestResult.Output.Set_Item($exceptionKey, $exceptionEncoded) 51 | } 52 | 53 | return $mexUrlTestResult; 54 | } 55 | catch [Exception] 56 | { 57 | return Create-ErrorExceptionTestResult $testName $_.Exception 58 | } 59 | } 60 | 61 | Function TestProxySslBindings 62 | { 63 | Param( 64 | [Parameter(Mandatory = $true)] 65 | [string] 66 | $AdfsSslThumbprint 67 | ) 68 | $testName = "TestProxySslBindings"; 69 | $testResult = New-Object TestResult -ArgumentList($testName); 70 | Out-Verbose "Parameter AdfsSslThumbprint = $AdfsSslThumbprint"; 71 | 72 | try 73 | { 74 | $bindings = GetSslBindings; 75 | Out-Verbose "Attempting to get federation service name."; 76 | $proxyInfo = Get-WmiObject -Class ProxyService -Namespace root\ADFS 77 | 78 | $federationServiceName = $proxyInfo.HostName; 79 | Out-Verbose "Retrieved federation service name: $federationServiceName."; 80 | 81 | $adfsPort = $proxyInfo.HostHttpsPort; 82 | $tlsPort = $proxyInfo.TlsClientPort; 83 | Out-Verbose "Retrieved ADFS Port: $adfsPort TLS Port: $tlsPort"; 84 | 85 | $erroneousBindings = @{} 86 | 87 | # Expected SSL bindings 88 | Out-Verbose "Attempting to validate expected SSL bindings." 89 | $ret = IsSslBindingValid -Bindings $bindings -BindingIpPortOrHostnamePort $($federationServiceName + ":" + $adfsPort) -CertificateThumbprint $AdfsSslThumbprint 90 | if (!($ret.IsValid)) 91 | { 92 | $erroneousBindings[$($federationServiceName + ":" + $adfsPort)] = $ret["Detail"]; 93 | } 94 | 95 | $bindings.Remove($($federationServiceName + ":" + $adfsPort)); 96 | 97 | $ret = IsSslBindingValid -Bindings $bindings -BindingIpPortOrHostnamePort $($federationServiceName + ":" + $tlsPort) -CertificateThumbprint $AdfsSslThumbprint -VerifyCtlStoreName $false; 98 | if (!($ret.IsValid)) 99 | { 100 | $erroneousBindings[$($federationServiceName + ":" + $tlsPort)] = $ret["Detail"]; 101 | } 102 | 103 | $bindings.Remove($($federationServiceName + ":" + $tlsPort)); 104 | 105 | # Check custom bindings that match the AD FS Application Id 106 | foreach ($key in $bindings.Keys) 107 | { 108 | if ($bindings[$key]["Application ID"] -eq $adfsApplicationId) 109 | { 110 | Out-Verbose "Checking custom SSL certificate binding $key."; 111 | 112 | # We can only validate the Thumbprint here since we do not know which ip/hostname port this binding is for. 113 | $ret = IsSslBindingValid -Bindings $bindings -BindingIpPortOrHostnamePort $key -CertificateThumbprint $AdfsSslThumbprint -VerifyCtlStoreName $false; 114 | if (!($ret.IsValid)) 115 | { 116 | $erroneousBindings[$key] = $ret["Detail"]; 117 | } 118 | } 119 | } 120 | 121 | if ($erroneousBindings.Count -ne 0) 122 | { 123 | $testResult.Result = [ResultType]::Fail; 124 | $testResult.Detail = "There were SSL bindings found that were incorrect. Check the output for more detail."; 125 | $testResult.Output = @{"ErroneousBindings" = $erroneousBindings}; 126 | } 127 | 128 | return $testResult; 129 | } 130 | catch [Exception] 131 | { 132 | return Create-ErrorExceptionTestResult $testName $_.Exception 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /diagnosticsModule/Private/CertificateChecks.ps1: -------------------------------------------------------------------------------- 1 | Function Test-CertificateAvailable 2 | { 3 | param( 4 | [Parameter(HelpMessage="Single element of list Generated by Get-AdfsCertificatesToTest")] 5 | $certificateAvailable, 6 | [string] 7 | $certificateType, 8 | [bool] 9 | $isPrimary = $true, 10 | [string] 11 | $notRunReason 12 | ) 13 | 14 | $testName = Create-CertCheckName -certType $certificateType -checkName "NotFoundInStore" -isPrimary $isPrimary 15 | 16 | if (-not $certificateAvailable -and [String]::IsNullOrEmpty($notRunReason)) 17 | { 18 | $notRunReason = "Certificate object is null." 19 | } 20 | 21 | if (-not [String]::IsNullOrEmpty($notRunReason)) 22 | { 23 | return Create-CertificateCheckResult -certCheckResult $null -testName $testName -result NotRun -detail $notRunReason 24 | } 25 | 26 | 27 | try 28 | { 29 | $thumbprint = $certificateAvailable.Thumbprint 30 | $testResult = New-Object TestResult -ArgumentList($testName) 31 | $testResult.Result = [ResultType]::NotRun; 32 | $testResult.Output = @{$tpKey = $thumbprint} 33 | 34 | if ($certificateAvailable.StoreLocation -eq "LocalMachine") 35 | { 36 | $certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($certificateAvailable.StoreName,` 37 | [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) 38 | try 39 | { 40 | $certStore.Open("IncludeArchived") 41 | 42 | $certSearchResult = $certStore.Certificates | where {$_.Thumbprint -eq $thumbprint} 43 | if (($certSearchResult | measure).Count -eq 0) 44 | { 45 | $testResult.Detail = "$certificateType certificate with thumbprint $thumbprint not found in LocalMachine\{0} store.`n" -f $certificateAvailable.StoreName 46 | $testResult.Result = [ResultType]::Fail 47 | } 48 | else 49 | { 50 | $testResult.Result = [ResultType]::Pass 51 | } 52 | } 53 | catch 54 | { 55 | $testResult.Result = [ResultType]:: NotRun; 56 | $testResult.Detail = "$certificateType certificate with thumbprint $thumbprint encountered exception with message`n" + $_.Exception.Message 57 | } 58 | finally 59 | { 60 | $certStore.Close() 61 | } 62 | } 63 | else 64 | { 65 | $testResult.Result = [ResultType]:: NotRun; 66 | $testResult.Detail = "$certificateType certificate with thumbprint $thumbprint not checked for availability because it is in store: " + $certificateAvailable.StoreLocation 67 | } 68 | 69 | return $testResult 70 | } 71 | catch [Exception] 72 | { 73 | return Create-NotRunExceptionTestResult $testName $_.Exception.Message 74 | } 75 | } 76 | 77 | function Test-CertificateExpired 78 | { 79 | param ( 80 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 81 | $certExpired, 82 | [string] 83 | $certificateType, 84 | [bool] 85 | $isPrimary = $true, 86 | [string] 87 | $notRunReason 88 | ) 89 | 90 | $checkName = "Expired" 91 | 92 | $testName = Create-CertCheckName -certType $certificateType -checkName $checkName -isPrimary $isPrimary 93 | 94 | if (-not $certExpired -and [String]::IsNullOrEmpty($notRunReason)) 95 | { 96 | $notRunReason = "Certificate object is null." 97 | } 98 | if (-not [String]::IsNullOrEmpty($notRunReason)) 99 | { 100 | return Create-CertificateCheckResult -certCheckResult $null -testName $testName -result NotRun -detail $notRunReason 101 | } 102 | 103 | try 104 | { 105 | if (Verify-IsCertExpired -isCertExpired $certExpired) 106 | { 107 | $tp = $certExpired.Thumbprint 108 | 109 | $certificateExpiredTestDetail = "$certificateType certificate with thumbprint $tp has expired.`n"; 110 | $certificateExpiredTestDetail += "Valid From: " + $certExpired.NotBefore.ToString() + "`nValid To: " + $certExpired.NotAfter.ToString(); 111 | $certificateExpiredTestDetail += "`nAutoCertificateRollover Enabled: " + (Retrieve-AdfsProperties).AutoCertificateRollover + "`n"; 112 | return Create-CertificateCheckResult -certCheckResult $certExpired -testName $testName -result Fail -detail $certificateExpiredTestDetail 113 | } 114 | else 115 | { 116 | return Create-CertificateCheckResult -certCheckResult $certExpired -testName $testName -result Pass 117 | } 118 | } 119 | catch [Exception] 120 | { 121 | return Create-NotRunExceptionTestResult $testName $_.Exception.Message 122 | } 123 | } 124 | 125 | function Test-CertificateAboutToExpire 126 | { 127 | 128 | param ( 129 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 130 | $certAboutToExpire, 131 | [string] 132 | $certificateType, 133 | [bool] 134 | $isPrimary = $true, 135 | [string] 136 | $notRunReason 137 | ) 138 | $checkName = "AboutToExpire" 139 | 140 | $testName = Create-CertCheckName -certType $certificateType -checkName $checkName -isPrimary $isPrimary 141 | 142 | $expiryLimitInDays = 90; 143 | 144 | 145 | if (-not $certAboutToExpire -and [String]::IsNullOrEmpty($notRunReason)) 146 | { 147 | $notRunReason = "Certificate object is null." 148 | } 149 | if (-not [String]::IsNullOrEmpty($notRunReason)) 150 | { 151 | return Create-CertificateCheckResult -certCheckResult $null -testName $testName -result NotRun -detail $notRunReason 152 | } 153 | 154 | try 155 | { 156 | $properties = Retrieve-AdfsProperties 157 | if ($properties.AutoCertificateRollover -and ($certificateType -eq "Token-Decrypting" -or $certificateType -eq "Token-Signing")) 158 | { 159 | return Create-CertificateCheckResult -certCheckResult $certAboutToExpire -testName $testName -result NotRun -detail "Check Skipped when AutoCertificateRollover is enabled" 160 | } 161 | 162 | $expirtyMinusToday = [System.Convert]::ToInt32(($certAboutToExpire.NotAfter - (Get-Date)).TotalDays); 163 | if ($expirtyMinusToday -le $expiryLimitInDays) 164 | { 165 | $tp = $certAboutToExpire.Thumbprint 166 | 167 | $certificateAboutToExpireTestDetail = "$certificateType certificate with thumbprint $tp is about to expire in $expirtyMinusToday days.`n" 168 | $certificateAboutToExpireTestDetail += "Valid From: " + $certAboutToExpire.NotBefore.ToString() + "`nValid To: " + $certAboutToExpire.NotAfter.ToString(); 169 | $certificateAboutToExpireTestDetail += "`nAutoCertificateRollover Enabled: " + (Retrieve-AdfsProperties).AutoCertificateRollover + "`n"; 170 | return Create-CertificateCheckResult -certCheckResult $certAboutToExpire -testName $testName -result Fail -detail $certificateAboutToExpireTestDetail 171 | } 172 | else 173 | { 174 | return Create-CertificateCheckResult -certCheckResult $certAboutToExpire -testName $testName -result Pass 175 | } 176 | } 177 | catch [Exception] 178 | { 179 | return Create-NotRunExceptionTestResult $testName $_.Exception.Message 180 | } 181 | } 182 | 183 | function Test-CertificateHasPrivateKey 184 | { 185 | param ( 186 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 187 | $certHasPrivateKey, 188 | [string] 189 | $certificateType, 190 | [bool] 191 | $isPrimary = $true, 192 | [string] 193 | $storeName, 194 | [string] 195 | $storeLocation, 196 | [string] 197 | $notRunReason 198 | ) 199 | 200 | $checkName = "PrivateKeyAbsent" 201 | 202 | $testName = Create-CertCheckName -certType $certificateType -checkName $checkName -isPrimary $isPrimary 203 | 204 | if (-not $certHasPrivateKey -and [String]::IsNullOrEmpty($notRunReason)) 205 | { 206 | $notRunReason = "Certificate object is null." 207 | } 208 | 209 | if (-not [String]::IsNullOrEmpty($notRunReason)) 210 | { 211 | return Create-CertificateCheckResult -certCheckResult $null -testName $testName -result NotRun -detail $notRunReason 212 | } 213 | 214 | try 215 | { 216 | $properties = Retrieve-AdfsProperties 217 | if ($properties.AutoCertificateRollover -and ($certificateType -eq "Token-Decrypting" -or $certificateType -eq "Token-Signing")) 218 | { 219 | return Create-CertificateCheckResult -certCheckResult $certHasPrivateKey -testName $testName -result NotRun -detail "Check Skipped when AutoCertificateRollover is enabled" 220 | } 221 | 222 | #special consideration to the corner case where auto certificate rollover was on, then turned off, leaving behind some certificates in the CU\MY store 223 | #in which case, we cannot ascertain whether the private key is present or not 224 | if ($storeLocation -eq "CurrentUser") 225 | { 226 | return Create-CertificateCheckResult -certCheckResult $certHasPrivateKey -testName $testName -result NotRun -detail "Check Skipped because the certificate is in the CU\MY store" 227 | } 228 | 229 | if ($certHasPrivateKey.HasPrivateKey) 230 | { 231 | return Create-CertificateCheckResult -certCheckResult $certHasPrivateKey -testName $testName -result Pass 232 | } 233 | else 234 | { 235 | $tp = $certHasPrivateKey.Thumbprint 236 | $detail = "$certificateType certificate with thumbprint $tp does not have a private key." 237 | return Create-CertificateCheckResult -certCheckResult $certHasPrivateKey -testName $testName -result Fail -detail $detail 238 | } 239 | } 240 | catch [Exception] 241 | { 242 | return Create-NotRunExceptionTestResult $testName $_.Exception.Message 243 | } 244 | } 245 | 246 | function Test-CertificateSelfSigned 247 | { 248 | param ( 249 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 250 | $certSelfSigned, 251 | [string] 252 | $certificateType, 253 | [bool] 254 | $isPrimary = $false, 255 | [string] 256 | $notRunReason 257 | ) 258 | 259 | $checkName = "IsSelfSigned" 260 | 261 | $testName = Create-CertCheckName -certType $certificateType -checkName $checkName -isPrimary $isPrimary 262 | 263 | if (-not $certSelfSigned -and [String]::IsNullOrEmpty($notRunReason)) 264 | { 265 | $notRunReason = "Certificate object is null." 266 | } 267 | 268 | if (-not [String]::IsNullOrEmpty($notRunReason)) 269 | { 270 | return Create-CertificateCheckResult -certCheckResult $null -testName $testName -result NotRun -detail $notRunReason 271 | } 272 | 273 | try 274 | { 275 | $properties = Retrieve-AdfsProperties 276 | if ($properties.AutoCertificateRollover -and ($certificateType -eq "Token-Decrypting" -or $certificateType -eq "Token-Signing")) 277 | { 278 | return Create-CertificateCheckResult -certCheckResult $certSelfSigned -testName $testName -result NotRun -detail "Check Skipped when AutoCertificateRollover is enabled" 279 | } 280 | if (Verify-IsCertSelfSigned $certSelfSigned) 281 | { 282 | $tp = $certSelfSigned.Thumbprint 283 | $detail = "$certificateType certificate with thumbprint $tp is self-signed." 284 | return Create-CertificateCheckResult -certCheckResult $certSelfSigned -testName $testName -result Fail -detail $detail 285 | } 286 | else 287 | { 288 | return Create-CertificateCheckResult -certCheckResult $certSelfSigned -testName $testName -result Pass 289 | } 290 | } 291 | catch [Exception] 292 | { 293 | return Create-NotRunExceptionTestResult $testName $_.Exception.Message 294 | } 295 | } 296 | 297 | function Test-CertificateCRL 298 | { 299 | param ( 300 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 301 | $certCrl, 302 | [string] 303 | $certificateType, 304 | [bool] 305 | $isPrimary = $false, 306 | [string] 307 | $notRunReason 308 | ) 309 | 310 | $checkName = "Revoked" 311 | $chainStatusKey = "ChainStatus" 312 | 313 | $testName = Create-CertCheckName -certType $certificateType -checkName $checkName -isPrimary $isPrimary 314 | 315 | if (-not $certCrl -and [String]::IsNullOrEmpty($notRunReason)) 316 | { 317 | $notRunReason = "Certificate object is null." 318 | } 319 | 320 | if (-not [String]::IsNullOrEmpty($notRunReason)) 321 | { 322 | return Create-CertificateCheckResult -certCheckResult $null -testName $testName -result NotRun -detail $notRunReason 323 | } 324 | 325 | try 326 | { 327 | $crlResult = VerifyCertificateCRL -certCrl $certCrl 328 | $passFail = [ResultType]::Pass 329 | if (($crlResult.ChainBuildResult -eq $false) -and ($crlResult.IsSelfSigned -eq $false)) 330 | { 331 | $passFail = [ResultType]::Fail 332 | } 333 | $testResult = Create-CertificateCheckResult -certCheckResult $certCrl -testName $testName -result $passFail 334 | $testDetail = "Thumbprint: " + $crlResult.Thumbprint + "`n" 335 | 336 | $testResult.Output.Add($chainStatusKey, "NONE") 337 | if ($crlResult.ChainStatus) 338 | { 339 | $testResult.Output.Set_Item($chainStatusKey, $crlResult.ChainStatus) 340 | foreach($chainStatus in $crlResult.ChainStatus) 341 | { 342 | $testDetail = $testDetail + $chainStatus.Status + "-" + $chainStatus.StatusInformation + [System.Environment]::NewLine 343 | } 344 | } 345 | 346 | $testResult.Detail = $testDetail 347 | return $testResult 348 | } 349 | catch [Exception] 350 | { 351 | return Create-NotRunExceptionTestResult $testName $_.Exception.Message 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /diagnosticsModule/Private/CertificateUtilities.ps1: -------------------------------------------------------------------------------- 1 | Function Create-CertCheckName 2 | { 3 | param( 4 | [string] 5 | $certType, 6 | [string] 7 | $checkName, 8 | [bool] 9 | $isPrimary = $true 10 | ) 11 | 12 | $primaryOrSecondary = "Secondary" 13 | if ($isPrimary) 14 | { 15 | $primaryOrSecondary = "Primary" 16 | } 17 | return "Test-Certificate-{0}-{1}-{2}" -f $certType, $primaryOrSecondary, $checkName 18 | } 19 | 20 | Function Create-CertificateCheckResult 21 | { 22 | param ( 23 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 24 | $certCheckResult, 25 | [string] 26 | $testName, 27 | [ResultType] 28 | $result, 29 | [Parameter(Mandatory=$false)] 30 | [string] 31 | $detail = $null 32 | ) 33 | 34 | $testResult = New-Object TestResult -ArgumentList($testName) 35 | $testResult.Result = $result 36 | $testResult.Detail = $detail 37 | if ($certCheckResult) 38 | { 39 | $testResult.Output = @{$tpKey = $certCheckResult.Thumbprint} 40 | } 41 | return $testResult 42 | } 43 | 44 | function Verify-IsCertExpired 45 | { 46 | param ( 47 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 48 | $isCertExpired 49 | ) 50 | 51 | return ($isCertExpired.NotAfter - (Get-Date)).TotalDays -le 0 52 | } 53 | 54 | function Verify-IsCertSelfSigned 55 | { 56 | param ( 57 | [System.Security.Cryptography.X509Certificates.X509Certificate2] 58 | $isCertSelfSigned 59 | ) 60 | 61 | return $isCertSelfSigned.Subject -eq $isCertSelfSigned.IssuerName.Name 62 | } 63 | 64 | function Generate-NotRunResults 65 | { 66 | param( 67 | [string] 68 | $certificateType, 69 | [string] 70 | $notRunReason, 71 | [bool] 72 | $isPrimary = $true, 73 | [int] 74 | $testsRanCount = 0, 75 | [string] 76 | $exceptionMessage = $null 77 | ) 78 | 79 | $results = @() 80 | 81 | if($testsRanCount-- -le 0){ $results += Test-CertificateAvailable -certificateAvailable $null -certificateType $certType -isPrimary $isPrimary -notRunReason $notRunReason } 82 | if($testsRanCount-- -le 0){ $results += Test-CertificateSelfSigned -certSelfSigned $null -certificateType $certType -isPrimary $isPrimary -notRunReason $notRunReason } 83 | if($testsRanCount-- -le 0){ $results += Test-CertificateHasPrivateKey -certHasPrivateKey $null -certificateType $certType -isPrimary $isPrimary -notRunReason $notRunReason -storeName "" -storeLocation ""} 84 | if($testsRanCount-- -le 0){ $results += Test-CertificateExpired -certExpired $null -certificateType $certType -isPrimary $isPrimary -notRunReason $notRunReason} 85 | if($testsRanCount-- -le 0){ $results += Test-CertificateCRL -certCrl $null -certificateType $certType -isPrimary $isPrimary -notRunReason $notRunReason} 86 | if($testsRanCount-- -le 0){ $results += Test-CertificateAboutToExpire -certAboutToExpire $null -certificateType $certType -isPrimary $isPrimary -notRunReason $notRunReason} 87 | 88 | if($exceptionMessage) 89 | { 90 | $results | ForEach {$_.ExceptionMessage = $exceptionMessage} 91 | } 92 | 93 | return $results 94 | } 95 | 96 | Function Get-AdfsCertificateList([switch] $RemovePrivateKey) 97 | { 98 | $adfsCertificateCollection = @() 99 | 100 | $adfsTokenCerts = Get-AdfsCertificate 101 | 102 | foreach ($adfsTokenCert in $adfsTokenCerts) 103 | { 104 | $certToAdd = new-Object PSObject 105 | if ($RemovePrivateKey) 106 | { 107 | $tokenCert = GetNormalizedCert $adfsTokenCert.Certificate 108 | } 109 | else 110 | { 111 | $tokenCert = $adfsTokenCert.Certificate 112 | } 113 | $certToAdd | Add-Member -NotePropertyName "Certificate" -NotePropertyValue $tokenCert 114 | $certToAdd | Add-Member -NotePropertyName "CertificateType" -NotePropertyValue $adfsTokenCert.CertificateType 115 | $certToAdd | Add-Member -NotePropertyName "IsPrimary" -NotePropertyValue $adfsTokenCert.IsPrimary 116 | $certToAdd | Add-Member -NotePropertyName "StoreName" -NotePropertyValue $adfsTokenCert.StoreName 117 | $certToAdd | Add-Member -NotePropertyName "StoreLocation" -NotePropertyValue $adfsTokenCert.StoreLocation 118 | $certToAdd | Add-Member -NotePropertyName "Thumbprint" -NotePropertyValue $adfsTokenCert.Thumbprint 119 | $adfsCertificateCollection += $certToAdd 120 | } 121 | 122 | $adfsSslBinding = GetSslBinding 123 | $sslCertToAdd = new-Object PSObject 124 | if ($RemovePrivateKey) 125 | { 126 | $sslCert = GetNormalizedCert $adfsSslBinding.Certificate 127 | } 128 | else 129 | { 130 | $sslCert = $adfsSslBinding.Certificate 131 | } 132 | $sslCertToAdd | Add-Member -NotePropertyName "Certificate" -NotePropertyValue $sslCert 133 | $sslCertToAdd | Add-Member -NotePropertyName "CertificateType" -NotePropertyValue "SSL" 134 | $sslCertToAdd | Add-Member -NotePropertyName "IsPrimary" -NotePropertyValue $true 135 | $sslCertToAdd | Add-Member -NotePropertyName "StoreName" -NotePropertyValue ([System.Security.Cryptography.X509Certificates.StoreName]::My) 136 | $sslCertToAdd | Add-Member -NotePropertyName "StoreLocation" -NotePropertyValue ([System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) 137 | $sslCertToAdd | Add-Member -NotePropertyName "Thumbprint" -NotePropertyValue ($adfsSslBinding.Thumbprint) 138 | 139 | $adfsCertificateCollection += $sslCertToAdd 140 | 141 | return $adfsCertificateCollection 142 | } 143 | 144 | Function Get-AdfsCertificatesToTest() 145 | { 146 | 147 | $endpoints = Get-AdfsEndpoint | where {$_.SecurityMode -eq 'Message' -and $_.Enabled -eq $true -and $_.AddressPath -ne '/adfs/services/trusttcp/windows'} 148 | $skipCommCert = ($endpoints -eq $null) 149 | 150 | $adfsCertificateCollection = Get-AdfsCertificateList 151 | 152 | if ($skipCommCert) 153 | { 154 | $adfsCertificateCollection = $adfsCertificateCollection | where {$_.CertificateType -ne "Service-Communications"} 155 | } 156 | 157 | return $adfsCertificateCollection 158 | } 159 | 160 | Function GetNormalizedCert([System.Security.Cryptography.X509Certificates.X509Certificate2]$normalizedCert) 161 | { 162 | if ($null -eq $normalizedCert) 163 | { 164 | return $null 165 | } 166 | 167 | $publicCertPortionBytes = [Byte[]]$normalizedCert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) 168 | $certToReturn = New-Object -Type System.Security.Cryptography.X509Certificates.X509Certificate2 169 | $certToReturn.Import($publicCertPortionBytes) 170 | 171 | return $certToReturn 172 | } 173 | 174 | function VerifyCertificateCRL($certCRL, $revocationCheckSetting) 175 | { 176 | if ( $null -eq $certCRL ) 177 | { 178 | return $null 179 | } 180 | 181 | $certSubject = $certCRL.Subject 182 | $isSelfSigned = $certSubject -eq $certCRL.IssuerName.Name 183 | 184 | if ($isSelfSigned) 185 | { 186 | #mark the test as passing for self-signed certificates 187 | $result = new-Object -TypeName PSObject 188 | $result | Add-Member -MemberType NoteProperty -Name Subject -Value $certCRL.Subject 189 | $result | Add-Member -MemberType NoteProperty -Name IsSelfSigned -Value $isSelfSigned 190 | $result | Add-Member -MemberType NoteProperty -Name Thumbprint -Value $certCRL.Thumbprint 191 | $result | Add-Member -MemberType NoteProperty -Name VerifyResult -Value "N/A" 192 | $result | Add-Member -MemberType NoteProperty -Name ChainBuildResult -Value @() 193 | $result | Add-Member -MemberType NoteProperty -Name ChainStatus -Value $true 194 | return $result 195 | } 196 | 197 | $chainBuildResult = $true 198 | $chainStatus = $null 199 | 200 | $verifyResult = $certCRL.Verify() 201 | 202 | #If set to none, ADFS will not even check this so ... scrap the results 203 | #to avoid surfacing noise to the user 204 | 205 | if ($revocationCheckSetting -ne "None") 206 | { 207 | $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain 208 | $chain.ChainPolicy.UrlRetrievalTimeout = New-TimeSpan -Seconds 10 209 | $chain.ChainPolicy.VerificationFlags = "AllowUnknownCertificateAuthority" 210 | 211 | switch($revocationCheckSetting) 212 | { 213 | "CheckEndCert" 214 | { 215 | $chain.ChainPolicy.RevocationFlag = "EndCertificateOnly" 216 | $chain.ChainPolicy.RevocationMode = "Online" 217 | } 218 | "CheckEndCertCacheOnly" 219 | { 220 | $chain.ChainPolicy.RevocationFlag = "EndCertificateOnly" 221 | $chain.ChainPolicy.RevocationMode = "Offline" 222 | } 223 | "CheckChain" 224 | { 225 | $chain.ChainPolicy.RevocationFlag = "EntireChain" 226 | $chain.ChainPolicy.RevocationMode = "Online" 227 | } 228 | 229 | "CheckChainCacheOnly" 230 | { 231 | $chain.ChainPolicy.RevocationFlag = "EntireChain" 232 | $chain.ChainPolicy.RevocationMode = "Offline" 233 | } 234 | "CheckChainExcludeRoot" 235 | { 236 | $chain.ChainPolicy.RevocationFlag = "ExcludeRoot" 237 | $chain.ChainPolicy.RevocationMode = "Online" 238 | } 239 | "CheckChainExcludeRootCacheOnly" 240 | { 241 | $chain.ChainPolicy.RevocationFlag = "ExcludeRoot" 242 | $chain.ChainPolicy.RevocationMode = "Offline" 243 | } 244 | default 245 | { 246 | $chain.ChainPolicy.RevocationFlag = "EntireChain" 247 | $chain.ChainPolicy.RevocationMode = "Online" 248 | } 249 | } 250 | 251 | $chainBuildResult = $chain.Build($certCRL) 252 | $chainStatus = $chain.ChainStatus 253 | } 254 | 255 | $certSubject = $certCRL.Subject 256 | $isSelfSigned = $certSubject -eq $certCRL.IssuerName.Name 257 | 258 | $result = new-Object -TypeName PSObject 259 | $result | Add-Member -MemberType NoteProperty -Name Subject -Value $certCRL.Subject 260 | $result | Add-Member -MemberType NoteProperty -Name IsSelfSigned -Value $isSelfSigned 261 | $result | Add-Member -MemberType NoteProperty -Name Thumbprint -Value $certCRL.Thumbprint 262 | $result | Add-Member -MemberType NoteProperty -Name VerifyResult -Value $verifyResult 263 | $result | Add-Member -MemberType NoteProperty -Name ChainBuildResult -Value $chainBuildResult 264 | $result | Add-Member -MemberType NoteProperty -Name ChainStatus -Value $chainStatus 265 | return $result 266 | } 267 | -------------------------------------------------------------------------------- /diagnosticsModule/Private/CommonHealthChecks.ps1: -------------------------------------------------------------------------------- 1 | # ADFS Service State 2 | Function TestIsAdfsRunning() 3 | { 4 | $testName = "IsAdfsRunning" 5 | $serviceStateOutputKey = "ADFSServiceState" 6 | try 7 | { 8 | $adfsServiceStateTestResult = New-Object TestResult -ArgumentList($testName); 9 | $adfsServiceState = (Get-WmiObject win32_service | Where-Object {$_.name -eq "adfssrv"}).State 10 | If ($adfsServiceState -ne "Running") 11 | { 12 | $adfsServiceStateTestResult.Result = [ResultType]::Fail; 13 | $adfsServiceStateTestResult.Detail = "Current State of adfssrv is: $adfsServiceState"; 14 | } 15 | $adfsServiceStateTestResult.Output = @{$serviceStateOutputKey = $adfsServiceState} 16 | 17 | return $adfsServiceStateTestResult; 18 | } 19 | catch [Exception] 20 | { 21 | return Create-ErrorExceptionTestResult $testName $_.Exception 22 | } 23 | } 24 | 25 | Function TestTLSMismatch 26 | { 27 | $testName = "TestTLSMismatch"; 28 | 29 | try 30 | { 31 | $testResult = New-Object TestResult -ArgumentList ($testName) 32 | 33 | $tls10Enabled = IsTlsVersionEnabled("1.0"); 34 | $tls11Enabled = IsTlsVersionEnabled("1.1"); 35 | $tls12Enabled = IsTlsVersionEnabled("1.2"); 36 | 37 | # If all TLS versions are enabled then there shouldn't be a mismatch between ADFS and WAP. 38 | if ($tls10Enabled -and $tls11Enabled -and $tls12Enabled) 39 | { 40 | return $testResult; 41 | } 42 | 43 | $str = "{0}{1}{2}" -f [int]$tls10Enabled, [int]$tls11Enabled, [int]$tls12Enabled; 44 | switch ($str) 45 | { 46 | "000" 47 | { 48 | Out-Verbose "All TLS versions are disabled" 49 | $testResult.Result = [ResultType]::Fail; 50 | $testResult.Detail = "Detected that all TLS versions are disabled. This will cause problems between your STS and Proxy servers. Fix this by enabling the correct TLS version."; 51 | } 52 | "001" 53 | { 54 | $message = "Detected that only TLS 1.2 is enabled. Ensure that this is also enabled on your other STS and Proxy servers."; 55 | Out-Warning $message; 56 | $testResult.Detail = $message; 57 | $testResult.Result = [ResultType]::Warning; 58 | } 59 | "010" 60 | { 61 | $message = "Detected that only TLS 1.1 is enabled. Ensure that this is also enabled on your other STS and Proxy servers."; 62 | Out-Warning $message; 63 | $testResult.Detail = $message; 64 | $testResult.Result = [ResultType]::Warning; 65 | } 66 | "100" 67 | { 68 | $message = "Detected that only TLS 1.0 is enabled. Ensure that this is also enabled on your other STS and Proxy servers."; 69 | Out-Warning $message; 70 | $testResult.Detail = $message; 71 | $testResult.Result = [ResultType]::Warning; 72 | } 73 | } 74 | 75 | return $testResult; 76 | } 77 | catch [Exception] 78 | { 79 | return Create-ErrorExceptionTestResult $testName $_.Exception; 80 | } 81 | } 82 | 83 | Function TestAdfsEventLogs 84 | { 85 | $testName = "TestAdfsEventLogs"; 86 | Out-Verbose "Running test to check event logs for event ids known to be associated with WAP issues." 87 | try 88 | { 89 | $testResult = New-Object TestResult -ArgumentList ($testName); 90 | 91 | $adfsVersion = Get-AdfsVersion; 92 | if($adfsVersion -eq $null) 93 | { 94 | throw "Unable to determine AD FS version." 95 | } 96 | 97 | $pastPeriod = (Get-Date).AddDays(-7); 98 | 99 | if ($adfsVersion -eq $adfs2x) 100 | { 101 | $logName = "AD FS 2.0/Admin"; 102 | } 103 | else 104 | { 105 | $logName = "AD FS/Admin"; 106 | } 107 | 108 | Out-Verbose "Event log name = $logName"; 109 | $role = Get-AdfsRole; 110 | switch ($role) 111 | { 112 | $adfsRoleSTS 113 | { 114 | # These are the event IDs for events on AD FS that are known to be related to WAP problems. 115 | $id = @(276); 116 | } 117 | $adfsRoleProxy 118 | { 119 | # These are the event IDs for events on WAP that are known to be related to WAP problems. 120 | $id = @(224, 393, 394); 121 | } 122 | default 123 | { 124 | throw "Unable to determine server role." 125 | } 126 | } 127 | 128 | Out-Verbose "Checking event IDs = $id"; 129 | 130 | $events = Get-WinEvent -FilterHashTable @{LogName = $logName; StartTime = $pastPeriod; ID = $id} -ErrorAction SilentlyContinue; 131 | 132 | if ($events -ne $null -and $events.Count -ne 0) 133 | { 134 | Out-Verbose "Found events that indicate a problem with WAP and AD FS." 135 | $testResult.Result = [ResultType]::Fail; 136 | $testResult.Detail = "There were events found in the AD FS event logs that may be causing issues with the AD FS and WAP trust. Check the output for more details." 137 | $testResult.Output = @{"Events" = $events}; 138 | } 139 | 140 | return $testResult; 141 | } 142 | catch [Exception] 143 | { 144 | return Create-ErrorExceptionTestResult $testName $_.Exception; 145 | } 146 | } 147 | 148 | Function TestTimeSync 149 | { 150 | Param( 151 | [string[]] 152 | $adfsServers = $null 153 | ) 154 | 155 | Out-Verbose "Checking time synchronization." 156 | $testName = "TestTimeSync"; 157 | 158 | try 159 | { 160 | $testResult = New-Object TestResult -ArgumentList ($testName); 161 | 162 | if (Test-RunningRemotely) 163 | { 164 | $testResult.Detail = "This test does not need to run remotely."; 165 | $testResult.Result = [ResultType]::NotRun; 166 | return $testResult; 167 | } 168 | 169 | $role = Get-AdfsRole; 170 | switch ($role) 171 | { 172 | $adfsRoleSTS 173 | { 174 | Out-Verbose "Detected that the current server is an ADFS server."; 175 | 176 | if ($adfsServers -eq $null -or $adfsServers.Count -eq 0) 177 | { 178 | Out-Verbose "No farm information was provided. Only checking time synchronization with the local server and reliable time server." 179 | 180 | if (!(IsServerTimeInSyncWithReliableTimeServer)) 181 | { 182 | $testResult.Result = [ResultType]::Fail; 183 | $testResult.Detail = "This server's time is out of sync with reliable time server. Check and correct any time synchronization issues." 184 | } 185 | } 186 | else 187 | { 188 | Out-Verbose "Detected that farm information was available, checking time synchronization across multiple servers."; 189 | $serversNotInSync = @(); 190 | 191 | Out-Verbose "Checking localhost"; 192 | if (!(IsServerTimeInSyncWithReliableTimeServer)) 193 | { 194 | $serversNotInSync += "Localhost"; 195 | } 196 | 197 | foreach ($server in $adfsServers) 198 | { 199 | $session = New-PSSession -ComputerName $server -ErrorAction SilentlyContinue; 200 | if ($session -eq $null) 201 | { 202 | Out-Warning "There was a problem connecting to $server, skipping this server." 203 | continue; 204 | } 205 | 206 | Out-Verbose "Checking $server"; 207 | 208 | $Private = @(Get-ChildItem -Path $PSScriptRoot\*.ps1 -ErrorAction SilentlyContinue); 209 | 210 | $commonFunctions = (Get-Command $Private).ScriptContents; 211 | $commonFunctions = $commonFunctions -join [Environment]::NewLine; 212 | 213 | $isInSync = Invoke-Command -Session $session -ArgumentList $commonFunctions -ScriptBlock { 214 | Param( 215 | $commonFunctions 216 | ) 217 | Invoke-Expression $commonFunctions; 218 | 219 | return IsServerTimeInSyncWithReliableTimeServer; 220 | } 221 | 222 | if (!$isInSync) 223 | { 224 | $serversNotInSync += $server; 225 | } 226 | 227 | Remove-PSSession $session 228 | } 229 | 230 | if ($serversNotInSync.Count -ne 0) 231 | { 232 | $testResult.Result = [ResultType]::Fail; 233 | $testResult.Detail = "Some of the servers in your AD FS farm were out of sync with reliable time server. Check the output for a list of servers." 234 | $testResult.Output = @{ "ServersOutOfSync" = $serversNotInSync; } 235 | } 236 | } 237 | } 238 | $adfsRoleProxy 239 | { 240 | Out-Verbose "Detected that the current server is a WAP server."; 241 | 242 | if (!(IsServerTimeInSyncWithReliableTimeServer)) 243 | { 244 | $testResult.Result = [ResultType]::Fail; 245 | $testResult.Detail = "This server's time is out of sync with reliable time server. Check and correct any time synchronization issues." 246 | } 247 | } 248 | default 249 | { 250 | throw "Unable to determine server role." 251 | } 252 | } 253 | 254 | return $testResult; 255 | } 256 | catch [Exception] 257 | { 258 | return Create-ErrorExceptionTestResult $testName $_.Exception; 259 | } 260 | } 261 | 262 | Function TestNonSelfSignedCertificatesInRootStore 263 | { 264 | $testName = "TestNonSelfSignedCertificatesInRootStore"; 265 | $testResult = New-Object TestResult -ArgumentList($testName) 266 | $certificateOutputKey = "NonSelfSignedCertificates"; 267 | 268 | try 269 | { 270 | $nonSelfSignedCertificates = Get-ChildItem Cert:\LocalMachine\root -Recurse | Where-Object {$_.Issuer -ne $_.Subject} | Select-Object FriendlyName, Issuer, Subject, Thumbprint; 271 | 272 | if ($nonSelfSignedCertificates.Count -ne 0) 273 | { 274 | $testResult.Detail = "There were non-self-signed certificates found in the root store. Move them to the intermediate store."; 275 | $testResult.Result = [ResultType]::Fail; 276 | $testResult.Output = @{$certificateOutputKey = $nonSelfSignedCertificates}; 277 | } 278 | 279 | return $testResult; 280 | } 281 | catch [Exception] 282 | { 283 | return Create-ErrorExceptionTestResult $testName $_.Exception 284 | } 285 | } 286 | 287 | Function TestSelfSignedCertificatesInIntermediateCaStore 288 | { 289 | $testName = "TestSelfSignedCertificatesInIntermediateCaStore"; 290 | $testResult = New-Object TestResult -ArgumentList($testName) 291 | $certificateOutputKey = "SelfSignedCertificates"; 292 | 293 | try 294 | { 295 | $selfSignedCertificates = Get-ChildItem Cert:\LocalMachine\CA | Where-Object { $_.Issuer -eq $_.Subject } | Select-Object FriendlyName, Issuer, Subject, Thumbprint; 296 | 297 | if ($selfSignedCertificates.Count -ne 0) 298 | { 299 | $testResult.Detail = "There were self-signed certificates found in the intermediate CA store. Move them to the root certificate store."; 300 | $testResult.Result = [ResultType]::Fail; 301 | $testResult.Output = @{$certificateOutputKey = $selfSignedCertificates}; 302 | } 303 | 304 | return $testResult; 305 | } 306 | catch [Exception] 307 | { 308 | return Create-ErrorExceptionTestResult $testName $_.Exception 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /diagnosticsModule/Private/Constants.ps1: -------------------------------------------------------------------------------- 1 | #################################### 2 | # Constants 3 | #################################### 4 | $adfs4 = "4.0" 5 | $adfs3 = "3.0"; 6 | $adfs2x = "2.0"; 7 | $adfsRoleSTS = "STS"; 8 | $adfsRoleProxy = "Proxy"; 9 | $tpKey = "Thumbprint"; 10 | $sslCertType = "SSL"; 11 | $adfsServiceName = "adfssrv"; 12 | $adfsProxyServiceName = "appproxysvc"; 13 | $ctlStoreName = "AdfsTrustedDevices"; 14 | $localMachine = "LocalMachine"; 15 | $adfsApplicationId = "{5d89a20c-beab-4389-9447-324788eb944a}"; 16 | $TlsPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS {0}"; 17 | $TlsServerPath = "{0}\Server"; 18 | $TlsClientPath = "{0}\Client"; 19 | $timeDifferenceMaximum = 300; #seconds 20 | 21 | $maxJsonDepth = 8 22 | 23 | $Tls10 = [System.Net.SecurityProtocolType]::Tls; 24 | $Tls11 = [System.Net.SecurityProtocolType]::Tls11; 25 | $Tls12 = [System.Net.SecurityProtocolType]::Tls12; 26 | 27 | $none = "NONE"; 28 | $script:adfsProperties = $null; 29 | $script:isAdfsSyncPrimaryRole = $null; 30 | 31 | # Email address regex taken from MSDN: http://msdn.microsoft.com/en-us/library/01escwtf.aspx 32 | $EmailAddressRegex = "^(?("")("".+?(? AllTests { get; set; } 35 | 36 | public List ReachableServers { get; set; } 37 | 38 | public List UnreachableServers { get; set; } 39 | 40 | public TestResultsContainer() 41 | { 42 | AllTests = new List(); 43 | } 44 | 45 | public TestResultsContainer(TestResult[] newResults, string[] reachableServers, string[] unreachableServers) 46 | { 47 | AllTests = new List(); 48 | AllTests.AddRange(newResults); 49 | ReachableServers = new List(); 50 | ReachableServers.AddRange(reachableServers); 51 | UnreachableServers = new List(); 52 | UnreachableServers.AddRange(unreachableServers); 53 | } 54 | 55 | public void Add(TestResult newResult) 56 | { 57 | AllTests.Add(newResult); 58 | } 59 | 60 | public void Add(TestResult[] newResults) 61 | { 62 | AllTests.AddRange(newResults); 63 | } 64 | 65 | public IEnumerable this[string testName] 66 | { 67 | get { return AllTests.Where(m => m.Name == testName).ToList(); } 68 | } 69 | 70 | public IEnumerable GetTestsByComputer(string computerName) 71 | { 72 | return AllTests.Where(m => m.ComputerName == computerName).ToList(); 73 | } 74 | 75 | public IEnumerable PassedTests 76 | { 77 | get { return AllTests.Where(m => m.Result == ResultType.Pass).ToList(); } 78 | } 79 | 80 | public IEnumerable WarningTests 81 | { 82 | get { return AllTests.Where(m => m.Result == ResultType.Warning).ToList(); } 83 | } 84 | 85 | public IEnumerable FailedTests 86 | { 87 | get { return AllTests.Where(m => m.Result == ResultType.Fail).ToList(); } 88 | } 89 | 90 | public IEnumerable ErrorTests 91 | { 92 | get { return AllTests.Where(m => m.Result == ResultType.Error).ToList(); } 93 | } 94 | 95 | public IEnumerable NotRunTests 96 | { 97 | get { return AllTests.Where(m => m.Result == ResultType.NotRun).ToList(); } 98 | } 99 | } 100 | 101 | public enum ResultType 102 | { 103 | Pass, 104 | NotRun, 105 | Fail, 106 | Error, 107 | Warning 108 | } 109 | 110 | public enum OSVersion 111 | { 112 | WS2012, 113 | WS2012R2, 114 | WS2016, 115 | Unknown 116 | } 117 | 118 | "@; 119 | 120 | #################################### 121 | # AdHealthAgentInformation Data type 122 | #################################### 123 | 124 | Add-Type -Language CSharp @" 125 | public class AdHealthAgentInformation 126 | { 127 | public string Version; 128 | public string UpdateState; 129 | public string LastUpdateAttemptVersion; 130 | public System.DateTime LastUpdateAttemptTime; 131 | public int NumberOfFailedAttempts; 132 | public string InstallerExitCode; 133 | } 134 | 135 | "@; 136 | 137 | Add-Type -Assembly System; 138 | 139 | Add-Type -Language CSharp @" 140 | 141 | using System; 142 | using System.Collections.Generic; 143 | using System.ComponentModel; 144 | using System.Linq; 145 | using System.Runtime.InteropServices; 146 | using System.Text; 147 | using System.Threading.Tasks; 148 | 149 | namespace Microsoft.Identity.Health.Adfs 150 | { 151 | public class NetUtil 152 | { 153 | [System.Flags] 154 | public enum AuditEventType 155 | { 156 | POLICY_AUDIT_EVENT_UNCHANGED=0x00000000, 157 | POLICY_AUDIT_EVENT_SUCCESS=0x00000001, 158 | POLICY_AUDIT_EVENT_FAILURE=0x00000002, 159 | POLICY_AUDIT_EVENT_NONE=0x00000004 160 | } 161 | 162 | [DllImport("Advapi32.dll")] 163 | private static extern void AuditFree(IntPtr pBuf); 164 | 165 | [StructLayout(LayoutKind.Sequential)] 166 | private class AUDIT_POLICY_INFORMATION 167 | { 168 | public GUID AuditSubCategoryGuid; 169 | public UInt32 AuditingInformation; 170 | public GUID AuditCategoryGuid; 171 | } 172 | 173 | private struct GUID 174 | { 175 | public int a; 176 | public short b; 177 | public short c; 178 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] 179 | public byte[] d; 180 | } 181 | 182 | [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] 183 | private static extern bool AuditQuerySystemPolicy( 184 | [In] ref GUID pSubCategoryGuids, 185 | [In] UInt32 PolicyCount, 186 | [Out] out IntPtr pAuditPolicies); 187 | 188 | 189 | [DllImport("kernel32.dll")] 190 | private static extern uint GetLastError(); 191 | 192 | public static AuditEventType CheckAudit() 193 | { 194 | IntPtr pBuffer = IntPtr.Zero; 195 | try 196 | { 197 | AUDIT_POLICY_INFORMATION info; 198 | ulong policyCount = 1; 199 | bool status; 200 | //<0cce9222-69ae-11d9-bed3-505054503030> guid corresponds to auditpol subcategory 201 | GUID guid = new GUID(); 202 | guid.a = 0x0cce9222; 203 | guid.b = 0x69ae; 204 | guid.c = 0x11d9; 205 | guid.d = new byte[] { 0xbe, 0xd3, 0x50, 0x50, 0x54, 0x50, 0x30, 0x30 }; 206 | 207 | status = AuditQuerySystemPolicy(ref guid, (UInt32)policyCount, out pBuffer); 208 | if (!status) 209 | { 210 | uint errorcode = GetLastError(); 211 | } 212 | 213 | info = (AUDIT_POLICY_INFORMATION)Marshal.PtrToStructure(pBuffer, typeof(AUDIT_POLICY_INFORMATION)); 214 | AuditEventType eventType = (AuditEventType)info.AuditingInformation; 215 | return eventType; 216 | } 217 | catch (Exception e) 218 | { 219 | throw e; 220 | } 221 | finally 222 | { AuditFree(pBuffer); } 223 | } 224 | } 225 | } 226 | 227 | "@ -------------------------------------------------------------------------------- /diagnosticsModule/Private/TestUtilities.ps1: -------------------------------------------------------------------------------- 1 | Function CreateTestResultFromPSObject($obj) 2 | { 3 | $testResult = New-Object TestResult -ArgumentList($obj.Name); 4 | $testResult.Result = $obj.Result; 5 | $testResult.Detail = $obj.Detail; 6 | $testResult.Output = $obj.Output; 7 | $testResult.ExceptionMessage = $obj.ExceptionMessage; 8 | $testResult.Exception = $obj.Exception; 9 | 10 | return $testResult; 11 | } 12 | 13 | Function Create-NotRunOnSecondaryTestResult 14 | { 15 | param( 16 | [string] $testName 17 | ) 18 | $testResult = New-Object TestResult -ArgumentList($testName) 19 | $testResult.Result = [ResultType]::NotRun; 20 | $testResult.Detail = "This check runs only on Primary Nodes." 21 | return $testResult 22 | } 23 | 24 | Function Create-ErrorExceptionTestResult 25 | { 26 | param( 27 | [string] 28 | $testName, 29 | [Exception] 30 | $exception 31 | ) 32 | 33 | $testResult = New-Object TestResult -ArgumentList($testName); 34 | $testResult.Result = [ResultType]::Error; 35 | $testResult.ExceptionMessage = $exception.Message; 36 | $testResult.Exception = $exception.ToString(); 37 | return $testResult; 38 | } 39 | 40 | Function Invoke-TestFunctions 41 | { 42 | param( 43 | [string] 44 | $role, 45 | [array] 46 | $functionsToRun, 47 | $functionArguments 48 | ) 49 | 50 | $results = @() 51 | $totalFunctions = $functionsToRun.Count 52 | $functionCount = 0 53 | foreach ($function in $functionsToRun) 54 | { 55 | $functionCount++ 56 | $percent = 100 * $functionCount / $totalFunctions 57 | Write-Progress -Activity "Executing Tests for $role" -Status $function -PercentComplete $percent 58 | $result = Invoke-Expression $function 59 | $results = $results + $result 60 | } 61 | 62 | return $results 63 | } 64 | 65 | Function TestAdfsSTSHealth() 66 | { 67 | Param 68 | ( 69 | $verifyO365 = $true, 70 | [string[]] 71 | $adfsServers = $null 72 | ) 73 | 74 | $functionArguments = @{"adfsServers" = $adfsServers}; 75 | 76 | $role = Get-ADFSRole 77 | 78 | if ($role -ne $adfsRoleSTS) 79 | { 80 | return 81 | } 82 | 83 | # Determine ADFS Version 84 | $ADFSVersion = Get-AdfsVersion 85 | 86 | Import-ADFSAdminModule 87 | 88 | #force refresh of ADFS Properties 89 | 90 | try 91 | { 92 | $props = Retrieve-AdfsProperties -force 93 | } 94 | catch 95 | { 96 | #do nothing, other than prevent the error record to go to the pipeline 97 | } 98 | 99 | $functionsToRun = @( ` 100 | "TestIsAdfsRunning", ` 101 | "TestIsWidRunning", ` 102 | "TestPingFederationMetadata", ` 103 | "TestSslBindings", ` 104 | "Test-AdfsCertificates", ` 105 | "TestADFSDNSHostAlias", ` 106 | "TestADFSDuplicateSPN", ` 107 | "TestServiceAccountProperties", ` 108 | "TestAppPoolIDMatchesServiceID", ` 109 | "TestComputerNameEqFarmName", ` 110 | "TestSSLUsingADFSPort", ` 111 | "TestSSLCertSubjectContainsADFSFarmName", ` 112 | "TestAdfsAuditPolicyEnabled", ` 113 | "TestAdfsRequestToken", ` 114 | "TestTrustedDevicesCertificateStore", ` 115 | "TestAdfsPatches", ` 116 | "TestServicePrincipalName", ` 117 | "TestTLSMismatch", ` 118 | "TestNonSelfSignedCertificatesInRootStore", ` 119 | "TestSelfSignedCertificatesInIntermediateCaStore"); 120 | 121 | if (($adfsServers -eq $null) -or ($adfsServers.Count -eq 0)) 122 | { 123 | $functionsToRun += "TestProxyTrustPropagation"; 124 | $functionsToRun += "TestTimeSync"; 125 | } 126 | else 127 | { 128 | $functionsToRun += "TestProxyTrustPropagation -adfsServers `$functionArguments.adfsServers"; 129 | $functionsToRun += "TestTimeSync -adfsServers `$functionArguments.adfsServers"; 130 | } 131 | 132 | if ($verifyO365 -eq $true) 133 | { 134 | $functionsToRun = $functionsToRun + @( ` 135 | "TestOffice365Endpoints", ` 136 | "TestADFSO365RelyingParty"); 137 | } 138 | 139 | return Invoke-TestFunctions -role $adfsRoleSTS -functionsToRun $functionsToRun -functionArguments $functionArguments; 140 | } 141 | 142 | Function TryTestAdfsSTSHealthOnFarmNodes() 143 | { 144 | Param( 145 | $verifyO365 = $true, 146 | [string[]] 147 | $adfsServers = $null, 148 | [switch] 149 | $local = $false 150 | ) 151 | $computerSystemObject = Get-WmiObject win32_computersystem 152 | $fqdn = $computerSystemObject.DNSHostName + "." + $computerSystemObject.Domain 153 | 154 | Out-Verbose "Attempting to run AD FS STS health checks farm wide."; 155 | if (($adfsServers -eq $null -or $adfsServers.Count -eq 0) -and (-not ($local))) 156 | { 157 | Out-Verbose "Detected that no farm information was provided."; 158 | $osVersion = Get-OsVersion; 159 | $isPrimary = IsAdfsSyncPrimaryRole; 160 | if ($osVersion -eq [OSVersion]::WS2016 -and $isPrimary) 161 | { 162 | $adfsServers = @(); 163 | Write-Host "Detected OS as Windows Server 2016, attempting to run health checks across all of your AD FS servers in your farm."; 164 | 165 | $nodes = (Get-AdfsFarmInformation).FarmNodes; 166 | foreach ($server in $nodes) 167 | { 168 | # We skip adding the node that corresponds to this server. 169 | if ($server.FQDN -like $fqdn) 170 | { 171 | continue; 172 | } 173 | 174 | $adfsServers += $server 175 | } 176 | Out-Verbose "Detected the following servers in the farm: $adfsServers"; 177 | } 178 | } 179 | else 180 | { 181 | # We filter out this computer's name and FQDN 182 | $adfsServers = $adfsServers | Where-Object { ($_ -notlike [System.Net.Dns]::GetHostByName(($env:computerName)).HostName) -and ($_ -notlike $env:COMPUTERNAME) -and ($_ -notlike $fqdn) }; 183 | } 184 | 185 | $results = @(); 186 | $reachableServer = @(); 187 | $unreachableServer = @(); 188 | 189 | $reachableServer += $fqdn; 190 | 191 | Write-Host "Running the health checks on the local machine."; 192 | $result = TestAdfsSTSHealth -verifyO365 $verifyO365 -verifyTrustCerts $verifyTrustCerts -adfsServers $adfsServers; 193 | foreach ($test in $result) 194 | { 195 | $test.ComputerName = $fqdn; 196 | } 197 | 198 | $results += $result; 199 | 200 | if (($adfsServers -ne $null -and $adfsServers.Count -ne 0) -and (-not ($local))) 201 | { 202 | Write-Host "Running health checks on other servers in farm." 203 | 204 | $Private = @(Get-ChildItem -Path $PSScriptRoot\*.ps1 -ErrorAction SilentlyContinue); 205 | $Public = @(Get-ChildItem -Path $PSScriptRoot\..\Public\*.ps1 -ErrorAction SilentlyContinue); 206 | $AllFunctionFiles = $Private + $Public; 207 | 208 | $commonFunctions = (Get-Command $AllFunctionFiles).ScriptContents; 209 | $commonFunctions = $commonFunctions -join [Environment]::NewLine; 210 | 211 | foreach ($server in $adfsServers) 212 | { 213 | Write-Host "Running health checks on $server."; 214 | $session = New-PSSession -ComputerName $server -ErrorAction SilentlyContinue; 215 | if ($session -eq $null) 216 | { 217 | Out-Warning "There was a problem connecting to $server, skipping this server." 218 | $unreachableServer += $server; 219 | continue; 220 | } 221 | 222 | $deserializedResult = Invoke-Command -Session $session -ArgumentList $commonFunctions -ScriptBlock { 223 | param($commonFunctions) 224 | Invoke-Expression $commonFunctions; 225 | return TestAdfsSTSHealth; 226 | } 227 | 228 | $reachableServer += $server; 229 | 230 | $serializedResult = @(); 231 | 232 | foreach ($obj in $deserializedResult) 233 | { 234 | $newObj = CreateTestResultFromPSObject $obj; 235 | $newObj.ComputerName = $server; 236 | $serializedResult += $newObj; 237 | } 238 | 239 | $results += $serializedResult; 240 | 241 | if ($session) 242 | { 243 | Remove-PSSession $Session 244 | } 245 | } 246 | } 247 | 248 | Write-Host "Successfully completed all health checks."; 249 | return New-Object TestResultsContainer -ArgumentList($results, $reachableServer, $unreachableServer); 250 | } 251 | 252 | Function TestAdfsProxyHealth() 253 | { 254 | Param( 255 | [string] 256 | $sslThumbprint 257 | ) 258 | 259 | $computerSystemObject = Get-WmiObject win32_computersystem 260 | $fqdn = $computerSystemObject.DNSHostName + "." + $computerSystemObject.Domain 261 | 262 | $functionArguments = @{"AdfsSslThumbprint" = $sslThumbprint}; 263 | 264 | $functionsToRun = @( ` 265 | "TestIsAdfsRunning", ` 266 | "TestIsAdfsProxyRunning", ` 267 | "TestSTSReachableFromProxy", ` 268 | "TestTLSMismatch", ` 269 | "TestTimeSync", ` 270 | "TestNonSelfSignedCertificatesInRootStore", ` 271 | "TestSelfSignedCertificatesInIntermediateCaStore"); 272 | 273 | if ([string]::IsNullOrWhiteSpace($sslThumbprint)) 274 | { 275 | # If Connect Health executed this test there is no way to find this thumbprint so we skip the test 276 | if (-not (IsExecutedByConnectHealth)) 277 | { 278 | $functionsToRun += "TestProxySslBindings" 279 | } 280 | } 281 | else 282 | { 283 | $functionsToRun += "TestProxySslBindings -AdfsSslThumbprint `$functionArguments.AdfsSslThumbprint"; 284 | } 285 | 286 | $results = Invoke-TestFunctions -role "Proxy" -functionsToRun $functionsToRun -functionArguments $functionArguments; 287 | foreach ($test in $results) 288 | { 289 | $test.ComputerName = $fqdn; 290 | } 291 | 292 | Write-Host "Successfully completed all health checks."; 293 | $reachableServer = @($fqdn); 294 | $unreachableServer = @(); 295 | return New-Object TestResultsContainer -ArgumentList($results, $reachableServer, $unreachableServer); 296 | } 297 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Export-AdfsDiagnosticsFile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Gathers and exports diagnostic data into a file. This cmdlet is used with the Diagnostics Analyzer Tool on 4 | the AD FS Help website (https://adfshelp.microsoft.com/DiagnosticsAnalyzer). 5 | 6 | .DESCRIPTION 7 | The Export-AdfsDiagnosticsFile cmdlet gathers diagnostic data from the current AD FS server and exports the diagnostic file 8 | required for the AD FS Help Diagnostic Analyzer. This cmdlet works on AD FS 2.0 and later. 9 | 10 | .PARAMETER FilePath 11 | String parameter that specifies the location of the exported file. By default, a file will be created in the current folder. 12 | 13 | .PARAMETER IncludeTrusts 14 | Boolean parameter that will enable additional checks for relying party trust and claims provider trust certificates. It is false by default. 15 | 16 | .PARAMETER SslThumbprint 17 | String parameter that corresponds to the thumbprint of the AD FS SSL certificate. This is required for running test cases on proxy servers. 18 | 19 | .PARAMETER AdfsServers 20 | Array of fully qualified domain names (FQDN) of all of the AD FS STS servers that you want to run health checks on. For Windows Server 2016 this is automatically populated using Get-AdfsFarmInformation. 21 | By default the tests are already run on the local machine, so it is not necessary include the FQDN of the current machine in this parameter. 22 | 23 | .PARAMETER Local 24 | Switch that indicates that you only want to run the health checks on the local machine. This takes precedence over -AdfsServers parameter. 25 | 26 | .EXAMPLE 27 | Export-AdfsDiagnosticsFile -IncludeTrusts:$true 28 | Export a diagnostic file of an AD FS Farm and examine the relying party trust and claims provider trust certificates. 29 | 30 | .EXAMPLE 31 | Export-AdfsDiagnosticsFile -adfsServers @("sts1.contoso.com", "sts2.contoso.com", "sts3.contoso.com") 32 | Export a diagnostic file of an AD FS farm by running checks on the following servers: sts1.contoso.com, sts2.contoso.com, sts3.contoso.com. This automatically runs the test on the local machine as well. 33 | 34 | .EXAMPLE 35 | Export-AdfsDiagnosticsFile -sslThumbprint ‎c1994504c91dfef663b5ce8dd22d1a44748a6e16 36 | Export a diagnostic file of a WAP server and utilize the provided thumbprint to check SSL bindings. 37 | #> 38 | 39 | 40 | # the final output format is as follows (in JSON): 41 | # diagnosticData: 42 | # { module1: { cmdlet1.1: results, cmdlet1.2: results, ...}, 43 | # module2: { cmdlet2.1: results, ...} 44 | # ... 45 | # } 46 | # where results will be the desired output or an exception message. 47 | Function Export-AdfsDiagnosticsFile() 48 | { 49 | # aggregate parameters for all cmdlets 50 | [CmdletBinding()] 51 | Param 52 | ( 53 | [string] $filePath = $null, 54 | [switch] $includeTrusts = $false, 55 | [string] $sslThumbprint = $null, 56 | [string[]] $adfsServers = $null, 57 | [switch] $local = $null 58 | ) 59 | 60 | # generate filePath at current folder if filePath is not provided by user 61 | if (!$filePath) 62 | { 63 | $filePath= -join("ADFSDiagnosticsFile-", (Get-Date -UFormat %Y%m%d%H%M%S), ".json") 64 | } 65 | 66 | # create file if the file doesn't exist 67 | if (!(Test-Path -Path $filePath)) 68 | { 69 | Out-Verbose "Creating file $filePath" 70 | New-Item $filePath -ItemType "file" > $null 71 | } 72 | 73 | $filePath = (Resolve-Path -Path $filePath).Path 74 | 75 | # run the private JSON generator for diagnostic data 76 | $JSONDiagnosticData = GenerateJSONDiagnosticData -includeTrusts:$includeTrusts -sslThumbprint $sslThumbprint -adfsServers $adfsServers -local:$local; 77 | 78 | Out-Verbose "Outputting diagnostic data at $filePath" 79 | Out-File -FilePath $filePath -InputObject $JSONDiagnosticData -Encoding ascii 80 | 81 | # print message for the user to find the file 82 | Write-Host "Please upload the diagnostic file located at $filePath to https://adfshelp.microsoft.com/DiagnosticsAnalyzer/Analyze." 83 | } 84 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Get-AdfsServerTrace.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Retrieves all the AD FS events generated given an Activity ID ID accross different computers 4 | 5 | .DESCRIPTION 6 | The Get-ADFSActivityIdRecords cmdlet queries all computers' event logs for the activity ID supplied in parallel, and them combines and sorts the results. 7 | This cmdlet works in AD FS 2.0 and later. 8 | 9 | 10 | .PARAMETER ActivityId 11 | Activity ID to search for. This typically comes from an AD FS error page. 12 | 13 | .PARAMETER ComputerName 14 | It is an array of computers, which represents the AD FS servers to try. If omitted, the local machine will be used 15 | 16 | .PARAMETER OutHtmlFilePath 17 | If supplied, the results will be generated in an html table format, saved in the path specified and opened in the internet browser. 18 | 19 | .EXAMPLE 20 | Get-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -ComputerName @("ADFSSRV1","ADFSSRV2") 21 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df on Servers ADFSSRV1 and ADFSSRV2 22 | 23 | .EXAMPLE 24 | Get-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -ComputerName (Get-Content .\Servers.txt) 25 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df from servers in a text file 26 | 27 | .EXAMPLE 28 | Get-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -IncludeDebug -ComputerName @("ADFSSRV1","ADFSSRV2") 29 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df on Server ADFSSRV1 and ADFSSRV2, including debug traces 30 | 31 | .EXAMPLE 32 | Get-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -IncludeDebug -ComputerName @("ADFSSRV1","ADFSSRV2") -OutHtmlFilePath ".\ActivityIdReport.htm" 33 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df on Server ADFSSRV1 and ADFSSRV2, including debug traces and save the result in an HTML file. 34 | 35 | .NOTES 36 | You need to run this function using an account that has permissions to read the event logs in all computers supplied. 37 | This is typically achieved having the account be part of the "Event Log Readers" Local Security Group. 38 | The computers supplied also should have firewall rules configured to allow remote readings. 39 | #> 40 | Function Get-AdfsServerTrace 41 | { 42 | [CmdletBinding()] 43 | param 44 | ( 45 | [Parameter(Mandatory = $true)] 46 | [ValidateNotNullOrEmpty()] 47 | [string] 48 | $ActivityId, 49 | 50 | [switch] 51 | $IncludeDebug, 52 | 53 | [string] 54 | $OutHtmlFilePath, 55 | 56 | [string[]] 57 | $ComputerName = @("localhost") 58 | ) 59 | 60 | #Get the background jobs to search all computers in parallel, and retrieve the results 61 | $jobs = Start-AdfsServerTrace -ActivityId $ActivityId -IncludeDebug:$IncludeDebug -ComputerName $ComputerName 62 | $results = Receive-AdfsServerTrace -Jobs $jobs 63 | 64 | if ($OutHtmlFilePath) 65 | { 66 | $results | ConvertTo-Html | Out-File $OutHtmlFilePath -Force 67 | Write-Output "Report Generated at $OutHtmlFilePath" 68 | Start $OutHtmlFilePath 69 | } 70 | else 71 | { 72 | Write-Output $results 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Get-AdfsSystemInformation.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Retrieves overall details of the computer 4 | 5 | .DESCRIPTION 6 | The Get-AdfsSystemInformation gathers information regarding operating system and hardware 7 | 8 | .EXAMPLE 9 | Get-AdfsSystemInformation | ConvertTo-Json | Out-File ".\ADFSFarmDetails.txt" 10 | Get the operating system data of the server and save it in JSON format 11 | #> 12 | Function Get-AdfsSystemInformation() 13 | { 14 | [CmdletBinding()] 15 | Param() 16 | 17 | try 18 | { 19 | $role = Get-ADFSRole 20 | } 21 | catch 22 | { 23 | $role = "none" 24 | } 25 | 26 | $systemOutput = New-Object PSObject; 27 | 28 | $osVersionPropertyName = "OSVersion" 29 | $adfsVersionPropertyName = "AdfsVersion" 30 | try 31 | { 32 | $OSVersion = [System.Environment]::OSVersion.Version 33 | $systemOutput | Add-Member NoteProperty -name $osVersionPropertyName -value $OSVersion -Force; 34 | $systemOutput | Add-Member NoteProperty -name $adfsVersionPropertyName -value (Get-AdfsVersion($OSVersion)) -Force; 35 | } 36 | catch 37 | { 38 | $systemOutput | Add-Member NoteProperty -name $osVersionPropertyName -value $null -Force 39 | $systemOutput | Add-Member NoteProperty -name $adfsVersionPropertyName -value $null -Force 40 | } 41 | $osNamePropertyName = "OSName" 42 | $lastRebootTimePropertyName = "LastRebootTime" 43 | try 44 | { 45 | $operatingSystem = Get-WmiObject -Class Win32_OperatingSystem; 46 | $systemOutput | Add-Member NoteProperty -name $osNamePropertyName -value $operatingSystem.Caption -Force; 47 | $systemOutput | Add-Member NoteProperty -name $lastRebootTimePropertyName -value $operatingSystem.ConvertToDateTime($operatingSystem.LastBootUpTime).ToUniversalTime() -Force; 48 | } 49 | catch 50 | { 51 | $systemOutput | Add-Member NoteProperty -name $osNamePropertyName -value $null -Force; 52 | $systemOutput | Add-Member NoteProperty -name $lastRebootTimePropertyName -value $null -Force; 53 | } 54 | 55 | $domainPropertyName = "MachineDomain" 56 | $machineTypePropertyName = "MachineType" 57 | $physicalMemoryPropertyName = "PhsicalMemory" 58 | $top10ProcessesByMemoryPropertyName = "Top10ProcessesByMemory" 59 | try 60 | { 61 | $computerSystem = Get-WmiObject -class win32_computersystem; 62 | $systemOutput | Add-Member NoteProperty -name $domainPropertyName -value $computerSystem.Domain -Force 63 | $systemOutput | Add-Member NoteProperty -name $machineTypePropertyName -value $computerSystem.Model -Force; 64 | 65 | try 66 | { 67 | $totalMemoryInMb = (Get-WmiObject -class "Win32_PerfRawData_Counters_HyperVDynamicMemoryIntegrationService" -ErrorAction Stop | Select-Object -ExpandProperty MaximumMemoryMBytes) 68 | } 69 | catch 70 | { 71 | # class (Win32_PerfRawData_Counters_HyperVDynamicMemoryIntegrationService) does not exist in Windows 2008 R2/Windows 7 or earlier operating systems 72 | # explicitly set to empty to force code to recalculate the physical memory 73 | $totalMemoryInMb = "" 74 | } 75 | 76 | if([string]::IsNullOrEmpty($totalMemoryInMb)) 77 | { 78 | $totalMemory = ($computerSystem | Measure-Object -Property TotalPhysicalMemory -Sum | Select-Object -ExpandProperty Sum) 79 | $totalMemoryInMb = [Math]::Round($totalMemory / 1Mb) 80 | } 81 | $systemOutput | Add-Member NoteProperty -name $physicalMemoryPropertyName -value $totalMemoryInMb -Force 82 | 83 | try 84 | { 85 | #Get the top 10 with the highest private working set memory, adding the percentage of total 86 | $processes = gwmi -Class Win32_PerfRawData_PerfProc_Process -Property @("Name","WorkingSetPrivate") 87 | $top10ProcessesByMemory = $processes | sort WorkingSetPrivate -Descending | Where-Object {$_.Name -ne "_Total"} | ` 88 | Select-Object -First 10 -Property ` 89 | Name,` 90 | @{Name="MemoryInMB";Expression = {$_.WorkingSetPrivate / 1Mb}},` 91 | @{Name="MemoryPercentOfTotal";Expression = {100 * $_.WorkingSetPrivate / $totalMemory}} 92 | $systemOutput | Add-Member NoteProperty -name $top10ProcessesByMemoryPropertyName -value $top10ProcessesByMemory -Force; 93 | } 94 | catch 95 | { 96 | $systemOutput | Add-Member NoteProperty -name $top10ProcessesByMemoryPropertyName -value $null -Force; 97 | } 98 | } 99 | catch 100 | { 101 | $systemOutput | Add-Member NoteProperty -name $domainPropertyName -value $null -Force 102 | $systemOutput | Add-Member NoteProperty -name $machineTypePropertyName -value $null -Force 103 | $systemOutput | Add-Member NoteProperty -name $physicalMemoryPropertyName -value $null -Force 104 | $systemOutput | Add-Member NoteProperty -name $top10ProcessesByMemoryPropertyName -value $null -Force; 105 | } 106 | 107 | $ipAddressPropertyName = "IPAddress" 108 | try 109 | { 110 | $systemOutput | Add-Member NoteProperty -name $ipAddressPropertyName -value (Get-WmiObject Win32_NetworkAdapterConfiguration -Namespace "root\CIMV2" | where{$_.IPEnabled -eq "True"}).IPAddress[0] -Force; 111 | } 112 | catch 113 | { 114 | $systemOutput | Add-Member NoteProperty -name $ipAddressPropertyName -value $null -Force; 115 | } 116 | 117 | $timeZonePropertyName = "TimeZone" 118 | try 119 | { 120 | $systemOutput | Add-Member NoteProperty -name $timeZonePropertyName -value ([System.TimeZone]::CurrentTimeZone.StandardName) -Force 121 | } 122 | catch 123 | { 124 | $systemOutput | Add-Member NoteProperty -name $timeZonePropertyName -value $null -Force 125 | } 126 | 127 | $processorsCountPropertyName = "NumberOfLogicalProcessors" 128 | $maxClockSpeedPropertyName = "MaxClockSpeed" 129 | try 130 | { 131 | $processor = Get-WmiObject -class win32_processor; 132 | $systemOutput | Add-Member NoteProperty -name $processorsCountPropertyName -value $processor.NumberOfLogicalProcessors -Force; 133 | $systemOutput | Add-Member NoteProperty -name $maxClockSpeedPropertyName -value $processor.MaxClockSpeed -Force; 134 | } 135 | catch 136 | { 137 | $systemOutput | Add-Member NoteProperty -name $processorsCountPropertyName -value $null -Force 138 | $systemOutput | Add-Member NoteProperty -name $maxClockSpeedPropertyName -value $null -Force 139 | } 140 | 141 | $hostsPropertyName = "Hosts" 142 | try 143 | { 144 | $hostsEntry = @{}; 145 | $hostsFile = Join-Path $env:SystemRoot "system32\drivers\etc\hosts" 146 | $regex = [regex] "\s+"; 147 | foreach ($line in Get-Content $hostsFile) 148 | { 149 | $ip = new-object System.Net.IPAddress -ArgumentList 0 150 | $trimmed = $line.Trim(); 151 | 152 | if (![string]::IsNullOrEmpty($trimmed) -and 153 | !($trimmed.StartsWith("#")) -and 154 | ($items = $regex.Split($trimmed)).Count -ge 2 -and 155 | ![string]::IsNullOrEmpty($items[1]) -and 156 | !$hostsEntry.ContainsKey($items[1]) -and 157 | [system.net.ipaddress]::TryParse($items[0], [ref] $ip)) 158 | { 159 | $hostsEntry.Add($items[1], $items[0]); 160 | } 161 | } 162 | $systemOutput | Add-Member NoteProperty -name $hostsPropertyName -value $hostsEntry -Force; 163 | } 164 | catch 165 | { 166 | $systemOutput | Add-Member NoteProperty -name $hostsPropertyName -value $null -Force; 167 | } 168 | 169 | $hotFixesPropertyName = "Hotfixes" 170 | try 171 | { 172 | $hotFixEntries = @{}; 173 | $hotFixes = Get-WmiObject Win32_QuickFixEngineering | Select HotfixId, InstalledOn; 174 | foreach ($hotFix in $hotFixes) 175 | { 176 | if (!($hotFixEntries.ContainsKey($hotFix.HotfixId))) 177 | { 178 | $hotFixEntries.Add($hotFix.HotfixId, $hotFix.InstalledOn); 179 | } 180 | } 181 | $systemOutput | Add-Member NoteProperty -name $hotFixesPropertyName -value $hotFixEntries -Force; 182 | } 183 | catch 184 | { 185 | $systemOutput | Add-Member NoteProperty -name $hotFixesPropertyName -value $null -Force; 186 | } 187 | 188 | $adfsWmiPropsPropertyName = "AdfsWmiProperties" 189 | try 190 | { 191 | $adfsWmiProperties = @{}; 192 | if ($role -eq "STS") 193 | { 194 | foreach ($adfsWmiProperty in (Get-WmiObject -namespace root/ADFS -class SecurityTokenService).Properties) 195 | { 196 | if (!($adfsWmiProperties.ContainsKey($adfsWmiProperty.Name))) 197 | { 198 | $adfsWmiProperties.Add($adfsWmiProperty.Name, $adfsWmiProperty.Value); 199 | } 200 | } 201 | } 202 | $systemOutput | Add-Member NoteProperty -name $adfsWmiPropsPropertyName -value $adfsWmiProperties -Force; 203 | } 204 | catch 205 | { 206 | $systemOutput | Add-Member NoteProperty -name $adfsWmiPropsPropertyName -value $null -Force; 207 | } 208 | 209 | $sslBindingsPropertyName = "SslBindings" 210 | try 211 | { 212 | $bindings = @(@{}); 213 | $bindingCount = -1; 214 | $bindingsStr = netsh http show sslcert 215 | 216 | #remove all title/extra lines 217 | $bindingsStr = $bindingsStr | foreach{$tok = $_.Split(":"); IF ($tok.Length -gt 1 -and $tok[1].TrimEnd() -ne "" -and $tok[0].StartsWith(" ")){$_}} 218 | 219 | foreach ($bindingLine in $bindingsStr) 220 | { 221 | if ($bindingLine.Trim().ToLower().StartsWith("ip:port")) 222 | { 223 | $bindings += @{}; 224 | $bindingCount = $bindingCount + 1; 225 | $bindings[$bindingCount].Add("IPPort", $bindingLine.Trim().Split(':')[2].Trim() + ":" + $bindingLine.Trim().Split(':')[3].Trim()); 226 | Continue; 227 | } 228 | if ($bindingLine.Trim().ToLower().StartsWith("hostname:port")) 229 | { 230 | $bindings += @{}; 231 | $bindingCount = $bindingCount + 1; 232 | $bindings[$bindingCount].Add("HostnamePort", $bindingLine.Trim().Split(':')[2].Trim() + ":" + $bindingLine.Trim().Split(':')[3].Trim()); 233 | Continue; 234 | } 235 | if ($bindingCount -ge 0) 236 | { 237 | $bindings[$bindingCount].Add($bindingLine.Trim().Split(':')[0].Trim(), $bindingLine.Trim().Split(':')[1].Trim()); 238 | } 239 | } 240 | $systemOutput | Add-Member NoteProperty -name $sslBindingsPropertyName -value $bindings -Force; 241 | } 242 | catch 243 | { 244 | $systemOutput | Add-Member NoteProperty -name $sslBindingsPropertyName -value $null -Force; 245 | } 246 | 247 | if ($role -ne "none") 248 | { 249 | $adfsServiceAcountPropertyName = "AdfssrvServiceAccount" 250 | try 251 | { 252 | $adfsServiceAccount = (Get-WmiObject win32_service | Where-Object {$_.name -eq "adfssrv"}).StartName; 253 | $systemOutput | Add-Member NoteProperty -name $adfsServiceAcountPropertyName -value $adfsServiceAccount -Force; 254 | } 255 | catch 256 | { 257 | $systemOutput | Add-Member NoteProperty -name $adfsServiceAcountPropertyName -value $null -Force; 258 | } 259 | } 260 | 261 | # No Try-catch needed for this property 262 | $systemOutput | Add-Member NoteProperty -name "Role" -value $role -Force; 263 | 264 | #get ADHealthAgent update information 265 | $agentInformationPropertyName = "AdHealthAgentInformation" 266 | try 267 | { 268 | $agentInformation = New-Object AdHealthAgentInformation 269 | 270 | $agentInformation.Version = (GetAdHealthAgentRegistryKeyValue -ValueName ([RegistryValueName]::CurrentVersion) -DefaultValue "Unknown") 271 | $agentInformation.UpdateState = (GetAdHealthAgentRegistryKeyValue -ValueName ([RegistryValueName]::UpdateState) -DefaultValue "None") 272 | $agentInformation.LastUpdateAttemptVersion = (GetAdHealthAgentRegistryKeyValue -ValueName ([RegistryValueName]::VersionOfUpdate) -DefaultValue "None") 273 | $agentInformation.NumberOfFailedAttempts = (GetAdHealthAgentRegistryKeyValue -ValueName ([RegistryValueName]::NumberOfFailedAttempts) -DefaultValue 0) 274 | # InstallerExitCode is no longer available 275 | $agentInformation.InstallerExitCode = "Unknown" 276 | 277 | $NotFound = "NotFound"; 278 | $LastUpdateAttemptTimeLong = GetAdHealthAgentRegistryKeyValue -ValueName ([RegistryValueName]::LastUpdateAttempt) -DefaultValue $NotFound 279 | if($LastUpdateAttemptTimeLong -eq $NotFound) 280 | { 281 | #use DateTime.min as LastUpdateAttempt value if it is not found in registry 282 | $agentInformation.LastUpdateAttemptTime = [dateTime]::MinValue 283 | } 284 | else 285 | { 286 | #convert from filetime to utc 287 | $LastUpdateAttemptUTC = [datetime]::FromFileTime($LastUpdateAttemptTimeLong).ToUniversalTime() 288 | $agentInformation.LastUpdateAttemptTime = $LastUpdateAttemptUTC 289 | } 290 | $systemOutput | Add-Member NoteProperty -Name $agentInformationPropertyName -Value $agentInformation -Force 291 | } 292 | catch 293 | { 294 | $systemOutput | Add-Member NoteProperty -Name $agentInformationPropertyName -Value $null -Force 295 | } 296 | 297 | return $systemOutput; 298 | } 299 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Get-AdfsVersionEx.ps1: -------------------------------------------------------------------------------- 1 | Function Get-AdfsVersionEx 2 | { 3 | [CmdletBinding()] 4 | param() 5 | 6 | $OSVersion = [Environment]::OSVersion.Version 7 | return Get-AdfsVersion($OSVersion) 8 | } 9 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Join-DiagnosticsFiles.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Merges one or more diagnostics output files into a single merged file named MergedDiagnosticsFile.json. This merged file is used with the Diagnostics Analyzer Tool on the AD FS Help website (https://adfshelp.microsoft.com/DiagnosticsAnalyzer). 4 | 5 | .DESCRIPTION 6 | The Join-DiagnosticsFiles cmdlet is used to merge one or more diagnostics output files into a single file. You can then upload this merged file to AD FS Help and view the farm health and configuration holistically. 7 | 8 | .PARAMETER FilePath 9 | The location of the output files that will be merged. The default value is the current folder. 10 | 11 | .EXAMPLE 12 | Join-DiagnosticsFiles 13 | Merge the diagnostics files located in the current folder 14 | 15 | .EXAMPLE 16 | Join-DiagnosticsFiles c:\output 17 | Merge the diagnostics files located in the c:\output folder 18 | #> 19 | Function Join-DiagnosticsFiles 20 | { 21 | [CmdletBinding()] 22 | Param 23 | ( 24 | [string] $FilePath = "." 25 | ) 26 | 27 | # Merged data file name 28 | $FileFilter = "ADFSDiagnosticsFile*.json" 29 | $OutputFileName = "\MergedDiagnosticsFile.json" 30 | 31 | # Dictionary of the objects used to create the merged test collection 32 | $mergedTestData = @{} 33 | $mergedTestData["AllTests"] = @() 34 | $mergedTestData["PassedTests"] = @() 35 | $mergedTestData["WarningTests"] = @() 36 | $mergedTestData["FailedTests"] = @() 37 | $mergedTestData["ErrorTests"] = @() 38 | $mergedTestData["NotRunTests"] = @() 39 | $mergedTestData["ReachableServers"] = @() 40 | $mergedTestData["UnreachableServers"] = @() 41 | 42 | # Objects to hold the version and ADFS Configuration information 43 | $version ="" 44 | $adfsConfiguration = "" 45 | 46 | # Read each file in the folder 47 | Write-Host "Locating diagnostics files at: $FilePath" 48 | 49 | $files = Get-ChildItem $FilePath -Filter $FileFilter 50 | 51 | # Make sure that at least one file exists 52 | if ($files.Length -eq 0) { 53 | Write-Error "There are no diagnostics files found at: '$FilePath'. Make sure the path is correct and the files are in format '$FileFilter'." 54 | return 55 | } 56 | 57 | $files | 58 | ForEach-Object { 59 | 60 | $fileName = $_ | Select-Object -ExpandProperty FullName 61 | 62 | Write-Host " - Merging file: $fileName" 63 | 64 | $adfsServerHealthData = @(Get-Content $fileName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth 65 | 66 | $mergedTestData["AllTests"] += $adfsServerHealthData.AllTests 67 | $mergedTestData["PassedTests"] += $adfsServerHealthData.PassedTests 68 | $mergedTestData["WarningTests"] += $adfsServerHealthData.WarningTests 69 | $mergedTestData["FailedTests"] += $adfsServerHealthData.FailedTests 70 | $mergedTestData["ErrorTests"] += $adfsServerHealthData.ErrorTests 71 | $mergedTestData["NotRunTests"] += $adfsServerHealthData.NotRunTests 72 | $mergedTestData["ReachableServers"] += $adfsServerHealthData.ReachableServers 73 | $mergedTestData["UnreachableServers"] += $adfsServerHealthData.UnreachableServers 74 | 75 | $configuration = @(Get-Content $fileName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Adfs-Configuration 76 | 77 | # Only add the ADFS Configuration information if this is coming from the primary node 78 | if (($configuration | Select-Object -ExpandProperty Role).ToString() -eq "1") { 79 | $adfsConfiguration = $configuration 80 | } 81 | 82 | $version = @(Get-Content $fileName -raw) | ConvertFrom-Json | Select-Object Version | Select Version 83 | } 84 | 85 | $outputPath = $FilePath + $OutputFileName 86 | Write-Host "Creating merged diagnostics file: $outputPath" 87 | 88 | $resultantData = @{} 89 | $resultantData["Test-AdfsServerHealth"] = $mergedTestData 90 | $resultantData["Adfs-Configuration"] = $adfsConfiguration 91 | 92 | $mergedOutput = @{} 93 | $mergedOutput["ADFSToolbox"] = $resultantData 94 | $mergedOutput["Version"] = $version.Version 95 | 96 | $mergedOutput | ConvertTo-JSON -depth $maxJsonDepth -Compress | Out-File $OutputPath -Encoding default 97 | } -------------------------------------------------------------------------------- /diagnosticsModule/Public/Receive-AdfsServerTrace.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Combines and sorts all the AD FS events generated given an activity ID from background jobs 4 | 5 | .DESCRIPTION 6 | The Receive-AdfsServerTrace them combines and sorts the results of each background job created with Start-AdfsServerTrace. 7 | If the jobs have not completed, the commandlet will wait until completion of all jobs. 8 | At the end, the jobs will be removed 9 | 10 | .PARAMETER Jobs 11 | Background jobs generated with the Start-AdfsServerTrace cmdlet 12 | 13 | .EXAMPLE 14 | $jobs = Start-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -ComputerName @("ADFSSRV1","ADFSSRV2") 15 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df on Servers ADFSSRV1 and ADFSSRV2 16 | 17 | On a regular basis, check how many have completed and how many are running 18 | $jobs | Get-Job -IncludeChildJob | Group-Object State 19 | 20 | At any point, receive the jobs 21 | $results = Receive-AdfsServerTrace -Jobs $jobs 22 | 23 | .NOTES 24 | The cmdlet sorts the events based on event timestamp first, then the source of the event (Audit, Admin, and Debug), and then the sequencing within the event source the event came from. 25 | #> 26 | Function Receive-AdfsServerTrace 27 | { 28 | [CmdletBinding()] 29 | param 30 | ( 31 | [Parameter(Mandatory = $true)] 32 | [array]$Jobs 33 | ) 34 | 35 | try 36 | { 37 | $activity = "Retrieving AD FS Server Trace" 38 | 39 | Write-Progress -Activity $activity -Status "Waiting for all jobs to finish" 40 | $jobs | Get-Job -IncludeChildJob | Wait-Job | Out-Null 41 | 42 | Write-Progress -Activity $activity -Status "Merging and sorting events found" 43 | $combined = @() 44 | foreach ($job in $jobs) 45 | { 46 | $result = $job | Get-Job -IncludeChildJob | Receive-Job -ErrorAction SilentlyContinue 47 | $combined = $combined + [array]$result 48 | } 49 | 50 | $combinedSorted = $combined | Sort-Object TimeCreated, SortKey, EventRecordID | Select-Object ComputerName, Source, TimeCreated, EventId, Message, Details, Details2 51 | 52 | Write-Output $combinedSorted 53 | } 54 | finally 55 | { 56 | #Clean after the jobs generated 57 | $Jobs | Get-Job | Remove-Job 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Set-ADFSDiagTestMode.ps1: -------------------------------------------------------------------------------- 1 | #for testability 2 | $testMode = $false 3 | Function Set-ADFSDiagTestMode 4 | { 5 | $testMode = $true 6 | } 7 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Start-AdfsServerTrace.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Starts background jobs to search events based on AD FS Activity ID accross different computers 4 | 5 | .DESCRIPTION 6 | The Start-AdfsServerTrace cmdlet queries all computers' event logs for the activity ID supplied in parallel as background jobs. 7 | Use the Receive-AdfsServerTrace cmdlet to retrieve and combine the results 8 | This cmdlets works in AD FS 2.0 and later. 9 | 10 | .PARAMETER activityId 11 | Activity ID to search for. This typically comes from an AD FS error page. 12 | 13 | .PARAMETER ComputerName 14 | It is an array of computers, which represents the AD FS servers to try. 15 | 16 | .EXAMPLE 17 | Start-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -ComputerName @("ADFSSRV1","ADFSSRV2") 18 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df on Servers ADFSSRV1 and ADFSSRV2 19 | 20 | .EXAMPLE 21 | Start-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -ComputerName (Get-Content .\Servers.txt) 22 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df from servers in a text file 23 | 24 | .EXAMPLE 25 | Start-AdfsServerTrace -ActivityId 00000000-0000-0000-9701-0080000000df -IncludeDebug -ComputerName @("ADFSSRV1","ADFSSRV2") 26 | Get Admin and Audits for activity ID 00000000-0000-0000-9701-0080000000df on Server ADFSSRV1 and ADFSSRV2, including debug traces 27 | 28 | .NOTES 29 | You need to run this function using an account that has permissions to read the event logs in all computers supplied. 30 | This is typically achieved having the account be part of the "Event Log Readers" Local Security Group. 31 | The computers supplied also should have firewall rules configured to allow remote readings. 32 | #> 33 | Function Start-AdfsServerTrace 34 | { 35 | [CmdletBinding()] 36 | param 37 | ( 38 | [Parameter(Mandatory = $true)] 39 | [ValidateNotNullOrEmpty()] 40 | [string] 41 | $ActivityId, 42 | 43 | [switch] 44 | $IncludeDebug, 45 | 46 | [Parameter(Mandatory = $true)] 47 | [string[]] 48 | $ComputerName 49 | ) 50 | 51 | #script block that gathers events from Debug and Admin logs 52 | $getEventWorker = { 53 | param([string]$sourceType, [string]$activityId, [string]$computerName) 54 | 55 | #common columns to return 56 | $idExpression = @{ label = 'EventId'; Expression = {$_.Id } } 57 | $timeExpression = @{ label = 'TimeCreated'; Expression = { $_.TimeCreated } } 58 | $eventRecordIDExpression = @{ label = 'EventRecordID'; Expression = {[System.Convert]::ToInt32((([xml]$_.ToXml()).Event.System.EventRecordId)) } } 59 | $messageExpression = @{ label = 'Message'; Expression = {$_.Message} } 60 | $detailsExpression = @{ label = 'Details'; Expression = {if ($_.Message -ne $_.properties[0].value) { $_.properties[0].value } else { "" } } } 61 | $details2Expression = @{ label = 'Details2'; Expression = {$_.properties[1].value } } 62 | $computerNameExpression = @{ label = 'ComputerName'; Expression = { $computerName } } 63 | $sourceExpression = @{ label = 'Source'; Expression = {$sourceType} } 64 | $activityIdExpression = @{ label = 'ActivityId'; Expression = {$_.ActivityId} } 65 | 66 | if ($sourceType -eq "Admin") 67 | { 68 | $sortKeyExpression = @{ label = 'SortKey'; Expression = { 2 } } 69 | $adfs2SourceName = "AD FS 2.0/Admin" 70 | $adfs3SourceName = "AD FS/Admin" 71 | $oldest = $false 72 | } 73 | 74 | if ($sourceType -eq "Debug") 75 | { 76 | $sortKeyExpression = @{ label = 'SortKey'; Expression = { 3 } } 77 | $adfs2SourceName = "AD FS 2.0 Tracing/Debug" 78 | $adfs3SourceName = "AD FS Tracing/Debug" 79 | $oldest = $true 80 | } 81 | 82 | [System.Guid]$activityGuid = [System.Guid]::Parse($activityId) 83 | $normalizedGuid = $activityGuid.ToString("B").ToUpper() 84 | $xpathFilter = "*[System/Correlation[@ActivityID='$normalizedGuid']]" 85 | 86 | $results = Get-WinEvent -LogName $adfs2SourceName -Oldest:$oldest -FilterXPath $xpathFilter -MaxEvents 100 -ErrorAction SilentlyContinue -ComputerName $computerName -ErrorVariable $errorVar 87 | $results = $results + [array](Get-WinEvent -LogName $adfs3SourceName -Oldest:$oldest -FilterXPath $xpathFilter -MaxEvents 100 -ErrorAction SilentlyContinue -ComputerName $computerName -ErrorVariable $errorVar) 88 | 89 | Write-Output $results | Select-Object $computerNameExpression, 90 | $sourceExpression, 91 | $sortKeyExpression, 92 | $idExpression, 93 | $timeExpression, 94 | $eventRecordIDExpression, 95 | $messageExpression, 96 | $detailsExpression, 97 | $details2Expression 98 | } 99 | 100 | #script block that gathers security audits 101 | $getAuditsWorker = { 102 | param([string]$activityId, [string]$computerName) 103 | [System.Guid]$activityGuid = [System.Guid]::Parse($activityId) 104 | $normalizedGuidForAudits = $activityGuid.ToString() 105 | $xpathFilterAudits = "*[EventData[Data='$normalizedGuidForAudits']]" 106 | 107 | $idExpression = @{ label = 'EventId'; Expression = {$_.Id } } 108 | $timeExpression = @{ label = 'TimeCreated'; Expression = { $_.TimeCreated } } 109 | $eventRecordIDExpression = @{ label = 'EventRecordID'; Expression = {[System.Convert]::ToInt32((([xml]$_.ToXml()).Event.System.EventRecordId)) } } 110 | $messageExpression = @{ label = 'Message'; Expression = {$_.Message} } 111 | $detailsExpression = @{ label = 'Details'; Expression = {if ($_.Message -ne $_.properties[0].value) { $_.properties[0].value } else { "" } } } 112 | $details2Expression = @{ label = 'Details2'; Expression = {$_.properties[1].value } } 113 | $computerNameExpression = @{ label = 'ComputerName'; Expression = { $computerName } } 114 | $sourceAuditExpression = @{ label = 'Source'; Expression = {"Audits"} } 115 | $sortKeyExpression = @{ label = 'SortKey'; Expression = { 1 } } 116 | 117 | $auditTraces = Get-WinEvent -LogName "Security" -Oldest -FilterXPath $xpathFilterAudits -ErrorAction SilentlyContinue -ComputerName $computerName 118 | 119 | $results = $auditTraces 120 | 121 | #audits also have instance ID. To harvest those, let's find the data1 fields that are like a guid and are not the activity id 122 | $instanceIds = $auditTraces | where { $_.Details -match "[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}" -and $_.Details -ne $normalizedGuidForAudits } | Select-Object -ExpandProperty Details -Unique 123 | foreach ($instanceId in $instanceIds) 124 | { 125 | $xpathFilterAuditsByInstId = "*[EventData[Data='$instanceId']]" 126 | $results = $results + [array](Get-WinEvent -LogName "Security" -Oldest -FilterXPath $xpathFilterAuditsByInstId -ErrorAction SilentlyContinue -ComputerName $computerName) 127 | } 128 | 129 | Write-Output $results | Select-Object $computerNameExpression, 130 | $sourceAuditExpression, 131 | $sortKeyExpression, 132 | $idExpression, 133 | $timeExpression, 134 | $eventRecordIDExpression, 135 | $messageExpression, 136 | $detailsExpression, 137 | $details2Expression 138 | } 139 | $jobs = @() 140 | 141 | $activity = "Getting AD FS request details for ActivityId=$activityId" 142 | 143 | Write-Progress -Activity $activity -Status "Querying event logs in parallel" 144 | foreach ($server in $computerName) 145 | { 146 | $jobs = [array]$jobs + (Start-Job -Name $server+"-Admin" -ScriptBlock $getEventWorker -ArgumentList @("Admin", $activityId, $server)) 147 | 148 | if ($includeDebug) 149 | { 150 | $jobs = [array]$jobs + (Start-Job -Name $server+"-Trace" -ScriptBlock $getEventWorker -ArgumentList @("Debug", $activityId, $server)) 151 | } 152 | 153 | $jobs = [array]$jobs + [array](Start-Job -Name $server+"-Audit" -ScriptBlock $getAuditsWorker -ArgumentList @($activityId, $server)) 154 | } 155 | 156 | Write-Output $jobs 157 | } 158 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Test-AdfsServerHealth.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Performs applicable health checks on the AD FS server (Proxy or STS) 4 | 5 | .DESCRIPTION 6 | The health checks generated by the Test-AdfsServerHealth cmdlet return a container of results with the following properties: 7 | - Name : Mnemonic identifier for the test 8 | - ComputerName: The name of the computer the test was run on 9 | - Result : One value of 'Pass','Fail','NotRun','Error','Warning' 10 | - Detail : Explanation of the 'Fail' and 'NotRun' result. It is typically empty when the check passes. 11 | - Output : Data collected for the specific test. It is a list of Key value pairs 12 | - ExceptionMessage: If the test encountered an exception, this property contains the exception message. 13 | - Exception: If the test encountered an exception, this property contains the exception. 14 | 15 | .PARAMETER VerifyO365 16 | Boolean parameter that will enable Office 365 targeted checks. It is true by default. 17 | 18 | .PARAMETER VerifyTrustCerts 19 | Boolean parameter that will enable additional checks for relying party trust and claims provider trust certificates. It is false by default. 20 | 21 | .PARAMETER SslThumbprint 22 | String parameter that corresponds to the thumbprint of the AD FS SSL certificate. This is required for running test cases on proxy servers. 23 | 24 | .PARAMETER AdfsServers 25 | Array of fully qualified domain names (FQDN) of all of the AD FS STS servers that you want to run health checks on. For Windows Server 2016 this is automatically populated using Get-AdfsFarmInformation. 26 | By default the tests are already run on the local machine, so it is not necessary include the FQDN of the current machine in this parameter. 27 | 28 | .PARAMETER Local 29 | Switch that indicates that you only want to run the health checks on the local machine. This takes precedence over -AdfsServers parameter. 30 | 31 | .EXAMPLE 32 | Test-AdfsServerHealth | Where-Object {$_.Result -ne "Pass"} 33 | Execute test suite and get only the tests that did not pass 34 | 35 | .EXAMPLE 36 | Test-AdfsServerHealth -verifyOffice365:$false 37 | Execute test suite in an AD FS farm where Office 365 is not configured 38 | 39 | .EXAMPLE 40 | Test-AdfsServerHealth -verifyTrustCerts:$true 41 | Execute test suite in an AD FS farm and examine the relying party trust and claims provider trust certificates 42 | 43 | .EXAMPLE 44 | Test-AdfsServerHealth -adfsServers @("sts1.contoso.com", "sts2.contoso.com", "sts3.contoso.com") 45 | Execute test suite in an AD FS farm and run the test on the following servers: ADFS1.contoso.com, ADFS2.contoso.com, ADFS3.contoso.com. This automatically runs the test on the local machine as well. 46 | 47 | .EXAMPLE 48 | Test-AdfsServerHealth -sslThumbprint ‎c1994504c91dfef663b5ce8dd22d1a44748a6e16 49 | Execute test suite on a WAP server and utilize the provided thumbprint to check SSL bindings. 50 | 51 | .NOTES 52 | Most of the checks require executing AD FS cmdlets. As a result: 53 | 1. The most comprehensive analysis occurs when running from the Primary Computer in a Windows Internal Database farm. 54 | 2. For secondary computers in a Windows Internal Database farm, the majority of checks will be marked as "NotRun" 55 | 3. For a SQL Server farm, all applicable tests will run succesfully. 56 | 4. If the AD FS service is stopped, the majority of checks will be returned as 'NotRun' 57 | #> 58 | Function Test-AdfsServerHealth() 59 | { 60 | [CmdletBinding(DefaultParameterSetName='AdfsServerLocal')] 61 | Param 62 | ( 63 | [Parameter(ParameterSetName='AdfsServerLocal')] 64 | [Parameter(ParameterSetName='AdfsServerRemote')] 65 | $verifyO365 = $true, 66 | [Parameter(ParameterSetName='AdfsServerLocal')] 67 | [Parameter(ParameterSetName='AdfsServerRemote')] 68 | $verifyTrustCerts = $false, 69 | [Parameter(ParameterSetName='ProxyServer')] 70 | [string] 71 | $sslThumbprint = $null, 72 | [Parameter(ParameterSetName='AdfsServerRemote')] 73 | [string[]] 74 | $adfsServers = $null, 75 | [Parameter(ParameterSetName='AdfsServerLocal')] 76 | [switch] 77 | $local = $false 78 | ) 79 | 80 | switch (Get-ADFSRole) 81 | { 82 | $adfsRoleSTS 83 | { 84 | Write-Host "Performing applicable health checks on your AD FS server." 85 | return TryTestAdfsSTSHealthOnFarmNodes -verifyO365 $verifyO365 -verifyTrustCerts $verifyTrustCerts -adfsServers $adfsServers -local:$local; 86 | } 87 | $adfsRoleProxy 88 | { 89 | Write-Host "Performing applicable health checks on your WAP server." 90 | return TestAdfsProxyHealth -sslThumbprint $sslThumbprint; 91 | } 92 | default 93 | { 94 | throw "Error: Unable to determine server role. This script should only be run on AD FS servers (Proxy or STS)"; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Test-AdfsServerHealthSingleCheck.ps1: -------------------------------------------------------------------------------- 1 | Function Test-AdfsServerHealthSingleCheck 2 | { 3 | [CmdletBinding()] 4 | param 5 | ( 6 | [ValidateNotNullOrEmpty()] 7 | [string] 8 | $testFunctionName 9 | ) 10 | 11 | Import-ADFSAdminModule 12 | $props = Retrieve-AdfsProperties -force; 13 | return Invoke-TestFunctions -Role "Tests" -functionsToRun @($testFunctionName) 14 | } 15 | -------------------------------------------------------------------------------- /diagnosticsModule/Public/Test-AdfsServerToken.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Performs a synthetic transaction to get a token against an AD FS farm 4 | 5 | .DESCRIPTION 6 | If a credential is provided, then the 2005/usernamemixed Endpoint will be used to get the token. 7 | Otherwise, the 2005/windowstransport endpoint will be used with the windows identity of the logged on user. 8 | The token is returned in XML format. By default this cmdlet will perform three transactions using Tls 1.0, Tls 1.1, and Tls 1.2. 9 | 10 | .PARAMETER FederationServer 11 | Federation Server (Farm) host name 12 | 13 | .PARAMETER AppliesTo 14 | Identifier of the target relying party 15 | 16 | .PARAMETER Credential 17 | Optional Username Credential used to retrieve the token 18 | 19 | .PARAMETER TestTls10 20 | Optional switch to specify performing a synthetic transaction using Tls 1.0 21 | 22 | .PARAMETER TestTls11 23 | Optional switch to specify performing a synthetic transaction using Tls 1.1 24 | 25 | .PARAMETER TestTls12 26 | Optional switch to specify performing a synthetic transaction using Tls 1.2 27 | 28 | .EXAMPLE 29 | Test-AdfsServerToken -FederationServer sts.contoso.com -AppliesTo urn:payrollapp 30 | Retrieves a token for the relying party with identifier urn:payrollapp against the farm 'sts.contoso.com' with logged on user windows credentials 31 | 32 | .EXAMPLE 33 | Test-AdfsServerToken -FederationServer sts.contoso.com -AppliesTo urn:payrollapp -Credential (Get-Credential) 34 | Retrieves a token for the relying party with identifier urn:payrollapp against the farm 'sts.contoso.com' using a UserName/Password credential 35 | 36 | .EXAMPLE 37 | $tokenString = Test-AdfsServerToken -FederationServer sts.contoso.com -AppliesTo urn:payrollapp 38 | $tokenXml = [Xml]$tokenString 39 | $tokenXml.Envelope.Body.RequestSecurityTokenResponse.RequestedSecurityToken.Assertion.AttributeStatement.Attribute | ft 40 | 41 | Retrieves a token, and see the claims in the attribute statement in a table format 42 | 43 | .EXAMPLE 44 | Test-AdfsServerToken -FederationServer sts.contoso.com -AppliesTo urn:payrollapp -TestTls10 -TestTls11 45 | Perform two synthetic transactions using Tls 1.0 and Tls 1.1 for the relying party with identifier urn:payrollapp against the farm 'sts.contoso.com' with logged on user windows credentials. 46 | 47 | .NOTES 48 | If credential parameter is provided, then the 2005/usernamemixed Endpoint needs to be enabled 49 | Otherwise, the 2005/windowstransport endpoint needs to be enabled 50 | 51 | #> 52 | 53 | Function Test-AdfsServerToken 54 | { 55 | param 56 | ( 57 | [ValidateNotNullOrEmpty()] 58 | [string] 59 | $FederationServer, 60 | 61 | [ValidateNotNullOrEmpty()] 62 | [string] 63 | $AppliesTo, 64 | 65 | [Parameter(Mandatory = $false)] 66 | $Credential, 67 | 68 | [Switch] 69 | $TestTls10, 70 | 71 | [Switch] 72 | $TestTls11, 73 | 74 | [Switch] 75 | $TestTls12 76 | ) 77 | 78 | $rst = $null 79 | $endpoint = $null 80 | [Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} 81 | 82 | if ($credential -ne $null) 83 | { 84 | $endpoint = "https://" + $federationServer + "/adfs/services/trust/2005/usernamemixed" 85 | $username = $credential.UserName 86 | $password = $credential.GetNetworkCredential().Password 87 | $rst = [String]::Format( 88 | 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issuehttp://www.w3.org/2005/08/addressing/anonymous{0}{1}{2}{3}0http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKeyhttp://schemas.xmlsoap.org/ws/2005/02/trust/Issuehttp://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0', ` 89 | $endpoint, 90 | $username, 91 | $password, 92 | $appliesTo) 93 | } 94 | else 95 | { 96 | $endpoint = "https://" + $federationServer + "/adfs/services/trust/2005/windowstransport" 97 | $rst = [String]::Format( 98 | 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issuehttp://www.w3.org/2005/08/addressing/anonymous{0}{1}0http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKeyhttp://schemas.xmlsoap.org/ws/2005/02/trust/Issuehttp://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0', ` 99 | $endpoint, 100 | $appliesTo) 101 | } 102 | 103 | $oldProtocol = [Net.ServicePointManager]::SecurityProtocol 104 | $protocolsToTest = @() 105 | 106 | if (!($TestTls10) -and !($TestTls11) -and !($TestTls12)) 107 | { 108 | $protocolsToTest = @($Tls10) 109 | } 110 | 111 | if ($TestTls10) 112 | { 113 | $protocolsToTest += $Tls10 114 | } 115 | 116 | if ($TestTls11) 117 | { 118 | $protocolsToTest += $Tls11 119 | } 120 | 121 | if ($TestTls12) 122 | { 123 | $protocolsToTest += $Tls12 124 | } 125 | 126 | $protocolsToTest | ForEach-Object { 127 | [Net.ServicePointManager]::SecurityProtocol = $_ 128 | $webresp = Invoke-WebRequest $endpoint -Method Post -Body $rst -ContentType "application/soap+xml" -UseDefaultCredentials -UseBasicParsing 129 | } 130 | 131 | [Net.ServicePointManager]::SecurityProtocol = $oldProtocol 132 | $tokenXml = [xml]$webresp.Content 133 | 134 | [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null; 135 | 136 | return $tokenXml.OuterXml 137 | } 138 | -------------------------------------------------------------------------------- /diagnosticsModule/Test/ADFSDiagnostics.Test.ps1: -------------------------------------------------------------------------------- 1 | # Determine our script root 2 | $root = Split-Path $PSScriptRoot -Parent 3 | 4 | # Load module via definition 5 | Import-Module $root\ADFSDiagnosticsModule.psm1 -Force 6 | 7 | InModuleScope ADFSDiagnosticsModule { 8 | Describe 'Load ADFSDiagnostics' { 9 | It 'should load ADFSDiagnostics module' { 10 | $ADFSDiagnosticsModule = Get-Module ADFSDiagnosticsModule -all 11 | 12 | $ADFSDiagnosticsModule | should be $true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /diagnosticsModule/Test/Private/AdfsProxyHealthChecks.Test.ps1: -------------------------------------------------------------------------------- 1 | # Determine our script root 2 | $parent = Split-Path $PSScriptRoot -Parent 3 | $root = Split-Path $parent -Parent 4 | # Load module via definition 5 | Import-Module $root\ADFSDiagnosticsModule.psm1 -Force 6 | 7 | InModuleScope ADFSDiagnosticsModule { 8 | # Shared constants 9 | $sharedError = "Error message" 10 | $sharedErrorException = "System.Management.Automation.RuntimeException: Error message" 11 | 12 | Describe "TestIsAdfsProxyRunning" { 13 | It "should pass" { 14 | # Arrange 15 | Mock -CommandName Get-ServiceState -MockWith { return "Running" } 16 | 17 | # Act 18 | $ret = TestIsAdfsProxyRunning 19 | 20 | # Assert 21 | $ret.Result | should beexactly Pass 22 | $ret.Output.ADFSProxyServiceState | should beexactly "Running" 23 | } 24 | 25 | It "should fail" { 26 | # Arrange 27 | Mock -CommandName Get-ServiceState -MockWith { return "Stopped" } 28 | 29 | # Act 30 | $ret = TestIsAdfsProxyRunning 31 | 32 | # Assert 33 | $ret.Result | should beexactly Fail 34 | $ret.Output.ADFSProxyServiceState | should beexactly "Stopped" 35 | } 36 | 37 | It "should error" { 38 | # Arrange 39 | Mock -CommandName Get-ServiceState -MockWith { throw $sharedError } 40 | 41 | # Act 42 | $ret = TestIsAdfsProxyRunning 43 | 44 | # Assert 45 | $ret.Result | should beexactly Error 46 | $ret.ExceptionMessage | should beexactly $sharedError 47 | $ret.Exception | should beexactly $sharedErrorException 48 | } 49 | } 50 | 51 | Describe "TestNonSelfSignedCertificatesInRootStore" { 52 | It "should pass" { 53 | # Arrange 54 | $subject = "CN=TestContosoCert" 55 | $store = "Cert:\CurrentUser\My" 56 | $cert = New-SelfSignedCertificate -Subject $subject -CertStoreLocation $store 57 | 58 | Mock -CommandName Get-ChildItem -MockWith { return @($cert, $cert) } 59 | 60 | # Act 61 | $ret = TestNonSelfSignedCertificatesInRootStore 62 | 63 | $cert | Remove-Item 64 | 65 | # Assert 66 | $ret.Result | should beexactly Pass 67 | } 68 | 69 | Function CreateMockX509Certificate($issuer, $subject, $friendlyName, $thumbprint) 70 | { 71 | return New-Object -TypeName PSObject -Property @{ 72 | "Issuer" = $issuer 73 | "Subject" = $subject 74 | "FriendlyName" = $friendlyName 75 | "Thumbprint" = $thumbprint 76 | } 77 | } 78 | 79 | It "should fail" { 80 | # Arrange 81 | $subject = "CN=TestContosoCert" 82 | $friendlyName = "Contoso Cert" 83 | $issuer = "CN=TrustedAuthority" 84 | $secondIssuer = "CN=TrustedAuthority2" 85 | $firstThumbprint = "a909502dd82ae41433e6f83886b00d4277a32a7b" 86 | $secondThumbprint = "32aa840238fba67210b0d779e84923d65403eda8" 87 | $thirdThumbprint = "01a834a8b2289263d50ade7f3a700438b794e2d6" 88 | 89 | # Since it is difficult to create an X509 certificate that is not self-signed via PowerShell we will just mock it up using a PSObject 90 | $nonSelfSignedCert = CreateMockX509Certificate $issuer $subject $friendlyName $firstThumbprint 91 | $secondNonSelfSignedCert = CreateMockX509Certificate $secondIssuer $subject $friendlyName $secondThumbprint 92 | $selfSignedCert = CreateMockX509Certificate $issuer $issuer $friendlyName $thirdThumbprint 93 | 94 | Mock -CommandName Get-ChildItem -MockWith { return @($nonSelfSignedCert, $selfSignedCert, $selfSignedCert, $secondNonSelfSignedCert) } 95 | # Act 96 | $ret = TestNonSelfSignedCertificatesInRootStore 97 | 98 | # Assert 99 | $ret.Result | should beexactly Fail 100 | $ret.Detail | should beexactly "There were non-self-signed certificates found in the root store. Move them to the intermediate store." 101 | 102 | $ret.Output.NonSelfSignedCertificates.Count | should beexactly 2 103 | 104 | $ret.Output.NonSelfSignedCertificates[0].Subject | should beexactly $subject 105 | $ret.Output.NonSelfSignedCertificates[0].Issuer | should beexactly $issuer 106 | $ret.Output.NonSelfSignedCertificates[0].FriendlyName | should beexactly $friendlyName 107 | $ret.Output.NonSelfSignedCertificates[0].Thumbprint | should beexactly $firstThumbprint 108 | 109 | $ret.Output.NonSelfSignedCertificates[1].Subject | should beexactly $subject 110 | $ret.Output.NonSelfSignedCertificates[1].Issuer | should beexactly $secondIssuer 111 | $ret.Output.NonSelfSignedCertificates[1].FriendlyName | should beexactly $friendlyName 112 | $ret.Output.NonSelfSignedCertificates[1].Thumbprint | should beexactly $secondThumbprint 113 | } 114 | 115 | It "should error" { 116 | # Arrange 117 | Mock -CommandName Get-ChildItem -MockWith { throw $sharedError } 118 | 119 | # Act 120 | $ret = TestNonSelfSignedCertificatesInRootStore 121 | 122 | # Assert 123 | $ret.Result | should beexactly Error 124 | $ret.ExceptionMessage | should beexactly $sharedError 125 | $ret.Exception | should beexactly $sharedErrorException 126 | } 127 | } 128 | 129 | Describe "TestProxySslBindings" { 130 | BeforeAll { 131 | $_hostname = "sts.contoso.com" 132 | $_hostHttpsPort = 443 133 | $_hostTlsClientPort = 49443 134 | $_adfsCertificateThumbPrint = "a909502dd82ae41433e6f83886b00d4277a32a7b" 135 | 136 | Mock -CommandName Get-WmiObject -MockWith { 137 | return New-Object -TypeName PSObject -Property @{ 138 | "HostName" = $_hostname 139 | "HostHttpsPort" = $_hostHttpsPort 140 | "TlsClientPort" = $_hostTlsClientPort 141 | } 142 | } 143 | } 144 | 145 | It "should pass" { 146 | # Arrange 147 | Mock -CommandName GetSslBindings -MockWith { 148 | return @{ 149 | "CustomBinding" = @{"Application ID" = $adfsApplicationId} 150 | } 151 | } 152 | Mock -CommandName IsSslBindingValid -MockWith { 153 | return @{ "IsValid" = $true } 154 | } 155 | 156 | # Act 157 | $ret = TestProxySslBindings $_adfsCertificateThumbPrint 158 | 159 | # Assert 160 | $ret.Result | should beexactly Pass 161 | } 162 | 163 | It "should fail" { 164 | # Arrange 165 | Mock -CommandName GetSslBindings -MockWith { 166 | return @{ 167 | "CustomBinding" = @{"Application ID" = $adfsApplicationId} 168 | } 169 | } 170 | Mock -CommandName IsSslBindingValid -MockWith { 171 | return @{ "IsValid" = $false; "Detail" = $sharedError } 172 | } 173 | 174 | # Act 175 | $ret = TestProxySslBindings $_adfsCertificateThumbPrint 176 | 177 | # Assert 178 | $ret.Result | should beexactly Fail 179 | (($_hostname + ":" + $_hostHttpsPort), ($_hostname + ":" + $_hostTlsClientPort), "CustomBinding") | ForEach-Object { 180 | $ret.Output.ErroneousBindings[$_] | should beexactly $sharedError 181 | } 182 | } 183 | 184 | It "should error" { 185 | # Arrange 186 | Mock -CommandName GetSslBindings -MockWith { throw $sharedError } 187 | 188 | # Act 189 | $ret = TestProxySslBindings $_adfsCertificateThumbPrint 190 | 191 | # Assert 192 | $ret.Result | should beexactly Error 193 | $ret.ExceptionMessage | should beexactly $sharedError 194 | $ret.Exception | should beexactly $sharedErrorException 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /diagnosticsModule/Test/Private/CommonHealthChecks.Test.ps1: -------------------------------------------------------------------------------- 1 | # Determine our script root 2 | $parent = Split-Path $PSScriptRoot -Parent 3 | $script:root = Split-Path $parent -Parent 4 | # Load module via definition 5 | Import-Module $script:root\ADFSDiagnosticsModule.psm1 -Force 6 | 7 | InModuleScope ADFSDiagnosticsModule { 8 | # Shared constants 9 | $sharedError = "Error message" 10 | $sharedErrorException = "System.Management.Automation.RuntimeException: Error message" 11 | 12 | Describe "TestTLSMismatch" { 13 | It "should pass because all TLS versions are enabled" { 14 | # Arrange 15 | Mock -CommandName IsTlsVersionEnabled -MockWith { return $true } 16 | 17 | # Act 18 | $ret = TestTlsMismatch 19 | 20 | # Assert 21 | $ret.Result | should be Pass 22 | } 23 | 24 | Context "Specific TLS versions" { 25 | ("1.0", "1.1", "1.2") | ForEach-Object { 26 | It "should warn because only TLS $_ is enabled" { 27 | # Arrange 28 | $tlsVersion = $_ 29 | Mock -CommandName IsTlsVersionEnabled -MockWith { return $false } -ParameterFilter { $version -ne $tlsVersion } 30 | Mock -CommandName IsTlsVersionEnabled -MockWith { return $true } -ParameterFilter { $version -eq $tlsVersion } 31 | 32 | # We mock the warning function to avoid writing to console 33 | Mock -CommandName Out-Warning -MockWith { } 34 | 35 | # Act 36 | $ret = TestTLSMismatch 37 | 38 | # Assert 39 | $ret.Result | should be Warning 40 | $ret.Detail | should be "Detected that only TLS $tlsVersion is enabled. Ensure that this is also enabled on your other STS and Proxy servers." 41 | 42 | Assert-MockCalled Out-Warning 43 | } 44 | } 45 | } 46 | 47 | It "should fail because all TLS versions are disabled" { 48 | # Arrange 49 | Mock -CommandName IsTlsVersionEnabled -MockWith { return $false } 50 | 51 | # Act 52 | $ret = TestTlsMismatch 53 | 54 | # Assert 55 | $ret.Result | should be Fail 56 | $ret.Detail | should be "Detected that all TLS versions are disabled. This will cause problems between your STS and Proxy servers. Fix this by enabling the correct TLS version." 57 | } 58 | 59 | It "should error" { 60 | # Arrange 61 | Mock -CommandName IsTlsVersionEnabled -MockWith { throw $sharedError } 62 | 63 | # Act 64 | $ret = TestTLSMismatch 65 | 66 | # Assert 67 | $ret.Result | should be Error 68 | $ret.ExceptionMessage | should be $sharedError 69 | $ret.Exception | should be $sharedErrorException 70 | } 71 | } 72 | 73 | Describe "TestAdfsEventLogs" { 74 | ($adfs2x, $adfs3) | ForEach-Object { 75 | Context "AD FS version $_" { 76 | $adfsVersionToTest = $_ 77 | Mock -CommandName Get-AdfsVersion -MockWith { return $adfsVersionToTest } 78 | 79 | ($adfsRoleSTS, $adfsRoleProxy) | ForEach-Object { 80 | Context "server role $_" { 81 | $adfsRoleToTest = $_ 82 | Mock -CommandName Get-AdfsRole -MockWith { return "$adfsRoleToTest" } 83 | 84 | It "should pass because Get-WinEvent returned null" { 85 | # Arrange 86 | Mock -CommandName Get-WinEvent -MockWith { return $null } 87 | 88 | # Act 89 | $ret = TestAdfsEventLogs 90 | 91 | # Assert 92 | $ret.Result | should beexactly Pass 93 | } 94 | 95 | It "should pass because Get-WinEvent returned an empty array" { 96 | # Arrange 97 | Mock -CommandName Get-WinEvent -MockWith { return @() } 98 | 99 | # Act 100 | $ret = TestAdfsEventLogs 101 | 102 | # Assert 103 | $ret.Result | should beexactly Pass 104 | } 105 | 106 | It "should fail" { 107 | # Arrange 108 | Mock -CommandName Get-WinEvent -MockWith { 109 | return @(New-Object -TypeName PSObject -Property @{ 110 | "TimeCreated" = (Get-Date) 111 | "Id" = 270 112 | "LevelDisplayName" = "Error" 113 | "Message" = "The federation server proxy was not able to authenticate to the Federation Service." 114 | }) 115 | } 116 | 117 | # Act 118 | $ret = TestAdfsEventLogs 119 | 120 | # Assert 121 | $ret.Result | should beexactly Fail 122 | $ret.Detail | should beexactly "There were events found in the AD FS event logs that may be causing issues with the AD FS and WAP trust. Check the output for more details." 123 | 124 | $ret.Output.Events.Id | should beexactly 270 125 | $ret.Output.Events.LevelDisplayName | should beexactly "Error" 126 | $ret.Output.Events.Message | should beexactly "The federation server proxy was not able to authenticate to the Federation Service." 127 | } 128 | } 129 | } 130 | 131 | It "should error because of invalid server role." { 132 | # Arrange 133 | Mock -CommandName Get-AdfsRole -MockWith { return "none" } 134 | 135 | # Act 136 | $ret = TestAdfsEventLogs 137 | 138 | # Assert 139 | $ret.Result | should beexactly Error 140 | $ret.Exceptionmessage | should beexactly "Unable to determine server role." 141 | $ret.Exception | should beexactly "System.Management.Automation.RuntimeException: Unable to determine server role." 142 | } 143 | } 144 | } 145 | 146 | It "should error because invalid AD FS Version" { 147 | # Arrange 148 | Mock -CommandName Get-AdfsVersion -MockWith { return $null } 149 | 150 | # Act 151 | $ret = TestAdfsEventLogs 152 | 153 | # Assert 154 | $ret.Result | should beexactly Error 155 | $ret.ExceptionMessage | should beexactly "Unable to determine AD FS version." 156 | $ret.Exception | should beexactly "System.Management.Automation.RuntimeException: Unable to determine AD FS version." 157 | } 158 | } 159 | 160 | Describe "TestTimeSync" { 161 | Mock -CommandName Test-RunningRemotely -MockWith { return $false } 162 | 163 | Context "server role STS" { 164 | Mock -CommandName Get-AdfsRole -MockWith { return $adfsRoleSTS } 165 | 166 | Mock -CommandName Out-Warning -MockWith { } 167 | 168 | Context "only run locally" { 169 | It "should pass" { 170 | # Arrange 171 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $true } 172 | 173 | # Act 174 | $ret = TestTimeSync 175 | 176 | # Assert 177 | $ret.Result | should beexactly Pass 178 | } 179 | 180 | It "should fail" { 181 | # Arrange 182 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $false } 183 | 184 | # Act 185 | $ret = TestTimeSync 186 | 187 | # Assert 188 | $ret.Result | should beexactly Fail 189 | $ret.Detail | should beexactly "This server's time is out of sync with reliable time server. Check and correct any time synchronization issues." 190 | } 191 | } 192 | 193 | Context "run farm-wide" { 194 | BeforeAll { 195 | $adfsServers = @("sts1.contoso.com", "sts2.contoso.com", "sts3.contoso.com") 196 | 197 | # since we have to mock out the remote PSSessions that gets created we just return the a PSSession to localhost 198 | # we create these session before the actual test because once we mock New-PSSession we cannot unmock it 199 | $localPSForPassTest = @() 200 | $localPSForFailTest = @() 201 | 202 | for ($i = 0; $i -lt $adfsServers.Count; $i++) 203 | { 204 | $localPSForPassTest += New-PSSession -ComputerName localhost -ErrorAction Stop 205 | $localPSForFailTest += New-PSSession -ComputerName localhost -ErrorAction Stop 206 | } 207 | } 208 | 209 | It "should pass" { 210 | # Arrange 211 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $true } 212 | $script:itr = 0 213 | Mock -CommandName New-PSSession -MockWith { 214 | $session = $localPSForPassTest[$itr] 215 | $script:itr += 1 216 | return $session 217 | } 218 | 219 | # Since we get all of the functions from the private folder and run Invoke-Expression on them; that replaces the function's mock with the original function. 220 | # We avoid this by setting the invoke expression within this script block to do nothing. 221 | Mock Invoke-Command { 222 | Return $ScriptBlock.InvokeWithContext(@{"Invoke-Expression" = {}}, @()) 223 | } 224 | 225 | # Act 226 | $ret = TestTimeSync -adfsServers $adfsServers 227 | 228 | # Assert 229 | $ret.Result | should beexactly Pass 230 | } 231 | 232 | It "should warn because it is unable to connect to remote server" { 233 | # Arrange 234 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $true } 235 | Mock -CommandName New-PSSession -MockWith { return $null } 236 | 237 | # Act 238 | TestTimeSync -adfsServers $adfsServers 239 | 240 | # Assert 241 | Assert-MockCalled -CommandName Out-Warning -Times 3 242 | } 243 | 244 | It "should fail" { 245 | # # Arrange 246 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $false } 247 | $script:itr = 0 248 | Mock -CommandName New-PSSession -MockWith { 249 | $session = $localPSForFailTest[$itr] 250 | $script:itr += 1 251 | return $session 252 | } 253 | 254 | # Since we get all of the functions from the private folder and run Invoke-Expression on them this replaces the function's mock with the original function. 255 | # We avoid this by setting the invoke expression within this script block to do nothing. 256 | Mock Invoke-Command { 257 | Return $ScriptBlock.InvokeWithContext(@{"Invoke-Expression" = {}}, @()) 258 | } 259 | 260 | # # Act 261 | $ret = TestTimeSync -adfsServers $adfsServers 262 | 263 | # Assert 264 | $ret.Result | should beexactly Fail 265 | ($adfsServers + "Localhost") | ForEach-Object { 266 | $ret.Output.ServersOutOfSync | should contain $_ 267 | } 268 | } 269 | } 270 | } 271 | 272 | Context "server role Proxy" { 273 | Mock -CommandName Get-AdfsRole -MockWith { 274 | return $adfsRoleProxy 275 | } 276 | 277 | It "should pass" { 278 | # Arrange 279 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $true } 280 | 281 | # Act 282 | $ret = TestTimeSync 283 | 284 | # Assert 285 | $ret.Result | should beexactly Pass 286 | } 287 | 288 | It "should fail" { 289 | # Arrange 290 | Mock -CommandName IsServerTimeInSyncWithReliableTimeServer -MockWith { return $false } 291 | 292 | # Act 293 | $ret = TestTimeSync 294 | 295 | # Assert 296 | $ret.Result | should beexactly Fail 297 | $ret.Detail | should beexactly "This server's time is out of sync with reliable time server. Check and correct any time synchronization issues." 298 | } 299 | } 300 | 301 | It "should not run" { 302 | # Arrange 303 | Mock -CommandName Test-RunningRemotely -MockWith { return $true } 304 | 305 | # Act 306 | $ret = TestTimeSync 307 | 308 | # Assert 309 | $ret.Result | should beexactly NotRun 310 | $ret.Detail | should beexactly "This test does not need to run remotely." 311 | } 312 | 313 | It "should error" { 314 | # Arrange 315 | Mock -CommandName Get-AdfsRole -MockWith { return "none" } 316 | Mock -CommandName Test-RunningRemotely -MockWith { return $false } 317 | 318 | # Act 319 | $ret = TestTimeSync 320 | 321 | # Assert 322 | $ret.Result | should beexactly Error 323 | $ret.ExceptionMessage | should beexactly "Unable to determine server role." 324 | $ret.Exception | should beexactly "System.Management.Automation.RuntimeException: Unable to determine server role." 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /diagnosticsModule/Test/Private/Data/Diagnostics/ADFSDiagnosticsFile-20190115134926.json: -------------------------------------------------------------------------------- 1 | {"ADFSToolbox":{"Test-AdfsServerHealth":{"AllTests":[{"Name":"IsAdfsRunning","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":{"ADFSServiceState":"Running"},"ExceptionMessage":null,"Exception":null},{"Name":"TestIsAdfsProxyRunning","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":{"ADFSProxyServiceState":4},"ExceptionMessage":null,"Exception":null},{"Name":"TestSTSReachableFromProxy","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":{"TestSTSReachableFromProxyException":"NONE"},"ExceptionMessage":null,"Exception":null},{"Name":"TestTLSMismatch","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null},{"Name":"TestTimeSync","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null},{"Name":"TestNonSelfSignedCertificatesInRootStore","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null},{"Name":"TestSelfSignedCertificatesInIntermediateCaStore","ComputerName":"aadtestdoms2.aadtestdom.com","Result":2,"Detail":"There were self-signed certificates found in the intermediate CA store. Move them to the root certificate store.","Output":{"SelfSignedCertificates":[{"FriendlyName":"","Issuer":"CN=Root Agency","Subject":"CN=Root Agency","Thumbprint":"FEE449EE0E3965A5246F000E87FDE2A065FD89D4"},{"FriendlyName":"","Issuer":"CN=aadtestdom-AADTESTDOMDC-CA, DC=aadtestdom, DC=com","Subject":"CN=aadtestdom-AADTESTDOMDC-CA, DC=aadtestdom, DC=com","Thumbprint":"E2D0EF85C434E5D09D66120BE290E0F81CA23F98"}]},"ExceptionMessage":null,"Exception":null},{"Name":"TestProxySslBindings","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null}],"ReachableServers":["aadtestdoms2.aadtestdom.com"],"UnreachableServers":["aadtestdoms3.aadtestdom.com"],"PassedTests":[{"Name":"IsAdfsRunning","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":{"ADFSServiceState":"Running"},"ExceptionMessage":null,"Exception":null},{"Name":"TestIsAdfsProxyRunning","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":{"ADFSProxyServiceState":4},"ExceptionMessage":null,"Exception":null},{"Name":"TestSTSReachableFromProxy","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":{"TestSTSReachableFromProxyException":"NONE"},"ExceptionMessage":null,"Exception":null},{"Name":"TestTLSMismatch","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null},{"Name":"TestTimeSync","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null},{"Name":"TestNonSelfSignedCertificatesInRootStore","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null},{"Name":"TestProxySslBindings","ComputerName":"aadtestdoms2.aadtestdom.com","Result":0,"Detail":null,"Output":null,"ExceptionMessage":null,"Exception":null}],"WarningTests":[],"FailedTests":[{"Name":"TestSelfSignedCertificatesInIntermediateCaStore","ComputerName":"aadtestdoms2.aadtestdom.com","Result":2,"Detail":"There were self-signed certificates found in the intermediate CA store. Move them to the root certificate store.","Output":{"SelfSignedCertificates":[{"FriendlyName":"","Issuer":"CN=Root Agency","Subject":"CN=Root Agency","Thumbprint":"FEE449EE0E3965A5246F000E87FDE2A065FD89D4"},{"FriendlyName":"","Issuer":"CN=aadtestdom-AADTESTDOMDC-CA, DC=aadtestdom, DC=com","Subject":"CN=aadtestdom-AADTESTDOMDC-CA, DC=aadtestdom, DC=com","Thumbprint":"E2D0EF85C434E5D09D66120BE290E0F81CA23F98"}]},"ExceptionMessage":null,"Exception":null}],"ErrorTests":[],"NotRunTests":[]},"Adfs-Configuration":{"Role":3,"MajorOsVersion":10,"CurrentFarmBehavior":null,"AdfsServers":null,"WapServers":["aadtestdoms2.aadtestdom.com"],"OperatingSystem":"Microsoft Windows Server 2016 Datacenter","Database":null,"FederationServiceName":null,"ServiceAccount":"NT AUTHORITY\\NetworkService","ServiceAccountType":"Standard service account","ServiceAccountSpn":null,"AdfsGlobalAuthenticationPolicy":{"AdditionalAuthenticationProvider":null,"DeviceAuthenticationEnabled":null,"AllowAdditionalAuthenticationAsPrimary":null,"EnablePaginatedAuthenticationPages":null,"DeviceAuthenticationMethod":null,"TreatDomainJoinedDevicesAsCompliant":null,"PrimaryIntranetAuthenticationProvider":null,"PrimaryExtranetAuthenticationProvider":null,"WindowsIntegratedFallbackEnabled":null,"ClientAuthenticationMethods":null},"AdfsSslCertificate":null,"AdfsCertificate":null,"AdfsRelyingPartyTrust":null}},"Version":"1.0.9"} 2 | -------------------------------------------------------------------------------- /diagnosticsModule/Test/Private/JoinDiagnosticsFiles.Test.ps1: -------------------------------------------------------------------------------- 1 | # Determine our script root 2 | $parent = Split-Path $PSScriptRoot -Parent 3 | $root = Split-Path $parent -Parent 4 | # Load module via definition 5 | Import-Module $root\ADFSDiagnosticsModule.psm1 -Force 6 | 7 | InModuleScope ADFSDiagnosticsModule { 8 | # The output file 9 | $MergedFiledName = ".\Test\Private\Data\Diagnostics\MergedDiagnosticsFile.json" 10 | 11 | # Cleanup any existing data file 12 | if (Test-Path $MergedFiledName) { 13 | Write-Host "Cleaning up merged file from previous run" 14 | Remove-Item $MergedFiledName 15 | } 16 | 17 | Describe "TestJoinDiagnosticsFile" { 18 | It "should pass" { 19 | Join-DiagnosticsFiles .\Test\Private\Data\Diagnostics 20 | 21 | # Make sure the merged file exists 22 | $exists = Test-Path $MergedFiledName 23 | $exists | should beexactly True 24 | 25 | # Parse the data and make sure that it is indeed merged from both files 26 | 27 | # Version check 28 | $version = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object Version | Select Version 29 | $version.Version | should beexactly "1.0.9" 30 | 31 | # Test data check 32 | $allTests = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select AllTests 33 | $allTests.AllTests.Length | should beexactly 49 34 | 35 | $passedTests = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select PassedTests 36 | $passedTests.PassedTests.Length | should beexactly 31 37 | 38 | $warningTests = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select WarningTests 39 | $warningTests.WarningTests.Length | should beexactly 1 40 | 41 | $failedTests = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select FailedTests 42 | $failedTests.FailedTests.Length | should beexactly 5 43 | 44 | $errorTests = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select ErrorTests 45 | $errorTests.ErrorTests.Length | should beexactly 0 46 | 47 | $notRunTests = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select NotRunTests 48 | $notRunTests.NotRunTests.Length | should beexactly 12 49 | 50 | $reachableServers = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select ReachableServers 51 | $reachableServers.ReachableServers.Length | should beexactly 2 52 | 53 | $unreachableServers = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Test-AdfsServerHealth | Select UnreachableServers 54 | $unreachableServers.UnreachableServers.Length | should beexactly 1 55 | 56 | # ADFS Configuration check 57 | # We don't need to check everything here. It's either all there or none of it is there. 58 | $configuration = @(Get-Content $MergedFiledName -raw) | ConvertFrom-Json | Select-Object -ExpandProperty ADFSToolbox | Select-Object -ExpandProperty Adfs-Configuration 59 | $configuration.AdfsGlobalAuthenticationPolicy.PrimaryIntranetAuthenticationProvider.Length | should beexactly 3 60 | } 61 | 62 | It "should fail" { 63 | # Arrange 64 | Mock -CommandName Write-Error -MockWith {} 65 | 66 | Join-DiagnosticsFiles . 67 | 68 | # Assert 69 | Assert-MockCalled Write-Error 70 | } 71 | 72 | # Cleanup 73 | if (Test-Path $MergedFiledName) { 74 | Write-Host "Cleaning up merged file from test run" 75 | Remove-Item $MergedFiledName 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /diagnosticsModule/build.ps1: -------------------------------------------------------------------------------- 1 | [cmdletbinding()] 2 | param( 3 | [string[]]$Task = 'default', # This task is defined in psakeBuild. We are just setting a default here. 4 | [switch] 5 | $CodeCoverage = $false 6 | ) 7 | 8 | # Verify that we have PackageManagement module installed 9 | if (!(Get-Command Install-Module)) 10 | { 11 | throw 'PackageManagement is not installed. You need V5 or https://www.microsoft.com/en-us/download/details.aspx?id=51451' 12 | } 13 | 14 | # Verify that our testing utilities are installed. 15 | if (!(Get-Module -Name Pester -ListAvailable)) 16 | { 17 | Install-Module -Name Pester 18 | } 19 | if (!(Get-Module -Name psake -ListAvailable)) 20 | { 21 | Install-Module -Name Psake 22 | } 23 | if (!(Get-Module -Name PSScriptAnalyzer -ListAvailable)) 24 | { 25 | Install-Module -Name PSScriptAnalyzer 26 | } 27 | 28 | # Run our test 29 | Invoke-psake -buildFile "$PSScriptRoot\psakeBuild.ps1" -taskList $Task -Verbose:$VerbosePreference -parameters @{"CodeCoverage" = $CodeCoverage} 30 | -------------------------------------------------------------------------------- /diagnosticsModule/en-US/about_ADFSDiagnostics.help.txt: -------------------------------------------------------------------------------- 1 | TOPIC 2 | about_ADFSDiagnostics 3 | 4 | SHORT DESCRIPTION 5 | Contains data gathering, health checks, and additional utilities for AD FS server deployments. 6 | 7 | LONG DESCRIPTION 8 | This module provides cmdlets that can be used to perform various tests on AD FS and WAP servers. The tests can help ensure that the AD FS / WAP service are up and running. Using the cmdlets in the module, you can root cause a service level issue faster. -------------------------------------------------------------------------------- /diagnosticsModule/psakeBuild.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 4 2 | #Requires -RunAsAdministrator 3 | 4 | [cmdletbinding()] 5 | param( 6 | [switch] 7 | $CodeCoverage = $false 8 | ) 9 | 10 | properties { 11 | $root = $PSScriptRoot 12 | } 13 | 14 | # Default task includes Analyzing and Testing of module 15 | task default -depends Analyze, Test 16 | 17 | # Analyze by running Invoke-ScriptAnalyzer. Check script against best known practices 18 | task Analyze { 19 | $saResults = Invoke-ScriptAnalyzer -Path "$root\Public" -Severity @('Error', 'Warning') -Recurse -ExcludeRule "PSAvoidUsingWriteHost", "PSUseDeclaredVarsMoreThanAssignments" -Verbose:$false 20 | $saResults += Invoke-ScriptAnalyzer -Path "$root\Private" -Severity @('Error', 'Warning') -Recurse -Verbose:$false 21 | if ($saResults) 22 | { 23 | $saResults | Format-Table 24 | Write-Error -Message 'One or more Script Analyzer errors/warnings where found. Build cannot continue!' 25 | } 26 | } 27 | 28 | # Run our test to make sure everything is in line 29 | task Test { 30 | $tests = @(Get-ChildItem -Path $PSScriptRoot\Test\**\*.Test.ps1 -Recurse) 31 | 32 | if ($CodeCoverage) 33 | { 34 | $codeCoveragePaths = @() 35 | foreach ($test in $tests) 36 | { 37 | $name = $test.Name -replace ".Test.ps1$", ".ps1" 38 | $directory = $test.Directory.Name 39 | $codeCoveragePath = $PSScriptRoot, $directory, $name -join "\" 40 | if (Test-Path $codeCoveragePath) 41 | { 42 | $codeCoveragePaths += $codeCoveragePath 43 | } 44 | } 45 | 46 | $testResults = Invoke-Pester -Path @($tests.FullName) -CodeCoverage @($codeCoveragePaths) -PassThru 47 | } 48 | else 49 | { 50 | $testResults = Invoke-Pester -Path @($tests.FullName) -PassThru 51 | } 52 | 53 | if ($testResults.FailedCount -gt 0) 54 | { 55 | $testResults.TestResult | Where-Object { $_.Result -ne "Passed" } | Format-List 56 | Write-Error -Message 'One or more Pester tests failed. Build cannot continue!' 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /eventsModule/README.md: -------------------------------------------------------------------------------- 1 | # AD FS Events Module 2 | 3 | ## AdfsEventsModule Overview 4 | 5 | This module provides tools for gathering related ADFS events from the security, admin, and debug logs, 6 | across multiple servers. This tool also allows the user to reconstruct the HTTP request/response headers 7 | from the logs. 8 | 9 | ## Install 10 | 11 | Follow the instructions [here](https://github.com/Microsoft/adfsToolbox#getting-started) to install this module. 12 | 13 | ## Cmdlets in AdfsEventsModule 14 | 15 | This module exposes the following cmdlets: 16 | 17 | 1. __```Get-AdfsEvents```__ - Allows you to query servers for ADFS logs. Contains options for querying, aggregation, and analysis 18 | 19 | 2. __```Write-ADFSEventsSummary```__ - Allows you to generate a summary of an ADFS request, based on the logs from ```Get-AdfsEvents``` 20 | 21 | 3. __```Enable-ADFSAuditing```__ - Enables all the ADFS and OS auditing switches on the current server, and enables just the ADFS switches on remote servers 22 | 23 | 4. __```Disable-ADFSAuditing```__ - Disables all the ADFS and OS auditing switches on the current server, and disables just the ADFS switches on remote servers 24 | 25 | The detailed parameters for __```Get-AdfsEvents```__ and __```Write-ADFSEventsSummary```__ are provided below. 26 | 27 | The ```Get-AdfsEvents``` cmdlet is used to aggregate events by correlation ID, while the ```Write-ADFSEventsSummary``` 28 | cmdlet is used to generate a PowerShell Table of only the most relevant logging information from the events that are piped 29 | in. 30 | 31 | ## Get-AdfsEvents Parameters 32 | 33 | * __Logs__ - A list of AD FS logs to include in the aggregation. Current options are: "Admin", "Debug", "Security". 34 | The default will pull from both Security and Admin. 35 | * __CorrelationID__ - The correlation ID for a single request. This will aggregate all chosen logs for this request 36 | * __All__ - This flag will cause all events in the desired logs to be grouped by correlation ID. 37 | * __CreateAnalysisData__ - This flag can be combined with any means of event collection (a single Correlation ID, all events, or 38 | time based) to reconstruct the HTTP requests that were performed for each Correlation ID. 39 | * __StartTime__ - The UTC start time to use when aggregating multiple requests. All requests that start after this 40 | time will be aggregated 41 | * __EndTime__ - The UTC end time to use when aggregating multiple requests. All requests that end before this time 42 | will be aggregated 43 | * __Server__ - A comma-separated list of server names to pull logs from. On ADFS 2016 and up, you can use "\*" to query all 44 | The default will query LocalHost 45 | * __FilePath__ - A file path to an EVTX log file that you want to read from, instead of querying your servers 46 | 47 | ## Get-AdfsEvents Output 48 | 49 | The output produced by Get-AdfsEvents is a list of objects, each containing the following properties. 50 | 51 | 1. __CorrelationID__ - the Correlation ID for this set of events 52 | 2. __Events__ - a list of [EventLogRecord](https://msdn.microsoft.com/en-us/library/system.diagnostics.eventing.reader.eventlogrecord) 53 | objects for the matching Correlation ID. 54 | 3. __AnalysisData__ - a JSON data blob containing details on the HTTP requests that were performed during the course of this transaction 55 | For more details on the AnalysisData blob, see below 56 | 57 | ## Using Get-AdfsEvents 58 | 59 | 1. Import the PowerShell Module 60 | 61 | In a PowerShell window, run the following: 62 | 63 | ```ipmo AdfsEventsModule.psm1``` 64 | 65 | 2. Run Get-AdfsEvents with your desired parameters to get a list of PowerShell objects 66 | 67 | EXAMPLE: Retrieve all logs from two servers for a specific request 68 | 69 | ```$logs = Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID 0c0fd6ee-4b1e-4260-0300-0080070000e3 -Server LocalHost, MyServer``` 70 | 71 | OUTPUT: 72 | 73 | ``` 74 | Events AnalysisData CorrelationID 75 | ------ ------- ------------- 76 | 77 | {EventLogRecord, EventLogRecord} {} 0c0fd6ee-4b1e-4260-0300-0080070000e3 78 | ``` 79 | 80 | 3. To view specific records: 81 | 82 | ```$logs[0].Events[0]``` 83 | 84 | OUTPUT: 85 | 86 | ``` 87 | Message : An HTTP request was received. See audit 510 with the same Instance ID for headers. 88 | 89 | Instance ID: 64fb88c5-7f4e-4888-8b61-7d0d85563b82 90 | 91 | Activity ID: 0c0fd6ee-4b1e-4260-0300-0080070000e3 92 | 93 | Request Details: 94 | Date And Time: 2017-09-19 20:50:43 95 | Client IP: 123.45.67.9 96 | HTTP Method: GET 97 | Url Absolute Path: /adfs/portal/logo/logo.png 98 | Query string: ?id=12345 99 | Local Port: 443 100 | Local IP: 123.45.67.8 101 | User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 102 | Content Length: 0 103 | Caller Identity: - 104 | Certificate Identity (if any): - 105 | Targeted relying party: - 106 | Through proxy: False 107 | Proxy DNS name: - 108 | CorrelationID : 0c0fd6ee-4b1e-4260-0300-0080070000e3 109 | PSComputerName : LocalHost 110 | RunspaceId : 6d3d7715-08db-4aa1-b299-40d51d5db682 111 | Id : 403 112 | Version : 113 | Qualifiers : 0 114 | Level : 0 115 | Task : 3 116 | Opcode : 117 | Keywords : 12345 118 | RecordId : 12345 119 | ProviderName : AD FS Auditing 120 | ProviderId : 121 | LogName : Security 122 | ProcessId : 123 | ThreadId : 124 | MachineName : contoso.com 125 | UserId : 126 | TimeCreated : 9/19/2017 1:50:43 PM 127 | ActivityId : 128 | RelatedActivityId : 129 | ContainerLog : security 130 | MatchedQueryIds : {} 131 | Bookmark : System.Diagnostics.Eventing.Reader.EventBookmark 132 | LevelDisplayName : Information 133 | OpcodeDisplayName : Info 134 | TaskDisplayName : 135 | KeywordsDisplayNames : {Audit Success, Classic} 136 | Properties : {} 137 | ``` 138 | 139 | 4. You can pipe your output to ```Write-ADFSEventsSummary``` 140 | 141 | EXAMPLE: 142 | 143 | ```Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID 0c0fd6ee-4b1e-4260-0300-0080070000e3 -Server LocalHost, MyServer | Write-ADFSEventsSummary``` 144 | 145 | OUTPUT: 146 | 147 | ``` 148 | Time : 9/19/2017 1:50:43 PM 149 | EventID : 403 150 | Details : An HTTP request was received. See audit 510 with the same Instance ID for headers. 151 | 152 | Instance ID: 64fb88c5-7f4e-4888-8b61-7d0d85563b82 153 | 154 | Activity ID: 0c0fd6ee-4b1e-4260-0300-0080070000e3 155 | 156 | Request Details: 157 | Date And Time: 2017-09-19 20:50:43 158 | Client IP: 123.45.67.9 159 | HTTP Method: GET 160 | Url Absolute Path: /adfs/portal/logo/logo.png 161 | Query string: ?id=12345 162 | Local Port: 443 163 | Local IP: 123.45.67.8 164 | User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 165 | Content Length: 0 166 | Caller Identity: - 167 | Certificate Identity (if any): - 168 | Targeted relying party: - 169 | Through proxy: False 170 | Proxy DNS name: - 171 | CorrelationID : 0c0fd6ee-4b1e-4260-0300-0080070000e3 172 | Machine : contoso.com 173 | Log : Security 174 | 175 | Time : 9/19/2017 1:50:43 PM 176 | EventID : 410 177 | Details : Following request context headers present : 178 | 179 | Activity ID: 0c0fd6ee-4b1e-4260-0300-0080070000e3 180 | 181 | X-MS-Client-Application: - 182 | X-MS-Client-User-Agent: - 183 | client-request-id: - 184 | X-MS-Endpoint-Absolute-Path: /adfs/portal/logo/logo.png 185 | X-MS-Forwarded-Client-IP: - 186 | X-MS-Proxy: - 187 | X-MS-ADFS-Proxy-Client-IP: - 188 | CorrelationID : 0c0fd6ee-4b1e-4260-0300-0080070000e3 189 | Machine : contoso.com 190 | Log : Security 191 | ``` 192 | 193 | 5. You can pipe the output of ```Write-ADFSEventsSummary``` to a CSV 194 | 195 | ```Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID 0c0fd6ee-4b1e-4260-0300-0080070000e3 -Server LocalHost, MyServer | Write-ADFSEventsSummary | Export-CSV mylogs.csv``` 196 | 197 | 198 | 6. You can output the full data objects from ```Get-AdfsEvents``` to XML using: 199 | 200 | ```Export-Clixml``` 201 | 202 | ```Import-Clixml``` 203 | 204 | 205 | ## The AnalysisData Blob 206 | 207 | The AnalysisData blob contains the following: 208 | 209 | * ```requests``` - a set of HTTP requests made during the current transaction. 210 | Each request contains request details, HTTP header information, and session token information (when available) 211 | 212 | * ```responses``` - a set of HTTP responses given during the current transaction. 213 | Each response contains response details, HTTP header information, and outgoing tokens (when available) 214 | 215 | * ```errors``` - a set of [EventLogRecord](https://msdn.microsoft.com/en-us/library/system.diagnostics.eventing.reader.eventlogrecord) objects from 216 | the current transaction that are marked as errors 217 | 218 | * ```timeline``` - a set of timeline events to show the progress of a transaction through the ADFS pipeline. 219 | Timeline events correspond to roughly the following: 220 | 221 | * ```incoming``` - ADFS received an incoming HTTP request 222 | * ```authn``` - ADFS is performing authentication 223 | * ```authz``` - ADFS is performing authorization checks 224 | * ```issuance``` - ADFS is performing token issuance 225 | 226 | Each timeline event contains a ```success``` or ```failure``` result, indicating whether the given pipeline step was a success or failure. 227 | 228 | ## Pester Tests 229 | 230 | This project includes a set of [Pester](https://github.com/pester/Pester) tests to ensure the basic functionality of the script. 231 | 232 | To run the tests, you must have Pester version 4.x or higher installed on the machine you will run ```Get-AdfsEvents``` from. 233 | For more information on installing Pester, see their [installation instructions](https://github.com/pester/Pester/wiki/Installation-and-Update). 234 | 235 | Once Pester is installed, you can copy the test file and script to the same location, and run the following: 236 | 237 | cd 238 | Invoke-Pester -Script .\Test.AdfsEventsModule.ps1 239 | 240 | For more details, see [the testing Readme](TESTDETAILS.md) 241 | 242 | 243 | ## Contributing 244 | 245 | This project welcomes contributions and suggestions. We encourage you to fork this project, include any scripts you 246 | use for parsing, managing, or manipulating ADFS logs, and then do a pull request to master. If your scripts work, 247 | we'll include them so everyone can benefit. 248 | 249 | Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the 250 | right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 251 | 252 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 253 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 254 | provided by the bot. You will only need to do this once across all repos using our CLA. 255 | 256 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 257 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 258 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 259 | -------------------------------------------------------------------------------- /eventsModule/TESTDETAILS.md: -------------------------------------------------------------------------------- 1 | # Pester Tests for AdfsEventsModule 2 | 3 | ## Pester Overview 4 | 5 | This project includes a set of [Pester](https://github.com/pester/Pester) tests to ensure the basic functionality of the AdfsEventsModule script. 6 | 7 | To run the tests, you must have Pester version 4.x or higher installed on the machine you will run ```Get-AdfsEvents``` from. 8 | For more information on installing Pester, see their [installation instructions](https://github.com/pester/Pester/wiki/Installation-and-Update). 9 | 10 | Once Pester is installed, you can copy the test file and script to the same location, and run the following: 11 | 12 | cd 13 | Invoke-Pester -Script .\Test.AdfsEventsModule.ps1 14 | 15 | ## Side Effects 16 | 17 | The testing module will create a Relying Party Trust on your machine, which will be used for generating request logs. 18 | You should manually remove the Relying Party when you have completed testing. 19 | 20 | ## Test Matrix 21 | 22 | 23 | | CorrelationID or All | CreateAnalysisData | FromFile | Server Count | ByTime| Scenario Num | Code Coverage | 24 | | --- | --- | --- | --- | --- | --- | --- | 25 | | All | No | No | 1 | No | 00000 | Covered | 26 | | All | No | No | 1 | Yes | 00001 | Covered | 27 | | All | No | No | 2 | No | 00010 | Multinode Not in scope | 28 | | All | No | No | 2 | Yes | 00011 | Multinode Not in scope | 29 | | All | No | Yes | 1 | No | 00100 | Covered | 30 | | All | No | Yes | 1 | Yes | 00101 | Covered | 31 | | All | No | Yes | 2 | No | 00110 | Multinode Not in scope | 32 | | All | No | Yes | 2 | Yes | 00111 | Multinode Not in scope | 33 | | All | Yes | No | 1 | No | 01000 | Covered | 34 | | All | Yes | No | 1 | Yes | 01001 | Covered | 35 | | All | Yes | No | 2 | No | 01010 | Multinode Not in scope | 36 | | All | Yes | No | 2 | Yes | 01011 | Multinode Not in scope| 37 | | All | Yes | Yes | 1 | No | 01100 | Covered | 38 | | All | Yes | Yes | 1 | Yes | 01101 | Covered | 39 | | All | Yes | Yes | 2 | No | 01110 | Multinode Not in scope | 40 | | All | Yes | Yes | 2 | Yes | 01111 | Multinode Not in scope | 41 | | ID | No | No | 1 | No | 10000 | Covered | 42 | | ID | No | No | 1 | Yes | 10001 | Not a valid scenario. Covered | 43 | | ID | No | No | 2 | No | 10010 | Multinode Not in scope | 44 | | ID | No | No | 2 | Yes | 10011 | Multinode Not in scope | 45 | | ID | No | Yes | 1 | No | 10100 | Covered | 46 | | ID | No | Yes | 1 | Yes | 10101 | Not a valid scenario | 47 | | ID | No | Yes | 2 | No | 10110 | Multinode Not in scope | 48 | | ID | No | Yes | 2 | Yes | 10111 | Multinode Not in scope | 49 | | ID | Yes | No | 1 | No | 11000 | Covered | 50 | | ID | Yes | No | 1 | Yes | 11001 | Not a valid scenario. Covered | 51 | | ID | Yes | No | 2 | No | 11010 | Multinode Not in scope | 52 | | ID | Yes | No | 2 | Yes | 11011 | Multinode Not in scope| 53 | | ID | Yes | Yes | 1 | No | 11100 | Covered | 54 | | ID | Yes | Yes | 1 | Yes | 11101 | Not a valid scenario | 55 | | ID | Yes | Yes | 2 | No | 11110 | Multinode Not in scope | 56 | | ID | Yes | Yes | 2 | Yes | 11111 | Multinode Not in scope | -------------------------------------------------------------------------------- /eventsModule/Test.AdfsEventsModule.ps1: -------------------------------------------------------------------------------- 1 | function Initialize() 2 | { 3 | ipmo .\AdfsEventsModule.psm1 4 | Set-AdfsProperties -AuditLevel Verbose 5 | 6 | # Try installing an RP. If it already exists, use the existing one 7 | try 8 | { 9 | $authzRules = "=>issue(Type = `"http://schemas.microsoft.com/authorization/claims/permit`", Value = `"true`"); " 10 | $issuanceRules = "x:[]=>issue(claim = x); " 11 | $redirectUrl = "https://adfshelp.microsoft.com/ClaimsXray/TokenResponse" 12 | $samlEndpoint = New-AdfsSamlEndpoint -Binding POST -Protocol SAMLAssertionConsumer -Uri $redirectUrl 13 | 14 | Add-ADFSRelyingPartyTrust -Name "ClaimsXray" -Identifier "urn:microsoft:adfs:claimsxray" -IssuanceAuthorizationRules $authzRules -IssuanceTransformRules $issuanceRules -WSFedEndpoint $redirectUrl -SamlEndpoint $samlEndpoint 15 | } 16 | catch 17 | { 18 | 19 | } 20 | 21 | # Clear any existing logs 22 | 23 | } 24 | 25 | function Make-Request([string]$Guid){ 26 | $farmhost = (Get-AdfsProperties).HostName 27 | $url = "https://" + $farmhost + "/adfs/ls?wa=wsignin1.0&wtrealm=urn:microsoft:adfs:claimsxray" 28 | 29 | if ( $Guid ) 30 | { 31 | $url = $url + "&client-request-id=" + $Guid 32 | } 33 | 34 | Invoke-WebRequest -URI $url 35 | } 36 | 37 | function Validate-LogEventCount([object]$logs){ 38 | if($logs.Count -gt 0 -and $logs[0].Events.Count -gt 0) 39 | { 40 | return $true 41 | } 42 | 43 | return $false 44 | } 45 | 46 | Describe 'Basic functionality of Get-AdfsEvents'{ 47 | BeforeAll { 48 | Initialize 49 | 50 | # Make a few requests for the ByTime tests 51 | $global:startTime = Get-Date 52 | for($i=0; $i -le 5; $i++) 53 | { 54 | Make-Request 55 | } 56 | 57 | $global:currentGuid = [guid]::NewGuid() 58 | Make-Request($global:currentGuid.Guid) 59 | 60 | # Give the auditing system time to flush the audits to the system 61 | Start-Sleep -Seconds 3 62 | 63 | $global:endTime = Get-Date 64 | 65 | $securityLogs = "Security" 66 | $global:exportFileName = (pwd).Path + "\SecurityLogs.evtx" 67 | wevtutil.exe export-log $securityLogs $exportFileName /overwrite:true 68 | } 69 | 70 | AfterAll { 71 | rm $global:exportFileName 72 | } 73 | 74 | It "[00000]: 'All' Flag Returns CorrIDs that are valid guids"{ 75 | $logs = Get-AdfsEvents -Logs Security -All 76 | 77 | Validate-LogEventCount($logs) | Should -Be $true 78 | 79 | $hasInvalidGuid = $false 80 | 81 | foreach ( $aggObj in $logs ) 82 | { 83 | $guidRef = [ref] [System.Guid]::NewGuid() 84 | $valid = [System.Guid]::TryParse( $aggObj.CorrelationID, $guidRef ) 85 | 86 | if ( !$valid -or ( $guidRef.Value -ne $aggObj.CorrelationID ) ) 87 | { 88 | $hasInvalidGuid = $true 89 | break 90 | } 91 | } 92 | 93 | $hasInvalidGuid | Should -Be $false 94 | } 95 | 96 | It "[00000]: 'All' Flag Returns Multiple Aggregate Objects, with Multiple Events"{ 97 | $logs = Get-AdfsEvents -Logs Security, Admin -All 98 | Validate-LogEventCount($logs) | Should -Be $true 99 | } 100 | 101 | It "[00000]: 'All' Flag Returns Aggregate Objects, with Events by correlation ID"{ 102 | $logs = Get-AdfsEvents -Logs Security, Admin -All 103 | 104 | $hasInvalidId = $false 105 | foreach ( $aggObj in $logs ) 106 | { 107 | foreach ( $event in $aggObj.Events ) 108 | { 109 | if ( $event.CorrelationID -ne $aggObj.CorrelationID ) 110 | { 111 | $hasInvalidId = $true 112 | } 113 | } 114 | } 115 | 116 | $hasInvalidId | Should -Be $false 117 | } 118 | 119 | It "[00100]: 'All' Flag with FromFile Returns Non-Empty Events List"{ 120 | $logs = Get-AdfsEvents -Logs Security -All -FilePath $global:exportFileName 121 | Validate-LogEventCount($logs) | Should -Be $true 122 | } 123 | 124 | It "[01000]: 'All' Flag with AnalysisData Returns Analysis Objects"{ 125 | $logs = Get-AdfsEvents -Logs Security -All -CreateAnalysisData 126 | 127 | $hasInvalidBlob = $false 128 | 129 | foreach ( $aggObj in $logs ) 130 | { 131 | if ( -not $aggObj.AnalysisData.requests.Count ) 132 | { 133 | $hasInvalidBlob = $true 134 | break 135 | } 136 | } 137 | 138 | $hasInvalidBlob | Should -Be $false 139 | } 140 | 141 | It "[01001]: ByTime with AnalysisData Returns Analysis Objects"{ 142 | $logs = Get-AdfsEvents -Logs Security -CreateAnalysisData -StartTime $global:startTime -EndTime $global:endTime 143 | 144 | $hasInvalidBlob = $false 145 | 146 | foreach ( $aggObj in $logs ) 147 | { 148 | if ( -not $aggObj.AnalysisData.requests.Count ) 149 | { 150 | $hasInvalidBlob = $true 151 | break 152 | } 153 | } 154 | 155 | $hasInvalidBlob | Should -Be $false 156 | } 157 | 158 | It "[01001]: ByTime returns Multiple Aggregate Objects, with Multiple Events"{ 159 | $logs = Get-AdfsEvents -Logs Security -StartTime $global:startTime -EndTime $global:endTime 160 | Validate-LogEventCount($logs) | Should -Be $true 161 | } 162 | 163 | It "[01100]: 'All' Flag with AnalysisData with FromFile Returns Non-Empty Events List"{ 164 | $logs = Get-AdfsEvents -Logs Security -All -FilePath $global:exportFileName -CreateAnalysisData 165 | Validate-LogEventCount($logs) | Should -Be $true 166 | } 167 | 168 | It "[01101]: ByTime with AnalysisData with FromFile returns Non-Empty Events List"{ 169 | $logs = Get-AdfsEvents -Logs Security -FilePath $global:exportFileName -CreateAnalysisData -StartTime $global:startTime -EndTime $global:endTime 170 | Validate-LogEventCount($logs) | Should -Be $true 171 | } 172 | 173 | It "[10000]: CorrelationID Call Returns Exactly 1 Aggregate Object"{ 174 | $logs = Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID $global:currentGuid.Guid 175 | 176 | # Note: despite the fact that PowerShell should always be giving us a list object out of Get-AdfsEvents, the 177 | # Count and Length calls do not work when there is only 1 entry in the list 178 | 179 | $hasAtLeastOne = $false 180 | $hasExactlyOne = $false 181 | 182 | if ( $logs[0].CorrelationID ) 183 | { 184 | $hasAtLeastOne = $true 185 | } 186 | 187 | if ( $hasAtLeastOne -and ( -not $logs[1] ) ) 188 | { 189 | $hasExactlyOne = $true 190 | } 191 | 192 | $hasExactlyOne | Should -Be $true 193 | } 194 | 195 | It "[10000]: CorrelationID Call Returns Non-Empty Events list"{ 196 | $logs = Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID $global:currentGuid.Guid 197 | $logs[0].Events.Count | Should -BeGreaterThan 0 198 | } 199 | 200 | It "[10000]: CorrelationID Call Returns Events list with 403 and 404"{ 201 | 202 | $logs = Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID $global:currentGuid.Guid 203 | 204 | $has403 = $false 205 | $has404 = $false 206 | 207 | foreach( $event in $logs.Events ) 208 | { 209 | if ( $event.Id -eq 403 ) 210 | { 211 | $has403 = $true 212 | } 213 | 214 | if ( $event.Id -eq 404 ) 215 | { 216 | $has404 = $true 217 | } 218 | } 219 | 220 | $hasBoth = $has403 -and $has404 221 | $hasBoth | Should -Be $true 222 | } 223 | 224 | It "[10000]: CorrelationID Call Returns Analysis Data With Single Request"{ 225 | $logs = Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID $global:currentGuid.Guid -CreateAnalysisData 226 | $logs.AnalysisData.requests.Count | Should -Be 1 227 | } 228 | 229 | It "[10000]: CorrelationID Call Returns Analysis Data With Single Timeline Event"{ 230 | $logs = Get-AdfsEvents -Logs Security, Admin, Debug -CorrelationID $global:currentGuid.Guid -CreateAnalysisData 231 | $logs.AnalysisData.timeline.Count | Should -Be 1 232 | $logs.AnalysisData.timeline[0].type | Should -Be "incoming" 233 | } 234 | 235 | It "[10100]: CorrelationID Call with FromFile returns Non-Empty Events List"{ 236 | $logs = Get-AdfsEvents -Logs Security -CorrelationID $global:currentGuid.Guid -FilePath $global:exportFileName 237 | $logs[0].Events.Count | Should -BeGreaterThan 0 238 | } 239 | 240 | It "[10101]: ByTime with CorrelationID is not a valid scenario"{ 241 | 242 | $invalidScenarioError = $false; 243 | 244 | try 245 | { 246 | $logs = Get-AdfsEvents -Logs Security -StartTime $global:startTime -EndTime $global:endTime -CorrelationID $global:currentGuid.Guid 247 | }catch [System.Management.Automation.ParameterBindingException] 248 | { 249 | $invalidScenarioError = $true; 250 | } 251 | 252 | $invalidScenarioError | Should -Be $true 253 | } 254 | 255 | It "[11100]: CorrelationID Call with AnalysisData with FromFile returns Non-Empty Events List"{ 256 | $logs = Get-AdfsEvents -Logs Security -CorrelationID $global:currentGuid.Guid -FilePath $global:exportFileName -CreateAnalysisData 257 | $logs[0].Events.Count | Should -BeGreaterThan 0 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /serviceAccountModule/README.md: -------------------------------------------------------------------------------- 1 | # Change AD FS Service Account 2 | 3 | ## Overview 4 | 5 | This powershell module allows the AD FS service account to be changed. Such functionality may be especially useful if the current service account has been compromised. 6 | 7 | The module exports four functions which may be used in conjunction to successfully migrate the service to a new account or recover from an error encountered along the way as described below. 8 | 9 | ## Warning Before Use 10 | 11 | It is highly recommended that you create a backup before attemptig to change the service account as executing cmdlets in the wrong order may result in non-functioning AD FS servers. Performing the change first in a test farm is also advised. 12 | 13 | Althouhgh it is recommended a list of secondary servers be provided when invoking Add-AdfsServiceAccountRule or Remove-AdfsServiceAccountRule on a multi-node WID farm, you can optionally manually force a sync on all secondary servers or allow it to occur on the next sync cycle (5 minutes by default). 14 | 15 | ## Install 16 | 17 | Follow the instructions [here](https://github.com/Microsoft/adfsToolbox#getting-started) to install this module. 18 | 19 | ## Available Cmdlets 20 | 21 | The module exposes the following cmdlets: 22 | 1. __```Add-AdfsServiceAccountRule```__ - Adds permission rule for the specified service account. Must be run prior to changing the service account on Windows Server 2016 or later. 23 | 2. __```Remove-AdfsServiceAccountRule```__ - Removes permission rule for the specified service account. This should not be run until the AD FS service is verfifed to work with the new service account. 24 | 3. __```Update-AdfsServiceAccount```__ - Changes the AD FS service account on the local machine. This cmdlet should be run on all secondary servers prior to execution on the primary machine. 25 | 4. __```Restore-AdfsSettingsFromBackup```__ - Restores the AD FS service settings with a backup generated during either Add-AdfsServiceAccountRule or Remove-AdfsServiceAccountRule. This cmdlet can be used to recover if an error occurs during either of the afforementioned commands. 26 | ## Requirements 27 | 28 | 1. The module is applicable for any AD FS farm and works for both SQL and WID environments. 29 | 30 | 2. Add-AdfsServiceAccountRule and Remove-AdfsServiceAccountRule only need to be run on Windows Server 2016 and later. 31 | 32 | 33 | ## Getting Started 34 | 35 | 1. Download the `ServiceAccount.psm1` module to all of your AD FS servers (primary and secondary) 36 | 37 | 2. Import the PowerShell Module on all servers 38 | 39 | In a PowerShell window, run the following: 40 | 41 | ```ipmo ServiceAccount.psm1``` 42 | 43 | 3. For Windows Server 2016 and later, add a rule granting the new service account necessary permissions. 44 | 45 | In a PowerShell window on the primary AD FS server, run the following: 46 | 47 | ```Add-AdfsServiceAccountRule -ServiceAccount -SecondaryServers ``` 48 | 49 | Note that `````` should be the service account you want to grant permissions to and can be provided either in the format ```Domain\User``` or merely ```User```. 50 | 51 | `````` should be replaced with a list of secondary servers if the environment is a WID farm so that the configuration database can be synced across all machines. 52 | 53 | 4. Change the service account on each machine in the farm. 54 | 55 | Beginning with the secondary servers, run the following: 56 | 57 | ```Update-AdfsServiceAccount``` 58 | 59 | Once the function has been executed on all secondary servers, proceed to run it on the primary server. 60 | 61 | 5. If Device Registration Services (DRS) is set up in your AD FS environment, you must also use the ```Set-AdfsDeviceRegistration``` cmdlet (an internal command exposed by the service) to add the proper permissions to the new service account. 62 | 63 | 64 | 6. For Windows Server 2016 and later, remove the rule granting permissions to the old service account. 65 | 66 | In a PowerShell window on the primary AD FS server, run the following: 67 | 68 | ```Remove-AdfsServiceAccountRule -ServiceAccount -SecondaryServers ``` 69 | 70 | Note that `````` should be the service account you want to revoke permissions for and can be provided either in the format ```Domain\User``` or merely ```User```. 71 | 72 | `````` should be replaced with a list of secondary servers if the environment is a WID farm so that the configuration database can be synced across all machines. 73 | 74 | 75 | 76 | 77 | ## Add-AdfsServiceAccountRule Parameters 78 | 79 | __`ServiceAccount`__ - Name of the service account for which to add a new rule. Can be provided in either the format Domain\User or User 80 | 81 | __`SecondaryServers`__ - Comma separated list of AD FS secondary servers. List is used to force a WID sync across all machines in the farm. 82 | 83 | ## Remove-AdfsServiceAccountRule Parameters 84 | 85 | __`ServiceAccount`__ - Name of the service account for which to remove permissions rule. Can be provided in either the format Domain\User or User 86 | 87 | __`SecondaryServers`__ - Comma separated list of AD FS secondary servers. List is used to force a WID sync across all machines in the farm. 88 | 89 | ## Restore-AdfsSettingsFromBackup Parameters 90 | 91 | __`BackupPath`__ - Path to backup file generated by either Add-AdfsServiceAccountRule or Remove-AdfsServiceAccountRule. 92 | 93 | Should resemble ```C:\Users\Administrator\Documents\serviceSettingsData-2018-04-11-12-04-03.xml``` 94 | 95 | ## Tests 96 | A test file validating basic functionality (adding and removing rules) can be found __[here](Tests)__. Note that since these tests create and remove AD users and also write to the configuration database they should primarily be used by developers to validate any changes made to the module. 97 | 98 | To execute the test suite, simply copy Test.ServiceAccount.ps1 into the same directory as ServiceAccount.psm1 and run the following command: ```.\Test.ServiceAccount.ps1``` 99 | 100 | 101 | -------------------------------------------------------------------------------- /serviceAccountModule/Tests/Test.ServiceAccount.ps1: -------------------------------------------------------------------------------- 1 | #Define global constants 2 | $script:Service_Account_Name = "newServiceAcct" 3 | 4 | function New-ServiceAccount 5 | { 6 | param( 7 | [Parameter(Mandatory=$true,Position=0)] 8 | [string]$username, 9 | 10 | [Parameter(Mandatory=$true,Position=1)] 11 | [string]$password 12 | ) 13 | Process 14 | { 15 | ipmo ActiveDirectory 16 | $oldLocation = Get-Location 17 | Set-Location AD: 18 | 19 | $encryptedPassword = $password | ConvertTo-SecureString -asPlainText -Force 20 | 21 | try 22 | { 23 | $userObject = Get-ADUser $userName -ErrorAction SilentlyContinue 24 | } 25 | catch 26 | { 27 | } 28 | 29 | if ($userObject -eq $null) 30 | { 31 | New-ADUser -Name $userName 32 | } 33 | 34 | Get-ADUser $userName | Set-ADAccountPassword -Reset -NewPassword $encryptedPassword -PassThru 35 | Get-ADUser $userName | Set-ADUser -PasswordNeverExpires $true -PassThru -Description "Password: $password" -Enabled $true 36 | 37 | Set-Location $oldLocation 38 | } 39 | } 40 | 41 | ##################################################################### 42 | ####Helper functions related to rule parsing logic################### 43 | ##################################################################### 44 | 45 | <# 46 | .SYNOPSIS 47 | Class to encapsulate parsing of the ADFS Issuances/Auth rules. 48 | #> 49 | 50 | class AdfsRules 51 | { 52 | [System.Collections.ArrayList] hidden $rules 53 | 54 | <# 55 | .SYNOPSIS 56 | Constructor 57 | #> 58 | AdfsRules([string]$rawRules) 59 | { 60 | $rulesArray = $this.ParseRules($rawRules) 61 | $this.rules = New-Object "System.Collections.ArrayList" 62 | $this.rules.AddRange($rulesArray) 63 | } 64 | 65 | <# 66 | .SYNOPSIS 67 | Utility function to parse the rules and return them as a string[]. 68 | #> 69 | [string[]] hidden ParseRules([string]$rawRules) 70 | { 71 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : BEGIN" 72 | 73 | $allRules = @() 74 | $singleRule = [string]::Empty 75 | 76 | $rawRules.Split("`n") | %{ 77 | 78 | $line = $_.ToString().Trim() 79 | 80 | if (-not ([string]::IsNullOrWhiteSpace($line)) ) 81 | { 82 | $singleRule += $_ + "`n" 83 | 84 | if ($line.StartsWith("=>")) 85 | { 86 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Parsed rule:`n$singleRule" 87 | $allRules += $singleRule 88 | $singleRule = [string]::Empty 89 | } 90 | } 91 | } 92 | 93 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : END" 94 | 95 | return $allRules 96 | } 97 | 98 | [int] NumberOfRules() 99 | { 100 | return $this.rules.Count 101 | } 102 | 103 | <# 104 | .SYNOPSIS 105 | Finds the rule by name in the format: @RuleName = "$ruleName". Returns $null if not found. 106 | #> 107 | [string] FindByRuleName([string]$ruleName) 108 | { 109 | $ruleNameSearchString = '@RuleName = "' + $ruleName + '"' 110 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Search string: $ruleNameSearchString" 111 | 112 | foreach ($rule in $this.rules) 113 | { 114 | if ($rule.Contains($ruleNameSearchString)) 115 | { 116 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Found.`n$rule" 117 | return $rule 118 | } 119 | } 120 | 121 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : NOT FOUND. Returning $null" 122 | return $null; 123 | } 124 | 125 | <# 126 | .SYNOPSIS 127 | Replaces the specified old rule with the new one. Returns $true if the old one was found and replaced; $false otherwise. 128 | #> 129 | [bool] ReplaceRule([string]$oldRule, [string]$newRule) 130 | { 131 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Trying to replace old rule with new.`n Old Rule:`n$oldRule`n New Rule:`n$newRule" 132 | $idx = $this.FindIndexForRule($oldRule) 133 | 134 | if ($idx -ge 0) 135 | { 136 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Replacing old rule with new." 137 | $this.rules[$idx] = $newRule 138 | return $true 139 | } 140 | 141 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Old rule is not found so NOT replacing it." 142 | return $false 143 | } 144 | 145 | <# 146 | .SYNOPSIS 147 | Removes the specified if found. Returns $true if found; $false otherwise. 148 | #> 149 | [bool] RemoveRule([string]$ruleToRemove) 150 | { 151 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Trying to remove rule.`n Rule:`n$ruleToRemove" 152 | 153 | $idx = $this.FindIndexForRule($ruleToRemove) 154 | 155 | if ($idx -ge 0) 156 | { 157 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Removing rule at index: $idx." 158 | $this.rules.RemoveAt($idx) 159 | return $true 160 | } 161 | 162 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Rule is not found so NOT removing it." 163 | return $false 164 | } 165 | 166 | <# 167 | .SYNOPSIS 168 | Helper function to find the index of the rule. Returns index if found; -1 otherwise. 169 | #> 170 | [int] FindIndexForRule([string]$ruleToFind) 171 | { 172 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Trying to find rule.`n Rule:`n$ruleToFind" 173 | 174 | for ($i = 0; $i -lt $this.rules.Count; $i++) 175 | { 176 | $rule = $this.rules[$i] 177 | 178 | if ($rule.trim().Equals($ruleToFind.trim())) 179 | { 180 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : Found at index: $i." 181 | return $i 182 | } 183 | } 184 | 185 | Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand) : NOT FOUND. Returning -1" 186 | return -1 187 | } 188 | 189 | <# 190 | .SYNOPSIS 191 | Returns all the rules as string. 192 | #> 193 | [string] ToString() 194 | { 195 | return [string]::Join("`n", $this.rules.ToArray()) 196 | } 197 | } 198 | 199 | # Gets internal ADFS settings by extracting them Get-AdfsProperties 200 | function Get-AdfsInternalSettings() 201 | { 202 | $settings = Get-AdfsProperties 203 | $settingsType = $settings.GetType() 204 | $propInfo = $settingsType.GetProperty("ServiceSettingsData", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) 205 | $internalSettings = $propInfo.GetValue($settings, $null) 206 | 207 | return $internalSettings 208 | } 209 | 210 | function ValidateRules 211 | { 212 | param 213 | ( 214 | [parameter()] 215 | [switch]$CheckNotPresent 216 | ) 217 | 218 | $Properties = Get-AdfsInternalSettings 219 | $AuthorizationPolicyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicy) 220 | $AuthorizationPolicyReadOnlyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicyReadOnly) 221 | 222 | $SID = (New-Object system.security.principal.NtAccount($Service_Account_Name )).translate([system.security.principal.securityidentifier]) 223 | $ServiceAccountRule = "@RuleName = `"Permit Service Account`"`nexists([Type == `"http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid`", Value == `"$SID`"])`n=> issue(Type = `"http://schemas.microsoft.com/authorization/claims/permit`", value = `"true`");`n`n" 224 | 225 | $AuthPolicyIndex = $AuthorizationPolicyRules.FindIndexForRule($ServiceAccountRule) 226 | $ReadOnlyIndex = $AuthorizationPolicyReadOnlyRules.FindIndexForRule($ServiceAccountRule) 227 | 228 | if($CheckNotPresent) 229 | { 230 | return ($AuthPolicyIndex -eq -1 -and $ReadOnlyIndex -eq -1) 231 | } 232 | return ($AuthPolicyIndex -ne -1 -and $ReadOnlyIndex -ne -1) 233 | 234 | } 235 | 236 | 237 | function Initialize() 238 | { 239 | ipmo .\ServiceAccount.psm1 240 | New-ServiceAccount -username $script:Service_Account_Name -password "Password" 241 | } 242 | 243 | Describe 'Basic functionality of adding and removing service account rule'{ 244 | BeforeAll { 245 | Initialize 246 | } 247 | 248 | AfterAll { 249 | Remove-ADUser -Identity $script:Service_Account_Name 250 | } 251 | 252 | It "[00000]: Add-AdfsServiceAccountRule adds permit rule to ruleset"{ 253 | Add-AdfsServiceAccountRule -ServiceAccount $script:Service_Account_Name 254 | ValidateRules | Should Be $true 255 | } 256 | 257 | It "[00000]: Add-AdfsServiceAccountRule fails if rule already exists"{ 258 | $BeforeProperties = Get-AdfsInternalSettings 259 | $BeforeAuthorizationPolicyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicy) 260 | $BeforeAuthorizationPolicyReadOnlyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicyReadOnly) 261 | 262 | Add-AdfsServiceAccountRule -ServiceAccount $script:Service_Account_Name 263 | 264 | $AfterProperties = Get-AdfsInternalSettings 265 | $AfterAuthorizationPolicyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicy) 266 | $AfterAuthorizationPolicyReadOnlyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicyReadOnly) 267 | 268 | $AuthPolicyMatches = $BeforeAuthorizationPolicyRules.NumberOfRules() -eq $AfterAuthorizationPolicyRules.NumberOfRules() 269 | $ReadOnlyMatches = $BeforeAuthorizationPolicyReadOnlyRules.NumberOfRules() -eq $AfterAuthorizationPolicyReadOnlyRules.NumberOfRules() 270 | 271 | 272 | 273 | ($AuthPolicyMatches -eq $ReadOnlyMatches) | Should Be $true 274 | } 275 | 276 | It "[00000]: Remove-AdfsServiceAccountRule removes permit rule to ruleset"{ 277 | Remove-AdfsServiceAccountRule -ServiceAccount $script:Service_Account_Name 278 | ValidateRules -CheckNotPresent | Should Be $true 279 | } 280 | 281 | It "[00000]: Remove-AdfsServiceAccountRule does nothing if rule isn't present"{ 282 | $BeforeProperties = Get-AdfsInternalSettings 283 | $BeforeAuthorizationPolicyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicy) 284 | $BeforeAuthorizationPolicyReadOnlyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicyReadOnly) 285 | 286 | Remove-AdfsServiceAccountRule -ServiceAccount $script:Service_Account_Name 287 | 288 | $AfterProperties = Get-AdfsInternalSettings 289 | $AfterAuthorizationPolicyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicy) 290 | $AfterAuthorizationPolicyReadOnlyRules = [AdfsRules]::new($Properties.PolicyStore.AuthorizationPolicyReadOnly) 291 | 292 | $AuthPolicyMatches = $BeforeAuthorizationPolicyRules.NumberOfRules() -eq $AfterAuthorizationPolicyRules.NumberOfRules() 293 | $ReadOnlyMatches = $BeforeAuthorizationPolicyReadOnlyRules.NumberOfRules() -eq $AfterAuthorizationPolicyReadOnlyRules.NumberOfRules() 294 | 295 | 296 | 297 | ($AuthPolicyMatches -eq $ReadOnlyMatches) | Should Be $true 298 | } 299 | 300 | It "[00000]: Add-AdfsServiceAccountRule adds permit rule to ruleset"{ 301 | $ErrorThrown = $false 302 | try 303 | { 304 | Add-AdfsServiceAccountRule -ServiceAccount "fakeAccount" 305 | } 306 | catch 307 | { 308 | $ErrorThrown = $true 309 | } 310 | $ErrorThrown | Should Be $true 311 | } 312 | 313 | } 314 | -------------------------------------------------------------------------------- /tlsModule/README.md: -------------------------------------------------------------------------------- 1 | # ADFS TLS Configuration 2 | 3 | ## Overview 4 | 5 | Configures ADFS servers for TLS 1.2 security. 6 | 7 | The module exports two functions which may be used to test a specific services current configuration for Transport Layer Security (TLS) 1.2 and to configure a specific server for TLS 1.2 exclusively. 8 | 9 | ``` 10 | Warning Before Use 11 | 12 | It is highly recommended that you create a backup before attempting to configure your ADFS servers. Performing the change first in a test farm is also advised. 13 | ``` 14 | When configuring an ADFS environment for TLS 1.2 each ADFS server and WAP server (proxy) in the farm should be tested and configured for TLS 1.2 use. The functions in this module work by reading and altering the SChannel related registry values on the Windows computer. Registry items used are detailed in [here](http://support2.microsoft.com/kb/245030/en-us). The ADFS configuration for TLS is documented [here]( https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/manage-ssl-protocols-in-ad-fs). 15 | 16 | Important: After configuring the servers for TLS 1.2 the servers must be rebooted for the new settings to take effect. 17 | 18 | ## Install 19 | 20 | Follow the instructions [here](https://github.com/Microsoft/adfsToolbox#getting-started) to install this module. 21 | 22 | ## Available Cmdlets 23 | 24 | The module exposes the following cmdlets: 25 | 1. `Get-ADFSTLSConfiguration` - Adds permission rule for the specified service account. Must be run prior to changing the service account on Windows Server 2016 or later. This cmdlet will write the configuration to the console and to an output text file. 26 | 2. `Set-ADFSTLSConfiguration` - Removes permission rule for the specified service account. This should not be run until the AD FS service is verified to work with the new service account. Results will be written to the console. 27 | 28 | ## Requirements 29 | 30 | 1. The module will work with Windows Server 2012 R2 and Windows Server 2016. 31 | 32 | 2. The module must be running as a local Administrator and in an elevated PowerShell console. 33 | 34 | ## Getting Started 35 | 36 | 1. Back up the ADFS farm. 37 | 38 | 2. Download the ADFSToolbox module to all your AD FS servers (primary and secondary) and Web Application Proxy (WAP) servers. 39 | 40 | 4. If testing the configuration on the servers: 41 | 42 | In a PowerShell window on the primary AD FS server and on each WAP server, run the following and the review the output for results: 43 | 44 | `Get-ADFSTLSConfiguration` 45 | 46 | 5. If setting a server to use TLS 1.2 run the cmdlet below: 47 | 48 | `Set-ADFSTLSConfiguration` 49 | 50 | Once the cmdlet has run reboot the server. 51 | 52 | Note: Apply the setting and reboot the servers one at a time. If the farm is a WID configuration, run the Set-ADFSTLSConfiguration on the Primary server and reboot it first. 53 | 54 | 55 | -------------------------------------------------------------------------------- /widSyncModule/AdfsWidSyncModule.psm1: -------------------------------------------------------------------------------- 1 | function Get-AdfsWidServiceStateSummary 2 | { 3 | $stsWMIObject = (Get-WmiObject -Namespace root\ADFS -Class SecurityTokenService) 4 | 5 | #Create SQL Connection 6 | $connection = new-object system.data.SqlClient.SqlConnection($stsWMIObject.ConfigurationDatabaseConnectionString); 7 | $connection.Open() 8 | 9 | $query = "SELECT * FROM IdentityServerPolicy.ServiceStateSummary"; 10 | $sqlcmd = $connection.CreateCommand(); 11 | $sqlcmd.CommandText = $query; 12 | 13 | $result = $sqlcmd.ExecuteReader(); 14 | $table = new-object "System.Data.DataTable" 15 | $table.Load($result) 16 | $table | ft 17 | } 18 | 19 | function Reset-AdfsWidServiceStateSummarySerialNumbers 20 | { 21 | $stsWMIObject = (Get-WmiObject -Namespace root\ADFS -Class SecurityTokenService) 22 | 23 | #Create SQL Connection 24 | $connection = new-object system.data.SqlClient.SqlConnection($stsWMIObject.ConfigurationDatabaseConnectionString); 25 | $connection.Open() 26 | 27 | $update = "UPDATE IdentityServerPolicy.ServiceStateSummary SET [SerialNumber] = '0'"; 28 | $sqlcmd = $connection.CreateCommand(); 29 | $sqlcmd.CommandText = $update; 30 | $sqlcmd.CommandTimeout = 600000; 31 | $rowsAffected = $sqlcmd.ExecuteNonQuery() 32 | Write-Host $rowsAffected "rows have been affected by the reset of SerialNumber column" 33 | } 34 | 35 | function Invoke-WidSync 36 | { 37 | param ( 38 | [Parameter(Mandatory=$false)] 39 | [switch] $Force 40 | ) 41 | 42 | if ( -not $force ) 43 | { 44 | Write-Host "You must use the 'Force' parameter" -ForegroundColor Yellow 45 | return 46 | } 47 | 48 | $role = (Get-AdfsSyncProperties).role 49 | $LastSyncStatus = (Get-AdfsSyncProperties).LastSyncStatus 50 | 51 | if ($role -eq "SecondaryComputer") 52 | { 53 | if ($LastSyncStatus -eq '0') 54 | { 55 | Write-Host "Resetting the serialnumber column of ServiceStateSummary table to force a full WID sync" -ForegroundColor Green 56 | 57 | Write-Host "ServiceStateSummary table content before reset:" -ForegroundColor Green 58 | Get-AdfsWidServiceStateSummary 59 | 60 | Write-Host "Resetting the serialnumber of ServiceStateSummary table" -ForegroundColor Green 61 | Reset-AdfsWidServiceStateSummarySerialNumbers 62 | 63 | Write-Host "ServiceStateSummary table content after reset:" -ForegroundColor Green 64 | Get-AdfsWidServiceStateSummary 65 | 66 | Write-Host "The full sync will occur on this AD FS Secondary server during the next normal sync poll (by default it occurs every 5 minutes)" -ForegroundColor Green 67 | } 68 | else 69 | { 70 | Write-Host "The last sync status was not sucessful. Cannot force WID sync." -ForegroundColor Yellow 71 | } 72 | } 73 | else 74 | { 75 | Write-Host "This AD FS server is not a secondary server. Please run this cmdlet on your secondary server." -ForegroundColor Yellow 76 | } 77 | } 78 | 79 | Export-ModuleMember -Function Invoke-WidSync; 80 | -------------------------------------------------------------------------------- /widSyncModule/README.md: -------------------------------------------------------------------------------- 1 | # AD FS WID Sync 2 | 3 | ## Overview 4 | 5 | This PowerShell script allows an AD FS administrator to force a full WID database re-synchronization from a primary AD FS server onto a secondary AD FS server, causing the secondary AD FS server WID database to match the contents of the primary server. 6 | 7 | This script is useful when the database of one AD FS node is inconsistent/divergent from the primary AD FS node. 8 | 9 | The script does not force an immediate synchronization, but instead, it forces a full synchronization to occur during the next configured sync interval. 10 | 11 | A full sync means all rows of all tables of the WID database will be re-synced from the Primary AD FS server to Secondary AD FS server (and not just the delta from a previous sync). 12 | 13 | 14 | ## Requirements 15 | 16 | 1. This script requires an AD FS environment with at least two servers (an AD FS Primary and Secondary node). 17 | 18 | 2. This script requires an AD FS environment that uses Windows Internal Database (WID) for AD FS configuration storage. 19 | 20 | ## Install 21 | 22 | Follow the instructions [here](https://github.com/Microsoft/adfsToolbox#getting-started) to install this module. 23 | 24 | ## Getting Started 25 | 26 | 1. Run Invoke-WidSync with the `-Force` parameter to cause a WID sync at the next poll interval on your AD FS secondary server 27 | 28 | ```Invoke-WidSync -Force``` 29 | 30 | ```OUTPUT: 31 | 32 | PS C:\Tools> ipmo AdfsWidSync.psm1 33 | PS C:\Tools> Invoke-WidSync -Force 34 | 35 | ServiceStateSummary table content before reset: 36 | 37 | ServiceObjectType SerialNumber SchemaVersionNumber LastUpdateTime 38 | ----------------- ------------ ------------------- -------------- 39 | AdfsTrustedFederationPartner 0 1 7/4/2017 9:55:38 AM 40 | ApplicationGroup 29 1 10/24/2017 10:25:36 AM 41 | Client 39 1 10/24/2017 10:25:36 AM 42 | FarmNode 8529 1 10/24/2017 10:25:36 AM 43 | IssuanceAuthority 474 1 10/24/2017 10:25:36 AM 44 | IssuanceAuthorityGroup 0 1 7/4/2017 9:55:38 AM 45 | IssuanceClaimDescriptor 81 1 10/24/2017 10:25:36 AM 46 | IssuanceScope 660 1 10/24/2017 10:25:36 AM 47 | IssuanceScopeGroup 0 1 10/24/2017 10:25:36 AM 48 | OAuthPermission 61 1 10/24/2017 10:25:36 AM 49 | OAuthScopeDescription 9 1 10/24/2017 10:25:36 AM 50 | PolicyTemplate 37 1 10/24/2017 10:25:36 AM 51 | ProxyTrust 198 1 10/24/2017 10:25:36 AM 52 | RelyingPartyWebTheme 0 1 10/24/2017 10:25:36 AM 53 | ServiceSettings 39 1 10/24/2017 10:25:36 AM 54 | WebApplicationProxyData 15 1 10/24/2017 10:25:36 AM 55 | WebCustomizationResource 0 1 10/24/2017 10:25:36 AM 56 | WebTheme 1 1 10/24/2017 10:25:36 AM 57 | 58 | 59 | Resetting the serialnumber of ServiceStateSummary table 60 | 18 rows have been affected by the reset of SerialNumber column 61 | ServiceStateSummary table content after reset: 62 | 63 | ServiceObjectType SerialNumber SchemaVersionNumber LastUpdateTime 64 | ----------------- ------------ ------------------- -------------- 65 | AdfsTrustedFederationPartner 0 1 7/4/2017 9:55:38 AM 66 | ApplicationGroup 0 1 10/24/2017 10:25:36 AM 67 | Client 0 1 10/24/2017 10:25:36 AM 68 | FarmNode 0 1 10/24/2017 10:25:36 AM 69 | IssuanceAuthority 0 1 10/24/2017 10:25:36 AM 70 | IssuanceAuthorityGroup 0 1 7/4/2017 9:55:38 AM 71 | IssuanceClaimDescriptor 0 1 10/24/2017 10:25:36 AM 72 | IssuanceScope 0 1 10/24/2017 10:25:36 AM 73 | IssuanceScopeGroup 0 1 10/24/2017 10:25:36 AM 74 | OAuthPermission 0 1 10/24/2017 10:25:36 AM 75 | OAuthScopeDescription 0 1 10/24/2017 10:25:36 AM 76 | PolicyTemplate 0 1 10/24/2017 10:25:36 AM 77 | ProxyTrust 0 1 10/24/2017 10:25:36 AM 78 | RelyingPartyWebTheme 0 1 10/24/2017 10:25:36 AM 79 | ServiceSettings 0 1 10/24/2017 10:25:36 AM 80 | WebApplicationProxyData 0 1 10/24/2017 10:25:36 AM 81 | WebCustomizationResource 0 1 10/24/2017 10:25:36 AM 82 | WebTheme 0 1 10/24/2017 10:25:36 AM 83 | 84 | 85 | The full sync will occur on this AD FS Secondary server during the next normal sync poll (by default it occurs every 5 minutes) 86 | ``` 87 | 88 | 89 | ## Invoke-WidSync Parameters 90 | 91 | __`-Force`__ - Switch that allows for the WID table serial number to be reset, which will force a full WID sync at the next poll interview 92 | 93 | 94 | ## Additional Details 95 | 96 | The full ADFS WID synchronization is obtained indirectly by setting the serial numbers ( `SerialNumber `) associated with each table to zero, which indicates that they should be synchronized. 97 | These serial numbers are referenced in a particular table ( `ServiceStateSummary `) on which the synchronization logic used by AD FS is based. This reset is to be done on the Secondary AD FS server that we wish to synchronize (only). 98 | 99 | Full synchronization will occur during the next normal sync cycle (which occurs every 5 minutes by default). 100 | 101 | Note, the serialnumber reset is only performed when each of the following conditions are true: 102 | * The script is launched explicitly with the option `-Force $true` 103 | * The script is run on a server with the ADFS Secondary role 104 | * The last WID sync status was successful (executed without error) --------------------------------------------------------------------------------