invictus-ir / Microsoft-Extractor-Suite

A PowerShell module for acquisition of data from Microsoft 365 and Azure for Incident Response and Cyber Security purposes.
https://microsoft-365-extractor-suite.readthedocs.io/en/latest/
GNU General Public License v2.0
481 stars 68 forks source link

Graph Authentication - Already Consented #73

Closed angry-bender closed 3 months ago

angry-bender commented 4 months ago
Hi,

We just released the update V2.0.0. I will close this issue for now, but please feel free to reopen it at any time or reach out with any suggestions or feedback.

The Get-all option is still on my Todo...

Originally posted by @JoeyInvictus in https://github.com/invictus-ir/Microsoft-Extractor-Suite/issues/57#issuecomment-2191101719

Hey @JoeyInvictus loving the new changes, just noticed that the new graph authentication changes required you to close the browser if already granted each time. This one will inhibit an unattended 'get-all' function in the future, if you get the auth right from the get go then run each module.

As much as I don't like the are you already logged in tests we had, and love the simplicity of V2, for these modules, I'm wondering if there is a way to check if consent has already been granted without it opening a new browser window? Might be a case of grabbing a single record to null in a try catch 😊

angry-bender commented 4 months ago

For context, i am using something like this just to try get some items unattended.

Connect-MgGraph -Scopes AuditLog.Read.All, AuditLogsQuery.Read.All, Directory.Read.All, Directory.AccessAsUser.All, Policy.Read.All, IdentityRiskEvent.Read.All,IdentityRiskyServicePrincipal.Read.All, IdentityRiskyUser.Read.All, User.Read.All, UserAuthenticationMethod.Read.All, User.ReadBasic.All > $null

....

try{
    Get-OAuthPermissions -OutputDir .\test
    ## Here is where the browser window comes up again
    Get-UALGraph -OutputDir .\test -searchName test
    ## And again here
    Get-ADSignInLogsGraph -OutputDir .\test
}
catch{
    Write-Host "ERROR: Log Acquisition failed" 
}
angry-bender commented 4 months ago

Hmmm,

Simplest solution could be to add a alreadyconsented argument to each of the growth modules, which bypasses the consent 😊, as it might be an edge case (without a Get-all script) to go this route

JoeyInvictus commented 4 months ago

Hi, I was just mentioning yesterday that it's been way too quiet since the 2.0 release. ;)

I've been considering removing the Connect-MgGraph part, but it might lead to more GitHub issues from people forgetting to connect before running the script. So, I'm not sure if that's what we want, haha.

Just to clarify, when using delegated permissions (not application permissions) and running commands like Get-MFA followed by Get-ADSignInLogsGraph, does your browser open each time, requiring you to close it again? I haven't encountered this issue, possibly because I have full admin consent for the environment. Do you have admin consent for your environment?

We could implement a check using Get-MgContext | Select-Object -ExpandProperty Scopes to verify if the necessary scopes are present. If they aren't, the script would run Connect-MgGraphwith the appropriate scopes.

JoeyInvictus commented 4 months ago

@angry-bender would like to hear your thoughts on the following:

I updated the Get-GraphAuthType function to not only return the type of authentication used but also the scopes of the current connection, if any.

function Get-GraphAuthType {
    $context = Get-MgContext
    $authType = $context | Select-Object -ExpandProperty AuthType
    $scopes = $context | Select-Object -ExpandProperty Scopes

    switch ($authType) {
        "AppOnly" { return @{ AuthType = "application"; Scopes = $scopes } }
        "Delegated" { return @{ AuthType = "delegated"; Scopes = $scopes } }
    }
}

In the scripts that uses Microsoft Graph, such as when calling Get-ConditionalAccessPolicies, I will check if the required scopes in this case, Policy.Read.All are included in the scopes returned by the Get-GraphAuthType function. If the required scope is present, the script will skip the reconnection step. If the scope is missing, the script will attempt to connect with the appropriate scope. If no connection is detected at all it will also try to connect with the appropiate scopes.

$graphAuth = Get-GraphAuthType
$requiredScopes = @("Policy.Read.All")
$joinedScopes = $requiredScopes -join ","

if ($graphAuth.AuthType -eq "delegated") {
    $missingScopes = @()
    foreach ($requiredScope in $requiredScopes) {
        if (-not ($graphAuth.Scopes -contains $requiredScope)) {
            $missingScopes += $requiredScope
            break
        }
    }

    if ($missingScopes.Count -gt 0) {
        foreach ($missingScope in $missingScopes) {
            Write-LogFile -Message "[INFO] Missing Graph scope detected: $missingScope" -Color "Yellow"
        }

        Write-LogFile -Message "[INFO] Attempting to re-authenticate with the appropriate scope(s): $joinedScopes" -Color "Green"
        Connect-MgGraph -NoWelcome -Scopes $joinedScopes > $null
    }
} 
elseif ($graphAuth.AuthType -eq "application") {
    Continue
}    
else {
    Write-LogFile -Message "[INFO] No active Connect-MgGraph session found. Attempting to connect with the appropriate scope(s): Connect-MgGraph -Scopes $joinedScopes" -Color "Red"
    Connect-MgGraph -Scopes $joinedScopes
}

