Category Archives: Series: PowerShell Monitoring

Monitoring with PowerShell: Monitoring users that are blocked for login

Hi guys. Today I’ll only have a short blog – I’ve been busy this weekend with non-tech stuff like building a table for dungeons and dragons, which is why I’ve only had time to write a somewhat shorter blog than normally.

This one is based on a blog from last week – Some users on Reddit asked if I could also create a monitoring set for blocked users. We’ve setup policies to make sure users are blocked after multiple failed logins, or when failing the second factor authentication a couple of times. Its best to monitor this to preventively to make sure you can give the users a call and check if everything is functioning as it should.

The following script helps you in this.

##############################
$ApplicationId = 'XXXX-XXXX-XXXX-XXX-XXX'
$ApplicationSecret = 'YourApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourTenantID.Onmicrosoft.com'
$RefreshToken = 'VeryLongRefreshToken'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All

$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
$BlockedUserlist = foreach ($customer in $customers) {
    write-host "Getting Blocked users for $($Customer.name)" -ForegroundColor Green
    $BlockedUsers = Get-MsolUser -TenantId $($customer.TenantID) | Where-Object {$_.BlockCredential -eq $true}
    foreach($User in $BlockedUsers){ "$($user.UserPrincipalName) is blocked from logon." }
}
if(!$BlockedUserlist) {  $BlockedUserlist = "Healthy" } 

And that’s it! as always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring Office 365 deleted users & License usage

I’ve been getting some requests to talk more about monitoring access and license management for Office 365. Some of you have asked how to be notified when users get deleted, or to get a notification right before a user is deleted permanently. Another question was on how to check if all licenses are assigned and you’re not wasting any resources or money on unused licenses. I’ve decided to blog about both 🙂

Monitoring deleted users

So the first one up is monitoring the deleted users – I understand monitoring this for a multitude of reasons. Imagine you’re distributing licenses and when a user gets deleted you need to update your billing systems, or imagine that you have a specific off-boarding procedure that needs to be kicked off when a user is deleted. I’ve created two cases; One to alert as soon as a deleted user has been found, another to alert when the user is about to be permanently deleted.

Monitoring all deleted users
##############################
$ApplicationId = 'XXXX-XXXX-XXXX-XXX-XXX'
$ApplicationSecret = 'YourApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourTenantID.Onmicrosoft.com'
$RefreshToken = 'VeryLongRefreshToken'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
$DeletedUserlist = foreach ($customer in $customers) {
    write-host "Getting Deleted users for $($Customer.name)" -ForegroundColor Green
    $DeletedUsers = Get-MsolUser -ReturnDeletedUsers -TenantId $($customer.TenantID)
    foreach($User in $DeletedUsers){ "$($user.UserPrincipalName) has been deleted on $($User.SoftDeletionTimestamp)" }
}
if(!$DeletedUserlist) {  $DeletedUserlist = "Healthy" }
Monitoring near permanent delete date.
##############################
$Daystomonitor = (Get-Date).AddDays(-28) #This means we will alert when a user has been deleted for 28 days, and is 1 day before permanent deletion.
##############################
$ApplicationId = 'XXXX-XXXX-XXXX-XXX-XXX'
$ApplicationSecret = 'YourApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourTenantID.Onmicrosoft.com'
$RefreshToken = 'VeryLongRefreshToken'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
$DeletedUserlist = foreach ($customer in $customers) {
    write-host "Getting Deleted users for $($Customer.name)" -ForegroundColor Green
    $DeletedUsers = Get-MsolUser -ReturnDeletedUsers -TenantId $($customer.TenantID) | Where-Object {$($User.SoftDeletionTimestamp) -lt $Daystomonitor}
    foreach ($User in $DeletedUsers) { "$($user.UserPrincipalName) has been deleted on $($User.SoftDeletionTimestamp)" }
}
if (!$DeletedUserlist) { $DeletedUserlist= "Healthy" }


 

To explain the scripts; The first script connects to each tenant in your Microsoft Partner portal, grabs all deleted users, and gives you a report of all deleted users. The second one does the same, but filters specifically on users that have been deleted for 28 days.

Monitoring unused licenses

