Alert on Login on Production environment

Almost one year ago, roadmap was ISO 27001 certified. This was a great feat for a startup of only a half a year old at that time. We weren’t entirely issue free of course, we had a couple of minors to fix, but only a couple. In general, we were in control. One of these minor points was that we needed to get an alert when someone logged on to a server in the production environment. Why? Because people shouldn’t. There is no need to be on a production server for writing or deploying software, because we use Octopus deploy for deployments. The only developer who we’d expect to find logging on to servers is the one on call, he or she is in the ‘ops’ role of Devops. And of course we’d expect Jeffrey, our dba. 

Recently, someone asked me just how we created such an alert, so I decided to share how we did it.

Our first attempt of to get the alert was to leverage the windows event with id 4624 together with LogicMonitor (https://www.logicmonitor.com). It is great tool to monitor your servers, you should check it out. My thought was that I could use LogicMonitor to check the event log and warn whenever a user logs on. But the log was full of these events; Windows didn’t just log the logon events from me and my colleagues, but also from AD accounts that we use to run services / tools. We needed to filter the logins to see if there were some strange logins.  Unfortunately, I was unable to filter to just the events to the level I needed,  despite LogicMonitors powerful capabilities on filtering.

After that we tried a different approach and had more luck with that: the Group Policy logon scripts. Details on how to enable running a powershell script when someone logs on or off can be found here: https://4sysops.com/archives/configuring-logon-powershell-scripts-with-group-policy/. After that we just had to come up with the script to send an email. Luckily this is really easy in powershell.

$user = [Environment]::UserName
$machine = [Environment]::MachineName
$now = [DateTime]::Now.ToString() 

Send-MailMessage -SmtpServer "<mailserveraddress>" `
		 -to "monitoring@getroadmap.com" `
		 -from "alert@getroadmap.com" `
		 -subject "AD RD Session Start" `
		 -Body "$user has logged into $machine at $now"

But this wasn’t all we needed, as we found out during the internal audit for ISO27001 half a year later. We needed to know why somebody was logging on. My assumption was that we would just change the script to add a logon reason. But alas, if it only were this simple.

To add the reason the first thing you need to do is to ask the user. This seems simple enough from powershell.

$reason = [microsoft.visualbasic.interaction]::inputbox($msg, $title, "", -1, -1)

There are a few major problems with this solution. First, you can dismiss the box (there is a cancel button and a close button). This was easy to fix with a while loop around it that would check for the reason to be filled. But even then there is still a chance of someone killing the script and we would never be informed of someone logging on. So we decided to send a second mail with the reason. We also got rid of the Visual Basic msgbox by using XAML as you will be able to see in the final script below.

Then there is the problem that the window needs to be opened on the foreground so it visible right away. This required us to do some dll imports. Here’s the final script that will send the second mail.

function ShowWindow {
$Xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="Window"
        Title="Name" Height="137" Width="444" MinHeight="137" MinWidth="100"
        FocusManager.FocusedElement="{Binding ElementName=TextBox}"
        ResizeMode="CanResizeWithGrip">
    <DockPanel Margin="8">
        <StackPanel DockPanel.Dock="Bottom" 
                    Orientation="Horizontal" HorizontalAlignment="Right">
            <Button x:Name="OKButton" Width="60" IsDefault="True" 
                    Margin="12,12,0,0" TabIndex="1" >_OK</Button>
            
        </StackPanel>
        <StackPanel >
            <Label x:Name="Label" Margin="-5,0,0,0" TabIndex="3">Label:</Label>
            <TextBox x:Name="TextBox" TabIndex="0" />
        </StackPanel>
    </DockPanel>
</Window>
'@

    if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne 'STA')
    {
        throw "Script can only be run if PowerShell is started with -STA switch."
    }

    Add-Type -Assembly PresentationCore,PresentationFrameWork

    $xmlReader = [System.Xml.XmlReader]::Create([System.IO.StringReader] $Xaml)
    $form = [System.Windows.Markup.XamlReader]::Load($xmlReader)
    $xmlReader.Close()

    $window = $form.FindName("Window")
    $window.Title = "Logon reason"

    $label = $form.FindName("Label")
    $label.Content = "Why are you logging on?"

    $textbox = $form.FindName("TextBox")

    $okButton = $form.FindName("OKButton")
    $okButton.add_Click({$window.DialogResult = $true})

    if ($form.ShowDialog())
    {
        if ([string]::IsNullOrEmpty($textbox.Text)){
            return ""
        }
        return $textbox.Text
    }
    else{
        return ""
    }
}
     
$activateWindow = {
        $sfw = '[DllImport("user32.dll")] public static extern bool SetForegroundWindow (IntPtr hWnd);'
        Add-Type -MemberDefinition $sig -name NativeMethods -namespace Win32
        $sig = '[DllImport("user32.dll")] public static extern bool BringWindowToTop (IntPtr hWnd);'
        Add-Type -MemberDefinition $sig -name NativeMethods -namespace Win32
        $fw = '[DllImport("user32.dll")] public static extern IntPtr FindWindow (String sClassName, String sAppName);'
        Add-Type -MemberDefinition $fw -name NativeMethods -namespace Win32
        $sw = '[DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);'
        Add-Type -MemberDefinition $fw -name NativeMethods -namespace Win32
  
        $null = [reflection.assembly]::loadwithpartialname("microsoft.visualbasic")
        $isWindowFound = $false
        while(-not $isWindowFound) {
            try {
                [microsoft.visualbasic.interaction]::AppActivate($args[0])
                $ptr = $fw::FindWindow([IntPtr]::Zero, $args[0])
                $sig::ShowWindowAsync($ptr)
                $sfw::SetForegroundWindow($ptr)
                $sw::ShowWindow($ptr, 1)
                $isWindowFound = $true
            }
            catch {
                sleep -Milliseconds 100
            }
        }
    }

$user = [Environment]::UserName
$machine = [Environment]::MachineName
$now = [DateTime]::Now.ToString() 
$reason = ""

while($reason -eq "") {
    $job = Start-Job $activateWindow -ArgumentList "Logon reason"
    $reason = ShowWindow
    Remove-Job $job -Force
}
Send-MailMessage -SmtpServer "<mailserveraddress>" `
		 -to "monitoring@getroadmap.com" `
		 -from "alert@getroadmap.com" `
		 -subject "AD RD Session Reason" `
		 -Body "$user has logged into $machine at $now because: $reason"

But we still weren’t there. The logon script will not allow the script to be run in the foreground and thus the window will never pop up in the right way. We had to pull another trick.

We changed the logon script to invoke the powershell script shown above.

Invoke-Item (start powershell ((Split-Path $MyInvocation.InvocationName) + "\SendMailOnLogonWithReason.ps1"))

Now we are greeted with a popup window every time we access a server in the production environment. We have to fill it in or it will keep bugging us until we do. We receive no less than 3 mails per session. One at logon time, one with the reason and one with the logoff time.

It does the job, but we can already see this change into a little sql script or maybe a publish of an azure service bus event rather than a collection of  emails.