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

Get-SignInLogsGraph - Date Type Issue #76

Closed angry-bender closed 3 months ago

angry-bender commented 4 months ago

Hey team, Sorry to be the crusher here,

Looks like the PowerShell date type issue is back again "createdDateTime":"\/Date(17000000000000)\/" , i know i've fixed this one a couple of times now (as it requires capturing the object and converting back), but v2.0 as it back again.

This will cause issues for those trying to push this JSON staight into a log aggregation platform, unless there is pre-parsing there to regex out the date and convert from what i think is miliseconds (Epoch Time). This is from the powershell conversion itself, and not the GraphAPI (Which produces a proper date type)

angry-bender commented 4 months ago

Also seems to be the case in Get-ADAuditLogsGraph

angry-bender commented 4 months ago

^^ Above menntion

$response = Invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType 'application/json' if ($response.value) { $date = [datetime]::Now.ToString('yyyyMMddHHmmss') $filePath = Join-Path -Path $OutputDir -ChildPath "$($date)-AuditLogs.json" $response.value | **ConvertTo-Json -Depth 100**| Out-File -FilePath $filePath -Append -Encoding $Encoding Write-LogFile -Message "[INFO] Audit logs written to $filePath" -ForegroundColor Green

Looks to me like we could avoid this issue, if we removed this field, as it should already be in JSON

angry-bender commented 4 months ago

Tricky issue to deal with, as its universally fairly well known how bad powershell is with date types for example - https://stackoverflow.com/questions/26067906/format-a-datetime-in-powershell-to-json-as-date1411704000000

I've been playing around with the request, but it may well be the case its better to add in the -OutputFilePath test1.json flag, as it seems to output the raw JSON. unfrotunatley if you do this in the $response variable, it wont keep the @odata.nextLink function, so you may need to do two requests something like this... (Tested and working for me, but may want customisation for this project)

        $date = [datetime]::Now.ToString('yyyyMMddHHmmss')
        $inc = 1 
        $apiUrl = "https://graph.microsoft.com/beta/security/auditLog/queries/$scanId/records"
        $ProgressPreference = 'SilentlyContinue'

        Do {
            $outputFilePath = "$($date)-$searchName-UnifiedAuditLog-$($inc).json"
            $filePath = Join-Path -Path $OutputDir -ChildPath $outputFilePath
            $response = invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType 'application/json' -OutputFilePath $filepath -PassThru
            $apiUrl = $response.'@odata.nextLink'
            $inc ++
        } While ($apiUrl)
JoeyInvictus commented 4 months ago

Yeah, this time issue has been going on for quite a while now. I feel like there are two camps on this: one that is not touching the output given by MS, and one modifying the output to change the date to a readable format. It might be best to add a parameter allowing both camps to be happy, haha. But playing with the date stuff remains a pain in the ass.

angry-bender commented 4 months ago

Yeah, this time issue has been going on for quite a while now. I feel like there are two camps on this: one that is not touching the output given by MS, and one modifying the output to change the date to a readable format. It might be best to add a parameter allowing both camps to be happy, haha. But playing with the date stuff remains a pain in the ass.

Problem is it isn't the raw output from Microsoft itself 😞, it's PowerShell stupidity 🤣, I don't know why PowerShell changes the type at all 😞. I'd rather leave it intact from the API, but it's the question of how without having to change to something like python.

JoeyInvictus commented 4 months ago

Yeah, you're right. The ConvertTo-Json cmdlet seems to mess it all up.

JoeyInvictus commented 4 months ago

We could implement a solution like this, but I feel dirty about it due to the potential performance impact. This approach needs extra processing to iterate over the results and modify an object, which could slow things down. However, can make it as a parameter such as -ReadableDate. What are your thoughts on this approach?

foreach ($data in $response.value) {
    $psObject = [PSCustomObject]@{
        appliedConditionalAccessPolicies = $data.appliedConditionalAccessPolicies
        isInteractive = $data.isInteractive
        location = $data.location
        conditionalAccessStatus = $data.conditionalAccessStatus
        resourceDisplayName = $data.resourceDisplayName
        userPrincipalName = $data.userPrincipalName
        riskLevelAggregated = $data.riskLevelAggregated
        appId = $data.appId
        deviceDetail = $data.deviceDetail
        riskDetail = $data.riskDetail
        riskState = $data.riskState
        status = $data.status
        ipAddress = $data.ipAddress
        userId = $data.userId
        id = $data.id
        resourceId = $data.resourceId
        appDisplayName = $data.appDisplayName
        clientAppUsed = $data.clientAppUsed
        correlationId = $data.correlationId
        riskEventTypes_v2 = $data.riskEventTypes_v2
        riskEventTypes = $data.riskEventTypes
        createdDateTime = $data.createdDateTime.ToString('yyyy-MM-ddTHH:mm:ss')
        riskLevelDuringSignIn = $data.riskLevelDuringSignIn
        userDisplayName = $data.userDisplayName
    }
    $modifiedEvents += $psObject
}

$modifiedEvents | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Encoding $Encoding
angry-bender commented 4 months ago

We could implement a solution like this, but I feel dirty about it due to the potential performance impact. This approach needs extra processing to iterate over the results and modify an object, which could slow things down. However, can make it as a parameter such as -ReadableDate. What are your thoughts on this approach?

foreach ($data in $response.value) {
  $psObject = [PSCustomObject]@{
      appliedConditionalAccessPolicies = $data.appliedConditionalAccessPolicies
      isInteractive = $data.isInteractive
      location = $data.location
      conditionalAccessStatus = $data.conditionalAccessStatus
      resourceDisplayName = $data.resourceDisplayName
      userPrincipalName = $data.userPrincipalName
      riskLevelAggregated = $data.riskLevelAggregated
      appId = $data.appId
      deviceDetail = $data.deviceDetail
      riskDetail = $data.riskDetail
      riskState = $data.riskState
      status = $data.status
      ipAddress = $data.ipAddress
      userId = $data.userId
      id = $data.id
      resourceId = $data.resourceId
      appDisplayName = $data.appDisplayName
      clientAppUsed = $data.clientAppUsed
      correlationId = $data.correlationId
      riskEventTypes_v2 = $data.riskEventTypes_v2
      riskEventTypes = $data.riskEventTypes
      createdDateTime = $data.createdDateTime.ToString('yyyy-MM-ddTHH:mm:ss')
      riskLevelDuringSignIn = $data.riskLevelDuringSignIn
      userDisplayName = $data.userDisplayName
  }
  $modifiedEvents += $psObject
}

$modifiedEvents | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Encoding $Encoding

I'm all for it in terms of making it work, but, my concern is that it would need to be maintained as Microsoft changes the schema.

I'm also, not keen on increasing the time, if anything, it like to figure out how asynchronous operations work in PowerShell to speed things up 😋.

I'll do some testing next week and see if removing the types from PowerShell with | Remove-TypeData either before or after the convert to JSON function works, just need to find some.time to play with a request in a variable between investigations 😊

Calvindd2f commented 4 months ago

Doesn't occur in PS7 because of below, not advocating for PS7 usage in module, as their are other issues with it, but it gave me something to go off in terms of a fix.

As of PowerShell 7.2, Extended Type System properties of DateTime and String objects are no longer serialized and only the simple object is converted to JSON format

Tested in PS5 with below results.

PS C:\Users\c> $PSVersionTable.PSVersion|select Major,Minor
Major Minor
----- -----
    5     1
PS C:\Users\c> Get-Date | ConvertTo-Json; Get-Date | ConvertTo-Json
<#
{
    "value":  "\/Date(1720919582928)\/",
    "DisplayHint":  2,
    "DateTime":  "Sunday 14 July 2024 02:13:02"
}
{
    "value":  "\/Date(1720919582929)\/",
    "DisplayHint":  2,
    "DateTime":  "Sunday 14 July 2024 02:13:02"
}
#>
PS C:\Users\c> [datetime]::Now.ToString('o') | ConvertTo-Json; [datetime]::Now.ToString('o') | ConvertTo-Json
<#
"2024-07-14T02:13:19.7728125+01:00"
"2024-07-14T02:13:19.7743166+01:00"
#>
PS C:\Users\c> [datetime]::Now.ToString('s') | ConvertTo-Json; [datetime]::Now.ToString('s') | ConvertTo-Json
<#
"2024-07-14T02:13:30"
"2024-07-14T02:13:30"
#>
PS C:\Users\c>

I am not sure where in the scripts that the Get-Date is called for this issue to take place but replacing it with .NET method should sort it out.

For contrast here are tests directly from PS7.4

┌──(c㉿CALVIN)-[C:\Users\c]
└─PS> $PSVersionTable.PSVersion|select Major,Minor
<#
Major Minor
----- -----
    7     4
#>
┌──(c㉿CALVIN)-[C:\Users\c]
└─PS> Get-Date | ConvertTo-Json; Get-Date | ConvertTo-Json
<#
"2024-07-14T02:19:36.015787+01:00"
"2024-07-14T02:19:36.0161707+01:00"
#>

┌──(c㉿CALVIN)-[C:\Users\c]
└─PS> [datetime]::Now.ToString('o') | ConvertTo-Json; [datetime]::Now.ToString('o') | ConvertTo-Json
<#
"2024-07-14T02:19:46.7870263+01:00"
"2024-07-14T02:19:46.7906365+01:00"
#>

┌──(c㉿CALVIN)-[C:\Users\c]
└─PS> [datetime]::Now.ToString('s') | ConvertTo-Json; [datetime]::Now.ToString('s') | ConvertTo-Json
<#
"2024-07-14T02:19:47"
"2024-07-14T02:19:47"
#>

I'll try find the instances where the Get-Date is called, fingers crossed it is simple swapping out Cmdlet for the .NET method.

Ref:

angry-bender commented 4 months ago

Hmm, 7 also has better support for Async operations, but I agree in not advocating for it as it adds more complexity to an install for an end user. It's nice to see it comes out in proper ISO time

I think v5 is very heavy in .net usage of objects whereas 7 does things more like python (probably so it works on Linux)

Calvindd2f commented 4 months ago

You're correct. Personally, I use PS7 everywhere I go, but I know that means I’m often on my own when using modules intended for PS5.1. The changes in 7.4 are quite visible, especially when writing modules/scripts - in terms of speed. but it makes sense given that v5.1 runs off .NETFramewrk4.5, while v7.5 is built on .NET9

If async is really what you want, inline C# (if you’re lazy) or binary cmdlets are the way to go. Binary modules are about as fast as PowerShell gets. You can also use runspaces, although they’re typically better handled from a different language. That said, it’s still possible to do it in PowerShell.

In Mac/Linux, 7 uses P/Invoke to invoke C lib, which is the glue.

JoeyInvictus commented 4 months ago

Thanks for the replies, both of you! @Calvindd2f I don't think Get-Date is the problem for the Get-SignInLogsGraph.

Getting the results:

$apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=$encodedFilterQuery"
$response = Invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType 'application/json'
write-output $response.value | Select-Object -First 1

When printing the output of $response.value It still shows the "normal" date format.

image

However, after this, the output is written to a JSON file using the following: $response.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding

When testing and running: write-output $response.value | Select-Object -First 1 | ConvertTo-Json -Depth 100

It shows the date in the epoch format. image

So I am not sure how we can fix this without bypassing the ConvertTo-Json command or creating a custom object.

JoeyInvictus commented 4 months ago

I will add the switch parameter ReadableDate in the next update. When specifying this parameter, a custom Powershell object will be created with the date stored in a readable format. This allows users to either choose to keep the JSON date or convert it into a readable one, which may cause the script to take longer to run.

angry-bender commented 4 months ago

Thanks for the replies, both of you! @Calvindd2f I don't think Get-Date is the problem for the Get-SignInLogsGraph.

Getting the results:

$apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=$encodedFilterQuery"
$response = Invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType 'application/json'
write-output $response.value | Select-Object -First 1

When printing the output of $response.value It still shows the "normal" date format.

image

However, after this, the output is written to a JSON file using the following: $response.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding

When testing and running: write-output $response.value | Select-Object -First 1 | ConvertTo-Json -Depth 100

It shows the date in the epoch format. image

So I am not sure how we can fix this without bypassing the ConvertTo-Json command or creating a custom object.

@JoeyInvictus , sorry I haven't had the chance yet, but can you try remove-typedata in the same tests you did above

$response.value | remove-typedata | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding

JoeyInvictus commented 4 months ago

I didn't experiment too much with it, but the example you provided results in an error:

remove-typedata : Error in TypeData "System.Collections.Hashtable": The type "System.Collections.Hashtable" was not found. The type name value must be the full name of the type. Verify the type name and run the command again.

I tried it like this, but it didn't work as well:

Remove-TypeData -TypeName System.DateTime
$response.value | ConvertTo-Json -Depth 100 | Out-File test.json
angry-bender commented 4 months ago

Damn it, I'll keep digging to see if I can find a solution, that's super annoying

Calvindd2f commented 4 months ago

Thanks for the detailed information, I was meant to respond earlier but work I guess.

It took some time to replicate issue as I initially tried to replicate with Invoke-RestMethod for some reason (returns the correct format for DateTime)

I played with some inline C# and other ways to try fit in post-processing [after serialization] or pre-processing [before serialization] but it was more of a struggle session than anything.

I opened the Microsoft.Graph.Authentication.dll file in dnSpy and took a look at some of the namespaces/methods

// Microsoft.Graph.PowerShell.Authentication.Common.GraphSettingsConverter.JsonConverter.WriteJson
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    serializer.Serialize(writer, value);
}