Currently, I have added this code to the Get-ConditionalAccessPolicies function to experiment with the idea. If you find it useful, I will try to add this approach to all functions that use Graph. I kinda want to rework this code a little bit so that most of the logic is handled within the Get-GraphAuthType function, reducing the number of lines needed in each function that useswith Microsoft Graph. In addition, I should also verify if the required scope is included in the application's authentication and provide a warning if it is not.

JoeyInvictus commented 4 months ago

Did not test, but by adding the application part you will get something like this:

$graphAuth = Get-GraphAuthType
$requiredScopes = @("Policy.Read.All")
$joinedScopes = $requiredScopes -join ","

$missingScopes = @()
foreach ($requiredScope in $requiredScopes) {
    if (-not ($graphAuth.Scopes -contains $requiredScope)) {
        $missingScopes += $requiredScope
    }
}

if ($graphAuth.AuthType -eq "delegated") {
    if ($missingScopes.Count -gt 0) {
        foreach ($missingScope in $missingScopes) {
            Write-LogFile -Message "[INFO] Missing Graph scope detected: $missingScope" -Color "Yellow"
        }

        Write-LogFile -Message "[INFO] Attempting to re-authenticate with the appropriate scope(s): $joinedScopes" -Color "Green"
        Connect-MgGraph -NoWelcome -Scopes $joinedScopes > $null
    }
} elseif ($graphAuth.AuthType -eq "application") {
    if ($missingScopes.Count -gt 0) {
        foreach ($missingScope in $missingScopes) {
            Write-LogFile -Message "[INFO] The connected application is missing Graph scope detected: $missingScope" -Color "Red"
        }            
    }
} else {
    Write-LogFile -Message "[INFO] No active Connect-MgGraph session found. Attempting to connect with the appropriate scope(s): $joinedScopes" -Color "Red"
    Connect-MgGraph -NoWelcome -Scopes $joinedScopes
}
angry-bender commented 4 months ago

That looks awesome, thanks for that 😊

angry-bender commented 4 months ago

Hi, I was just mentioning yesterday that it's been way too quiet since the 2.0 release. ;)

I've been considering removing the Connect-MgGraph part, but it might lead to more GitHub issues from people forgetting to connect before running the script. So, I'm not sure if that's what we want, haha.

Just to clarify, when using delegated permissions (not application permissions) and running commands like Get-MFA followed by Get-ADSignInLogsGraph, does your browser open each time, requiring you to close it again? I haven't encountered this issue, possibly because I have full admin consent for the environment. Do you have admin consent for your environment?

We could implement a check using Get-MgContext | Select-Object -ExpandProperty Scopes to verify if the necessary scopes are present. If they aren't, the script would run Connect-MgGraphwith the appropriate scopes.

I think for the get all usecase making people consent first up, is going to be a good idea. But for the individual modules just a check and then sign in should work IMO

angry-bender commented 4 months ago

The proposed changes you have above, look like they would go awesome in a get all function 😊.

JoeyInvictus commented 4 months ago

Haha, the get all function sounds like so much pain to create😊

angry-bender commented 4 months ago

Haha, the get all function sounds like so much pain to create😊

I coded one internally in an hour or so, albeit it literally gets everything including the kitchen sink, no filters like uses or anything like that. Wasnt too painful 😊

For the legacy commandlets it's working well, but this is why I raised the issues with the graph side, as when I tried it using that method it all broke in a horrific way 🀣

JoeyInvictus commented 4 months ago

Haha, maybe I'm just trying to find an excuse to avoid spending my free time creating a get-all function 🀣. How do you handle the "run all" process internally? Do you execute tasks one by one, or do you have a sequence where certain scripts run behind each other? Considering that the UAL takes the longest, do you save it for last, or do you run it in parallel with the other collections?

We utilize Azure Functions internally, with each function dedicated to collecting a specific log source. These functions are triggered simultaneously using an Azure Logic App workflow, and the output is written to a Blob Storage Container. Azure Data Factory automatically processes the data and loads the results into our SIEM, where automatically custom detection rules are ran against the data.

angry-bender commented 4 months ago