So this is one that we are going to use internally after getting some requests from all of you – I think it’s a pretty smart idea to monitor if licenses are in use, and if not to alert on them. It helps to compare the administrative side of licensing to the actual cost of licensing.

##############################
$ApplicationId = 'XXXX-XXXX-XXXX-XXX-XXX'
$ApplicationSecret = 'YourApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourTenantID.Onmicrosoft.com'
$RefreshToken = 'VeryLongRefreshToken'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
$UnusedLicensesList = foreach ($customer in $customers) {
    write-host "Getting licenses $($customer.name)" -ForegroundColor Green
    $Licenses = Get-MsolAccountSku -TenantId $($customer.TenantId)
    foreach ($License in $Licenses) { 
        if ($License.ActiveUnits -lt $License.consumedUnits) { "$($customer.name) - $($License.AccountSkuId) has licenses available." }

    }
}
if (!$UnusedLicensesList) { $UnusedLicensesList = "Healthy" } 

And that’s it! a somewhat longer blog than normal, with multiple scripts. But I hope you’ve enjoyed it. As always, Happy Powershelling.

Monitoring with PowerShell: Monitoring SQL server health

Seems like this is the week of SQL server blogs! This time we’re going to cover monitoring the SQL server health. SQL server health monitoring is important to keep all line of business applications in check and to make sure they perform well. We’ll be focussed on monitoring the server, databases, and jobs.

We will use the same trick as we did in the last SQL post. We’re going to be using the SQL Server module called SQLPS, which loads a PSDrive to browse all databases and get the state of each database. So let’s get started!

The script

I’d like to take a moment to point at that this script offers only very basic monitoring. This is often enough for MSPs and non-dba type administrators. If you want more extensive monitoring you should really look into the amazing dbatools module by Chrissy Lemaire and her team of amazing PowerShell admins/dbas. 🙂

The script alerts on databases that are not in a normal state, That have a recovery model other than “Simple” and where a database max size has been set. Also we’re checking if the database is located on C:\. You might want to comment out one of these if you do not care about one of these settings. These are just some of the things that we look out for in our environments. Its fairly straight forward to edit this script to monitor the backup dates instead, or if the database has the correct collation.

import-module SQLPS
$Instances = Get-ChildItem "SQLSERVER:\SQL\$($ENV:COMPUTERNAME)"
foreach ($Instance in $Instances) {
    $databaseList = get-childitem "SQLSERVER:\SQL\$($ENV:COMPUTERNAME)\$($Instance.Displayname)\Databases"
    $SkipDatabases = @("Master","Model","ReportServer","SLDModel.SLDData")
    $Errors =  foreach ($Database in $databaselist | Where-Object {$_.Name -notin $SkipDatabases}) {
        if ($Database.status -ne "normal") {"$($Database.name) has the status: $($Database.status)" }
        if ($Database.RecoveryModel -ne "Simple") {  "$($Database.name) is in logging mode $($Database.RecoveryModel)" }
        if ($database.filegroups.files.MaxSize -ne "-1") { "$($Database.name) has a Max Size set." }
        if ($database.filegroups.files.filename -contains "C:") { "$($Database.name) is located on the C:\ drive." }
    }
}
if (!$errors) { $HealthState = "Healthy" } else { $HealthState = $Errors }  

And that’s it! Of course, modify these scripts to your own environment and requirements. And as always, Happy PowerShelling.

Monitoring and Documenting with PowerShell: End of year review

Hi! So this is the final post of this year. I’m going to be enjoying some well deserved holidays and spend Christmas with my family. The past year has been pretty cool. I’ve been doing so many cool projects.

I figured I also would list the top blogs of this year by views, and just generally some stuff I’m proud of, so lets get started:

Top blogs

The most viewed blog this year is my Functional PowerShell for MSPs webinar, which is pretty amazing because it was only posted 3 months ago. I still see the views racking up on the Teams Live Event recording and I am going to be giving another (albeit slightly shorter one) the 16th of december.

The runner up in this is the start of the Documenting with PowerShell series. That entire series seems to have been a favorite for most people. The third place is going to the unofficial IT-Glue backup script.