Above is what is causing the problem , I am not sure if it is because of the version of Newtonsoft.Json used by PS5.1 or something similar but the issue stopped occurring with PS7.2 because they just made the extended type system in powershell read the JSONs type [System.DateTime] to a string like sane people.

Anyways here is the fix, it appears to be one of these headers or the content-type but, I do not know which. The ConvertTo-Json requires ConvertFrom-Json before being piped as the outputtype of invoke-mggraphrequest is already json (it throws error otherwise)

# For testing with Invoke-RestMethod
$token=Get-Token -scope https://graph.microsoft.com/.default
# For testing with Delegated
Connect-MgGraph -Scopes "auditlog.read.all" -DeviceCode

$Headers=@{};
$Headers["X-ResponseFormat"] = "json"
$ContentType=  "application/json; odata.metadata=minimal; odata.streaming=true;"
$Method='Get'
$Headers["x-serializationlevel"] = "Partial"
$Headers["Authorization"] = "Bearer $($token)"
 $Headers["X-prefer"] = "odata.maxpagesize=1000"
$Headers["X-ResponseFormat"] = "json" ## Can also be "clixml"
$Headers["accept-charset"] = "UTF-8"
#$SendChunked = $true;
#$TransferEncoding = "gzip"
$UserAgent = "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-IE) WindowsPowerShell/5.1.19041.1682"
$Accept = "application/json"
$Timeout = $(30*1000)
$uri="https://graph.microsoft.com/beta/users/c@lvin.ie"

