This script will monitor for PAC files (or any other type of configuration file which can also include WPAD as well) this is a second version of my first attempt that handles the processing differently and the reporting differently as well.
This script will also read the endpoints at the end of the PAC file and test those for valid connectivity and then report it connectivity fails, the test site for this test is Google.
PAC File Monitoring v2
This script will create the folder structure required for the script to function then based on the PAC file used it will complete the following actions:
PAC File Endpoint Testing
- Initial script will download the baseline PAC file
- Store in the folder structure under Archive
- Waits for 15 minutes
- Download the PAC file and stores in the Archive folder
- Check for changes since the last PAC file
- If no changes are detected, it waits for another 15 minutes
- If changes are detected then it will e-mail you the changes and then waits for 15 minutes
- Return to step 3
This is a sample of what it looks like:
This shows the archived PAC files:
Then you will see that on this scan it has detected a change so it will send you an email of those changes that have been found from the backup and it also will only keep 100 previous version after which it will purge its cache.
The email you get will look like this, where the report will be divided up into additions and removals as you can see:
Script : PacMonitorv2.ps1
Then you will see that on this scan it has detected a change so it will send you an email of those changes that have been found from the backup and it also will only keep 100 previous version after which it will purge its cache.
The email you get will look like this, where the report will be divided up into additions and removals as you can see:
Script : PacMonitorv2.ps1
You just need to customise the variables in bold and the rest to automatic.
# Configuration
$PacFile = "<configuration_url>"
$MonitorFolder = Join-Path $PSScriptRoot "Proxy"
$ArchiveFolder = Join-Path $MonitorFolder "Proxy-Archive"
$MaxArchiveFiles = 100
$CheckIntervalMinutes = 15
$LogFile = Join-Path $MonitorFolder "LiveLog.log"
# Create necessary folders
New-Item -ItemType Directory -Force -Path $MonitorFolder | Out-Null
New-Item -ItemType Directory -Force -Path $ArchiveFolder | Out-Null
function Write-VerboseOutput {
param (
[string]$message,
[string]$type = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$type] $message"
switch ($type) {
"WARNING" { Write-Host $logEntry -ForegroundColor Yellow }
"ERROR" { Write-Host $logEntry -ForegroundColor Red }
"SUCCESS" { Write-Host $logEntry -ForegroundColor Green }
default { Write-Host $logEntry -ForegroundColor Cyan }
}
$logEntry >> $LogFile
}
function Download-PacFile {
param (
[string]$url,
[string]$outputPath
)
Write-VerboseOutput "Downloading PAC file from: $url"
$webClient = New-Object System.Net.WebClient
$webClient.DownloadFile($url, $outputPath)
Write-VerboseOutput "PAC file downloaded to: $outputPath" "SUCCESS"
return Get-Content $outputPath -Raw
}
function Send-Email {
param (
[string]$subject,
[string]$body
)
Write-VerboseOutput "Preparing to send email notification"
$smtpServer = "smtprelay.stwater.intra"
$smtpPort = 25
$senderEmail = "pac.monitor@severntrent.co.uk"
$receiverEmail = "lee.croucher@severntrent.co.uk"
try {
$smtp = New-Object Net.Mail.SmtpClient($smtpServer, $smtpPort)
$smtp.EnableSsl = $false
$msg = New-Object Net.Mail.MailMessage($senderEmail, $receiverEmail, $subject, $body)
$msg.IsBodyHtml = $true
$smtp.Send($msg)
Write-VerboseOutput "Email sent successfully" "SUCCESS"
}
catch {
Write-VerboseOutput "Failed to send email: $_" "ERROR"
}
}
function Cleanup-OldFiles {
$files = Get-ChildItem -Path $ArchiveFolder | Sort-Object CreationTime -Descending
Write-VerboseOutput "Starting archive cleanup - Current archive count: $($files.Count)"
if ($files.Count -gt $MaxArchiveFiles) {
$filesToRemove = $files | Select-Object -Skip $MaxArchiveFiles
Write-VerboseOutput "Archive exceeds limit of $MaxArchiveFiles - Removing $($filesToRemove.Count) files" "WARNING"
$filesToRemove | ForEach-Object {
Remove-Item $_.FullName -Force
Write-VerboseOutput "Removed old file: $($_.Name)"
}
$remainingFiles = Get-ChildItem -Path $ArchiveFolder
Write-VerboseOutput "Cleanup complete - Remaining files: $($remainingFiles.Count)" "SUCCESS"
}
else {
Write-VerboseOutput "No cleanup needed - Current count ($($files.Count)) below limit ($MaxArchiveFiles)"
}
}
function Generate-ChangeReport {
param (
[array]$added,
[array]$removed,
[string]$pacFileName
)
Write-VerboseOutput "Generating HTML change report"
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PAC File Change Report</title>
<style>
:root {
--added-color: #4caf50;
--removed-color: #f44336;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
min-height: 100vh;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
h2 {
color: #1a237e;
font-size: 1.8rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e0e0e0;
}
.box {
margin: 1.5rem 0;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s ease;
border: 1px solid #ddd;
}
.box:hover { transform: translateY(-2px); }
.header {
padding: 1rem;
font-weight: bold;
font-size: 1.1rem;
color: #000;
background: #f5f5f5;
}
.added .header { border-left: 4px solid var(--added-color); }
.removed .header { border-left: 4px solid var(--removed-color); }
pre {
margin: 0;
padding: 1rem;
background: #fff;
overflow-x: auto;
font-family: 'Consolas', monospace;
line-height: 1.5;
}
.added pre { color: #1b5e20; }
.removed pre { color: #b71c1c; }
.timestamp {
color: #666;
font-size: 0.9rem;
margin-top: 1rem;
text-align: right;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<h2>PAC File Change Report - $pacFileName</h2>
<div class="box added">
<div class="header">Added Lines</div>
<pre>$(($added -join "`n"))</pre>
</div>
<div class="box removed">
<div class="header">Removed Lines</div>
<pre>$(($removed -join "`n"))</pre>
</div>
<div class="timestamp">Generated on $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</div>
</div>
</body>
</html>
"@
return $html
}
function Monitor-PacFile {
Write-VerboseOutput "Starting PAC file monitoring"
Write-VerboseOutput "Monitor folder: $MonitorFolder"
Write-VerboseOutput "Archive folder: $ArchiveFolder"
Write-VerboseOutput "Check interval: $CheckIntervalMinutes minutes"
Write-VerboseOutput "Maximum archive files: $MaxArchiveFiles"
while ($true) {
try {
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$currentFile = Join-Path $ArchiveFolder "pac_$timestamp.txt"
Download-PacFile -url $PacFile -outputPath $currentFile
# Get previous file
$previousFile = Get-ChildItem -Path $ArchiveFolder -Filter "pac_*.txt" |
Where-Object { $_.FullName -ne $currentFile } |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($previousFile) {
Write-VerboseOutput "Comparing with previous version: $($previousFile.Name)"
$currentContent = Get-Content $currentFile
$lastContent = Get-Content $previousFile.FullName
$comparison = Compare-Object $lastContent $currentContent
if ($comparison) {
Write-VerboseOutput "Changes detected!" "WARNING"
$added = @($comparison | Where-Object { $_.SideIndicator -eq '=>' } | Select-Object -ExpandProperty InputObject)
$removed = @($comparison | Where-Object { $_.SideIndicator -eq '<=' } | Select-Object -ExpandProperty InputObject)
Write-VerboseOutput "Added lines: $($added.Count)"
Write-VerboseOutput "Removed lines: $($removed.Count)"
$htmlReport = Generate-ChangeReport -added $added -removed $removed -pacFileName (Split-Path $PacFile -Leaf)
Send-Email -subject "PAC File Changes Detected" -body $htmlReport
} else {
Write-VerboseOutput "No changes detected"
}
} else {
Write-VerboseOutput "Initial PAC file downloaded" "SUCCESS"
}
Cleanup-OldFiles
Write-VerboseOutput "Waiting $CheckIntervalMinutes minutes before next check..."
Start-Sleep -Seconds ($CheckIntervalMinutes * 60)
} catch {
Write-VerboseOutput "Error occurred: $_" "ERROR"
Start-Sleep -Seconds 60
}
}
}
# Start monitoring
Write-VerboseOutput "=== PAC File Monitor Starting ===" "SUCCESS"
Monitor-PacFile
When using PAC file or WPAD you usually end these files with the proxy definitions or endpoints like this and these are the servers that are used if no other variables are matched in the configuration file.
return "PROXY 10.70.1.259:3129; PROXY 10.21.8.259:3129;
return "PROXY 10.70.1.259:3129; PROXY 10.21.8.259:3129;
These variables are always in the last couple of lines of these files so for this script we complete the following actions:
However if the endpoint is not available you will get a non-200 status and this will immediately produce a HTML exception report which will be mailed to the people on the list, for this I recommend using a distribution list.
- Recursively search the current directory for "log" files
- Locate all the "log" files
- Use the latest version of the log file found
- Read the last 5 lines of the "log" file
- Look for PROXY and then parse the proxy:port data in these lines
- Test each proxy:port configuration against a website that is public
- Test each endpoint 5 times with a 5 second wait (to reduce false positives)
- Obtain the HTTP response from remote website (5 HTTP requests in total from each proxy)
- If the response is HTTP:200 then display on the screen
- If the response is not HTTP:200 then produce a html exceptions report
However if the endpoint is not available you will get a non-200 status and this will immediately produce a HTML exception report which will be mailed to the people on the list, for this I recommend using a distribution list.
Ensure you update the variables in bold to match your environment.
$VerbosePreference = "Continue"
$results = @()
$failures = @()
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Configuration
$emailTo = "lee@croucher.cloud"
$emailFrom = "Endpoint.Checker@croucher.cloud"
$smtpServer = "smtp.bear.local"
$retryCount = 5 #seconds
$retryDelay = 5 # seconds
$failureThreshold = 3 # Number of failures needed to consider a proxy down
Write-Verbose "Getting latest pac file..."
$latestFile = Get-ChildItem -Path . -Filter "pac_*.txt" -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Write-Verbose "Reading from: $($latestFile.FullName)"
$lastLines = Get-Content $latestFile.FullName -Tail 5
Write-Verbose "Content: `n$($lastLines -join "`n")"
$proxyPattern = 'PROXY\s+(\d+\.\d+\.\d+\.\d+):(\d+)'
$proxyMatches = [regex]::Matches(($lastLines -join " "), $proxyPattern)
foreach ($match in $proxyMatches) {
$proxyIP = $match.Groups[1].Value
$proxyPort = $match.Groups[2].Value
$proxyFailures = 0
$probeResults = @()
Write-Verbose "Testing proxy: $proxyIP`:$proxyPort with $retryCount attempts..."
for ($i = 1; $i -le $retryCount; $i++) {
Write-Verbose "Attempt $i of $retryCount"
try {
$request = [System.Net.WebRequest]::Create("http://www.google.com")
$request.Proxy = New-Object System.Net.WebProxy("http://$proxyIP`:$proxyPort")
$request.Timeout = 10000 # 10 second timeout
$response = $request.GetResponse()
$status = [int]$response.StatusCode
$statusDesc = $response.StatusDescription
$response.Close()
$probeResults += [PSCustomObject]@{
Attempt = $i
Status = $status
Description = $statusDesc
Success = $true
}
}
catch [System.Net.WebException] {
$proxyFailures++
$status = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { "N/A" }
$statusDesc = if ($_.Exception.Response) { $_.Exception.Response.StatusDescription } else { $_.Exception.Message }
$probeResults += [PSCustomObject]@{
Attempt = $i
Status = $status
Description = $statusDesc
Success = $false
}
}
if ($i -lt $retryCount) {
Write-Verbose "Waiting $retryDelay seconds before next attempt..."
Start-Sleep -Seconds $retryDelay
}
}
# Summarize results for this proxy
$successRate = ($probeResults | Where-Object Success -eq $true).Count / $retryCount * 100
$finalStatus = if ($proxyFailures -ge $failureThreshold) { "Failed" } else { "Operational" }
$results += [PSCustomObject]@{
Proxy = "$proxyIP`:$proxyPort"
Status = $finalStatus
SuccessRate = "$successRate%"
FailedProbes = $proxyFailures
Timestamp = $timestamp
}
if ($proxyFailures -ge $failureThreshold) {
$failures += [PSCustomObject]@{
Proxy = "$proxyIP`:$proxyPort"
Status = $finalStatus
SuccessRate = "$successRate%"
FailedProbes = $proxyFailures
ProbeDetails = $probeResults
Timestamp = $timestamp
}
}
}
# Display results table
$results | Format-Table -AutoSize
# Generate HTML report if there are failures
if ($failures.Count -gt 0) {
$reportPath = "ProxyFailureReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
$htmlHeader = @"
<!DOCTYPE html>
<html>
<head>
<title>Proxy Test Failure Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #2c3e50; }
.summary { background-color: #f8f9fa; padding: 20px; margin: 20px 0; border-radius: 5px; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #2c3e50; color: white; }
tr:nth-child(even) { background-color: #f8f9fa; }
.timestamp { color: #666; font-size: 0.9em; }
.critical { background-color: #ffe6e6; }
.warning { background-color: #fff3e6; }
.probe-details { margin-left: 20px; font-size: 0.9em; }
</style>
</head>
<body>
<h1>Proxy Test Failure Report</h1>
<div class="summary">
<p>Report generated: $timestamp</p>
<p>Source file: $($latestFile.Name)</p>
<p>Total proxies tested: $($results.Count)</p>
<p>Failed proxies: $($failures.Count)</p>
<p>Test configuration: $retryCount attempts with $retryDelay second intervals</p>
<p>Failure threshold: $failureThreshold failed probes</p>
</div>
<table>
<tr>
<th>Proxy</th>
<th>Status</th>
<th>Success Rate</th>
<th>Failed Probes</th>
<th>Probe Details</th>
<th>Timestamp</th>
</tr>
"@
$htmlRows = $failures | ForEach-Object {
$probeDetailsHtml = ($_.ProbeDetails | ForEach-Object {
"Attempt $($_.Attempt): Status $($_.Status) - $($_.Description)"
}) -join "<br>"
$rowClass = if ($_.FailedProbes -eq $retryCount) { "critical" } else { "warning" }
"<tr class='$rowClass'>
<td>$($_.Proxy)</td>
<td>$($_.Status)</td>
<td>$($_.SuccessRate)</td>
<td>$($_.FailedProbes)</td>
<td class='probe-details'>$probeDetailsHtml</td>
<td>$($_.Timestamp)</td>
</tr>"
}
$htmlFooter = @"
</table>
</body>
</html>
"@
$htmlContent = $htmlHeader + ($htmlRows -join "`n") + $htmlFooter
$htmlContent | Out-File -FilePath $reportPath
Write-Host "Failure report generated: $reportPath"
try {
$smtpClient = New-Object Net.Mail.SmtpClient($smtpServer)
$mailMessage = New-Object Net.Mail.MailMessage($emailFrom, $emailTo, "Proxy Test Failure Report - $timestamp", $htmlContent)
$mailMessage.IsBodyHtml = $true
$smtpClient.Send($mailMessage)
Write-Host "Failure report emailed to $emailTo"
}
catch {
Write-Host "Failed to send email: $($_.Exception.Message)"
}
finally {
if ($mailMessage) { $mailMessage.Dispose() }
if ($smtpClient) { $smtpClient.Dispose() }
}
}