My personal favorite has to be a recent blog; either the Secure Application Model blog or the OneDrive monitoring script which uses user impersonation.

Documenting with PowerShell series

The documenting with PowerShell series has been a hit. I’ve taken a small break from it to reorganise and make it a little more “Eye-candy” focussed as this was the primary question I’ve been getting. I love how some of you have adapted the scripts. Most of them were made as an example so it’s cool to see all different variations of it. I will be continuing this series at the start of next year. If you have any wishes, let me know!

Monitoring with PowerShell series

The monitoring with PowerShell series is still my baby, I love doing it and showing all the different methods of using PowerShell over SNMP, or PowerShell over generic WMI monitoring. Next year I hope to still be posting at least 1 blog a week. I’ve recently been mailed some question that I will be picking up next year too.

Special thanks

This year was great! I especially want to thank my peers in MSP’r’Us that always help me find new ideas. I also want to thank Datto for the way we have been collaborating and adding my blogs and script to their product. It’s been a great adventure.

And that’s it! I wish all my readers an amazing Christmas, and of course a happy new year. As always, Happy PowerShelling!

Monitoring with PowerShell: User Recycle bin Remediation

I deploy a lot of environments where there is some form of folder redirection – be it classical folder redirection using a GPO or UPD on Windows Virtual Desktop, or even Known Folder Redirection using OneDrive. The benefits to using these forms of folder redirection is clear, but comes with another cool feature; The recycle bin is often redirected too. This is great because each user has his own recycler this way and we never have to worry about anyone seeing files or folders from anyone else.

The downside to this is that it can quickly eat away at disk space you don’t want to lose, Which is why I’ve built the following component, you can run this from your RMM system, put it in a logon script or logoff script in your GPO, or just run it on demand.

You can modify $days to set to any amount, 0 means it will clear the entire recycle bin for that user – Something we often don’t like doing as accidental deletes can happen.

$Days = 14
$Shell = New-Object -ComObject Shell.Application
$Global:Recycler = $Shell.NameSpace(0xa)
foreach ($item in $Recycler.Items()) {
    $DateDel = $Recycler.GetDetailsOf($item, 2) -replace "\u200f|\u200e", "" | get-date
    If ($DateDel -lt (Get-Date).AddDays(-$Days)) { Remove-Item -Path $item.Path -Confirm:$false -Force -Recurse }
} 

And that’s it! as always, Happy PowerShelling!

Monitoring with PowerShell: Alerting on large Office 365 mailboxes

This script is one we’ve used in the past as a sales tool – Some companies tend to use their mailbox as a storage location more than just a mailbox. They save large attachments, use it as a personal CRM system or even just really like sending eachother large PDFs 😉

When mailboxes get too large your users will start experiencing performance or caching issues. It’s also just not a good practice to have huge mailboxes, just imagine you’ll want to work on a Remote Desktop or Windows Virtual Desktop server with a 60GB mailbox cached…

Anyway; to make sure that when users experience large growth in mailboxes I’ve been using the following monitoring set in our N-central RMM system. This monitoring script alerts whenever a user has a mailbox larger than 60GB. As always I’ve included two scripts: one for a single tenant, one for multiple tenants. As always, my scripts are using the Secure Application Model.

Multiple tenant script

$ApplicationId         = 'xxxx-xxxx-xxx-xxxx-xxxx'
$ApplicationSecret     = 'TheSecretTheSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'UPN-Used-To-Generate-Tokens'
$SizeToMonitor         = 60

$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
$LargeMailboxes = @()
foreach ($customer in $customers) {
    write-host "Getting started for $($Customer.name)" -foregroundcolor green
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session -allowclobber -Disablenamechecking
    $Mailboxes = Get-Mailbox | Get-MailboxStatistics | Select-Object DisplayName, @{name = "TotalItemSize (GB)"; expression = { [math]::Round((($_.TotalItemSize.Value.ToString()).Split("(")[1].Split(" ")[0].Replace(",", "") / 1GB), 2) } }, ItemCount | Sort "TotalItemSize (GB)" -Descending
    foreach ($Mailbox in $Mailboxes) { if ($Mailbox.'TotalItemSize (GB)' -gt  $SizeToMonitor) { $LargeMailboxes += $Mailbox } }
    Remove-PSSession $session
}