# Bread and butter
$response=Invoke-MgGraphRequest -Uri $uri -Method $Method -Headers $Headers -ContentType $ContentType -UserAgent $UserAgent -OutputType Json

No clue what in here got it to want to play ball but just to make sure I was not doing some API stuff in the background I nulled the Authorization header to ensure I was using Invoke-MgGraphRequet over delegated authentication.

# Same Session and variables as above
$Headers.Remove("Authorization")
$response=Invoke-MgGraphRequest -Uri $uri -Method $Method -Headers $Headers -ContentType $ContentType -UserAgent $UserAgent -OutputType Json

$fix=$response|ConvertFrom-Json|ConvertTo-Json -Depth 15
<#-----------[StdOut from Console contains dates not in Epoch]-----------#>

$fix >> 'Jason.json'
cat 'Jason.json'
<#-----------[StdOut from Console contains dates not in Epoch]-----------#>

Finally - testing it for the specific API endpoint discussed in this issue

$apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=UserId eq 'c@lvin.ie'"
$response=Invoke-MgGraphRequest -Uri $uri -Method $Method -Headers $Headers -ContentType $ContentType -UserAgent $UserAgent -OutputType Json
$response #print response to see if replied in the weird way that makes it work
$winner=$response|ConvertFrom-Json|ConvertTo-Json -Depth 15
$winner > 'GoAwayEpoch.json'

