When managing IIS websites with Windows Authentication, I often need to track who's accessing specific virtual directories. The IIS logs contain the domain\username format, but for reporting purposes, I typically need the User Principal Name (UPN). I've created a PowerShell script that automates this analysis process.
The Challenge
IIS logs store authenticated users in the format DOMAIN\username, but modern reporting and integration often requires the UPN format (user@domain.com). Additionally, analyzing raw IIS logs to understand access patterns can be time-consuming when dealing with thousands of log entries.
The Solution
I've developed a PowerShell script that:
- Reads IIS log files from a specified location
- Filters entries for a specific virtual directory path
- Converts SAM account names to UPNs using Active Directory
- Generates both on-screen and CSV reports showing access statistics
The script provides:
- Configurable number of days to analyze using the
-DaysBackparameter - Automatic AD lookup to convert
BEAR\usernametouser@bear.local - Tracking of total hits per user
- List of unique URLs accessed
- First and last access timestamps
- Client IP addresses used
- CSV export for further analysis
The Complete Script
#Requires -Modules ActiveDirectory
<#
.SYNOPSIS
Analyzes IIS logs for Authenticator access and generates user activity reports
.DESCRIPTION
This script parses IIS W3C log files looking for requests to the /Authenticator path,
identifies unique users, looks up their UPNs from Active Directory, and generates
both on-screen and CSV reports of user activity.
.PARAMETER DaysBack
Number of days to look back in the logs. Default is 30 days.
Use 0 to analyze all available logs.
.EXAMPLE
.\Analyze-AuthenticatorAccess.ps1
Analyzes logs from the last 30 days (default)
.EXAMPLE
.\Analyze-AuthenticatorAccess.ps1 -DaysBack 7
Analyzes logs from the last 7 days
.EXAMPLE
.\Analyze-AuthenticatorAccess.ps1 -DaysBack 0
Analyzes all available log files
.AUTHOR
Your Name
.DATE
January 2026
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[int]$DaysBack = 30
)
# Define all variables at the top for easy configuration
$LogPath = "\\wwwsrv1.bearl.local\c$\inetpub\logs\LogFiles\W3SVC1"
$SearchPath = "/Authenticator"
$OutputCSVPath = "C:\Temp\AuthenticatorAccessReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$DomainPrefix = "BEAR" # Domain prefix without backslash
# Create output directory if it doesn't exist
$OutputDir = Split-Path -Parent $OutputCSVPath
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
# Function to parse IIS log line
function Parse-IISLogLine {
param(
[string]$LogLine
)
# Skip comment lines
if ($LogLine -match "^#") {
return $null
}
# Split the log line by spaces
$fields = $LogLine -split '\s+'
# W3C Extended Log Format field positions (based on your sample)
# 0: date, 1: time, 2: s-ip, 3: cs-method, 4: cs-uri-stem, 5: cs-uri-query
# 6: s-port, 7: cs-username, 8: c-ip, 9: cs(User-Agent), 10: cs(Referer)
# 11: sc-status, 12: sc-substatus, 13: sc-win32-status, 14: time-taken
if ($fields.Count -ge 15) {
return [PSCustomObject]@{
DateTime = [DateTime]::Parse("$($fields[0]) $($fields[1])")
ServerIP = $fields[2]
Method = $fields[3]
UriStem = $fields[4]
UriQuery = $fields[5]
Port = $fields[6]
Username = $fields[7]
ClientIP = $fields[8]
UserAgent = $fields[9]
Referer = $fields[10]
Status = $fields[11]
SubStatus = $fields[12]
Win32Status = $fields[13]
TimeTaken = $fields[14]
}
}
return $null
}
# Function to get UPN from samAccountName
function Get-UserUPN {
param(
[string]$SamAccountName
)
try {
# Remove domain prefix if present (handle both DOMAIN\user and user formats)
if ($SamAccountName -match '\\') {
$username = $SamAccountName.Split('\')[1]
} else {
$username = $SamAccountName
}
# Query AD for the user
$user = Get-ADUser -Identity $username -Properties UserPrincipalName -ErrorAction SilentlyContinue
if ($user) {
return $user.UserPrincipalName
} else {
return "UPN Not Found"
}
} catch {
# Return just the error type for cleaner output
return "Not Found in AD"
}
}
Write-Host "IIS Authenticator Access Analysis Script" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Get log files to process
Write-Host "Searching for log files in: $LogPath" -ForegroundColor Yellow
$logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" -File -ErrorAction SilentlyContinue
if ($DaysBack -gt 0) {
$cutoffDate = (Get-Date).AddDays(-$DaysBack)
$logFiles = $logFiles | Where-Object { $_.LastWriteTime -ge $cutoffDate }
Write-Host "Analyzing logs from the last $DaysBack days (since $($cutoffDate.ToString('yyyy-MM-dd')))" -ForegroundColor Yellow
} else {
Write-Host "Analyzing all available log files" -ForegroundColor Yellow
}
if ($logFiles.Count -eq 0) {
Write-Host "No log files found in $LogPath" -ForegroundColor Red
if ($DaysBack -gt 0) {
Write-Host "Try increasing the -DaysBack parameter or use -DaysBack 0 for all logs" -ForegroundColor Yellow
}
exit 1
}
Write-Host "Found $($logFiles.Count) log file(s) to process" -ForegroundColor Green
Write-Host ""
# Initialize results hashtable
$userActivity = @{}
$totalRequests = 0
$processedFiles = 0
$skippedLines = 0
# Process each log file
foreach ($logFile in $logFiles) {
$processedFiles++
Write-Progress -Activity "Processing IIS Logs" -Status "File $processedFiles of $($logFiles.Count): $($logFile.Name)" -PercentComplete (($processedFiles / $logFiles.Count) * 100)
try {
$content = Get-Content -Path $logFile.FullName -ErrorAction Stop
foreach ($line in $content) {
$logEntry = Parse-IISLogLine -LogLine $line
if ($logEntry) {
# Check if entry is within our date range (for cases where log file contains older entries)
if ($DaysBack -gt 0 -and $logEntry.DateTime -lt $cutoffDate) {
$skippedLines++
continue
}
if ($logEntry.UriStem -like "*$SearchPath*") {
$totalRequests++
# Clean up username
$username = $logEntry.Username
if ($username -ne "-" -and $username -ne $null) {
# Initialize user entry if not exists
if (-not $userActivity.ContainsKey($username)) {
$userActivity[$username] = @{
Username = $username
UPN = $null
TotalHits = 0
UniqueUrls = @{}
FirstAccess = $logEntry.DateTime
LastAccess = $logEntry.DateTime
ClientIPs = @{}
}
}
# Update user statistics
$userActivity[$username].TotalHits++
$userActivity[$username].UniqueUrls[$logEntry.UriStem] = $true
$userActivity[$username].ClientIPs[$logEntry.ClientIP] = $true
# Update first/last access times
if ($logEntry.DateTime -lt $userActivity[$username].FirstAccess) {
$userActivity[$username].FirstAccess = $logEntry.DateTime
}
if ($logEntry.DateTime -gt $userActivity[$username].LastAccess) {
$userActivity[$username].LastAccess = $logEntry.DateTime
}
}
}
}
}
} catch {
Write-Warning "Error processing file $($logFile.Name): $($_.Exception.Message)"
}
}
Write-Progress -Activity "Processing IIS Logs" -Completed
Write-Host ""
Write-Host "Log processing complete. Found $totalRequests total requests to $SearchPath" -ForegroundColor Green
if ($skippedLines -gt 0) {
Write-Host "Skipped $skippedLines log entries outside the date range" -ForegroundColor Yellow
}
Write-Host ""
# Look up UPNs for all users
if ($userActivity.Count -gt 0) {
Write-Host "Looking up user UPNs from Active Directory..." -ForegroundColor Yellow
$userCount = 0
foreach ($user in $userActivity.Keys) {
$userCount++
Write-Progress -Activity "Looking up UPNs" -Status "Processing $user" -PercentComplete (($userCount / $userActivity.Count) * 100)
$userActivity[$user].UPN = Get-UserUPN -SamAccountName $user
}
Write-Progress -Activity "Looking up UPNs" -Completed
} else {
Write-Host "No user activity found for the specified time period." -ForegroundColor Yellow
exit 0
}
# Prepare report data
$reportData = @()
foreach ($user in $userActivity.Keys) {
$userData = $userActivity[$user]
$reportData += [PSCustomObject]@{
Username = $userData.Username
UPN = $userData.UPN
TotalHits = $userData.TotalHits
UniqueURLs = $userData.UniqueUrls.Keys -join "; "
URLCount = $userData.UniqueUrls.Count
FirstAccess = $userData.FirstAccess.ToString("yyyy-MM-dd HH:mm:ss")
LastAccess = $userData.LastAccess.ToString("yyyy-MM-dd HH:mm:ss")
UniqueClientIPs = $userData.ClientIPs.Keys -join "; "
ClientIPCount = $userData.ClientIPs.Count
}
}
# Sort by total hits descending
$reportData = $reportData | Sort-Object -Property TotalHits -Descending
# Display on-screen report
Write-Host ""
Write-Host "=== Authenticator Access Report ===" -ForegroundColor Cyan
if ($DaysBack -gt 0) {
Write-Host "Period: Last $DaysBack days (since $($cutoffDate.ToString('yyyy-MM-dd')))" -ForegroundColor Gray
} else {
Write-Host "Period: All available logs" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Top Users by Access Count:" -ForegroundColor Yellow
Write-Host ""
$reportData | Select-Object -First 10 | Format-Table -Property @(
@{Name="Username"; Expression={$_.Username}; Width=20}
@{Name="UPN"; Expression={$_.UPN}; Width=40}
@{Name="Total Hits"; Expression={$_.TotalHits}; Width=12}
@{Name="Unique URLs"; Expression={$_.URLCount}; Width=12}
@{Name="Last Access"; Expression={$_.LastAccess}; Width=20}
) -AutoSize
# Display summary statistics
Write-Host ""
Write-Host "Summary Statistics:" -ForegroundColor Yellow
Write-Host "==================" -ForegroundColor Yellow
Write-Host "Total Unique Users: $($reportData.Count)" -ForegroundColor Green
Write-Host "Total Requests: $totalRequests" -ForegroundColor Green
Write-Host "Average Requests per User: $([Math]::Round($totalRequests / [Math]::Max($reportData.Count, 1), 2))" -ForegroundColor Green
Write-Host ""
# Export to CSV
try {
$reportData | Export-Csv -Path $OutputCSVPath -NoTypeInformation -Encoding UTF8
Write-Host "Full report exported to: $OutputCSVPath" -ForegroundColor Green
} catch {
Write-Host "Error exporting to CSV: $($_.Exception.Message)" -ForegroundColor Red
}
# Display all unique URLs accessed
Write-Host ""
Write-Host "All Unique URLs Accessed:" -ForegroundColor Yellow
$allUrls = @{}
foreach ($user in $userActivity.Values) {
foreach ($url in $user.UniqueUrls.Keys) {
$allUrls[$url] = $true
}
}
$allUrls.Keys | Sort-Object | ForEach-Object {
Write-Host " $_" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Script completed successfully!" -ForegroundColor Green
# Optional: Display users who couldn't be found in AD
$notFoundUsers = $reportData | Where-Object { $_.UPN -eq "Not Found in AD" }
if ($notFoundUsers.Count -gt 0) {
Write-Host ""
Write-Host "WARNING: The following users could not be found in Active Directory:" -ForegroundColor Yellow
$notFoundUsers | ForEach-Object {
Write-Host " $($_.Username)" -ForegroundColor Yellow
}
}
Configuration
Before running the script, I need to customize these variables at the top of the script:
$LogPath = "\\wwwsrv1.bear.local\c$\inetpub\logs\LogFiles\W3SVC1"
$SearchPath = "/Authenticator"
$OutputCSVPath = "C:\Temp\AuthenticatorAccessReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$DomainPrefix = "Bear"
Usage Examples
Default Usage (Last 30 Days)
.\Analyze-AuthenticatorAccess.ps1
Analyze Last 7 Days
.\Analyze-AuthenticatorAccess.ps1 -DaysBack 7
Analyze All Available Logs
.\Analyze-AuthenticatorAccess.ps1 -DaysBack 0
Sample Output
Here's what the output looks like when I run the script:
IIS Authenticator Access Analysis Script
========================================
Searching for log files in: \\wwwsrv.bear.local\c$\inetpub\logs\LogFiles\W3SVC1
Analyzing logs from the last 30 days (since 2025-12-13)
Found 25 log file(s) to process
Log processing complete. Found 847 total requests to /Authenticator
Looking up user UPNs from Active Directory...
=== Authenticator Access Report ===
Period: Last 30 days (since 2025-12-13)
Top Users by Access Count:
Username UPN Total Hits Unique URLs Last Access
-------- --- ---------- ----------- -----------
BEAR\JSmith john.smith@croucher.cloud 125 3 2026-01-09 14:32:15
BEAR\MCroucher mary.croucher@croucher.cloud 98 2 2026-01-09 13:45:22
BEAR\PCroucher paul.croucher@croucher.cloud 76 1 2026-01-09 12:18:45
BEAR\RJones robert.jones@croucher.cloud 54 2 2026-01-09 11:22:31
BEAR\SCroucher sarah.croucher@croucher.cloud 48 1 2026-01-09 10:15:18
BEAR\DWilliams david.williams@croucher.cloud 45 3 2026-01-09 09:52:44
BEAR\LCroucher lisa.croucher@croucher.cloud 42 2 2026-01-09 08:34:12
BEAR\KBrown kevin.brown@croucher.cloud 38 1 2026-01-08 16:45:33
BEAR\TCroucher thomas.croucher@croucher.cloud 35 2 2026-01-08 15:22:18
BEAR\AMiller alice.miller@croucher.cloud 32 1 2026-01-08 14:18:56
Summary Statistics:
==================
Total Unique Users: 43
Total Requests: 847
Average Requests per User: 19.70
Full report exported to: C:\Temp\AuthenticatorAccessReport_20260109_143215.csv
All Unique URLs Accessed:
/Authenticator/
/Authenticator/Login
/Authenticator/Verify
CSV Output
The CSV file contains additional details including:
- Username (SAM format)
- UPN (user@domain.com format)
- Total hits
- All unique URLs accessed (semicolon-separated)
- Count of unique URLs
- First access timestamp
- Last access timestamp
- Client IP addresses used
- Count of unique client IPs
Requirements
To run this script, you will require:
- PowerShell 5.1 or later
- ActiveDirectory PowerShell module installed
- Read access to the IIS log directory
- Permissions to query Active Directory
- IIS logging enabled in W3C Extended Log Format
Customization for Other IIS Sites
This script works for any IIS website where Windows Authentication is enabled. Simply modify the configuration variables:
- Update
$LogPathto point to your IIS log directory - Change
$SearchPathto the virtual directory you want to analyze - Modify
$DomainPrefixto match your domain - Adjust
$OutputCSVPathto your preferred output location
Conclusion
This script has saved me countless hours when generating access reports for IIS applications. The ability to automatically convert SAM account names to UPNs and generate both on-screen and CSV reports makes it invaluable for compliance auditing and user activity monitoring.
The script is particularly useful for monitoring access to sensitive applications, tracking adoption of new internal tools, or investigating security incidents where you need to know who accessed specific resources and when.