Get-AuditReport.ps1

<#PSScriptInfo
 
.VERSION 1.0
 
.GUID 03a6a60d-01b5-49a8-adb9-ca890ea6f2eb
 
.AUTHOR Juan Granados
 
.COPYRIGHT 2021 Juan Granados
 
.TAGS Audit Report HTML csv mail File Access
 
.LICENSEURI https://raw.githubusercontent.com/juangranados/powershell-scripts/main/LICENSE
 
.PROJECTURI https://github.com/juangranados/powershell-scripts/tree/main/File%20Server%20Access%20Audit%20Report%20with%20PowerShell
 
.RELEASENOTES
    Initial release
 
#>


<#
.DESCRIPTION
    This PowerShell script allows to audit several file servers and send a report in CSV and HTML by mail.
    CSV file can be import on Excel to generate a File Audit Report.
    HTML report can filter and sorting rows by server, time, user, file or operation (read, delete or write).
    Requirements:
        Enable Audit in Windows and target folders, instructions: https://github.com/juangranados/powershell-scripts/tree/main/File%20Server%20Access%20Audit%20Report%20with%20PowerShell
    Author: Juan Granados
#>


#########################################################################################################################################
#Variables to modify
#########################################################################################################################################
# List of servers to check audit events
$server = "SRVJ-FS01","SVMEN01"
# List of file extensions to ignore
$ignoredExtensions = "tmp","rgt","mta","tlg","nd","ps","log","ldb","crdownload","DS_Store","cdf-ms","ini"
# List of users to ignore. If you do not want to ignore users, leave it empty.
# Example: $skippedUsers = "administrator","audit-test"
$skippedUsers = ""
# List of files to audit if you are interested only in a few files. If empty, all files (except those with ignored extensions) will be included in report.
# For example: $filesToAudit = "MontlyReport.xlsx","Internal Database.mdb" -> Only this two files will be included in report.
$filesToAudit = ""
# Number of hours back in time to check audit events
$hoursBackToCheck = "24"
# Reports path
$timestamp = Get-Date -format yyyy-MM-dd_HH-mm-ss # Timestamp to add to report name
$htmlReportPath = "$PSScriptRoot\" + "$timestamp" + "_AuditReport.html" # default: html report path in script path.
$csvReportPath = "$PSScriptRoot\" + "$timestamp" + "_AuditReport.csv" # default: csv report path in script path.
$transcriptPath = "$PSScriptRoot\" + "$timestamp" + "_AuditReport.log" # default: PowerShell transcript in script path.
# Mail settings
[string]$SMTPServer="mail.contoso.com" # If "None", no mail is sending. Example: [string]$SMTPServer="mail.contoso.com"
[string[]]$Recipient="jgranados@contoso.com" # List of recipients. Example: [string[]]$Recipient="jdoe@contoso.com","fsmith@contoso.com"
[string]$Sender = "audit-reports@contoso.com" # Sender. Example: [string]$Sender="reports@contoso.com"
[string]$Username="audit-reports@contoso.com" # User name to authenticate with mail server. If "None", no auth is performed. Example: [string]$Username="jdoe@gmail.com"
[string]$Password="P@ssw0rd" # Password to to authenticate with mail server. If "None", no auth is performed. Example: [string]$Password="P@ssw0rd"
[string]$SSL="True" # Using TLS/SSL to authenticate. Example: [string]$SSL="True" (Is required for Gmail or Office365)
[int]$Port=25 # Port of mail server. Example: [int]$Port=587
#########################################################################################################################################
#Internal variables. Do not modify.
#########################################################################################################################################
$ErrorActionPreference = "Stop"
$startDate = (get-date).AddHours(-$hoursBackToCheck)
$ns = @{e = "http://schemas.microsoft.com/win/2004/08/events/event"}
$htmlEvents = [System.Collections.ArrayList]@()
$csvContents = @()
$accessMasks = [ordered]@{
  '0x80' = 'Read'
  '0x2' = 'Write'
  '0x10000' = 'Delete'
}
$previousTimeCreated = ""
$previousSubjectUserName = ""
$previousObjectName = ""
$previousAccessMask = ""
$lastEventTimeCreated = ""
$evts = $null
#########################################################################################################################################
#Functions. Do not modify.
#########################################################################################################################################
Function getFileExtension($path) {
    $file = (Split-Path -Path $path -Leaf).Split(".")
    return $file[$file.Length-1]
}
# This function checks to see if the file should be ignored.
Function isTempFile($path) {
    $fileName = (Split-Path -Path $path -Leaf)
    $fileExtension = getFileExtension $path
    
    If ($fileName.substring(0,1) -eq "~" -or $fileName -eq "thumbs.db" -or $fileName.Substring(0,1) -eq "$") {
        return $true
    }
    
    ForEach($extension in $ignoredExtensions) {
        if ($fileExtension -eq $extension) {
            return $true
        }
    }
    
    return $false
}
Function isUserToSkip($user) {
    foreach ($svr in $server) {
        $serverUser = $svr + '$'
        if ($user -eq $serverUser) {
            return $true
        }
    }
    foreach ($skippedUser in $skippedUsers) {
        if ($user -eq $skippedUser) {
            return $true
        }
    }
    return $false
}
Function isFileToAudit($path) {
    if ($filesToAudit -eq "") {
     return $true
    } else {
        $fileName = (Split-Path -Path $path -Leaf)
        foreach ($file in $filesToAudit) {
            if ($file -eq $fileName) {
                return $true
            }    
        }
        return $false
    }
}
Function isFile($path) {
    try {
        if ((Get-Item $path) -is [System.IO.FileInfo]) {
            return $true
        }
    } catch {
        if ((Split-Path -Path $path -Leaf) -like "*.*") {
            return $true
        }
    }
}
function checkIfAddEvent($serverName, $timeCreated, $userName, $objectName, [string]$accessMask) {
    if (-not [string]::IsNullOrEmpty($accessMask)){
        if ($script:previousTimeCreated) {
            if ($timeCreated -gt $script:previousTimeCreated.AddSeconds(1) -or $userName -ne $script:previousSubjectUserName -or $objectName -ne $script:previousObjectName) {
                Write-Host "Adding event: $serverName | $script:previousTimeCreated | $script:previousSubjectUserName | $script:previousObjectName | $script:previousAccessMask" -ForegroundColor Green
                # Add event to html
                $htmlEvents.add("<tr>`n") | Out-Null
                $htmlEvents.add(" <td>$($serverName)</td>`n") | Out-Null # Time of access
                $htmlEvents.add(" <td>$($script:previousTimeCreated.ToString("yyyy-MM-ddTHH:mm:ss"))</td>`n") | Out-Null # Time of access
                $htmlEvents.add(" <td>$($script:previousSubjectUserName)</td>`n") | Out-Null # User
                $htmlEvents.add(" <td>$($script:previousObjectName)</td>`n") | Out-Null # File
                $htmlEvents.add(" <td>$($script:previousAccessMask)</td>`n") | Out-Null # Action
                $htmlEvents.add("</tr>`n") | Out-Null
                # Add event to csv
                $row = New-Object System.Object
                $row | Add-Member -MemberType NoteProperty -Name "Server" -Value $serverName
                $row | Add-Member -MemberType NoteProperty -Name "Time" -Value $script:previousTimeCreated.ToString("yyyy-MM-ddTHH:mm:ss")
                $row | Add-Member -MemberType NoteProperty -Name "User" -Value $script:previousSubjectUserName
                $row | Add-Member -MemberType NoteProperty -Name "File" -Value $script:previousObjectName
                $row | Add-Member -MemberType NoteProperty -Name "Action" -Value $script:previousAccessMask
                $script:csvContents += $row

                Write-Host "Analizing event: $timeCreated | $userName | $objectName | $accessMask" -ForegroundColor Cyan
                $script:lastEventTimeCreated = $script:previousTimeCreated
                $script:previousTimeCreated = $timeCreated
                $script:previousSubjectUserName = $userName
                $script:previousObjectName = $objectName
                $script:previousAccessMask = $accessMask
            } else {
                Write-Host "Analizing event: $timeCreated | $userName | $objectName | $accessMask" -ForegroundColor Cyan
                if ($script:previousAccessMask -ne "Write") {
                    $script:previousAccessMask = $accessMask
                }
            }
        } else {
            Write-Host "Analizing event: $timeCreated | $userName | $objectName | $accessMask" -ForegroundColor Cyan
            $script:previousTimeCreated = $timeCreated
            $script:previousSubjectUserName = $userName
            $script:previousObjectName = $objectName
            $script:previousAccessMask = $accessMask
        }
    } else {
        Write-Host "Audit event $accessMask not included in 'Read', 'Modified' or 'Deleted'" -ForegroundColor DarkRed
    }
    
}
#########################################################################################################################################
# Main. Do not modify.
#########################################################################################################################################
cls
Start-Transcript $transcriptPath
foreach ($svr in $server) {
    Write-Output "Getting events with ID 4663 of $svr since $startDate. This may take several minutes depending of log size..."
    $evts = Get-WinEvent -computer $svr -FilterHashtable @{LogName="security";ProviderName="Microsoft-Windows-Security-Auditing";ID="4663";StartTime=$startDate} -oldest
    foreach($evt in $evts) {
        $xml = [xml]$evt.ToXML()
        $SubjectUserName = Select-Xml -Xml $xml -Namespace $ns -XPath "//e:Data[@Name='SubjectUserName']/text()" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
        $ObjectName = Select-Xml -Xml $xml -Namespace $ns -XPath "//e:Data[@Name='ObjectName']/text()" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
        $AccessMask = Select-Xml -Xml $xml -Namespace $ns -XPath "//e:Data[@Name='AccessMask']/text()" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value

        if ($evt.TimeCreated -ge $startDate){
            if (isFile $ObjectName) {
                if (-not (isTempFile $ObjectName) -and -not (isUserToSkip $SubjectUserName) -and (isFileToAudit $ObjectName)) {
                    checkIfAddEvent $svr $evt.TimeCreated $SubjectUserName $ObjectName $accessMasks[$AccessMask.ToString()]
                }
            }
        }    
    }
    # Last event in case of has not been added
    if ($lastEventTimeCreated -ne $previousTimeCreated -and $previousTimeCreated -ne $null -and !([string]::IsNullOrWhiteSpace($previousTimeCreated))) {
        Write-Host "Adding event : $previousTimeCreated | $previousSubjectUserName | $previousObjectName | $previousAccessMask" -ForegroundColor Green
        $htmlEvents.add("<tr>`n") | Out-Null
        $htmlEvents.add(" <td>$($svr)</td>`n") | Out-Null
        $htmlEvents.add(" <td>$($previousTimeCreated.ToString("yyyy-MM-ddTHH:mm:ss"))</td>`n") | Out-Null # Time of access
        $htmlEvents.add(" <td>$($previousSubjectUserName)</td>`n") | Out-Null # User
        $htmlEvents.add(" <td>$($previousObjectName)</td>`n") | Out-Null # File or folder
        $htmlEvents.add(" <td>$($previousAccessMask)</td>`n") | Out-Null # Action
        $htmlEvents.add("</tr>`n") | Out-Null
        # Add event to csv
        $row = New-Object System.Object
        $row | Add-Member -MemberType NoteProperty -Name "Server" -Value $svr
        $row | Add-Member -MemberType NoteProperty -Name "Time" -Value $previousTimeCreated.ToString("yyyy-MM-ddTHH:mm:ss")
        $row | Add-Member -MemberType NoteProperty -Name "User" -Value $previousSubjectUserName
        $row | Add-Member -MemberType NoteProperty -Name "File" -Value $previousObjectName
        $row | Add-Member -MemberType NoteProperty -Name "Action" -Value $previousAccessMask
        $csvContents += $row
    }
    $previousTimeCreated = ""
    $previousSubjectUserName = ""
    $previousObjectName = ""
    $previousAccessMask = ""
}
$head = @"
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<script>
function filterRows() {
    document.getElementById("spinner").className='loading';
    setTimeout(function() {
        var input0,input1, input2, input3, input4,
        filter0, filter1, filter2, filter3, filter4,
        table, tr, td0, td1, td2, td3, td4, i,
        txtValue0, txtValue1, txtValue2, txtValue3, txtValue4;
 
        input0 = document.getElementById("myInput0");
        input1 = document.getElementById("myInput1");
        input2 = document.getElementById("myInput2");
        input3 = document.getElementById("myInput3");
        input4 = document.getElementById("myInput4");
        filter0 = input0.value.toUpperCase();
        filter1 = input1.value.toUpperCase();
        filter2 = input2.value.toUpperCase();
        filter3 = input3.value.toUpperCase();
        filter4 = input4.value.toUpperCase();
        table = document.getElementById("myTable");
        tr = table.getElementsByTagName("tr");
 
        for (i = 1; i < tr.length; i++) {
            td0 = tr[i].getElementsByTagName("td")[0];
            td1 = tr[i].getElementsByTagName("td")[1];
            td2 = tr[i].getElementsByTagName("td")[2];
            td3 = tr[i].getElementsByTagName("td")[3];
            td4 = tr[i].getElementsByTagName("td")[4];
            if (td0) {
                txtValue0 = td0.textContent || td0.innerText;
            } else {
                txtValue0="";
            }
            if (td1) {
                txtValue1 = td1.textContent || td1.innerText;
            } else {
                txtValue1="";
            }
            if (td2) {
                txtValue2 = td2.textContent || td2.innerText;
            } else {
                txtValue2="";
            }
            if (td3) {
                txtValue3 = td3.textContent || td3.innerText;
            } else {
                txtValue3="";
            }
            if (td4) {
                txtValue4 = td4.textContent || td4.innerText;
            } else {
                txtValue4="";
            }
            if (txtValue0.toUpperCase().indexOf(filter0) > -1 &&
                txtValue1.toUpperCase().indexOf(filter1) > -1 &&
                txtValue2.toUpperCase().indexOf(filter2) > -1 &&
                txtValue3.toUpperCase().indexOf(filter3) > -1 &&
                txtValue4.toUpperCase().indexOf(filter4) > -1
            ) {
                tr[i].style.display = "";
                } else {
                tr[i].style.display = "none";
                }
            }
            document.getElementById("spinner").className='hidden';
        }, 100);
}
const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;
 