"createdDateTime": "2022-06-29T17:55:59Z",
"refreshTokensValidFromDateTime": "2023-09-01T17:48:55Z",
"signInSessionsValidFromDateTime": "2023-09-01T17:48:55Z",
"assignedDateTime": "2024-01-25T07:24:34Z",

Final

# 5.1 - already connected to delegate graph
$UserIds="c@lvin.ie"
$encodedFilterQuery=$null;$apiUrl=$null;$filterQuery=$null
[string]$endDate=(get-date).ToString('yyyy-MM-ddTHH:mm:ssZ')
[string]$startDate=(get-date).AddDays(-7).ToString('yyyy-MM-ddTHH:mm:ssZ')
$filterQuery = "createdDateTime ge $StartDate and createdDateTime le $EndDate"
if ($UserIds) {$filterQuery += " and startsWith(initiatedBy/user/userPrincipalName, '$UserIds')"}
$encodedFilterQuery = [System.Web.HttpUtility]::UrlEncode($filterQuery)
$apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=$encodedFilterQuery"
# https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=createdDateTime+ge+2024-07-08T21%3a09%3a03Z+and+createdDateTime+le+2024-07-15T21%3a08%3a53Z+and+startsWith(userPrincipalName%2c+%27c%40lvin.ie%27)

