├── LICENSE ├── README.md └── RecurringADChecks.ps1 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PaulHCode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RecurringADChecks 2 | 3 | ## Overview 4 | This script is designed to be used as a framework for others to modify as they see fit for their environments, not as a one-size-fits-all solution. 5 | If you would like a more in-depth and sophisticated scan pleast contact Microsoft for an AD assessment. 6 | This is intended to run as a scheduled task or as part of a SCORCH runbook and has a hardcoded value. 7 | 8 | ## Requirements 9 | - Modify the $LogFile variable to a location on the network the reports should be written to. 10 | - If emails are desired uncomment lines 21-25 and populate the variables appropriately, also uncomment line 184. 11 | 12 | ## Modifications 13 | Create new items to monitor by simply adding 2-3 lines using the same structure. 14 | -------------------------------------------------------------------------------- /RecurringADChecks.ps1: -------------------------------------------------------------------------------- 1 | #Monthly AD Health Checks 2 | #Requires -Modules grouppolicy 3 | #Requires -Modules dhcpserver 4 | #Requires -Modules activedirectory 5 | 6 | <# 7 | .SYNOPSIS 8 | Runs some simple checks against AD and generates a report that should be reviewed on a regular basis 9 | .DESCRIPTION 10 | This script is designed to be used as a framework for others to modify as they see fit for their environments, not as a one-size-fits-all solution. 11 | If you would like a more in-depth and sophisticated scan pleast contact Microsoft for an AD assessment. 12 | This is intended to run as a scheduled task or as part of a SCORCH runbook and has a hardcoded value. 13 | .NOTES 14 | Author: Paul Harrison 15 | #> 16 | 17 | $LogFile = '\\servername\share\path\ADReport.html' 18 | 19 | #Values to define if you want it to email automatically - also uncomment the last line of this file 20 | <# 21 | $Body = "Please review the attached file for AD recommendations. The attached report has shortcuts to each section that are not in the body of the email.`n$HTMLReport" 22 | $SubjectLine = "Recurring AD Report" 23 | $ToAddress = @("Alice@contoso.com","Bob@contoso.com","Carmet@contoso.com","David@contoso.com") 24 | $FromAddress = "DoNotReply@contoso.com" 25 | $SMTPServer = 'mail.contoso.com' 26 | #> 27 | 28 | Function New-HTMLReport { 29 | [CmdletBinding()] 30 | param ( 31 | [parameter(Mandatory = $true)] 32 | [ValidateNotNullOrEmpty()] 33 | [string] 34 | $Title 35 | ) 36 | "

$($title)


