├── 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 |
--------------------------------------------------------------------------------