O365_Unified_Auditlog_parser.ps1


<#PSScriptInfo
 
.VERSION 1.3
 
.GUID 2082a2b3-155c-42e5-b3f6-c67434a0a3e1
 
.AUTHOR mikko@lavento.com
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS O365 UnifiedAuditlog Auditlog parser
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
Added one columnheader to result-csv table: TargetUserOrGroupType
 
#>
 

















<#
 
.DESCRIPTION
  
O365 auditlog (Unified log) parser.
 
Applies to logs downloaded from https://protection.office.com - Search & investigation - Audit log search - Download - .csv
 
Downloaded log has 4 colums:
CreationDate | UserIds | Operations | Auditdata
Problem: the most important one (Auditdata) is string mess where data is delimited with ; , and [] and you can't really import it to excel to filter reasonably for examing.
Also problem: different services log more or less data so no fixed amount of columns
 
This parser will modify the Auditdata column, creates a table and exports the parsered csv file (to be imported to excel).
More comments inside the script.
 
 
#>
 

Param()


# 29.5.2018 M.Lavento
# O365 auditlog (Unified log) parser
# Applies to logs downloaded from https://protection.office.com - Search & investigation - Audit log search - Download .csv
#

# Downloaded log has 4 colums
# CreationDate | UserIds | Operations | Auditdata
# Problem: the most important one (Auditdata) is "string mess" where data is delimited with ; , and [] and you can't really import it to excel to filter reasonably.
# Also problem: different services log more or less data so no fixed amount of columns

# Solution: delimiter to be used while importing to Excel seems to be "," BUT NOT INSIDE brackets [{ }] so we change all "," --> �� as delimiter inside brackets []
# Delimiter char changes atleast for me to �� --> ??, but it doesn't really matter
# Then we can split and construct a table in a format where it is csv exportable


# But first Columns for the result table#
# Bad thing is that cloumns has to be known beforehand.
# Used Microsoft's own info what they might be and I added atleast five extra ones what came up while testing
# https://support.office.com/en-us/article/detailed-properties-in-the-office-365-audit-log-ce004100-9e7f-443e-942b-9b04098fcfc3

# But because it's Microsoft and there might be more in the future or misinterpretations in the code we have to create the headers.

# I created column name Miscellaneous as the last column

$columnheaders = `
"CreationDate",`
"UserIds",`
"Operations",`
"Actor",`
"ActorContextId",`
"ActorIpAddress",`
"AddOnName",`
"AddOnType",`
"ApplicationDisplayName",`
"ApplicationId",`
"AzureActiveDirectoryEventType",`
"ChannelGuid",`
"ChannelName",`
"Client",`
"ClientInfoString",`
"ClientIP",`
"ClientIPAddress",`
"CorrelationId",`
"CreationTime",`
"CustomUniqueId",`
"DestinationFileExtension",`
"DestinationFileName",`
"DestinationRelativeUrl",`
"EventData",`
"EventSource",`
"ExternalAccess",`
"ExtendedProperties",`
"ID",`
"InternalLogonType",`
"InterSystemsId",`
"IntraSystemId",`
"ItemType",`
"ListId",`
"ListItemUniqueId",`
"LoginStatus",`
"LogonError",`
"LogonType",`
"MailboxGuid",`
"MailboxOwnerUPN",`
"Members",`
"ModifiedProperties (Name, NewValue, OldValue)",`
"ObjectID",`
"Operation",`
"OrganizationID",`
"OrganizationName",`
"OriginatingServer",`
"Path",`
"Parameters",`
"RecordType",`
"ResultStatus",`
"SecurityComplianceCenterEventType",`
"SharingType",`
"Site",`
"SiteUrl",`
"SourceFileExtension",`
"SourceFileName",`
"SourceRelativeUrl",`
"Subject",`
"TabType",`
"Target",`
"TargetContextId",`
"TargetUserOrGroupName",`
"TargetUserOrGroupType",`
"TeamGuid",`
"TeamName",`
"UniqueSharingId",`
"UserAgent",`
"UserDomain",`
"UserID",`
"UserKey",`
"UserSharedWith",`
"UserType",`
"Version",`
"WebId",`
"Workload",`
"Miscellaneous"

############# Main script

