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!

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

  1. alexander john

    Thank you so much for the script!

    I’m trying to get it to work but I get following error:

    Ausnahme beim Aufrufen von “StartProcessAsCurrentUser” mit 4 Argument(en): “StartProcessAsCurrentUser: GetSessionUserToken failed.”
    In Zeile:282 Zeichen:148
    + … dowsPowershell\v1.0\Powershell.exe”, “-command $($scriptblock)”,”C:\W …
    + ~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : Exception

    I commented out line 271 und 272 (I have the dll and folder already).

    Reply
    1. Kelvin Tegelaar Post author

      This script is supposed to run as SYSTEM, the error where you cannot get a user token means you are not running it as system. to do that, you can use PSEXEC or your RMM, as most RMMs run everything as system.

      Reply
  2. Alex Appleton

    This is great, thank you for this! I modified the if condition on the foreach loop a bit so that it wasn’t reporting on old profiles not in use. Just want to receive current ones here:

    if ($ODStat.StatusString -in $ErrorList `
    -and (Get-CimInstance win32_userprofile | ? {($_.SID -eq $ODStat.SID).LastUseTime}) `
    -and (Get-CimInstance win32_userprofile | ? {($_.SID -eq $ODStat.SID).LastUseTime}) -lt ((get-date).AddDays(-7)))

    Reply
  3. Martyn

    Firstly thank-you so much for sharing this!
    Unfortunately, and probably because I’m doing something wrong, when I run it I receive the output as below. Not sure what this indicates. Any ideas?

    Mode LastWriteTime Length Name
    —- ————- —— —-
    d—– 30/05/2020 14:45 Microsoft OneDrive
    True

    Reply
    1. Kelvin Tegelaar Post author

      Thats expected behaviour, the actual state is stored in the $ODerrors variable; most RMM systems are able to pick up variables. if yours does not you can add “write-host $ODerrors” at the end of the script.

      Reply
      1. Martyn

        Thanks. We’re using Solarwinds RMM but I’ll modify the script to output the variable. Awesome work by the way!

        Reply
  4. Ryan

    Thanks a bunch for the script Kelvin, works a treat.

    Just for anyone else using CWA(or any other RMM that doesn’t read PowerShell vars) separating everything after “Start sleep” to another script step works great for just getting the message.
    Otherwise all of the previous console text will be returned.

    Reply
    1. Andrew

      I found that adding > $null to the New-Item line stopped the output of the directory creation. So the whole line became:
      New-Item ‘C:\programdata\Microsoft OneDrive’ -ItemType directory -Force -ErrorAction SilentlyContinue > $null

      Reply
  5. Ryan

    Thank you for the work – this is going to save a lot of time.

    Likely an easy question, but is there a way to remove OneDrive personal from triggering an error result? We do not have Personal accounts logged in and it is showing the “OnDemandOrUnknown” as an error state. I can remove that error state from the component list, but that is the same state shown if the program is closed and would not help.

    Thank you again for your help!

    Reply
    1. Sasa

      Ryan, you can just filter Servicetype
      e.g.
      $ODStat.StatusString -in $ErrorList -and $odstat.servicetype -notlike “personal”

      Reply
  6. Pingback: Monitoring with PowerShell: Monitoring the Onedrive client limitations - CyberDrain

  7. Pingback: Automating with PowerShell: Impersonating users while running as SYSTEM - CyberDrain

  8. Shital Girmal

    It is throw me two error one is OneDriveLogging.txt file is missing and second one Exception calling ‘StartProcessAsCurrentUser’ mit 4 Argument(en): “StartProcessAsCurrentUser: GetSessionUserToken failed.

    Reply
  9. Jimmy

    Hi, I use component at DattoRMM ComponentStore.
    If FileOnDemand is enable always return error: “OnDemandOrUnknown”
    StatusString : OnDemandOrUnknown

    Reply
    1. Kelvin Tegelaar Post author

      That has nothing to do with files on demand. “OnDemand” is the status that the sync engine is paused and is only running on-demand, unknown is often a state in which a file that cannot be live synced is open, PST, PS1, CSV, etc.

      Reply
  10. Reta Kneale

    when i first implemented this was great — however today all of a sudden i am getting errors everywhere…
    Monitor OneDrive Sync Status [WIN] – Alert: Hash of OneDriveLib.DLL does not match. Check hash. (OneDriveLibHash: 3FED7E2E24A64835FA5CC241BC0ECAB3)

    ???? HELP!

    Reply
  11. Michie DeBerry

    This doesn’t appear to be working for me. Get-ODStatus in the spawned process doesn’t provide any output; however, when I run it from a regular PowerShell window, it works as expected. Any thoughts?

    Reply
  12. Boyd

    Hi Kelvin,

    First let me thank you for the awesome script. It works flawlessly, but every once in a while we get a bunch of errors because of a changed Hash.
    I was just wondering why that check is in the script. I have tried to read it myself and see if there is a reason for it, but my knowledge is lacking and I am unable to find out why.
    Could you help me with the answer?

    Reply
    1. Kelvin Tegelaar Post author

      Hi Boyd,

      The datto version does a hash check on the DLL file as we’re downloading it from an untrusted external source(github). for security reasons you want to be sure that the version downloaded is the one we’re expecting and not one a bad actor injected.

      Reply
      1. Boyd

        Hi Kelvin,

        That makes perfect sense. Thank you for the quick answer.
        We are loving your scripts in Datto. We have been able to monitor a lot more since you started working with them.
        Thank you.

        Kind regards,
        Boyd

        Reply
  13. Brandon Allison

    Hello,
    Thank you for providing this script. However, is there any way to test this script by putting OneDrive in an error state? We also use Continuum as our RMM, but I don’t believe it has the ability to create custom monitor scripts?

    Reply
  14. Eric

    Hi. First off, thank you so much. Your scripts have been so helpful. My team and I have put them to great use.

    I know others have said it before but I wanted to bring it up to see if you have any additional advice. I have this setup in my RMM and it is running in SYSTEM context. For most of my machines that I’ve tested it with, I get OnDemandOrUnknown as the status. I’ve confirmed that it’s checking the correct library. Any tips you can suggest? It very well (and most likely) is an issue with the computers or my deployment but I’m hoping you can help shed some light on it.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.