const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
    v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
    )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
 
window.onload=function(){
document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    document.getElementById("spinner").className='loading';
    const table = th.closest('table');
    setTimeout(function() {
      Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
      document.getElementById("spinner").className='hidden';
    }, 100);
})));
}
</script>
<style>
body{
    font-family:Verdana, Arial, sans-serif;
}
.myInput {
  width: 20%;
  font-size: 16px;
  padding: 12px 20px 12px 40px;
  border: 1px solid #ddd;
  margin-bottom: 12px;
}
 
 
#myTable {
  border-collapse: collapse;
  width: 100%;
  border: 1px solid #ddd;
  font-size: 18px;
}
 
#myTable th, #myTable td {
  text-align: left;
  padding: 12px;
}
 
#myTable tr {
  border-bottom: 1px solid #ddd;
}
 
#myTable tr.header, #myTable tr:hover {
  background-color: #f1f1f1;
  cursor: pointer;
}
th b.sort-by {
    padding-right: 18px;
    position: relative;
}
b.sort-by:before,
b.sort-by:after {
    border: 4px solid transparent;
    content: "";
    display: block;
    height: 0;
    right: 5px;
    top: 50%;
    position: absolute;
    width: 0;
}
b.sort-by:before {
    border-bottom-color: #666;
    margin-top: -9px;
}
b.sort-by:after {
    border-top-color: #666;
    margin-top: 1px;
}
 