#Open dialog by Dan Stolts
$openFileDialog = New-Object windows.forms.openfiledialog   
$openFileDialog.initialDirectory = [System.IO.Directory]::GetCurrentDirectory()   
$openFileDialog.title = "Select Log File to be parsered"   
$openFileDialog.filter = "CSV Files|*.csv|All Files|*.*" 
$openFileDialog.ShowHelp = $True   
Write-Host "Select Downloaded Settings File... (see FileOpen Dialog)" -ForegroundColor Green  
$result = $openFileDialog.ShowDialog()   # Display the Dialog / Wait for user response
# in ISE you may have to alt-tab or minimize ISE to see dialog box
$result 
if($result -eq "OK")    {    
    Write-Host "Selected Downloaded Settings File:"  -ForegroundColor Green  
    $OpenFileDialog.filename   
} 
else { Write-Host "File Selection Cancelled!" -ForegroundColor Yellow; exit} 

$logsource = $openFileDialog.FileName
$logdirectory = Split-Path -Parent $logsource
$exported_log = $logsource -replace ".csv","_parsered.csv"

#Progressbar Start time
$PBStartTime = Get-Date

$log = Import-Csv -Path $logsource

#Choose �� for chars replacing the , inside certain Auditdata's values, namely between: [{ }]
#check if there is already �� in original log to be sure

$ErotinCount = [regex]::matches($log,����).count
if ($ErotinCount -eq 0)
{
$ErotinChar = "��"
}
else
{
$ErotinChar = "�M�"   
}

#total amount of rows in the log
$rivienmaara = $log.count
$rivilaskuri = 0

#create table to be populated, if exists then remove
$tabName = "O365LogTable"

if (Get-Variable -Name table -ErrorAction SilentlyContinue)
{
Remove-Variable table
}
$table = New-Object system.Data.DataTable �$tabName�


# Build table columns
$columnloop = 0
$cols = @()
$colheaders = @()
do
{
$cols += $columnheaders[$columnloop]
$colheaders += New-Object system.Data.DataColumn $cols[$columnloop],([string])

#Add the Columns
$table.columns.add($colheaders[$columnloop])

$columnloop++
}
until ($columnloop -gt ($columnheaders.count)-1)



# Begin to parser line by line

