Multi-Tenant Office365 password reminder

This topic contains 3 replies, has 2 voices, and was last updated by Profile photo of Rob Simmers Rob Simmers 1 year, 3 months ago.

  • Author
    Posts
  • #30493
    Profile photo of Matthew Dewart
    Matthew Dewart
    Participant

    I'm working on revising and updating a password reminder script that will email users from several different Office 365 accounts if their password is within 14 days of expiring(people ignore the Outlook notifications).

    It currently works, but poorly. I want to add HTML reports via email or SMB to an admin as well as much better error handling(and add errors to the html report in case of failed logins). I also want to bring the entire thing in line with best practices and more secure credential handling. Below is my script, any tips or suggestions on how to go about this?

    Function PullCredentials
    {
        ForEach ($account in $credhash.GetEnumerator()) # Go through each account listed in the INI file.
        {
            # Make a PSCredential object out of the gathered credentials
            Try
            {
                $365creds = New-Object System.Management.Automation.PSCredential (($account.Value.user).ToString(), (ConvertTo-SecureString ($account.Value.pass).tostring() -AsPlainText -Force))
                Write-Host "Logging in with" $account.Value.user
                # Connect to Office 365
                $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $365creds -Authentication Basic -AllowRedirection
                Import-PSSession $Session -AllowClobber
                Import-Module MSOnline
                Connect-MsolService -credential $365creds
    
                # Move to function for gathering users and emailing them.
                EmailUsers($account)
            
                # End Office 365 Connection
                Remove-PSSession $Session
                $365creds = $null
            }
            Catch
            {
                Write-Verbose "Failed to connect to $account.Name"
            }
        }
    }
    
    
    Function EmailUsers($arg1)
    {
        # Get Users From 365 who are Enabled, Passwords Expire and are Not Currently Expired
        Write-Host $arg1.Name
        $users = Get-MsolUser | where {$_.PasswordNeverExpires -eq $false}
        $users = $users | where {$_.userprincipalname.endswith($arg1.Name)}
        $domain = Get-MsolDomain | where {$_.IsDefault -eq $true }
        $maxPasswordAge = ((Get-MsolPasswordPolicy -DomainName $domain.Name).ValidityPeriod)
        # If the default password policy is configured(90 days) the previous command returns null.
        If ($maxPasswordAge -eq $null)
        {
            $maxPasswordAge = "90"
        }
        $maxPasswordAge = $maxPasswordAge.ToString()
    
        foreach ($user in $users)
        {
            $Name = $user.DisplayName
            $emailaddress = $user.UserPrincipalName
            $passwordSetDate = $user.LastPasswordChangeTimestamp
            $ExpiresOn = $passwordSetDate + $maxPasswordAge
            $today = (get-date)
            $daystoexpire = (New-TimeSpan -Start $today -End $Expireson).Days
            #write-host (Add-Content $logfile "$date,$Name,$emailaddress,$daystoExpire,$expireson")
            # Set Greeting based on Number of Days to Expiry.
            $messageDays = $daystoexpire
    
            if (($messageDays) -ge "1")
            {
                $messageDays = "in " + "$daystoexpire" + " days."
            }
            else
            {
                $messageDays = "today."
            }
    
            # Email Subject Set Here
            $subject="Your Office 365 password will expire $messageDays"
      
            # Email Body Set Here, Note You can use HTML, including Images.
            $body ="
            Dear $name,
             Your Office 365 Password will expire $messageDays.
            To change your password, please follow the attached instructions.
            You will need to visit http://mail.office365.com/ to complete this process.
            
            You will receive this notice once per day until your password is changed.
            Thanks, 
            Support Staff
    
            Please do not reply to this email."
    
           # If Testing Is Enabled - Email Administrator
            if (($testing) -eq "Enabled")
            {
                $emailaddress = $testRecipient
            } # End Testing
    
            # If a user has no email address listed
            if (($emailaddress) -eq $null)
            {
                $emailaddress = $testRecipient
            }# End No Valid Email
    
            if (($daystoexpire -ge "0") -and ($daystoexpire -lt $expireindays))
            {
                # If Logging is Enabled Log Details
                if (($logging) -eq "Enabled")
                {
                    Add-Content $logfile "$date,$Name,$emailaddress,$daystoExpire,$expireson" 
                }
    
                # Send Email Message
                if (($daystoexpire -ge "0") -and ($daystoexpire -lt $expireindays))
                {
                    Write-Host $user.UserPrincipalName "is expiring"
                    Send-Mailmessage -smtpServer $smtpServer -from $from -to $emailaddress -subject $subject -body $body -bodyasHTML -priority High -Attachments $attachments -UseSsl -Port 587 -Credential $SMTPcreds
                } # End Send Message
        
            } # End User Processing
        }
    }
    
    
    Function Get-IniContent
    {  
        < #  
        .Synopsis  
            Gets the content of an INI file  
              
        .Description  
            Gets the content of an INI file and returns it as a hashtable  
              
        .Notes  
            Author        : Oliver Lipkau   
            Blog        : http://oliver.lipkau.net/blog/  
            Source        : https://github.com/lipkau/PsIni 
                          http://gallery.technet.microsoft.com/scriptcenter/ea40c1ef-c856-434b-b8fb-ebd7a76e8d91 
            Version        : 1.0 - 2010/03/12 - Initial release  
                          1.1 - 2014/12/11 - Typo (Thx SLDR) 
                                             Typo (Thx Dave Stiff) 
              
            #Requires -Version 2.0  
              
        .Inputs  
            System.String  
              
        .Outputs  
            System.Collections.Hashtable  
              
        .Parameter FilePath  
            Specifies the path to the input file.  
              
        .Example  
            $FileContent = Get-IniContent "C:\myinifile.ini"  
            -----------  
            Description  
            Saves the content of the c:\myinifile.ini in a hashtable called $FileContent  
          
        .Example  
            $inifilepath | $FileContent = Get-IniContent  
            -----------  
            Description  
            Gets the content of the ini file passed through the pipe into a hashtable called $FileContent  
          
        .Example  
            C:\PS>$FileContent = Get-IniContent "c:\settings.ini"  
            C:\PS>$FileContent["Section"]["Key"]  
            -----------  
            Description  
            Returns the key "Key" of the section "Section" from the C:\settings.ini file  
              
        .Link  
            Out-IniFile  
        #>  
          
        [CmdletBinding()]  
        Param(  
            [ValidateNotNullOrEmpty()]  
            [ValidateScript({(Test-Path $_) -and ((Get-Item $_).Extension -eq ".ini")})]  
            [Parameter(ValueFromPipeline=$True,Mandatory=$True)]  
            [string]$FilePath  
        )  
          
        Begin  
            {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"}  
              
        Process  
        {  
            Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing file: $Filepath"  
                  
            $ini = @{}  
            switch -regex -file $FilePath  
            {  
                "^\[(.+)\]$" # Section  
                {  
                    $section = $matches[1]  
                    $ini[$section] = @{}  
                    $CommentCount = 0  
                }  
                "^(;.*)$" # Comment  
                {  
                    if (!($section))  
                    {  
                        $section = "No-Section"  
                        $ini[$section] = @{}  
                    }  
                    $value = $matches[1]  
                    $CommentCount = $CommentCount + 1  
                    $name = "Comment" + $CommentCount  
                    $ini[$section][$name] = $value  
                }   
                "(.+?)\s*=\s*(.*)" # Key  
                {  
                    if (!($section))  
                    {  
                        $section = "No-Section"  
                        $ini[$section] = @{}  
                    }  
                    $name,$value = $matches[1..2]  
                    $ini[$section][$name] = $value  
                }  
            }  
            Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath"  
            Return $ini  
        }  
              
        End  
            {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"}  
    } 
    
    
    
    $myDir = Split-Path -Parent $MyInvocation.MyCommand.Path
    Start-Transcript -Path "$myDir\log.txt"
    $credfile = $myDir + "\credfile.ini"
    $logging = "Enabled" # Set to Disabled to Disable Logging
    $logFile = $myDir + "\PasswordExpirations.csv" # ie. c:\mylog.csv
    $smtpServer="smtp.office365.com"
    $SMTPpass = ConvertTo-SecureString "atotallylegitpassword" -AsPlainText -Force
    $SMTPcreds = New-Object System.Management.Automation.PSCredential ("Expirations@myorgs-site.com", $SMTPpass)
    $date = Get-Date -format ddMMyyyy
    $expireindays = 7
    $credhash = Get-IniContent $credfile
    $testRecipient = "expirations@myorgs-site.com"
    $testing = "Enabled" # Set to Disabled to Email Users
    $from = "Password Expirations "
    $attachments = $myDir + "\Password Change Walkthrough v0.1.docx"
    
    if (($logging) -eq "Enabled")
    {
        # Test Log File Path
        $logfilePath = (Test-Path $logFile)
        if (($logFilePath) -ne "True")
        {
            # Create CSV File and Headers
            New-Item $logfile -ItemType File
            Add-Content $logfile "Date,Name,EmailAddress,DaystoExpire,ExpiresOn"
        }
    } # End Logging Check
    
    
    PullCredentials
    
    
    Stop-Transcript
  • #30785
    Profile photo of Rob Simmers
    Rob Simmers
    Participant

    Hey Matthew, I've been trying to get to you post but been a little nuts on this end.

    If you look at how Powershell functions are typically designed, they use verbs to indicate what they are doing (e.g. Get, Set, Send, etc.). Your logic is parse a INI, pass it to function to connect to MSOL and then call another function to email the user where you are actually returning all users and searching for a user in that resultset passed from a loop of your INI results. In pseudo code, my thoughts are that the logic should be more like:

    • Get-MailDomains – return all of the mail domains that you are going to search
    • Get-User -Domain [domains from Get-MailDomains] – this function would connect to MSOL and return users for each domain. You need to work on your filtering here. There is a switch on Get-MSOLUser for -DomainName or -SearchString that should allow you to filter the users returned versus returning all users versus returning ALL users and then doing Where statements to do the filtering. Additionally, when you call your Import-PSSession, if you're importing like 3000 cmdlets but only using 3, you may want to consider only importing the commands you will need:
      Import-PSSession -Session $Exch365Session -CommandName Get-Mailbox, Set-MailBox | Out-Null
      

      The goal of this function is to return all users that you need to work with in a PSObject.

    • Send-ExpNotification – this function could be built to with parameters like -EmailAddress, -ExpirationDate -User that you would need to build a message and send the message. You should also take a look at Powershell Splatting to keep long command lines neat and Here-Strings to build HTML with variables indented and easy to read.

    If your script you separate your logic to get the users and then do something with them, you can process each item in that PSObject and even add status to each person and at the end have a single PSObject that indicates what happened for each user.

  • #30792
    Profile photo of Matthew Dewart
    Matthew Dewart
    Participant

    Thanks Rob, the functions and logic flow is definitely going to get overhauled. I've been looking at splatting recently and think that it will make the error logging via HTML much easier.

  • #30800
    Profile photo of Rob Simmers
    Rob Simmers
    Participant

    Here is a snipplet of some code in the beginning block of a function using Exchange 365

        begin{
            $sessionParam = @{
                ConfigurationName = "Microsoft.Exchange";
                ConnectionUri = "https://outlook.office365.com/powershell-liveid/";
                Authentication = "Basic"
                AllowRedirection = $true
            }
            if ($Credential) {$sessionParam.Add("Credential", $Credential)}
            try {
                #Create a cloud session
                $Exch365Session = New-PSSession @sessionParam  -ErrorAction StopThis 
                #Import all of the commands for Exchange 365
                Import-PSSession -Session $Exch365Session -CommandName Get-Mailbox, Set-MailBox | Out-Null
            }
            catch {
                Write-Verbose ("{0}: Issue connecting to Office 365 cloud session. {1}" -f $MyInvocation.Command, $_.Exception.Message)
                Write-Error -Message ("{0}: Issue connecting to Office 365 cloud session. {1}" -f $MyInvocation.Command, $_.Exception.Message)
            } #catch New-PSSession\Import-PSSession
        } #begin
    
    

    Then wrap the function call with a try\catch:

    try {
        Get-User -ErrorAction Stop
    }
    catch {
        #Email admin bad news
    }
    

You must be logged in to reply to this topic.