.loading {
  position: fixed;
  z-index: 999;
  height: 2em;
  width: 2em;
  overflow: visible;
  margin: auto;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
 
.loading:before {
  content: '';
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0,0,0,0.3);
}
 
.loading:not(:required) {
  /* hide "loading..." text */
  font: 0/0 a;
  color: transparent;
  text-shadow: none;
  background-color: transparent;
  border: 0;
}
 
.loading:not(:required):after {
  content: '';
  display: block;
  font-size: 10px;
  width: 1em;
  height: 1em;
  margin-top: -0.5em;
  -webkit-animation: spinner 1500ms infinite linear;
  -moz-animation: spinner 1500ms infinite linear;
  -ms-animation: spinner 1500ms infinite linear;
  -o-animation: spinner 1500ms infinite linear;
  animation: spinner 1500ms infinite linear;
  border-radius: 0.5em;
  -webkit-box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.5) -1.5em 0 0 0, rgba(0, 0, 0, 0.5) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
  box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) -1.5em 0 0 0, rgba(0, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
}
 
 
@-webkit-keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@-moz-keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@-o-keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
 
.hidden {display:none;}
.visible{display:block;}
</style>
"@

$body = @"
<H1>File Audit Report</H1>
<p>Click on a row to short by. Click again to reverse shorting.</p>
<p>Start typing on search box to filter by server, time, user, file or operation. You can filter by multiple fields.</p>
<input class="myInput" type="text" id="myInput0" onkeyup="filterRows()" placeholder="Search for server..">
<input class="myInput" type="text" id="myInput1" onkeyup="filterRows()" placeholder="Search for time..">
<input class="myInput" type="text" id="myInput2" onkeyup="filterRows()" placeholder="Search for user..">
<input class="myInput" type="text" id="myInput3" onkeyup="filterRows()" placeholder="Search for file..">
<input class="myInput" type="text" id="myInput4" onkeyup="filterRows()" placeholder="Search for operation..">
<div class="hidden" id="spinner">
  <div class="rect1"></div>
  <div class="rect2"></div>
  <div class="rect3"></div>
  <div class="rect4"></div>
  <div class="rect5"></div>