<#----[Test]----#>
$response = Invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType 'application/json'
$response
    #Name                           Value
    #----                           -----
    #@odata.context                 https://graph.microsoft.com/v1.0/$metadata#auditLogs/signIns
    #value                          {System.Collections.Hashtable, System.Collections.Hashtable...}
<#----[Test]----#>

This part appears to be the fix

<#----[Fix is somewhere in the headers]----#>
$Headers=@{};
$Headers["X-ResponseFormat"] = "json"
$ContentType=  "application/json; odata.metadata=minimal; odata.streaming=true;"
$Headers["x-serializationlevel"] = "Partial"
$Headers["X-prefer"] = "odata.maxpagesize=1000"
$Headers["X-ResponseFormat"] = "json"
$Headers["accept-charset"] = "UTF-8"
$Accept = "application/json"
$response = Invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType $ContentType -Headers $Headers -OutputType Json
<#----[Fix is somewhere in the headers]----#>

<#-----[Print of `$response is a long jsonstring]-----#>
$response|ConvertFrom-Json|ConvertTo-Json -Depth 15

<#-------[OUTPUT]-------#>
<# Half-assed sanitization , it's a tenant and there is nothing interesting anyways. Here is the output.#>
<#
{
    "@odata.context":  "https://graph.microsoft.com/v1.0/$metadata#auditLogs/signIns",
    "value":  [
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-15T11:08:47Z",
                      "userDisplayName":  "",
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  "",
                      "appId":  "",
                      "appDisplayName":  "",
                      "ipAddress":  "",
                      "clientAppUsed":  "",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "",
                      "isInteractive":  ,
                      "riskDetail":  "",
                      "riskLevelAggregated":  "",
                      "riskLevelDuringSignIn":  "",
                      "riskState":  "",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  "",
                      "resourceId":  "",
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  "",
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  "",
                                       "state":  "",
                                       "countryOrRegion":  "",
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  ,
                                                              "longitude":  -
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-15T11:08:40Z",
                      "userDisplayName":  "",
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  "",
                      "appDisplayName":  null,
                      "ipAddress":  null,
                      "clientAppUsed":  null,
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  null,
                      "riskDetail":  null,
                      "riskLevelAggregated":  null,
                      "riskLevelDuringSignIn":  null,
                      "riskState":  null,
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName": null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  null,
                                     "failureReason":  "The user or administrator has not consented to use the application with ID \u0027{identifier}\u0027{namePhrase}. Send an interactive authorization request for this user and resource.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  -null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-15T10:44:38Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName":  "Microsoft Graph Command Line Tools",
                      "ipAddress": null,
                      "clientAppUsed":  "Mobile Apps and Desktop clients",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName": null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  -null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-15T10:44:34Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId": null,
                      "appDisplayName":  "Microsoft Graph Command Line Tools",
                      "ipAddress": null,
                      "clientAppUsed":  "Mobile Apps and Desktop clients",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName": null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  50199,
                                     "failureReason":  "For security reasons, user confirmation is required for this request. Please repeat the request allowing user interaction.",
                                     "additionalDetails":  "MFA completed in Azure AD"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  -null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-12T19:20:43Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-12T19:13:08Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,556189a6-fcff-4923-8e25-f24e2a191d00",
                      "createdDateTime":  "2024-07-12T19:10:38Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-12T18:49:50Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId": null,
                      "appDisplayName":  "Microsoft Graph Command Line Tools",
                      "ipAddress":  null,
                      "clientAppUsed":  "Mobile Apps and Desktop clients",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  "Windows Azure Active Directory",
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement skipped due to remembered device"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-12T18:49:42Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId": null,
                      "appDisplayName":  "Microsoft Graph Command Line Tools",
                      "ipAddress":  null,
                      "clientAppUsed":  "Mobile Apps and Desktop clients",
                      "correlationId":  "4005e098-7d3e-444a-a89e-cd5c4b926761",
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  "Windows Azure Active Directory",
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  65001,
                                     "failureReason":  "The user or administrator has not consented to use the application with ID \u0027{identifier}\u0027{namePhrase}. Send an interactive authorization request for this user and resource.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,3cf58afe-4709-46e8-8003-284ddcc81b00",
                      "createdDateTime":  "2024-07-12T18:49:28Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  "e80c10c9-fba8-4bf9-8ded-1a8f66ee5f67",
                      "appDisplayName":  "PiaDocumentation",
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  "ac5b4937-792a-426a-bab6-acnull49",
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName": null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  65004,
                                     "failureReason":  "User declined to consent to access the app.",
                                     "additionalDetails":  "Have the user retry the sign-in and consent to the app."
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,9d450928-11a9-4b81-8c41-80cfe9301700",
                      "createdDateTime":  "2024-07-12T18:48:56Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  "e80c10c9-fba8-4bf9-8ded-1a8f66ee5f67",
                      "appDisplayName":  "PiaDocumentation",
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName": null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  65001,
                                     "failureReason":  "The user or administrator has not consented to use the application with ID \u0027{identifier}\u0027{namePhrase}. Send an interactive authorization request for this user and resource.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       "countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-12T18:36:45Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,d86ba33d-cda8-4519-84da-c6480c4d1b00",
                      "createdDateTime":  "2024-07-12T18:34:19Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement skipped due to remembered device"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,f59d0ff1-3d75-4dfb-a502-2936a9af1700",
                      "createdDateTime":  "2024-07-12T18:34:14Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress":  null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  50126,
                                     "failureReason":  "Error validating credentials due to invalid username or password.",
                                     "additionalDetails":  "The user didn\u0027t enter the right credentials.  It\u0027s expected to see some number of these errors in your logs due to users making mistakes."
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  },
                  {
                      "id":  null,
                      "createdDateTime":  "2024-07-09T14:55:47Z",
                      "userDisplayName": null,
                      "userPrincipalName":  "c@lvin.ie",
                      "userId":  null,
                      "appId":  null,
                      "appDisplayName": null,,
                      "ipAddress": null,
                      "clientAppUsed":  "Browser",
                      "correlationId":  null,
                      "conditionalAccessStatus":  "notApplied",
                      "isInteractive":  true,
                      "riskDetail":  "none",
                      "riskLevelAggregated":  "none",
                      "riskLevelDuringSignIn":  "none",
                      "riskState":  "none",
                      "riskEventTypes":  [

                                         ],
                      "riskEventTypes_v2":  [

                                            ],
                      "resourceDisplayName":  null,
                      "resourceId":  null,
                      "status":  {
                                     "errorCode":  0,
                                     "failureReason":  "Other.",
                                     "additionalDetails":  "MFA requirement satisfied by claim in the token"
                                 },
                      "deviceDetail":  {
                                           "deviceId":  "",
                                           "displayName":  "",
                                           "operatingSystem":  null,
                                           "browser":  null,
                                           "isCompliant":  false,
                                           "isManaged":  false,
                                           "trustType":  ""
                                       },
                      "location":  {
                                       "city":  null,
                                       "state":  null,
                                       countryOrRegion":   null,
                                       "geoCoordinates":  {
                                                              "altitude":  null,
                                                              "latitude":  null,
                                                              "longitude":  null
                                                          }
                                   },
                      "appliedConditionalAccessPolicies":  [

                                                           ]
                  }
              ]
}
#>