`n" 37 | } 38 | 39 | Function New-HTMLReportSection { 40 | [CmdletBinding()] 41 | param ( 42 | [Parameter(Mandatory = $true)] 43 | [ValidateNotNullOrEmpty()] 44 | [string] 45 | $SectionTitle, 46 | [Parameter()] 47 | [array] 48 | $SectionContents = $Null 49 | ) 50 | $emptyNote = [PSCustomObject]@{message = '[empty]' } 51 | $MyOut = @() 52 | $MyOut += "

$SectionTitle

`n" 53 | If ($SectionContents -eq '' -or $SectionContents -eq $Null) { 54 | $MyOut += "
$($emptyNote | Select-Object message | ConvertTo-HTML -Fragment)`n" 55 | } 56 | Else { 57 | $MyOut += "
$($SectionContents | ConvertTo-Html -Fragment)`n" 58 | } 59 | $MyOut 60 | } 61 | 62 | #Find domains to run this report against 63 | $Trusts = Get-ADTrust -Filter * | Where-Object { $_.Direction -in @([Microsoft.ActiveDirectory.Management.ADTrustDirection]::BiDirectional, [Microsoft.ActiveDirectory.Management.ADTrustDirection]::Inbound) } 64 | $FoundForestToRunAgainst = $Trusts | ForEach-Object { try { Get-ADForest $_.name; if ($?) { $_.Name } }catch {} } 65 | $AdditionalDomainsToRunAgainst = ForEach ($Forest in $FoundForestToRunAgainst) { 66 | ForEach ($domain in $Forest.Domains) { 67 | try { $Null = Get-ADDomain $domain; if ($?) { domain } }catch {} 68 | } 69 | } 70 | $TargetDomains = [array]$AdditionalDomainsToRunAgainst + (Get-ADDomain).DNSRoot | Select-Object -Unique 71 | 72 | [string]$HTMLReport = '' 73 | 74 | ForEach ($domain in $TargetDomains) { 75 | 76 | $HTMLReport += New-HTMLReport -Title "AD Report for $domain on $((Get-Date).ToShortDateString()) at $((Get-Date).ToLongTimeString())" 77 | 78 | $DomainDN = (Get-ADDomain -Server $domain).distinguishedName 79 | 80 | #get objects in the computers container 81 | $ObjectInComputerContainer = Get-ADObject -SearchBase $('CN=Computers,' + $DomainDN) -Filter { ObjectClass -ne 'container' } -Server $domain 82 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Objects in the Computers container" -SectionContents $($ObjectInComputerContainer | Select-Object name, objectclass, objectguid) 83 | 84 | #Get objects in the users container 85 | $NormalObjectsInUsers = @('DnsAdmins', 'DnsUpdateProxy') #This can be populated with exceptions like built in objects 86 | $ExtraObjectsInUsersContainer = Get-ADObject -SearchBase $('CN=Users,' + $DomainDN) -Filter { ObjectClass -ne 'container' } -Properties isCriticalSystemObject, samaccountname -Server $domain | 87 | Where-Object { $_.SamAccountName -notin $NormalObjectsInUsers -and $_.isCriticalSystemObject -ne $true -and $_.SamAccountName -ne $Null -and -not($_.samaccountname.startswith('AAD_')) -and -not($_.samaccountname.startswith('MOL_')) -and ($_.samaccountname -ne 'SUPPORT_388945a0') -and -not($_.samaccountname -like 'CAS_*}*') } 88 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Extra objects in the Users container" -SectionContents $($ExtraObjectsInUsersContainer | Select-Object name, objectclass) 89 | 90 | #Find all empty groups 91 | $emptyGroups = Get-ADGroup -Filter {isCriticalSystemObject -ne $true} -Properties members,isCriticalSystemObject -Server $domain | Where-Object { $($_.members.count) -eq 0 } 92 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Empty groups" -SectionContents $($emptyGroups | Select-Object samaccountname, distinguishedName) 93 | 94 | #Find DCs not protected from accidental deletion 95 | $DCsUnprotectedFromAccidentalDeletion = ((get-addomaincontroller -filter * -Server $domain).computerObjectDN | Get-ADObject -Server $domain -properties ProtectedFromAccidentalDeletion | Where-Object { -not $_.ProtectedFromAccidentalDeletion }) 96 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - DCs not protected from accidental deletion" -SectionContents $($DCsUnprotectedFromAccidentalDeletion | Select-Object name) 97 | 98 | #OUs not protected from accidental deletion 99 | $OUsUnprotectedFromAccidentalDeletion = ((Get-ADOrganizationalUnit -Filter * -Server $domain).DistinguishedName | Get-ADObject -properties ProtectedFromAccidentalDeletion -Server $domain | Where-Object { -not $_.ProtectedFromAccidentalDeletion }) 100 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - OUs not protected from accidental deletion" -SectionContents $($OUsUnprotectedFromAccidentalDeletion | Select-Object distinguishedName) 101 | 102 | #Find computers with the DHCP server naming convention that are not authorized - comment out this section if you don't want to run against DHCP servers - don't forget to remove the #Requires for DHCPserrver at the top too 103 | If ($domain -eq (Get-ADDomain).dnsroot) { 104 | #only works against the domain this machine is on 105 | $AuthorizedDHCPServers = get-dhcpServerInDC 106 | $UnauthorizedDHCPServers = (get-adcomputer -filter { samaccountname -like '*pattern*' }).HostName | Where-Object { $_.dnsHostName -notin $AuthorizedDHCPServers.dnsName } 107 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Servers with the name of a DHCP server that are not authorized DHCP servers" -SectionContents $($UnauthorizedDHCPServers | Select-Object name) 108 | } 109 | 110 | #Disabled computer objects 111 | $DisabledComputerObjects = Get-ADComputer -filter * -Properties whencreated, LastLogonDate -Server $domain | Where-Object { -not $_.Enabled } 112 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Disabled computer objects" -SectionContents $($DisabledComputerObjects | Select-Object name, distinguishedname, whencreated, lastlogondate) 113 | 114 | #disabled users 115 | $DisabledUserObjects = Get-ADUser -filter { Name -notlike 'SystemMailbox*' } -properties whencreated, lastlogondate, memberof -Server $domain | Where-Object { -not $_.enabled } 116 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Disabled user objects" -SectionContents $($DisabledUserObjects | Select-Object samaccountname, enabled, lastlogondate) 117 | 118 | #Tombstone info 119 | $TombstoneLifetime = (Get-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=services,$((Get-ADRootDSE -Server $domain).configurationNamingContext)" -properties tombstoneLifetime -Server $domain).tombstoneLifetime 120 | $TombstoneDate = (get-Date).AddDays(-1 * $TombstoneLifetime) 121 | 122 | #Computer objects with lastlogondate older than tombstone 123 | $oldComputers = Get-ADComputer -filter { LastLogonDate -lt $TombstoneDate } -properties LastLogonDate -Server $domain 124 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Computers with lastLogonDate older than the tombstone - $TombstoneLifetime days ago - $($TombstoneDate.ToShortDateString())" -SectionContents $($oldComputers | Select-Object name, lastlogondate, distinguishedname) 125 | 126 | #Disabled user objects with group membership 127 | $DisabledUsersWithGroupMembership = $DisabledUserObjects | Where-Object { $_.memberof.count -eq 0 } 128 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Disabled users with group membership" -SectionContents $($DisabledUsersWithGroupMembership | Select-Object name, givenname, surname, enabled, whencreated, lastlogondate, distinguishedname) 129 | 130 | #summary of computers by OS 131 | $ComputersByOS = get-adcomputer -filter * -properties operatingsystem -Server $domain | Group-Object operatingsystem | Sort-Object count -descending | Select-Object count, name 132 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Summary of computers by OS" -SectionContents $ComputersByOS 133 | 134 | #users with admincount = 1 135 | $UsersWithAdminCount1 = Get-ADUser -filter { admincount -eq 1 } -Server $domain 136 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Users with admincount = 1" -SectionContents $($UsersWithAdminCount1 | Select-Object name, givenname, surname) 137 | 138 | #Find unlinked GPOs 139 | $UnlinkedGPOs = Get-GPO -All -Domain $domain | Where-Object { $_ | Get-GPOReport -ReportType XML -Domain $domain | Select-String -NotMatch '' } 140 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Unlinked GPOs" -SectionContents $($UnlinkedGPOs | Select-Object displayname, creationtime, modificationtime, @{N='WmiFilter';E={$_.wmifilter.Name}}) 141 | 142 | #Users without a password required 143 | $UsersWithoutAPasswordRequired = Get-ADUser -Filter { PasswordNotRequired -eq $true } -Properties passwordNotRequired -Server $domain 144 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Users without a password required (passwordNotRequired = true)" -SectionContents $($UsersWithoutAPasswordRequired | Select-Object samaccountname) 145 | 146 | #User objects with PasswordNeverExpires = true that are not in a service accounts OU 147 | $PwdNeverExpires = Get-ADUser -Filter { PasswordNeverExpires -eq $true -and samaccountname -notlike 'HealthMailbox*' } -Properties PasswordNeverExpires -Server $domain | Where-Object { $_.distinguishedName -notlike "*OU=Service Accounts*,$DomainDN" } 148 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Users with PasswordNeverExpires = true that are not in a service accounts OU" -SectionContents $($PwdNeverExpires | Select-Object samaccountname, enabled, name, passwordneverexpires, distinguishedname) 149 | 150 | #Users with a passwordLastSet over 1 year old 151 | $old = (Get-Date).AddDays(-365) 152 | $OldUserObjects = Get-ADUser -Filter { passwordLastSet -lt $old -and samaccountname -notlike 'HealthMailbox*' -and Enabled -eq $true } -Properties PasswordLastSet -Server $domain 153 | $HTMLReport += New-HTMLReportSection -SectionTitle "$domain - Enabled User Objects with a password over 1 year old or never set" -SectionContents $($OldUserObjects | Select-Object samaccountname, enabled, name, passwordneverexpires, distinguishedname) 154 | 155 | } #finished collecting data 156 | 157 | #generate a table of contents with links to each item 158 | $HTMLObject = New-Object -ComObject 'HTMLFile' 159 | $HTMLObject.write([System.Text.Encoding]::Unicode.GetBytes($HTMLReport)) 160 | $AllIDLines = $HTMLObject.all.tags('H2') 161 | $AllIDs = forEach ($line in $AllIDLines) { 162 | $start = $line.outerHTML.IndexOf('id=') + 3 163 | $end = $line.outerHTML.IndexOf('>', $start) - 1 164 | $end2 = $line.outerHTML.IndexOf('<', $($end + 2)) 165 | [pscustomobject]@{ 166 | ID = $line.outerHTML[$start..$end] -join ('') 167 | Title = $line.outerHTML[$($end + 2)..$($end2 - 1)] -join ('') 168 | } 169 | } 170 | $TableOfContents = forEach ($ID in $AllIDs) { 171 | '' + $($ID.Title) + '
' 172 | } 173 | 174 | $HTMLReportWithTOC = $TableOfContents + $HTMLReport 175 | 176 | #output 177 | If (Test-Path $LogFile) { 178 | Remove-Item $LogFile -Force 179 | } 180 | $HTMLReportWithTOC | Out-File $LogFile -Force 181 | 182 | 183 | 184 | #Send-MailMessage -Attachments $LogFile -BodyAsHtml $Body -Subject $SubjectLine -To $ToAddress -From $FromAddress -SmtpServer $SMTPServer 185 | --------------------------------------------------------------------------------