if (!$LargeMailboxes) { "No Large mailboxes found" }

Single Tenant Script

$ApplicationId         = 'xxxx-xxxx-xxx-xxxx-xxxx'
$ApplicationSecret     = 'TheSecretTheSecrey' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'UPN-Used-To-Generate-Tokens'
$customertenant        = 'CustomerTenant.onmicrosoft.com'
$SizeToMonitor         = 60 

$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$LargeMailboxes = @()

    write-host "Getting Large mailboxes" -ForegroundColor green
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customertenant
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customertenant
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session -allowclobber -Disablenamechecking
    $Mailboxes = Get-Mailbox | Get-MailboxStatistics | Select-Object DisplayName, @{name = "TotalItemSize (GB)"; expression = { [math]::Round((($_.TotalItemSize.Value.ToString()).Split("(")[1].Split(" ")[0].Replace(",", "") / 1GB), 2) } }, ItemCount | Sort "TotalItemSize (GB)" -Descending
    foreach ($Mailbox in $Mailboxes) { if ($Mailbox.'TotalItemSize (GB)' -gt  $SizeToMonitor  { $LargeMailboxes += $Mailbox } }
    Remove-PSSession $session

if (!$LargeMailboxes) { "No Large mailboxes found" } 

And that’s it! as always, Happy PowerShelling.

Ps: I’m giving a new PowerShell webinar soon. Join me by clicking this link.

Monitoring with PowerShell: Alerting on Shodan results

This is a bit of a short script again – but that’s just because sometimes life can made be real simple. Shodan is a tool that scans the entire internet and documents which open ports are available, if it is vulnerable for specific CVE’s, and lots of cool other stuff explained here.

We’ve seen some MSP’s offer a simple Shodan query and selling it as a “Dark Web Scan” – Please note that this is absolutely not a comprehensive scan and finding online exposed services is not always such a big deal, for example in controlled environments.

The script I’ve made is one we run at our clients on IP addresses where we know nothing should be listed in Shodan, networks that should not have exposed services, or just IP addresses where we want to alert on changes. Simply change the list of IPs to the list you would like to monitor.

$APIKEY = "YourShodanAPIKey"
$CurrentIP = (Invoke-WebRequest -uri "http://ifconfig.me/ip" -UseBasicParsing ).Content
$ListIPs = @("1.1.1.1","2.2.2.2",$CurrentIP)
foreach($ip in $ListIPs){
   $Shodan = Invoke-RestMethod -uri "https://api.shodan.io/shodan/host/$($ip)?key=$APIKEY"
}
if(!$Shodan) { $HealthState = "Healthy"} else { $HealthState = "Alert - $($Shodan.ip_str) is found in Shodan."} 

We also like running these scripts at our prospects as a part of a security survey, because if Shodan has found external services such as RDP on a different port it often shows bad security practices as a whole.

Getting a Shodan subscription is absolutely worth it because it gives you that little bit more of visibility on how exposed you actually. Anyway, as always happy PowerShelling!

Monitoring with PowerShell: Monitoring OneDrive status for current logged on user!

Since the release of Onedrive and Onedrive for business, a lot of system administrators have been trying to figure out how to monitor the onedrive status. Rodney Viana at Microsoft made a pretty awesome module to be able to get the current OneDrive Sync status, you can find that module here.

The issue with this module is that it has to run under the current logged on user, You don’t always have the ability to do that, especially when using RMM systems that always use the NT AUTHORITY\SYSTEM account. Now PowerShell has the ability to load .NET components as code and execute them, this gave me the idea to use impersonation of the current user in my PowerShell script to monitor OneDrive.

After messing around trying to build my own, a friend of mine pointed me to Roger Zanders post here. Combining these two scripts was pretty simple and resolved the entire onedrive monitoring issue for me.

So without any more ado; I introduce the CyberDrain OneDrive Status monitoring script for RMM systems. The script downloads the latest version of the OneDriveLib.dll, runs Get-ODStatus cmdlet under the current user, and returns the state of the OneDrive sync in $ODErrors.

The script

$Source = @"
using System;  
using System.Runtime.InteropServices;

namespace murrayju.ProcessExtensions  
{
    public static class ProcessExtensions
    {
        #region Win32 Constants

        private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
        private const int CREATE_NO_WINDOW = 0x08000000;

        private const int CREATE_NEW_CONSOLE = 0x00000010;

        private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
        private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;

        #endregion

        #region DllImports

        [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            String lpApplicationName,
            String lpCommandLine,
            IntPtr lpProcessAttributes,
            IntPtr lpThreadAttributes,
            bool bInheritHandle,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            String lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation);

        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
        private static extern bool DuplicateTokenEx(
            IntPtr ExistingTokenHandle,
            uint dwDesiredAccess,
            IntPtr lpThreadAttributes,
            int TokenType,
            int ImpersonationLevel,
            ref IntPtr DuplicateTokenHandle);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

        [DllImport("userenv.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hSnapshot);

        [DllImport("kernel32.dll")]
        private static extern uint WTSGetActiveConsoleSessionId();

        [DllImport("Wtsapi32.dll")]
        private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);

        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern int WTSEnumerateSessions(
            IntPtr hServer,
            int Reserved,
            int Version,
            ref IntPtr ppSessionInfo,
            ref int pCount);

        #endregion

        #region Win32 Structs

        private enum SW
        {
            SW_HIDE = 0,
            SW_SHOWNORMAL = 1,
            SW_NORMAL = 1,
            SW_SHOWMINIMIZED = 2,
            SW_SHOWMAXIMIZED = 3,
            SW_MAXIMIZE = 3,
            SW_SHOWNOACTIVATE = 4,
            SW_SHOW = 5,
            SW_MINIMIZE = 6,
            SW_SHOWMINNOACTIVE = 7,
            SW_SHOWNA = 8,
            SW_RESTORE = 9,
            SW_SHOWDEFAULT = 10,
            SW_MAX = 10
        }

        private enum WTS_CONNECTSTATE_CLASS
        {
            WTSActive,
            WTSConnected,
            WTSConnectQuery,
            WTSShadow,
            WTSDisconnected,
            WTSIdle,
            WTSListen,
            WTSReset,
            WTSDown,
            WTSInit
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;
        }

        private enum SECURITY_IMPERSONATION_LEVEL
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3,
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct STARTUPINFO
        {
            public int cb;
            public String lpReserved;
            public String lpDesktop;
            public String lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public uint dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        private enum TOKEN_TYPE
        {
            TokenPrimary = 1,
            TokenImpersonation = 2
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WTS_SESSION_INFO
        {
            public readonly UInt32 SessionID;

            [MarshalAs(UnmanagedType.LPStr)]
            public readonly String pWinStationName;

            public readonly WTS_CONNECTSTATE_CLASS State;
        }

        #endregion

        // Gets the user token from the currently active session
        private static bool GetSessionUserToken(ref IntPtr phUserToken)
        {
            var bResult = false;
            var hImpersonationToken = IntPtr.Zero;
            var activeSessionId = INVALID_SESSION_ID;
            var pSessionInfo = IntPtr.Zero;
            var sessionCount = 0;

            // Get a handle to the user access token for the current active session.
            if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
            {
                var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
                var current = pSessionInfo;

                for (var i = 0; i < sessionCount; i++)
                {
                    var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
                    current += arrayElementSize;

                    if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive)
                    {
                        activeSessionId = si.SessionID;
                    }
                }
            }

            // If enumerating did not work, fall back to the old method
            if (activeSessionId == INVALID_SESSION_ID)
            {
                activeSessionId = WTSGetActiveConsoleSessionId();
            }

            if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0)
            {
                // Convert the impersonation token to a primary token
                bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                    (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                    ref phUserToken);

                CloseHandle(hImpersonationToken);
            }

            return bResult;
        }

        public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true)
        {
            var hUserToken = IntPtr.Zero;
            var startInfo = new STARTUPINFO();
            var procInfo = new PROCESS_INFORMATION();
            var pEnv = IntPtr.Zero;
            int iResultOfCreateProcessAsUser;

            startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));

            try
            {
                if (!GetSessionUserToken(ref hUserToken))
                {
                    throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed.");
                }

                uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW);
                startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE);
                startInfo.lpDesktop = "winsta0\\default";

                if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false))
                {
                    throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed.");
                }

                if (!CreateProcessAsUser(hUserToken,
                    appPath, // Application Name
                    cmdLine, // Command Line
                    IntPtr.Zero,
                    IntPtr.Zero,
                    false,
                    dwCreationFlags,
                    pEnv,
                    workDir, // Working directory
                    ref startInfo,
                    out procInfo))
                {
                    throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.\n");
                }

                iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
            }
            finally
            {
                CloseHandle(hUserToken);
                if (pEnv != IntPtr.Zero)
                {
                    DestroyEnvironmentBlock(pEnv);
                }
                CloseHandle(procInfo.hThread);
                CloseHandle(procInfo.hProcess);
            }
            return true;
        }
    }
}


