├── Add-LogEntry └── add-LogEntry.ps1 ├── Get-DotNetProxies └── Get-DotNetProxies.ps1 ├── Get-NetworkLogons └── Get-NetworkLogons.ps1 ├── Get-Office365Forwarders ├── Get-Office365Forwarders.ps1 └── README.md ├── LICENSE ├── README.md └── Update-AllPSModules └── README.md /Add-LogEntry/add-LogEntry.ps1: -------------------------------------------------------------------------------- 1 | function add-LogEntry 2 | { 3 | <# 4 | .SYNOPSIS 5 | Add-LogEntry sends output to the host and a log file. 6 | 7 | .DESCRIPTION 8 | Add-LogEntry sends output to the host and a log file. 9 | Output sent to the log file includes time entries. Those are not generally needed on the host and may take up too much room. 10 | You can pass on directions to indent output or indicate that it was a Success, Warning, or Failure. Everything else is marked as Info. 11 | Info data is sent to the screen in the default white font, but everything else uses appropriate colours 12 | 13 | .PARAMETER Logfile 14 | The full path to where you want to save the log. There's no need to specify this every time if you enter this in your main script: 15 | $PSDefaultParameterValues = @{'add-LogEntry:LogFile' = 'C:\scripts\sample output.log'} 16 | 17 | .PARAMETER Output 18 | The data you wish to send to the host and logfile. 19 | 20 | .PARAMETER ClearLog 21 | Overwrites the current logfile with this entry only. Generally would be used at the start of the script. 22 | 23 | .PARAMETER BlankLine 24 | Still outputs the timespamp, but does not include any data. Useful for separating sections of the log. 25 | 26 | .PARAMETER IndentSize 27 | The number of spaces that text is indented by. The default is 4. 28 | 29 | .PARAMETER Indent 30 | Data that you want to indent by IndentSize x Indent spaces. Can help readability in some situations. 31 | 32 | .PARAMETER IsError 33 | Marks the entry as [Error] in the logfile and colours the data in RED in the host. 34 | 35 | .PARAMETER IsPrompt 36 | Marks the entry as [Prompt] in the logfile and colours the data in YELLOW in the host. 37 | 38 | .PARAMETER IsSuccess 39 | Marks the entry as [Success] in the logfile and colours the data in GREEN in the host. 40 | 41 | .PARAMETER IsWarning 42 | Marks the entry as [Warning] in the logfile and colours the data in YELLOW in the host. 43 | 44 | .PARAMETER IsDebug 45 | Marks the entry as [Debug] in the logfile and colours the data in CYAN in the host. 46 | 47 | .EXAMPLE 48 | add-LogEntry -Output "Starting script" 49 | Host: 50 | Starting script 51 | 52 | Logfile: 53 | 2021-05-03 10:01:43 INFO Starting script 54 | 55 | .EXAMPLE 56 | add-LogEntry -Output "Computer '$computer' is uncontactable" -IsWarning 57 | Host: 58 | Computer 'PC01' is uncontactable 59 | 60 | Logfile: 61 | 2021-05-03 14:03:39 [WARNING] Computer 'PC01' is uncontactable 62 | 63 | 64 | .EXAMPLE 65 | add-LogEntry -Output "Querying computer '$computer'" 66 | add-LogEntry -Output "Processor: $CPU" -indent 1 67 | add-LogEntry -Output "Memory: $RAM" -indent 1 68 | 69 | Host: 70 | Querying computer 'PC01' 71 | Processor: Core i5-11600K 72 | Memory: 16 GB 73 | 74 | Logfile: 75 | 2021-05-03 14:07:58 INFO Querying computer 'PC01' 76 | 2021-05-03 14:08:00 INFO Processor: Core i5-11600K 77 | 2021-05-03 14:08:01 INFO Memory: 16 GB 78 | 79 | .EXAMPLE 80 | add-LogEntry -Output 'Checking if all required Windows Features are installed:' 81 | foreach($RequiredWindowsFeature in $RequiredWindowsFeatures) 82 | { 83 | add-LogEntry -Output $RequiredWindowsFeature -Indent 1 84 | If (-not(Get-WindowsFeature -Name $RequiredWindowsFeature).Installed) 85 | { 86 | add-LogEntry -Output 'Feature is missing, will attempt to install now.' -indent 2 87 | try 88 | { 89 | $null = Add-WindowsFeature -Name $RequiredWindowsFeature -ErrorAction Stop 90 | add-LogEntry -Output 'Success' -IsSuccess -indent 2 91 | } 92 | catch 93 | { 94 | add-LogEntry -Output "Failed to install '$RequiredWindowsFeature'" -indent 2 -IsError 95 | } 96 | } 97 | } 98 | 99 | Host: 100 | Checking if all required Windows Features are installed: 101 | RSAT-ADDS-Tools 102 | Feature is missing, will attempt to install now. 103 | Success 104 | 105 | Logfile: 106 | 2021-06-08 15:45:56 INFO Checking if all required Windows Features are installed: 107 | 2021-06-08 15:45:57 INFO RSAT-ADDS-Tools 108 | 2021-06-08 15:45:57 INFO Feature is missing, will attempt to install now. 109 | 2021-06-08 15:46:39 [SUCCESS] Success 110 | 111 | .NOTES 112 | Filename: add-LogEntry.ps1 113 | Contributors: Kieran Walsh 114 | Created: 2018-01-12 115 | Last Updated: 2024-04-18 116 | Version: 1.13.00 117 | #> 118 | [CmdletBinding()] 119 | Param 120 | ( 121 | [Parameter()] 122 | [Alias('Message')] 123 | [string]$Output = $(if( 124 | (-not($BlankLine) -and (-not($Clearlog))) -and 125 | ($null -eq $Output) 126 | ) 127 | { 128 | $Output = Read-Host 'Please specify the output you wish to log' 129 | } 130 | Elseif($BlankLine) 131 | { 132 | $Output = '' 133 | } 134 | ), 135 | [int]$IndentSize = 4, 136 | [string]$LogFile = 'C:\Windows\Temp\file.log', 137 | [switch]$BlankLine, 138 | [switch]$ClearLog, 139 | [int]$Indent, 140 | [switch]$IsDebug, 141 | [switch]$IsError, 142 | [switch]$IsPrompt, 143 | [switch]$IsSuccess, 144 | [switch]$IsWarning 145 | ) 146 | $ForegroundColor = 'White' 147 | if($Indent) 148 | { 149 | $Space = ($IndentSize * $Indent) + 1 150 | } 151 | Else 152 | { 153 | $Space = 1 154 | } 155 | $Type = 'INFO' 156 | if($IsDebug) 157 | { 158 | $Type = '[DEBUG]' 159 | $ForegroundColor = 'Cyan' 160 | } 161 | if($IsError) 162 | { 163 | $Type = '[ERROR]' 164 | $ForegroundColor = 'Red' 165 | } 166 | if($IsPrompt) 167 | { 168 | $Type = '[PROMPT]' 169 | $ForegroundColor = 'Yellow' 170 | } 171 | if($IsSuccess) 172 | { 173 | $Type = '[SUCCESS]' 174 | $ForegroundColor = 'Green' 175 | } 176 | if($IsWarning) 177 | { 178 | $Type = '[WARNING]' 179 | $ForegroundColor = 'Yellow' 180 | } 181 | 182 | Write-Host -Object $Output -ForegroundColor $ForegroundColor 183 | if($BlankLine) 184 | { 185 | '{0,-22}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Out-File -FilePath $LogFile -Encoding 'utf8' -Append 186 | } 187 | Elseif($ClearLog) 188 | { 189 | Clear-Content -Path $LogFile 190 | } 191 | Else 192 | { 193 | "{0,-22}{1,-11}{2,-$Space}{3}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Type, ' ', $Output | Out-File -FilePath $LogFile -Encoding 'utf8' -Append 194 | } 195 | } -------------------------------------------------------------------------------- /Get-DotNetProxies/Get-DotNetProxies.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Filename: Get-DotNetProxies.ps1 3 | Contributors: Kieran Walsh 4 | Created: 2022-11-08 5 | Last Updated: 2022-11-08 6 | Version: 1.00.00 7 | #> 8 | 9 | $DotNetInstallationPaths = (Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | 10 | Get-ItemProperty -Name 'InstallPath' -ErrorAction 'SilentlyContinue' | 11 | Where-Object{$_.InstallPath} | 12 | Sort-Object -Property 'InstallPath' -Unique).InstallPath 13 | 14 | if($DotNetInstallationPaths) 15 | { 16 | $ConfigFiles = ( 17 | Get-ChildItem -File -Filter '*.config' -Force -Path $DotNetInstallationPaths -Recurse -ErrorAction 'SilentlyContinue' | 18 | Where-Object {$_.Name -match 'Machine.config|Web.config'} 19 | ).FullName 20 | if($ConfigFiles) 21 | { 22 | $Found = $false 23 | "Checking configuration in $(($ConfigFiles | Measure-Object).Count) matching files." 24 | foreach($ConfigFile in $ConfigFiles) 25 | { 26 | [xml]$XmlDocument = Get-Content -Path $ConfigFile 27 | $Proxy = $XmlDocument.configuration.'system.net'.defaultProxy.proxy.proxyaddress 28 | if($proxy) 29 | { 30 | 'Proxy settings configured in:' 31 | "`tPath: '$ConfigFile'" 32 | "`tProxy: '$Proxy'" 33 | $Found = $true 34 | } 35 | } 36 | if(-not($Found)) 37 | { 38 | 'No proxy settings found.' 39 | } 40 | } 41 | Else 42 | { 43 | 'No matching config files found.' 44 | } 45 | } 46 | Else 47 | { 48 | 'No .NET installations found in the registry.' 49 | } -------------------------------------------------------------------------------- /Get-NetworkLogons/Get-NetworkLogons.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Filename: Get-NetworkLogons.ps1 3 | Contributors: Kieran Walsh 4 | Created: 2020-11-15 5 | Last Updated: 2021-11-01 6 | Version: 2.07.00 7 | #> 8 | 9 | [CmdletBinding()] 10 | Param( 11 | [Parameter(Position = 1)] 12 | [Alias('Name')] 13 | [string]$ComputerName = '', 14 | [ValidateSet('Workstation', 'Server')] 15 | [string]$Type = 'Workstation', 16 | [switch]$HideOff, 17 | [switch]$AvailableOnly, 18 | [switch]$RemoteFirstAvailable, 19 | [Switch]$Restart 20 | ) 21 | 22 | function Start-RemoteServices 23 | { 24 | $Services = 'WinRM', 'RemoteRegistry', 'Winmgmt' 25 | Foreach($Service in $Services) 26 | { 27 | $Service 28 | try 29 | { 30 | Get-Service -ComputerName $Computer -Name $Service -ErrorAction Stop | Where-Object -FilterScript { 31 | $_.Status -ne 'Running' 32 | } 33 | Set-Service -Name $Service -ComputerName $Computer -StartupType Automatic -Status Running 34 | } 35 | catch 36 | { 37 | $False 38 | Return 39 | } 40 | $true 41 | } 42 | } 43 | 44 | If($PSVersionTable.PSVersion.Major -lt 3) 45 | { 46 | 'You must be running PowerShell version 3 or higher to run this script.' 47 | "This machine is on PowerShell $($PSVersionTable.PSVersion.Major)." 48 | break 49 | } 50 | 51 | $OnCount = 0 52 | $FreeCount = 0 53 | $Finish = $False 54 | $Date = (Get-Date).AddDays(-20) 55 | $CommandPath = $MyInvocation.MyCommand.Path 56 | $ScriptVersion = (Get-Content -Path $CommandPath | Select-String 'Version:')[0] -replace 'Version:', '' -replace ' ', '' 57 | "Starting script version '$ScriptVersion'." 58 | # Make sure AD DS Snap-Ins, Command-Line Tools and Active Directory module for Windows PowerShell are installed 59 | $prerequisites = ('RSAT-ADDS-Tools', 'RSAT-AD-PowerShell', 'GPMC') 60 | Import-Module -Name servermanager 61 | foreach($prerequisite in $prerequisites) 62 | { 63 | If (-not(Get-WindowsFeature -Name $prerequisite).Installed) 64 | { 65 | Install-WindowsFeature -Name $prerequisite 66 | } 67 | } 68 | 69 | # Find requested AD computers. 70 | $Computers = Get-ADComputer -Properties OperatingSystem, LastLogonTimeStamp -Filter { 71 | (OperatingSystem -like '*Windows*') -and (LastLogonTimeStamp -gt $Date) 72 | } 73 | If ($Type -eq 'Workstation') 74 | { 75 | $Computers = ($Computers | Where-Object -FilterScript { 76 | ($_.OperatingSystem -notmatch 'Server') -and ($_.name -match $ComputerName) 77 | }).Name | Sort-Object 78 | } 79 | ElseIf ($Type -eq 'Server') 80 | { 81 | $Computers = ($Computers | Where-Object -FilterScript { 82 | ($_.OperatingSystem -match 'Server') -and ($_.name -match $ComputerName) 83 | }).Name | Sort-Object 84 | } 85 | Else 86 | { 87 | $Computers = ($Computers | Where-Object -FilterScript { 88 | $_.name -match $ComputerName 89 | }).Name | Sort-Object 90 | } 91 | 92 | "There are $($Computers.count) computers to check." 93 | $MaxLength = ($Computers | 94 | Sort-Object -Property length -Descending | 95 | Select-Object -Property length -First 1).length + 2 96 | 97 | if($AvailableOnly) 98 | { 99 | $HideOff = $true 100 | } 101 | 102 | Foreach ($Computer in $Computers) 103 | { 104 | if($Finish) 105 | { 106 | break 107 | } 108 | 109 | If(Test-Connection -ComputerName $Computer -Count 1 -Quiet) 110 | { 111 | $OnCount++ 112 | } 113 | Else 114 | { 115 | if(-not ($HideOff)) 116 | { 117 | Write-Host -ForegroundColor 'Red' -Object $("{0,-$MaxLength}{1}" -f $Computer, 'Uncontactable') 118 | } 119 | continue 120 | } 121 | 122 | $Sessions = (C:\Windows\System32\quser.exe /server:$Computer 2>&1) 123 | 124 | If(-not($Sessions)) 125 | { 126 | $FreeCount++ 127 | $User = 'Unused' 128 | $Fontcol = 'Yellow' 129 | Write-Host -ForegroundColor $Fontcol -Object $("{0,-$MaxLength}{1}" -f $Computer, $User) 130 | if($RemoteFirstAvailable) 131 | { 132 | $Finish = $true 133 | mstsc.exe /f /v $Computer 134 | } 135 | if($Restart) 136 | { 137 | Restart-Computer -ComputerName $Computer 138 | } 139 | continue 140 | } 141 | if($AvailableOnly) 142 | { 143 | continue 144 | } 145 | 146 | if($Sessions -match 'Access is denied.') 147 | { 148 | $User = 'Cannot query users - access denied.' 149 | $Fontcol = 'Yellow' 150 | Write-Host -ForegroundColor $Fontcol -Object $("{0,-$MaxLength}{1}" -f $Computer, $User) 151 | continue 152 | } 153 | if($Sessions -match '0x000006BA') 154 | { 155 | $RemoteIP = (Test-NetConnection -ComputerName $Computer).RemoteAddress.IPAddressToString 156 | $RemoteComputer = (Resolve-DnsName -Name $RemoteIP).NameHost 157 | if(-not($Computer -match $RemoteComputer)) 158 | { 159 | Write-Host "DNS duplicate issues. The IP of computer '$Computer' resolves to the device '$(($RemoteComputer -split '\.')[0])'. Check DNS scavenging." -ForegroundColor Red 160 | continue 161 | } 162 | Else 163 | { 164 | Invoke-Command -ComputerName $Computer -Command { Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRPC -Value 0x1 -Force } 165 | $Sessions = (C:\Windows\System32\quser.exe /server:$Computer 2>&1) 166 | if($Sessions -notmatch 'SESSIONNAME') 167 | { 168 | $User = 'Cannot query users.' 169 | $Fontcol = 'Yellow' 170 | Write-Host -ForegroundColor $Fontcol -Object $("{0,-$MaxLength}{1}" -f $Computer, $User) 171 | continue 172 | } 173 | } 174 | } 175 | 176 | $Sessions = $Sessions | Select-Object -Skip 1 177 | $SessionUsers = foreach($Session in $Sessions) 178 | { 179 | If($Session.State -eq 'Active') 180 | { 181 | $Days = 0 182 | $Hours = 0 183 | $Mins = 0 184 | } 185 | Else 186 | { 187 | $IdleTime = $Session.Substring(54, 11).trim() 188 | If($IdleTime -match '\+') 189 | { 190 | $Days = ($IdleTime -split '\+')[0] 191 | } 192 | Else 193 | { 194 | $Days = 0 195 | } 196 | 197 | If($IdleTime -match ':') 198 | { 199 | $Hours = ($IdleTime -split { 200 | $_ -eq '+' -or $_ -eq ':' 201 | })[1] 202 | } 203 | Else 204 | { 205 | $Hours = 0 206 | } 207 | If($IdleTime -match 'none') 208 | { 209 | $Mins = 0 210 | } 211 | Else 212 | { 213 | $Mins = ($IdleTime -split { 214 | $_ -eq '+' -or $_ -eq ':' 215 | })[-1] 216 | } 217 | } 218 | 219 | [PSCustomObject]@{ 220 | 'Computer' = $Computer 221 | 'Username' = $Session.Substring(1, 22).trim() 222 | 'SessionName' = $Session.Substring(23, 19).trim() 223 | 'Id' = $Session.Substring(42, 4).trim() 224 | 'State' = $Session.Substring(46, 8).trim() 225 | 'IdleDays' = $Days 226 | 'IdleHours' = $Hours 227 | 'IdleMins' = $Mins 228 | 'LogonTime' = (Get-Date -Date $Session.Substring(65, ($Session.length - 65)).trim() -Format 'yyyy-MM-dd HH:mm') 229 | } 230 | } 231 | $SessionUsers = $SessionUsers | Sort-Object -Property State 232 | 233 | foreach($SessionUser in $SessionUsers) 234 | { 235 | if($SessionUser.State -eq 'Active') 236 | { 237 | if(($SessionUser.IdleDays -eq 0) -and ($SessionUser.IdleHours -eq 0) -and ($SessionUser.IdleMins -eq 0)) 238 | { 239 | $Fontcol = 'Green' 240 | Write-Host -ForegroundColor $Fontcol -Object $("{0,-$MaxLength}{1,-22}{2,-20}Logon: {3,-18}" -f $Computer, 'Connected - Active', $SessionUser.Username, $SessionUser.LogonTime) 241 | continue 242 | } 243 | Else 244 | { 245 | $Fontcol = 'Green' 246 | Write-Host -ForegroundColor $Fontcol -Object $("{0,-$MaxLength}{1,-22}{2,-20}Logon: {3,-18}Idle time - Days: {4}, Hours: {5}, Minutes: {6}" -f $Computer, 'Connected - Idle', $SessionUser.Username, $SessionUser.LogonTime, $SessionUser.IdleDays, $SessionUser.IdleHours, $SessionUser.IdleMins) 247 | continue 248 | } 249 | } 250 | $Fontcol = 'White' 251 | Write-Host -ForegroundColor $Fontcol -Object $("{0,-$MaxLength}{1,-22}{2,-20}Logon: {3,-18}Idle time - Days: {4}, Hours: {5}, Minutes: {6}" -f ' ', 'Disconnected', $SessionUser.Username, $SessionUser.LogonTime, $SessionUser.IdleDays, $SessionUser.IdleHours, $SessionUser.IdleMins) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Get-Office365Forwarders/Get-Office365Forwarders.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Lists all Office 365 mailboxes which forward or redirect email. 4 | .DESCRIPTION 5 | Lists all Office 365 mailboxes which forward or redirect email. By default it only lists email that is sent outside the tenancy, but you can also decide to list internal forwards. 6 | You can also decide to list only Outlook Rules, or Office 365 mailbox configuration forwards. 7 | All data is saved to a the CSV file listed in the cmdlet variable. If the parameter is not entered, the CSV will be saved to the Windows Temp folder by default. 8 | .PARAMETER CSVFile 9 | The full path to where you want to save the CSV. 10 | .Parameter IncludeInternal 11 | If you want to include internal forwards or redirects use this switch. 12 | .PARAMETER NoOutlook 13 | Use this if you don't care about end user Outlook rules and only wish to list Office 365 rules. 14 | .PARAMETER NoOffice365 15 | This parameter will list all Outlook rules, but not Office 365 configurations. 16 | .PARAMETER EmailAddresses 17 | If you don't want to query all mailboxes in Office 365, use this parameter to specify a list of email addresses. 18 | .EXAMPLE 19 | Get-Office365Forwarders 20 | This will query all mailboxes that send email externally and save the results to the Windows Temp folder. 21 | .EXAMPLE 22 | Get-Office365Forwarders -CSVFile C:\Temp\ExternallyForwardingMailboxes.csv 23 | This script will query all mailboxes and save the results to the file C:\Temp\ExternallyForwardingMailboxes.csv 24 | .EXAMPLE 25 | Get-Office365Forwarders -NoOutlook 26 | Will list Office 365 forwarding settings, but not Outlook rules. 27 | .Example 28 | Get-Office365Forwarders -NoOffice365 29 | Will list rules in Outlook but not Office 365. 30 | .Example 31 | Get-Office365Forwarders -EmailAddresses 'Anna@mysite.com', 'Zahra@mysite.com' 32 | Will query only the mailboxes with the specified email addresses. 33 | .Notes 34 | Filename: Get-Office365Forwarders.ps1 35 | Contributors: Kieran Walsh 36 | Created: 2021-11-26 37 | Last Updated: 2021-12-08 38 | Version: 0.04.06 39 | #> 40 | 41 | [CmdletBinding()] 42 | Param( 43 | [Parameter()] 44 | [string]$CSVFile = "$env:windir\temp\Office 365 forwarding accounts.csv", 45 | [switch]$NoOutlook, 46 | [switch]$NoOffice365, 47 | [switch]$IncludeInternal, 48 | [string[]]$EmailAddresses 49 | ) 50 | 51 | try 52 | { 53 | $OS = (Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop).caption 54 | } 55 | catch 56 | { 57 | Write-Warning 'Unable to query operating system name. This script has been tested for Windows only so cannot continue.' 58 | break 59 | } 60 | 61 | if($OS -notmatch 'Windows') 62 | { 63 | Write-Warning 'This script has been tested for Windows only so cannot continue.' 64 | break 65 | } 66 | 67 | $RequiredModules = @( 68 | 'ExchangeOnlineManagement' 69 | 'MSOnline' 70 | ) 71 | $MissingModules = @() 72 | foreach($RequiredModule in $RequiredModules) 73 | { 74 | if(-not(Get-Module -ListAvailable -Name $RequiredModule)) 75 | { 76 | $MissingModules += $RequiredModule 77 | } 78 | } 79 | 80 | if($MissingModules) 81 | { 82 | Write-Host 'The following modules are required but missing:' 83 | $MissingModules | ForEach-Object { 84 | "`t'$_'" 85 | } 86 | Write-Host 'Would you like me to install those now? (y/n)' 87 | $Answer = Read-Host 88 | if($Answer.ToUpper() -eq 'Y') 89 | { 90 | foreach($MissingModule in $MissingModules) 91 | { 92 | Write-Host "Installing '$MissingModule'." 93 | try 94 | { 95 | Install-Module -Name $MissingModule -ErrorAction Stop 96 | } 97 | catch 98 | { 99 | Write-Host "Failed to install $MissingModule" 100 | break 101 | } 102 | } 103 | } 104 | Else 105 | { 106 | Write-Warning 'Unable to continue.' 107 | break 108 | } 109 | } 110 | 111 | $RequiredModules | ForEach-Object { 112 | Import-Module $_ -Force 113 | } 114 | 115 | try 116 | { 117 | $AcceptedDomains = (Get-AcceptedDomain -ErrorAction Stop).DomainName 118 | } 119 | catch 120 | { 121 | Write-Host -Object 'You are not connected to Office 365. Would you like to connect now? (Y/N)' 122 | $Answer = Read-Host 123 | if($Answer.ToUpper() -eq 'Y') 124 | { 125 | Connect-MsolService -ErrorAction Stop 126 | Connect-ExchangeOnline -ErrorAction Stop 127 | $AcceptedDomains = (Get-AcceptedDomain).DomainName 128 | } 129 | Else 130 | { 131 | Write-Warning 'Unble to connect to Office 365.' 132 | break 133 | } 134 | 135 | } 136 | $AcceptedDomains = (Get-AcceptedDomain).DomainName 137 | 138 | if(-not($AcceptedDomains)) 139 | { 140 | 'You are not connected to Office 365. Connect and try again.' 141 | break 142 | } 143 | 144 | $SiteName = $((Get-MsolCompanyInformation).DisplayName) 145 | Write-Host -Object "You are connected to the '$SiteName' tenancy. Is this the one you wish to query? (Y/N)" 146 | $Answer = Read-Host 147 | If($Answer.ToUpper() -ne 'Y') 148 | { 149 | $null = [Microsoft.Online.Administration.Automation.ConnectMsolService]::ClearUserSessionState() 150 | $null = Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue 151 | Write-Host 'I have closed the existing connections. Please re-run the script to connect to the correct Office 365 tenancy.' 152 | Break 153 | } 154 | 155 | Write-Host "The following domains are in the 'Accepted Domains' list so anything else is considered external:" 156 | $AcceptedDomains | ForEach-Object {"`t$_"} 157 | ' ' 158 | 159 | $Mailboxes = @() 160 | $StartTime = Get-Date 161 | if($EmailAddresses) 162 | { 163 | foreach($EmailAddress in $EmailAddresses) 164 | { 165 | if(-not($EmailAddress -as [System.Net.Mail.MailAddress])) 166 | { 167 | Write-Warning -Message "The inputted value '$EmailAddress' is not a valid email address so will be skipped." 168 | continue 169 | } 170 | try 171 | { 172 | $Mailboxes += Get-Mailbox -Identity $EmailAddress -ErrorAction stop | Select-Object AccountDisabled, DisplayName, ForwardingAddress, ForwardingSmtpAddress, IsDirSynced, IsMailboxEnabled, Name, PrimarySmtpAddress, WhenChanged, WhenMailboxCreated | Sort-Object Name 173 | } 174 | catch 175 | { 176 | Write-Warning -Message "Could not find mailbox '$EmailAddress'." 177 | } 178 | } 179 | } 180 | Else 181 | { 182 | Write-Host -Object 'Querying Office 365 mailboxes. This may take some time.' 183 | try 184 | { 185 | $Mailboxes = Get-Mailbox -ResultSize Unlimited -ErrorAction Stop | Select-Object AccountDisabled, DisplayName, ForwardingAddress, ForwardingSmtpAddress, IsDirSynced, IsMailboxEnabled, Name, PrimarySmtpAddress, WhenChanged, WhenMailboxCreated | Sort-Object Name 186 | } 187 | catch 188 | { 189 | Write-Host -Object 'Could not find any mailboxes.' 190 | break 191 | } 192 | } 193 | $Total = ($Mailboxes | Measure-Object).count 194 | if($Total -lt 1) 195 | { 196 | 'There are no mailboxes to check.' 197 | break 198 | } 199 | Write-Host -Object "There are $Total mailboxes to check." 200 | $Loop = 0 201 | 202 | $Output = foreach ($Mailbox In $Mailboxes) 203 | { 204 | $Loop++ 205 | Write-Host -Object ('{0,4} of {1,-5} {2}' -f $Loop, $Total, $Mailbox.PrimarySmtpAddress) 206 | $AccountType = switch ($Mailbox.IsDirSynced) 207 | { 208 | 'True' 209 | { 210 | 'Active Directory' 211 | } 212 | 'False' 213 | { 214 | 'Cloud' 215 | } 216 | } 217 | $AccountEnabled = switch ($Mailbox.AccountDisabled) 218 | { 219 | 'True' 220 | { 221 | 'FALSE' 222 | } 223 | 'False' 224 | { 225 | 'TRUE' 226 | } 227 | } 228 | 229 | if(-not($NoOutlook)) 230 | { 231 | $ForwardingRules = Get-InboxRule -Mailbox $Mailbox.PrimarySmtpAddress | Where-Object {($_.ForwardAsAttachmentTo -ne $null) -or ($_.ForwardTo -ne $null) -or ($_.RedirectTo -ne $null)} 232 | foreach ($Rule in $ForwardingRules) 233 | { 234 | if($Rule.ForwardTo -or $Rule.ForwardAsAttachmentTo -or $Rule.RedirectTo) 235 | { 236 | $ForwardingAddresses = @( 237 | $Rule.DeliverToMailboxAndForward 238 | $Rule.ForwardTo 239 | $Rule.ForwardAsAttachmentTo 240 | $Rule.RedirectTo 241 | ) 242 | foreach($Email in $ForwardingAddresses) 243 | { 244 | $RuleType = 'Outlook' 245 | $RecipientAddress = (($Email -split '\[')[1] -split ']')[0] -replace 'smtp:', '' 246 | $EmailDomain = ($RecipientAddress -split '@')[1] 247 | if($IncludeInternal) 248 | { 249 | if($EmailDomain) 250 | { 251 | if($Rule.DeliverToMailboxAndForward) 252 | { 253 | $String = 'forwards to' 254 | } 255 | if($Rule.ForwardTo) 256 | { 257 | $String = 'forwards to' 258 | } 259 | if($Rule.ForwardAsAttachmentTo) 260 | { 261 | $String = 'forwards as attachment to' 262 | } 263 | if($Rule.RedirectTo) 264 | { 265 | $String = 'redirects to' 266 | } 267 | Write-Host "`tMailbox '$($Mailbox.PrimarySmtpAddress)' - Outlook Rule Name '$($Rule.Name)' with a Rule Identity of '$($Rule.Identity)' $String '$RecipientAddress'." 268 | [PSCustomObject]@{ 269 | 'Account' = $Mailbox.DisplayName 270 | 'Account Type' = $AccountType 271 | 'Account Enabled' = $AccountEnabled 272 | 'Account Created' = Get-Date($Mailbox.WhenMailboxCreated) -Format 'yyyy-MM-dd' 273 | 'Account Changed' = Get-Date($Mailbox.WhenChanged) -Format 'yyyy-MM-dd' 274 | 'Mailbox Enabled' = $Mailbox.IsMailboxEnabled 275 | 'Rule Type' = $RuleType 276 | 'Rule Name' = $Rule.Name 277 | 'Rule Identity' = $Rule.Identity 278 | 'Mailbox' = $Mailbox.PrimarySmtpAddress 279 | 'Action' = $String 280 | 'Email Address' = $RecipientAddress 281 | } 282 | } 283 | } 284 | Else 285 | { 286 | if(($EmailDomain) -and ($AcceptedDomains -notcontains $EmailDomain)) 287 | { 288 | if($Rule.ForwardTo) 289 | { 290 | $String = 'forwards to' 291 | } 292 | if($Rule.ForwardAsAttachmentTo) 293 | { 294 | $String = 'forwards as attachment to' 295 | } 296 | if($Rule.RedirectTo) 297 | { 298 | $String = 'redirects to' 299 | } 300 | Write-Host "`tMailbox '$($Mailbox.PrimarySmtpAddress)' - Outlook Rule Name '$($Rule.Name)' with a Rule Identity of '$($Rule.Identity)' $String '$RecipientAddress'." 301 | [PSCustomObject]@{ 302 | 'Account' = $Mailbox.DisplayName 303 | 'Account Type' = $AccountType 304 | 'Account Enabled' = $AccountEnabled 305 | 'Account Created' = Get-Date($Mailbox.WhenMailboxCreated) -Format 'yyyy-MM-dd' 306 | 'Account Changed' = Get-Date($Mailbox.WhenChanged) -Format 'yyyy-MM-dd' 307 | 'Mailbox Enabled' = $Mailbox.IsMailboxEnabled 308 | 'Rule Type' = $RuleType 309 | 'Rule Name' = $Rule.Name 310 | 'Rule Identity' = $Rule.Identity 311 | 'Mailbox' = $Mailbox.PrimarySmtpAddress 312 | 'Action' = $String 313 | 'Email Address' = $RecipientAddress 314 | } 315 | } 316 | } 317 | } 318 | } 319 | } 320 | } 321 | if(-not($NoOffice365)) 322 | { 323 | $O365Rules = $Mailbox | Where-Object {($_.ForwardingAddress -ne $null) -or ($_.ForwardingSmtpAddress -ne $null)} 324 | foreach($O365Rule in $O365Rules) 325 | { 326 | $ForwardingAddresses = @( 327 | $O365Rule.ForwardingAddress 328 | $O365Rule.ForwardingSmtpAddress 329 | ) 330 | foreach($Email in $ForwardingAddresses) 331 | { 332 | $RuleType = 'Office 365' 333 | $RecipientAddress = $Email -replace 'smtp:', '' 334 | $EmailDomain = ($RecipientAddress -split '@')[1] 335 | if($IncludeInternal) 336 | { 337 | if($EmailDomain) 338 | { 339 | if($O365Rule.ForwardingAddress) 340 | { 341 | $String = 'forwards to' 342 | } 343 | if($O365Rule.ForwardingSmtpAddress) 344 | { 345 | $String = 'forwards to' 346 | } 347 | 348 | Write-Host "`tMailbox '$($Mailbox.PrimarySmtpAddress)' - has an Office 365 configuration which $String email to '$RecipientAddress'." 349 | [PSCustomObject]@{ 350 | 'Account' = $Mailbox.DisplayName 351 | 'Account Type' = $AccountType 352 | 'Account Enabled' = $AccountEnabled 353 | 'Account Created' = Get-Date($Mailbox.WhenMailboxCreated) -Format 'yyyy-MM-dd' 354 | 'Account Changed' = Get-Date($Mailbox.WhenChanged) -Format 'yyyy-MM-dd' 355 | 'Mailbox Enabled' = $Mailbox.IsMailboxEnabled 356 | 'Rule Type' = $RuleType 357 | 'Rule Name' = 'N/A' 358 | 'Rule Identity' = 'N/A' 359 | 'Mailbox' = $Mailbox.PrimarySmtpAddress 360 | 'Action' = $String 361 | 'Email Address' = $RecipientAddress 362 | } 363 | } 364 | } 365 | Else 366 | { 367 | if(($EmailDomain) -and ($AcceptedDomains -notcontains $EmailDomain)) 368 | { 369 | if($Rule.ForwardTo) 370 | { 371 | $String = 'forwards to' 372 | } 373 | if($Rule.ForwardAsAttachmentTo) 374 | { 375 | $String = 'forwards as attachment to' 376 | } 377 | if($Rule.RedirectTo) 378 | { 379 | $String = 'redirects to' 380 | } 381 | Write-Host "`tMailbox '$($Mailbox.PrimarySmtpAddress)' - has an Office 365 configuration which $String email to '$RecipientAddress'." 382 | [PSCustomObject]@{ 383 | 'Account' = $Mailbox.DisplayName 384 | 'Account Type' = $AccountType 385 | 'Account Enabled' = $AccountEnabled 386 | 'Account Created' = Get-Date($Mailbox.WhenMailboxCreated) -Format 'yyyy-MM-dd' 387 | 'Account Changed' = Get-Date($Mailbox.WhenChanged) -Format 'yyyy-MM-dd' 388 | 'Mailbox Enabled' = $Mailbox.IsMailboxEnabled 389 | 'Rule Type' = $RuleType 390 | 'Rule Name' = 'N/A' 391 | 'Rule Identity' = 'N/A' 392 | 'Mailbox' = $Mailbox.PrimarySmtpAddress 393 | 'Action' = $String 394 | 'Email Address' = $RecipientAddress 395 | } 396 | } 397 | } 398 | if(($EmailDomain) -and ($AcceptedDomains -notcontains $EmailDomain)) 399 | { 400 | if($O365Rule.ForwardingAddress) 401 | { 402 | $String = 'forwards to' 403 | } 404 | if($O365Rule.ForwardingSmtpAddress) 405 | { 406 | $String = 'forwards to' 407 | } 408 | 409 | Write-Host "`tMailbox '$($Mailbox.PrimarySmtpAddress)' - has an Office 365 configuration which $String email to '$RecipientAddress'." 410 | [PSCustomObject]@{ 411 | 'Account' = $Mailbox.DisplayName 412 | 'Account Type' = $AccountType 413 | 'Account Enabled' = $AccountEnabled 414 | 'Account Created' = Get-Date($Mailbox.WhenMailboxCreated) -Format 'yyyy-MM-dd' 415 | 'Account Changed' = Get-Date($Mailbox.WhenChanged) -Format 'yyyy-MM-dd' 416 | 'Mailbox Enabled' = $Mailbox.IsMailboxEnabled 417 | 'Rule Type' = $RuleType 418 | 'Rule Name' = 'N/A' 419 | 'Rule Identity' = 'N/A' 420 | 'Mailbox' = $Mailbox.PrimarySmtpAddress 421 | 'Action' = $String 422 | 'Email Address' = $RecipientAddress 423 | } 424 | } 425 | } 426 | } 427 | } 428 | } 429 | 430 | try 431 | { 432 | $Output | ConvertTo-Csv -NoTypeInformation | Out-File -FilePath $CSVFile -Encoding UTF8 -ErrorAction stop 433 | Write-Host ' ' 434 | Write-Host "All data has been saved to '$CSVFile'." 435 | } 436 | Catch 437 | { 438 | "The CSV file '$CSVFile' could not be created. Please ensure that the path exists. Cannot write to a file that is currently open, so please close it if so." 439 | Write-Host -Object "When you've corrected problems I can try to save again. Should I try to save again now? (Y/N)" 440 | $Answer = Read-Host 441 | if($Answer.ToUpper() -ne 'Y') 442 | { 443 | try 444 | { 445 | $Output | ConvertTo-Csv -NoTypeInformation | Out-File -FilePath $CSVFile -Encoding UTF8 -ErrorAction stop 446 | } 447 | catch 448 | { 449 | Write-Warning "Failed to save to path '$CSVFile'." 450 | } 451 | } 452 | } 453 | 454 | $EndTime = Get-Date 455 | $TimeTaken = '' 456 | $TakenSpan = New-TimeSpan -Start $StartTime -End $EndTime 457 | if($TakenSpan.Hours) 458 | { 459 | $TimeTaken += "$($TakenSpan.Hours) hours, $($TakenSpan.Minutes) minutes, " 460 | } 461 | Elseif($TakenSpan.Minutes) 462 | { 463 | $TimeTaken += "$($TakenSpan.Minutes) minutes, " 464 | } 465 | $TimeTaken += "$($TakenSpan.Seconds) seconds" 466 | 467 | "The script took $TimeTaken to complete." -------------------------------------------------------------------------------- /Get-Office365Forwarders/README.md: -------------------------------------------------------------------------------- 1 | # Get-Office365Forwarders 2 | 3 | This script was written after a customer's Office 365 tenancy was compromised and we needed to see if any accounts were sending email externally. 4 | 5 | None of the existing solutions examined both Outlook rules, and forwarders set in the user's Office 365 settings. 6 | 7 | You can examine just Outlook, or Office, by using the '-NoOffice365' or '-NoOutlook' switches. 8 | 9 | By default all Office 365 mailboxes are checked, but you can limit to certain accounts by using the '-EmailAddresses' switch. 10 | 11 | As this was built to check for possible compromises it only lists forwarders/redirects to email outside of the tenancy. If you wish to see all email that is forwarded or redirected use the '-IncludeInternal' switch. 12 | 13 | All data is saved to a CSV that you can specify with the '-CSVFile' switch. If no file is specified the data is saved to "C:\Windows\temp\Office 365 forwarding accounts.csv" 14 | 15 | It looks like this while running: 16 | ![Gif of the script in action](https://github.com/kieranwalsh/img/blob/main/Get-Office365Forwarders.gif) 17 | 18 | Here is an example of the CSV data: 19 | ![Image of the finished CSV data](https://github.com/kieranwalsh/img/blob/main/Get-Office365Forwarders%20-%20csv.png) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kieran Walsh 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 | # PowerShell 2 | 3 | [Add-LogEntry](https://github.com/kieranwalsh/PowerShell/blob/main/Add-LogEntry/add-LogEntry.ps1) 4 | Similar to `Tee-Object`, this function will output data to the screen and a log file. However, it will timestamp the log file entries and allow for various indentations in the log, and colours in the host screen, depending on the data submitted. 5 | 6 | [Get-NetworkLogons](https://github.com/kieranwalsh/PowerShell/tree/main/Get-NetworkLogons) 7 | This script will search all AD computers (or ones matching the -ComputerName entry) and display the logged-on user if anyone. I mostly use it to find an unused end-user device to remote into and check if policies are applying how I expect. 8 | 9 | [Get-Office365Forwarders](https://github.com/kieranwalsh/PowerShell/tree/main/Get-Office365Forwarders) 10 | Lists all Office 365 mailboxes with rules to forward or redirect emails. Lists emails that go outside the tenancy by default, but can be used to include any internal redirects as well. 11 | 12 | [Update-AllPSModules](https://github.com/kieranwalsh/PowerShell/tree/main/Update-AllPSModules) 13 | Use this script to update installed PowerShell modules to the latest version it can find online. It will also attempt to update PackageManagement and PowerShellGet so that it can update to pre-release versions. 14 | -------------------------------------------------------------------------------- /Update-AllPSModules/README.md: -------------------------------------------------------------------------------- 1 | # Update-AllPSModules 2 | 3 | This link is being kept up for historical reasons, but the project can now be found [here](https://github.com/kieranwalsh/Update-AllPSModules) 4 | 5 | This script will update all locally installed PowerShell modules to the latest version it can find online. It will also attempt to update PackageManagement and PowerShellGet so that it can update to pre-release versions. 6 | 7 | You can use the '-NoPreviews' switch to avoid modules with 'beta', 'nightly', 'preview' etc., in the name. 8 | 9 | This is what it looks like while running: 10 | 11 | ![Image of Update-AllPSModules sample](https://github.com/kieranwalsh/img/blob/main/Update-AllPSModules%20Sample.png) 12 | 13 | ![Gif of Update-AllPSModules in action](https://github.com/kieranwalsh/img/blob/main/Update-AllPSModules.gif) --------------------------------------------------------------------------------