</div>
<table id="myTable">
    <tr class="header">
        <th style="width:20%;"><b class="sort-by">Server</th>
        <th style="width:20%;"><b class="sort-by">Time</th>
        <th style="width:20%;"><b class="sort-by">User</th>
        <th style="width:20%;"><b class="sort-by">File</th>
        <th style="width:20%;"><b class="sort-by">Operation</th>
    </tr>
        $htmlEvents
</table>
"@

if ($htmlEvents.Count -eq 0) {
    Write-Host "There is not any audit events to report. Check audit configuration." -ForegroundColor Red
    Stop-Transcript
    Exit(1)
}
$title = "File Audit Report"
$timestamp = Get-Date -format yyyy-MM-dd_HH-mm-ss
$htmlReportPath = "$PSScriptRoot\" + "$timestamp" + "_AuditReport.html"
$csvReportPath = "$PSScriptRoot\" + "$timestamp" + "_AuditReport.csv"
Write-Host "Creating CSV file: $csvReportPath" -ForegroundColor Yellow
$csvContents | Export-CSV -Path $csvReportPath -Encoding UTF8 -NoTypeInformation
try{
Write-Host "Creating HTML file: $htmlReportPath" -ForegroundColor Yellow
ConvertTo-HTML -Title $title -Head $Head -Body $Body | Out-File $htmlReportPath
    }catch
         {
          Write-Host "Error storing htlm report" -ForegroundColor Red
          write-host "Caught an exception:" -ForegroundColor Red
          write-host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
          write-host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red 
         }
