Repository: EvotecIT/PasswordSolution Branch: master Commit: 2d674fe39473 Files: 64 Total size: 506.8 KB Directory structure: gitextract_pdir1fnf/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── Build/ │ └── Manage-Module.ps1 ├── CHANGELOG.MD ├── Docs/ │ ├── Find-Password.md │ ├── Find-PasswordNotification.md │ ├── Find-PasswordQuality.md │ ├── New-PasswordConfigurationEmail.md │ ├── New-PasswordConfigurationOption.md │ ├── New-PasswordConfigurationReport.md │ ├── New-PasswordConfigurationRule.md │ ├── New-PasswordConfigurationRuleReminder.md │ ├── New-PasswordConfigurationTemplate.md │ ├── New-PasswordConfigurationType.md │ ├── Readme.md │ ├── Show-PasswordQuality.md │ └── Start-PasswordSolution.md ├── Examples/ │ ├── Example-EncryptPassword.ps1 │ ├── Example-FindUsers.ps1 │ ├── Example-FindUsersAdvanced.ps1 │ ├── Example-FindUsersEntra.ps1 │ ├── Example-FindUsersPasswordQualityConsole.ps1 │ ├── Example-FindUsersPasswordQualityConsoleWithReplacements.ps1 │ ├── Example-FindUsersPasswordQualityReport.ps1 │ ├── Example-FindUsersPasswordQualityReportWithReplacements.ps1 │ ├── Example-PasswordDashboard.ps1 │ ├── Example-PasswordSolution-Entra.ps1 │ ├── Example-PasswordSolution-LegacyConfiguration01.ps1 │ ├── Example-PasswordSolution-ModernConfiguration01.ps1 │ ├── Example-ReRegisterTaskAsGMSA.ps1 │ ├── Example-SearchNotification.ps1 │ └── WeakPasswordGenerator.ps1 ├── PasswordSolution.psd1 ├── PasswordSolution.psm1 ├── Private/ │ ├── Add-ManagerInformation.ps1 │ ├── Add-ParametersToString.ps1 │ ├── Export-SearchInformation.ps1 │ ├── Fromat-ReminderDays.ps1 │ ├── Import-SearchInformation.ps1 │ ├── Invoke-PasswordRuleProcessing.ps1 │ ├── New-HTMLReport.ps1 │ ├── Send-PasswordAdminNotifications.ps1 │ ├── Send-PasswordEmail.ps1 │ ├── Send-PasswordManagerNotifications.ps1 │ ├── Send-PasswordSecurityNotifications.ps1 │ ├── Send-PasswordUserNotifications.ps1 │ └── Set-PasswordConfiguration.ps1 ├── Public/ │ ├── Find-Password.ps1 │ ├── Find-PasswordEntra.ps1 │ ├── Find-PasswordNotification.ps1 │ ├── Find-PasswordQuality.ps1 │ ├── New-PasswordConfigurationEmail.ps1 │ ├── New-PasswordConfigurationEntra.ps1 │ ├── New-PasswordConfigurationExternalUsers.ps1 │ ├── New-PasswordConfigurationOption.ps1 │ ├── New-PasswordConfigurationReplacement.ps1 │ ├── New-PasswordConfigurationReport.ps1 │ ├── New-PasswordConfigurationRule.ps1 │ ├── New-PasswordConfigurationRuleReminder.ps1 │ ├── New-PasswordConfigurationTemplate.ps1 │ ├── New-PasswordConfigurationType.ps1 │ ├── Show-PasswordQuality.ps1 │ └── Start-PasswordSolution.ps1 └── README.MD ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: PrzemyslawKlys custom: https://paypal.me/PrzemyslawKlys ================================================ FILE: .gitignore ================================================ Ignore/* .vs/* .vscode/* *.html *.log *.xml Artefacts/* ================================================ FILE: Build/Manage-Module.ps1 ================================================ Clear-Host Import-Module "PSPublishModule" -Force Invoke-ModuleBuild -ModuleName 'PasswordSolution' { # Usual defaults as per standard module $Manifest = [ordered] @{ # Version number of this module. ModuleVersion = '2.1.X' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') PowerShellVersion = '5.1' # ID used to uniquely identify this module GUID = 'c58ff818-1de6-4500-961c-a243c2043255' # Author of this module Author = 'Przemyslaw Klys' # Company or vendor of this module CompanyName = 'Evotec' # Copyright statement for this module Copyright = "(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved." # Description of the functionality provided by this module Description = "This module allows the creation of password expiry emails for users, managers, administrators, and security according to defined templates. It's able to work with different rules allowing to fully customize who gets the email and when." # Minimum version of the Windows PowerShell engine required by this module Tags = 'password', 'passwordexpiry', 'activedirectory', 'windows' # A URL to the main website for this project. ProjectUri = 'https://github.com/EvotecIT/PasswordSolution' # A URL to an icon representing this module. IconUri = 'https://evotec.xyz/wp-content/uploads/2022/08/PasswordSolution.png' } New-ConfigurationManifest @Manifest New-ConfigurationModule -Type RequiredModule -Name 'PSSharedGoods', 'PSWriteHTML', 'PSWriteColor' -Guid Auto -Version Latest New-ConfigurationModule -Type RequiredModule -Name 'Mailozaurr' -Guid Auto -Version 1.0.0 New-ConfigurationModule -Type ExternalModule -Name @( #"Microsoft.PowerShell.Management" #"Microsoft.PowerShell.Utility" "ActiveDirectory" ) New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword' New-ConfigurationModuleSkip -IgnoreModuleName @( # Default modules 'NetTCPIP' 'Microsoft.WSMan.Management' # Password Quality 'DSInternals' # Graph Cmdlets 'Microsoft.Graph.Users' 'Microsoft.Graph.Identity.DirectoryManagement' ) -IgnoreFunctionName @( 'Select-Unique', 'Compare-TwoArrays' # those functions are internal within private function # Password Quality 'Get-ADReplAccount' 'Test-PasswordQuality' # Graph Cmdlets 'Get-MgDomain' 'Get-MgUser' ) $ConfigurationFormat = [ordered] @{ RemoveComments = $true RemoveEmptyLines = $true PlaceOpenBraceEnable = $true PlaceOpenBraceOnSameLine = $true PlaceOpenBraceNewLineAfter = $true PlaceOpenBraceIgnoreOneLineBlock = $false PlaceCloseBraceEnable = $true PlaceCloseBraceNewLineAfter = $false PlaceCloseBraceIgnoreOneLineBlock = $true PlaceCloseBraceNoEmptyLineBefore = $false UseConsistentIndentationEnable = $true UseConsistentIndentationKind = 'space' UseConsistentIndentationPipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' UseConsistentIndentationIndentationSize = 4 UseConsistentWhitespaceEnable = $true UseConsistentWhitespaceCheckInnerBrace = $true UseConsistentWhitespaceCheckOpenBrace = $true UseConsistentWhitespaceCheckOpenParen = $true UseConsistentWhitespaceCheckOperator = $true UseConsistentWhitespaceCheckPipe = $true UseConsistentWhitespaceCheckSeparator = $true AlignAssignmentStatementEnable = $true AlignAssignmentStatementCheckHashtable = $true UseCorrectCasingEnable = $true } # format PSD1 and PSM1 files when merging into a single file # enable formatting is not required as Configuration is provided New-ConfigurationFormat -ApplyTo 'OnMergePSM1', 'OnMergePSD1' -Sort None @ConfigurationFormat # format PSD1 and PSM1 files within the module # enable formatting is required to make sure that formatting is applied (with default settings) New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'DefaultPSM1' -EnableFormatting -Sort None # when creating PSD1 use special style without comments and with only required parameters New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal' # configuration for documentation, at the same time it enables documentation processing New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' New-ConfigurationImportModule -ImportSelf New-ConfigurationBuild -Enable:$true -SignModule -MergeModuleOnBuild -MergeFunctionsFromApprovedModules -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' #New-ConfigurationTest -TestsPath "$PSScriptRoot\..\Tests" -Enable New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked" -AddRequiredModules New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -ArtefactName '.v.zip' # options for publishing to github/psgallery #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true } -ExitCode ================================================ FILE: CHANGELOG.MD ================================================ # PasswordSolution Release History ## 2.0.3 - Fix formatting in HTML output - Fix deletion of log files/reports to specific extensions only [#24](https://github.com/EvotecIT/PasswordSolution/issues/24) ## 2.0.2 - 2024.10.01 - Add `SearchBase` when managers are not required, or the scope for both users and managers is very limited (use FilterOrganizationalUnit instead) ## 2.0.1 - 2024.09.30 - Fixes `AdminSection` not working properly for modern configuration - Improve error reporting when sending emails ## 2.0.0 - 2024.09.08 - **EXPERIMENTAL** - Added basic support for Microsoft Entra ID (Azure AD) - Improved reporting allowing to exclude properties from HTML reports to make them smaller - Hide **'Manager', 'ManagerDN', 'MemberOf'** by default in HTML reports (zero out ExcludeProperties to get rid of this behavior) - Made `ScrollX` default to `true` in HTML reports to make them more readable ## 1.3.2 - 2024.08.23 - Improvement on logging - Small improvement to message ## 1.3.1 - 2024.08.23 - Fixes `FilterOrganizationalUnit` not working properly - Add more logs ## 1.3.0 - 2024.08.23 - Added `NotifyOnUserMatchingRuleForManager`, `NotifyOnUserMatchingRuleForManagerButNotCompliant` to `New-PasswordConfigurationOption` to allow for more granular control over logging - Fixes sending emails to managers based on weekdays when using modern setup configuration - Added additional check logic to prevent wrong rules configuration ## 1.2.9 - 2024.08.22 - Fixes logging functionality when using modern settings ## 1.2.8 - 2024.08.22 - Added `FilterOrganizationalUnit` to `New-PasswordConfigurationOption` to allow for filtering users based on OrganizationalUnit This speeds up the process of scanning users, and allows for more granular control over which users are scanned without having to go thru all users in the domain The module still gets all the users but only processes the ones that match the filter - Improved console colors a bit, to prevent some colors from being invisible ```powershell $Options = @{ # Logging to file and to screen ShowTime = $true LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" TimeFormat = "yyyy-MM-dd HH:mm:ss" LogMaximum = 365 NotifyOnSkipUserManagerOnly = $false NotifyOnSecuritySend = $true NotifyOnManagerSend = $true NotifyOnUserSend = $true NotifyOnUserMatchingRule = $false NotifyOnUserDaysToExpireNull = $false SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml" EmailDateFormat = "yyyy-MM-dd" EmailDateFormatUTCConversion = $true FilterOrganizationalUnit = @( "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" "*OU=Administration,DC=ad,DC=evotec,DC=xyz" ) } New-PasswordConfigurationOption @Options ``` ## 1.2.7 - 2024.08.21 - Adds sending email without credentials (using SMTP server) - Fixes `SkipCertificateValidation` typo in `New-PasswordConfigurationEmail` - Small cleanup ## 1.2.6 - 2024.08.12 - Fix wrong publish of cmdlets ## 1.2.5 - 2024.07.16 - Remove duplicate records from external managers ## 1.2.4 - 2024.07.15 - Fixes email address being wrong when using external system and using overwrite property ## 1.2.3 - 2024.06.23 - Small report improvements ## 1.2.2 - 2024.06.23 - Add reporting for replacements of emails from external sources ## 1.2.1 - 2024.06.23 - Allow using DSL and normal configuration at the same time ## 1.2.0 - 2024.06.23 - This version adds ability to allow overwritting email address from external system, or even CSV records based on prepared data - Added `New-PasswordConfigurationExternalUsers` to allow for overwritting emails with external data in form of array of objects ## 1.1.1 - 2024.01.16 - Small improvement to error message being provided when sending email fails with summary of emails ## 1.1.0 - 2023.11.12 - Add support for weak password hashes (NTLM) in Password Quality Check - Add new parameters for `Find-PasswordQuality` - Add new parameters for `Show-PasswordQuality` ## 1.0.5 - 2023.10.18 - I can't type apparently so I fixed typo in code ## 1.0.3 - 2023.06.12 - Resolves issue with scanning **Active Directory** without exchange attributes ## 1.0.2 - 2023.06.06 - Improves `New-PasswordConfigurationRuleReminder` by allowing: `New-PasswordConfigurationRuleReminder -Type 'Manager' -ExpirationDays @(-200..-1), 0, 1, 2, 3, 7, 15, 30, 60 -ComparisonType eq` configuration ## 1.0.1 - 2023.06.01 - Fixes AdminSection not working properly ## 1.0.0 - 2023.05.25 - Improves exclusions https://github.com/EvotecIT/PasswordSolution/issues/7 - Simplifies configuration https://github.com/EvotecIT/PasswordSolution/issues/8 - Improves configuration https://github.com/EvotecIT/PasswordSolution/issues/3 - Improves configuration https://github.com/EvotecIT/PasswordSolution/issues/6 - Improve HTML report to be better in naming things - Allow for overwriting manager field with different properties based on SamAccountName/DN - New configuration option for Report (NestedRules), making separate tab for all rules in HTML - Small documentation updates - Adds defaults for templates meaning it's possible to skip their definitions although not very useful if you want personalized thing for your company users This actually means: - We now support DSL language for configuration (see examples) - We now support much easier way of building configuration - We now support overwrite email property per rule (global still works) - We now support overwrite manager property per rule (global still works) ## 0.0.37 - 2023.04.25 - Improve `Maps` in Password Quality - Improve colors, add column to a report ## 0.0.36 - 2023.04.24 - Added `Maps` to Password Quality - Improved reporting in Password Quality with countries for Weak Passwords and Duplicate Groups - Added Logs to Password Quality - Minor improvements to reporting - Make report much smaller ## 0.0.35 - 2023.04.19 - Small fix to report ## 0.0.34 - 2023.04.19 - Fixes reporting of quality passwords in overview, charts - Removed tables for Duplicate Groups in favor of single table (1000 tabs don't seem to work in HTML 🤯) ## 0.0.33 - 2023.04.18 - General improvements - Added separate duplicate groups tables to report for easier visibility - Prefer writable DCs when quering AD ## 0.0.32 - 2023.01.18 - Removed DSInternals from required modules (made it optional / disables Password Quality Check). This is to not trigger any security alerts in your environment if you just want to use the module for password expiration and not password quality checks. ## 0.0.31 - 2023.01.17 - Small updates to logging ## 0.0.30 - 2023.01.17 - Add ability to check password quality (requires higher permissions) and based on DSInternals ## 0.0.29 - 2022.10.11 - Fixes to report **ShowSkippedUsers** to skip Contacts ## 0.0.28 - Fixes to report **All Users** to skip Contacts - Fixes to report conditional formatting which would format unnessecary fields ## 0.0.26 - Remove Body from **EmailConfiguration** in HTML Report - Fixes an issue with table in reports having borders (from the EmailBody) ## 0.0.25 - Fix ManagerStatus when Overwriteproperty is in use - Add HTML report to Email to Admins ```powershell HTMLReports = @( # Accepts a list of reports to generate. Can be multiple reprorts having different sections, or just one having it all [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $true ShowSkippedLocations = $true ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true # new option } ) ``` ## 0.0.24 - Bump dependencies of PSWriteHTML, Mailozaurr to newest versions - Add support for managers of users being a Contact (with an email field) ## 0.0.23 - Small cleanup - Docs update ## 0.0.22 - Fixes dashboard not showing some objects - Added properties describing which rule was used to find user - Added some additional logging ================================================ FILE: Docs/Find-Password.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # Find-Password ## SYNOPSIS Scan Active Directory forest for all users and their password expiration date ## SYNTAX ``` Find-Password [[-Forest] ] [[-ExcludeDomains] ] [[-IncludeDomains] ] [[-ExtendedForestInformation] ] [[-OverwriteEmailProperty] ] [[-ReturnObjectsType] ] [[-OverwriteManagerProperty] ] [] ``` ## DESCRIPTION Scan Active Directory forest for all users and their password expiration date ## EXAMPLES ### EXAMPLE 1 ``` Find-Password | ft ``` ## PARAMETERS ### -Forest Target different Forest, by default current forest is used ```yaml Type: String Parameter Sets: (All) Aliases: ForestName Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeDomains Exclude domain from search, by default whole forest is scanned ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeDomains Include only specific domains, by default whole forest is scanned ```yaml Type: String[] Parameter Sets: (All) Aliases: Domain, Domains Required: False Position: 3 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing ```yaml Type: IDictionary Parameter Sets: (All) Aliases: Required: False Position: 4 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteEmailProperty Overwrite EmailAddress property with different property name ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 5 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ReturnObjectsType {{ Fill ReturnObjectsType Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 7 Default value: @('Users', 'Contacts') Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteManagerProperty Overwrite Manager property with different property name. Can use DistinguishedName or SamAccountName ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 10 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ## OUTPUTS ## NOTES General notes ## RELATED LINKS ================================================ FILE: Docs/Find-PasswordNotification.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # Find-PasswordNotification ## SYNOPSIS Searches thru XML logs created by Password Solution ## SYNTAX ``` Find-PasswordNotification [-SearchPath] [-Manager] [] ``` ## DESCRIPTION Searches thru XML logs created by Password Solution ## EXAMPLES ### EXAMPLE 1 ``` Find-PasswordNotification -SearchPath $PSScriptRoot\Search\SearchLog.xml | Format-Table ``` ### EXAMPLE 2 ``` Find-PasswordNotification -SearchPath "$PSScriptRoot\Search\SearchLog_2021-06.xml" -Manager | Format-Table ``` ## PARAMETERS ### -SearchPath Path to file where the XML log is located ```yaml Type: String Parameter Sets: (All) Aliases: Required: True Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Manager Search thru manager escalations ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ## OUTPUTS ## NOTES General notes ## RELATED LINKS ================================================ FILE: Docs/Find-PasswordQuality.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # Find-PasswordQuality ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` Find-PasswordQuality [[-WeakPasswords] ] [-IncludeStatistics] [[-Forest] ] [[-ExcludeDomains] ] [[-IncludeDomains] ] [[-ExtendedForestInformation] ] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -WeakPasswords {{ Fill WeakPasswords Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeStatistics {{ Fill IncludeStatistics Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Forest Target different Forest, by default current forest is used ```yaml Type: String Parameter Sets: (All) Aliases: ForestName Required: False Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeDomains Exclude domain from search, by default whole forest is scanned ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 3 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeDomains Include only specific domains, by default whole forest is scanned ```yaml Type: String[] Parameter Sets: (All) Aliases: Domain, Domains Required: False Position: 4 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing ```yaml Type: IDictionary Parameter Sets: (All) Aliases: Required: False Position: 5 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationEmail.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationEmail ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ### Compatibility ``` New-PasswordConfigurationEmail [-Server ] [-Port ] -From [-ReplyTo ] [-Priority ] [-DeliveryNotificationOption ] [-DeliveryStatusNotificationType ] [-Credential ] [-SecureSocketOptions ] [-UseSsl] [-SkipCertificateRevocation] [-SkipCertificateValidatation] [-Timeout ] [-LocalDomain ] [-WhatIf] [-Confirm] [] ``` ### oAuth ``` New-PasswordConfigurationEmail [-Server ] [-Port ] -From [-ReplyTo ] [-Priority ] [-DeliveryNotificationOption ] [-DeliveryStatusNotificationType ] [-Credential ] [-SecureSocketOptions ] [-UseSsl] [-SkipCertificateRevocation] [-SkipCertificateValidatation] [-Timeout ] [-oAuth2] [-LocalDomain ] [-WhatIf] [-Confirm] [] ``` ### SecureString ``` New-PasswordConfigurationEmail [-Server ] [-Port ] -From [-ReplyTo ] [-Priority ] [-DeliveryNotificationOption ] [-DeliveryStatusNotificationType ] [-Username ] [-Password ] [-SecureSocketOptions ] [-UseSsl] [-SkipCertificateRevocation] [-SkipCertificateValidatation] [-Timeout ] [-AsSecureString] [-LocalDomain ] [-WhatIf] [-Confirm] [] ``` ### SendGrid ``` New-PasswordConfigurationEmail -From [-ReplyTo ] [-Priority ] -Credential [-SendGrid] [-SeparateTo] [-WhatIf] [-Confirm] [] ``` ### MgGraphRequest ``` New-PasswordConfigurationEmail -From [-ReplyTo ] [-Priority ] [-RequestReadReceipt] [-RequestDeliveryReceipt] [-Graph] [-MgGraphRequest] [-DoNotSaveToSentItems] [-WhatIf] [-Confirm] [] ``` ### Graph ``` New-PasswordConfigurationEmail -From [-ReplyTo ] [-Priority ] -Credential [-RequestReadReceipt] [-RequestDeliveryReceipt] [-Graph] [-DoNotSaveToSentItems] [-WhatIf] [-Confirm] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -AsSecureString {{ Fill AsSecureString Description }} ```yaml Type: SwitchParameter Parameter Sets: SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Confirm Prompts you for confirmation before running the cmdlet. ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: cf Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Credential {{ Fill Credential Description }} ```yaml Type: PSCredential Parameter Sets: Compatibility, oAuth Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ```yaml Type: PSCredential Parameter Sets: SendGrid, Graph Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DeliveryNotificationOption {{ Fill DeliveryNotificationOption Description }} ```yaml Type: String[] Parameter Sets: Compatibility, oAuth, SecureString Aliases: Accepted values: None, OnSuccess, OnFailure, Delay, Never Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DeliveryStatusNotificationType {{ Fill DeliveryStatusNotificationType Description }} ```yaml Type: DeliveryStatusNotificationType Parameter Sets: Compatibility, oAuth, SecureString Aliases: Accepted values: Unspecified, Full, HeadersOnly Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DoNotSaveToSentItems {{ Fill DoNotSaveToSentItems Description }} ```yaml Type: SwitchParameter Parameter Sets: MgGraphRequest, Graph Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -From {{ Fill From Description }} ```yaml Type: Object Parameter Sets: (All) Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Graph {{ Fill Graph Description }} ```yaml Type: SwitchParameter Parameter Sets: MgGraphRequest, Graph Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -LocalDomain {{ Fill LocalDomain Description }} ```yaml Type: String Parameter Sets: Compatibility, oAuth, SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -MgGraphRequest {{ Fill MgGraphRequest Description }} ```yaml Type: SwitchParameter Parameter Sets: MgGraphRequest Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Password {{ Fill Password Description }} ```yaml Type: String Parameter Sets: SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Port {{ Fill Port Description }} ```yaml Type: Int32 Parameter Sets: Compatibility, oAuth, SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Priority {{ Fill Priority Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Importance Accepted values: Low, Normal, High Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ReplyTo {{ Fill ReplyTo Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -RequestDeliveryReceipt {{ Fill RequestDeliveryReceipt Description }} ```yaml Type: SwitchParameter Parameter Sets: MgGraphRequest, Graph Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -RequestReadReceipt {{ Fill RequestReadReceipt Description }} ```yaml Type: SwitchParameter Parameter Sets: MgGraphRequest, Graph Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SecureSocketOptions {{ Fill SecureSocketOptions Description }} ```yaml Type: SecureSocketOptions Parameter Sets: Compatibility, oAuth, SecureString Aliases: Accepted values: None, Auto, SslOnConnect, StartTls, StartTlsWhenAvailable Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SendGrid {{ Fill SendGrid Description }} ```yaml Type: SwitchParameter Parameter Sets: SendGrid Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SeparateTo {{ Fill SeparateTo Description }} ```yaml Type: SwitchParameter Parameter Sets: SendGrid Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Server {{ Fill Server Description }} ```yaml Type: String Parameter Sets: Compatibility, oAuth, SecureString Aliases: SmtpServer Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SkipCertificateRevocation {{ Fill SkipCertificateRevocation Description }} ```yaml Type: SwitchParameter Parameter Sets: Compatibility, oAuth, SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SkipCertificateValidatation {{ Fill SkipCertificateValidatation Description }} ```yaml Type: SwitchParameter Parameter Sets: Compatibility, oAuth, SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Timeout {{ Fill Timeout Description }} ```yaml Type: Int32 Parameter Sets: Compatibility, oAuth, SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -UseSsl {{ Fill UseSsl Description }} ```yaml Type: SwitchParameter Parameter Sets: Compatibility, oAuth, SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Username {{ Fill Username Description }} ```yaml Type: String Parameter Sets: SecureString Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: wi Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -oAuth2 {{ Fill oAuth2 Description }} ```yaml Type: SwitchParameter Parameter Sets: oAuth Aliases: oAuth Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationOption.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationOption ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` New-PasswordConfigurationOption [-ShowTime] [[-LogFile] ] [[-TimeFormat] ] [[-LogMaximum] ] [-NotifyOnSkipUserManagerOnly] [-NotifyOnSecuritySend] [-NotifyOnManagerSend] [-NotifyOnUserSend] [-NotifyOnUserMatchingRule] [-NotifyOnUserDaysToExpireNull] [[-SearchPath] ] [[-EmailDateFormat] ] [-EmailDateFormatUTCConversion] [[-OverwriteEmailProperty] ] [[-OverwriteManagerProperty] ] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -EmailDateFormat {{ Fill EmailDateFormat Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 4 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -EmailDateFormatUTCConversion {{ Fill EmailDateFormatUTCConversion Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -LogFile {{ Fill LogFile Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 0 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -LogMaximum {{ Fill LogMaximum Description }} ```yaml Type: Int32 Parameter Sets: (All) Aliases: Required: False Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NotifyOnManagerSend {{ Fill NotifyOnManagerSend Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NotifyOnSecuritySend {{ Fill NotifyOnSecuritySend Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NotifyOnSkipUserManagerOnly {{ Fill NotifyOnSkipUserManagerOnly Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NotifyOnUserDaysToExpireNull {{ Fill NotifyOnUserDaysToExpireNull Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NotifyOnUserMatchingRule {{ Fill NotifyOnUserMatchingRule Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NotifyOnUserSend {{ Fill NotifyOnUserSend Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteEmailProperty {{ Fill OverwriteEmailProperty Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 5 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteManagerProperty {{ Fill OverwriteManagerProperty Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 6 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SearchPath {{ Fill SearchPath Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 3 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowTime {{ Fill ShowTime Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TimeFormat {{ Fill TimeFormat Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationReport.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationReport ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` New-PasswordConfigurationReport [-Enable] [-ShowHTML] [[-Title] ] [-Online] [-DisableWarnings] [-ShowConfiguration] [-ShowAllUsers] [-ShowRules] [-ShowUsersSent] [-ShowManagersSent] [-ShowEscalationSent] [-ShowSkippedUsers] [-ShowSkippedLocations] [-ShowSearchUsers] [-ShowSearchManagers] [-ShowSearchEscalations] [[-FilePath] ] [-AttachToEmail] [-NestedRules] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -Enable {{ Fill Enable Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowHTML {{ Fill ShowHTML Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Title {{ Fill Title Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Online {{ Fill Online Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DisableWarnings {{ Fill DisableWarnings Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowConfiguration {{ Fill ShowConfiguration Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowAllUsers {{ Fill ShowAllUsers Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowRules {{ Fill ShowRules Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowUsersSent {{ Fill ShowUsersSent Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowManagersSent {{ Fill ShowManagersSent Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowEscalationSent {{ Fill ShowEscalationSent Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowSkippedUsers {{ Fill ShowSkippedUsers Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowSkippedLocations {{ Fill ShowSkippedLocations Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowSearchUsers {{ Fill ShowSearchUsers Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowSearchManagers {{ Fill ShowSearchManagers Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ShowSearchEscalations {{ Fill ShowSearchEscalations Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -FilePath {{ Fill FilePath Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -AttachToEmail {{ Fill AttachToEmail Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -NestedRules Specifies whether to display nested password rules. Each rule has it's own tab with output. Having many rules and all other settings enabled can result in a very long list of tabs that's hard to navigate. This setting forces separate tab for all rules. The default value is $false. ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationRule.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationRule ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` New-PasswordConfigurationRule [[-ReminderConfiguration] ] [-Name] [-Enable] [-IncludeExpiring] [-IncludePasswordNeverExpires] [[-PasswordNeverExpiresDays] ] [[-IncludeNameProperties] ] [[-IncludeName] ] [[-ExcludeNameProperties] ] [[-ExcludeName] ] [[-IncludeOU] ] [[-ExcludeOU] ] [[-IncludeGroup] ] [[-ExcludeGroup] ] [-ReminderDays] [-ManagerReminder] [-ManagerNotCompliant] [[-ManagerNotCompliantDisplayName] ] [[-ManagerNotCompliantEmailAddress] ] [-ManagerNotCompliantDisabled] [-ManagerNotCompliantMissing] [-ManagerNotCompliantMissingEmail] [[-ManagerNotCompliantLastLogonDays] ] [-SecurityEscalation] [[-SecurityEscalationDisplayName] ] [[-SecurityEscalationEmailAddress] ] [[-OverwriteEmailProperty] ] [[-OverwriteManagerProperty] ] [-ProcessManagersOnly] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -ReminderConfiguration {{ Fill ReminderConfiguration Description }} ```yaml Type: ScriptBlock Parameter Sets: (All) Aliases: Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Name {{ Fill Name Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: True Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Enable {{ Fill Enable Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeExpiring {{ Fill IncludeExpiring Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludePasswordNeverExpires {{ Fill IncludePasswordNeverExpires Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -PasswordNeverExpiresDays {{ Fill PasswordNeverExpiresDays Description }} ```yaml Type: Int32 Parameter Sets: (All) Aliases: Required: False Position: 3 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeNameProperties {{ Fill IncludeNameProperties Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 4 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeName {{ Fill IncludeName Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 5 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeNameProperties Exclude user from rule if any of the properties match the value as defined in ExcludeName ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 6 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeName Exclude user from rule if any of the properties match the value of Name in the properties defined in ExcludeNameProperties ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 7 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeOU {{ Fill IncludeOU Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 8 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeOU {{ Fill ExcludeOU Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 9 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeGroup Parameter description ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 10 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeGroup Parameter description ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 11 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ReminderDays Parameter description ```yaml Type: Array Parameter Sets: (All) Aliases: ExpirationDays, Days Required: True Position: 12 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerReminder {{ Fill ManagerReminder Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliant {{ Fill ManagerNotCompliant Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliantDisplayName {{ Fill ManagerNotCompliantDisplayName Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 13 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliantEmailAddress {{ Fill ManagerNotCompliantEmailAddress Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 14 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliantDisabled {{ Fill ManagerNotCompliantDisabled Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliantMissing {{ Fill ManagerNotCompliantMissing Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliantMissingEmail {{ Fill ManagerNotCompliantMissingEmail Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerNotCompliantLastLogonDays {{ Fill ManagerNotCompliantLastLogonDays Description }} ```yaml Type: Int32 Parameter Sets: (All) Aliases: Required: False Position: 15 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SecurityEscalation {{ Fill SecurityEscalation Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SecurityEscalationDisplayName {{ Fill SecurityEscalationDisplayName Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 16 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SecurityEscalationEmailAddress {{ Fill SecurityEscalationEmailAddress Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 17 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteEmailProperty Parameter description ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 18 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteManagerProperty Parameter description ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 19 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ProcessManagersOnly This parameters is used to process users, but only managers will be notified. Sending emails to users within the rule will be skipped completly. This is useful if users would have email addresses, that would normally trigger an email to them. ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationRuleReminder.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationRuleReminder ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ### Daily (Default) ``` New-PasswordConfigurationRuleReminder -Type -ExpirationDays [-ComparisonType ] [] ``` ### DayOfMonth ``` New-PasswordConfigurationRuleReminder -Type -ExpirationDays -DayOfMonth [-ComparisonType ] [] ``` ### DayOfWeek ``` New-PasswordConfigurationRuleReminder -Type -ExpirationDays -DayOfWeek [-ComparisonType ] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -ComparisonType {{ Fill ComparisonType Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Accepted values: lt, gt, eq, in Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DayOfMonth {{ Fill DayOfMonth Description }} ```yaml Type: Array Parameter Sets: DayOfMonth Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DayOfWeek {{ Fill DayOfWeek Description }} ```yaml Type: Array Parameter Sets: DayOfWeek Aliases: Accepted values: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExpirationDays {{ Fill ExpirationDays Description }} ```yaml Type: Array Parameter Sets: (All) Aliases: ConditionDays, Days Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Type {{ Fill Type Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Accepted values: Manager, ManagerNotCompliant, Security Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationTemplate.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationTemplate ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` New-PasswordConfigurationTemplate [-Template] [-Subject] [-Type] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -Subject {{ Fill Subject Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: True Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Template {{ Fill Template Description }} ```yaml Type: ScriptBlock Parameter Sets: (All) Aliases: Required: True Position: 0 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Type {{ Fill Type Description }} ```yaml Type: Object Parameter Sets: (All) Aliases: Accepted values: PreExpiry, PostExpiry, Manager, ManagerNotCompliant, Security, Admin Required: True Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/New-PasswordConfigurationType.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # New-PasswordConfigurationType ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` New-PasswordConfigurationType [-Type] [-Enable] [[-SendCountMaximum] ] [[-DefaultEmail] ] [-AttachCSV] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -AttachCSV {{ Fill AttachCSV Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DefaultEmail {{ Fill DefaultEmail Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Enable {{ Fill Enable Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SendCountMaximum {{ Fill SendCountMaximum Description }} ```yaml Type: Int32 Parameter Sets: (All) Aliases: Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Type {{ Fill Type Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Accepted values: User, Manager, Security, Admin Required: True Position: 0 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/Readme.md ================================================ --- Module Name: PasswordSolution Module Guid: c58ff818-1de6-4500-961c-a243c2043255 Download Help Link: {{ Update Download Link }} Help Version: {{ Please enter version of help manually (X.X.X.X) format }} Locale: en-US --- # PasswordSolution Module ## Description PasswordSolution is a PowerShell module that provides Password Expiry notifications to users, managers, security and administrators. It's very configurable and was designed for enterprise use. ## PasswordSolution Cmdlets ### [Find-Password](Find-Password.md) Scan Active Directory forest for all users and their password expiration date ### [Find-PasswordNotification](Find-PasswordNotification.md) Searches thru XML logs created by Password Solution ### [Find-PasswordQuality](Find-PasswordQuality.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationEmail](New-PasswordConfigurationEmail.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationOption](New-PasswordConfigurationOption.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationReport](New-PasswordConfigurationReport.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationRule](New-PasswordConfigurationRule.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationRuleReminder](New-PasswordConfigurationRuleReminder.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationTemplate](New-PasswordConfigurationTemplate.md) {{ Fill in the Synopsis }} ### [New-PasswordConfigurationType](New-PasswordConfigurationType.md) {{ Fill in the Synopsis }} ### [Show-PasswordQuality](Show-PasswordQuality.md) {{ Fill in the Synopsis }} ### [Start-PasswordSolution](Start-PasswordSolution.md) Starts Password Expiry Notifications for the whole forest ================================================ FILE: Docs/Show-PasswordQuality.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # Show-PasswordQuality ## SYNOPSIS {{ Fill in the Synopsis }} ## SYNTAX ``` Show-PasswordQuality [[-Forest] ] [[-ExcludeDomains] ] [[-IncludeDomains] ] [[-ExtendedForestInformation] ] [[-FilePath] ] [-DontShow] [-Online] [[-WeakPasswords] ] [-SeparateDuplicateGroups] [-PassThru] [-AddWorldMap] [[-LogPath] ] [[-LogMaximum] ] [-LogShowTime] [[-LogTimeFormat] ] [] ``` ## DESCRIPTION {{ Fill in the Description }} ## EXAMPLES ### Example 1 ```powershell PS C:\> {{ Add example code here }} ``` {{ Add example description here }} ## PARAMETERS ### -Forest {{ Fill Forest Description }} ```yaml Type: String Parameter Sets: (All) Aliases: ForestName Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExcludeDomains {{ Fill ExcludeDomains Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 2 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -IncludeDomains {{ Fill IncludeDomains Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Domain, Domains Required: False Position: 3 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ExtendedForestInformation {{ Fill ExtendedForestInformation Description }} ```yaml Type: IDictionary Parameter Sets: (All) Aliases: Required: False Position: 4 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -FilePath {{ Fill FilePath Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 5 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -DontShow {{ Fill DontShow Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Online {{ Fill Online Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -WeakPasswords {{ Fill WeakPasswords Description }} ```yaml Type: String[] Parameter Sets: (All) Aliases: Required: False Position: 6 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SeparateDuplicateGroups If specified, report will show duplicate groups separately, one group per tab. ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### -PassThru {{ Fill PassThru Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### -AddWorldMap {{ Fill AddWorldMap Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### -LogPath {{ Fill LogPath Description }} ```yaml Type: String Parameter Sets: (All) Aliases: LogFile Required: False Position: 7 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -LogMaximum {{ Fill LogMaximum Description }} ```yaml Type: Int32 Parameter Sets: (All) Aliases: Required: False Position: 8 Default value: 0 Accept pipeline input: False Accept wildcard characters: False ``` ### -LogShowTime {{ Fill LogShowTime Description }} ```yaml Type: SwitchParameter Parameter Sets: (All) Aliases: Required: False Position: Named Default value: False Accept pipeline input: False Accept wildcard characters: False ``` ### -LogTimeFormat {{ Fill LogTimeFormat Description }} ```yaml Type: String Parameter Sets: (All) Aliases: Required: False Position: 9 Default value: Yyyy-MM-dd HH:mm:ss Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ### None ## OUTPUTS ### System.Object ## NOTES ## RELATED LINKS ================================================ FILE: Docs/Start-PasswordSolution.md ================================================ --- external help file: PasswordSolution-help.xml Module Name: PasswordSolution online version: schema: 2.0.0 --- # Start-PasswordSolution ## SYNOPSIS Starts Password Expiry Notifications for the whole forest ## SYNTAX ### DSL (Default) ``` Start-PasswordSolution [[-ConfigurationDSL] ] [] ``` ### Legacy ``` Start-PasswordSolution -EmailParameters [-OverwriteEmailProperty ] [-OverwriteManagerProperty ] -UserSection -ManagerSection -SecuritySection -AdminSection -Rules [-TemplatePreExpiry ] [-TemplatePreExpirySubject ] [-TemplatePostExpiry ] [-TemplatePostExpirySubject ] -TemplateManager -TemplateManagerSubject -TemplateSecurity -TemplateSecuritySubject -TemplateManagerNotCompliant -TemplateManagerNotCompliantSubject -TemplateAdmin -TemplateAdminSubject [-Logging ] [-HTMLReports ] [-SearchPath ] [] ``` ## DESCRIPTION Starts Password Expiry Notifications for the whole forest ## EXAMPLES ### EXAMPLE 1 ``` An example ``` ## PARAMETERS ### -ConfigurationDSL Parameter description ```yaml Type: ScriptBlock Parameter Sets: DSL Aliases: Required: False Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -EmailParameters Parameters for Email. Uses Mailozaurr splatting behind the scenes, so it supports all options that Mailozaurr does. ```yaml Type: IDictionary Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteEmailProperty Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address. ```yaml Type: String Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -OverwriteManagerProperty {{ Fill OverwriteManagerProperty Description }} ```yaml Type: String Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -UserSection Parameter description ```yaml Type: IDictionary Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -ManagerSection Parameter description ```yaml Type: IDictionary Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SecuritySection Parameter description ```yaml Type: IDictionary Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -AdminSection Parameter description ```yaml Type: IDictionary Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Rules Parameter description ```yaml Type: Array Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplatePreExpiry Parameter description ```yaml Type: ScriptBlock Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplatePreExpirySubject Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplatePostExpiry Parameter description ```yaml Type: ScriptBlock Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplatePostExpirySubject Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateManager Parameter description ```yaml Type: ScriptBlock Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateManagerSubject Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateSecurity Parameter description ```yaml Type: ScriptBlock Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateSecuritySubject Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateManagerNotCompliant Parameter description ```yaml Type: ScriptBlock Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateManagerNotCompliantSubject Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateAdmin Parameter description ```yaml Type: ScriptBlock Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -TemplateAdminSubject Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: True Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -Logging Parameter description ```yaml Type: IDictionary Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -HTMLReports Parameter description ```yaml Type: Array Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### -SearchPath Parameter description ```yaml Type: String Parameter Sets: Legacy Aliases: Required: False Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS ## OUTPUTS ## NOTES General notes ## RELATED LINKS ================================================ FILE: Examples/Example-EncryptPassword.ps1 ================================================ (Get-Credential -UserName 'test' -Message 'Password').Password | ConvertFrom-SecureString | Set-Clipboard ================================================ FILE: Examples/Example-FindUsers.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $Users = Find-Password -OverwriteEmailProperty 'extensionAttribute7' -OverwriteManagerProperty extensionAttribute1 -FilterOrganizationalUnit @( "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" ) $Users | Format-Table UserPrincipalName, Name, Domain, Type, SamAccountName, OrganizationalUnit, Manager, ManagerEmail, ManagerStatus #$Users | Sort-Object -Property Manager | Format-Table Name, Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus, ManagerLastLogonDays, ManagerType, Domain, UserPrincipalName ================================================ FILE: Examples/Example-FindUsersAdvanced.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $RuleProperties = @( 'ExtensionAttribute7' 'extensionAttribute7' 'extensionAttribute8' ) $Users = Find-Password -OverwriteEmailProperty 'extensionAttribute7' -RulesProperties $RuleProperties #| Where-Object { $_.Name -eq 'Test Contact' } #$Users | Format-Table Name, Extension*, DateExpiry #Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus, ManagerLastLogonDays, ManagerType, Domain, UserPrincipalName $Users | Select-Object -First 5 | Format-Table Name, Extension*, DateExpiry, LastLogonDate, LastLogonDays ================================================ FILE: Examples/Example-FindUsersEntra.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force Connect-MgGraph -Scopes "User.Read.All" -NoWelcome $Users = Find-PasswordEntra #-Verbose #$Users | Format-Table $Users | Out-HtmlView -ScrollX -Filtering -DataStore JavaScript { New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Value $True -BackgroundColor TeaGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'IsLicensed' -ComparisonType string -Value $True -BackgroundColor TeaGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'IsSynchronized' -ComparisonType string -Value $True -BackgroundColor TeaGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordExpired' -ComparisonType string -Value $false -BackgroundColor TeaGreen -FailBackgroundColor Salmon } -DisablePaging ================================================ FILE: Examples/Example-FindUsersPasswordQualityConsole.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $Users = Find-PasswordQuality $Users | Format-Table #$Users = Find-PasswordQuality -IncludeDomains 'ad.evotec.pl' #$Users | Format-Table ================================================ FILE: Examples/Example-FindUsersPasswordQualityConsoleWithReplacements.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $Replacements = @( New-PasswordConfigurationReplacement -PropertyName 'Country' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' 'AT' = 'Austria' 'IT' = 'Italy' 'Unknown' = 'Not specified in AD' } -OverwritePropertyName 'CountryCode' ) $Users = Find-PasswordQuality -Replacements $Replacements $Users | Format-Table ================================================ FILE: Examples/Example-FindUsersPasswordQualityReport.ps1 ================================================ Clear-Host Import-Module .\PasswordSolution.psd1 -Force # option 1, one-liner # Show-PasswordQuality -FilePath C:\Temp\PasswordQuality.html -Online -WeakPasswords "Test1", "Test2", "Test3" -Verbose -SeparateDuplicateGroups -AddWorldMap -PassThru # option 2, for easier reading with splatting $showPasswordQualitySplat = @{ FilePath = "$PSScriptRoot\Reporting\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" WeakPasswords = "Test1", "Test2", "Test3", 'February2023!#!@ok', $Passwords | ForEach-Object { $_ } WeakPasswordsHashesFile = 'C:\Support\GitHub\PwnedDatabaseDownloader\pwnedpasswords_ntlm.txt' WeakPasswordsHashesSortedFile = 'C:\Support\GitHub\PwnedDatabaseDownloader\pwnedpasswords_ntlm.txt' SeparateDuplicateGroups = $true PassThru = $true AddWorldMap = $true LogPath = "$PSScriptRoot\Logs\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).log" Online = $true LogMaximum = 5 } Show-PasswordQuality @showPasswordQualitySplat -Verbose ================================================ FILE: Examples/Example-FindUsersPasswordQualityReportWithReplacements.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $showPasswordQualitySplat = @{ FilePath = "$PSScriptRoot\Reporting\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" WeakPasswords = "Test1", "Test2", "Test3", 'February2023!#!@ok', $Passwords | ForEach-Object { $_ } SeparateDuplicateGroups = $true PassThru = $true AddWorldMap = $true LogPath = "$PSScriptRoot\Logs\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).log" Online = $true LogMaximum = 5 Replacements = New-PasswordConfigurationReplacement -PropertyName 'ExtensionAttribute4' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' 'AT' = 'Austria' 'IT' = 'Italy' 'Unknown' = 'Not specified in AD' } -OverwritePropertyName 'CountriesByExtension' GroupBy = 'DelegatableAdmins' } Show-PasswordQuality @showPasswordQualitySplat -Verbose ================================================ FILE: Examples/Example-PasswordDashboard.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $Passwords = Find-Password New-HTML { New-TableOption -DataStore JavaScript -ArrayJoin -BoolAsString New-HTMLTable -DataTable $Passwords -SearchBuilder -Filtering { } } -ShowHTML ================================================ FILE: Examples/Example-PasswordSolution-Entra.ps1 ================================================ Clear-Host Import-Module .\PasswordSolution.psd1 -Force $Date = Get-Date Connect-MgGraph -Scopes "User.Read.All" -NoWelcome Start-PasswordSolution { $Options = @{ # Logging to file and to screen ShowTime = $false LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" TimeFormat = "yyyy-MM-dd HH:mm:ss" LogMaximum = 365 NotifyOnSkipUserManagerOnly = $true NotifyOnSecuritySend = $true NotifyOnManagerSend = $true NotifyOnUserSend = $true NotifyOnUserMatchingRule = $true NotifyOnUserDaysToExpireNull = $true NotifyOnUserMatchingRuleForManager = $true NotifyOnUserMatchingRuleForManagerButNotCompliant = $true SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml" EmailDateFormat = "yyyy-MM-dd" EmailDateFormatUTCConversion = $true # FilterOrganizationalUnit = @( # "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" # "*OU=Administration,DC=ad,DC=evotec,DC=xyz" # ) } New-PasswordConfigurationOption @Options New-PasswordConfigurationEntra -Enable $GraphCredentials = @{ ClientID = '0fb383f1-8bfe-4c68-8ce2-5f6aa1d602fe' DirectoryID = 'ceb371f6-8745-4876-a040-69f2d10a9d1a' ClientSecret = Get-Content -Raw -LiteralPath "C:\Support\Important\O365-GraphEmailTestingKey.txt" } # (full support for Mailozaurr parameters) $EmailParameters = @{ Credential = ConvertTo-GraphCredential -ClientID $GraphCredentials.ClientID -ClientSecret $GraphCredentials.ClientSecret -DirectoryID $GraphCredentials.DirectoryID Graph = $true Priority = 'Normal' From = 'przemyslaw.klys@test.pl' WhatIf = $true ReplyTo = 'contact+testgithub@test.pl' } New-PasswordConfigurationEmail @EmailParameters # Configure behavior for different types of actions New-PasswordConfigurationType -Type User -Enable -SendCountMaximum 10 -DefaultEmail 'przemyslaw.klys+testgithub1@evotec.pl' New-PasswordConfigurationType -Type Manager -Enable -SendCountMaximum 10 -DefaultEmail 'przemyslaw.klys+testgithub2@evotec.pl' New-PasswordConfigurationType -Type Security -Enable -SendCountMaximum 1 -DefaultEmail 'przemyslaw.klys+testgithub3@evotec.pl' #-AttachCSV # Configure reporting $Report = [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $false ShowSkippedLocations = $false ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true NestedRules = $false FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true } New-PasswordConfigurationReport @Report # Configure rules for different types of users # New-PasswordConfigurationRule -Name 'Administrative Accounts' -Enable -IncludeExpiring -IncludePasswordNeverExpires -PasswordNeverExpiresDays 90 { # # follow expiration days of a user # New-PasswordConfigurationRuleReminder -Type 'Manager' # # use a custom expiration days, and send only on specific days 1st, 10th and 15th of a month # New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfMonth 1, 10, 15 -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 # # use a custom expiration days (only if it's less then 10 days left), and send only on specific days of a week # New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfWeek Monday, Wednesday, Friday -ExpirationDays 10 -ComparisonType 'lt' # } -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 -IncludeNameProperties 'SamAccountName' -IncludeName = @( # "ADM_*" # "SADM_*" # "PADM_*" # "MADM_*" # "NADM_*" # "ADM0_*" # "ADM1_*" # "ADM2_*" # ) # Configure rules for different types of users $newPasswordConfigurationRuleSplat = @{ Name = 'Administrative Accounts' Enable = $true IncludeExpiring = $true IncludePasswordNeverExpires = $true PasswordNeverExpiresDays = 90 ReminderConfiguration = { # follow expiration days of a user New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfWeek Monday, Wednesday, Friday -ExpirationDays 60 -ComparisonType lt #-ExpirationDays @(-200..-1), 0, 1, 2, 3, 7, 15, 30, 60 -ComparisonType lt New-PasswordConfigurationRuleReminder -Type 'ManagerNotCompliant' -DayOfWeek Friday -ExpirationDays 300 -ComparisonType lt New-PasswordConfigurationRuleReminder -Type 'Security' -DayOfWeek Monday -ExpirationDays -1 -ComparisonType lt } #ReminderDays = '-45', '-30', '-15', '-7' #, 0, 1, 2, 3, 7, 15, 30, 60, 29, 28 # IncludeOU = @( # "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" # "*OU=Administration,DC=ad,DC=evotec,DC=xyz" # ) IncludeName = @( 'Przem*' ) IncludeNameProperties = 'DisplayName', 'SamAccountName' ManagerReminder = $true ProcessManagersOnly = $true ManagerNotCompliant = $true ManagerNotCompliantDisplayName = 'Global Service Desk' ManagerNotCompliantEmailAddress = 'przemyslaw.klys@test.pl' ManagerNotCompliantDisabled = $true ManagerNotCompliantMissing = $true ManagerNotCompliantMissingEmail = $true ManagerNotCompliantLastLogonDays = 90 SecurityEscalation = $true SecurityEscalationDisplayName = 'IT Security' SecurityEscalationEmailAddress = 'przemyslaw.klys@test.pl' } New-PasswordConfigurationRule @newPasswordConfigurationRuleSplat # New-PasswordConfigurationRule -Name 'All others' -Enable -ReminderDays @(500..-500), 60, 59, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45 { # # follow expiration days of a user, you need to enable ManagerReminder for this functionality to work # New-PasswordConfigurationRuleReminder -Type 'Manager' -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 # # use a custom expiration days, and send only on specific days 1st, 10th and 15th of a month # New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfMonth 1, 10, 15 -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 # # use a custom expiration days (only if it's less then 10 days left), and send only on specific days of a week # New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfWeek Monday, Wednesday, Friday -ExpirationDays 10 -ComparisonType 'lt' # } -IncludeExpiring -OverwriteEmailProperty 'extensionAttribute5' -OverwriteManagerProperty 'extensionAttribute1' -ManagerReminder # Template to user when sending email to user before password expires New-PasswordConfigurationTemplate -Type PreExpiry -Template { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Evotec Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Evotec Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the Policy (at least 8 characters, at least one uppercase letter, at least one lowercase letter, at least one number, at least one special character)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Evotec Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)' # Template to user when sending email to user after password expires New-PasswordConfigurationTemplate -Type PostExpiry -Template { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Evotec Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Evotec Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the Policy" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Evotec Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject '[Password Expiring] Your password expired on $DateExpiry ($DaysToExpire days ago)' # Template to security team with all service accounts that have expired passwords and password never expires set to true New-PasswordConfigurationTemplate -Type Security { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal EmailText -Text "Below is a summary of ", "all service accounts", " where the passwords have exceeded the time limit stipulated in the password policy KGD. These accounts are all in violation of the KGD and immediate action/escalation should take place." -LineBreak -FontWeight normal, bold, normal EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text "Many thanks in advance." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject "[Passsword Expired] Following accounts are expired!" # Template to manager with all accounts that have expired passwords New-PasswordConfigurationTemplate -Type Manager { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:" EmailList { EmailListItem -Text 'Managed by you' EmailListItem -Text 'You are the manager of the owner of these accounts.' } EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy." -LineBreak EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text @( "Please note that for Service Accounts, even though the ", "'password never expires' " "flag remains set to " "'true' " ", the password MUST be changed before the expiry date specified in the above table. " "It is the responsibility of the manager of the account to ensure that this takes place. " ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak EmailText -Text @( "Please make an effort " "to change password yourself using known methods rather than asking the Service Desk to change the password for you. " "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! " "Do you really want the Service Desk agent to know the password to critical system you manage/own? " "Be responsible!" ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red EmailText -Text "One of the ways to change the password is: " -FontWeight bold EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy." EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed" } EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject "[Passsword Expiring] Accounts you manage/own are expiring or already expired" # Template to Service Desk with information about manager missing, disabled, last logon >90 days, missing email for service accounts New-PasswordConfigurationTemplate -Type ManagerNotCompliant { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:" EmailList { EmailListItem -Text "Missing a Manager in the AD - please add an active manager" EmailListItem -Text "The Manager in AD is Disabled - please add an active manager" EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee at Evotec/change the manager to an active manager" EmailListItem -Text "Manager is missing email - add manager email" } EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter EmailText -LineBreak EmailText -Text "Please note that all Service Accounts must have a Manager set in the AD, in order to fall within the Password Policy compliance notifications that are sent globally." -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } -Subject "[Password Escalation] Accounts are expiring with non-compliant manager" # Template to Admins with information summarizing what happened New-PasswordConfigurationTemplate -Type Admin { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Here's the summary of password notifications:" EmailList { EmailListItem -Text "Found users matching rule to send emails: ", $SummaryUsersEmails.Count EmailListItem -Text "Sent emails to users: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $true }).Count EmailListItem -Text "Couldn't send emails because of no email: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -eq 'No email address for user' }).Count EmailListItem -Text "Couldn't send emails because other reasons: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -ne 'No email address for user' }).Count EmailListItem -Text "Sent emails to managers: ", $SummaryManagersEmails.Count EmailListItem -Text "Sent emails to security: ", $SummaryEscalationEmails.Count } EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak EmailText -Text "Hope everything works correctly! ", " You can take a look at [Password Solution Report](https://adcompliance.Evotec.local/CustomReports/PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html) for details." -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } -Subject '[Password Summary] Passwords summary' } ================================================ FILE: Examples/Example-PasswordSolution-LegacyConfiguration01.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force $Date = Get-Date $GraphCredentials = @{ ClientID = '0fb383f1-8bfe-4c68-8ce2-5f6aa1d602fe' DirectoryID = 'ceb371f6-8745-4876-a040-69f2d10a9d1a' ClientSecret = Get-Content -Raw -LiteralPath "C:\Support\Important\O365-GraphEmailTestingKey.txt" } $PasswordSolution = [ordered] @{ # Graph based credentials (full support for Mailozaurr parameters) EmailParameters = @{ Credential = ConvertTo-GraphCredential -ClientID $GraphCredentials.ClientID -ClientSecret $GraphCredentials.ClientSecret -DirectoryID $GraphCredentials.DirectoryID Graph = $true Priority = 'Normal' From = 'przemyslaw.klys+testgithub@evotec.pl' WhatIf = $false ReplyTo = 'contact+testgithub@evotec.pl' } # Standard SMTP credentials (full support for Mailozaurr parameters) # EmailParameters = [ordered] @{ # UserName = 'ADAutomations@evotec.pl' # Password = Get-Content -LiteralPath D:\Secrets\WO_SVC_ADAutomations.txt # From = 'ADAutomations@evotec.pl' # Server = 'smtp.office365.com' # Priority = 'High' # UseSsl = $true # Port = 587 # Verbose = $false # WhatIf = $true # AsSecureString = $true # } OverwriteEmailProperty = 'extensionAttribute13' UserSection = @{ Enable = $true SendCountMaximum = 3 SendToDefaultEmail = $true # if enabled $EmailParameters are used (good for testing) DefaultEmail = 'przemyslaw.klys+testgithub@evotec.pl' # your default email field (IMPORTANT) } ManagerSection = @{ Enable = $true SendCountMaximum = 3 SendToDefaultEmail = $true # if enabled $EmailParameters are used (good for testing) DefaultEmail = 'przemyslaw.klys+testgithub@evotec.pl' # your default email field (IMPORTANT) } SecuritySection = @{ Enable = $true SendCountMaximum = 3 SendToDefaultEmail = $true # if enabled $EmailParameters are used (good for testing) DefaultEmail = 'przemyslaw.klys+testgithub@evotec.pl' # your default email field (IMPORTANT) AttachCSV = $true } AdminSection = @{ Enable = $true # doesn't processes this section at all Email = 'przemyslaw.klys+testgithub@evotec.pl' Subject = "[Reporting Evotec] Summary of password reminders" Manager = [ordered] @{ DisplayName = 'Administrators' EmailAddress = 'przemyslaw.klys+testgithub@evotec.pl' } } Rules = @( # rules are new way to define things. You can define more than one rule and limit it per group/ou # the primary rule above can be set or doesn't have to, all parameters from rules below can be used across different rules # only one email will be sent even if the rules are overlapping, the first one wins #region "admins" [ordered] @{ Name = 'Administrative Accounts' Enable = $false # doesn't processes this section at all if $false Reminders = -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 #Reminders = @(-200..-1), 0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 28, 30, @(30..60), @(61..370) # this means we want to process only users that NeverExpire IncludeExpiring = $true IncludePasswordNeverExpires = $true PasswordNeverExpiresDays = 90 IncludeNameProperties = 'SamAccountName' IncludeName = @( "ADM_*" "SADM_*" "PADM_*" "MADM_*" "NADM_*" "ADM0_*" "ADM1_*" "ADM2_*" ) SendToManager = @{ Manager = [ordered] @{ Enable = $true Reminders = @{ OnDay = @{ Enable = $true Days = 'Monday' Reminder = 10 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } # Manager not compliant will be processed regardless of Reminder for Users ManagerNotCompliant = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = 'Global Service Desk' EmailAddress = 'servicedesk@evotec.pl' } Disabled = $true Missing = $true MissingEmail = $true LastLogon = $true LastLogonDays = 90 Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 10, 21 Reminder = 50 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } } } #endregion admins #region "ITR01" [ordered] @{ Name = 'ITR01 SVC' Enable = $false # doesn't processes this section at all if $false Reminders = -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 IncludeExpiring = $true IncludePasswordNeverExpires = $true PasswordNeverExpiresDays = 360 IncludeNameProperties = 'DisplayName', 'SamAccountName', 'Name', 'UserPrincipalName' IncludeName = @( "*SVC_*" ) # limit group or limit OU can limit people with password never expire to certain users only IncludeOU = @( '*OU=ITR01,DC=*' ) # It's important to use single quotes to not activate variables SendToManager = @{ Manager = [ordered] @{ Enable = $true # it uses manager from AD in this section Reminders = @{ Default = @{ Enable = $true } OnDay = @{ Enable = $true Days = 'Monday', 'Thursday' Reminder = 15 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } # Security escalation will be processed regardless of Reminder for Users # Meaning Reminders definded below can be different then what users get SecurityEscalation = [ordered] @{ Enable = $true Manager = [ordered] @{ DisplayName = 'IT Security' EmailAddress = 'security@evotec.pl' } Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 10, 21 Reminder = -1 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } # Manager not compliant will be processed regardless of Reminder for Users ManagerNotCompliant = [ordered] @{ Enable = $true Manager = [ordered] @{ DisplayName = 'ITR01 Service Desk' EmailAddress = 'przemyslaw.klys+testgithub@evotec.pl' } Disabled = $true Missing = $true MissingEmail = $true LastLogon = $true LastLogonDays = 90 Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 10, 21 Reminder = 50 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } } } [ordered] @{ Name = 'ITR01 USR' Enable = $false # doesn't processes this section at all if $false Reminders = -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 #Reminders = @(-200..-1), 0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 28, 30, @(30..60), @(61..370) # this means we want to process only users that NeverExpire IncludeExpiring = $true IncludePasswordNeverExpires = $true PasswordNeverExpiresDays = 360 IncludeNameProperties = 'DisplayName', 'SamAccountName', 'Name', 'UserPrincipalName' IncludeName = @( "*_USR_*" ) # limit group or limit OU can limit people with password never expire to certain users only IncludeOU = @( '*OU=ITR01,DC=*' ) SendToManager = @{ Manager = [ordered] @{ Enable = $true Reminders = @{ Default = @{ Enable = $true } OnDay = @{ Enable = $true Days = 'Monday', 'Thursday' Reminder = 15 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } # Security escalation will be processed regardless of Reminder for Users # Meaning Reminders definded below can be different then what users get SecurityEscalation = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = 'IT Security' EmailAddress = 'security@evotec.pl' } Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 10, 21 Reminder = -1 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } ManagerNotCompliant = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = 'ITR01 Service Desk' EmailAddress = 'przemyslaw.klys+testgithub@evotec.pl' } Disabled = $true Missing = $true MissingEmail = $true LastLogon = $true LastLogonDays = 90 Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 10, 21 Reminder = 50 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } } } # [ordered] @{ # Name = 'ITR01' # Enable = $true # doesn't processes this section at all if $false # Reminders = 60, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45 # # this means we want to process only users that NeverExpire # IncludeExpiring = $true # # limit group or limit OU can limit people with password never expire to certain users only # IncludeOU = @( # '*OU=ITR01,DC=*' # ) # } [ordered] @{ Name = 'ITR01' Enable = $true # doesn't processes this section at all if $false Reminders = 60, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45 # this means we want to process only users that NeverExpire IncludeExpiring = $true # limit group or limit OU can limit people with password never expire to certain users only IncludeOU = @( '*OU=ITR01,DC=*' ) IncludeNameProperties = 'SamAccountName' IncludeName = @( "HACO" ) SendToManager = @{ Manager = [ordered] @{ Enable = $true Reminders = @{ Default = @{ Enable = $true } OnDay = @{ Enable = $true Days = 'Monday', 'Thursday' Reminder = 15 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } } } #endregion #region "All others" [ordered] @{ Name = 'All others' Enable = $true # doesn't processes this section at all if $false Reminders = @(500..-500), 60, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45 IncludeExpiring = $true OverwriteEmailProperty = 'extensionAttribute5' SendToManager = @{ Manager = [ordered] @{ Enable = $true # it uses manager from AD in this section Reminders = @{ Default = @{ Enable = $true } OnDay = @{ Enable = $true Days = 'Monday', 'Thursday' Reminder = 15 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } # Security escalation will be processed regardless of Reminder for Users # Meaning Reminders definded below can be different then what users get SecurityEscalation = [ordered] @{ Enable = $true Manager = [ordered] @{ DisplayName = 'IT Security' EmailAddress = 'przemyslaw.klys+testgithub@evotec.pl' } Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 1, 21 Reminder = -1 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } # Manager not compliant will be processed regardless of Reminder for Users ManagerNotCompliant = [ordered] @{ Enable = $true Manager = [ordered] @{ DisplayName = 'ITR01 Service Desk' EmailAddress = 'przemyslaw.klys+testgithub@evotec.pl' } Disabled = $true Missing = $true MissingEmail = $true LastLogon = $true LastLogonDays = 90 Reminders = @{ OnDayOfMonth = @{ Enable = $true Days = 1, 21 Reminder = 50 ComparisonType = 'lt' # lt = less then, gt = greater then, eq = equal, in = inside } } } } } #endregion "All others" ) # Keep in mind those are script block not hashtable TemplatePreExpiry = { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Evotec Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Evotec Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the [KGD: 2-96-IS-POL-01113513.](http://search.evotec.local/Open/2-96-IS-POL-01113513)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Evotec Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } TemplatePreExpirySubject = '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)' TemplatePostExpiry = { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Evotec Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Evotec Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the [KGD: 2-96-IS-POL-01113513.](http://search.Evotec.local/Open/2-96-IS-POL-01113513)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Evotec Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } TemplatePostExpirySubject = '[Password Expiring] Your password expired on $DateExpiry ($DaysToExpire days ago)' TemplateManager = { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:" EmailList { EmailListItem -Text 'Managed by you' EmailListItem -Text 'You are the manager of the owner of these accounts.' } EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy [KGD: 2-96-IS-POL-01113513](http://search.Evotec.local/Open/2-96-IS-POL-01113513)." -LineBreak EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text @( "Please note that for Service Accounts, even though the ", "'password never expires' " "flag remains set to " "'true' " ", the password MUST be changed before the expiry date specified in the above table. " "It is the responsibility of the manager of the account to ensure that this takes place. " ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak EmailText -Text @( "Please make an effort " "to change password yourself using known methods rather than asking the Service Desk to change the password for you. " "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! " "Do you really want the Service Desk agent to know the password to critical system you manage/own? " "Be responsible!" ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red EmailText -Text "One of the ways to change the password is: " -FontWeight bold EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy: [KGD: 2-96-IS-POL-01113513.](http://search.Evotec.local/Open/2-96-IS-POL-01113513)" EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed" } EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } TemplateManagerSubject = "[Passsword Expiring] Accounts you manage/own are expiring or already expired" TemplateSecurity = { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal EmailText -Text "Below is a summary of ", "all service accounts", " where the passwords have exceeded the time limit stipulated in the password policy KGD. These accounts are all in violation of the KGD and immediate action/escalation should take place." -LineBreak -FontWeight normal, bold, normal EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text "Many thanks in advance." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } TemplateSecuritySubject = "[Passsword Expired] Following accounts are expired!" TemplateManagerNotCompliant = { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:" EmailList { EmailListItem -Text "Missing a Manager in the AD - please add an active manager" EmailListItem -Text "The Manager in AD is Disabled - please add an active manager" EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee at Evotec/change the manager to an active manager" EmailListItem -Text "Manager is missing email - add manager email" } EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter EmailText -LineBreak EmailText -Text "Please note that all Service Accounts must have a Manager set in the AD, in order to fall within the Password Policy compliance notifications that are sent globally." -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } TemplateManagerNotCompliantSubject = "[Password Escalation] Accounts are expiring with non-compliant manager" TemplateAdmin = { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Here's the summary of password notifications:" EmailList { EmailListItem -Text "Found users matching rule to send emails: ", $SummaryUsersEmails.Count EmailListItem -Text "Sent emails to users: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $true }).Count EmailListItem -Text "Couldn't send emails because of no email: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -eq 'No email address for user' }).Count EmailListItem -Text "Couldn't send emails because other reasons: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -ne 'No email address for user' }).Count EmailListItem -Text "Sent emails to managers: ", $SummaryManagersEmails.Count EmailListItem -Text "Sent emails to security: ", $SummaryEscalationEmails.Count } EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak EmailText -Text "Hope everything works correctly! ", " You can take a look at [Password Solution Report](https://adcompliance.Evotec.local/CustomReports/PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html) for details." -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } TemplateAdminSubject = '[Password Summary] Passwords summary' Logging = @{ # Logging to file and to screen ShowTime = $true LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" TimeFormat = "yyyy-MM-dd HH:mm:ss" LogMaximum = 365 NotifyOnSkipUserManagerOnly = $false NotifyOnSecuritySend = $true NotifyOnManagerSend = $true NotifyOnUserSend = $true NotifyOnUserMatchingRule = $true NotifyOnUserDaysToExpireNull = $true EmailDateFormat = "yyyy-MM-dd HH:mm:ss" EmailDateFormatUTCConversion = $true } HTMLReports = @( # Accepts a list of reports to generate. Can be multiple reprorts having different sections, or just one having it all [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $true ShowSkippedLocations = $true ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true NestedRules = $false FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true } ) SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml" } Start-PasswordSolution @PasswordSolution ================================================ FILE: Examples/Example-PasswordSolution-ModernConfiguration01.ps1 ================================================ Clear-Host Import-Module .\PasswordSolution.psd1 -Force $Date = Get-Date Start-PasswordSolution { $Options = @{ # Logging to file and to screen ShowTime = $false LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" TimeFormat = "yyyy-MM-dd HH:mm:ss" LogMaximum = 365 NotifyOnSkipUserManagerOnly = $true NotifyOnSecuritySend = $true NotifyOnManagerSend = $true NotifyOnUserSend = $true NotifyOnUserMatchingRule = $true NotifyOnUserDaysToExpireNull = $true NotifyOnUserMatchingRuleForManager = $true NotifyOnUserMatchingRuleForManagerButNotCompliant = $true SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml" EmailDateFormat = "yyyy-MM-dd" EmailDateFormatUTCConversion = $true FilterOrganizationalUnit = @( "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" "*OU=Administration,DC=ad,DC=evotec,DC=xyz" ) # Using SearchBase is risky, as it can lead to missing managers "connection" to users, which may be filtered out # If you use Manager functionality use FilterOrganizationalUnit instead, unless you're sure what you're doing #SearchBase = @( # "OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" # "OU=Administration,DC=ad,DC=evotec,DC=xyz" #) } New-PasswordConfigurationOption @Options $GraphCredentials = @{ ClientID = '0fb383f1-8bfe-4c68-8ce2-5f6aa1d602fe' DirectoryID = 'ceb371f6-8745-4876-a040-69f2d10a9d1a' ClientSecret = Get-Content -Raw -LiteralPath "C:\Support\Important\O365-GraphEmailTestingKey.txt" } # (full support for Mailozaurr parameters) $EmailParameters = @{ Credential = ConvertTo-GraphCredential -ClientID $GraphCredentials.ClientID -ClientSecret $GraphCredentials.ClientSecret -DirectoryID $GraphCredentials.DirectoryID Graph = $true Priority = 'Normal' From = 'przemyslaw.klys@test.pl' WhatIf = $true ReplyTo = 'contact+testgithub@test.pl' } New-PasswordConfigurationEmail @EmailParameters # Configure behavior for different types of actions New-PasswordConfigurationType -Type User -Enable -SendCountMaximum 10 -DefaultEmail 'przemyslaw.klys+testgithub1@test.pl' New-PasswordConfigurationType -Type Manager -Enable -SendCountMaximum 10 -DefaultEmail 'przemyslaw.klys+testgithub2@test.pl' New-PasswordConfigurationType -Type Security -Enable -SendCountMaximum 1 -DefaultEmail 'przemyslaw.klys+testgithub3@test.pl' -AttachCSV New-PasswordConfigurationType -Type Admin -Enable -EmailAddress 'przemyslaw.klys+testgithub3@test.pl' -DisplayName 'Administrators' # Configure reporting $Report = [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $false ShowSkippedLocations = $false ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true NestedRules = $false FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true } New-PasswordConfigurationReport @Report # Configure rules for different types of users # New-PasswordConfigurationRule -Name 'Administrative Accounts' -Enable -IncludeExpiring -IncludePasswordNeverExpires -PasswordNeverExpiresDays 90 { # # follow expiration days of a user # New-PasswordConfigurationRuleReminder -Type 'Manager' # # use a custom expiration days, and send only on specific days 1st, 10th and 15th of a month # New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfMonth 1, 10, 15 -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 # # use a custom expiration days (only if it's less then 10 days left), and send only on specific days of a week # New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfWeek Monday, Wednesday, Friday -ExpirationDays 10 -ComparisonType 'lt' # } -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 -IncludeNameProperties 'SamAccountName' -IncludeName = @( # "ADM_*" # "SADM_*" # "PADM_*" # "MADM_*" # "NADM_*" # "ADM0_*" # "ADM1_*" # "ADM2_*" # ) # Configure rules for different types of users $newPasswordConfigurationRuleSplat = @{ Name = 'Administrative Accounts' Enable = $true IncludeExpiring = $true IncludePasswordNeverExpires = $true PasswordNeverExpiresDays = 90 ReminderConfiguration = { # follow expiration days of a user New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfWeek Monday, Wednesday, Friday -ExpirationDays 60 -ComparisonType lt #-ExpirationDays @(-200..-1), 0, 1, 2, 3, 7, 15, 30, 60 -ComparisonType lt #New-PasswordConfigurationRuleReminder -Type 'ManagerNotCompliant' -DayOfWeek Friday -ExpirationDays 300 -ComparisonType lt #New-PasswordConfigurationRuleReminder -Type 'Security' -DayOfWeek Monday -ExpirationDays -1 -ComparisonType lt } ReminderDays = '-45', '-30', '-15', '-7', -195 #, 0, 1, 2, 3, 7, 15, 30, 60, 29, 28 IncludeOU = @( "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" "*OU=Administration,DC=ad,DC=evotec,DC=xyz" ) ManagerReminder = $true ProcessManagersOnly = $false ManagerNotCompliant = $true ManagerNotCompliantDisplayName = 'Global Service Desk' ManagerNotCompliantEmailAddress = 'przemyslaw.klys@test.pl' ManagerNotCompliantDisabled = $true ManagerNotCompliantMissing = $true ManagerNotCompliantMissingEmail = $true ManagerNotCompliantLastLogonDays = 90 SecurityEscalation = $true SecurityEscalationDisplayName = 'IT Security' SecurityEscalationEmailAddress = 'przemyslaw.klys@test.pl' #DisableDays = @(-171,-170,-172,-200) #DisableType = 'in' #DisableWhatIf = $true } New-PasswordConfigurationRule @newPasswordConfigurationRuleSplat New-PasswordConfigurationRule -Name 'All others' -Enable -ReminderDays @(500..-500), 60, 59, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45, 600, -505 { # follow expiration days of a user, you need to enable ManagerReminder for this functionality to work New-PasswordConfigurationRuleReminder -Type 'Manager' -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 -ComparisonType 'in' # use a custom expiration days, and send only on specific days 1st, 10th and 15th of a month #New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfMonth 1, 10, 15 -ExpirationDays -45, -30, -15, -7, 0, 1, 2, 3, 7, 15, 30, 60 -ComparisonType 'in' # use a custom expiration days (only if it's less then 10 days left), and send only on specific days of a week #New-PasswordConfigurationRuleReminder -Type 'Manager' -DayOfWeek Monday, Wednesday, Friday -ExpirationDays 10 -ComparisonType 'lt' } -ExcludeOUFromOtherRules -IncludeExpiring -OverwriteEmailProperty 'extensionAttribute5' -OverwriteManagerProperty 'extensionAttribute1' -ManagerReminder #-DisableType 'in' -DisableDays @(-171,-200) -DisableWhatIf # Template to user when sending email to user before password expires New-PasswordConfigurationTemplate -Type PreExpiry -Template { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Evotec Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Evotec Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the Policy (at least 8 characters, at least one uppercase letter, at least one lowercase letter, at least one number, at least one special character)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Evotec Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)' # Template to user when sending email to user after password expires New-PasswordConfigurationTemplate -Type PostExpiry -Template { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Evotec Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Evotec Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the Policy" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Evotec Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject '[Password Expiring] Your password expired on $DateExpiry ($DaysToExpire days ago)' # Template to security team with all service accounts that have expired passwords and password never expires set to true New-PasswordConfigurationTemplate -Type Security { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal EmailText -Text "Below is a summary of ", "all service accounts", " where the passwords have exceeded the time limit stipulated in the password policy KGD. These accounts are all in violation of the KGD and immediate action/escalation should take place." -LineBreak -FontWeight normal, bold, normal EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text "Many thanks in advance." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject "[Passsword Expired] Following accounts are expired!" # Template to manager with all accounts that have expired passwords New-PasswordConfigurationTemplate -Type Manager { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:" EmailList { EmailListItem -Text 'Managed by you' EmailListItem -Text 'You are the manager of the owner of these accounts.' } EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy." -LineBreak EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text @( "Please note that for Service Accounts, even though the ", "'password never expires' " "flag remains set to " "'true' " ", the password MUST be changed before the expiry date specified in the above table. " "It is the responsibility of the manager of the account to ensure that this takes place. " ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak EmailText -Text @( "Please make an effort " "to change password yourself using known methods rather than asking the Service Desk to change the password for you. " "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! " "Do you really want the Service Desk agent to know the password to critical system you manage/own? " "Be responsible!" ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red EmailText -Text "One of the ways to change the password is: " -FontWeight bold EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy." EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed" } EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } -Subject "[Passsword Expiring] Accounts you manage/own are expiring or already expired" # Template to Service Desk with information about manager missing, disabled, last logon >90 days, missing email for service accounts New-PasswordConfigurationTemplate -Type ManagerNotCompliant { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:" EmailList { EmailListItem -Text "Missing a Manager in the AD - please add an active manager" EmailListItem -Text "The Manager in AD is Disabled - please add an active manager" EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee at Evotec/change the manager to an active manager" EmailListItem -Text "Manager is missing email - add manager email" } EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter EmailText -LineBreak EmailText -Text "Please note that all Service Accounts must have a Manager set in the AD, in order to fall within the Password Policy compliance notifications that are sent globally." -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } -Subject "[Password Escalation] Accounts are expiring with non-compliant manager" # Template to Admins with information summarizing what happened New-PasswordConfigurationTemplate -Type Admin { EmailImage -Source 'https://evotec.pl/wp-content/uploads/2015/05/Logo-evotec-012.png' -UrlLink '' -AlternativeText 'Evotec Logo' -Width '200' -Inline #-Height '100px' EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Here's the summary of password notifications:" EmailList { EmailListItem -Text "Found users matching rule to send emails: ", $CountUserEmails EmailListItem -Text "Sent emails to users: ", $CountUserEmailsSent EmailListItem -Text "Couldn't send emails because of no email: ", $CountUserEmailsNotSentLackOfEmail EmailListItem -Text "Couldn't send emails because other reasons: ", $CountUserEmailsNotSentOther EmailListItem -Text "Sent emails to managers: ", $CountManagerEmails EmailListItem -Text "Sent emails to security: ", $CountEscalationEmails } EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak EmailText -Text "Hope everything works correctly! ", " You can take a look at [Password Solution Report](https://adcompliance.Evotec.local/CustomReports/PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html) for details." -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } -Subject '[Password Summary] Passwords summary' } ================================================ FILE: Examples/Example-ReRegisterTaskAsGMSA.ps1 ================================================ $Tasks = Get-ScheduledTask -TaskPath "\" -TaskName "Automated-PasswordSolution" # Fix all tasks to use proper account foreach ($Task in $Tasks) { schtasks /Change /TN $Task.TaskName /RU "GMSA$" /RP "" } foreach ($Task in $Tasks) { #Start-ScheduledTask -TaskName $Task.TaskName -Verbose } ================================================ FILE: Examples/Example-SearchNotification.ps1 ================================================ Import-Module .\PasswordSolution.psd1 -Force Find-PasswordNotification -SearchPath "$PSScriptRoot\Search\SearchLog_2022-08.xml" | Format-Table #Find-PasswordNotification -SearchPath "$PSScriptRoot\Search\SearchLog_2021-06.xml" -Manager | Format-Table ================================================ FILE: Examples/WeakPasswordGenerator.ps1 ================================================ $Months = @( # english "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" # polish "Styczen", "Luty", "Marzec", "Kwiecien", "Maj", "Czerwiec", "Lipiec", "Sierpien", "Wrzesien", "Pazdziernik", "Listopad", "Grudzien" # spanish 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Setiembre', 'Octubre', 'Noviembre', 'Diciembre' # german "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" # russian "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" # french 'Janvier', 'Fevrier', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Aout', 'Septembre', 'Octobre', 'Novembre', 'Decembre' ) | Sort-Object -Unique $Numbers = 0..9 $Years = 2020..2023 $SpecialChar = @("!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "-", "+", "=", "[", "]", "{", "}", "|", "\") $Passwords = foreach ($Year in $Years) { Write-Color -Text "Year: ", $Year -Color Yellow, White $YearPasswords = foreach ($month in $months) { foreach ($number in $numbers) { foreach ($special in $SpecialChar) { $month + $Year.ToString() + $number.ToString() + $special $Year.ToString() + $month + $number.ToString() + $special $month + $Year.ToString() + $special } } } Write-Color -Text "Year: ", $Year, " passwords created: ", $YearPasswords.Count -Color Yellow, White $YearPasswords } $Passwords.Count ================================================ FILE: PasswordSolution.psd1 ================================================ @{ AliasesToExport = @() Author = 'Przemyslaw Klys' CmdletsToExport = @() CompanyName = 'Evotec' CompatiblePSEditions = @('Desktop', 'Core') Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' Description = 'This module allows the creation of password expiry emails for users, managers, administrators, and security according to defined templates. It''s able to work with different rules allowing to fully customize who gets the email and when.' FunctionsToExport = @('Find-Password', 'Find-PasswordEntra', 'Find-PasswordNotification', 'Find-PasswordQuality', 'New-PasswordConfigurationEmail', 'New-PasswordConfigurationEntra', 'New-PasswordConfigurationExternalUsers', 'New-PasswordConfigurationOption', 'New-PasswordConfigurationReplacement', 'New-PasswordConfigurationReport', 'New-PasswordConfigurationRule', 'New-PasswordConfigurationRuleReminder', 'New-PasswordConfigurationTemplate', 'New-PasswordConfigurationType', 'Show-PasswordQuality', 'Start-PasswordSolution') GUID = 'c58ff818-1de6-4500-961c-a243c2043255' ModuleVersion = '2.1.0' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ ExternalModuleDependencies = @('ActiveDirectory') IconUri = 'https://evotec.xyz/wp-content/uploads/2022/08/PasswordSolution.png' ProjectUri = 'https://github.com/EvotecIT/PasswordSolution' Tags = @('password', 'passwordexpiry', 'activedirectory', 'windows') } } RequiredModules = @(@{ Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' ModuleName = 'PSSharedGoods' ModuleVersion = '0.0.307' }, @{ Guid = 'a7bdf640-f5cb-4acf-9de0-365b322d245c' ModuleName = 'PSWriteHTML' ModuleVersion = '1.28.0' }, @{ Guid = '0b0ba5c5-ec85-4c2b-a718-874e55a8bc3f' ModuleName = 'PSWriteColor' ModuleVersion = '1.0.1' }, @{ Guid = '2b0ea9f1-3ff1-4300-b939-106d5da608fa' ModuleName = 'Mailozaurr' ModuleVersion = '1.0.0' }, 'ActiveDirectory') RootModule = 'PasswordSolution.psm1' } ================================================ FILE: PasswordSolution.psm1 ================================================ #Get public and private function definition files. $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Classes = @( Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Enums = @( Get-ChildItem -Path $PSScriptRoot\Enums\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue if ($AssemblyFolders.BaseName -contains 'Standard') { $Assembly = @( Get-ChildItem -Path $PSScriptRoot\Lib\Standard\*.dll -ErrorAction SilentlyContinue ) } else { if ($PSEdition -eq 'Core') { $Assembly = @( Get-ChildItem -Path $PSScriptRoot\Lib\Core\*.dll -ErrorAction SilentlyContinue ) } else { $Assembly = @( Get-ChildItem -Path $PSScriptRoot\Lib\Default\*.dll -ErrorAction SilentlyContinue ) } } $FoundErrors = @( Foreach ($Import in @($Assembly)) { try { Add-Type -Path $Import.Fullname -ErrorAction Stop } catch [System.Reflection.ReflectionTypeLoadException] { Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique foreach ($E in $LoaderExceptions) { Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" } $true #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" } catch { Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique foreach ($E in $LoaderExceptions) { Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" } $true #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" } } #Dot source the files Foreach ($Import in @($Private + $Public + $Classes + $Enums)) { Try { . $Import.Fullname } Catch { Write-Error -Message "Failed to import functions from $($import.Fullname): $_" $true } } ) if ($FoundErrors.Count -gt 0) { $ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName Write-Warning "Importing module $ModuleName failed. Fix errors before continuing." break } Export-ModuleMember -Function '*' -Alias '*' ================================================ FILE: Private/Add-ManagerInformation.ps1 ================================================ function Add-ManagerInformation { [CmdletBinding()] param( [System.Collections.IDictionary] $SummaryDictionary, [string] $Type, [string] $ManagerType, [Object] $Key, [PSCustomObject] $User, [PSCustomObject] $Rule, [System.Collections.IDictionary] $Entra ) if ($Key) { if ($Entra.Enabled) { # If entra is enabled we can use UserPrincipalName $UserSearchString = $User.UserPrincipalName if ($Key -is [string]) { $KeyDN = $Key } else { $KeyDN = $Key.AdditionalProperties.displayName } } else { $UserSearchString = $User.DistinguishedName if ($Key -is [string]) { $KeyDN = $Key } else { $KeyDN = $Key.DisplayName } } if (-not $SummaryDictionary[$KeyDN]) { $SummaryDictionary[$KeyDN] = [ordered] @{ Manager = $Key ManagerDefault = [ordered] @{} ManagerNotCompliant = [ordered] @{} Security = [ordered] @{} } } $SummaryDictionary[$KeyDN][$Type][$UserSearchString] = [ordered] @{ Manager = $User.ManagerDN User = $User Rule = $Rule ManagerOption = $Type Output = [ordered] @{} } $Default = [ordered] @{ DisplayName = $User.DisplayName Enabled = $User.Enabled SamAccountName = $User.SamAccountName Domain = $User.Domain DateExpiry = $User.DateExpiry DaysToExpire = $User.DaysToExpire PasswordLastSet = $User.PasswordLastSet PasswordExpired = $User.PasswordExpired } if ($Type -ne 'ManagerDefault') { $Extended = [ordered] @{ 'Status' = $ManagerType 'Manager' = $User.Manager 'Manager Email' = $User.ManagerEmail } $SummaryDictionary[$KeyDN][$Type][$UserSearchString]['Output'] = [PSCustomObject] ( $Extended + $Default) } else { $SummaryDictionary[$KeyDN][$Type][$UserSearchString]['Output'] = [PSCustomObject] $Default } } } ================================================ FILE: Private/Add-ParametersToString.ps1 ================================================ function Add-ParametersToString { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER String Parameter description .PARAMETER Parameter Parameter description .EXAMPLE $Test = 'this is a string $Test - and $Test2 AND $tEST3' Add-ParametersToString -String $Test -Parameter @{ Testooo = 'sdsds' Test = 'oh my god' Test2 = 'ole ole' TEST3 = '56555' } .NOTES General notes #> [CmdletBinding()] param( [string] $String, [System.Collections.IDictionary] $Parameter ) $Sorted = $Parameter.Keys | Sort-Object { $_.length } -Descending foreach ($Key in $Sorted) { $String = $String -ireplace [Regex]::Escape("`$$Key"), $Parameter[$Key] } $String } ================================================ FILE: Private/Export-SearchInformation.ps1 ================================================ function Export-SearchInformation { [CmdletBinding()] param( [string] $SearchPath, [System.Collections.IDictionary] $SummarySearch, [string] $Today, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails ) if ($SearchPath) { Write-Color -Text "[i]" , " Saving Search report " -Color White, Yellow, Green if ($SummaryUsersEmails) { $SummarySearch['EmailSent'][$Today] += $SummaryUsersEmails } if ($SummaryEscalationEmails) { $SummarySearch['EmailEscalations'][$Today] += $SummaryEscalationEmails } if ($SummaryManagersEmails) { $SummarySearch['EmailManagers'][$Today] += $SummaryManagersEmails } try { $SummarySearch | Export-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't save to file $SearchPath", ". Error: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } Write-Color -Text "[i]" , " Saving Search report ", "Done" -Color White, Yellow, Green } } ================================================ FILE: Private/Fromat-ReminderDays.ps1 ================================================ function Format-ReminderDays { <# .SYNOPSIS Formats an array of reminder days into a readable, concise format. .DESCRIPTION This function accepts an array of numbers (which may include nested arrays) and always sorts them in ascending order. It then groups contiguous sequences (where each subsequent number is either equal to or exactly 1 greater than the previous) and formats groups of three or more unique numbers as a range. For clarity, if any number in the range is negative the range is displayed using " to " (e.g. "-500 to 500") to avoid confusion with hyphenated negatives. .PARAMETER Days An array of integers (or nested arrays of integers) representing reminder days. .EXAMPLE Format-ReminderDays -Days @(1,2,3,7,30) # Returns: "1-3, 7, 30" .EXAMPLE $xxx = @(500..-500), 60, 59, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45, 600, -505 Format-ReminderDays -Days $xxx # Returns: "-505, -500 to 500, 600" .EXAMPLE Format-ReminderDays -Days @(15..-500), 60, 59, 30, 15, 7, 3, 2, 1, 0, -7, -15, -30, -45, 600, -505 # Returns: -505, -500 to 15, 30, 59, 60, 600 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [Array]$Days ) # Flatten the input array (to handle nested arrays like those produced by ranges) $flatDays = @() foreach ($item in $Days) { if ($item -is [Array]) { $flatDays += $item } else { $flatDays += $item } } # Convert all items to integers. $flatDays = $flatDays | ForEach-Object { [int]$_ } # Always sort in ascending order. $sortedDays = $flatDays | Sort-Object # Group contiguous numbers. $groups = @() $currentGroup = @($sortedDays[0]) for ($i = 1; $i -lt $sortedDays.Count; $i++) { $current = $sortedDays[$i] $previous = $sortedDays[$i - 1] # Group if the current number is equal (duplicate) or exactly 1 greater than the previous. if ($current -eq $previous -or $current -eq $previous + 1) { $currentGroup += $current } else { $groups += , @($currentGroup) $currentGroup = @($current) } } $groups += , @($currentGroup) # Format each group into a string. $formattedGroups = foreach ($group in $groups) { # Count unique numbers in the group. $uniqueCount = ($group | Select-Object -Unique).Count if ($uniqueCount -ge 3) { # Use " to " when any value is negative (for clarity) if ($group[0] -lt 0 -or $group[-1] -lt 0) { "$($group[0]) to $($group[-1])" } else { "$($group[0])-$($group[-1])" } } else { # For groups with fewer than 3 unique numbers, list the numbers separated by commas. $group -join ", " } } return ($formattedGroups -join ", ") } ================================================ FILE: Private/Import-SearchInformation.ps1 ================================================ function Import-SearchInformation { [CmdletBinding()] param( [string] $SearchPath ) if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { Write-Color -Text "[i]", " Loading file ", $SearchPath -Color White, Yellow, White, Yellow, White, Yellow, White $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } } } if (-not $SummarySearch) { $SummarySearch = [ordered] @{ EmailSent = [ordered] @{} EmailManagers = [ordered] @{} EmailEscalations = [ordered] @{} } } $SummarySearch } ================================================ FILE: Private/Invoke-PasswordRuleProcessing.ps1 ================================================ function Invoke-PasswordRuleProcessing { [CmdletBinding()] param( [System.Collections.IDictionary] $Rule, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $AllSkipped, [System.Collections.IDictionary] $Locations, [System.Collections.IDictionary] $Logging, [System.Collections.IDictionary] $UsersExternalSystem, [DateTime] $TodayDate, [System.Collections.IDictionary] $Entra ) # Go for each rule and check if the user is in any of those rules if ($Rule.Enable -eq $true) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Yellow, White, Green, White, Green, White, Green, White # Lets create summary for the rule if (-not $Summary['Rules'][$Rule.Name] ) { $Summary['Rules'][$Rule.Name] = [ordered] @{} } # this will make sure to expand array of multiple arrays of ints if provided # for example: (-150..-100),(-60..0), 1, 2, 3 if ($null -ne $Rule.Reminders) { $Rule.Reminders = $Rule.Reminders | ForEach-Object { $_ } } # Do the same for DisableDays, if provided if ($null -ne $Rule.DisableDays) { $Rule.DisableDays = $Rule.DisableDays | ForEach-Object { $_ } } foreach ($User in $CachedUsers.Values) { if ($Entra.Enabled) { $UserSearchString = $User.UserPrincipalName } else { $UserSearchString = $User.DistinguishedName } if ($User.Enabled -eq $false) { # We don't want to have disabled users continue } if ($Rule.ExcludeOUFromOtherRules) { # Rule defined that user within OU's already used in processed Rules are excluded $FoundOU = $false foreach ($OU in $Summary.Tracking.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.ExcludeOU.Count -gt 0) { # Rule defined that user within specific OU has to be excluded $FoundOU = $false foreach ($OU in $Rule.ExcludeOU) { # Lets update global tracking if ($Summary['Tracking']['ExcludeOU'] -notcontains $OU) { $Summary['Tracking']['ExcludeOU'].Add($OU) } if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { continue } } if ($Rule.IncludeOU.Count -gt 0) { # Rule defined that only user within specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.IncludeOU) { # Lets update global tracking if ($Summary['Tracking']['IncludeOU'] -notcontains $OU) { $Summary['Tracking']['IncludeOU'].Add($OU) } if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { continue } } if ($Rule.ExcludeGroup.Count -gt 0) { # Rule defined that only user within specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # If found, we need to skip user if ($FoundGroup) { continue } } if ($Rule.IncludeGroup.Count -gt 0) { # Rule defined that only user within specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { continue } } if ($Rule.IncludeName.Count -gt 0) { $IncludeName = $false foreach ($Name in $Rule.IncludeName) { foreach ($Property in $Rule.IncludeNameProperties) { if ($User.$Property -like $Name) { $IncludeName = $true break } } if ($IncludeName) { break } } if (-not $IncludeName) { continue } } if ($Rule.ExcludeName.Count -gt 0) { $ExcludeName = $false foreach ($Name in $Rule.ExcludeName) { foreach ($Property in $Rule.ExcludeNameProperties) { if ($User.$Property -like $Name) { $ExcludeName = $true break } } if ($ExcludeName) { break } } if ($ExcludeName) { continue } } if ($Summary['Notify'][$UserSearchString] -and $Summary['Notify'][$UserSearchString].ProcessManagersOnly -ne $true) { # User already exists in the notifications - rules are overlapping, we only take the first one # We also check for ProcessManagersOnly because we don't want first rule to ignore any other rules for users continue } if ($Rule.IncludePasswordNeverExpires -and $Rule.IncludeExpiring) { if ($User.PasswordNeverExpires -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } } elseif ($Rule.IncludeExpiring) { if ($User.PasswordNeverExpires -eq $true) { # we skip those that never expire continue } } elseif ($Rule.IncludePasswordNeverExpires) { if ($User.PasswordNeverExpires -eq $true) { $DaysToPasswordExpiry = $Rule.PasswordNeverExpiresDays - $User.PasswordDays $User.DaysToExpire = $DaysToPasswordExpiry } else { # we skip users who expire continue } } else { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, " doesn't include IncludePasswordNeverExpires nor IncludeExpiring so skipping." -Color Yellow, White, Green, White, Green, White, Green, White continue } if ($null -eq $User.DaysToExpire) { # This is to track users that our account may not have permissions over if ($Logging.NotifyOnUserDaysToExpireNull) { Write-Color -Text @( "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire not set. ", "(", "Password Last Set: ", $User.PasswordLastSet, ")", " (Password at next logon: ", $User.PasswordAtNextLogon, ")" ) -Color Yellow, White, Yellow, White, Yellow, White, White, White, Yellow, DarkCyan, White, Yellow, DarkCyan, White } # if days to expire is not set, password last set is not set either # this means account either was never used or account we're using to has no permissions over that account $AllSkipped[$UserSearchString] = $User $Location = $User.OrganizationalUnit if (-not $Location) { $Location = 'Default' } if (-not $Locations[$Location]) { $Locations[$Location] = [PSCustomObject] @{ Location = $Location Count = 0 CountExpired = 0 Names = [System.Collections.Generic.List[string]]::new() NamesExpired = [System.Collections.Generic.List[string]]::new() } } if ($User.PasswordExpired) { $Locations[$Location].CountExpired++ $Locations[$Location].NamesExpired.Add($User.SamAccountName) } else { $Locations[$Location].Count++ $Locations[$Location].Names.Add($User.SamAccountName) } } # Lets find users that expire, and match our rule if ($null -ne $User.DaysToExpire -and $User.DaysToExpire -in $Rule.Reminders) { # check if we need to notify user or just manager if (-not $Rule.ProcessManagersOnly) { if ($Logging.NotifyOnUserMatchingRule) { Write-Color -Text "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } # This is required for email notification to different email address # User wanted to use different email address for notifications based on external property such as # employeeID, employeeNumber, extensionAttributes, etc. # normally this wouldn't be required if it's global setting, but if only a handful of users need to have their address changed # the per rule overwriteemailproperty should be used if ($Rule.OverwriteEmailProperty) { $NewPropertyWithEmail = $Rule.OverwriteEmailProperty if ($NewPropertyWithEmail -and $User.$NewPropertyWithEmail) { $User.EmailAddress = $User.$NewPropertyWithEmail } } if ($Rule.OverwriteEmailFromExternalUsers) { $ExternalUser = $null $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*') { $User.EmailAddress = $ExternalUser.$EmailProperty } } $Summary['Notify'][$UserSearchString] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } # If we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$UserSearchString]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('User') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $Summary['Rules'][$Rule.Name][$UserSearchString] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('User') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } } } # this is to overwrite manager by using extensionAttribute or any other field in AD # it works on SamAccountName, DistinguishedName only if ($Rule.OverwriteManagerProperty) { $NewPropertyWithManager = $Rule.OverwriteManagerProperty if ($NewPropertyWithManager -and $User.$NewPropertyWithManager) { $NewManager = $CachedUsers[$User.$NewPropertyWithManager] if ($NewManager -and $NewManager.Mail -like "*@*") { $User.ManagerEmail = $NewManager.Mail $User.Manager = $NewManager.DisplayName $User.ManagerSamAccountName = $NewManager.SamAccountName $User.ManagerEnabled = $NewManager.Enabled $User.ManagerLastLogon = $NewManager.LastLogonDate if ($User.ManagerLastLogon) { $User.ManagerLastLogonDays = $( - $($User.ManagerLastLogon - $Today).Days) } else { $User.ManagerLastLogonDays = $null } $User.ManagerType = $NewManager.ObjectClass $User.ManagerDN = $NewManager.DistinguishedName } } } # Lets find users that we need to notify manager about if ($null -ne $User.DaysToExpire -and $Rule.SendToManager) { if ($Rule.SendToManager.Manager -and $Rule.SendToManager.Manager.Enable -eq $true -and $User.ManagerStatus -eq 'Enabled' -and $User.ManagerEmail -like "*@*") { $SendToManager = $true # Manager is enabled and has an email, this is standard situation for manager in AD # But before we go and do that, maybe user wants to send emails to managers if those users are in specific group or OU if ($Rule.SendToManager.Manager.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.SendToManager.Manager.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.Manager.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { $SendToManager = $false } } if ($SendToManager -and $Rule.SendToManager.Manager.IncludeGroup.Count -gt 0) { # Rule defined that only user within specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.Manager.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { $SendToManager = $false } } if ($SendToManager) { $SendToManager = $false if ($Rule.SendToManager.Manager.Reminders.Default.Enable -eq $true -and $null -eq $Rule.SendToManager.Manager.Reminders.Default.Reminder -and $User.DaysToExpire -in $Rule.Reminders) { # Use default reminder as per user, not per manager $SendToManager = $true } elseif ($Rule.SendToManager.Manager.Reminders.Default.Enable -eq $true -and $Rule.SendToManager.Manager.Reminders.Default.Reminder -and $User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.Default.Reminder) { # User manager reminder as per manager config $SendToManager = $true } if (-not $SendToManager -and $Rule.SendToManager.Manager.Reminders.OnDay -and $Rule.SendToManager.Manager.Reminders.OnDay.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.Manager.Reminders.OnDay.Days) { if ($Day -eq "$($TodayDate.DayOfWeek)") { if ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendtoManager.Manager.Reminders.OnDay.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.OnDay.Reminder) { $SendToManager = $true break } } } } } if (-not $SendToManager -and $Rule.SendToManager.Manager.Reminders.OnDayOfMonth -and $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days) { if ($Day -eq $TodayDate.Day) { if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } elseif ($Rule.SendtoManager.Manager.Reminders.OnDayOfMonth.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder) { $SendToManager = $true break } } } } } if ($SendToManager) { if ($Logging.NotifyOnUserMatchingRuleForManager) { Write-Color -Text "[i]", " User (manager rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } # If we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$UserSearchString]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('Manager') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $Summary['Rules'][$Rule.Name][$UserSearchString] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('Manager') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } # Push manager to list $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerDefault' ManagerType = 'Ok' Key = $User.ManagerDN User = $User Rule = $Rule Entra = $Entra } Add-ManagerInformation @Splat } } } else { if ($Rule.SendToManager.Manager -and $Rule.SendToManager.Manager.Enable -eq $true) { # Manager rule is enabled but manager is not enabled or has no email if ($Logging.NotifyOnUserMatchingRuleForManagerButNotCompliant) { Write-Color -Text "[i]", " User (manager rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, ", manager status: ", $User.ManagerStatus, ". Reason to skip: ", "No manager or manager is not enabled or manager has no email " -Color Yellow, White, Yellow, White, Yellow, White, White, Red, White, Red, White, Red } } } } # Lets find users that have no manager, manager is not enabled or manager has no email if ($Rule.SendToManager -and $Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -eq $true -and $Rule.SendToManager.ManagerNotCompliant.Manager) { # Not compliant (missing, disabled, no email), covers all the below options if ($Rule.SendToManager.ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.Enable -and $Rule.SendToManager.ManagerNotCompliant.Manager) { $ManagerNotCompliant = $true # But before we go and do that, maybe user wants to send emails to managers only if those users are in specific group or OU if ($Rule.SendToManager.ManagerNotCompliant.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.ManagerNotCompliant.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { $ManagerNotCompliant = $false } } if ($ManagerNotCompliant -and $Rule.SendToManager.ManagerNotCompliant.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.ManagerNotCompliant.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { $ManagerNotCompliant = $false } } if ($Rule.SendToManager.ManagerNotCompliant.Reminders) { $ManagerNotCompliant = $false if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default -and $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable -eq $true) { $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder = $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder | ForEach-Object { $_ } if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) { $ManagerNotCompliant = $true } } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay -and $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days) { if ($Day -eq "$($TodayDate.DayOfWeek)") { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendtoManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder) { $ManagerNotCompliant = $true break } } } } } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth -and $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days) { if ($Day -eq $TodayDate.Day) { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } elseif ($Rule.SendtoManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder) { $ManagerNotCompliant = $true break } } } } } } if ($ManagerNotCompliant -eq $true) { $ManagerNotCompliantMatched = $false if ($Rule.SendToManager.ManagerNotCompliant.MissingEmail -and $User.ManagerStatus -in 'Enabled, bad email', 'No email') { # Manager is enabled but missing email $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = if ($User.ManagerStatus -eq 'Enabled, bad email') { 'Manager has bad email' } else { 'Manager has no email' } Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.Disabled -and $User.ManagerStatus -eq 'Disabled') { # Manager is disabled, regardless if he/she has email $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager disabled' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.LastLogon -and $User.ManagerLastLogonDays -ge $Rule.SendToManager.ManagerNotCompliant.LastLogonDays) { # Manager Last Logon over X days $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not logging in' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } elseif ($Rule.SendToManager.ManagerNotCompliant.Missing -and $User.ManagerStatus -eq 'Missing') { # Manager is missing $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifyManager'] Type = 'ManagerNotCompliant' ManagerType = 'Manager not set' Key = $Rule.SendToManager.ManagerNotCompliant.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat $ManagerNotCompliantMatched = $true } if ($ManagerNotCompliantMatched) { if ($Logging.NotifyOnUserMatchingRuleForManagerNotCompliant) { Write-Color -Text "[i]", " User (manager not compliant rule) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } # If we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$UserSearchString]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('Manager Not Compliant') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $Summary['Rules'][$Rule.Name][$UserSearchString] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('Manager Not Compliant') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } } else { if ($User.ManagerStatus -eq 'Enabled') { # do nothing } else { # This shouldn't happen, but just in case - we can log if this happens if ($Logging.NotifyOnUserMatchingRuleForManagerNotCompliant) { Write-Color -Text "[i]", " User (manager not compliant rule not processed) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " manager status: ", $User.ManagerStatus -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } } } } } } # Lets find users that require escalation if ($null -ne $User.DaysToExpire -and $Rule.SendToManager -and $Rule.SendToManager.SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.Enable -eq $true -and $Rule.SendToManager.SecurityEscalation.Manager) { $SecurityEscalation = $true if ($Rule.SendToManager.SecurityEscalation.IncludeOU.Count -gt 0) { # Rule defined that only user withi specific OU has to be found $FoundOU = $false foreach ($OU in $Rule.SendToManager.SecurityEscalation.IncludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } if (-not $FoundOU) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeOU.Count -gt 0) { $FoundOU = $false foreach ($OU in $Rule.SendToManager.SecurityEscalation.ExcludeOU) { if ($User.OrganizationalUnit -like $OU) { $FoundOU = $true break } } # if OU is found we need to exclude the user if ($FoundOU) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.ExcludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.ExcludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } # if Group found, we need to skip this user if ($FoundGroup) { $SecurityEscalation = $false } } if ($SecurityEscalation -and $Rule.SendToManager.SecurityEscalation.IncludeGroup.Count -gt 0) { # Rule defined that only user withi specific group has to be found $FoundGroup = $false foreach ($Group in $Rule.SendToManager.SecurityEscalation.IncludeGroup) { if ($User.MemberOf -contains $Group) { $FoundGroup = $true break } } if (-not $FoundGroup) { $SecurityEscalation = $false } } if ($Rule.SendToManager.SecurityEscalation.Reminders) { $SecurityEscalation = $false if ($Rule.SendToManager.SecurityEscalation.Reminders.Default -and $Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable -eq $true) { $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder = $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder | ForEach-Object { $_ } if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder) { $SecurityEscalation = $true } } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay -and $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Days) { if ($Day -eq "$($TodayDate.DayOfWeek)") { if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendtoManager.SecurityEscalation.Reminders.OnDay.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder) { $SecurityEscalation = $true break } } } } } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth -and $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Enable -eq $true) { foreach ($Day in $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Days) { if ($Day -eq $TodayDate.Day) { if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'lt') { if ($User.DaysToExpire -lt $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'gt') { if ($User.DaysToExpire -gt $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'eq') { if ($User.DaysToExpire -eq $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } elseif ($Rule.SendtoManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType -eq 'in') { if ($User.DaysToExpire -in $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder) { $SecurityEscalation = $true break } } } } } } if ($SecurityEscalation) { if ($Logging.NotifyOnUserMatchingRuleForSecurityEscalation) { Write-Color -Text "[i]", " User (security escalation) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire, " " -Color Yellow, White, Yellow, White, Yellow, White, White, Blue } # If we need to send an email to manager we need to update rules, just in case the user has not matched for user section if ($Summary['Rules'][$Rule.Name][$UserSearchString]) { # User exists, update reason $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('Security esclation') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } else { # User doesn't exists in rules, add it $Summary['Rules'][$Rule.Name][$UserSearchString] = [ordered] @{ User = $User Rule = $Rule ProcessManagersOnly = $Rule.ProcessManagersOnly } $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleOptions.Add('Security esclation') $Summary['Rules'][$Rule.Name][$UserSearchString].User.RuleName = $Rule.Name } $Splat = [ordered] @{ SummaryDictionary = $Summary['NotifySecurity'] Type = 'Security' ManagerType = 'Escalation' Key = $Rule.SendToManager.SecurityEscalation.Manager User = $User Rule = $Rule } Add-ManagerInformation @Splat } } } } else { if ($null -ne $Rule.Name -and $null -ne $Rule.Enable) { Write-Color -Text "[i]", " Processing rule ", $Rule.Name, ' status: ', $Rule.Enable -Color Red, White, Red, White, Red, White, Red, White } } } ================================================ FILE: Private/New-HTMLReport.ps1 ================================================ function New-HTMLReport { [CmdletBinding()] param( [System.Collections.IDictionary] $Report, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $Logging, [string] $SearchPath, [Array] $Rules, [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $AdminSection, [System.Collections.IDictionary] $CachedUsers, [System.Collections.IDictionary] $Summary, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails, [System.Collections.IDictionary] $SummarySearch, [System.Collections.IDictionary] $Locations, [System.Collections.IDictionary] $AllSkipped, [System.Collections.IDictionary] $ExternalSystemReplacements, [ScriptBlock] $TemplateAdmin, [string] $TemplateAdminSubject, [Array] $FilterOrganizationalUnit, [Array] $SearchBase ) $TranslateOperators = @{ 'lt' = 'Less than' 'gt' = 'Greater than' 'eq' = 'Equal to' 'ne' = 'Not equal to' 'le' = 'Less than or equal to' 'ge' = 'Greater than or equal to' 'in' = 'In' } Write-Color -Text "[i]", " Generating HTML report ", $Report.Title -Color White, Yellow, Green if ($Report.DisableWarnings -eq $true) { $WarningAction = 'SilentlyContinue' } else { $WarningAction = 'Continue' } if (-not $Report.Title) { $Report.Title = "Password Solution Report" } # Create report New-HTML { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Report.ShowConfiguration) { New-HTMLTab -Name "About" { New-HTMLTab -Name "Configuration" { New-HTMLSection -Invisible { New-HTMLSection -HeaderText "Email Configuration" { New-HTMLList { foreach ($Key in $EmailParameters.Keys) { if ($Key -eq 'Body') { } elseif ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $EmailParameters[$Key] -FontWeight normal, normal, bold } else { New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold } } } } New-HTMLSection -HeaderText "Logging" { New-HTMLList { foreach ($Key in $Logging.Keys) { if ($Key -ne 'Password') { New-HTMLListItem -Text $Key, ": ", $Logging[$Key] -FontWeight normal, normal, bold } else { New-HTMLListItem -Text $Key, ": ", "REDACTED" -FontWeight normal, normal, bold } } } } New-HTMLSection -HeaderText "Other" { New-HTMLList { if ($Report.FilePath) { New-HTMLListItem -Text 'FilePath', ": ", $Report.FilePath -FontWeight normal, normal, bold } else { New-HTMLListItem -Text 'FilePath', ": ", "Not set" -FontWeight normal, normal, bold } if ($Report.Email) { New-HTMLListItem -Text 'Email', ": ", $Report.Email -FontWeight normal, normal, bold } else { New-HTMLListItem -Text 'Email', ": ", "Not set" -FontWeight normal, normal, bold } if ($SearchPath) { New-HTMLListItem -Text 'SearchPath', ": ", $SearchPath -FontWeight normal, normal, bold } else { New-HTMLListItem -Text 'SearchPath', ": ", "Not set" -FontWeight normal, normal, bold } if ($FilterOrganizationalUnit.Count -gt 0) { New-HTMLListItem -Text 'FilterOrganizationalUnit', ": " { New-HTMLList { foreach ($OU in $FilterOrganizationalUnit) { New-HTMLListItem -Text 'OU', ": ", $OU -FontWeight normal, normal, bold } } } -FontWeight normal, normal, bold } else { New-HTMLListItem -Text 'FilterOrganizationalUnit', ": ", "Not set" -FontWeight normal, normal, bold } if ($SearchBase.Count -gt 0) { New-HTMLListItem -Text 'SearchBase', ": " { New-HTMLList { foreach ($OU in $SearchBase) { New-HTMLListItem -Text 'OU', ": ", $OU -FontWeight normal, normal, bold } } } -FontWeight normal, normal, bold } else { New-HTMLListItem -Text 'SearchBase', ": ", "Not set" -FontWeight normal, normal, bold } } } } New-HTMLSection -Invisible { New-HTMLSection -HeaderText "User Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $UserSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $UserSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $UserSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($UserSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Manager Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $ManagerSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $ManagerSection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($ManagerSection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Security Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $SecuritySection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendCountMaximum: ", $SecuritySection.SendCountMaximum -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "SendToDefaultEmail: ", $SecuritySection.SendToDefaultEmail -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "DefaultEmail: ", ($SecuritySection.DefaultEmail -join ", ") -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Attach CSV: ", ($SecuritySection.AttachCSV -join ",") -FontWeight normal, bold -TextDecoration underline, none } } New-HTMLSection -HeaderText "Admin Section" { New-HTMLList { New-HTMLListItem -Text "Enabled: ", $AdminSection.Enable -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Subject: ", $TemplateAdminSubject -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager: ", $AdminSection.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email: ", ($AdminSection.Manager.EmailAddress -join ", ") -FontWeight normal, bold -TextDecoration underline, none } } } } New-HTMLTab -Name 'Rules Configuration' { New-HTMLText -Text "There are ", $Rules.Count, " rules defined in the Password Solution. ", "Please keep in mind that order of the rules matter." -FontWeight normal, bold, normal -Color None, Blue, None foreach ($Rule in $Rules) { if ($Rule.Enable) { $SectionColor = 'SpringGreen' } else { $SectionColor = 'Coral' } New-HTMLSection -HeaderText "Rule $($Rule.Name)" -CanCollapse -HeaderBackGroundColor $SectionColor { New-HTMLList { if ($Rule.Enable) { New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "enabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Green } else { New-HTMLListItem -Text "Rule ", $Rule.Name, " is ", "disabled" -FontWeight normal, bold, normal, bold, normal, normal -Color None, None, None, Red } New-HTMLList { New-HTMLListItem -Text "Notify till expiry on ", $(Format-ReminderDays -Days $Rule.Reminders), " day" -FontWeight normal, bold, normal if ($Rule.IncludeExpiring) { New-HTMLListItem -Text "Include expiring accounts is ", "enabled" -FontWeight bold, bold -Color None, Green } else { New-HTMLListItem -Text "Include expiring accounts is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.IncludePasswordNeverExpires) { New-HTMLListItem -Text "Include passwords never expiring with ", $Rule.PasswordNeverExpiresDays, " days rule" -FontWeight bold -Color Amethyst } else { New-HTMLListItem -Text "Do not include passwords that never expire." -FontWeight bold -Color Blue } if ($Rule.IncludeName.Count -gt 0 -and $Rule.IncludeNameProperties.Count -gt 0) { New-HTMLListItem -Text "Apply naming rule to require that account contains of of names ", $($Rule.IncludeName -join ", "), " in at least one property ", ($Rule.IncludeNameProperties -join ", ") -FontWeight normal, bold, normal, bold, normal -Color None, Blue, None, Blue } else { New-HTMLListItem -Text "Do not apply special name rules" -Color Blue -FontWeight bold } if ($Rule.IncludeOU) { New-HTMLListItem -Text "Apply Organizational Unit inclusion on ", ($Rule.IncludeOU -join ", ") -FontWeight normal, bold -Color None, Blue } else { New-HTMLListItem -Text "Do not apply Organizational Unit limit" -Color Blue -FontWeight bold } if ($Rule.ExcludeOU) { New-HTMLListItem -Text "Apply Organizational Unit exclusion on ", $Rule.ExcludeOU -FontWeight normal, bold -Color None, Green } else { New-HTMLListItem -Text "Do not exclude any Organizational Unit" -Color Blue -FontWeight bold } if ($Rule.IncludeGroup) { New-HTMLListItem -Text "Appply Group Membership inclusion (direct only) ", ($Rule.IncludeGroup -join ", ") } else { New-HTMLListItem -Text "Do not apply Group Membership limit" } if ($Rule.ExcludeGroup) { New-HTMLListItem -Text "Apply Group Membership exclusion (direct only): ", ($Rule.ExcludeGroup -join ", ") } else { New-HTMLListItem -Text "Do not apply Group Membership exclusion" } New-HTMLListItem -Text "Send to manager" -NestedListItems { New-HTMLList { if ($Rule.SendToManager.Manager.Enable) { New-HTMLListItem -Text "Manager ", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.Manager.Reminders.Default.Enable) { if ($Rule.SendToManager.Manager.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $(Format-ReminderDays -Days $Rule.SendToManager.Manager.Reminders.Default.Reminder), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $(Format-ReminderDays -Days $Rule.Reminders), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.Manager.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.Manager.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.Manager.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Days -join ","), " with comparison ", $TranslateOperators[$Rule.SendToManager.Manager.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.Manager.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Manager ", " is ", 'disabled' -FontWeight bold, normal, bold -Color None, None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Enable) { New-HTMLListItem -Text "Manager Escalation", " is ", 'enabled' -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.ManagerNotCompliant.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.ManagerNotCompliant.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none } New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Enable) { if ($Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $(Format-ReminderDays -Days $Rule.SendToManager.ManagerNotCompliant.Reminders.Default.Reminder), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $(Format-ReminderDays -Days $Rule.Reminders), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.ManagerNotCompliant.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Manager Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red } if ($Rule.SendToManager.SecurityEscalation.Enable) { New-HTMLListItem -Text "Security Escalation ", "is", " enabled" -FontWeight bold, normal, bold -Color None, None, Green { New-HTMLList { New-HTMLListItem -Text "Manager Name: ", $Rule.SendToManager.SecurityEscalation.Manager.DisplayName -FontWeight normal, bold -TextDecoration underline, none New-HTMLListItem -Text "Manager Email Address: ", $Rule.SendToManager.SecurityEscalation.Manager.EmailAddress -FontWeight normal, bold -TextDecoration underline, none } New-HTMLList { New-HTMLListItem -Text "Rules: " { New-HTMLList { <# if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable) { New-HTMLListItem -Text "Default: ", $Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } #> if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Enable) { if ($Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder) { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $(Format-ReminderDays -Days $Rule.SendToManager.SecurityEscalation.Reminders.Default.Reminder), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } else { New-HTMLListItem -Text "Default ", "is enabled", " sent on ", $(Format-ReminderDays -Days $Rule.Reminders), " days to expiry of user." -FontWeight normal, bold, normal, bold, normal -Color None, Green, None, Green } } else { New-HTMLListItem -Text "Default rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Enable) { New-HTMLListItem -Text @( "On day of the week ", "is ", "enabled" " on days: ", ($Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.SecurityEscalation.Reminders.OnDay.ComparisonType], ' value ', $Rule.SendToManager.SecurityEscalation.Reminders.OnDay.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of week rule is ", "disabled" -FontWeight bold, bold -Color None, Red } if ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Enable) { New-HTMLListItem -Text @( "On day of the month rule ", "is", " enabled", " on days ", ($Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Days -join ", "), " with comparison ", $TranslateOperators[$Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.ComparisonType], ' value ', $Rule.SendToManager.SecurityEscalation.Reminders.OnDayOfMonth.Reminder ) -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold -Color None, None, Green, None, Green, None, Green, None, Green } else { New-HTMLListItem -Text "On day of month rule is ", "disabled" -FontWeight bold, bold -Color None, Red } } } } } } else { New-HTMLListItem -Text "Security Escalation", " is ", "disabled" -FontWeight bold, normal, bold -Color None, None, Red } } } } } } } } } } if ($Report.ShowAllUsers) { $AllUsers = foreach ($User in $CachedUsers.Values) { if ($User.Type -eq 'Contact') { continue } $User } New-HTMLTab -Name 'All Users' { New-HTMLTable -DataTable $AllUsers -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } -ExcludeProperty $Report.ExcludeProperties -ScrollX } } if ($Report.ShowRules) { if ($Report.NestedRules) { # nested rules view under single tab if ($Summary['Rules'].Keys.Count -gt 0) { New-HTMLTab -Name 'Rules Information' { foreach ($Rule in $Summary['Rules'].Keys) { if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) { $Color = 'LawnGreen' $IconSolid = 'Star' } else { $Color = 'Salmon' $IconSolid = 'Stop' } New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } -ExcludeProperty $Report.ExcludeProperties -ScrollX } } } } } else { foreach ($Rule in $Summary['Rules'].Keys) { if ((Measure-Object -InputObject $Summary['Rules'][$Rule].Values.User).Count -gt 0) { $Color = 'LawnGreen' $IconSolid = 'Star' } else { $Color = 'Salmon' $IconSolid = 'Stop' } New-HTMLTab -Name $Rule -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $Summary['Rules'][$Rule].Values.User -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } -ExcludeProperty $Report.ExcludeProperties -ScrollX } } } } if ($Report.ShowUsersSent) { if ((Measure-Object -InputObject $SummaryUsersEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryUsersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'Disabled' -BackgroundColor LawnGreen -Value $true -ComparisonType string -HighlightHeaders 'Disabled', 'DisabledError' New-TableCondition -Name 'Disabled' -BackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'DisabledError' -BackgroundColor ColumbiaBlue -Value 'WhatIf' -ComparisonType string -HighlightHeaders 'Disabled', 'DisabledError' } -Filtering -ScrollX } } if ($Report.ShowManagersSent) { if ((Measure-Object -InputObject $SummaryManagersEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to manager' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryManagersEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'DisabledAccountsError' -BackgroundColor LawnGreen -Value 'Not disabled', 'WhatIf', '' -ComparisonType string -Operator notin -HighlightHeaders 'DisabledAccounts', 'DisabledAccountsCount', 'DisabledAccountsError' } -Filtering -ScrollX } } if ($Report.ShowEscalationSent) { if ((Measure-Object -InputObject $SummaryEscalationEmails).Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'Email sent to Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $SummaryEscalationEmails { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering -ScrollX } } if ($Report.ShowExternalSystemReplacementsUsers) { if ($ExternalSystemReplacements.Users.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'External System Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ExternalSystemReplacements.Users { #New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' #New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowExternalSystemReplacementsManagers) { if ($ExternalSystemReplacements.Managers.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'External System Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ExternalSystemReplacements.Managers { #New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' #New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering } } if ($Report.ShowSearchUsers) { [Array] $UsersSent = $SummarySearch['EmailSent'].Values #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($UsersSent.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Emails To Users' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $UsersSent { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'Disabled' -BackgroundColor LawnGreen -Value $true -ComparisonType string -HighlightHeaders 'Disabled', 'DisabledError' New-TableCondition -Name 'Disabled' -BackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'DisabledError' -BackgroundColor ColumbiaBlue -Value 'WhatIf' -ComparisonType string -HighlightHeaders 'Disabled', 'DisabledError' } -Filtering -AllProperties -ScrollX } } if ($Report.ShowSearchManagers) { [Array] $ShowSearchManagers = $SummarySearch['EmailManagers'].Values #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($ShowSearchManagers.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Emails To Managers' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ShowSearchManagers { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' New-TableCondition -Name 'DisabledAccountsError' -BackgroundColor LawnGreen -Value 'Not disabled', 'WhatIf', '' -ComparisonType string -Operator notin -HighlightHeaders 'DisabledAccounts', 'DisabledAccountsCount', 'DisabledAccountsError' } -Filtering -AllProperties -ScrollX } } if ($Report.ShowSearchEscalations) { [Array] $ShowSearchEscalations = $SummarySearch['EmailEscalations'].Values #| ForEach-Object { if ($_ -ne $null) { $_ } } if ($ShowSearchEscalations.Count -gt 0) { $Color = 'BrightTurquoise' $IconSolid = 'sticky-note' } else { $Color = 'Amaranth' $IconSolid = 'stop-circle' } New-HTMLTab -Name 'History Email To Security' -TextColor $Color -IconColor $Color -IconSolid $IconSolid { New-HTMLTable -DataTable $ShowSearchEscalations { New-TableHeader -Names 'Status', 'StatusError', 'SentTo', 'StatusWhen' -Title 'Email Summary' New-TableCondition -Name 'Status' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $true -ComparisonType string -HighlightHeaders 'Status', 'StatusWhen', 'StatusError', 'SentTo' } -Filtering -AllProperties -ScrollX } } if ($Report.ShowSkippedUsers) { New-HTMLTab -Name 'Skipped Users' -IconSolid users { $SkippedUsers = foreach ($User in $AllSkipped.Values) { if ($User.Type -ne 'Contact') { $User } } New-HTMLPanel -AlignContentText center { New-HTMLText -FontSize 15pt -Text "Those users have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. " } -Invisible New-HTMLTable -DataTable $SkippedUsers -Filtering { New-TableCondition -Name 'Enabled' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'HasMailbox' -BackgroundColor LawnGreen -FailBackgroundColor BlueSmoke -Value $true -ComparisonType string -Operator eq New-TableCondition -Name 'PasswordExpired' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'PasswordExpired' -BackgroundColor Salmon -Value $true -ComparisonType string New-TableCondition -Name 'PasswordNeverExpires' -BackgroundColor LawnGreen -FailBackgroundColor Salmon -Value $false -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor BlueSmoke -Value $true -ComparisonType string New-TableCondition -Name 'PasswordAtNextLogon' -BackgroundColor LawnGreen -Value $false -ComparisonType string New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Missing', 'Disabled' -BackgroundColor Salmon -Operator in New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Enabled' -BackgroundColor LawnGreen New-TableCondition -Name 'ManagerStatus' -HighlightHeaders Manager, ManagerSamAccountName, ManagerEmail, ManagerStatus -ComparisonType string -Value 'Not available' -BackgroundColor BlueSmoke } -ScrollX } } if ($Report.ShowSkippedLocations) { New-HTMLTab -Name 'Skipped Locations' -IconSolid building { New-HTMLPanel -AlignContentText center { New-HTMLText -FontSize 15pt -Text "Users in those Organizational Units have no password date set. This means account running expiration checks doesn't have permissions or acccout never had password set or account is set to change password on logon. " } -Invisible New-HTMLTable -DataTable $Locations.Values -Filtering { New-TableHeader -ResponsiveOperations none -Names 'Names', 'NamesExpired' } -ScrollX } } } -ShowHTML:$Report.ShowHTML -FilePath $Report.FilePath -Online:$Report.Online -WarningAction $WarningAction -TitleText $Report.Title Write-Color -Text "[i]" , " Generating HTML report ", $Report.Title, ". Done" -Color White, Yellow, Green } ================================================ FILE: Private/Send-PasswordAdminNotifications.ps1 ================================================ function Send-PasswordAdminNotifications { [CmdletBinding()] param( [System.Collections.IDictionary] $AdminSection, [scriptblock] $TemplateAdmin, [string] $TemplateAdminSubject, [string] $TimeEnd, [System.Collections.IDictionary] $EmailParameters, [Array] $HtmlAttachments, [System.Collections.IDictionary] $Logging ) if ($AdminSection.Enable) { Write-Color -Text "[i] Sending summary information " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEmail = @( $CountSecurity++ # This user is provided by user in config file $ManagerUser = $AdminSection.Manager $EmailSplat = [ordered] @{} # User uses global template $EmailSplat.Template = $TemplateAdmin $EmailSplat.Subject = $TemplateAdminSubject $EmailSplat.User = $ManagerUser $EmailSplat.SummaryUsersEmails = $SummaryUsersEmails $EmailSplat.SummaryManagersEmails = $SummaryManagersEmails $EmailSplat.SummaryEscalationEmails = $SummaryEscalationEmails $EmailSplat.TimeToProcess = $TimeEnd $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailParameters.To = $AdminSection.Manager.EmailAddress $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($HtmlAttachments.Count -gt 0) { $EmailSplat.Attachments = $HtmlAttachments } Write-Color -Text "[i] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow $EmailResult = Send-PasswordEmail @EmailSplat if ($EmailResult.Error) { Write-Color -Text "[r] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") ) (status: ", $EmailResult.Status, ", sent to: ", $EmailResult.SentTo, ", error: ", $EmailResult.Error, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } else { Write-Color -Text "[r] Sending summary information ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") ) (status: ", $EmailResult.Status, ", sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Template = 'Unknown' } ) Write-Color -Text "[i] Sending summary information (sent: ", $SummaryEmail.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White } else { Write-Color -Text "[i] Sending summary information is ", "disabled!" -Color White, Yellow, DarkMagenta } } ================================================ FILE: Private/Send-PasswordEmail.ps1 ================================================ function Send-PasswordEmail { [CmdletBinding()] param( [scriptblock] $Template, [PSCustomObject] $User, [Array] $ManagedUsers, [Array] $ManagedUsersManagerNotCompliant, [Array] $SummaryUsersEmails, [Array] $SummaryManagersEmails, [Array] $SummaryEscalationEmails, [string] $TimeToProcess, [Array] $Attachments, [System.Collections.IDictionary] $EmailParameters, [string] $Subject, [string] $EmailDateFormat, [switch] $EmailDateFormatUTCConversion ) if ($Template) { if ($User.PasswordLastSet) { if ($EmailDateFormat) { if ($EmailDateFormatUTCConversion) { $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime().ToString($EmailDateFormat) } else { $PasswordLastSet = $User.PasswordLastSet.ToString($EmailDateFormat) } } else { if ($EmailDateFormatUTCConversion) { $PasswordLastSet = $User.PasswordLastSet.ToUniversalTime() } else { $PasswordLastSet = $User.PasswordLastSet } } } else { $PasswordLastSet = $User.PasswordLastSet } if ($User.DateExpiry) { if ($EmailDateFormat) { if ($EmailDateFormatUTCConversion) { $ExpiryDate = $User.DateExpiry.ToUniversalTime().ToString($EmailDateFormat) } else { $ExpiryDate = $User.DateExpiry.ToString($EmailDateFormat) } } else { if ($EmailDateFormatUTCConversion) { $ExpiryDate = $User.DateExpiry.ToUniversalTime() } else { $ExpiryDate = $User.DateExpiry } } } else { $ExpiryDate = $User.DateExpiry } # Simplify counting for a user used variables $CountUserEmails = 0 $CountUserEmailsSent = 0 $CountUserEmailsNotSentLackOfEmail = 0 $CountUserEmailsNotSentOther = 0 foreach ($User in $SummaryUsersEmails) { $CountUserEmails++ if ($User.Status -eq $true) { $CountUserEmailsSent++ } else { if ($User.StatusError -eq 'No email address for user') { $CountUserEmailsNotSentLackOfEmail++ } else { $CountUserEmailsNotSentOther++ } } } $CountManagerEmails = $SummaryManagersEmails.Count $CountEscalationEmails = $SummaryEscalationEmails.Count $SourceParameters = [ordered] @{ ManagerDisplayName = $User.DisplayName ManagerUsersTable = $ManagedUsers ManagerUsersTableManagerNotCompliant = $ManagedUsersManagerNotCompliant SummaryEscalationEmails = $SummaryEscalationEmails SummaryManagersEmails = $SummaryManagersEmails SummaryUsersEmails = $SummaryUsersEmails TimeToProcess = $TimeToProcess # Summary counting CountUserEmails = $CountUserEmails CountUserEmailsSent = $CountUserEmailsSent CountUserEmailsNotSentLackOfEmail = $CountUserEmailsNotSentLackOfEmail CountUserEmailsNotSentOther = $CountUserEmailsNotSentOther CountManagerEmails = $CountManagerEmails CountEscalationEmails = $CountEscalationEmails # Only works if User is set UserPrincipalName = $User.UserPrincipalName # : adm.pklys@ad.evotec.xyz SamAccountName = $User.SamAccountName # : adm.pklys Domain = $User.Domain # : ad.evotec.xyz Enabled = $User.Enabled EmailAddress = $User.EmailAddress # : DateExpiry = $ExpiryDate # : DaysToExpire = $User.DaysToExpire # : PasswordExpired = $User.PasswordExpired # : False PasswordLastSet = $PasswordLastSet # : 05.09.2020 11:07:29 PasswordNotRequired = $User.PasswordNotRequired # : False PasswordNeverExpires = $User.PasswordNeverExpires # : True ManagerSamAccountName = $User.ManagerSamAccountName # : przemyslaw.klys ManagerEmail = $User.ManagerEmail # : przemyslaw.klys@test.pl ManagerStatus = $User.ManagerStatus # : Enabled ManagerLastLogonDays = $User.ManagerLastLogonDays # : 0 Manager = $User.Manager # : Przemysław Kłys DisplayName = $User.DisplayName # : Administrator Przemysław Kłys GivenName = $User.GivenName # : Administrator Przemysław Surname = $User.Surname # : Kłys OrganizationalUnit = $User.OrganizationalUnit # : OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz MemberOf = $User.MemberOf # : {CN=GDS-TestGroup4,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=GDS-TestGroup2,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz, CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz} DistinguishedName = $User.DistinguishedName # : CN=Administrator Przemysław Kłys,OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz ManagerDN = $User.ManagerDN # : CN=Przemysław Kłys,OU=Users,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz } $Body = EmailBody -EmailBody $Template -Parameter $SourceParameters # Below command would require to define variables as they are used in scriptblock #$EmailParameters.Subject = $ExecutionContext.InvokeCommand.ExpandString($Subject) # following replacement is a bit more cumbersome the the one above but a bit more secure and doesn't require creating 20+ unused variables $EmailParameters.Subject = Add-ParametersToString -String $Subject -Parameter $SourceParameters $EmailParameters.Body = $Body if ($Attachments) { $EmailParameters.Attachment = $Attachments } else { $EmailParameters.Attachment = @() } try { Send-EmailMessage @EmailParameters -ErrorAction Stop -WarningAction SilentlyContinue } catch { if ($_.Exception.Message -like "*Credential*") { Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red Write-Color -Text "[i] " , "Please make sure you have valid credentials in your configuration file (graph encryption issue?)" -Color Yellow, White, Red } else { Write-Color -Text "[e] " , "Failed to send email to $($EmailParameters.EmailParameters) because error: $($_.Exception.Message)" -Color Yellow, White, Red } } } } ================================================ FILE: Private/Send-PasswordManagerNotifications.ps1 ================================================ function Send-PasswordManagerNofifications { [CmdletBinding()] param( [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $CachedUsers, [ScriptBlock] $TemplateManager, [string] $TemplateManagerSubject, [ScriptBlock] $TemplateManagerNotCompliant, [string] $TemplateManagerNotCompliantSubject, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $Logging, [System.Collections.IDictionary] $GlobalManagersCache ) if ($ManagerSection.Enable) { Write-Color -Text "[i] Sending notifications to managers " -Color White, Yellow, White, Yellow, White, Yellow, White $CountManagers = 0 [Array] $SummaryManagersEmails = foreach ($Manager in $Summary['NotifyManager'].Keys) { $CountManagers++ if ($CachedUsers[$Manager]) { # This user is "findable" in AD $ManagerUser = $CachedUsers[$Manager] } elseif ($GlobalManagersCache[$Manager]) { # This user is findable in managers cache # This is required when user uses `FilterOrganizationalUnit` feature and manager is not in the same OU # This causes Manager Data to be not processed in the same way as User Data so we need to process it separately $ManagerUser = $GlobalManagersCache[$Manager] } else { # This user is provided by user in config file $ManagerUser = $Summary['NotifyManager'][$Manager]['Manager'] } [Array] $ManagedUsers = $Summary['NotifyManager'][$Manager]['ManagerDefault'].Values.Output [Array] $ManagedUsersManagerNotCompliant = $Summary['NotifyManager'][$Manager]['ManagerNotCompliant'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifyManager'][$Manager].ManagerDefault.Count -gt 0) { if ($TemplateManager) { # User uses global template $EmailSplat.Template = $TemplateManager } else { # User uses built-in template $EmailSplat.Template = { } } if ($TemplateManagerSubject) { $EmailSplat.Subject = $TemplateManagerSubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Manager - Your accounts are expiring!" } } elseif ($Summary['NotifyManager'][$Manager].ManagerNotCompliant.Count -gt 0) { if ($TemplateManagerNotCompliant) { # User uses global template $EmailSplat.Template = $TemplateManagerNotCompliant } else { # User uses built-in template $EmailSplat.Template = { } } if ($TemplateManagerNotCompliantSubject) { $EmailSplat.Subject = $TemplateManagerNotCompliantSubject } else { $EmailSplat.Subject = "[Password Escalation] Accounts are expiring with non-compliant manager" } } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers $EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($ManagerSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $ManagerSection.DefaultEmail } if ($Logging.NotifyOnManagerSend) { Write-Color -Text "[i] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } [Array] $DisabledAccounts = foreach ($ManagedUserDN in $Summary['NotifyManager'][$Manager].ManagerDefault.Keys) { $AccountToDisable = [PSCustomObject] @{ SamAccountName = $ManagedUser.User.SamAccountName Domain = $ManagedUser.User.Domain Disabled = $null Error = $null } $ManagedUser = $Summary['NotifyManager'][$Manager].ManagerDefault[$ManagedUserDN] if ($ManagedUser.Rule -and $null -ne $ManagedUser.Rule.DisableDays) { $CompareSuccess = $false # We need to check if the user is in the disable days list if ($ManagedUser.Rule.DisableType -eq 'in') { $CompareSuccess = $ManagedUser.User.DaysToExpire -in $ManagedUser.Rule.DisableDays } elseif ($ManagedUser.Rule.DisableType -eq 'lt') { $CompareSuccess = $ManagedUser.User.DaysToExpire -lt $ManagedUser.Rule.DisableDays } elseif ($ManagedUser.Rule.DisableType -eq 'gt') { $CompareSuccess = $ManagedUser.User.DaysToExpire -gt $ManagedUser.Rule.DisableDays } elseif ($ManagedUser.Rule.DisableType -eq 'eq') { $CompareSuccess = $ManagedUser.User.DaysToExpire -eq $ManagedUser.Rule.DisableDays } else { Write-Color -Text "[r] Unknown disable type: ", $ManagedUser.Rule.DisableType, " for user ", $ManagedUser.User.DisplayName, " (", $ManagedUser.User.UserPrincipalName, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } if ($CompareSuccess) { # Write-Color -Text "[i] Disabling user ", $ManagedUser.User.DisplayName, " (", $ManagedUser.User.UserPrincipalName, ") (DaysToExpire: ", $ManagedUser.User.DaysToExpire, ")" -Color Yellow, White, Magenta, White, Magenta, White, White, Blue #$ManagedUser.User if ($ManagedUser.Rule.DisableWhatIf) { Write-Color -Text "[i] Disabling user ", $ManagedUser.User.DisplayName, " (", $ManagedUser.User.UserPrincipalName, ")", " would be disabled" -Color Cyan, White, Red, Cyan, Red, Yellow $AccountToDisable.Disabled = $false $AccountToDisable.Error = "WhatIf" } else { # Disable the user Write-Color -Text "[i] Disabling user ", $ManagedUser.User.DisplayName, " (", $ManagedUser.User.UserPrincipalName, ")" -Color Cyan, White, Magenta, White, Magenta, White, White, Blue if ($ManagedUser.User.Enabled) { try { Disable-ADAccount -Identity $ManagedUser.User.DistinguishedName -Confirm:$false -WhatIf:$ManagedUser.Rule.DisableWhatIf -ErrorAction Stop $AccountToDisable.Disabled = $true $AccountToDisable.Error = $null } catch { $AccountToDisable.Disabled = $false $AccountToDisable.Error = $_.Exception.Message Write-Color -Text "[r] Disabling user ", $ManagedUser.User.DisplayName, " (", $ManagedUser.User.UserPrincipalName, ") failed: ", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } } else { $AccountToDisable.Disabled = $false $AccountToDisable.Error = "Already disabled" Write-Color -Text "[i] User is already disabled: ", $ManagedUser.User.DisplayName, " (", $ManagedUser.User.UserPrincipalName, ")" -Color Cyan, White, Magenta, White, Magenta, White, White, Blue } } $AccountToDisable } } } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnManagerSend) { if ($EmailResult.Error) { if ($EmailResult.SentTo) { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ", error: ", $EmailResult.Error, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } else { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, ", error: ", $EmailResult.Error, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } } else { if ($EmailResult.SentTo) { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } else { Write-Color -Text "[r] Sending notifications to managers ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } } } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count DisabledAccounts = $DisabledAccounts.SamAccountName DisabledAccountsCount = $DisabledAccounts.Count DisabledAccountsError = $DisabledAccounts.Error | Sort-Object -Unique Template = 'Unknown' ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName #ManagerMissingCount = $ManagedUsersManagerMissing.Count #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count } if ($ManagerSection.SendCountMaximum -gt 0) { if ($ManagerSection.SendCountMaximum -le $CountManagers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkRed break } } } Write-Color -Text "[i] Sending notifications to managers (sent: ", $SummaryManagersEmails.Count, " out of ", $Summary['NotifyManager'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White $SummaryManagersEmails } else { Write-Color -Text "[i] Sending notifications to managers is ", "disabled!" -Color White, Yellow, DarkRed } } ================================================ FILE: Private/Send-PasswordSecurityNotifications.ps1 ================================================ function Send-PasswordSecurityNotifications { [CmdletBinding()] param( [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $Summary, [ScriptBlock] $TemplateSecurity, [string] $TemplateSecuritySubject, [System.Collections.IDictionary] $Logging ) if ($SecuritySection.Enable) { Write-Color -Text "[i] Sending notifications to security " -Color White, Yellow, White, Yellow, White, Yellow, White $CountSecurity = 0 [Array] $SummaryEscalationEmails = foreach ($Manager in $Summary['NotifySecurity'].Keys) { $CountSecurity++ # This user is provided by user in config file $ManagerUser = $Summary['NotifySecurity'][$Manager]['Manager'] [Array] $ManagedUsers = $Summary['NotifySecurity'][$Manager]['Security'].Values.Output $EmailSplat = [ordered] @{} if ($Summary['NotifySecurity'][$Manager].Security.Count -gt 0) { # User uses global template $EmailSplat.Template = $TemplateSecurity if ($TemplateSecuritySubject) { $EmailSplat.Subject = $TemplateSecuritySubject } else { $EmailSplat.Subject = "[Password Expiring] Dear Security - Accounts expired" } } else { continue } if ($SecuritySection.AttachCSV -and $ManagedUsers.Count -gt 0) { $ManagedUsers | Export-Csv -LiteralPath $Env:TEMP\ManagedUsersSecurity.csv -NoTypeInformation -Force -Encoding UTF8 -ErrorAction Stop $EmailSplat.Attachments = @( if (Test-Path -LiteralPath "$Env:TEMP\ManagedUsersSecurity.csv") { "$Env:TEMP\ManagedUsersSecurity.csv" } ) } $EmailSplat.User = $ManagerUser $EmailSplat.ManagedUsers = $ManagedUsers | Select-Object -Property 'Status', 'DisplayName', 'Enabled', 'SamAccountName', 'Domain', 'DateExpiry', 'DaysToExpire', 'PasswordLastSet', 'PasswordExpired' #$EmailSplat.ManagedUsersManagerNotCompliant = $ManagedUsersManagerNotCompliant #$EmailSplat.ManagedUsersManagerDisabled = $ManagedUsersManagerDisabled #$EmailSplat.ManagedUsersManagerMissing = $ManagedUsersManagerMissing #$EmailSplat.ManagedUsersManagerMissingEmail = $ManagedUsersManagerMissingEmail $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($SecuritySection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $ManagerUser.EmailAddress } else { $EmailSplat.EmailParameters.To = $SecuritySection.DefaultEmail } if ($Logging.NotifyOnSecuritySend) { Write-Color -Text "[i] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } $EmailResult = Send-PasswordEmail @EmailSplat if ($Logging.NotifyOnSecuritySend) { if ($EmailResult.Error) { Write-Color -Text "[r] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ", error: ", $EmailResult.Error, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } else { Write-Color -Text "[r] Sending notifications to security ", $ManagerUser.DisplayName, " (", $ManagerUser.EmailAddress, ") (SendToDefaultEmail: ", $ManagerSection.SendToDefaultEmail, ") (status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ")" -Color White, Yellow, White, Yellow, White, Yellow, White, Yellow, White, Yellow } } [PSCustomObject] @{ DisplayName = $ManagerUser.DisplayName SamAccountName = $ManagerUser.SamAccountName Domain = $ManagerUser.Domain Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" SentTo = $EmailResult.SentTo StatusError = $EmailResult.Error Accounts = $ManagedUsers.SamAccountName AccountsCount = $ManagedUsers.Count Template = 'Unknown' # ManagerNotCompliant = $ManagedUsersManagerNotCompliant.SamAccountName # ManagerNotCompliantCount = $ManagedUsersManagerNotCompliant.Count #ManagerDisabled = $ManagedUsersManagerDisabled.SamAccountName #ManagerDisabledCount = $ManagedUsersManagerDisabled.Count #ManagerMissing = $ManagedUsersManagerMissing.SamAccountName #ManagerMissingCount = $ManagedUsersManagerMissing.Count #ManagerMissingEmail = $ManagedUsersManagerMissingEmail.SamAccountName #ManagerMissingEmailCount = $ManagedUsersManagerMissingEmail.Count } if ($SecuritySection.SendCountMaximum -gt 0) { if ($SecuritySection.SendCountMaximum -le $CountSecurity) { Write-Color -Text "[i]", " Send count maximum reached. There may be more managers that match the rule." -Color Red, DarkRed break } } } Write-Color -Text "[i] Sending notifications to security (sent: ", $SummaryEscalationEmails.Count, " out of ", $Summary['NotifySecurity'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White $SummaryEscalationEmails } else { Write-Color -Text "[i] Sending notifications to security is ", "disabled!" -Color White, Yellow, DarkRed } } ================================================ FILE: Private/Send-PasswordUserNotifications.ps1 ================================================ function Send-PasswordUserNofifications { [CmdletBinding()] param( [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $Summary, [System.Collections.IDictionary] $Logging, [ScriptBlock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptBlock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [System.Collections.IDictionary] $EmailParameters ) if ($UserSection.Enable) { Write-Color -Text "[i] Sending notifications to users " -Color White, Yellow, White, Yellow, White, Yellow, White $CountUsers = 0 [Array] $SummaryUsersEmails = foreach ($Notify in $Summary['Notify'].Values) { $CountUsers++ $User = $Notify.User $Rule = $Notify.Rule # This shouldn't happen, but just in case, to be removed later on, as ProcessManagerOnly is skipping earlier on if ($Notify.ProcessManagersOnly -eq $true) { if ($Logging.NotifyOnSkipUserManagerOnly) { Write-Color -Text "[i]", " Skipping User (Manager Only - $($Rule.Name)) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Magenta, White, Magenta, White, White, Blue } continue } $EmailSplat = [ordered] @{} if ($Notify.User.DaysToExpire -ge 0) { if ($Notify.Rule.TemplatePreExpiry) { # User uses template per rule $EmailSplat.Template = $Notify.Rule.TemplatePreExpiry } elseif ($TemplatePreExpiry) { # User uses global template $EmailSplat.Template = $TemplatePreExpiry } else { # User uses built-in template $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePreExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePreExpirySubject } elseif ($TemplatePreExpirySubject) { $EmailSplat.Subject = $TemplatePreExpirySubject } else { $EmailSplat.Subject = '[Password] Your password will expire on $DateExpiry ($DaysToExpire days)' } } else { if ($Notify.Rule.TemplatePostExpiry) { $EmailSplat.Template = $Notify.Rule.TemplatePostExpiry } elseif ($TemplatePostExpiry) { $EmailSplat.Template = $TemplatePostExpiry } else { $EmailSplat.Template = { } } if ($Notify.Rule.TemplatePostExpirySubject) { $EmailSplat.Subject = $Notify.Rule.TemplatePostExpirySubject } elseif ($TemplatePostExpirySubject) { $EmailSplat.Subject = $TemplatePostExpirySubject } else { $EmailSplat.Subject = '[Password] Your password expired on $DateExpiry ($DaysToExpire days ago)' } } $EmailSplat.User = $Notify.User $EmailSplat.EmailParameters = $EmailParameters $EmailSplat.EmailDateFormat = $Logging.EmailDateFormat $EmailSplat.EmailDateFormatUTCConversion = $Logging.EmailDateFormatUTCConversion if ($UserSection.SendToDefaultEmail -ne $true) { $EmailSplat.EmailParameters.To = $Notify.User.EmailAddress } else { $EmailSplat.EmailParameters.To = $UserSection.DefaultEmail } $Disabled = $null $DisabledError = $null if ($null -ne $Rule.DisableDays) { if ($User.DaysToExpire -in $Rule.DisableDays) { Write-Color -Text "[i]", " Disabling User (DisableDays - $($Rule.Name)) ", $User.DisplayName, " (", $User.UserPrincipalName, ")", " days to expire: ", $User.DaysToExpire -Color Yellow, White, Magenta, White, Magenta, White, White, Blue if ($Rule.DisableWhatIf) { Write-Color -Text "[i]", " Disabling user ", $User.DisplayName, " (", $User.UserPrincipalName, ") would be disabled" -Color Yellow, White, Red $Disabled = $false $DisabledError = 'WhatIf' } else { if ($User.Enabled) { try { Disable-ADAccount -Identity $User.DistinguishedName -Confirm:$false -ErrorAction Stop $Disabled = $true $DisabledError = $null } catch { $Disabled = $false $DisabledError = $_.Exception.Message Write-Color -Text "[e]", " Disabling user ", $User.DisplayName, " (", $User.UserPrincipalName, ") failed because of error: ", $_.Exception.Message -Color Yellow, White, Red } } else { $Disabled = $false $DisabledError = 'Already disabled' Write-Color -Text "[i]", " User ", $User.DisplayName, " (", $User.UserPrincipalName, ") is already disabled" -Color Yellow, White, Red } } } } if ($Notify.User.EmailAddress -like "*@*") { # Regardless if we send email to default email or to user, if user doesn't have email address we shouldn't send an email $EmailResult = Send-PasswordEmail @EmailSplat [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet EmailFrom = $EmailSplat.User.EmailFrom Disabled = $Disabled DisabledError = $DisabledError } } else { # Email not sent $EmailResult = @{ Status = $false Error = 'No email address for user' SentTo = '' } [PSCustomObject] @{ UserPrincipalName = $EmailSplat.User.UserPrincipalName SamAccountName = $EmailSplat.User.SamAccountName Domain = $EmailSplat.User.Domain Rule = $Notify.Rule.Name Status = $EmailResult.Status StatusWhen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" StatusError = $EmailResult.Error SentTo = $EmailResult.SentTo DateExpiry = $EmailSplat.User.DateExpiry DaysToExpire = $EmailSplat.User.DaysToExpire PasswordExpired = $EmailSplat.User.PasswordExpired PasswordNeverExpires = $EmailSplat.User.PasswordNeverExpires PasswordLastSet = $EmailSplat.User.PasswordLastSet EmailFrom = $EmailSplat.User.EmailFrom Disabled = $Disabled DisabledError = $DisabledError } } if ($Logging.NotifyOnUserSend) { if ($EmailResult.SentTo) { if ($EmailResult.Error) { Write-Color -Text "[i]", " Sending notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo, ", details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue } else { Write-Color -Text "[i]", " Sending notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " sent to: ", $EmailResult.SentTo -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White } } else { Write-Color -Text "[i]", " Skipping notifications to user ", $Notify.User.DisplayName, " (", $Notify.User.EmailAddress, ")", " status: ", $EmailResult.Status, " details: ", $EmailResult.Error -Color Yellow, White, Yellow, White, Yellow, White, White, Blue, White, Blue } } if ($UserSection.SendCountMaximum -gt 0) { if ($UserSection.SendCountMaximum -le $CountUsers) { Write-Color -Text "[i]", " Send count maximum reached. There may be more accounts that match the rule." -Color Red, DarkRed break } } } Write-Color -Text "[i] Sending notifications to users (sent: ", $SummaryUsersEmails.Count, " out of ", $Summary['Notify'].Values.Count, ")" -Color White, Yellow, White, Yellow, White, Yellow, White $SummaryUsersEmails } else { Write-Color -Text "[i] Sending notifications to users is ", "disabled!" -Color White, Yellow, DarkRed } } ================================================ FILE: Private/Set-PasswordConfiguration.ps1 ================================================ function Set-PasswordConfiguration { [CmdletBinding()] param( [System.Collections.IDictionary] $Logging, [scriptblock] $ConfigurationDSL, [scriptblock] $TemplatePreExpiry, [string] $TemplatePreExpirySubject, [scriptblock] $TemplatePostExpiry, [string] $TemplatePostExpirySubject, [scriptblock] $TemplateManager, [string] $TemplateManagerSubject, [scriptblock] $TemplateSecurity, [string] $TemplateSecuritySubject, [scriptblock] $TemplateManagerNotCompliant, [string] $TemplateManagerNotCompliantSubject, [scriptblock] $TemplateAdmin, [string] $TemplateAdminSubject, [System.Collections.IDictionary] $EmailParameters, [System.Collections.IDictionary] $UserSection, [System.Collections.IDictionary] $ManagerSection, [System.Collections.IDictionary] $SecuritySection, [System.Collections.IDictionary] $AdminSection, [System.Collections.IDictionary] $UsersExternalSystem, [Array] $HTMLReports, [Array] $Rules, [string] $SearchPath, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [string[]] $FilterOrganizationalUnit, [string[]] $SearchBase, [System.Collections.IDictionary] $Entra ) if (-not $Rules) { $Rules = @() # not worth the effort for generic list } if (-not $HTMLReports) { $HTMLReports = @() # not worth the effort for generic list } if ($ConfigurationDSL) { try { $ConfigurationExecuted = & $ConfigurationDSL foreach ($Configuration in $ConfigurationExecuted) { if ($Configuration.Type -eq 'PasswordConfigurationOption') { if ($Configuration.Settings.SearchPath) { $SearchPath = $Configuration.Settings.SearchPath } if ($Configuration.Settings.OverwriteEmailProperty) { $OverwriteEmailProperty = $Configuration.Settings.OverwriteEmailProperty } if ($Configuration.Settings.OverwriteManagerProperty) { $OverwriteManagerProperty = $Configuration.Settings.OverwriteManagerProperty } if ($Configuration.Settings.FilterOrganizationalUnit) { $FilterOrganizationalUnit = $Configuration.Settings.FilterOrganizationalUnit } if ($Configuration.Settings.SearchBase) { $SearchBase = $Configuration.Settings.SearchBase } foreach ($Setting in $Configuration.Settings.Keys) { if ($Setting -notin 'SearchPath', 'OverwriteEmailProperty', 'OverwriteManagerProperty', 'FilterOrganizationalUnit') { $Logging[$Setting] = $Configuration.Settings[$Setting] } } } elseif ($Configuration.Type -eq 'PasswordConfigurationEmail') { $EmailParameters = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeUser') { $UserSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeManager') { $ManagerSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeSecurity') { $SecuritySection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationTypeAdmin') { $AdminSection = $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationReport') { $HTMLReports += $Configuration.Settings } elseif ($Configuration.Type -eq 'PasswordConfigurationRule') { if ($Configuration.Error) { return } $Rules += $Configuration.Settings } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePreExpiry") { $TemplatePreExpiry = $Configuration.Settings.Template $TemplatePreExpirySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplatePostExpiry") { $TemplatePostExpiry = $Configuration.Settings.Template $TemplatePostExpirySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManager") { $TemplateManager = $Configuration.Settings.Template $TemplateManagerSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateSecurity") { $TemplateSecurity = $Configuration.Settings.Template $TemplateSecuritySubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateManagerNotCompliant") { $TemplateManagerNotCompliant = $Configuration.Settings.Template $TemplateManagerNotCompliantSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq "PasswordConfigurationTemplateAdmin") { $TemplateAdmin = $Configuration.Settings.Template $TemplateAdminSubject = $Configuration.Settings.Subject } elseif ($Configuration.Type -eq 'ExternalUsers') { $UsersExternalSystem = $Configuration } elseif ($Configuration.Type -eq 'PasswordConfigurationEntra') { $Entra = $Configuration.Settings } } } catch { Write-Color -Text "[e]", " Processing configuration failed because of error in line ", $_.InvocationInfo.ScriptLineNumber, " in ", $_.InvocationInfo.InvocationName, " with message: ", $_.Exception.Message -Color Yellow, White, Red return } } if (-not $TemplatePreExpiry) { Write-Color -Text "[i]", " TemplatePreExpiry not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePreExpiry = { EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password will expire in $DaysToExpire days and if you do not change it, you will not be able to connect to the Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Internal Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Internal Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplatePreExpirySubject) { Write-Color -Text "[i]", " TemplatePreExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePreExpirySubject = '[Password Expiring] Your password will expire on $DateExpiry ($DaysToExpire days)' } if (-not $TemplatePostExpiry) { Write-Color -Text "[i]", " TemplatePostExpiry not defined. Using default template (built-in)" -Color Yellow, Red # Template to user when sending email to user after password expires $TemplatePostExpiry = { EmailText -LineBreak EmailText -Text "Dear ", "$DisplayName," -LineBreak EmailText -Text "Your password already expired on $PasswordLastSet. If you do not change it, you will not be able to connect to the Network and IT services. " EmailText -Text "Depending on your situation, please follow one of the methods below to change your password." -LineBreak EmailText -Text "If you are connected to the Network (either directly or through VPN):" EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "Type in your old password and then type the new one according to the password policy (twice)" EmailListItem -Text "After the change is complete you will be prompted with information that the password has been changed" } EmailText -Text "If you are not connected to the Internal Network:" EmailList { EmailListItem -Text "Open [Password Change Link](https://account.activedirectory.windowsazure.com/ChangePassword.aspx) using your web browser" EmailListItem -Text "Login using your current credentials" EmailListItem -Text "On the change password form, type your old password and the new password that you want to set (twice)" EmailListItem -Text "Click Submit" } EmailText -Text "Please also remember to modify your password on the email configuration of your Smartphone or Tablet." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplatePostExpirySubject) { Write-Color -Text "[i]", " TemplatePostExpirySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplatePostExpirySubject = '[Password Expired] Your password expired on $DateExpiry ($DaysToExpire days ago)' } # Template to security team with all service accounts that have expired passwords and password never expires set to true if (-not $TemplateSecurity) { Write-Color -Text "[i]", " TemplateSecurity not defined. Using default template (built-in)" -Color Yellow, Red $TemplateSecurity = { EmailText -LineBreak EmailText -Text "Hello ", "$ManagerDisplayName", "," -LineBreak -FontWeight normal, bold, normal EmailText -Text @( "Below is a summary of ", "all service accounts", " where the passwords have exceeded the time limit stipulated in the password policy. These accounts are all in violation of the policy and immediate action/escalation should take place." ) -LineBreak -FontWeight normal, bold, normal EmailText -Text "It has been agreed that the ", "password never expires", " flag has been set to ", "true", " to avoid business disruption/loss of service. As a result we require your escalation to the managers of the account to take immediate action to change the password ASAP." -LineBreak -FontWeight normal, bold, normal, bold, normal EmailText -Text "Numerous automated reminders have been sent to the Manager, but no response/action has been taken yet." -LineBreak EmailText -Text "Please reach out directly to the manager/site to ensure that these passwords are changed immediately." -LineBreak EmailText -Text "If there is still lack of responses/action taken, it will be in your (IT Security) discretion to disable the account(s) question and take any appropriate action." -LineBreak -FontWeight bold EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text "Many thanks in advance." -LineBreak EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplateSecuritySubject) { Write-Color -Text "[i]", " TemplateSecuritySubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplateSecuritySubject = "[Passsword Expired] Following accounts are expired!" } if (-not $TemplateManager) { Write-Color -Text "[i]", " TemplateManager not defined. Using default template (built-in)" -Color Yellow, Red $TemplateManager = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where the password is due to expire soon. These accounts are either:" EmailList { EmailListItem -Text 'Managed by you' EmailListItem -Text 'You are the manager of the owner of these accounts.' } EmailText -Text "Where you are the owner, please action the password change on each account outlined below, according to the rules specified by Password Policy." -LineBreak EmailTable -DataTable $ManagerUsersTable -HideFooter EmailText -LineBreak EmailText -Text @( "Please note that for Service Accounts, even though the ", "'password never expires' " "flag remains set to " "'true' " ", the password MUST be changed before the expiry date specified in the above table. " "It is the responsibility of the manager of the account to ensure that this takes place. " ) -FontWeight normal, bold, normal, bold, normal, normal -LineBreak EmailText -Text @( "Please make an effort " "to change password yourself using known methods rather than asking the Service Desk to change the password for you. " "If password is changed by Service Desk agent, there are at least 2 people knowing the password - Service Desk Agent and You! " "Do you really want the Service Desk agent to know the password to critical system you manage/own? " "Be responsible!" ) -FontWeight bold, normal, normal, normal, bold -LineBreak -Color None, None, None, None, Red EmailText -Text "One of the ways to change the password is: " -FontWeight bold EmailList { EmailListItem -Text "Press CTRL+ALT+DEL" EmailListItem -Text "Choose Change password" EmailListItem -Text "In the account name - change it to the account you want to change password for." -FontWeight bold EmailListItem -Text "Type in current password for the account and then type the new one according to the rules specified in the password policy." EmailListItem -Text "After the change is complete you will be provided with information that the password has been changed" } EmailText -Text "Failure to take action could result in loss of service/escalation to the IT Security team." -LineBreak -FontWeight bold EmailText -Text "Kind regards," EmailText -Text "IT Service Desk" } } if (-not $TemplateManagerSubject) { Write-Color -Text "[i]", " TemplateManagerSubject not defined. Using default template (built-in)" -Color Yellow, Red $TemplateManagerSubject = "[Passsword Expiring] Accounts you manage/own are expiring or already expired" } # Template to Service Desk with information about manager missing, disabled, last logon >90 days, missing email for service accounts if (-not $TemplateManagerNotCompliant) { $TemplateManagerNotCompliant = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Below is a summary of accounts where there is missing 'critical' information. These accounts are either:" EmailList { EmailListItem -Text "Missing a Manager in the AD - please add an active manager" EmailListItem -Text "The Manager in AD is Disabled - please add an active manager" EmailListItem -Text "Manager Last logon >90 days - please confirm if the manager is still an employee/change the manager to an active manager" EmailListItem -Text "Manager is missing email - add manager email" } EmailText -Text "Please contact the respective local IT Service Desk (outlined in the below table) to update this Manager's attributes in the AD directly. The suggested action to take can be found in the below table." -LineBreak EmailTable -DataTable $ManagerUsersTableManagerNotCompliant -HideFooter EmailText -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } } if (-not $TemplateManagerNotCompliantSubject) { $TemplateManagerNotCompliantSubject = "[Password Escalation] Accounts are expiring with non-compliant manager" } if (-not $TemplateAdmin) { # Template to Admins with information summarizing what happened $TemplateAdmin = { EmailText -LineBreak EmailText -Text "Hello $ManagerDisplayName," -LineBreak EmailText -Text "Here's the summary of password notifications:" EmailList { EmailListItem -Text "Found users matching rule to send emails: ", $SummaryUsersEmails.Count EmailListItem -Text "Sent emails to users: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $true }).Count EmailListItem -Text "Couldn't send emails because of no email: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -eq 'No email address for user' }).Count EmailListItem -Text "Couldn't send emails because other reasons: ", ($SummaryUsersEmails | Where-Object { $_.Status -eq $false -and $_.StatusError -ne 'No email address for user' }).Count EmailListItem -Text "Sent emails to managers: ", $SummaryManagersEmails.Count EmailListItem -Text "Sent emails to security: ", $SummaryEscalationEmails.Count } EmailText -Text "It took ", $TimeToProcess , " seconds to process the template." -LineBreak EmailText -Text "Hope everything works correctly! " -LineBreak EmailText -Text "Kind regards," -LineBreak EmailText -Text "IT Service Desk" -LineBreak } if (-not $TemplateAdminSubject) { $TemplateAdminSubject = '[Password Summary] Passwords summary' } } # Lets return information to the caller $OutputInformation = [ordered] @{ EmailParameters = $EmailParameters UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection HTMLReports = $HTMLReports Rules = $Rules SearchPath = $SearchPath OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty Logging = $Logging TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject UsersExternalSystem = $UsersExternalSystem FilterOrganizationalUnit = $FilterOrganizationalUnit SearchBase = $SearchBase Entra = $Entra } $OutputInformation } ================================================ FILE: Public/Find-Password.ps1 ================================================ function Find-Password { <# .SYNOPSIS Scan Active Directory forest for all users and their password expiration date .DESCRIPTION Scan Active Directory forest for all users and their password expiration date. This function retrieves detailed information about user accounts from Active Directory, including password status, manager information, and email addresses. It can scan multiple domains, filter by organizational units, and integrate with external systems. .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing. This is useful when you already have forest information cached. .PARAMETER OverwriteEmailProperty Overwrite EmailAddress property with different property name. Useful when the email address is stored in a non-standard attribute. .PARAMETER OverwriteManagerProperty Overwrite Manager property with different property name. Can use DistinguishedName or SamAccountName. This is useful for service accounts or other accounts that don't have a standard manager field in AD. .PARAMETER RulesProperties Add additional properties to be returned from rules. These properties will be added to the output objects for use in further processing. .PARAMETER UsersExternalSystem A dictionary containing external user information that can be used to supplement AD data. This allows integration with external identity management systems. .PARAMETER ExternalSystemReplacements A dictionary that tracks email replacements made from external systems. This helps maintain a record of changes for auditing purposes. .PARAMETER FilterOrganizationalUnit Filter results to include only users from specified organizational units. This helps reduce processing time for large domains when only specific OUs are needed. .PARAMETER SearchBase Limit the search to specific containers in Active Directory. Provides an alternative way to filter results by specifying exact search paths. .PARAMETER AsHashTable Return results as a hashtable instead of objects. This is useful for lookups when you need to quickly find specific users. .PARAMETER AsHashTableObject Return results as a hashtable of ordered dictionaries instead of PSObjects. This gives more flexibility when manipulating the results programmatically. .PARAMETER AddEmptyProperties Add empty properties to the output objects. Useful when you need consistent object properties even when some data is missing. .PARAMETER ReturnObjectsType Specify which types of objects to return (Users, Contacts). Default is both Users and Contacts. .PARAMETER Cache A dictionary used to cache user information for faster processing. Particularly useful when dealing with large datasets. .PARAMETER HashtableField Field to use as the key when returning results as a hashtable. Default is 'DistinguishedName'. .PARAMETER CacheManager A dictionary used to cache manager information for faster processing. .PARAMETER Replacements A list of replacement configurations for property values. Allows for standardizing or translating values in the result set. .EXAMPLE Find-Password | Format-Table Retrieves all users from the current forest and displays them in a table format. .EXAMPLE Find-Password -IncludeDomains "contoso.com" -FilterOrganizationalUnit "OU=Sales,DC=contoso,DC=com" Retrieves users only from the contoso.com domain, filters to only those in the Sales OU, and then further filters to show only users with expired passwords. .EXAMPLE $replacements = @( New-PasswordConfigurationReplacement -PropertyName 'Country' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' } -OverwritePropertyName 'CountryCode' ) Find-Password -Replacements $replacements | Select-Object SamAccountName, CountryCode, Country Retrieves users and replaces country codes with full country names using the specified replacement configuration. .NOTES This function is part of the PasswordSolution module. It requires Active Directory PowerShell module to be installed. Performance considerations: - For large environments, consider using filters like FilterOrganizationalUnit or SearchBase - Pre-caching forest information can speed up execution when running multiple queries - Using AsHashTable can improve lookup performance for subsequent operations For integration with external systems, use the UsersExternalSystem parameter with a properly configured external user repository. #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [string] $OverwriteEmailProperty, [Parameter(DontShow)][switch] $AsHashTable, [Parameter(DontShow)][string] $HashtableField = 'DistinguishedName', [ValidateSet('Users', 'Contacts')][string[]] $ReturnObjectsType = @('Users', 'Contacts'), [Parameter(DontShow)][switch] $AsHashTableObject, [Parameter(DontShow)][string[]] $AddEmptyProperties = @(), [Parameter(DontShow)][string[]] $RulesProperties, [string] $OverwriteManagerProperty, [Parameter(DontShow)][System.Collections.IDictionary] $UsersExternalSystem, [Parameter(DontShow)][System.Collections.IDictionary] $ExternalSystemReplacements = [ordered] @{ Managers = [System.Collections.Generic.List[PSCustomObject]]::new() Users = [System.Collections.Generic.List[PSCustomObject]]::new() }, [string[]] $FilterOrganizationalUnit, [string[]] $SearchBase, [System.Collections.IDictionary] $Cache = [ordered] @{}, [System.Collections.IDictionary] $CacheManager = [ordered] @{}, [System.Collections.IDictionary[]] $Replacements ) $ExternalSystemManagers = [ordered]@{} if ($UsersExternalSystem.Name) { Write-Color -Text '[i] ', "Using external system ", $UsersExternalSystem.Name, " for EMAIL replacement functionality" -Color Yellow, White, Yellow, White Write-Color -Text '[i] ', "There are ", $UsersExternalSystem.Users.Count, " users in the external system" -Color Yellow, White, Yellow, White } if (-not $ExternalSystemReplacements.Users) { $ExternalSystemReplacements.Users = [System.Collections.Generic.List[PSCustomObject]]::new() } if (-not $ExternalSystemReplacements.Managers) { $ExternalSystemReplacements.Managers = [System.Collections.Generic.List[PSCustomObject]]::new() } $Today = Get-Date $GuidForExchange = Convert-ADSchemaToGuid -SchemaName 'msExchMailboxGuid' if ($GuidForExchange) { $ExchangeProperty = 'msExchMailboxGuid' } $CachedReplacements = [ordered] @{} $CachedReplacementsProperties = [System.Collections.Generic.List[string]]::new() foreach ($ReplacementItem in $Replacements.Settings) { $CachedReplacementsProperties.Add($ReplacementItem.PropertyName) $CachedReplacements[$ReplacementItem.PropertyName] = [ordered] @{ OverwritePropertyName = $ReplacementItem.OverwritePropertyName } foreach ($Replacement in $ReplacementItem.PropertyReplacementHash.Keys) { $CachedReplacements[$ReplacementItem.PropertyName][$Replacement] = $ReplacementItem.PropertyReplacementHash[$Replacement] } } $Properties = @( 'Manager', 'DisplayName', 'GivenName', 'Surname', 'SamAccountName', 'EmailAddress', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordExpired', 'PasswordLastSet', 'PasswordNotRequired', 'Enabled', 'PasswordNeverExpires', 'Mail', 'MemberOf', 'LastLogonDate', 'Name' 'userAccountControl' 'pwdLastSet', 'ObjectClass' 'LastLogonDate' 'Country' if ($UsersExternalSystem -and $UsersExternalSystem.Type -eq 'ExternalUsers') { $UsersExternalSystem.ActiveDirectoryProperty } if ($ExchangeProperty) { $ExchangeProperty } if ($OverwriteEmailProperty) { $OverwriteEmailProperty } if ($OverwriteManagerProperty) { $OverwriteManagerProperty } foreach ($Rule in $RulesProperties) { $Rule } if ($CachedReplacementsProperties.Count -gt 0) { foreach ($PropertyName in $CachedReplacementsProperties) { $PropertyName } } ) $Properties = $Properties | Sort-Object -Unique # lets build extended properties that need [Array] $ExtendedProperties = foreach ($Rule in $RulesProperties) { $Rule } [Array] $ExtendedProperties = $ExtendedProperties | Sort-Object -Unique $PropertiesContacts = @( 'SamAccountName', 'CanonicalName', 'WhenChanged', 'WhenChanged', 'DisplayName', 'DistinguishedName', 'Name', 'Mail', 'ObjectClass' ) # We're caching all users in their inital form to make sure it's speedy gonzales when querying for Managers if (-not $Cache) { $Cache = [ordered] @{ } } # We're caching all processed users to make sure it's easier later on to find users if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } Write-Color -Text '[i] ', "Discovering forest information" -Color Yellow, White $ForestInformation = Get-WinADForestDetails -PreferWritable -Extended -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation $SearchBaseCache = [ordered]@{} if ($SearchBase) { foreach ($S in $SearchBase) { $ConvertedS = ConvertFrom-DistinguishedName -DistinguishedName $S -ToDomainCN if (-not $SearchBaseCache[$ConvertedS]) { $SearchBaseCache[$ConvertedS] = [System.Collections.Generic.List[string]]::new() } $SearchBaseCache[$ConvertedS].Add($S) } } # lets get domain name / netbios hashtable for easy use $DNSNetBios = @{ } foreach ($NETBIOS in $ForestInformation.DomainsExtendedNetBIOS.Keys) { $DNSNetBios[$ForestInformation.DomainsExtendedNetBIOS[$NETBIOS].DnsRoot] = $NETBIOS } [Array] $Users = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] if ($SearchBase) { foreach ($SB in $SearchBaseCache[$Domain]) { Write-Color -Text "[i] ", "Getting users from ", "$($Domain)", " using ", $Server, " and SearchBase ", $SB -Color Yellow, White, Yellow, White, Yellow, White, Yellow try { Get-ADUser -Server $Server -Filter '*' -SearchBase $SB -Properties $Properties -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } } else { Write-Color -Text "[i] ", "Getting users from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White try { Get-ADUser -Server $Server -Filter '*' -Properties $Properties -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } } Write-Color -Text "[i] ", "Caching users for easy access" -Color Yellow, White foreach ($User in $Users) { $Cache[$User.DistinguishedName] = $User # SAmAccountName will overwrite itself when we have multiple domains and there are duplicates # but sicne we use only on in case manager is used in special fields such as extensionAttribute, it shouldn't affect much $Cache[$User.SamAccountName] = $User } if ($ReturnObjectsType -contains 'Contacts') { [Array] $Contacts = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] if ($SearchBase) { foreach ($SB in $SearchBaseCache[$Domain]) { Write-Color -Text "[i] ", "Getting contacts from ", "$($Domain)", " using ", $Server, " and SearchBase ", $SB -Color Yellow, White, Yellow, White, Yellow, White, Yellow try { Get-ADObject -LDAPFilter "objectClass=Contact" -Server $Server -SearchBase $SB -Properties $PropertiesContacts -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } } else { Write-Color -Text "[i] ", "Getting contacts from ", "$($Domain)", " using ", $Server -Color Yellow, White, Yellow, White try { Get-ADObject -LDAPFilter "objectClass=Contact" -Server $Server -Properties $PropertiesContacts -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " Write-Color '[e] Error: ', $ErrorMessage -Color White, Red } } } foreach ($Contact in $Contacts) { $Cache[$Contact.DistinguishedName] = $Contact } } Write-Color -Text "[i] ", "Preparing users ", $Users.Count, " for analysis across the forest ", $Forest.Name -Color Yellow, White, Yellow, White, Yellow, White foreach ($OU in $FilterOrganizationalUnit) { Write-Color -Text "[i] ", "Filtering users by Organizational Unit ", $OU -Color Yellow, White, Yellow, White } $CountUsers = 0 foreach ($User in $Users) { $CountUsers++ Write-Verbose -Message "Processing $($User.DisplayName) / $($User.DistinguishedName) - $($CountUsers)/$($Users.Count)" $SkipUser = $false $DateExpiry = $null $DaysToExpire = $null $PasswordDays = $null $PasswordNeverExpires = $null $PasswordAtNextLogon = $null $HasMailbox = $null $OUPath = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit # Allow filtering to prevent huge time processing for huge domains when only some users are needed # from specific Organizational Units foreach ($OU in $FilterOrganizationalUnit) { if ($null -ne $OUPath -and $OUPath -like "$OU") { $SkipUser = $false break } else { $SkipUser = $true } } if ($SkipUser) { continue } # This is a special case for users that have a manager in a special field such as extensionAttributes # This is useful for service accounts or other accounts that don't have a manager in AD if ($OverwriteManagerProperty) { # fix this for a user $ManagerTemp = $User.$OverwriteManagerProperty if ($ManagerTemp) { $ManagerSpecial = $Cache[$ManagerTemp] } else { $ManagerSpecial = $null } } else { $ManagerSpecial = $null } if ($ManagerSpecial) { # We have manager in different field such as extensionAttribute $ManagerDN = $ManagerSpecial.DistinguishedName $Manager = $ManagerSpecial.DisplayName $ManagerSamAccountName = $ManagerSpecial.SamAccountName $ManagerDisplayName = $ManagerSpecial.DisplayName $ManagerEmail = $ManagerSpecial.Mail # we check if we have external system and if we have global email replacement for managers in place # we check only if SamAccountName is there (contacts don't have it) if ($ManagerSamAccountName -and $UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty if ($ADProperty -eq 'SamAccountName') { # we need to find manager by SamAccountName, and we need to find it in external system # any other property is not supported $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$ManagerSamAccountName] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $ExternalUser.$EmailProperty -ne $ManagerEmail) { $ReplacedManagerEmail = $ManagerEmail $ManagerEmail = $ExternalUser.$EmailProperty if (-not $ExternalSystemManagers[$ManagerSamAccountName]) { $ExternalSystemManagers[$ManagerSamAccountName] = $ManagerSamAccountName $ExternalSystemReplacements.Managers.Add( [PSCustomObject]@{ ManagerSamAccountName = $ManagerSamAccountName ExternalEmail = $ManagerEmail ADEmailAddress = $ReplacedManagerEmail ExternalSystem = $UsersExternalSystem.Name } ) } #Write-Color -Text '[i] ', "Overwriting manager email address for ", $Manager, " with ", $ManagerEmail, " (old email: $ReplacedManagerEmail)", " from ", $UsersExternalSystem.Name -Color Yellow, White, Yellow, White, Green, Red, White, Yellow } } } $ManagerEnabled = $ManagerSpecial.Enabled $ManagerLastLogon = $ManagerSpecial.LastLogonDate if ($ManagerLastLogon) { $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days) } else { $ManagerLastLogonDays = $null } $ManagerType = $ManagerSpecial.ObjectClass } elseif ($User.Manager) { $ManagerDN = $Cache[$User.Manager].DistinguishedName $Manager = $Cache[$User.Manager].DisplayName $ManagerSamAccountName = $Cache[$User.Manager].SamAccountName $ManagerDisplayName = $Cache[$User.Manager].DisplayName $ManagerEmail = $Cache[$User.Manager].Mail # we check if we have external system and if we have global email replacement for managers in place # we check only if SamAccountName is there (contacts don't have it) if ($ManagerSamAccountName -and $UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty if ($ADProperty -eq 'SamAccountName') { # we need to find manager by SamAccountName, and we need to find it in external system # any other property is not supported $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$ManagerSamAccountName] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $ExternalUser.$EmailProperty -ne $ManagerEmail) { $ReplacedManagerEmail = $ManagerEmail $ManagerEmail = $ExternalUser.$EmailProperty if (-not $ExternalSystemManagers[$ManagerSamAccountName]) { $ExternalSystemManagers[$ManagerSamAccountName] = $ManagerSamAccountName $ExternalSystemReplacements.Managers.Add( [PSCustomObject]@{ ManagerSamAccountName = $ManagerSamAccountName ExternalEmail = $ManagerEmail ADEmailAddress = $ReplacedManagerEmail ExternalSystem = $UsersExternalSystem.Name } ) } #Write-Color -Text '[i] ', "Overwriting manager email address for ", $Manager, " with ", $ManagerEmail, " (old email: $ReplacedManagerEmail)", " from ", $UsersExternalSystem.Name -Color Yellow, White, Yellow, White, Green, Red, White, Yellow } } } $ManagerEnabled = $Cache[$User.Manager].Enabled $ManagerLastLogon = $Cache[$User.Manager].LastLogonDate if ($ManagerLastLogon) { $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days) } else { $ManagerLastLogonDays = $null } $ManagerType = $Cache[$User.Manager].ObjectClass } else { if ($User.ObjectClass -eq 'user') { $ManagerStatus = 'Missing' } else { $ManagerStatus = 'Not available' } $ManagerDN = $null $Manager = $null $ManagerSamAccountName = $null $ManagerDisplayName = $null $ManagerEmail = $null $ManagerEnabled = $null $ManagerLastLogon = $null $ManagerLastLogonDays = $null $ManagerType = $null } if ($ManagerDN -and -not $CacheManager[$ManagerDN]) { $CacheManager[$ManagerDN] = [PSCustomObject] @{ DistinguishedName = $ManagerDN Domain = ConvertFrom-DistinguishedName -DistinguishedName $ManagerDN -ToDomainCN DisplayName = $ManagerDisplayName SamAccountName = $ManagerSamAccountName EmailAddress = $ManagerEmail Enabled = $ManagerEnabled LastLogonDate = $ManagerLastLogon LastLogonDays = $ManagerLastLogonDays Type = $ManagerType } } if ($OverwriteEmailProperty) { # fix this for a user $EmailTemp = $User.$OverwriteEmailProperty if ($EmailTemp -like '*@*') { $EmailAddress = $EmailTemp } else { $EmailAddress = $User.EmailAddress } # Fix this for manager as well if ($Cache["$($User.Manager)"]) { if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') { # $UserManager.Mail = $UserManager.$OverwriteEmailProperty $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty } } } else { $EmailAddress = $User.EmailAddress } if ($UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { if ($UsersExternalSystem.Type -eq 'ExternalUsers') { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty] # $EmailAddress = $User.EmailAddress $EmailFrom = 'AD' if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*' -and $EmailAddress -ne $ExternalUser.$EmailProperty) { $EmailFrom = 'ILM' $EmailAddress = $ExternalUser.$EmailProperty $ExternalSystemReplacements.Users.Add( [PSCustomObject]@{ UserSamAccountName = $User.SamAccountName ExternalEmail = $EmailAddress ADEmailAddress = $User.EmailAddress ExternalSystem = $UsersExternalSystem.Name } ) } } else { Write-Color -Text '[-] ', "External system type not supported. Please use only type as provided using 'New-PasswordConfigurationExternalUsers'." -Color Yellow, White, Red return } } else { $EmailFrom = 'AD' } if ($User.PasswordLastSet) { $PasswordDays = (New-TimeSpan -Start ($User.PasswordLastSet) -End ($Today)).Days } else { $PasswordDays = $null } # Since we fixed manager above, we now check for status if ($User.Manager) { if ($ManagerEnabled -and $ManagerEmail) { if ((Test-EmailAddress -EmailAddress $ManagerEmail).IsValid -eq $true) { $ManagerStatus = 'Enabled' } else { $ManagerStatus = 'Enabled, bad email' } } elseif ($ManagerEnabled) { $ManagerStatus = 'No email' } elseif ($Cache[$User.Manager].ObjectClass -eq 'Contact') { $ManagerStatus = 'Enabled' # we need to treat it as always enabled } else { $ManagerStatus = 'Disabled' } } if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) { # This is standard situation where users password is expiring as needed try { $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed")) } catch { $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed" } try { $DaysToExpire = (New-TimeSpan -Start ($Today) -End ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))).Days } catch { $DaysToExpire = $null } $PasswordNeverExpires = $User.PasswordNeverExpires } else { # This is non-standard situation. This basically means most likely Fine Grained Group Policy is in action where it makes PasswordNeverExpires $true # Since FGP policies are a bit special they do not tick the PasswordNeverExpires box, but at the same time value for "msDS-UserPasswordExpiryTimeComputed" is set to 9223372036854775807 $PasswordNeverExpires = $true } if ($User.pwdLastSet -eq 0 -and $DateExpiry.Year -eq 1601) { $PasswordAtNextLogon = $true } else { $PasswordAtNextLogon = $false } if ($PasswordNeverExpires -or $null -eq $User.PasswordLastSet) { # If password last set is null or password never expires is set to true, then date of expiry and days to expire is not applicable $DateExpiry = $null $DaysToExpire = $null } $UserAccountControl = Convert-UserAccountControl -UserAccountControl $User.UserAccountControl if ($UserAccountControl -contains 'INTERDOMAIN_TRUST_ACCOUNT') { continue } if ($ExchangeProperty) { if ($User.'msExchMailboxGuid') { $HasMailbox = 'Yes' } else { $HasMailbox = 'No' } } else { $HasMailbox = 'Unknown' } if ($User.LastLogonDate) { $LastLogonDays = $( - $($User.LastLogonDate - $Today).Days) } else { $LastLogonDays = $null } if ($User.Country) { $Country = Convert-CountryCodeToCountry -CountryCode $User.Country $CountryCode = $User.Country } else { $Country = 'Unknown' $CountryCode = 'Unknown' } if ($AddEmptyProperties.Count -gt 0) { $StartUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.SamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $User.Enabled HasMailbox = $HasMailbox EmailAddress = $EmailAddress SystemEmailAddress = $User.EmailAddress DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $User.PasswordExpired PasswordDays = $PasswordDays PasswordAtNextLogon = $PasswordAtNextLogon PasswordLastSet = $User.PasswordLastSet PasswordNotRequired = $User.PasswordNotRequired PasswordNeverExpires = $PasswordNeverExpires LastLogonDate = $User.LastLogonDate LastLogonDays = $LastLogonDays } foreach ($Property in $AddEmptyProperties) { $StartUser.$Property = $null } $EndUser = [ordered] @{ Manager = $Manager ManagerDisplayName = $ManagerDisplayName ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays ManagerType = $ManagerType DisplayName = $User.DisplayName Name = $User.Name GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = $OUPath MemberOf = $User.MemberOf DistinguishedName = $User.DistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' EmailFrom = $EmailFrom } $MyUser = $StartUser + $EndUser } else { $MyUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.SamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $User.Enabled HasMailbox = $HasMailbox EmailAddress = $EmailAddress SystemEmailAddress = $User.EmailAddress DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $User.PasswordExpired PasswordDays = $PasswordDays PasswordAtNextLogon = $PasswordAtNextLogon PasswordLastSet = $User.PasswordLastSet PasswordNotRequired = $User.PasswordNotRequired PasswordNeverExpires = $PasswordNeverExpires LastLogonDate = $User.LastLogonDate LastLogonDays = $LastLogonDays Manager = $Manager ManagerDisplayName = $ManagerDisplayName ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays ManagerType = $ManagerType DisplayName = $User.DisplayName Name = $User.Name GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $User.DistinguishedName -ToOrganizationalUnit MemberOf = $User.MemberOf DistinguishedName = $User.DistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' EmailFrom = $EmailFrom } } foreach ($Property in $ConditionProperties) { $MyUser["$Property"] = $User.$Property } foreach ($E in $ExtendedProperties) { $MyUser[$E] = $User.$E } if ($HashtableField -eq 'NetBiosSamAccountName') { $HashField = $DNSNetBios[$MyUser.Domain] + '\' + $MyUser.SamAccountName if ($AsHashTableObject) { $CachedUsers["$HashField"] = $MyUser } else { $CachedUsers["$HashField"] = [PSCustomObject] $MyUser } } else { if ($AsHashTableObject) { $CachedUsers["$($User.$HashtableField)"] = $MyUser } else { $CachedUsers["$($User.$HashtableField)"] = [PSCustomObject] $MyUser } } # This function is used to replace values in the object with the replacement values # It uses the $CachedReplacements hashtable to find the replacement values # It uses the $MyUser object to find the values to be replaced <# For example: $Replacements = @( New-PasswordConfigurationReplacement -PropertyName 'Country' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' 'AT' = 'Austria' 'IT' = 'Italy' 'Unknown' = 'Dupa' } -OverwritePropertyName 'CountryCode' ) $Users = Find-PasswordQuality -Replacements $Replacements $Users | Format-Table #> if ($CachedReplacements.Count -gt 0) { foreach ($PropertyName in $CachedReplacements.Keys) { $ReplacementData = $CachedReplacements[$PropertyName] $SearchValue = $MyUser[$PropertyName] $SearchValueFromUser = $User.$PropertyName if ($ReplacementData.OverwritePropertyName) { $ExpectedPropertyName = $ReplacementData.OverwritePropertyName } else { $ExpectedPropertyName = $PropertyName } if ($null -ne $SearchValue -and $ReplacementData[$SearchValue]) { $MyUser[$ExpectedPropertyName] = $ReplacementData[$SearchValue] } elseif ($null -ne $SearchValueFromUser -and $ReplacementData[$SearchValueFromUser]) { $MyUser[$ExpectedPropertyName] = $ReplacementData[$SearchValueFromUser] } } } } if ($ReturnObjectsType -contains 'Contacts') { $CountContacts = 0 foreach ($Contact in $Contacts) { $CountContacts++ $OUPath = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToOrganizationalUnit # Allow filtering to prevent huge time processing for huge domains when only some users are needed foreach ($OU in $FilterOrganizationalUnit) { if ($null -eq $OUPath) { $SkipUser = $true break } elseif ($OUPath -notlike "$OU") { $SkipUser = $true break } } if ($SkipUser) { continue } Write-Verbose -Message "Processing $($Contact.DisplayName) - $($CountContacts)/$($Contacts.Count)" # create dummy objects for manager contacts $MyUser = [ordered] @{ UserPrincipalName = $null SamAccountName = $null Domain = ConvertFrom-DistinguishedName -DistinguishedName $Contact.DistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $true HasMailbox = $null EmailAddress = $Contact.Mail SystemEmailAddress = $Contact.Mail DateExpiry = $null DaysToExpire = $null PasswordExpired = $null PasswordDays = $null PasswordAtNextLogon = $null PasswordLastSet = $null PasswordNotRequired = $null PasswordNeverExpires = $null LastLogonDate = $null LastLogonDays = $null Manager = $null ManagerDisplayName = $null ManagerSamAccountName = $null ManagerEmail = $null ManagerStatus = $null ManagerLastLogonDays = $null ManagerType = $null DisplayName = $Contact.DisplayName Name = $Contact.Name GivenName = $null Surname = $null OrganizationalUnit = $OUPath MemberOf = $Contact.MemberOf DistinguishedName = $Contact.DistinguishedName ManagerDN = $null Country = $null CountryCode = $null Type = 'Contact' EmailFrom = $EmailFrom } # this allows to extend the object with custom properties requested by user # especially custom extensions for use within rules foreach ($E in $ExtendedProperties) { $MyUser[$E] = $User.$E } if ($HashtableField -eq 'NetBiosSamAccountName') { # Contacts do not have NetBiosSamAccountName continue } else { if ($AsHashTableObject) { $CachedUsers["$($Contact.$HashtableField)"] = $MyUser } else { $CachedUsers["$($Contact.$HashtableField)"] = [PSCustomObject] $MyUser } } } } if ($AsHashTable) { $CachedUsers } else { $CachedUsers.Values } } ================================================ FILE: Public/Find-PasswordEntra.ps1 ================================================ function Find-PasswordEntra { [CmdletBinding()] param( [Parameter(DontShow)][string] $HashtableField = 'UserPrincipalName', [Parameter(DontShow)][switch] $AsHashTable, [string] $OverwriteEmailProperty, [Parameter(DontShow)][string[]] $AddEmptyProperties = @(), [Parameter(DontShow)][string[]] $RulesProperties, [string] $OverwriteManagerProperty, [System.Collections.IDictionary] $Cache = [ordered] @{}, [System.Collections.IDictionary] $CacheManager = [ordered] @{}, [Parameter(DontShow)][System.Collections.IDictionary] $UsersExternalSystem, [Parameter(DontShow)][System.Collections.IDictionary] $ExternalSystemReplacements = [ordered] @{ Managers = [System.Collections.Generic.List[PSCustomObject]]::new() Users = [System.Collections.Generic.List[PSCustomObject]]::new() }, [string[]] $FilterOrganizationalUnit ) $ExternalSystemManagers = [ordered]@{} if ($UsersExternalSystem.Name) { Write-Color -Text '[i] ', "Using external system ", $UsersExternalSystem.Name, " for EMAIL replacement functionality" -Color Yellow, White, Yellow, White Write-Color -Text '[i] ', "There are ", $UsersExternalSystem.Users.Count, " users in the external system" -Color Yellow, White, Yellow, White } if (-not $ExternalSystemReplacements.Users) { $ExternalSystemReplacements.Users = [System.Collections.Generic.List[PSCustomObject]]::new() } if (-not $ExternalSystemReplacements.Managers) { $ExternalSystemReplacements.Managers = [System.Collections.Generic.List[PSCustomObject]]::new() } $Today = Get-Date # We're caching all users in their inital form to make sure it's speedy gonzales when querying for Managers if (-not $Cache) { $Cache = [ordered] @{ } } # We're caching all processed users to make sure it's easier later on to find users if (-not $CachedUsers) { $CachedUsers = [ordered] @{ } } $Properties = @( 'DisplayName', 'GivenName', 'Surname', 'Mail', 'UserPrincipalName', 'Id' 'lastPasswordChangeDateTime', 'signInActivity' 'country', 'AccountEnabled' 'Manager', 'passwordPolicies', 'passwordProfile', 'OnPremisesDistinguishedName', 'OnPremisesSyncEnabled', 'OnPremisesLastSyncDateTime', 'OnPremisesSamAccountName', 'UserType' 'assignedLicenses' if ($UsersExternalSystem -and $UsersExternalSystem.Type -eq 'ExternalUsers') { $UsersExternalSystem.ActiveDirectoryProperty } if ($OverwriteEmailProperty) { $OverwriteEmailProperty } if ($OverwriteManagerProperty) { $OverwriteManagerProperty } foreach ($Rule in $RulesProperties) { $Rule } ) $Properties = $Properties | Sort-Object -Unique # lets build extended properties that need [Array] $ExtendedProperties = foreach ($Rule in $RulesProperties) { $Rule } [Array] $ExtendedProperties = $ExtendedProperties | Sort-Object -Unique <# 'signInActivity' LastNonInteractiveSignInDateTime LastNonInteractiveSignInRequestId LastSignInDateTime LastSignInRequestId -------------------------------- --------------------------------- ------------------ ------------------- 10.05.2022 21:50:17 66e349fd-2768-4f0c-811f-ce49219f6300 16.07.2020 11:16:38 108a99e5-b958-4071-8a11-3330c808d700 #> # $Users[-2].Manager.AdditionalProperties try { $PasswordPolicies = Get-MgDomain -ErrorAction Stop } catch { Write-Color -Text '[-] ', "Couldn't get password policies. Unable to asses. Error: ", $_.Exception.Message -Color Yellow, White, Red return } if ($PasswordPolicies) { $PasswordPolicies = $PasswordPolicies.PasswordValidityPeriodInDays | Select-Object -First 1 } else { Write-Color -Text '[-] ', "Couldn't get password policies. Unable to asses." -Color Yellow, White, Red return } if ($PasswordPolicies -eq '2147483647') { $GlobalPasswordPolicy = 'PasswordNeverExpires' $GlobalPasswordPolicyDays = $null } else { $GlobalPasswordPolicy = "$PasswordPolicies days" $GlobalPasswordPolicyDays = $PasswordPolicies } Write-Color -Text "[i] ", "Global password policy is set to $GlobalPasswordPolicy" -Color Yellow, White Write-Color -Text "[i] ", "Preparing all users for password expirations in EntraID" -Color Yellow, White, Yellow, White try { # Get only members, not guests or other types -Filter "userType eq 'member'" $Users = Get-MgUser -All -ErrorAction Stop -Property $Properties -ConsistencyLevel eventual -ExpandProperty Manager | Select-Object -Property $Properties } catch { Write-Color -Text '[-] ', "Couldn't cache users. Please fix 'Find-PasswordEntra'. Error: ", "$($_.Exception.Message)" -Color Yellow, White, Red return } $CountUsers = 0 foreach ($User in $Users) { $CountUsers++ Write-Verbose -Message "Processing $($User.DisplayName) - $($CountUsers)/$($Users.Count)" $LastLogonDate = $null $LastLogonDays = $null if ($User.SignInActivity) { if ($User.SignInActivity -and $User.LastNonInteractiveSignInDateTime) { if ($User.SignInActivity.LastNonInteractiveSignInDateTime -gt $User.SignInActivity.LastSignInDateTime) { $LastLogonDate = $User.SignInActivity.LastNonInteractiveSignInDateTime } else { $LastLogonDate = $User.SignInActivity.LastSignInDateTime } } else { if ($User.SignInActivity.LastSignInDateTime) { $LastLogonDate = $User.SignInActivity.LastSignInDateTime } elseif ($User.SignInActivity.LastNonInteractiveSignInDateTime) { $LastLogonDate = $User.SignInActivity.LastNonInteractiveSignInDateTime } } if ($null -ne $LastLogonDate) { $LastLogonDays = ($Today - $LastLogonDate).Days } } $DateExpiry = $null $DaysToExpire = $null $PasswordDays = $null $PasswordNeverExpires = $false $PasswordAtNextLogon = $null #$HasMailbox = $null $Country = $User.Country if ($Country) { $CountryCode = Convert-CountryToCountryCode -CountryName $User.Country } else { $CountryCode = $null } # if ($User.ObjectClass -eq 'user') { # $ManagerStatus = 'Missing' # } else { # $ManagerStatus = 'Not available' # } if ($User.Manager.AdditionalProperties) { $Manager = $User.Manager.AdditionalProperties $ManagerSamAccountName = $User.Manager.AdditionalProperties.onPremisesSamAccountName $ManagerDisplayName = $User.Manager.AdditionalProperties.displayName $ManagerEmail = $User.Manager.AdditionalProperties.mail $ManagerEnabled = $User.Manager.AdditionalProperties.accountEnabled #$ManagerLastLogon = $null #$ManagerLastLogonDays = $null $ManagerType = $User.Manager.AdditionalProperties.userType if ($ManagerEnabled -and $ManagerEmail) { if ((Test-EmailAddress -EmailAddress $ManagerEmail).IsValid -eq $true) { $ManagerStatus = 'Enabled' } else { $ManagerStatus = 'Enabled, bad email' } } elseif ($ManagerEnabled -eq $true) { $ManagerStatus = 'No email' } elseif ($ManagerEnabled -eq $false) { $ManagerStatus = 'Disabled' } else { $ManagerStatus = 'Missing' } } else { $Manager = $null $ManagerSamAccountName = $null $ManagerDisplayName = $null $ManagerEmail = $nullf $ManagerStatus = 'Missing' $ManagerLastLogonDays = $null $ManagerType = $null } $IsSynchronized = $null -ne $User.OnPremisesDistinguishedName $IsLicensed = $User.AssignedLicenses.Count -gt 0 if ($User.lastPasswordChangeDateTime) { $PasswordLastSet = $User.lastPasswordChangeDateTime $PasswordDays = ($Today - $PasswordLastSet).Days } <# This value is an enumeration with one possible value being DisableStrongPassword, which allows weaker passwords than the default policy to be specified. DisablePasswordExpiration can also be specified. The two may be specified together; for example: DisablePasswordExpiration, DisableStrongPassword. For more information on the default password policies, see Microsoft Entra password policies. Supports $filter (ne, not, and eq on null values). #> if ($null -eq $User.PasswordPolicies -or $User.PasswordPolicies -eq 'None') { If ($GlobalPasswordPolicy -contains 'PasswordNeverExpires') { $PasswordNeverExpires = $true $DaysToExpire = $null $DateExpiry = $null } else { $PasswordNeverExpires = $false try { # Get the date when password expires based on PasswordLastSet and GlobalPasswordPolicyDays $DateExpiry = $PasswordLastSet.AddDays($GlobalPasswordPolicyDays) $DaysToExpire = ($DateExpiry - $Today).Days } catch { $DaysToExpire = $null $DateExpiry = $null } } } elseif ($User.PasswordPolicies -contains 'DisablePasswordExpiration') { $PasswordNeverExpires = $true $DaysToExpire = $null $DateExpiry = $null } else { Write-Color -Text '[-] ', "Password policy ($($User.PasswordPolicies)) not supported. We need to investigate what changed" -Color Yellow, White, Red return } if ($PasswordNeverExpires) { $PasswordExpired = $false } else { if ($PasswordDays -gt $GlobalPasswordPolicyDays) { $PasswordExpired = $true } else { $PasswordExpired = $false } } if ($OverwriteEmailProperty) { # fix this for a user $EmailTemp = $User.$OverwriteEmailProperty if ($EmailTemp -like '*@*') { $EmailAddress = $EmailTemp } else { $EmailAddress = $User.Mail } # Fix this for manager as well if ($Cache["$($User.Manager)"]) { if ($Cache["$($User.Manager)"].$OverwriteEmailProperty -like '*@*') { # $UserManager.Mail = $UserManager.$OverwriteEmailProperty $ManagerEmail = $Cache["$($User.Manager)"].$OverwriteEmailProperty } } } else { $EmailAddress = $User.Mail } if ($UsersExternalSystem -and $UsersExternalSystem.Global -eq $true) { if ($UsersExternalSystem.Type -eq 'ExternalUsers') { $ADProperty = $UsersExternalSystem.ActiveDirectoryProperty $EmailProperty = $UsersExternalSystem.EmailProperty $ExternalUser = $UsersExternalSystem['Users'][$User.$ADProperty] if ($ExternalUser -and $ExternalUser.$EmailProperty -like '*@*') { $EmailAddress = $ExternalUser.$EmailProperty } else { $EmailAddress = $User.Mail } } else { Write-Color -Text '[-] ', "External system type not supported. Please use only type as provided using 'New-PasswordConfigurationExternalUsers'." -Color Yellow, White, Red return } } if ($AddEmptyProperties.Count -gt 0) { $StartUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.OnPremisesSamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.OnPremisesDistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $User.AccountEnabled IsLicensed = $IsLicensed EmailAddress = $EmailAddress SystemEmailAddress = $User.Mail UserType = $User.UserType IsSynchronized = $IsSynchronized PasswordPolicies = if ($User.PasswordPolicies) { $User.PasswordPolicies } else { 'Not set' } ForceChangePasswordNextSignIn = $User.PasswordProfile.ForceChangePasswordNextSignIn ForceChangePasswordNextSignInWithMfa = $User.PasswordProfile.ForceChangePasswordNextSignInWithMfa DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $PasswordExpired PasswordDays = $PasswordDays PasswordAtNextLogon = $PasswordAtNextLogon PasswordLastSet = $User.lastPasswordChangeDateTime PasswordNeverExpires = $PasswordNeverExpires LastLogonDate = $LastLogonDate LastLogonDays = $LastLogonDays } foreach ($Property in $AddEmptyProperties) { $StartUser.$Property = $null } $EndUser = [ordered] @{ Manager = $Manager ManagerDisplayName = $ManagerDisplayName ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays ManagerType = $ManagerType DisplayName = $User.DisplayName Name = $User.Name GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $User.OnPremisesDistinguishedName -ToOrganizationalUnit MemberOf = $User.MemberOf DistinguishedName = $User.OnPremisesDistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' } $MyUser = $StartUser + $EndUser } else { $MyUser = [ordered] @{ UserPrincipalName = $User.UserPrincipalName SamAccountName = $User.OnPremisesSamAccountName Domain = ConvertFrom-DistinguishedName -DistinguishedName $User.OnPremisesDistinguishedName -ToDomainCN RuleName = '' RuleOptions = [System.Collections.Generic.List[string]]::new() Enabled = $User.AccountEnabled IsLicensed = $IsLicensed EmailAddress = $EmailAddress SystemEmailAddress = $User.Mail UserType = $User.UserType IsSynchronized = $IsSynchronized PasswordPolicies = if ($User.PasswordPolicies) { $User.PasswordPolicies } else { 'Not set' } ForceChangePasswordNextSignIn = $User.PasswordProfile.ForceChangePasswordNextSignIn ForceChangePasswordNextSignInWithMfa = $User.PasswordProfile.ForceChangePasswordNextSignInWithMfa DateExpiry = $DateExpiry DaysToExpire = $DaysToExpire PasswordExpired = $PasswordExpired PasswordDays = $PasswordDays PasswordAtNextLogon = $PasswordAtNextLogon PasswordLastSet = $User.lastPasswordChangeDateTime PasswordNeverExpires = $PasswordNeverExpires LastLogonDate = $LastLogonDate LastLogonDays = $LastLogonDays Manager = $Manager ManagerDisplayName = $ManagerDisplayName ManagerSamAccountName = $ManagerSamAccountName ManagerEmail = $ManagerEmail ManagerStatus = $ManagerStatus ManagerLastLogonDays = $ManagerLastLogonDays ManagerType = $ManagerType DisplayName = $User.DisplayName Name = $User.Name GivenName = $User.GivenName Surname = $User.Surname OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $User.OnPremisesDistinguishedName -ToOrganizationalUnit MemberOf = $User.MemberOf DistinguishedName = $User.OnPremisesDistinguishedName ManagerDN = $User.Manager Country = $Country CountryCode = $CountryCode Type = 'User' } } foreach ($Property in $ConditionProperties) { $MyUser["$Property"] = $User.$Property } foreach ($E in $ExtendedProperties) { $MyUser[$E] = $User.$E } if ($HashtableField -eq 'NetBiosSamAccountName') { $HashField = $DNSNetBios[$MyUser.Domain] + '\' + $MyUser.SamAccountName if ($AsHashTableObject) { $CachedUsers["$HashField"] = $MyUser } else { $CachedUsers["$HashField"] = [PSCustomObject] $MyUser } } else { if ($AsHashTableObject) { $CachedUsers["$($User.$HashtableField)"] = $MyUser } else { $CachedUsers["$($User.$HashtableField)"] = [PSCustomObject] $MyUser } } } if ($AsHashTable) { $CachedUsers } else { $CachedUsers.Values } } ================================================ FILE: Public/Find-PasswordNotification.ps1 ================================================ function Find-PasswordNotification { <# .SYNOPSIS Searches thru XML logs created by Password Solution .DESCRIPTION Searches thru XML logs created by Password Solution .PARAMETER SearchPath Path to file where the XML log is located .PARAMETER Manager Search thru manager escalations .EXAMPLE Find-PasswordNotification -SearchPath $PSScriptRoot\Search\SearchLog.xml | Format-Table .EXAMPLE Find-PasswordNotification -SearchPath "$PSScriptRoot\Search\SearchLog_2021-06.xml" -Manager | Format-Table .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory)][string] $SearchPath, [switch] $Manager ) if ($SearchPath) { if (Test-Path -LiteralPath $SearchPath) { try { $SummarySearch = Import-Clixml -LiteralPath $SearchPath -ErrorAction Stop #$SummarySearch = Get-Content -LiteralPath $SearchPath -Raw | ConvertFrom-Json } catch { Write-Color -Text "[e]", " Couldn't load the file $SearchPath", ". Skipping...", $_.Exception.Message -Color White, Yellow, White, Yellow, White, Yellow, White } if ($SummarySearch -and $Manager) { $SummarySearch.EmailEscalations.Values } elseif ($SummarySearch -and $Manager -eq $false) { $SummarySearch.EmailSent.Values } } } } ================================================ FILE: Public/Find-PasswordQuality.ps1 ================================================ function Find-PasswordQuality { <# .SYNOPSIS Scan Active Directory forest for asses password quality of users .DESCRIPTION Scan Active Directory forest for asses password quality of users including weak passwords, duplicate groups and more. .PARAMETER WeakPasswords List of weak passwords to check against .PARAMETER WeakPasswordsFilePath Path to a file that contains weak passwords, one password per line. .PARAMETER WeakPasswordsHashesFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. For performance reasons, the -WeakPasswordHashesSortedFile parameter should be used instead. .PARAMETER WeakPasswordsHashesSortedFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. The hashes must be sorted alphabetically, because a binary search is performed. This parameter is typically used with a list of leaked password hashes from HaveIBeenPwned. .PARAMETER IncludeStatistics Include statistics in output .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing .PARAMETER Replacements Ability to provide replacements for properties to be used in the output .EXAMPLE Find-PasswordQuality -WeakPasswords "Test1", "Test2", "Test3" .EXAMPLE Find-PasswordQuality -WeakPasswords "Test1", "Test2", "Test3" -IncludeStatistics .EXAMPLE $Replacements = @( New-PasswordConfigurationReplacement -PropertyName 'Country' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' 'AT' = 'Austria' 'IT' = 'Italy' 'Unknown' = 'Poland' } -OverwritePropertyName 'CountryCode' ) $Users = Find-PasswordQuality -Replacements $Replacements $Users | Format-Table .NOTES General notes #> [CmdletBinding()] param( [string[]] $WeakPasswords, [string] $WeakPasswordsFilePath, [string] $WeakPasswordsHashesFile, [string] $WeakPasswordsHashesSortedFile, [switch] $IncludeStatistics, [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [System.Collections.IDictionary[]] $Replacements ) $PropertiesToAdd = @( 'ClearTextPassword' 'LMHash' 'EmptyPassword' 'WeakPassword' #'DefaultComputerPassword' #'PasswordNotRequired' #'PasswordNeverExpires' 'AESKeysMissing' 'PreAuthNotRequired' 'DESEncryptionOnly' 'Kerberoastable' 'DelegatableAdmins' 'SmartCardUsersWithPassword' 'DuplicatePasswordGroups' ) if ($WeakPasswordsHashesFile) { if (Test-Path -LiteralPath $WeakPasswordsHashesFile) { Write-Color -Text "[i] ", "Weak password hashes available to read from ", $WeakPasswordsHashesFile -Color Yellow, Gray, White, Yellow, White, Yellow, White $WeakPasswordHashesStats = Get-FileInformation -File $WeakPasswordsHashesFile } else { Write-Color -Text "[e] ", "Weak password hashes file not found at ", $WeakPasswordsHashesFile -Color Red, Yellow, White, Yellow, Red return } } if ($WeakPasswordsHashesSortedFile) { if (Test-Path -LiteralPath $WeakPasswordsHashesSortedFile) { Write-Color -Text "[i] ", "Weak passwords hashes (sorted) available to read from ", $WeakPasswordsHashesSortedFile -Color Yellow, Gray, White, Yellow, White, Yellow, White $WeakPasswordHashesSortedStats = Get-FileInformation -File $WeakPasswordsHashesSortedFile } else { Write-Color -Text "[e] ", "Weak passwords hashes (sorted) file not found at ", $WeakPasswordsHashesSortedFile -Color Red, Yellow, White, Yellow, Red return } } if ($WeakPasswordsFilePath) { if (Test-Path -LiteralPath $WeakPasswordsFilePath) { Write-Color -Text "[i] ", "Weak passwords available to read from ", $WeakPasswordsFilePath -Color Yellow, Gray, White, Yellow, White, Yellow, White $WeakPasswordsStats = Get-FileInformation -File $WeakPasswordsFilePath } else { Write-Color -Text "[e] ", "Weak passwords file not found at ", $WeakPasswordsFilePath -Color Red, Yellow, White, Yellow, Red return } } $ModuleExists = Get-Command -Module DSInternals -ErrorAction SilentlyContinue if (-not $ModuleExists) { Write-Color -Text "[e] ", "DSInternals module is not installed. Please install it using Install-Module DSInternals -Verbose" -Color Yellow, Red return } $AllUsers = Find-Password -AsHashTable -HashtableField NetBiosSamAccountName -ReturnObjectsType Users -AsHashTableObject -AddEmptyProperties $PropertiesToAdd -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -Replacements $Replacements Write-Color -Text "[i] ", "Discovering forest information" -Color Yellow, Gray, White, Yellow, White, Yellow, White $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -ExtendedForestInformation $ExtendedForestInformation $PasswordsInHash = [ordered] @{} $PasswordQuality = foreach ($Domain in $ForestInformation.Domains) { Write-Color -Text "[i] ", "Discovering DC for domain ", "$($Domain)", " in forest ", $ForestInformation.Name -Color Yellow, Gray, White, Yellow, White, Yellow, White $Server = $ForestInformation['QueryServers'][$Domain]['HostName'][0] Write-Color -Text "[i] ", "Getting replication data from ", "$($Domain)", " using ", $Server -Color Yellow, Gray, White, Yellow, White, Yellow, White $testPasswordQualitySplat = @{ WeakPasswords = $WeakPasswords WeakPasswordsFile = $WeakPasswordsFilePath WeakPasswordHashesFile = $WeakPasswordsHashesFile WeakPasswordHashesSortedFile = $WeakPasswordsHashesSortedFile IncludeDisabledAccounts = $true } Remove-EmptyValue -Hashtable $testPasswordQualitySplat try { Get-ADReplAccount -All -Server $Server -ErrorAction Stop } catch { Write-Color -Text "[e] ", "Unable to get replication data from ", "$($Domain)", " using ", $Server, ". Error: ", $_.Exception.Message -Color Red, Yellow, White, Yellow, Red, Red } } Write-Color -Text "[i] Testing password quality" -Color Yellow, Gray, White, Yellow, White, Yellow, White $Quality = $PasswordQuality | Test-PasswordQuality @testPasswordQualitySplat Write-Color -Text "[i] Processing results, merging data from DSInternals" -Color Yellow, Gray, White, Yellow, White, Yellow, White foreach ($Property in $Quality.PSObject.Properties.Name) { $PasswordsInHash[$Property] = $Quality.$Property } $PasswordGroupsUsers = [ordered] @{} $Count = 0 foreach ($Group in $PasswordsInHash.DuplicatePasswordGroups) { $Count++ foreach ($User in $Group) { $PasswordGroupsUsers[$User] = "Group $Count" } } $QualityStatistics = [ordered] @{ AESKeysMissing = $PasswordsInHash.AESKeysMissing.Count AESKeysMissingEnabledOnly = 0 AESKeysMissingDisabledOnly = 0 DESEncryptionOnly = $PasswordsInHash.DESEncryptionOnly.Count DESEncryptionOnlyEnabledOnly = 0 DESEncryptionOnlyDisabledOnly = 0 DelegatableAdmins = $PasswordsInHash.DelegatableAdmins.Count DelegatableAdminsEnabledOnly = 0 DelegatableAdminsDisabledOnly = 0 DuplicatePasswordGroups = $PasswordsInHash.DuplicatePasswordGroups.Count DuplicatePasswordUsers = $PasswordGroupsUsers.Keys.Count DuplicatePasswordUsersEnabledOnly = 0 DuplicatePasswordUsersDisabledOnly = 0 ClearTextPassword = $PasswordsInHash.ClearTextPassword.Count ClearTextPasswordEnabledOnly = 0 ClearTextPasswordDisabledOnly = 0 LMHash = $PasswordsInHash.LMHash.Count LMHashEnabledOnly = 0 LMHashDisabledOnly = 0 EmptyPassword = $PasswordsInHash.EmptyPassword.Count EmptyPasswordEnabledOnly = 0 EmptyPasswordDisabledOnly = 0 WeakPassword = $PasswordsInHash.WeakPassword.Count WeakPasswordEnabledOnly = 0 WeakPasswordDisabledOnly = 0 #DefaultComputerPassword = $PasswordsInHash.DefaultComputerPassword.Count #DefaultComputerPasswordEnabledOnly = 0 #DefaultComputerPasswordDisabledOnly = 0 PasswordNotRequired = 0 # $PasswordsInHash.PasswordNotRequired.Count PasswordNotRequiredEnabledOnly = 0 PasswordNotRequiredDisabledOnly = 0 PasswordNeverExpires = 0 #$PasswordsInHash.PasswordNeverExpires.Count PasswordNeverExpiresEnabledOnly = 0 PasswordNeverExpiresDisabledOnly = 0 PreAuthNotRequired = $PasswordsInHash.PreAuthNotRequired.Count PreAuthNotRequiredEnabledOnly = 0 PreAuthNotRequiredDisabledOnly = 0 Kerberoastable = $PasswordsInHash.Kerberoastable.Count KerberoastableEnabledOnly = 0 KerberoastableDisabledOnly = 0 SmartCardUsersWithPassword = $PasswordsInHash.SmartCardUsersWithPassword.Count SmartCardUsersWithPasswordEnabledOnly = 0 SmartCardUsersWithPasswordDisabledOnly = 0 } $CountryStatistics = [ordered] @{ DuplicatePasswordUsers = [ordered] @{} WeakPassword = [ordered] @{} } $ContinentStatistics = [ordered] @{ DuplicatePasswordUsers = [ordered] @{} WeakPassword = [ordered] @{} } $CountryCodeStatistics = [ordered] @{ DuplicatePasswordUsers = [ordered] @{} WeakPassword = [ordered] @{} } $CountryToContinent = Convert-CountryToContinent $OutputUsers = foreach ($User in $AllUsers.Keys) { if ($AllUsers[$User].Country) { $Continent = $CountryToContinent[$AllUsers[$User].Country] if (-not $Continent) { $Continent = 'Unknown' } } else { $Continent = 'Unknown' } if ($AllUsers[$User].PasswordNotRequired) { $QualityStatistics.PasswordNotRequired++ if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics.PasswordNotRequiredEnabledOnly++ } else { $QualityStatistics.PasswordNotRequiredDisabledOnly++ } } if ($AllUsers[$User].PasswordNeverExpires) { $QualityStatistics.PasswordNeverExpires++ if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics.PasswordNeverExpiresEnabledOnly++ } else { $QualityStatistics.PasswordNeverExpiresDisabledOnly++ } } foreach ($Property in $PasswordsInHash.Keys) { if ($Property -eq 'DuplicatePasswordGroups') { if ($PasswordGroupsUsers[$User]) { $AllUsers[$User][$Property] = $PasswordGroupsUsers[$User] if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics["$($Property)EnabledOnly"]++ $QualityStatistics.DuplicatePasswordUsersEnabledOnly++ } else { $QualityStatistics["$($Property)DisabledOnly"]++ $QualityStatistics.DuplicatePasswordUsersDisabledOnly++ } # we keep stats per country for weak passwords and duplicate passwords $CountryStatistics['DuplicatePasswordUsers'][$AllUsers[$User].Country]++ $ContinentStatistics['DuplicatePasswordUsers'][$Continent]++ $CountryCodeStatistics['DuplicatePasswordUsers'][$AllUsers[$User].CountryCode]++ } else { $AllUsers[$User][$Property] = '' } } elseif ($Property -in $PropertiesToAdd) { if ($PasswordsInHash[$Property] -contains $User) { $AllUsers[$User][$Property] = $true if ($AllUsers[$User].Enabled -eq $true) { $QualityStatistics["$($Property)EnabledOnly"]++ } else { $QualityStatistics["$($Property)DisabledOnly"]++ } # we keep stats per country for weak passwords and duplicate passwords if ($Property -eq 'WeakPassword') { $CountryStatistics[$Property][$AllUsers[$User].Country]++ $ContinentStatistics[$Property][$Continent]++ $CountryCodeStatistics[$Property][$AllUsers[$User].CountryCode]++ } } else { $AllUsers[$User][$Property] = $false } } } [PSCustomObject] $AllUsers[$User] } if ($IncludeStatistics) { [ordered] @{ Forest = $ForestInformation.Forest Domains = $ForestInformation.Domains Statistics = $QualityStatistics StatisticsCountry = $CountryStatistics StatisticsCountryCode = $CountryCodeStatistics StatisticsContinents = $ContinentStatistics Users = $OutputUsers WeakPasswordsFileInformation = [ordered] @{ WeakPasswordHashesStats = $WeakPasswordHashesStats WeakPasswordHashesSortedStats = $WeakPasswordHashesSortedStats WeakPasswordsStats = $WeakPasswordsStats } } } else { $OutputUsers } } ================================================ FILE: Public/New-PasswordConfigurationEmail.ps1 ================================================ function New-PasswordConfigurationEmail { [cmdletBinding(DefaultParameterSetName = 'Compatibility', SupportsShouldProcess)] param( [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SmtpServer')][string] $Server, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Port, [Parameter(Mandatory, ParameterSetName = 'SecureString')] [Parameter(Mandatory, ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(Mandatory, ParameterSetName = 'MgGraphRequest')] [Parameter(Mandatory, ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [object] $From, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [string] $ReplyTo, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')][string[]] $DeliveryNotificationOption, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Net.Smtp.DeliveryStatusNotificationType] $DeliveryStatusNotificationType, [Parameter(ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [pscredential] $Credential, [Parameter(ParameterSetName = 'SecureString')] [string] $Username, [Parameter(ParameterSetName = 'SecureString')] [string] $Password, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Security.SecureSocketOptions] $SecureSocketOptions, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $UseSsl, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $SkipCertificateRevocation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SkipCertificateValidatation')][switch] $SkipCertificateValidation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Timeout, [Parameter(ParameterSetName = 'oAuth')] [alias('oAuth')][switch] $oAuth2, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestReadReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestDeliveryReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $Graph, [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $MgGraphRequest, [Parameter(ParameterSetName = 'SecureString')] [switch] $AsSecureString, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SendGrid, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SeparateTo, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $DoNotSaveToSentItems, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [string] $LocalDomain ) $Output = [ordered] @{ Type = 'PasswordConfigurationEmail' Settings = [ordered] @{ Server = if ($PSBoundParameters.ContainsKey('Server')) { $Server } else { $null } Port = if ($PSBoundParameters.ContainsKey('Port')) { $Port } else { $null } From = if ($PSBoundParameters.ContainsKey('From')) { $From } else { $null } ReplyTo = if ($PSBoundParameters.ContainsKey('ReplyTo')) { $ReplyTo } else { $null } Priority = if ($PSBoundParameters.ContainsKey('Priority')) { $Priority } else { $null } DeliveryNotificationOption = if ($PSBoundParameters.ContainsKey('DeliveryNotificationOption')) { $DeliveryNotificationOption } else { $null } DeliveryStatusNotificationType = if ($PSBoundParameters.ContainsKey('DeliveryStatusNotificationType')) { $DeliveryStatusNotificationType } else { $null } Credential = if ($PSBoundParameters.ContainsKey('Credential')) { $Credential } else { $null } Username = if ($PSBoundParameters.ContainsKey('Username')) { $Username } else { $null } Password = if ($PSBoundParameters.ContainsKey('Password')) { $Password } else { $null } SecureSocketOptions = if ($PSBoundParameters.ContainsKey('SecureSocketOptions')) { $SecureSocketOptions } else { $null } UseSsl = if ($PSBoundParameters.ContainsKey('UseSsl')) { $UseSsl } else { $null } SkipCertificateRevocation = if ($PSBoundParameters.ContainsKey('SkipCertificateRevocation')) { $SkipCertificateRevocation } else { $null } SkipCertificateValidation = if ($PSBoundParameters.ContainsKey('SkipCertificateValidatation')) { $SkipCertificateValidation } else { $null } Timeout = if ($PSBoundParameters.ContainsKey('Timeout')) { $Timeout } else { $null } oAuth2 = if ($PSBoundParameters.ContainsKey('oAuth2')) { $oAuth2 } else { $null } RequestReadReceipt = if ($PSBoundParameters.ContainsKey('RequestReadReceipt')) { $RequestReadReceipt } else { $null } RequestDeliveryReceipt = if ($PSBoundParameters.ContainsKey('RequestDeliveryReceipt')) { $RequestDeliveryReceipt } else { $null } Graph = if ($PSBoundParameters.ContainsKey('Graph')) { $Graph } else { $null } MgGraphRequest = if ($PSBoundParameters.ContainsKey('MgGraphRequest')) { $MgGraphRequest } else { $null } AsSecureString = if ($PSBoundParameters.ContainsKey('AsSecureString')) { $AsSecureString } else { $null } SendGrid = if ($PSBoundParameters.ContainsKey('SendGrid')) { $SendGrid } else { $null } SeparateTo = if ($PSBoundParameters.ContainsKey('SeparateTo')) { $SeparateTo } else { $null } DoNotSaveToSentItems = if ($PSBoundParameters.ContainsKey('DoNotSaveToSentItems')) { $DoNotSaveToSentItems } else { $null } WhatIf = $WhatIfPreference } } Remove-EmptyValue -Hashtable $Output.Settings $Output } ================================================ FILE: Public/New-PasswordConfigurationEntra.ps1 ================================================ function New-PasswordConfigurationEntra { [CmdletBinding()] param( [switch] $Enable ) $Output = [ordered] @{ Type = "PasswordConfigurationEntra" Settings = [ordered] @{ Enabled = $Enable.IsPresent } } $Output } ================================================ FILE: Public/New-PasswordConfigurationExternalUsers.ps1 ================================================ function New-PasswordConfigurationExternalUsers { <# .SYNOPSIS This function caches users from external systems to be used in the password configuration. .DESCRIPTION This function caches users from external systems to be used in the password configuration. It provides ability to find user by some property and get another property of the user. .PARAMETER Users Parameter description .PARAMETER ActiveDirectoryProperty Property in Active Directory to search for when comparing against SearchProperty. .PARAMETER SearchProperty Property to cache on the user object. .PARAMETER EmailProperty How the email property is called in the user object. .PARAMETER Global Tells the solution to globally overwrite email addresses for all users. .PARAMETER Name Name of the configuration. Visible in HTML reports. .EXAMPLE New-PasswordConfigurationExternalUsers -Users $ExportDataFromHrSystem -SearchProperty '' -EmailProperty '' -ActiveDirectoryProperty 'SamAccountName' .NOTES General notes #> [CmdletBinding()] param( [parameter(Mandatory)][string] $Name, [parameter(Mandatory)][Array] $Users, [parameter(Mandatory)][string] $ActiveDirectoryProperty, [parameter(Mandatory)][string] $SearchProperty, [parameter(Mandatory)][string] $EmailProperty, [switch] $Global ) $CachedUsers = [ordered] @{} if ($Users.Count -gt 0 -and $Users[0].$SearchProperty -and $Users[0].$EmailProperty) { Write-Color -Text '[+] ', "Caching users for '$Name'" -Color Green, White } else { Write-Color -Text '[-] ', "Couldn't cache users as either users not provided or email/search property are invalid. Please fix 'New-PasswordConfigurationExternalUsers'" -Color Yellow, White return } try { foreach ($User in $Users) { if ($User.$SearchProperty) { $CachedUsers[$User.$SearchProperty] = $User | Select-Object -Property $EmailProperty } } } catch { Write-Color -Text '[-] ', "Couldn't cache users. Please fix 'New-PasswordConfigurationExternalUsers'. Error: ", "$($_.Exception.Message)" -Color Yellow, White, Red return } [ordered] @{ Type = 'ExternalUsers' ActiveDirectoryProperty = $ActiveDirectoryProperty SearchProperty = $SearchProperty EmailProperty = $EmailProperty Users = $CachedUsers Global = $Global.IsPresent Name = $Name } } ================================================ FILE: Public/New-PasswordConfigurationOption.ps1 ================================================ function New-PasswordConfigurationOption { <# .SYNOPSIS Provides a way to create a PasswordConfigurationOption object. .DESCRIPTION This function provides a way to create a PasswordConfigurationOption object. The object is used to store configuration options for the Password Solution module. .PARAMETER ShowTime Show time in the console output. If not provided, time will not be shown. Time in the log file is always shown. .PARAMETER LogFile File path to the log file. If not provided, there will be no logging to file .PARAMETER TimeFormat Time format used in the logging functionality. .PARAMETER LogMaximum Maximum number of log files to keep. Default is 0 (unlimited). Once the number of log files exceeds the limit, the oldest log files will be deleted. .PARAMETER NotifyOnSkipUserManagerOnly Provides a way to control output to screen for SkipUserManagerOnly. .PARAMETER NotifyOnSecuritySend Provides a way to control output to screen for SecuritySend. .PARAMETER NotifyOnManagerSend Provides a way to control output to screen for ManagerSend. .PARAMETER NotifyOnUserSend Provides a way to control output to screen for UserSend. .PARAMETER NotifyOnUserMatchingRule Provides a way to control output to screen for UserMatchingRule. .PARAMETER NotifyOnUserDaysToExpireNull Provides a way to control output to screen for UserDaysToExpireNull. .PARAMETER NotifyOnUserMatchingRuleForManager Provides a way to control output to screen for UserMatchingRuleForManager. .PARAMETER NotifyOnUserMatchingRuleForManagerButNotCompliant Provides a way to control output to screen for UserMatchingRuleForManagerButNotCompliant. .PARAMETER SearchPath Path to XML file that will be used for storing search results. .PARAMETER EmailDateFormat Parameter description .PARAMETER EmailDateFormatUTCConversion Parameter description .PARAMETER OverwriteEmailProperty Parameter description .PARAMETER OverwriteManagerProperty Parameter description .PARAMETER FilterOrganizationalUnit Provides a way to filter users by Organizational Unit limiting the scope of the search. The search is performed using 'like' operator, so you can use wildcards if needed. .PARAMETER SearchBase Provides a way to filter users by Organizational Unit limiting the scope of the search. The search is passed to Get-ADUser cmdlet. This command should only be used when not using manager functionality. Otherwise you won't be able to find matching managers for your users. Use FilterOrganizationalUnit instead. .EXAMPLE $Options = @{ # Logging to file and to screen ShowTime = $true LogFile = "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" TimeFormat = "yyyy-MM-dd HH:mm:ss" LogMaximum = 365 NotifyOnSkipUserManagerOnly = $false NotifyOnSecuritySend = $true NotifyOnManagerSend = $true NotifyOnUserSend = $true NotifyOnUserMatchingRule = $false NotifyOnUserDaysToExpireNull = $false SearchPath = "$PSScriptRoot\Search\SearchLog_$((Get-Date).ToString('yyyy-MM')).xml" EmailDateFormat = "yyyy-MM-dd" EmailDateFormatUTCConversion = $true FilterOrganizationalUnit = @( "*OU=Accounts,OU=Administration,DC=ad,DC=evotec,DC=xyz" "*OU=Administration,DC=ad,DC=evotec,DC=xyz" ) } New-PasswordConfigurationOption @Options .NOTES General notes #> [CmdletBinding()] param( [switch] $ShowTime , #= $true [string] $LogFile , #= "$PSScriptRoot\Logs\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" [string] $TimeFormat , #= "yyyy-MM-dd HH:mm:ss" [int] $LogMaximum , #= 365 [switch] $NotifyOnSkipUserManagerOnly , #= $false [switch] $NotifyOnSecuritySend , #= $true [switch] $NotifyOnManagerSend , #= $true [switch] $NotifyOnUserSend , #= $true [switch] $NotifyOnUserMatchingRule , #= $true [switch] $NotifyOnUserDaysToExpireNull , #= $true [switch] $NotifyOnUserMatchingRuleForManager, [switch] $NotifyOnUserMatchingRuleForManagerButNotCompliant, [string] $SearchPath, [string] $EmailDateFormat, [switch] $EmailDateFormatUTCConversion, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [string[]] $FilterOrganizationalUnit, [string[]] $SearchBase ) $Output = [ordered] @{ Type = "PasswordConfigurationOption" Settings = [ordered] @{ ShowTime = $ShowTime.IsPresent LogFile = $LogFile TimeFormat = $TimeFormat LogMaximum = $LogMaximum NotifyOnSkipUserManagerOnly = $NotifyOnSkipUserManagerOnly.IsPresent NotifyOnSecuritySend = $NotifyOnSecuritySend.IsPresent NotifyOnManagerSend = $NotifyOnManagerSend.IsPresent NotifyOnUserSend = $NotifyOnUserSend.IsPresent NotifyOnUserMatchingRule = $NotifyOnUserMatchingRule.IsPresent NotifyOnUserDaysToExpireNull = $NotifyOnUserDaysToExpireNull.IsPresent NotifyOnUserMatchingRuleForManager = $NotifyOnUserMatchingRuleForManager.IsPresent NotifyOnUserMatchingRuleForManagerButNotCompliant = $NotifyOnUserMatchingRuleForManagerButNotCompliant.IsPresent SearchPath = $SearchPath # conversion for DateExpiry/PasswordLastSet only EmailDateFormat = $EmailDateFormat EmailDateFormatUTCConversion = $EmailDateFormatUTCConversion.IsPresent # email property conversion (global) OverwriteEmailProperty = $OverwriteEmailProperty # manager property conversion (global) OverwriteManagerProperty = $OverwriteManagerProperty # filtering FilterOrganizationalUnit = $FilterOrganizationalUnit # SearchBase SearchBase = $SearchBase } } Remove-EmptyValue -Hashtable $Output.Settings $Output } ================================================ FILE: Public/New-PasswordConfigurationReplacement.ps1 ================================================ function New-PasswordConfigurationReplacement { <# .SYNOPSIS Password configuration replacement function for replacing properties in the password configuration .DESCRIPTION Password configuration replacement function for replacing properties in the password configuration This function is used to create a replacement configuration for password properties. It takes a property name, a type of comparison, a hash table for property replacements, and an optional overwrite property name. It provides ability to replace specific value on given user object, on given property with different value. For example user having ExtensionAttribute1 with value 'PL' can be replaced with 'Poland'. .PARAMETER PropertyName Property name to be replaced in the password configuration. This is the name of the property in the password configuration that will be replaced. .PARAMETER Type Type of comparison to be used for the replacement. This parameter specifies the type of comparison to be used when replacing the property value. .PARAMETER PropertyReplacementHash Hash table containing the property replacements to be made. This parameter specifies the hash table that contains the mappings of property values to be replaced. .PARAMETER OverwritePropertyName Name of the property to be overwritten in the password configuration. This parameter specifies the name of the property that will be overwritten in the password configuration. This is an optional parameter. If the property doesn't exists, it will be added. If the property exists, it will be overwritten regardless of it's value. .EXAMPLE $showPasswordQualitySplat = @{ FilePath = "$PSScriptRoot\Reporting\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" WeakPasswords = "Test1", "Test2", "Test3", 'February2023!#!@ok', $Passwords | ForEach-Object { $_ } SeparateDuplicateGroups = $true PassThru = $true AddWorldMap = $true LogPath = "$PSScriptRoot\Logs\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).log" Online = $true LogMaximum = 5 Replacements = New-PasswordConfigurationReplacement -PropertyName 'Country' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' 'AT' = 'Austria' 'IT' = 'Italy' 'Unknown' = 'Not specified in AD' } -OverwritePropertyName 'AddMe' } Show-PasswordQuality @showPasswordQualitySplat -Verbose .EXAMPLE $Replacements = @( New-PasswordConfigurationReplacement -PropertyName 'Country' -Type eq -PropertyReplacementHash @{ 'PL' = 'Poland' 'DE' = 'Germany' 'AT' = 'Austria' 'IT' = 'Italy' 'Unknown' = 'Not specified in AD' } -OverwritePropertyName 'CountryCode' ) $Users = Find-PasswordQuality -Replacements $Replacements $Users | Format-Table .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory)][string] $PropertyName, [ValidateSet('eq')][string] $Type = 'eq', [Parameter(Mandatory)][System.Collections.IDictionary] $PropertyReplacementHash, [string] $OverwritePropertyName ) if ($PropertyReplacementHash.Count -eq 0) { Write-Color -Text '[-] ', "Couldn't create replacement configuration as the hash is empty. Please fix 'New-PasswordConfigurationReplacement'" -Color Yellow, White return } $Output = [ordered] @{ Type = "PasswordConfigurationReplacement" Settings = @{ PropertyName = $PropertyName Type = $Type PropertyReplacementHash = $PropertyReplacementHash OverwritePropertyName = $OverwritePropertyName } } $Output } ================================================ FILE: Public/New-PasswordConfigurationReport.ps1 ================================================ function New-PasswordConfigurationReport { <# .SYNOPSIS Provides HTML report configuration for Password Notifications in Password Solution. .DESCRIPTION Provides HTML report configuration for Password Notifications in Password Solution. The New-PasswordConfigurationReport function generates configuration for HTML report. .PARAMETER Enable Specifies whether to enable the report generation. The default value is $false. .PARAMETER ShowHTML Specifies whether to display the report in HTML format right after it's generated in default browser. The default value is $false. .PARAMETER Title Specifies the title of the report. The default value is "Password Solution Summary". .PARAMETER Online Specifies whether to generate the report using CDN for CSS and JS scripts, or use it locally. It doesn't require internet connectivity during generation. Makes the final output 3MB smaller. The default value is $false. .PARAMETER DisableWarnings Specifies whether to disable warning messages during report generation. The default value is $false. .PARAMETER ShowConfiguration Specifies whether to display the current Password Solution configuration settings. The default value is $false. .PARAMETER ShowAllUsers Specifies whether to display information about all user accounts. The default value is $false. .PARAMETER ShowRules Specifies whether to display information from the rules. The default value is $false. .PARAMETER ShowUsersSent Specifies whether to display information about users who have received (or not) password expiry notifications. The default value is $false. .PARAMETER ShowManagersSent Specifies whether to display information about managers who have received password expiry notifications. The default value is $false. .PARAMETER ShowEscalationSent Specifies whether to display information about escalation contacts who have received password expiry notifications. The default value is $false. .PARAMETER ShowSkippedUsers Specifies whether to display information about users who were during password expiry notifications because of inability to asses their expiration date. The default value is $false. .PARAMETER ShowSkippedLocations Specifies whether to display information about locations where skipped users are located. The default value is $false. .PARAMETER ShowSearchUsers Specifies whether to display information for searching who got password expiry notifications. The default value is $false. .PARAMETER ShowSearchManagers Specifies whether to display information for searching who got password expiry notifications and for which accounts from managers. The default value is $false. .PARAMETER ShowSearchEscalations Specifies whether to display information for searching who got password escalation notifications and what's the status of that message. The default value is $false. .PARAMETER ShowExternalSystemReplacementsUsers Specifies whether to display information about users who's email address was replaced by an external system. The default value is $false. .PARAMETER ShowExternalSystemReplacementsManagers Specifies whether to display information about managers who's email address was replaced by an external system. The default value is $false. .PARAMETER FilePath Specifies the file path for the report .PARAMETER AttachToEmail Specifies whether to attach the report to an administrative email. The default value is $false. .PARAMETER NestedRules Specifies whether to display nested password rules. Each rule has it's own tab with output. Having many rules and all other settings enabled can result in a very long list of tabs that's hard to navigate. This setting forces separate tab for all rules. The default value is $false. .PARAMETER ExcludeProperties Specifies an array of properties to exclude from the report. The default value is @('Manager', 'ManagerDN', 'MemberOf'). Manager, ManagerDN are not really needed in the report as they are already displayed in other form. MemberOf is not needed as it's not really relevant to the report, and can take a lot of space. .OUTPUTS The function returns an ordered dictionary that contains the report settings. .EXAMPLE New-PasswordConfigurationReport -ShowHTML -Title "Password Configuration Report" -FilePath "C:\Reports\PasswordReport.html" .EXAMPLE $Date = Get-Date $Report = [ordered] @{ Enable = $true ShowHTML = $true Title = "Password Solution Summary" Online = $true DisableWarnings = $true ShowConfiguration = $true ShowAllUsers = $true ShowRules = $true ShowUsersSent = $true ShowManagersSent = $true ShowEscalationSent = $true ShowSkippedUsers = $true ShowSkippedLocations = $true ShowSearchUsers = $true ShowSearchManagers = $true ShowSearchEscalations = $true NestedRules = $false FilePath = "$PSScriptRoot\Reporting\PasswordSolution_$(($Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" AttachToEmail = $true } New-PasswordConfigurationReport @Report #> [CmdletBinding()] param( [switch] $Enable, [switch] $ShowHTML, [string] $Title, [switch] $Online, [switch] $DisableWarnings, [switch] $ShowConfiguration, [switch] $ShowAllUsers, [switch] $ShowRules, [switch] $ShowUsersSent, [switch] $ShowManagersSent, [switch] $ShowEscalationSent, [switch] $ShowSkippedUsers, [switch] $ShowSkippedLocations, [switch] $ShowSearchUsers, [switch] $ShowSearchManagers, [switch] $ShowSearchEscalations , [string] $FilePath, [switch] $AttachToEmail, [switch] $NestedRules, [switch] $ShowExternalSystemReplacementsUsers, [switch] $ShowExternalSystemReplacementsManagers, [string[]] $ExcludeProperties = @('Manager', 'ManagerDN', 'MemberOf') ) $Output = [ordered] @{ Type = "PasswordConfigurationReport" Settings = [ordered] @{ Enable = $Enable.IsPresent ShowHTML = $ShowHTML.IsPresent Title = $Title Online = $Online.IsPresent DisableWarnings = $DisableWarnings.IsPresent ShowConfiguration = $ShowConfiguration.IsPresent ShowAllUsers = $ShowAllUsers.IsPresent ShowRules = $ShowRules.IsPresent ShowUsersSent = $ShowUsersSent.IsPresent ShowManagersSent = $ShowManagersSent.IsPresent ShowEscalationSent = $ShowEscalationSent.IsPresent ShowSkippedUsers = $ShowSkippedUsers.IsPresent ShowSkippedLocations = $ShowSkippedLocations.IsPresent ShowSearchUsers = $ShowSearchUsers.IsPresent ShowSearchManagers = $ShowSearchManagers.IsPresent ShowSearchEscalations = $ShowSearchEscalations.IsPresent FilePath = $FilePath AttachToEmail = $AttachToEmail.IsPresent NestedRules = $NestedRules.IsPresent ShowExternalSystemReplacementsUsers = $ShowExternalSystemReplacementsUsers.IsPresent ShowExternalSystemReplacementsManagers = $ShowExternalSystemReplacementsManagers.IsPresent ExcludeProperties = $ExcludeProperties } } $Output } ================================================ FILE: Public/New-PasswordConfigurationRule.ps1 ================================================ function New-PasswordConfigurationRule { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER ReminderConfiguration Parameter description .PARAMETER Name Parameter description .PARAMETER Enable Parameter description .PARAMETER IncludeExpiring Parameter description .PARAMETER IncludePasswordNeverExpires Parameter description .PARAMETER PasswordNeverExpiresDays Parameter description .PARAMETER IncludeName Include user in rule if any of the properties match the value of Name in the properties defined in IncludeNameProperties .PARAMETER IncludeNameProperties Include user in rule if any of the properties match the value as defined in IncludeName .PARAMETER ExcludeName Exclude user from rule if any of the properties match the value of Name in the properties defined in ExcludeNameProperties .PARAMETER ExcludeNameProperties Exclude user from rule if any of the properties match the value as defined in ExcludeName .PARAMETER IncludeOU Include user in rule if user is in any of the OUs defined in the IncludeOU parameter .PARAMETER ExcludeOU Exclude user from rule if users are in any of the OUs defined in the ExcludeOU parameter .PARAMETER ExcludeOUFromOtherRules Exclude users from rule if the user is in any of the OUs defined in the IncludeOU parameter in all other rules above it .PARAMETER IncludeGroup Include user in rule if users are in any of the groups defined in the IncludeGroup parameter .PARAMETER ExcludeGroup Exclude user from rule if users are in any of the groups defined in the ExcludeGroup parameter .PARAMETER ReminderDays Days before expiration to send reminder. If not set and ProcessManagersOnly is not set, the rule will throw an error. .PARAMETER ManagerReminder Parameter description .PARAMETER ManagerNotCompliant Parameter description .PARAMETER ManagerNotCompliantDisplayName Parameter description .PARAMETER ManagerNotCompliantEmailAddress Parameter description .PARAMETER ManagerNotCompliantDisabled Parameter description .PARAMETER ManagerNotCompliantMissing Parameter description .PARAMETER ManagerNotCompliantMissingEmail Parameter description .PARAMETER ManagerNotCompliantLastLogonDays Parameter description .PARAMETER SecurityEscalation Parameter description .PARAMETER SecurityEscalationDisplayName Parameter description .PARAMETER SecurityEscalationEmailAddress Parameter description .PARAMETER OverwriteEmailProperty Overwrite email property for specific rule. This is used to overwrite the email address of the user in the rule. For example, if the user has an email address in the property 'mail' and you want to use the property 'ExtensionAttribute7' instead, you can set this parameter to 'ExtensionAttribute7'. The email address will be used for the user in the rule. .PARAMETER OverwriteManagerProperty Overwrite manager property for specific rule. This is used to overwrite the manager of the user in the rule. For example, if the user has a manager in the property 'manager' and you want to use the property 'ExtensionAttribute8' instead, you can set this parameter to 'ExtensionAttribute8'. The manager will be used for the user in the rule. .PARAMETER OverwriteEmailFromExternalUsers Allow to overwrite email from external users for specific rule .PARAMETER ProcessManagersOnly This parameters is used to process users, but only managers will be notified. Sending emails to users within the rule will be skipped completly. This is useful if users would have email addresses, that would normally trigger an email to them. .PARAMETER DisableDays Define days to disable the user. This is used to disable users that are not already expired. .PARAMETER DisableWhatIf If set, the user will not be disabled, but a message will be shown that the user would be disabled. .PARAMETER DisableType Type of comparison to use for the DisableDays parameter. Default is 'eq'. Possible values are 'eq', 'in', 'lt', 'gt'. 'eq' - Days of expiration has to be equal to the value of DisableDays 'in' - Days of expiration has to be in the value of DisableDays 'lt' - Days of expiration has to be less than the value of DisableDays 'gt' - Days of expiration has to be greater than the value of DisableDays .EXAMPLE An example .NOTES General notes #> [CmdletBinding()] param( [scriptblock] $ReminderConfiguration, [parameter(Mandatory)][string] $Name, [switch] $Enable, [switch] $IncludeExpiring, [switch] $IncludePasswordNeverExpires, [nullable[int]]$PasswordNeverExpiresDays, [string[]] $IncludeNameProperties, [string[]] $IncludeName, [string[]] $ExcludeNameProperties, [string[]] $ExcludeName, [string[]] $IncludeOU, [switch] $ExcludeOUFromOtherRules, [string[]] $ExcludeOU, [string[]] $IncludeGroup, [string[]] $ExcludeGroup, [alias('ExpirationDays', 'Days')][Array] $ReminderDays, [switch] $ManagerReminder, [switch] $ManagerNotCompliant, [string] $ManagerNotCompliantDisplayName, [string] $ManagerNotCompliantEmailAddress, [switch] $ManagerNotCompliantDisabled, [switch] $ManagerNotCompliantMissing, [switch]$ManagerNotCompliantMissingEmail, [nullable[int]] $ManagerNotCompliantLastLogonDays, [switch] $SecurityEscalation, [string] $SecurityEscalationDisplayName, [string] $SecurityEscalationEmailAddress, [string] $OverwriteEmailProperty, [string] $OverwriteManagerProperty, [switch] $ProcessManagersOnly, [switch] $OverwriteEmailFromExternalUsers, [ValidateSet('eq', 'in', 'lt', 'gt')][string] $DisableType = 'eq', [Array] $DisableDays, [switch] $DisableWhatIf ) # Check if the parameters are set correctly if (-not $ProcessManagersOnly) { if ($null -eq $ReminderDays) { $ErrorMessage = "'ReminderDays' is required for rule '$Name', unless 'ProcessManagersOnly' is set. This is to make sure the rule is not skipped completly." Write-Color -Text "[e]", " Processing rule ", $Name, " failed because of error: ", $ErrorMessage -Color Yellow, White, Red return [ordered] @{ Type = 'PasswordConfigurationRule' Error = $ErrorMessage } } } # Logic to check if the DisableDays is set and if the DisableType is valid if ($DisableDays.Count -gt 0) { if ($DisableType -in 'eq', 'lt', 'gt') { if ($DisableDays.Count -gt 1) { $ErrorMessage = "Only one number for 'DisableDays' can be specified for Rule when using comparison types 'eq', 'lt', and 'gt'. Current values are $($DisableDays -join ', ') for '$DisableType'" Write-Color -Text "[e]", " Processing rule ", $Name, " failed because of error: ", $ErrorMessage -Color Yellow, White, Red return [ordered] @{ Type = 'PasswordConfigurationRule' Error = $ErrorMessage } } else { $DisableDaysToUse = $DisableDays[0] } } else { $DisableDaysToUse = $DisableDays } } $Output = [ordered] @{ Name = $Name Enable = $Enable.IsPresent IncludeExpiring = $IncludeExpiring.IsPresent IncludePasswordNeverExpires = $IncludePasswordNeverExpires.IsPresent Reminders = $ReminderDays PasswordNeverExpiresDays = $PasswordNeverExpiresDays IncludeNameProperties = $IncludeNameProperties IncludeName = $IncludeName IncludeOU = $IncludeOU ExcludeOUFromOtherRules = $ExcludeOUFromOtherRules.IsPresent ExcludeOU = $ExcludeOU SendToManager = [ordered] @{} ProcessManagersOnly = $ProcessManagersOnly.IsPresent OverwriteEmailProperty = $OverwriteEmailProperty # properties to overwrite manager based on different field OverwriteManagerProperty = $OverwriteManagerProperty OverwriteEmailFromExternalUsers = $OverwriteEmailFromExternalUsers.IsPresent DisableType = $DisableType DisableDays = $DisableDaysToUse DisableWhatIf = $DisableWhatIf.IsPresent } $Output.SendToManager['Manager'] = [ordered] @{ Enable = $false Reminders = [ordered] @{} } $Output.SendToManager['ManagerNotCompliant'] = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = $ManagerNotCompliantDisplayName EmailAddress = $ManagerNotCompliantEmailAddress } Disabled = $ManagerNotCompliantDisabled Missing = $ManagerNotCompliantMissing MissingEmail = $ManagerNotCompliantMissingEmail LastLogon = if ($PSBoundParameters.ContainsKey('ManagerNotCompliantLastLogonDays')) { $true } else { $false } LastLogonDays = $ManagerNotCompliantLastLogonDays Reminders = [ordered] @{ } } $Output.SendToManager['SecurityEscalation'] = [ordered] @{ Enable = $false Manager = [ordered] @{ DisplayName = $SecurityEscalationDisplayName EmailAddress = $SecurityEscalationEmailAddress } Reminders = [ordered] @{} } if ($ManagerReminder) { $Output.SendToManager['Manager'].Enable = $true } if ($ManagerNotCompliant) { $Output.SendToManager['ManagerNotCompliant'].Enable = $true } if ($SecurityEscalation) { $Output.SendToManager['SecurityEscalation'].Enable = $true } if ($ReminderConfiguration) { try { $RemindersExecution = & $ReminderConfiguration } catch { Write-Color -Text "[e]", " Processing rule ", $Output.Name, " failed because of error: ", $_.Exception.Message -Color Yellow, White, Red return [ordered] @{ Type = 'PasswordConfigurationRule' Error = $_.Exception.Message } } $ManagerRemindersFound = $false foreach ($Reminder in $RemindersExecution) { if ($Reminder.Type -eq 'Manager') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['Manager'].Reminders += $ReminderReminders $ManagerRemindersFound = $true } } elseif ($Reminder.Type -eq 'ManagerNotCompliant') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['ManagerNotCompliant'].Reminders += $ReminderReminders $ManagerRemindersFound = $true } } elseif ($Reminder.Type -eq 'Security') { foreach ($ReminderReminders in $Reminder.Reminders) { $Output.SendToManager['SecurityEscalation'].Reminders += $ReminderReminders $ManagerRemindersFound = $true } } else { # Should not happen throw "Invalid reminder type: $($Reminder.Type)" } } if ($ProcessManagersOnly) { if (-not $ManagerRemindersFound) { $ErrorMessage = "At least 1 reminder in 'ReminderConfiguration' is required for rule '$Name' when 'ProcessManagersOnly' is set. This is to make sure the rule is not skipped completly." Write-Color -Text "[e]", " Processing rule ", $Name, " failed because of error: ", $ErrorMessage -Color Yellow, White, Red return [ordered] @{ Type = 'PasswordConfigurationRule' Error = $ErrorMessage } } } } Remove-EmptyValue -Hashtable $Output -Recursive -Rerun 2 $Configuration = [ordered] @{ Type = 'PasswordConfigurationRule' Settings = $Output } $Configuration } ================================================ FILE: Public/New-PasswordConfigurationRuleReminder.ps1 ================================================ function New-PasswordConfigurationRuleReminder { [CmdletBinding(DefaultParameterSetName = 'Daily')] param( [Parameter(Mandatory, ParameterSetName = 'Daily')] [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [ValidateSet('Manager', 'ManagerNotCompliant', 'Security')][string] $Type, [Parameter(Mandatory, ParameterSetName = 'Daily')] [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [alias('ConditionDays', 'Days')][Array] $ExpirationDays, [Parameter(Mandatory, ParameterSetName = 'DayOfWeek')] [ValidateSet( 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' )][Array] $DayOfWeek, [Parameter(Mandatory, ParameterSetName = 'DayOfMonth')] [Array] $DayOfMonth, [Parameter(ParameterSetName = 'Daily')] [Parameter(ParameterSetName = 'DayOfWeek')] [Parameter(ParameterSetName = 'DayOfMonth')] [ValidateSet('lt', 'gt', 'eq', 'in')][string] $ComparisonType = 'eq' ) if ($ComparisonType -in 'eq', 'lt', 'gt') { if ($ExpirationDays.Count -gt 1) { throw "Only one number for 'ExpirationDays' can be specified for RuleReminder when using comparison types 'eq', 'lt', and 'gt'. Current values are $($ExpirationDays -join ', ') for '$ComparisonType'" } else { $ExpirationDaysToUse = $ExpirationDays[0] } } else { $ExpirationDaysToUse = $ExpirationDays } if ($PSCmdlet.ParameterSetName -eq 'Daily') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ Default = [ordered] @{ Enable = $true } } } } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfWeek') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ OnDay = [ordered] @{ Enable = $true Reminder = $ExpirationDaysToUse ComparisonType = $ComparisonType Days = $DayOfWeek } } } } elseif ($PSCmdlet.ParameterSetName -eq 'DayOfMonth') { $Reminders = [ordered] @{ Type = $Type Reminders = @{ OnDayOfMonth = [ordered] @{ Enable = $true Reminder = $ExpirationDaysToUse ComparisonType = $ComparisonType Days = $DayOfMonth } } } } $Reminders } ================================================ FILE: Public/New-PasswordConfigurationTemplate.ps1 ================================================ function New-PasswordConfigurationTemplate { [CmdletBinding()] param( [parameter(Mandatory)][ScriptBlock] $Template, [parameter(Mandatory)][string] $Subject, [parameter(Mandatory)][ValidateSet('PreExpiry', 'PostExpiry', 'Manager', 'ManagerNotCompliant', 'Security', 'Admin')] $Type ) $Output = [ordered] @{ Type = "PasswordConfigurationTemplate$Type" Settings = [ordered] @{ Template = $Template Subject = $Subject } } $Output } ================================================ FILE: Public/New-PasswordConfigurationType.ps1 ================================================ function New-PasswordConfigurationType { <# .SYNOPSIS Configures behavior of password notification emails for different types of users. .DESCRIPTION Configures behavior of password notification emails for different types of users. It is used to define how the notification emails should be sent to different types of users. It supports User, Manager, Security, and Admin types. Main function is to prevent sending emails to real users during testing phase. .PARAMETER Type Type of the configuration. Possible values are User, Manager, Security, and Admin. .PARAMETER Enable Enable sending emails for the specified type. .PARAMETER SendCountMaximum Maximum number of emails that can be sent for the specified type. This is used to prevent sending emails to a large number of users during testing phase. .PARAMETER DefaultEmail Default email address to which the emails should be sent. This is used to prevent sending emails to real users during testing phase. All emails will be sent to this email address for the specified type, with the exception of Admin type. Admin type will send emails to the specified email address as is. .PARAMETER AttachCSV Attach a CSV file with the list of users to the email. This is used to provide additional information about the users to the recipient. This is only used for Security type. .PARAMETER DisplayName Display name of the Admin user. This is used to provide a custom display name for the Admin user. If not specified, the default display name is "Administrators". .EXAMPLE New-PasswordConfigurationType -Type User -Enable -SendCountMaximum 10 -DefaultEmail 'przemyslaw.klys+testgithub1@test.pl' .EXAMPLE New-PasswordConfigurationType -Type Manager -Enable -SendCountMaximum 10 -DefaultEmail 'przemyslaw.klys+testgithub2@test.pl' .EXAMPLE New-PasswordConfigurationType -Type Security -Enable -SendCountMaximum 1 -DefaultEmail 'przemyslaw.klys+testgithub3@test.pl' -AttachCSV .EXAMPLE New-PasswordConfigurationType -Type Admin -Enable -EmailAddress 'przemyslaw.klys+testgithub3@test.pl' -DisplayName 'Administrators' .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory)][ValidateSet('User', 'Manager', 'Security', 'Admin')][string] $Type, [switch] $Enable, [int] $SendCountMaximum, [Alias('EmailAddress')][string] $DefaultEmail, [switch] $AttachCSV, [string] $DisplayName ) $Output = [ordered] @{ Type = "PasswordConfigurationType$Type" Settings = @{ Enable = $Enable.IsPresent SendCountMaximum = $SendCountMaximum SendToDefaultEmail = if ($DefaultEmail) { $true } else { $false } DefaultEmail = $DefaultEmail OverwriteEmailProperty = $OverwriteEmailProperty AttachCSV = $AttachCSV.IsPresent } } if ($Type -eq "Admin") { $Output.Settings.Manager = @{ DisplayName = if (-not $DisplayName) { "Administrators" } else { $DisplayName } EmailAddress = $DefaultEmail } } $Output } ================================================ FILE: Public/Show-PasswordQuality.ps1 ================================================ function Show-PasswordQuality { <# .SYNOPSIS Creates an HTML report showing password quality for all user objects in Active Directory. .DESCRIPTION Creates an HTML report showing password quality for all user objects in Active Directory. This comman utilizes DSInternals PowerShell module to get the data. Then it uses PSWriteHTML to create nice looking report. .PARAMETER FilePath Path to the file where report will be saved. .PARAMETER DontShow If specified, report will not be opened in a browser. .PARAMETER Online If specified report will use CDN for JS and CSS files. If not specified, it will merge all CSS and JS files into one HTML file. This makes the file at least 3MB bigger, even if there is very small amount of data. Keep in mind that this report can be created without internet access, just that opening it in a browser with -Online switch will require internet access. .PARAMETER WeakPasswords List of weak passwords that should be checked for. Provide a list of common passwords that you want to check for, and that your users may have used. .PARAMETER WeakPasswordsFilePath Path to a file that contains weak passwords, one password per line. .PARAMETER WeakPasswordsHashesFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. For performance reasons, the -WeakPasswordHashesSortedFile parameter should be used instead. .PARAMETER WeakPasswordsHashesSortedFile Path to a file that contains NT hashes of weak passwords, one hash in HEX format per line. The hashes must be sorted alphabetically, because a binary search is performed. This parameter is typically used with a list of leaked password hashes from HaveIBeenPwned. .PARAMETER SeparateDuplicateGroups If specified, report will show duplicate groups separately, one group per tab. .PARAMETER PassThru If specified, the function will return PowerShell object with all relevant information. .PARAMETER AddWorldMap If specified, a world map will be added to the report. .PARAMETER LogFile Path to the log file where the script will write its log entries. .PARAMETER LogMaximum Maximum number of files to keep in the log folder. .PARAMETER LogShowTime If specified, the log in console will show time on the left side. .PARAMETER LogTimeFormat Format of the time in the log file. Default is "yyyy-MM-dd HH:mm:ss". .PARAMETER Replacements List of replacements to be used in the report. .PARAMETER GroupBy List of properties to group by in the report. This expands 'DuplicatePasswordGroups' to improve information about given group from existing properties .EXAMPLE Show-PasswordQuality -FilePath $PSScriptRoot\Reporting\PasswordQuality.html -Online -WeakPasswords "Test1", "Test2", "Test3" -Verbose .EXAMPLE Show-PasswordQuality -FilePath "C:\Support\GitHub\TheDashboard\Ignore\Reports\CustomReports\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" -WeakPasswords "Test1", "Test2", "Test3" #-Verbose .NOTES General notes #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [string] $FilePath, [switch] $DontShow, [switch] $Online, [alias('KnownPasswords')][string[]] $WeakPasswords, [alias('KnownPasswordsFilePath')][string] $WeakPasswordsFilePath, [alias('KnownPasswordsHashesFile')][string] $WeakPasswordsHashesFile, [alias('KnownPasswordsHashesSortedFile')][string] $WeakPasswordsHashesSortedFile, [switch] $SeparateDuplicateGroups, [switch] $PassThru, [switch] $AddWorldMap, [alias('LogFile')][string] $LogPath, [int] $LogMaximum, [switch] $LogShowTime, [string] $LogTimeFormat = "yyyy-MM-dd HH:mm:ss", [System.Collections.IDictionary[]] $Replacements, [string[]] $GroupBy ) $TimeStart = Start-TimeLog $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Show-PasswordQuality' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName # since the first entry didn't go to log file, this will Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta -NoConsoleOutput Write-Color '[i]', ' Gathering passwords data' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color '[i]', ' Using provided ', $WeakPasswords.Count, " weak passwords to verify against." -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $ReplacementProperties = [System.Collections.Generic.List[string]]::new() $TimeStartPasswords = Start-TimeLog $findPasswordQualitySplat = @{ IncludeStatistics = $true WeakPasswords = $WeakPasswords WeakPasswordsFilePath = $WeakPasswordsFilePath WeakPasswordsHashesFile = $WeakPasswordsHashesFile WeakPasswordsHashesSortedFile = $WeakPasswordsHashesSortedFile Forest = $Forest ExcludeDomains = $ExcludeDomains IncludeDomains = $IncludeDomains ExtendedForestInformation = $ExtendedForestInformation } if ($Replacements) { $findPasswordQualitySplat['Replacements'] = $Replacements # Let's check if we have any replacements that are not in the list of properties foreach ($Replacement in $Replacements.Settings) { if ($Replacement.OverwritePropertyName -and $Replacement.OverwritePropertyName -notin $ReplacementProperties) { $ReplacementProperties.Add($Replacement.OverwritePropertyName) } elseif ($Replacement.PropertyName -and $Replacement.PropertyName -notin $ReplacementProperties) { $ReplacementProperties.Add($Replacement.PropertyName) } } } $PasswordQuality = Find-PasswordQuality @findPasswordQualitySplat if (-not $PasswordQuality) { # most likely DSInternals not installed return } $Users = $PasswordQuality.Users $Statistics = $PasswordQuality.Statistics $Countries = $PasswordQuality.StatisticsCountry $CountriesCodes = $PasswordQuality.StatisticsCountryCode $Continents = $PasswordQuality.StatisticsContinents $EndLogPasswords = Stop-TimeLog -Time $TimeStartPasswords -Option OneLiner Write-Color '[i]', ' Time to gather passwords data ', $EndLogPasswords -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $TimeStartHTML = Start-TimeLog Write-Color -Text '[i] ', 'Generating HTML report...' -Color Yellow, DarkGray New-HTML { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Password Solution - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } Write-Color -Text '[i] ', 'Generating summary statistics' -Color Yellow, DarkGray New-HTMLSection { New-HTMLSection -Invisible { New-HTMLPanel -Invisible { New-HTMLText -Text @( "This report shows current status of an Active Directory forest $($PasswordQuality.Forest)." "It focuses on the password quality of users in the following domains: " ) -FontSize 12px New-HTMLList { foreach ($Domain in $PasswordQuality.Domains) { New-HTMLListItem -Text $Domain -Color Blue } } -FontSize 12px $WeakPasswordsFileInformation = $PasswordQuality.WeakPasswordsFileInformation if ($WeakPasswords.Count -gt 0 -or $WeakPasswordsFileInformation.WeakPasswordsStats -or $WeakPasswordsFileInformation.WeakPasswordHashesStats -or $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats) { New-HTMLText -Text @( "The report uses following weak password features: " ) -FontSize 12px New-HTMLList { if ($WeakPasswords.Count -gt 0) { New-HTMLListItem -Text @( "This report uses ", $WeakPasswords.Count, " weak passwords to check for, as provided during runtime." ) -FontSize 12px -Color None, Red, None -FontWeight normal, bold, normal } if ($WeakPasswordsFileInformation.WeakPasswordsStats) { New-HTMLListItem -Text @( "This report uses weak passwords from ", $WeakPasswordsFileInformation.WeakPasswordsStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordsStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordsStats.LastWriteTime, "." ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal } if ($WeakPasswordsFileInformation.WeakPasswordHashesStats) { New-HTMLListItem -Text @( "This report uses weak passwords hashes from ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordHashesStats.LastWriteTime, "." ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal } if ($WeakPasswordsFileInformation.WeakPasswordHashesSortedStats) { New-HTMLListItem -Text @( "This report uses weak passwords hashes from ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.FullName, " to check for, as provided during runtime, size ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.Size, ", last write time ", $WeakPasswordsFileInformation.WeakPasswordHashesSortedStats.LastWriteTime, "." ) -FontSize 12px -Color None, Red, None, Blue, None, Blue, None -FontWeight normal, bold, normal, bold, normal, bold, normal } } } #New-HTMLText -LineBreak New-HTMLText -Text "Here's a short overview of what this report shows:" -Color None -FontSize 12px #New-HTMLText -LineBreak New-HTMLList { foreach ($Statistic in $Statistics.Keys | Where-Object { $_ -notlike '*EnabledOnly' -and $_ -notlike '*DisabledOnly' } ) { $ValueTotal = $Statistics[$Statistic] if ($Statistic -eq "DuplicatePasswordGroups") { $ValueEnabled = $Statistics['DuplicatePasswordUsersEnabledOnly'] $ValueDisabled = $Statistics['DuplicatePasswordUsersDisabledOnly'] New-HTMLListItem -Text @( "$($Statistic)", " property shows there are " "$ValueTotal" " groups of people with duplicate passwords." ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal } elseif ($Statistic -eq 'DuplicatePasswordUsers') { $ValueEnabled = $Statistics['DuplicatePasswordUsersEnabledOnly'] $ValueDisabled = $Statistics['DuplicatePasswordUsersDisabledOnly'] New-HTMLListItem -Text @( "$($Statistic)", " property shows there are " "$ValueEnabled " "enabled accounts, and " $ValueDisabled " disabled accounts having duplicate passwords with other accounts." ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal } else { $ValueEnabled = $Statistics[$Statistic + 'EnabledOnly'] $ValueDisabled = $Statistics[$Statistic + 'DisabledOnly'] New-HTMLListItem -Text @( "$($Statistic)", " property shows there are " "$ValueEnabled " "enabled accounts, and " "$ValueDisabled " "that are disabled." ) -Color Blue, None, Salmon, None, LightSkyBlue, None -FontWeight bold, normal, bold, normal, bold, normal } } } -Type Unordered -FontSize 12px New-HTMLText -Text "Please review the report and make sure that you're happy with findings!" -Color Blue -FontSize 12px } } New-HTMLSection -Invisible { New-HTMLChart { New-ChartBarOptions -Type barStacked New-ChartAxisY -LabelMaxWidth 250 -Show -LabelAlign left New-ChartLegend -LegendPosition bottom -HorizontalAlign center -Color Alizarin, LightSkyBlue -Names 'Enabled', 'Disabled' foreach ($Statistic in $Statistics.Keys | Where-Object { $_ -notlike '*EnabledOnly' -and $_ -notlike '*DisabledOnly' } ) { if ($Statistic -eq "DuplicatePasswordGroups") { $ValueTotal = $Statistics[$Statistic] New-ChartBar -Name $Statistic -Value @($ValueTotal, 0) } else { $ValueEnabled = $Statistics[$Statistic + 'EnabledOnly'] $ValueDisabled = $Statistics[$Statistic + 'DisabledOnly'] New-ChartBar -Name $Statistic -Value @($ValueEnabled, $ValueDisabled) } } # # Define event # New-ChartEvent -DataTableID 'NewIDtoSearchInChart' -ColumnID 0 } } } $PropertiesHighlight = @( 'ClearTextPassword' 'LMHash' 'EmptyPassword' 'WeakPassword' #'DefaultComputerPassword' #'PasswordNotRequired' #'PasswordNeverExpires' 'AESKeysMissing' 'PreAuthNotRequired' 'DESEncryptionOnly' 'Kerberoastable' 'DelegatableAdmins' 'SmartCardUsersWithPassword' #'DuplicatePasswordGroups' ) Write-Color -Text '[i] ', 'Generating users table with all information' -Color Yellow, DarkGray New-HTMLSection -HeaderText "Password Quality" { New-HTMLTable -DataTable $Users -Filtering { New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator lt -Value 30 -BackgroundColor LimeGreen -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30 -BackgroundColor Orange -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Alizarin -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value '' -BackgroundColor None -HighlightHeaders LastLogonDays, LastLogonDate New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Alizarin -HighlightHeaders PasswordLastSet, PasswordLastChangedDays New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin New-HTMLTableCondition -Name 'PasswordExpired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin -HighlightHeaders PasswordExpired, DaysToExpire, DateExpiry foreach ($Property in $PropertiesHighlight) { New-HTMLTableCondition -Name $Property -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightGreen } New-HTMLTableCondition -Name 'DuplicatePasswordGroups' -ComparisonType string -Operator ne -Value "" -BackgroundColor Orange -FailBackgroundColor LightGreen } -ScrollX -ExcludeProperty 'RuleName', 'RuleOptions', 'CountryCode', 'Type', 'ManagerDN', 'DistinguishedName', 'MemberOf' -AllProperties } if ($SeparateDuplicateGroups) { Write-Color -Text '[i] ', 'Generating duplicate password groups section' -Color Yellow, DarkGray New-HTMLSection -HeaderText "Duplicate Password Groups" { $TotalDuplicateGroups = 0 $EnabledUsersInDuplicateGroups = 0 $DisabledUsersInDuplicateGroups = 0 $DuplicateGroups = [ordered] @{} foreach ($User in $Users) { if ($User.DuplicatePasswordGroups) { if ($User.Enabled) { $EnabledUsersInDuplicateGroups++ } else { $DisabledUsersInDuplicateGroups++ } if (-not $DuplicateGroups[$User.DuplicatePasswordGroups]) { $DuplicateGroups[$User.DuplicatePasswordGroups] = [ordered] @{ GroupName = $User.DuplicatePasswordGroups UsersTotal = 0 UsersEnabled = 0 UsersDisabled = 0 WeakPassword = $false Users = [System.Collections.Generic.List[string]]::new() Country = [System.Collections.Generic.List[string]]::new() UsersBySamAccountName = [System.Collections.Generic.List[string]]::new() UsersByUPN = [System.Collections.Generic.List[string]]::new() UsersByEmail = [System.Collections.Generic.List[string]]::new() } foreach ($Property in $ReplacementProperties) { $DuplicateGroups[$User.DuplicatePasswordGroups].$Property = [System.Collections.Generic.List[string]]::new() } foreach ($Property in $GroupBy | Sort-Object -Unique) { $DuplicateGroups[$User.DuplicatePasswordGroups].$Property = [System.Collections.Generic.List[string]]::new() } $DuplicateGroups[$User.DuplicatePasswordGroups] = [PSCustomObject] $DuplicateGroups[$User.DuplicatePasswordGroups] } if ($User.WeakPassword) { $DuplicateGroups[$User.DuplicatePasswordGroups].WeakPassword = $true } $DuplicateGroups[$User.DuplicatePasswordGroups].Users.Add($User.Name) if ($User.Enabled) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersEnabled++ } else { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersDisabled++ } $DuplicateGroups[$User.DuplicatePasswordGroups].UsersTotal++ if ($User.EmailAddress) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersByEmail.Add($User.EmailAddress) } if ($User.UserPrincipalName) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersByUPN.Add($User.UserPrincipalName) } if ($User.SamAccountName) { $DuplicateGroups[$User.DuplicatePasswordGroups].UsersBySamAccountName.Add($User.SamAccountName) } $DuplicateGroups[$User.DuplicatePasswordGroups].Country.Add($User.Country) foreach ($Property in $ReplacementProperties) { if (-not [string]::IsNullOrEmpty($User.$Property)) { $DuplicateGroups[$User.DuplicatePasswordGroups].$Property.Add($User.$Property) } } foreach ($Property in $GroupBy | Sort-Object -Unique) { if (-not [string]::IsNullOrEmpty($User.$Property)) { $DuplicateGroups[$User.DuplicatePasswordGroups].$Property.Add($User.$Property) } } } } $TotalDuplicateGroups = $DuplicateGroups.Keys.Count foreach ($Group in $DuplicateGroups.Values) { # $Group.UsersTotal = $Group.Users.Count $Group.Country = $Group.Country | Select-Object -Unique } New-HTMLContainer { New-HTMLSection { New-HTMLPanel { New-HTMLToast -TextHeader 'Total Duplicate Groups' -Text "Groups of users to review: $TotalDuplicateGroups" -BarColorLeft MayaBlue -IconSolid info-circle -IconColor MayaBlue } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'Enabled Users' -Text "Users with duplicate password that are enabled: $EnabledUsersInDuplicateGroups" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'Disabled Users' -Text "Users with duplicate password that are disabled: $DisabledUsersInDuplicateGroups" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible } -Invisible New-HTMLText -Text @( 'The following table shows the users that have the same password as other users in the same group. ' 'The table is sortable and filterable. ' 'The table also shows the country of the user. ' 'The table also shows the email address, UPN and SamAccountName of the user.' ) -FontSize 12px New-HTMLSection -Invisible { New-HTMLTable -DataTable $DuplicateGroups.Values -Filtering -Title "Duplicate Password Group: $DuplicateGroup" { New-HTMLTableCondition -Name 'WeakPassword' -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightBlue }-ScrollX -ExcludeProperty 'RuleName', 'RuleOptions', 'Type', 'CountryCode' } New-HTMLText -Text @( "Please NOTE: " "number of " "users" " , may not be the same as the number of users in " "UsersBySamAccountName" ", " "UsersByUpn" " or " "UsersByEmail" " columns. We only show users with email address, UPN or SamAccountName if it exists. " "If the account doesn't have email, UPN or SamAccountName, we don't show it in the table." ) -FontSize 12px -FontWeight bold, normal, bold, normal, bold, normal, bold, normal, bold, normal, normal } } } if ($AddWorldMap) { Write-Color -Text '[i] ', 'Generating duplicate passwords map' -Color Yellow, DarkGray New-HTMLSection -HeaderText 'Duplicate Passwords Per Country' { New-HTMLTabPanel { New-HTMLTab -Name 'Map showing duplicate passwords per country' { New-HTMLSection -Invisible { New-HTMLPanel { New-HTMLMap -Map world_countries { # add the map areas # we will add unknown countries to the Greenland area foreach ($Country in $CountriesCodes['DuplicatePasswordUsers'].Keys) { if ($Country -eq 'Unknown') { New-MapArea -Area 'GL' -Value $CountriesCodes['DuplicatePasswordUsers'][$Country] -Tooltip { New-HTMLText -Text @( 'Unknown / Unavailable' '
' "Users with duplicate passwords $($CountriesCodes['DuplicatePasswordUsers'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } else { New-MapArea -Area $Country -Value $CountriesCodes['DuplicatePasswordUsers'][$Country] -Tooltip { New-HTMLText -Text @( Convert-CountryCodeToCountry -CountryCode $Country '
' "Users with duplicate passwords $($CountriesCodes['DuplicatePasswordUsers'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } } # configure legend New-MapLegendOption -Type 'Area' -Mode horizontal New-MapLegendOption -Type 'Plot' -Mode horizontal # add legend New-MapLegendSlice -Type 'Area' -Label 'Duplicate passwords up to 5' -Min 0 -Max 5 -SliceColor 'Bisque' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 5 and 15' -Min 6 -Max 15 -SliceColor 'Amber' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 16 and 30' -Min 16 -Max 30 -SliceColor 'CarnationPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate between 31 and 50' -Min 31 -Max 50 -SliceColor 'BrinkPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Duplicate over 50' -Min 51 -SliceColor 'Red' -StrokeWidth 0 } -ShowAreaLegend #-AreaTitle "Duplicate Passwords Users" New-HTMLText -Text @( "The map shows the number of users with duplicate passwords per country. The legend shows the number of users with duplicate passwords per color." ) -FontSize 12px } } } New-HTMLTab -Name 'Duplicate Passwords Per Country' { New-HTMLTable -DataTable $Countries['DuplicatePasswordUsers'] -Filtering } New-HTMLTab -Name 'Duplicate Passwords Per Continent' { New-HTMLTable -DataTable $Continents['DuplicatePasswordUsers'] -Filtering } } } Write-Color -Text '[i] ', 'Generating weak password map' -Color Yellow, DarkGray New-HTMLSection -HeaderText 'Weak Password Per Country' { New-HTMLTabPanel { New-HTMLTab -Name 'Map showing weak password per country' { New-HTMLSection -Invisible { New-HTMLPanel { New-HTMLMap -Map world_countries { # add the map areas # we will add unknown countries to the Greenland area foreach ($Country in $CountriesCodes['WeakPassword'].Keys) { if ($Country -eq 'Unknown') { New-MapArea -Area 'GL' -Value $CountriesCodes['WeakPassword'][$Country] -Tooltip { New-HTMLText -Text @( 'Unknown / Unavailable' '
' "Users with weak passwords $($CountriesCodes['WeakPassword'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } else { New-MapArea -Area $Country -Value $CountriesCodes['WeakPassword'][$Country] -Tooltip { New-HTMLText -Text @( Convert-CountryCodeToCountry -CountryCode $Country '
' "Users with weak passwords $($CountriesCodes['WeakPassword'][$Country])" ) -Color Black, Black, Blue -FontWeight bold, normal, normal -SkipParagraph -FontSize 15px, 14px, 14px } } } # configure legend New-MapLegendOption -Type 'Area' -Mode horizontal New-MapLegendOption -Type 'Plot' -Mode horizontal # add legend New-MapLegendSlice -Type 'Area' -Label 'Weak passwords up to 5' -Min 0 -Max 5 -SliceColor 'Bisque' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak between 5 and 15' -Min 6 -Max 15 -SliceColor 'Amber' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak between 16 and 30' -Min 16 -Max 30 -SliceColor 'CarnationPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak between 31 and 50' -Min 31 -Max 50 -SliceColor 'BrinkPink' -StrokeWidth 0 New-MapLegendSlice -Type 'Area' -Label 'Weak over 50' -Min 51 -SliceColor 'Red' -StrokeWidth 0 } -ShowAreaLegend #-AreaTitle "Weak Password Users" } } } New-HTMLTab -Name 'Weak Password Per Country' { New-HTMLTable -DataTable $Countries['WeakPassword'] -Filtering } New-HTMLTab -Name 'Weak Password Per Continent' { New-HTMLTable -DataTable $Continents['WeakPassword'] -Filtering } } } if ($LogPath -and (Test-Path -LiteralPath $LogPath)) { $LogContent = Get-Content -Raw -LiteralPath $LogPath New-HTMLSection -Name 'Log' { New-HTMLCodeBlock -Code $LogContent -Style generic } } } } -ShowHTML:(-not $DontShow.IsPresent) -Online:$Online.IsPresent -TitleText "Password Solution - Quality Password Check" -Author "Password Solution" -FilePath $FilePath $EndLogHTML = Stop-TimeLog -Time $TimeStartHTML -Option OneLiner $EndLog = Stop-TimeLog -Time $TimeStart -Option OneLiner Write-Color '[i]', ' Time to generate HTML ', $EndLogHTML -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color '[i]', ' Time to generate ', $EndLog -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta if ($PassThru) { $PasswordQuality } } ================================================ FILE: Public/Start-PasswordSolution.ps1 ================================================ function Start-PasswordSolution { <# .SYNOPSIS Starts Password Expiry Notifications for the whole forest .DESCRIPTION Starts Password Expiry Notifications for the whole forest .PARAMETER ConfigurationDSL Parameter description .PARAMETER EmailParameters Parameters for Email. Uses Mailozaurr splatting behind the scenes, so it supports all options that Mailozaurr does. .PARAMETER OverwriteEmailProperty Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address. .PARAMETER UserSection Parameter description .PARAMETER ManagerSection Parameter description .PARAMETER SecuritySection Parameter description .PARAMETER AdminSection Parameter description .PARAMETER UsersExternalSystem Property responsible for overwriting the default email field in Active Directory. Useful when the password notification has to go somewhere else than users email address. It comes in a specific format as generated by `New-PasswordConfigurationExternalUsers` .PARAMETER Rules Parameter description .PARAMETER TemplatePreExpiry Parameter description .PARAMETER TemplatePreExpirySubject Parameter description .PARAMETER TemplatePostExpiry Parameter description .PARAMETER TemplatePostExpirySubject Parameter description .PARAMETER TemplateManager Parameter description .PARAMETER TemplateManagerSubject Parameter description .PARAMETER TemplateSecurity Parameter description .PARAMETER TemplateSecuritySubject Parameter description .PARAMETER TemplateManagerNotCompliant Parameter description .PARAMETER TemplateManagerNotCompliantSubject Parameter description .PARAMETER TemplateAdmin Parameter description .PARAMETER TemplateAdminSubject Parameter description .PARAMETER Entra Parameter description .PARAMETER OverwriteManagerProperty Parameter description .PARAMETER FilterOrganizationalUnit Provides a way to filter users by Organizational Unit limiting the scope of the search. The search is performed using 'like' operator, so you can use wildcards if needed. .PARAMETER SearchBase Provides a way to filter users by Organizational Unit limiting the scope of the search. The search is passed to Get-ADUser cmdlet. This command should only be used when not using manager functionality. Otherwise you won't be able to find matching managers for your users. Use FilterOrganizationalUnit instead. .PARAMETER Logging Parameter description .PARAMETER HTMLReports Parameter description .PARAMETER SearchPath Parameter description .EXAMPLE An example .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'DSL')] param( [Parameter(ParameterSetName = 'Legacy', Position = 0)] [Parameter(ParameterSetName = 'DSL', Position = 0)][scriptblock] $ConfigurationDSL, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $EmailParameters, [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteEmailProperty, [Parameter(ParameterSetName = 'Legacy')][string] $OverwriteManagerProperty, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UserSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $ManagerSection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $SecuritySection, [Parameter(Mandatory, ParameterSetName = 'Legacy')][System.Collections.IDictionary] $AdminSection, [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $UsersExternalSystem, [Parameter(Mandatory, ParameterSetName = 'Legacy')][Array] $Rules, [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePreExpiry, [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePreExpirySubject, [Parameter(ParameterSetName = 'Legacy')][scriptblock] $TemplatePostExpiry, [Parameter(ParameterSetName = 'Legacy')][string] $TemplatePostExpirySubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManager, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerSubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateSecurity, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateSecuritySubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateManagerNotCompliant, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateManagerNotCompliantSubject, [Parameter(Mandatory, ParameterSetName = 'Legacy')][scriptblock] $TemplateAdmin, [Parameter(Mandatory, ParameterSetName = 'Legacy')][string] $TemplateAdminSubject, [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $Entra = [ordered] @{}, [Parameter(ParameterSetName = 'Legacy')][System.Collections.IDictionary] $Logging = [ordered] @{}, [Parameter(ParameterSetName = 'Legacy')][Array] $HTMLReports, [Parameter(ParameterSetName = 'Legacy')][string] $SearchPath, [Parameter(ParameterSetName = 'Legacy')][string[]] $FilterOrganizationalUnit, [Parameter(ParameterSetName = 'Legacy')][string[]] $SearchBase ) $TimeStart = Start-TimeLog $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-PasswordSolution' -RepositoryOwner 'evotecit' -RepositoryName 'PasswordSolution' Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $TodayDate = Get-Date $Today = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $Summary = [ordered] @{} $Summary['Notify'] = [ordered] @{} $Summary['NotifyManager'] = [ordered] @{} $Summary['NotifySecurity'] = [ordered] @{} $Summary['Rules'] = [ordered] @{} $Summary['Tracking'] = [ordered] @{ 'IncludeOU' = [System.Collections.Generic.List[string]]::new() 'ExcludeOU' = [System.Collections.Generic.List[string]]::new() } $AllSkipped = [ordered] @{} $Locations = [ordered] @{} # lets try to get configuration, and if not provided do some defaults $SplatPasswordConfiguration = [ordered] @{ ConfigurationDSL = $ConfigurationDSL EmailParameters = $EmailParameters OverwriteEmailProperty = $OverwriteEmailProperty OverwriteManagerProperty = $OverwriteManagerProperty UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection Rules = $Rules TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject Logging = $Logging HTMLReports = $HTMLReports SearchPath = $SearchPath UsersExternalSystem = $UsersExternalSystem FilterOrganizationalUnit = $FilterOrganizationalUnit SearchBase = $SearchBase Entra = $Entra } $InitialVariables = Set-PasswordConfiguration @SplatPasswordConfiguration if (-not $InitialVariables) { return } $EmailParameters = $InitialVariables.EmailParameters $OverwriteEmailProperty = $InitialVariables.OverwriteEmailProperty $OverwriteManagerProperty = $InitialVariables.OverwriteManagerProperty $UserSection = $InitialVariables.UserSection $ManagerSection = $InitialVariables.ManagerSection $SecuritySection = $InitialVariables.SecuritySection $AdminSection = $InitialVariables.AdminSection $Rules = $InitialVariables.Rules $TemplatePreExpiry = $InitialVariables.TemplatePreExpiry $TemplatePreExpirySubject = $InitialVariables.TemplatePreExpirySubject $TemplatePostExpiry = $InitialVariables.TemplatePostExpiry $TemplatePostExpirySubject = $InitialVariables.TemplatePostExpirySubject $TemplateManager = $InitialVariables.TemplateManager $TemplateManagerSubject = $InitialVariables.TemplateManagerSubject $TemplateSecurity = $InitialVariables.TemplateSecurity $TemplateSecuritySubject = $InitialVariables.TemplateSecuritySubject $TemplateManagerNotCompliant = $InitialVariables.TemplateManagerNotCompliant $TemplateManagerNotCompliantSubject = $InitialVariables.TemplateManagerNotCompliantSubject $TemplateAdmin = $InitialVariables.TemplateAdmin $TemplateAdminSubject = $InitialVariables.TemplateAdminSubject $Logging = $InitialVariables.Logging $HTMLReports = $InitialVariables.HTMLReports $SearchPath = $InitialVariables.SearchPath $UsersExternalSystem = $InitialVariables.UsersExternalSystem $FilterOrganizationalUnit = $InitialVariables.FilterOrganizationalUnit $SearchBase = $InitialVariables.SearchBase $Entra = $InitialVariables.Entra Set-LoggingCapabilities -LogPath $Logging.LogFile -LogMaximum $Logging.LogMaximum -ShowTime:$Logging.ShowTime -TimeFormat $Logging.TimeFormat -ScriptPath $MyInvocation.ScriptName # since the first entry didn't go to log file, this will Write-Color -Text '[i]', "[PasswordSolution] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta -NoConsoleOutput # this is to get properties from rules to be used in building up user output [Array] $ExtendedProperties = foreach ($Rule in $Rules ) { if ($Rule.OverwriteEmailProperty) { $Rule.OverwriteEmailProperty } if ($Rule.OverwriteManagerProperty) { $Rule.OverwriteManagerProperty } } $SummarySearch = Import-SearchInformation -SearchPath $SearchPath Write-Color -Text "[i]", " Starting process to find expiring users" -Color Yellow, White, Green, White, Green, White, Green, White $ExternalSystemReplacements = [ordered] @{} # This is to cache users from AD before we start processing them # Will be used by Managers and Security notifications when using filtering $GlobalManagerCache = [ordered] @{} if ($Entra.Enabled) { # Doesn't support external system replacements $CachedUsers = Find-PasswordEntra -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty -RulesProperties $ExtendedProperties -OverwriteManagerProperty $OverwriteManagerProperty -UsersExternalSystem $UsersExternalSystem -ExternalSystemReplacements $ExternalSystemReplacements -FilterOrganizationalUnit $FilterOrganizationalUnit -CacheManager $GlobalManagerCache } else { $CachedUsers = Find-Password -AsHashTable -OverwriteEmailProperty $OverwriteEmailProperty -RulesProperties $ExtendedProperties -OverwriteManagerProperty $OverwriteManagerProperty -UsersExternalSystem $UsersExternalSystem -ExternalSystemReplacements $ExternalSystemReplacements -FilterOrganizationalUnit $FilterOrganizationalUnit -CacheManager $GlobalManagerCache -SearchBase $SearchBase } if (-not $CachedUsers -or $CachedUsers.Count -eq 0) { Write-Color -Text "[e]", " No users found to be processed by Password Rules according to filtering settings. Terminating" -Color Yellow, White, Red return } Write-Color -Text "[i]", " Found ", $CachedUsers.Count, " users to be processed by Password Rules according to filtering settings" -Color Yellow, White, Green, White, Green, White, Green, White if ($Rules.Count -eq 0) { Write-Color -Text "[e]", " No rules found. Please add some rules to configuration" -Color Yellow, White, Red return } foreach ($Rule in $Rules) { $SplatProcessingRule = [ordered] @{ Rule = $Rule Summary = $Summary CachedUsers = $CachedUsers AllSkipped = $AllSkipped Locations = $Locations Loggin = $Logging TodayDate = $TodayDate UsersExternalSystem = $UsersExternalSystem Entra = $Entra } Invoke-PasswordRuleProcessing @SplatProcessingRule } $SplatUserNotifications = [ordered] @{ UserSection = $UserSection Summary = $Summary Logging = $Logging TemplatePreExpiry = $TemplatePreExpiry TemplatePreExpirySubject = $TemplatePreExpirySubject TemplatePostExpiry = $TemplatePostExpiry TemplatePostExpirySubject = $TemplatePostExpirySubject EmailParameter = $EmailParameters } [Array] $SummaryUsersEmails = Send-PasswordUserNofifications @SplatUserNotifications $SplatManagerNotifications = [ordered] @{ ManagerSection = $ManagerSection Summary = $Summary CachedUsers = $CachedUsers TemplateManager = $TemplateManager TemplateManagerSubject = $TemplateManagerSubject TemplateManagerNotCompliant = $TemplateManagerNotCompliant TemplateManagerNotCompliantSubject = $TemplateManagerNotCompliantSubject EmailParameters = $EmailParameters Loggin = $Logging GlobalManagersCache = $GlobalManagerCache } [Array] $SummaryManagersEmails = Send-PasswordManagerNofifications @SplatManagerNotifications $SplatSecurityNotifications = [ordered] @{ SecuritySection = $SecuritySection Summary = $Summary TemplateSecurity = $TemplateSecurity TemplateSecuritySubject = $TemplateSecuritySubject Logging = $Logging } [Array] $SummaryEscalationEmails = Send-PasswordSecurityNotifications @SplatSecurityNotifications $TimeEnd = Stop-TimeLog -Time $TimeStart -Option OneLiner Export-SearchInformation -SearchPath $SearchPath -SummarySearch $SummarySearch -Today $Today -SummaryUsersEmails $SummaryUsersEmails -SummaryManagersEmails $SummaryManagersEmails -SummaryEscalationEmails $SummaryEscalationEmails $HtmlAttachments = [System.Collections.Generic.List[string]]::new() foreach ($Report in $HTMLReports) { if ($Report.Enable) { $ReportSettings = @{ Report = $Report EmailParameters = $EmailParameters Logging = $Logging SearchPath = $SearchPath Rules = $Rules UserSection = $UserSection ManagerSection = $ManagerSection SecuritySection = $SecuritySection AdminSection = $AdminSection CachedUsers = $CachedUsers Summary = $Summary SummaryUsersEmails = $SummaryUsersEmails SummaryManagersEmails = $SummaryManagersEmails SummaryEscalationEmails = $SummaryEscalationEmails SummarySearch = $SummarySearch Locations = $Locations AllSkipped = $AllSkipped ExternalSystemReplacements = $ExternalSystemReplacements TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject SearchBase = $SearchBase FilterOrganizationalUnit = $FilterOrganizationalUnit } New-HTMLReport @ReportSettings if ($Report.AttachToEmail) { if (Test-Path -LiteralPath $Report.FilePath) { $HtmlAttachments.Add($Report.FilePath) } else { Write-Color -Text "[w] HTML report ", $Report.FilePath, " does not exist! Probably a temporary path was used. " -Color DarkYellow, Red, DarkYellow } } } } $AdminSplat = [ordered] @{ AdminSection = $AdminSection TemplateAdmin = $TemplateAdmin TemplateAdminSubject = $TemplateAdminSubject TimeEnd = $TimeEnd EmailParameters = $EmailParameters HtmlAttachment = $HtmlAttachments } Send-PasswordAdminNotifications @AdminSplat } ================================================ FILE: README.MD ================================================ 

# PasswordSolution **PasswordSolution** is a PowerShell module that provides Password Expiry notifications to users, managers, security and administrators. It's very configurable and was designed for enterprise use. ## Features - Find all users with passwords that are expiring and send notifications including service accounts, admin accounts and accounts that never expire - Find and asses password quality of users in an Active Directory forest. ## Support This Project If you find this project helpful, please consider supporting its development. Your sponsorship will help the maintainers dedicate more time to maintenance and new feature development for everyone. It takes a lot of time and effort to create and maintain this project. By becoming a sponsor, you can help ensure that it stays free and accessible to everyone who needs it. To become a sponsor, you can choose from the following options: - [Become a sponsor via GitHub Sponsors :heart:](https://github.com/sponsors/PrzemyslawKlys) - [Become a sponsor via PayPal :heart:](https://paypal.me/PrzemyslawKlys) Your sponsorship is completely optional and not required for using this project. We want this project to remain open-source and available for anyone to use for free, regardless of whether they choose to sponsor it or not. If you work for a company that uses our .NET libraries or PowerShell Modules, please consider asking your manager or marketing team if your company would be interested in supporting this project. Your company's support can help us continue to maintain and improve this project for the benefit of everyone. Thank you for considering supporting this project! ## Installing Everyone can install this module from **PowerShellGallery** hosted by Microsoft. It's recommended way to work with the module. Version on **PowershellGallery** is optimized for speed and signed. Using code from **GitHub** is **recommended for development**. ```powershell Install-Module -Name PasswordSolution -AllowClobber -Force -Verbose ``` If you want to use Password Quality checks you need to install DSInternals manually. Due to sensitive nature of the module it's not included in the package. ```powershell Install-Module DSInternals -Force -Verbose ``` Force and AllowClobber aren't necessary, but they do skip errors in case some appear, and they do update module if newer version is available. ## Updating ```powershell Update-Module -Name PasswordSolution ``` That's it. Whenever there's a new version, you run the command, and you can enjoy it. Remember that you may need to close, reopen PowerShell session if you have already used module before updating it. **The essential thing** is if something works for you on production, keep using it till you test the new version on a test computer. I do changes that may not be big, but big enough that auto-update may break your code. For example, small rename to a parameter and your code stops working! Be responsible! ## Usage - Password Quality Password Quality report uses [DSInternals](https://github.com/MichaelGrafnetter/DSInternals) module to scan Active Directory for passwords and then assesses them. It's a very powerful tool that can be used to find weak passwords in your environment. It's also a great tool to find out if your password policy is working as expected. PasswordSolution wraps around DSInternals to make it easier to use and to provide a nice HTML report. ```powershell Show-PasswordQuality -FilePath C:\Temp\PasswordQuality.html -Online -WeakPasswords "Test1", "Test2", "Test3" -Verbose -SeparateDuplicateGroups -AddWorldMap -PassThru ``` or ```powershell $showPasswordQualitySplat = @{ FilePath = "$PSScriptRoot\Reporting\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).html" WeakPasswords = "Test1", "Test2", "Test3", 'February2023!#!@ok', $Passwords | ForEach-Object { $_ } SeparateDuplicateGroups = $true PassThru = $true AddWorldMap = $true LogPath = "$PSScriptRoot\Logs\PasswordQuality_$(Get-Date -f yyyy-MM-dd_HHmmss).log" Online = $true LogMaximum = 5 } Show-PasswordQuality @showPasswordQualitySplat -Verbose ``` And here's what you get ![PasswordQuality](https://raw.githubusercontent.com/EvotecIT/PasswordSolution/master/Docs/Images/PasswordQuality1.png) ![PasswordQuality](https://raw.githubusercontent.com/EvotecIT/PasswordSolution/master/Docs/Images/PasswordQuality2.png) ![PasswordQuality](https://raw.githubusercontent.com/EvotecIT/PasswordSolution/master/Docs/Images/PasswordQuality3.png) ![PasswordQuality](https://raw.githubusercontent.com/EvotecIT/PasswordSolution/master/Docs/Images/PasswordQuality4.png) ![PasswordQuality](https://raw.githubusercontent.com/EvotecIT/PasswordSolution/master/Docs/Images/PasswordQuality5.png) ![PasswordQuality](https://raw.githubusercontent.com/EvotecIT/PasswordSolution/master/Docs/Images/PasswordQuality6.png)