Closed angry-bender closed 3 months ago
Also seems to be the case in Get-ADAuditLogsGraph
^^ 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
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)
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.
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.
Yeah, you're right. The ConvertTo-Json
cmdlet seems to mess it all up.
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
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 😊
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:
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)
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.
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.
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.
So I am not sure how we can fix this without bypassing the ConvertTo-Json
command or creating a custom object.
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.
Thanks for the replies, both of you! @Calvindd2f I don't think
Get-Date
is the problem for theGet-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.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.
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
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
Damn it, I'll keep digging to see if I can find a solution, that's super annoying
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",
# 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]----#>
<#----[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
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.
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)
}
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!
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)