"@
New-Item 'C:\programdata\Microsoft OneDrive' -ItemType directory -Force -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/rodneyviana/ODSyncService/master/Binaries/PowerShell/OneDriveLib.dll' -OutFile 'C:\programdata\Microsoft OneDrive\OneDriveLib.dll'

Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp 
$scriptblock = {
    Unblock-File 'C:\programdata\Microsoft OneDrive\OneDriveLib.dll'
    import-module 'C:\programdata\Microsoft OneDrive\OneDriveLib.dll'
    $ODStatus = Get-ODStatus | convertto-json | out-file 'C:\programdata\Microsoft OneDrive\OneDriveLogging.txt'
}


[murrayju.ProcessExtensions.ProcessExtensions]::StartProcessAsCurrentUser("C:\Windows\System32\WindowsPowershell\v1.0\Powershell.exe", "-command $($scriptblock)","C:\Windows\System32\WindowsPowershell\v1.0",$false)
start-sleep 5
$ErrorList = @("NotInstalled", "ReadOnly", "Error", "OndemandOrUnknown")
$ODStatus = (get-content "C:\programdata\Microsoft OneDrive\OneDriveLogging.txt" | convertfrom-json).value
foreach ($ODStat in $ODStatus) {
    if ($ODStat.StatusString -in $ErrorList) { $ODerrors = "$($ODStat.LocalPath) is in state $($ODStat.StatusString)" }
}
if (!$ODerrors) {
    $ODerrors = "Healthy"
}

 