do
{
$string = $log[$rivilaskuri]

$parseredstring = $string

#trimming brackets
$parseredstring.Auditdata = $parseredstring.Auditdata.Trim("{","}")

$parseredstringaudit = $parseredstring.AuditData
    
    #NonGreedy, fecth only [{ }] and all of them to regex-groups
    $AllMatches = $parseredstringaudit | Select-String -Pattern "\[{.*?\}]" -AllMatches | foreach {$_.Matches}
   
    $matchcount = $AllMatches.Count
   
    if($AllMatches.Success)            
    {            
            
        #if only one hit, then don't iterate by items

        if($matchcount -eq 1){

            $old = $AllMatches.Value
            $New = $old -replace ",",$ErotinChar
   
            $parseredstringaudit = $parseredstringaudit.Replace($old,$new)
            $parsered = $parseredstringaudit

        }
        else
        {
            $matchloop = 0

           
            #loop line with regex-hits and replace comma (,) --> �� ONLY inside [{ }]
            do
            {
            
            $old = $AllMatches[$matchloop].Value
            $New = $old -replace ",",$ErotinChar
   
            $parseredstringaudit = $parseredstringaudit.Replace($old,$new)
            $parsered = $parseredstringaudit

            $matchloop++
            }
            until ($matchloop -gt $matchcount-1)
         }
     }

 
    ######## Done replacing between [{ }]

    #after that we still have to look if there is ", " (<= ,<empty>) in the line
    
    #NonGreedy, fecth only ", " and all of them to regex-groups
    $AllMatches = $parseredstringaudit | Select-String -Pattern "\, " -AllMatches | foreach {$_.Matches}
    $matchcount = $AllMatches.Count

    if($AllMatches.Success)            
    {                    
        #if only one hit, then don't iterate by items

        if($matchcount -eq 1){

            $old = $AllMatches.Value
            $New = $old -replace ",",$ErotinChar
   
            $parseredstringaudit = $parseredstringaudit.Replace($old,$new)
            $parsered = $parseredstringaudit

        }
        else
        {
            $matchloop = 0

           
            #loop line with regex-hits and replace comma (,) --> �� ONLY inside [{ }]
            do
            {
            
            $old = $AllMatches[$matchloop].Value
            $New = $old -replace ",",$ErotinChar
   
            $parseredstringaudit = $parseredstringaudit.Replace($old,$new)
            $parsered = $parseredstringaudit

            $matchloop++
            }
            until ($matchloop -gt $matchcount-1)
         }
     }

 
        ######## Done ", " -replacements
        
        # and now we are able to split

        $parseredsplitted = $parsered.Split(",")
        $parseredsplitcount = $parseredsplitted.Count

        $splitloop = 0

        #Create a row to table
        $row = $table.NewRow()
        
        #loop and get headers and values
        do
        {
    
        #header (left-part) and value (right part)
        $pos = $parseredsplitted[$splitloop].IndexOf(":")
        
        $leftPart = $parseredsplitted[$splitloop].Substring(0, $pos)
        $rightpart = $parseredsplitted[$splitloop].Substring($pos+1)

        #trim "

        $leftPart = $leftPart.Trim('"','"')
        $rightpart = $rightpart.Trim('"','"')


        ##### populate the values based on headers
        ##### best part of this that you can populate table by pointing to header names.
        ##### we don't have to worry about missing column values in the log row

        #find matching header "number"
        $colnumber = $table.Columns.IndexOf($leftPart)
        #If there is no match then use Miscellaneous-column
        if ($colnumber -eq "-1")
        {
        $colnumber = $table.Columns.IndexOf("Miscellaneous")
        Write-Host "No column, using misc, linenumber: $rivilaskuri"
        }

        #Enter data in the row
        $row.($cols[$colnumber]) = $rightpart

        $splitloop++
        }
        until ($splitloop -gt $parseredsplitcount-1)
  

        #Let's add three first columns to row from the original log
        $row.($cols[0]) = $log[$rivilaskuri].CreationDate
        $row.($cols[1]) = $log[$rivilaskuri].UserIds
        $row.($cols[2]) = $log[$rivilaskuri].Operations

        #Add the populated row to the table
        $table.Rows.Add($row)

        #skip to next line in log
        $rivilaskuri++   
        
        #Show some progresinfo to user
         
        ## -- Calculate The Percentage Completed
        [Int]$Percentage = ($rivilaskuri/$rivienmaara)*100
        #$PB.Value = $Percentage

        #calculate seconds
        $SecondsElapsed = ((Get-Date) - $PBStartTime).TotalSeconds
        $SecondsRemaining = ($SecondsElapsed / ($rivilaskuri/$rivienmaara)) - $SecondsElapsed

        #transform to more readable format
        $kulsek =  [timespan]::fromseconds($SecondsElapsed)
        $sekunnitkulunut = $kulsek.ToString("hh\:mm\:ss")

        $jalsek =  [timespan]::fromseconds($SecondsRemaining)
        $sekunnitjaljella = $jalsek.ToString("hh\:mm\:ss")

        Write-Progress -Activity "Prosessing" -Status "Processing: $rivilaskuri / $rivienmaara Elapsed Time: $sekunnitkulunut, Estimated time left: $sekunnitjaljella" -PercentComplete $Percentage
      
}
until ($rivilaskuri -gt $rivienmaara-1)

#close the bar
Write-Progress -Activity "Prosessing" -Status "Ready" -Completed

#Display the table
#$table | Format-Table


$table | Export-Csv -Path $exported_log -NoTypeInformation
Write-Host "`nOriginal log: $logsource" -ForegroundColor Yellow 
Write-Host "Parsered log: $exported_log`n" -ForegroundColor Yellow 


# Ask user if wants to open web-page where O365 detailed log is

Write-host "Would you like to open in browser detailed O365-log properties webpage (Microsoft's)?" -ForegroundColor Yellow 
    $Readhost = Read-Host " ( y / n ) " 
    Switch ($ReadHost) 
     { 
       Y {Write-host "Yes,opening"; Start-Process "https://support.office.com/en-us/article/detailed-properties-in-the-office-365-audit-log-ce004100-9e7f-443e-942b-9b04098fcfc3"} 
       N {Write-Host "No"; $PublishSettings=$false} 
       Default {Write-Host "Default, opening"; Start-Process "https://support.office.com/en-us/article/detailed-properties-in-the-office-365-audit-log-ce004100-9e7f-443e-942b-9b04098fcfc3"} 
     } 


# Ask user if wants to open Folder of Parsered-csv

Write-host "Would you like to open Folder of End-Product ie. Parsered-csv?" -ForegroundColor Yellow 
    $Readhost = Read-Host " ( y / n ) " 
    Switch ($ReadHost) 
     { 
       Y {Write-host "Yes,opening"; Invoke-Item $logdirectory} 
       N {Write-Host "No"; $PublishSettings=$false} 
       Default {Write-Host "Default, opening"; Invoke-Item $logdirectory} 
     }