Privileged Access Management (PAM) is a critical security control for protecting Active Directory environments against insider threats and external attacks targeting privileged accounts. This guide covers comprehensive PAM implementation strategies.
PAM Architecture Overview
Core Components
- Just-in-Time (JIT) Access: Temporary privilege elevation
- Privileged Account Monitoring: Real-time activity tracking
- Access Governance: Approval workflows and reviews
- Session Management: Recorded and monitored privileged sessions
- Credential Vaulting: Secure storage of privileged credentials
┌─────────────────────────────────────────────────────────────────┐
│ PAM Architecture │
├─────────────────────────────────────────────────────────────────┤
│ Layer │ Components │
│ ├─ Governance │ Approval Workflows, Policy Management │
│ ├─ Access │ JIT Elevation, Session Broker │
│ ├─ Monitoring │ Activity Logging, Behavioral Analytics │
│ ├─ Vaulting │ Credential Storage, Password Rotation │
│ └─ Integration │ AD, SIEM, HR Systems, ITSM │
└─────────────────────────────────────────────────────────────────┘
Just-in-Time Access Implementation
PowerShell JIT Framework
<#
.SYNOPSIS
Just-in-Time privileged access management for Active Directory.
.DESCRIPTION
Provides temporary elevation with automated approval workflows,
time-based access controls, and comprehensive audit logging.
#>
class JITAccessRequest {
[string]$RequestID
[string]$UserName
[string]$TargetGroup
[string]$Justification
[datetime]$RequestTime
[datetime]$ExpirationTime
[string]$Status
[string]$ApproverName
[datetime]$ApprovalTime
[string]$DenialReason
[hashtable]$Metadata
JITAccessRequest([string]$User, [string]$Group, [string]$Reason, [int]$DurationMinutes) {
$this.RequestID = [Guid]::NewGuid().ToString()
$this.UserName = $User
$this.TargetGroup = $Group
$this.Justification = $Reason
$this.RequestTime = Get-Date
$this.ExpirationTime = (Get-Date).AddMinutes($DurationMinutes)
$this.Status = "Pending"
$this.Metadata = @{}
}
}
class JITAccessManager {
[string]$RequestStorePath
[hashtable]$GroupApprovers
[hashtable]$AutoApprovalRules
[string]$SMTPServer
[string]$LogPath
JITAccessManager([string]$StorePath) {
$this.RequestStorePath = $StorePath
$this.GroupApprovers = @{}
$this.AutoApprovalRules = @{}
$this.SMTPServer = "smtp.company.com"
$this.LogPath = "$StorePath\Logs"
# Ensure directories exist
if (!(Test-Path $this.RequestStorePath))
{
New-Item -ItemType Directory -Path $this.RequestStorePath -Force | Out-Null
}
if (!(Test-Path $this.LogPath))
{
New-Item -ItemType Directory -Path $this.LogPath -Force | Out-Null
}
}
[string] SubmitRequest([string]$UserName, [string]$TargetGroup, [string]$Justification, [int]$DurationMinutes) {
# Validate user and group
try
{
$User = Get-ADUser -Identity $UserName -ErrorAction Stop
$Group = Get-ADGroup -Identity $TargetGroup -ErrorAction Stop
}
catch
{
throw "Invalid user or group: $($_.Exception.Message)"
}
# Create request
$Request = [JITAccessRequest]::new($UserName, $TargetGroup, $Justification, $DurationMinutes)
# Add metadata
$Request.Metadata["SubmittedFrom"] = $env:COMPUTERNAME
$Request.Metadata["UserDN"] = $User.DistinguishedName
$Request.Metadata["GroupDN"] = $Group.DistinguishedName
$Request.Metadata["RequesterIP"] = (Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway}).IPv4Address.IPAddress
# Check auto-approval rules
if ($this.CheckAutoApproval($Request))
{
$this.ApproveRequest($Request.RequestID, "System Auto-Approval")
}
else
{
# Send to approvers
$this.SendApprovalRequest($Request)
}
# Store request
$this.SaveRequest($Request)
# Log request
$this.LogActivity("RequestSubmitted", $Request)
return $Request.RequestID
}
[bool] CheckAutoApproval([JITAccessRequest]$Request) {
# Check if group has auto-approval rules
if ($this.AutoApprovalRules.ContainsKey($Request.TargetGroup))
{
$Rules = $this.AutoApprovalRules[$Request.TargetGroup]
# Check time-based rules
if ($Rules.ContainsKey("BusinessHours"))
{
$CurrentHour = (Get-Date).Hour
$IsBusinessHours = $CurrentHour -ge 8 -and $CurrentHour -le 17
if ($Rules.BusinessHours -eq $IsBusinessHours)
{
return $true
}
}
# Check duration-based rules
if ($Rules.ContainsKey("MaxDurationMinutes"))
{
$RequestDuration = ($Request.ExpirationTime - $Request.RequestTime).TotalMinutes
if ($RequestDuration -le $Rules.MaxDurationMinutes)
{
return $true
}
}
# Check user-based rules
if ($Rules.ContainsKey("PreapprovedUsers"))
{
if ($Request.UserName -in $Rules.PreapprovedUsers)
{
return $true
}
}
}
return $false
}
[void] SendApprovalRequest([JITAccessRequest]$Request) {
$Approvers = $this.GetApproversForGroup($Request.TargetGroup)
if ($Approvers.Count -eq 0)
{
throw "No approvers configured for group: $($Request.TargetGroup)"
}
$EmailBody = @"
A privileged access request requires your approval:
Request ID: $($Request.RequestID)
User: $($Request.UserName)
Target Group: $($Request.TargetGroup)
Justification: $($Request.Justification)
Duration: $(($Request.ExpirationTime - $Request.RequestTime).TotalMinutes) minutes
Requested: $($Request.RequestTime)
Expires: $($Request.ExpirationTime)
To approve this request:
Approve-JITRequest -RequestID $($Request.RequestID)
To deny this request:
Deny-JITRequest -RequestID $($Request.RequestID) -Reason "Your reason here"
Request details available at: \\PAMServer\Requests\$($Request.RequestID).json
"@
foreach ($Approver in $Approvers)
{
Send-MailMessage -From "pam@company.com" -To $Approver -Subject "PAM Approval Required: $($Request.UserName) -> $($Request.TargetGroup)" -Body $EmailBody -SmtpServer $this.SMTPServer
}
}
[string[]] GetApproversForGroup([string]$GroupName) {
if ($this.GroupApprovers.ContainsKey($GroupName))
{
return $this.GroupApprovers[$GroupName]
}
# Default to security group owners or administrators
return @("security@company.com")
}
[void] ApproveRequest([string]$RequestID, [string]$ApproverName) {
$Request = $this.LoadRequest($RequestID)
if ($Request.Status -ne "Pending")
{
throw "Request $RequestID is not in pending status"
}
# Update request
$Request.Status = "Approved"
$Request.ApproverName = $ApproverName
$Request.ApprovalTime = Get-Date
# Grant access
Add-ADGroupMember -Identity $Request.TargetGroup -Members $Request.UserName
# Schedule revocation
$this.ScheduleAccessRevocation($Request)
# Save updated request
$this.SaveRequest($Request)
# Log approval
$this.LogActivity("RequestApproved", $Request)
# Notify user
$this.NotifyUser($Request, "Approved")
}
[void] DenyRequest([string]$RequestID, [string]$ApproverName, [string]$Reason) {
$Request = $this.LoadRequest($RequestID)
if ($Request.Status -ne "Pending")
{
throw "Request $RequestID is not in pending status"
}
# Update request
$Request.Status = "Denied"
$Request.ApproverName = $ApproverName
$Request.ApprovalTime = Get-Date
$Request.DenialReason = $Reason
# Save updated request
$this.SaveRequest($Request)
# Log denial
$this.LogActivity("RequestDenied", $Request)
# Notify user
$this.NotifyUser($Request, "Denied")
}
[void] ScheduleAccessRevocation([JITAccessRequest]$Request) {
$TaskAction = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-Command `"Remove-ADGroupMember -Identity '$($Request.TargetGroup)' -Members '$($Request.UserName)' -Confirm:`$false`""
$TaskTrigger = New-ScheduledTaskTrigger -Once -At $Request.ExpirationTime
$TaskName = "PAM-Revoke-$($Request.RequestID)"
Register-ScheduledTask -TaskName $TaskName -Action $TaskAction -Trigger $TaskTrigger -Description "Auto-revoke PAM access for $($Request.UserName)"
}
[void] RevokeAccess([string]$RequestID, [string]$Reason) {
$Request = $this.LoadRequest($RequestID)
if ($Request.Status -ne "Approved")
{
throw "Request $RequestID is not in approved status"
}
# Remove access
Remove-ADGroupMember -Identity $Request.TargetGroup -Members $Request.UserName -Confirm:$false
# Update request
$Request.Status = "Revoked"
$Request.Metadata["RevocationReason"] = $Reason
$Request.Metadata["RevocationTime"] = Get-Date
# Remove scheduled task
$TaskName = "PAM-Revoke-$($Request.RequestID)"
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
# Save updated request
$this.SaveRequest($Request)
# Log revocation
$this.LogActivity("AccessRevoked", $Request)
}
[void] SaveRequest([JITAccessRequest]$Request) {
$RequestFile = Join-Path $this.RequestStorePath "$($Request.RequestID).json"
$Request | ConvertTo-Json -Depth 10 | Out-File -FilePath $RequestFile -Encoding UTF8
}
[JITAccessRequest] LoadRequest([string]$RequestID) {
$RequestFile = Join-Path $this.RequestStorePath "$RequestID.json"
if (!(Test-Path $RequestFile))
{
throw "Request $RequestID not found"
}
$RequestData = Get-Content $RequestFile | ConvertFrom-Json
$Request = [JITAccessRequest]::new($RequestData.UserName, $RequestData.TargetGroup, $RequestData.Justification, 0)
# Restore properties
$Request.RequestID = $RequestData.RequestID
$Request.RequestTime = $RequestData.RequestTime
$Request.ExpirationTime = $RequestData.ExpirationTime
$Request.Status = $RequestData.Status
$Request.ApproverName = $RequestData.ApproverName
$Request.ApprovalTime = $RequestData.ApprovalTime
$Request.DenialReason = $RequestData.DenialReason
$Request.Metadata = $RequestData.Metadata
return $Request
}
[void] LogActivity([string]$Action, [JITAccessRequest]$Request) {
$LogEntry = [PSCustomObject]@{
Timestamp = Get-Date
Action = $Action
RequestID = $Request.RequestID
UserName = $Request.UserName
TargetGroup = $Request.TargetGroup
Status = $Request.Status
ApproverName = $Request.ApproverName
Metadata = $Request.Metadata
}
$LogFile = Join-Path $this.LogPath "PAM-$(Get-Date -Format 'yyyyMM').log"
$LogEntry | ConvertTo-Json -Compress | Out-File -FilePath $LogFile -Append -Encoding UTF8
# Also write to Windows Event Log
try
{
Write-EventLog -LogName Application -Source "PAM" -EventId 3001 -EntryType Information -Message "$Action for user $($Request.UserName) to group $($Request.TargetGroup)"
}
catch
{
# Event source might not exist, continue without error
}
}
[void] NotifyUser([JITAccessRequest]$Request, [string]$Decision) {
try
{
$User = Get-ADUser -Identity $Request.UserName -Properties EmailAddress
if ($User.EmailAddress)
{
$Subject = "PAM Request $Decision: $($Request.TargetGroup)"
if ($Decision -eq "Approved")
{
$Body = @"
Your privileged access request has been approved.
Request ID: $($Request.RequestID)
Target Group: $($Request.TargetGroup)
Access Expires: $($Request.ExpirationTime)
Approved by: $($Request.ApproverName)
Your access is now active and will be automatically revoked at the expiration time.
"@
}
else
{
$Body = @"
Your privileged access request has been denied.
Request ID: $($Request.RequestID)
Target Group: $($Request.TargetGroup)
Denial Reason: $($Request.DenialReason)
Reviewed by: $($Request.ApproverName)
Please contact your security team if you believe this is an error.
"@
}
Send-MailMessage -From "pam@company.com" -To $User.EmailAddress -Subject $Subject -Body $Body -SmtpServer $this.SMTPServer
}
}
catch
{
Write-Warning "Failed to notify user: $($_.Exception.Message)"
}
}
}
# Global functions for easy use
function Initialize-PAM
{
[CmdletBinding()]
param(
[Parameter()]
[string]$StorePath = "C:\PAM"
)
$Global:PAMManager = [JITAccessManager]::new($StorePath)
# Configure default approvers and rules
$Global:PAMManager.GroupApprovers = @{
"Domain Admins" = @("security@company.com", "ciso@company.com")
"Enterprise Admins" = @("security@company.com", "ciso@company.com")
"Schema Admins" = @("security@company.com", "ciso@company.com")
"Server Operators" = @("serveradmins@company.com")
"Backup Operators" = @("backupadmins@company.com")
}
$Global:PAMManager.AutoApprovalRules = @{
"Server Operators" = @{
"BusinessHours" = $true
"MaxDurationMinutes" = 60
}
"Backup Operators" = @{
"MaxDurationMinutes" = 240
}
}
Write-Host "PAM Manager initialized at: $StorePath" -ForegroundColor Green
}
function Request-PrivilegedAccess
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$UserName,
[Parameter(Mandatory)]
[string]$TargetGroup,
[Parameter(Mandatory)]
[string]$Justification,
[Parameter()]
[int]$DurationMinutes = 120
)
if (!$Global:PAMManager)
{
Initialize-PAM
}
try
{
$RequestID = $Global:PAMManager.SubmitRequest($UserName, $TargetGroup, $Justification, $DurationMinutes)
Write-Host "Request submitted successfully. Request ID: $RequestID" -ForegroundColor Green
return $RequestID
}
catch
{
Write-Error "Failed to submit request: $($_.Exception.Message)"
}
}
function Approve-JITRequest
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$RequestID,
[Parameter()]
[string]$ApproverName = $env:USERNAME
)
if (!$Global:PAMManager)
{
Initialize-PAM
} try
{
$Global:PAMManager.ApproveRequest($RequestID, $ApproverName)
Write-Host "Request $RequestID approved successfully" -ForegroundColor Green
}
catch
{
Write-Error "Failed to approve request: $($_.Exception.Message)"
}
}
function Deny-JITRequest
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$RequestID,
[Parameter(Mandatory)]
[string]$Reason,
[Parameter()]
[string]$ApproverName = $env:USERNAME
)
if (!$Global:PAMManager)
{
Initialize-PAM
}
try
{
$Global:PAMManager.DenyRequest($RequestID, $ApproverName, $Reason)
Write-Host "Request $RequestID denied successfully" -ForegroundColor Green
}
catch
{
Write-Error "Failed to deny request: $($_.Exception.Message)"
}
}
function Revoke-JITAccess
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$RequestID,
[Parameter()]
[string]$Reason = "Manual revocation"
)
if (!$Global:PAMManager)
{
Initialize-PAM
}
try
{
$Global:PAMManager.RevokeAccess($RequestID, $Reason)
Write-Host "Access revoked successfully for request $RequestID" -ForegroundColor Green
}
catch
{
Write-Error "Failed to revoke access: $($_.Exception.Message)"
}
}
Privileged Session Management
Session Recording and Monitoring
function Start-PrivilegedSession
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$UserName,
[Parameter(Mandatory)]
[string]$TargetSystem,
[Parameter()]
[string]$SessionType = "RDP",
[Parameter()]
[string]$Justification
)
$SessionID = [Guid]::NewGuid().ToString()
$SessionStart = Get-Date
# Create session record
$SessionRecord = [PSCustomObject]@{
SessionID = $SessionID
UserName = $UserName
TargetSystem = $TargetSystem
SessionType = $SessionType
Justification = $Justification
StartTime = $SessionStart
EndTime = $null
Status = "Active"
RecordingPath = "\\SessionRecorder\Recordings\$SessionID"
KeystrokeLog = "\\SessionRecorder\Keystrokes\$SessionID.log"
ScreenRecording = "\\SessionRecorder\Screens\$SessionID.mp4"
Metadata = @{
SourceIP = (Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway}).IPv4Address.IPAddress
ComputerName = $env:COMPUTERNAME
ProcessId = $PID
}
}
# Log session start
Write-EventLog -LogName Application -Source "PAM" -EventId 4001 -EntryType Information -Message "Privileged session started: User $UserName accessing $TargetSystem (Session: $SessionID)"
# Start recording if configured
if ($SessionType -eq "RDP")
{
Start-RDPRecording -SessionID $SessionID -TargetSystem $TargetSystem
}
# Save session record
$SessionFile = "C:\PAM\Sessions\$SessionID.json"
$SessionRecord | ConvertTo-Json | Out-File -FilePath $SessionFile -Encoding UTF8
Write-Host "Privileged session started. Session ID: $SessionID" -ForegroundColor Green
return $SessionID
}
function Stop-PrivilegedSession
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SessionID,
[Parameter()]
[string]$EndReason = "Normal termination"
)
$SessionFile = "C:\PAM\Sessions\$SessionID.json"
if (!(Test-Path $SessionFile))
{
Write-Error "Session $SessionID not found"
return
}
# Load session record
$SessionRecord = Get-Content $SessionFile | ConvertFrom-Json
# Update session
$SessionRecord.EndTime = Get-Date
$SessionRecord.Status = "Completed"
$SessionRecord.Metadata.EndReason = $EndReason
$SessionRecord.Metadata.Duration = ($SessionRecord.EndTime - $SessionRecord.StartTime).TotalMinutes
# Stop recording
Stop-SessionRecording -SessionID $SessionID
# Save updated record
$SessionRecord | ConvertTo-Json | Out-File -FilePath $SessionFile -Encoding UTF8
# Log session end
Write-EventLog -LogName Application -Source "PAM" -EventId 4002 -EntryType Information -Message "Privileged session ended: $SessionID (Duration: $($SessionRecord.Metadata.Duration) minutes)"
Write-Host "Privileged session $SessionID ended" -ForegroundColor Green
}
Privileged Account Discovery
Account Risk Assessment
function Get-PrivilegedAccountRiskAssessment
{
[CmdletBinding()]
param(
[Parameter()]
[string[]]$DomainControllers = @(),
[Parameter()]
[string]$ReportPath = "C:\PAM\Reports\PrivilegedAccountRisk.html"
)
if ($DomainControllers.Count -eq 0)
{
$DomainControllers = (Get-ADDomainController -Filter *).Name
}
$RiskAssessment = @()
$PrivilegedGroups = @(
'Domain Admins',
'Enterprise Admins',
'Schema Admins',
'Administrators',
'Backup Operators',
'Server Operators',
'Account Operators',
'Print Operators'
)
foreach ($GroupName in $PrivilegedGroups)
{
try
{
$Group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
$Members = Get-ADGroupMember -Identity $Group -Recursive
foreach ($Member in $Members)
{
if ($Member.objectClass -eq 'user')
{
$User = Get-ADUser -Identity $Member.SamAccountName -Properties LastLogonDate, PasswordLastSet, PasswordNeverExpires, AccountExpirationDate, LockedOut, Enabled, ServicePrincipalName
# Calculate risk score
$RiskScore = 0
$RiskFactors = @()
# Age-based risks
if ($User.LastLogonDate -lt (Get-Date).AddDays(-30))
{
$RiskScore += 20
$RiskFactors += "Inactive account (>30 days)"
}
if ($User.PasswordLastSet -lt (Get-Date).AddDays(-90))
{
$RiskScore += 15
$RiskFactors += "Old password (>90 days)"
}
if ($User.PasswordNeverExpires)
{
$RiskScore += 25
$RiskFactors += "Password never expires"
}
# Account configuration risks
if (!$User.AccountExpirationDate)
{
$RiskScore += 10
$RiskFactors += "No account expiration"
}
if ($User.ServicePrincipalName)
{
$RiskScore += 15
$RiskFactors += "Service account with SPN"
}
# Security state risks
if (!$User.Enabled)
{
$RiskScore += 5
$RiskFactors += "Disabled account in privileged group"
}
if ($User.LockedOut)
{
$RiskScore += 5
$RiskFactors += "Locked out account"
}
# Group-specific risks
if ($GroupName -in @('Domain Admins', 'Enterprise Admins', 'Schema Admins'))
{
$RiskScore += 10
$RiskFactors += "High-privilege group membership"
}
$RiskLevel = switch ($RiskScore) {
{$_ -ge 60} { "Critical" }
{$_ -ge 40} { "High" }
{$_ -ge 20} { "Medium" }
default { "Low" }
}
$RiskAssessment += [PSCustomObject]@{
UserName = $User.SamAccountName
DisplayName = $User.Name
PrivilegedGroup = $GroupName
RiskScore = $RiskScore
RiskLevel = $RiskLevel
RiskFactors = $RiskFactors -join '; '
LastLogon = $User.LastLogonDate
PasswordLastSet = $User.PasswordLastSet
PasswordNeverExpires = $User.PasswordNeverExpires
AccountExpires = $User.AccountExpirationDate
Enabled = $User.Enabled
LockedOut = $User.LockedOut
HasSPN = [bool]$User.ServicePrincipalName
}
}
}
}
catch
{
Write-Warning "Failed to assess group $GroupName : $($_.Exception.Message)"
}
}
# Generate risk report
$CriticalAccounts = ($RiskAssessment | Where-Object RiskLevel -eq "Critical").Count
$HighRiskAccounts = ($RiskAssessment | Where-Object RiskLevel -eq "High").Count
$TotalAccounts = $RiskAssessment.Count
$ReportHTML = @"
<!DOCTYPE html>
<html>
<head>
<title>Privileged Account Risk Assessment</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background-color: #d32f2f; color: white; padding: 15px; text-align: center; }
.summary { background-color: #f8f9fa; padding: 15px; margin: 20px 0; border-left: 4px solid #d32f2f; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.critical { background-color: #ffebee; color: #d32f2f; font-weight: bold; }
.high { background-color: #fff3e0; color: #f57c00; }
.medium { background-color: #e3f2fd; color: #1976d2; }
.low { background-color: #e8f5e8; color: #388e3c; }
</style>
</head>
<body>
<div class="header">
<h1>Privileged Account Risk Assessment</h1>
<p>Generated: $(Get-Date)</p>
</div>
<div class="summary">
<h2>Risk Summary</h2>
<ul>
<li>Total Privileged Accounts: $TotalAccounts</li>
<li>Critical Risk: $CriticalAccounts</li>
<li>High Risk: $HighRiskAccounts</li>
<li>Overall Risk Rating: $(if ($CriticalAccounts -gt 0) { "Critical" } elseif ($HighRiskAccounts -gt 0) { "High" } else { "Medium" })</li>
</ul>
</div>
<h2>Detailed Risk Assessment</h2>
<table>
<tr>
<th>User</th>
<th>Group</th>
<th>Risk Level</th>
<th>Score</th>
<th>Risk Factors</th>
<th>Last Logon</th>
<th>Password Age</th>
</tr>
"@
foreach ($Account in ($RiskAssessment | Sort-Object RiskScore -Descending))
{
$RowClass = $Account.RiskLevel.ToLower()
if ($Account.PasswordLastSet)
{
$PasswordAge = ((Get-Date) - $Account.PasswordLastSet).Days
}
else
{
$PasswordAge = "Unknown"
}
$ReportHTML += @"
<tr class="$RowClass">
<td>$($Account.UserName)</td>
<td>$($Account.PrivilegedGroup)</td>
<td>$($Account.RiskLevel)</td>
<td>$($Account.RiskScore)</td>
<td>$($Account.RiskFactors)</td>
<td>$($Account.LastLogon)</td>
<td>$PasswordAge days</td>
</tr>
"@
}
$ReportHTML += @"
</table>
<h2>Recommendations</h2>
<ul>
<li>Review and remove unnecessary privileged group memberships</li>
<li>Implement regular access reviews for privileged accounts</li>
<li>Enforce password rotation for service accounts</li>
<li>Set account expiration dates for temporary privileged access</li>
<li>Monitor and investigate inactive privileged accounts</li>
<li>Implement Just-in-Time access for routine administrative tasks</li>
</ul>
<p><em>Generated by PAM Risk Assessment Tool</em></p>
</body>
</html>
"@
# Save report
$ReportDir = Split-Path $ReportPath -Parent
if (!(Test-Path $ReportDir))
{
New-Item -ItemType Directory -Path $ReportDir -Force | Out-Null
}
$ReportHTML | Out-File -FilePath $ReportPath -Encoding UTF8
Write-Host "Risk assessment completed. Report saved to: $ReportPath" -ForegroundColor Green
Write-Host "Critical Risk Accounts: $CriticalAccounts" -ForegroundColor Red
Write-Host "High Risk Accounts: $HighRiskAccounts" -ForegroundColor Yellow
return $RiskAssessment
}