And that’s it! I’m quite proud of this one as I have seen a lot of people struggle with it so I hope it helps. As always, Happy PowerShelling!

Monitoring with PowerShell: Monitoring the creation of new teams.

Since my last blog about the Secure App Model I’ve been playing with using it in different cases such as collecting automatic documentation, but also using it as an early warning tool for events our helpdesk might want to monitor.

One of these tasks is monitoring the creation of new Teams. We like keeping our clients environments in check but of course we also love the teams structure that anyone can create a team at any moment to jump right into their work. The issue with this is often one of compliance and security – We need to be aware of creation to apply the correct processes to the Team and maybe assist the end-user in further setup.

To do this I’ve made the following script; The script uses the Graph API to find all current teams. It then alerts on any team that has been created in the last day. You can of course modify this to your own needs by changing the $MonitorDate variable.

All Tenants script:

##############################
$MonitorDate = (get-date).AddDays(-1)
$ApplicationId = 'XXXX-XXXX-XXXX-XXX-XXX'
$ApplicationSecret = 'YourApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourTenantID.Onmicrosoft.com'
$RefreshToken = 'VeryLongRefreshToken'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
write-host "Generating access tokens" -ForegroundColor Green
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
write-host "Connecting to MSOLService" -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
write-host "Grabbing client list" -ForegroundColor Green
$customers = Get-MsolPartnerContract -All
write-host "Connecting to clients" -ForegroundColor Green