Haha, maybe I'm just trying to find an excuse to avoid spending my free time creating a get-all function 🀣. How do you handle the "run all" process internally? Do you execute tasks one by one, or do you have a sequence where certain scripts run behind each other? Considering that the UAL takes the longest, do you save it for last, or do you run it in parallel with the other collections?

We utilize Azure Functions internally, with each function dedicated to collecting a specific log source. These functions are triggered simultaneously using an Azure Logic App workflow, and the output is written to a Blob Storage Container. Azure Data Factory automatically processes the data and loads the results into our SIEM, where automatically custom detection rules are ran against the data.

Ha ha, so was I until I needed it to avoid hovering over the acquisition process late at night 🀣.

Basically I go

Check dependent packages Request auth Collect all logs (UAL Last) Collect all environmental details

If I have a known user, I just change the get-ual function down to a single user, as your right the whole UAL does take quite some time (1/2 day or more).

I also try catch the dependent packages and auth as a exit on fail. Else for logs I also have a try catch, but it just errors and continues.

I want to use a Azure function so badly, but I haven't been able to figure out how to get authentication cross tenancies / non-interactivley. Hence the get all from a disposable VM.

JoeyInvictus commented 4 months ago

Nice, that sounds like a solid workflow! We experimenting using a multi-tenant application created in our tenant that we register in the client environment. We request the appropriate Graph scopes and, if necessary, add some roles using an ARM template. This allows us to use the application in the client environment to execute regular PowerShell cmdlets and Graph API calls like we do in the Extractor Suite.

angry-bender commented 4 months ago

Nice, that sounds like a solid workflow! We experimenting using a multi-tenant application created in our tenant that we register in the client environment. We request the appropriate Graph scopes and, if necessary, add some roles using an ARM template. This allows us to use the application in the client environment to execute regular PowerShell cmdlets and Graph API calls like we do in the Extractor Suite.

That's exactly how I was thinking of doing a more mature version too, just been a case of not having the time to figure out the nuts and bolts of the how, but it's good to know it should work 😊

JoeyInvictus commented 3 months ago

Hi, I fixed this in the new update. Feel free to open a new issue if anything is not working. The fix should prevent you from closing the browser if you already constend.

How it works:

For example, in the Get-RiskyUsers function, we use the following two lines:

$requiredScopes = @("IdentityRiskEvent.Read.All","IdentityRiskyServicePrincipal.Read.All","IdentityRiskyUser.Read.All")
$graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes

The variable $requiredScopes is an array that contains the necessary permissions needed to access information via Microsoft Graph. In this case, the scopes are:

IdentityRiskEvent.Read.All
IdentityRiskyServicePrincipal.Read.All
IdentityRiskyUser.Read.All

The function Get-GraphAuthType is then called with the required scopes.

It retrieves the current Graph context using Get-MgContext. If no context is found, it sets the authentication type to "none". It checks the current context to see if the required scopes are already present. If any required scopes are missing, they are added to the $missingScopes array.

Depending on the authentication type:

Full function code:

function Get-GraphAuthType {
    param (
        [string[]]$RequiredScopes
    )

    $context = Get-MgContext
    if (-not $context) {
        $authType = "none"
        $scopes = @()
    } else {
        $authType = $context | Select-Object -ExpandProperty AuthType
        $scopes = $context | Select-Object -ExpandProperty Scopes
    }

    $missingScopes = @()
    foreach ($requiredScope in $RequiredScopes) {
        if (-not ($scopes -contains $requiredScope)) {
            $missingScopes += $requiredScope
        }
    }

    $joinedScopes = $RequiredScopes -join ","
    switch ($authType) {
        "delegated" {
            if ($missingScopes.Count -gt 0) {
                foreach ($missingScope in $missingScopes) {
                    Write-LogFile -Message "[INFO] Missing Graph scope detected: $missingScope" -Color "Yellow"
                }

                Write-LogFile -Message "[INFO] Attempting to re-authenticate with the appropriate scope(s): $joinedScopes" -Color "Green"
                Connect-MgGraph -NoWelcome -Scopes $joinedScopes > $null
            }
        }
        "application" {
            if ($missingScopes.Count -gt 0) {
                foreach ($missingScope in $missingScopes) {
                    Write-LogFile -Message "[INFO] The connected application is missing Graph scope detected: $missingScope" -Color "Red"
                }
            }
        }
        "none" {
            Write-LogFile -Message "[INFO] No active Connect-MgGraph session found. Attempting to connect with the appropriate scope(s): $joinedScopes" -Color "Green"
            Connect-MgGraph -NoWelcome -Scopes $joinedScopes
        }
    }

    return @{
        AuthType = $authType
        Scopes = $scopes
        MissingScopes = $missingScopes
    }
}