# Mail sending
if($SMTPServer -ne "None"){
    #Creating a Mail object
    $msg = new-object Net.Mail.MailMessage

    #Creating SMTP server object
    $smtp = new-object Net.Mail.SmtpClient($SMTPServer,$Port)

    #Email structure
    $msg.From = $Sender
    $msg.ReplyTo = $Sender
    ForEach($mail in $Recipient)
    {
        $msg.To.Add($mail)
    }
    if ($Username -ne "None" -and $Password -ne "None")
        {
            $smtp.Credentials = new-object System.Net.NetworkCredential($Username, $Password)
        }
    if ($SSL -ne "False")
        {
            $smtp.EnableSsl = $true 
        }
    #Email subject
    $msg.subject = "Audit Report"
    #Email body
    $msg.body = "File Audit Reports Attached."
    $msg.IsBodyHtml = $true
    $msg.Attachments.Add($csvReportPath)
    $msg.Attachments.Add($htmlReportPath)
    #Sending email
    try{
        Write-Host "Sending email" -ForegroundColor Yellow
        $smtp.Send($msg)
        Write-Host "Mail sending ok. End of script" -ForegroundColor Green
        }catch
            {
                Write-Host "Error sending email" -ForegroundColor Red
                write-host "Caught an exception:" -ForegroundColor Red
                write-host "Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red
                write-host "Exception Message: $($_.Exception.Message)" -ForegroundColor Red 
            }
}
Stop-Transcript