foreach ($customer in $customers) {
    write-host "Generating token for $($Customer.name)" -ForegroundColor Green
    $graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $customer.TenantID
    $Header = @{
        Authorization = "Bearer $($graphToken.AccessToken)"
    }
    write-host "Grabbing Teams for $($Customer.name)" -ForegroundColor Green
    $GroupUri = "https://graph.microsoft.com/v1.0/Groups?`$top=999"
    $Groups = (Invoke-RestMethod -Uri $GroupUri -Headers $Header -Method Get -ContentType "application/json").value | Where-Object { $_.resourceProvisioningOptions -eq "Team" }
    $NewGroups = foreach ($group in $Groups | Where-Object { [datetime]$_.CreatedDateTime -gt $MonitorDate }) { 
        "$($Group.displayName) has been created on $($group.createdDateTime)"
    
    }
}
if(!$NewGroups){ $NewGroups = "Healthy. No New groups have been created."} 

this script grabs the new-teams for each of the tenants under your administration and alerts into $NewGroups is a group is newer than the date you’ve given.

Single Tenant script:

##############################
$MonitorDate = (get-date).AddDays(-1)
$ApplicationId = 'XXXX-XXXX-XXXX-XXX-XXX'
$ApplicationSecret = 'YourApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'TenantYouWantTOLogInto.Onmicrost.com'
$RefreshToken = 'VeryLongRefreshToken'
##############################
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
write-host "Generating access tokens" -ForegroundColor Green
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
write-host "Connecting to MSOLService" -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
    $Header = @{
        Authorization = "Bearer $($graphToken.AccessToken)"
    }
    write-host "Grabbing Teams" -ForegroundColor Green
    $GroupUri = "https://graph.microsoft.com/v1.0/Groups?`$top=999"
    $Groups = (Invoke-RestMethod -Uri $GroupUri -Headers $Header -Method Get -ContentType "application/json").value | Where-Object { $_.resourceProvisioningOptions -eq "Team" }
    $NewGroups = foreach ($group in $Groups | Where-Object { &#91;datetime]$_.CreatedDateTime -gt $MonitorDate }) { 
        "$($Group.displayName) has been created on $($group.createdDateTime)"
    
    }
if(!$NewGroups){ $NewGroups = "Healthy. No New groups have been created."}

And that’s it! hope it helps, and as always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring disk speed

Sometimes we get complaints from clients reporting “my machine is slow” and not really get any leads to work with. The client just experiences slowness. In most cases this is due to disk speeds – the client bought some cheap computer with a 5400RPM spinner in it and is expecting it to perform just as good as any machine we supplied with an SSD.

To prevent this we could look at things such as the disk queue explained in an earlier blog here. But the problem with this type of monitoring is that its quite intermittent; You often only find the issue after a user has already complained. Because of this I’ve created a monitoring set that runs once or twice a (work) day in our RMM system. This script simply does a quick test on how fast it can actually create and read files and is used for reporting on it.

The script

So the script uses Diskspd.exe by Microsoft. You can download diskspd.exe here. You have to download the file and host it somewhere yourself. The script then downloads diskspd.exe from this location and executes two commands; a read of a 50mb file for 30 seconds, and writing a 50mb file for 30 seconds, totaling to 1 minute of performance testing.

$DownloadURL = "https://example.com/diskspd.exe"
Invoke-WebRequest -Uri $DownloadURL -OutFile "C:\Windows\Temp\diskspd.exe"
$ReadTest =  & "C:\Windows\Temp\diskspd.exe" -b128K -d30 -o32 -t1 -W0 -S -w0 -c50M test.dat
$Writetest = & "C:\Windows\Temp\diskspd.exe" -b128K -d30 -o32 -t1 -W0 -S -w100 -Z128K -c50M test.dat
$ReadResults = $readtest[-8] | convertfrom-csv -Delimiter "|" -Header Bytes,IO,Mib,IOPS,File | Select-Object IO,MIB,IOPs
$writeResults = $writetest[-1] | convertfrom-csv -Delimiter "|" -Header Bytes,IO,Mib,IOPS,File | Select-Object IO,MIB,IOPS

$ReadResults and $WriteResults will contain the IO, Mib/ps and IOPS. You can alert on each of these values. My personal preference is alering on them when the Mib/ps drops below 500mbps, because then you can be fairly sure its either a spinning disk, performance issue, or ancient SSD that needs to be replaced.

And that’s all for today! as always, happy PowerShelling.