Writing to file works. I've omitted it for brevity.

I'll try get a POC on the go but it should be fairly simple.

Something like:

<#-----[somewhere in the middle of the function]-----#>
$Headers=@{
    "X-ResponseFormat"= "json"
    "X-serializationlevel" = "Partial"
    "X-prefer" = "odata.maxpagesize=1000"
    "X-ResponseFormat" = "json"
    "Accept-charset"] = "UTF-8"
};
$ContentType=  "application/json; odata.metadata=minimal; odata.streaming=true;"
<#------[Skip to where Invoke-MgGraphRequest is called]------#>
$response = Invoke-MgGraphRequest -Method Get -Uri $apiUrl -ContentType $ContentType -Headers $Headers -OutputType Json
<#------[When response is being redirected to file, include the ConvertFrom-Json then pipe the ConvertTo-Json]------#>
$response|ConvertFrom-Json|ConvertTo-Json -Depth 15
JoeyInvictus commented 4 months ago

Dam, that's amazing! Thanks again for the detailed explanation and the work. I managed to get it working with the following:

PS C:\Users\Joey-IR> $apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?"
PS C:\Users\Joey-IR> $response = Invoke-MgGraphRequest -Uri $apiUrL -Method Get -ContentType "application/json; odata.metadata=minimal; odata.streaming=true;" -OutputType Json
PS C:\Users\Joey-IR> $fix=$response|ConvertFrom-Json|ConvertTo-Json -Depth 15

I'll add this as a default to the Audit/Signin/UAL logs so that we get a human readable format.

JoeyInvictus commented 4 months ago

I got it working for the Sign-in part with the following code. I first do ConvertFrom-Json on the response. If I do ConvertTo-Json directly after, I am unable to parse out the value and nextLink fields. So now, I get those first and then ConvertTo-Json, and I get the dates in the correct format.

$filterQuery = "createdDateTime ge $StartDate and createdDateTime le $EndDate"
if ($UserIds) {
    $filterQuery += " and startsWith(userPrincipalName, '$UserIds')"
}

$encodedFilterQuery = [System.Web.HttpUtility]::UrlEncode($filterQuery)
$apiUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=$encodedFilterQuery"

try {
    Do {
        $response = Invoke-MgGraphRequest -Uri $apiUrL -Method Get -ContentType "application/json; odata.metadata=minimal; odata.streaming=true;" -OutputType Json
        $responseJson = $response | ConvertFrom-Json 

        if ($responseJson.value) {
            $date = [datetime]::Now.ToString('yyyyMMddHHmmss') 
                       $filePath = Join-Path -Path $OutputDir -ChildPath "$($date)-SignInLogsGraph.json"

            $responseJson.value | ConvertTo-Json -Depth 100 | Out-File -FilePath $filePath -Append -Encoding $Encoding      
                        Write-LogFile -Message "[INFO] Sign-in logs written to $filePath" -ForegroundColor Green
        }
        $apiUrl = $responseJson.'@odata.nextLink'
    } While ($apiUrl)
}

image

JoeyInvictus commented 3 months ago

Hi,

This issue with the Graph sign-in and audit logs functionalities should now be fixed, as shown in the examples above (thanks @Calvindd2f ). If it still doesn't work properly, please let me know!