Copy <#
.SYNOPSIS
CloudTechtiq Enhanced Windows Security Auditor
.DESCRIPTION
Performs comprehensive security audits on local machines and Domain Controllers.
Generates a detailed, expandable HTML report with findings and remediation steps.
Includes: Application inventory, startup programs, security event analysis,
EDR detection (Sophos), and Backup Solution detection (Acronis).
.OUTPUTS
HTML report saved to the user's Desktop.
.NOTES
Version: 4.2
Author: CloudTechtiq Pvt. Ltd.
Changes: Added EDR (Sophos) and Backup (Acronis) detection with exception handling
#>
# ===============================
# ENHANCED EXCEPTION HANDLING CONFIGURATION
# ===============================
$ErrorActionPreference = "Continue" # Changed from "Stop" to continue on errors
$Error.Clear() # Clear any existing errors
$Script:ExecutionErrors = @() # Global variable to track errors
$Date = Get-Date -Format "yyyy-MM-dd_HH-mm"
$ReportPath = "$env:USERPROFILE\Desktop\CloudTechtiq_Security_Audit_$Date.html"
# Function to log errors without stopping execution
function Log-Error {
param(
[string]$FunctionName,
[string]$ErrorMessage,
[string]$ErrorType = "Warning"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$errorEntry = [PSCustomObject]@{
Timestamp = $timestamp
Function = $FunctionName
Message = $ErrorMessage
Type = $ErrorType
}
$Script:ExecutionErrors += $errorEntry
Write-Host "[$ErrorType] $FunctionName`: $ErrorMessage" -ForegroundColor Yellow
}
# Function to safely execute code with error handling
function Invoke-Safely {
param(
[string]$FunctionName,
[scriptblock]$ScriptBlock,
[object]$DefaultReturn = $null,
[switch]$ContinueOnError
)
try {
return & $ScriptBlock
}
catch {
$errorMsg = "Error in $FunctionName`: $_"
Log-Error -FunctionName $FunctionName -ErrorMessage $errorMsg -ErrorType "Error"
if ($ContinueOnError) {
Write-Host " [!] Continuing despite error in $FunctionName..." -ForegroundColor Yellow
}
return $DefaultReturn
}
}
# ===============================
# REMAINING CONFIGURATION
# ===============================
$IsDomainController = Invoke-Safely -FunctionName "Check-DC" -ScriptBlock {
(Get-WmiObject Win32_ComputerSystem).DomainRole -in @(4, 5)
} -DefaultReturn $false
# Security scoring parameters
$ApplicationSecurityScores = @{
"Microsoft" = 10
"Adobe" = 7
"Java" = 4
"Chrome" = 8
"Firefox" = 8
"Zoom" = 6
"TeamViewer" = 3
"AnyDesk" = 2
"VNC" = 3
"PuTTY" = 7
"OpenSSH" = 9
"Python" = 8
"Node.js" = 8
"Docker" = 7
"VMware" = 8
"VirtualBox" = 7
"7-Zip" = 9
"WinRAR" = 6
"VLC" = 9
}
# Known risky startup applications
$RiskyStartupApps = @(
"utorrent", "bittorrent", "torrent",
"cryptominer", "miner", "coinminer",
"keylogger", "spy", "hack", "crack",
"cheat", "trainer", "patch", "loader"
)
# Known safe startup applications
$SafeStartupApps = @(
"windows", "microsoft", "intel", "amd",
"nvidia", "realtek", "broadcom", "dell",
"hp", "lenovo", "acer", "asus", "logitech",
"adobe", "java", "vmware", "citrix", "cisco"
)
# ===============================
# CORE FUNCTIONS (from original script)
# ===============================
function Get-EnhancedGroupMembers {
<#
.SYNOPSIS
Retrieves all members of a specified local group, expanding nested groups.
#>
param (
[Parameter(Mandatory = $true)]
[string]$GroupName,
[Parameter(Mandatory = $false)]
[string]$ComputerName = $env:COMPUTERNAME
)
$AllMembers = @()
try {
Write-Host " [i] Retrieving members of group: $GroupName" -ForegroundColor Cyan
$DirectMembers = Get-LocalGroupMember -Group $GroupName -ErrorAction Stop
foreach ($Member in $DirectMembers) {
$MemberObject = [PSCustomObject]@{
Name = $Member.Name
ObjectClass = $Member.ObjectClass
Source = 'Direct'
ParentGroup = $GroupName
PrincipalType = if ($Member.Name -like "*\$env:COMPUTERNAME*") { 'Local' } elseif ($Member.Name -like "*\*") { 'Domain' } else { 'Unknown' }
}
$AllMembers += $MemberObject
if ($Member.ObjectClass -eq 'Group') {
Write-Host " [+] Found nested group: $($Member.Name)" -ForegroundColor Cyan
}
}
} catch {
$errorMsg = "Error accessing group '$GroupName': $_"
Log-Error -FunctionName "Get-EnhancedGroupMembers" -ErrorMessage $errorMsg
$AllMembers = [PSCustomObject]@{
Name = "Group '$GroupName' not found or inaccessible"
ObjectClass = 'N/A'
Source = 'Error'
ParentGroup = 'N/A'
PrincipalType = 'N/A'
}
}
if ($AllMembers.Count -eq 0) {
$AllMembers = [PSCustomObject]@{
Name = "No members found in '$GroupName'"
ObjectClass = 'N/A'
Source = 'Empty'
ParentGroup = $GroupName
PrincipalType = 'N/A'
}
}
return $AllMembers
}
function Get-InstalledApplications {
<#
.SYNOPSIS
Retrieves installed applications with security scoring.
#>
Write-Host " [i] Collecting installed applications..." -ForegroundColor Cyan
$Applications = @()
# Method 1: Get from registry (32-bit and 64-bit)
$RegPaths = @(
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($Path in $RegPaths) {
try {
if (Test-Path $Path) {
$Apps = Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object {
$null -ne $_.DisplayName -and $_.DisplayName -ne ""
}
foreach ($App in $Apps) {
$AppName = $App.DisplayName
$Publisher = if ($App.Publisher) { $App.Publisher } else { "Unknown" }
$Version = if ($App.DisplayVersion) { $App.DisplayVersion } else { "Unknown" }
$InstallDate = if ($App.InstallDate) { $App.InstallDate } else { "Unknown" }
# Calculate security score
$SecurityScore = 5 # Default score
$ScoreReason = "Neutral application"
# Check against known vendors (using case-insensitive regex escape)
foreach ($Vendor in $ApplicationSecurityScores.Keys) {
# First clean the vendor string, then escape it for regex
$CleanVendor = $Vendor -replace '\\', ''
$EscapedVendor = [regex]::Escape($CleanVendor)
if ($AppName -match $EscapedVendor -or ($Publisher -and $Publisher -match $EscapedVendor)) {
$SecurityScore = $ApplicationSecurityScores[$Vendor]
$ScoreReason = "Known vendor: $CleanVendor"
break
}
}
# Check for risky keywords (case-insensitive)
$RiskyKeywords = @("crack", "keygen", "hack", "patch", "trainer", "cheat", "torrent", "miner")
foreach ($Keyword in $RiskyKeywords) {
if ($AppName -match $Keyword -or ($Publisher -and $Publisher -match $Keyword)) {
$SecurityScore = 1
$ScoreReason = "Contains risky keyword: $Keyword"
break
}
}
# Check for outdated versions of common software
if ($AppName -match "(Java|Adobe|Flash|Reader|Chrome|Firefox)") {
# For Java specifically, check for older versions
if ($AppName -match "Java" -and $Version -match "^(\d+)") {
$MajorVersion = [int]$Matches[1]
if ($MajorVersion -lt 11) {
$SecurityScore = [math]::Min($SecurityScore, 3)
$ScoreReason = "Outdated Java version ($MajorVersion)"
}
}
# For Adobe Flash, flag any version as risky (Flash is deprecated)
if ($AppName -match "Flash" -or $AppName -match "Adobe Flash") {
$SecurityScore = [math]::Min($SecurityScore, 2)
$ScoreReason = "Adobe Flash is deprecated and insecure"
}
}
$Applications += [PSCustomObject]@{
Name = $AppName
Publisher = $Publisher
Version = $Version
InstallDate = $InstallDate
SecurityScore = $SecurityScore
ScoreReason = $ScoreReason
}
}
}
} catch {
$errorMsg = "Error reading registry path $Path`: $_"
Log-Error -FunctionName "Get-InstalledApplications" -ErrorMessage $errorMsg
continue
}
}
# Method 2: Get from Programs and Features (alternative method)
try {
$Programs = Get-WmiObject -Class Win32_Product -ErrorAction SilentlyContinue | Select-Object Name, Version, Vendor, InstallDate
foreach ($Program in $Programs) {
if ($Program.Name) {
$AppName = $Program.Name
# Check if already in list
if ($Applications.Name -notcontains $AppName) {
$SecurityScore = 5
$ScoreReason = "Neutral application"
foreach ($Vendor in $ApplicationSecurityScores.Keys) {
$EscapedVendor = [regex]::Escape($Vendor -replace '\\', '')
if ($AppName -match $EscapedVendor -or ($Program.Vendor -and $Program.Vendor -match $EscapedVendor)) {
$SecurityScore = $ApplicationSecurityScores[$Vendor]
$ScoreReason = "Known vendor: $($Vendor -replace '\\\\', '')"
break
}
}
$Applications += [PSCustomObject]@{
Name = $AppName
Publisher = if ($Program.Vendor) { $Program.Vendor } else { "Unknown" }
Version = if ($Program.Version) { $Program.Version } else { "Unknown" }
InstallDate = if ($Program.InstallDate) {
try {
[DateTime]::ParseExact($Program.InstallDate.Substring(0,8), "yyyyMMdd", $null).ToString("yyyy-MM-dd")
} catch {
$Program.InstallDate
}
} else { "Unknown" }
SecurityScore = $SecurityScore
ScoreReason = $ScoreReason
}
}
}
}
} catch {
$errorMsg = "Could not retrieve programs via WMI: $_"
Log-Error -FunctionName "Get-InstalledApplications" -ErrorMessage $errorMsg
}
# Remove duplicates and sort by security score (lowest first)
try {
# Group by Name and select the first entry for each group
$UniqueApps = $Applications | Group-Object Name | ForEach-Object {
$_.Group | Sort-Object SecurityScore | Select-Object -First 1
} | Sort-Object SecurityScore, Name
} catch {
$errorMsg = "Error processing application list: $_"
Log-Error -FunctionName "Get-InstalledApplications" -ErrorMessage $errorMsg
$UniqueApps = $Applications | Select-Object -First 100 # Return first 100 if sorting fails
}
Write-Host " [✓] Found $($UniqueApps.Count) installed applications" -ForegroundColor Green
return $UniqueApps
}
function Get-StartupApplications {
<#
.SYNOPSIS
Retrieves startup applications with security analysis.
#>
Write-Host " [i] Analyzing startup applications..." -ForegroundColor Cyan
$StartupItems = @()
# Check common startup locations
$StartupLocations = @(
@{Path = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"; Type = "User Startup"},
@{Path = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"; Type = "System Startup"},
@{Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; Type = "Registry (HKLM Run)"},
@{Path = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; Type = "Registry (HKCU Run)"},
@{Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"; Type = "Registry (HKLM RunOnce)"},
@{Path = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"; Type = "Registry (HKCU RunOnce)"}
)
foreach ($Location in $StartupLocations) {
$Path = $Location.Path
$Type = $Location.Type
try {
if ($Path -like "*\*" -and $Path -notlike "*Registry*") {
# File system path
if (Test-Path $Path) {
$Files = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue
foreach ($File in $Files) {
$Assessment = "Review Recommended"
$RiskLevel = "Medium"
$Reason = "Startup application"
# Analyze the startup item
$FileName = $File.Name.ToLower()
# Check for known safe applications
$IsSafe = $false
foreach ($SafeApp in $SafeStartupApps) {
if ($FileName -match $SafeApp) {
$Assessment = "Likely Safe"
$RiskLevel = "Low"
$Reason = "Known safe application: $SafeApp"
$IsSafe = $true
break
}
}
# Check for risky applications if not already marked safe
if (-not $IsSafe) {
foreach ($RiskyApp in $RiskyStartupApps) {
if ($FileName -match $RiskyApp) {
$Assessment = "Potentially Risky"
$RiskLevel = "High"
$Reason = "Contains risky keyword: $RiskyApp"
break
}
}
}
# Check file properties
try {
$FileVersion = (Get-ItemProperty -Path $File.FullName -ErrorAction SilentlyContinue).VersionInfo.FileVersion
if (-not $FileVersion) { $FileVersion = "Unknown" }
} catch {
$FileVersion = "Unknown"
}
$StartupItems += [PSCustomObject]@{
Name = $File.Name
Path = $File.FullName
Type = $Type
Assessment = $Assessment
RiskLevel = $RiskLevel
Reason = $Reason
FileVersion = $FileVersion
LastModified = $File.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
}
}
}
} else {
# Registry path
if (Test-Path $Path) {
$RegEntries = Get-ItemProperty -Path $Path -ErrorAction SilentlyContinue
$RegEntries.PSObject.Properties | Where-Object {
$_.Name -notin @("PSPath", "PSParentPath", "PSChildName", "PSDrive", "PSProvider")
} | ForEach-Object {
$RegValue = $_.Value
$RegName = $_.Name
$Assessment = "Review Recommended"
$RiskLevel = "Medium"
$Reason = "Registry startup entry"
# Analyze registry entry
$RegValueLower = $RegValue.ToLower()
# Check for known safe applications
$IsSafe = $false
foreach ($SafeApp in $SafeStartupApps) {
if ($RegValueLower -match $SafeApp -or $RegName.ToLower() -match $SafeApp) {
$Assessment = "Likely Safe"
$RiskLevel = "Low"
$Reason = "Known safe application: $SafeApp"
$IsSafe = $true
break
}
}
# Check for risky applications
if (-not $IsSafe) {
foreach ($RiskyApp in $RiskyStartupApps) {
if ($RegValueLower -match $RiskyApp -or $RegName.ToLower() -match $RiskyApp) {
$Assessment = "Potentially Risky"
$RiskLevel = "High"
$Reason = "Contains risky keyword: $RiskyApp"
break
}
}
}
# Check for suspicious paths
if ($RegValueLower -match "(temp|tmp|appdata|local settings|\.\\.*)") {
if ($Assessment -eq "Review Recommended") {
$Assessment = "Suspicious Location"
$RiskLevel = "Medium"
$Reason = "Starts from temporary or hidden location"
}
}
$StartupItems += [PSCustomObject]@{
Name = $RegName
Path = $RegValue
Type = $Type
Assessment = $Assessment
RiskLevel = $RiskLevel
Reason = $Reason
FileVersion = "N/A (Registry)"
LastModified = "N/A"
}
}
}
}
} catch {
$errorMsg = "Error checking startup location $Path`: $_"
Log-Error -FunctionName "Get-StartupApplications" -ErrorMessage $errorMsg
continue
}
}
# Also check scheduled tasks that run at startup
try {
$StartupTasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object {
$_.Triggers | Where-Object { $_.StartBoundary -like "*AtStartup*" -or $_.Enabled -eq $true }
} | Select-Object -First 10
foreach ($Task in $StartupTasks) {
$TaskName = $Task.TaskName
$TaskPath = $Task.TaskPath
$Assessment = "Review Recommended"
$RiskLevel = "Medium"
$Reason = "Scheduled task at startup"
$StartupItems += [PSCustomObject]@{
Name = $TaskName
Path = $TaskPath
Type = "Scheduled Task"
Assessment = $Assessment
RiskLevel = $RiskLevel
Reason = $Reason
FileVersion = "N/A (Scheduled Task)"
LastModified = "N/A"
}
}
} catch {
$errorMsg = "Could not retrieve scheduled tasks: $_"
Log-Error -FunctionName "Get-StartupApplications" -ErrorMessage $errorMsg
}
Write-Host " [✓] Found $($StartupItems.Count) startup items" -ForegroundColor Green
try {
return $StartupItems | Sort-Object RiskLevel -Descending
} catch {
return $StartupItems # Return unsorted if sorting fails
}
}
function Get-SecurityEvents {
<#
.SYNOPSIS
Retrieves critical security events from Windows Event Logs.
#>
Write-Host " [i] Analyzing security events..." -ForegroundColor Cyan
$SecurityEvents = @()
# Define critical event IDs to look for
$CriticalEventIDs = @{
# Account Management
4720 = "A user account was created"
4722 = "A user account was enabled"
4723 = "An attempt was made to change an account's password"
4724 = "An attempt was made to reset an account's password"
4725 = "A user account was disabled"
4726 = "A user account was deleted"
4738 = "A user account was changed"
# Logon Events
4624 = "An account was successfully logged on"
4625 = "An account failed to log on"
4634 = "An account was logged off"
4648 = "A logon was attempted using explicit credentials"
4672 = "Special privileges assigned to new logon"
# Security Events
4688 = "A new process has been created"
4697 = "A service was installed in the system"
4698 = "A scheduled task was created"
4699 = "A scheduled task was deleted"
4700 = "A scheduled task was enabled"
4701 = "A scheduled task was disabled"
4702 = "A scheduled task was updated"
# Audit Events
4719 = "System audit policy was changed"
4902 = "The Per-user audit policy table was created"
4907 = "Auditing settings on object were changed"
# Privilege Use
4673 = "A privileged service was called"
4674 = "An operation was attempted on a privileged object"
# System Events
4616 = "System time was changed"
5024 = "The Windows Firewall Service has started successfully"
5025 = "The Windows Firewall Service has been stopped"
5031 = "The Windows Firewall Service blocked an application from accepting incoming connections"
}
try {
# Get events from last 7 days
$StartTime = (Get-Date).AddDays(-7)
# Get Security events
$Events = Get-WinEvent -LogName "Security" -MaxEvents 100 -ErrorAction SilentlyContinue |
Where-Object { $_.TimeCreated -ge $StartTime -and $_.Id -in $CriticalEventIDs.Keys } |
Select-Object -First 20
foreach ($Event in $Events) {
$EventID = $Event.Id
$EventTime = $Event.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss")
$EventSource = "Security"
# Get event message
$EventMessage = $Event.Message
if ([string]::IsNullOrEmpty($EventMessage)) {
$EventMessage = if ($CriticalEventIDs.ContainsKey($EventID)) {
$CriticalEventIDs[$EventID]
} else {
"Event ID: $EventID"
}
} else {
# Truncate long messages
if ($EventMessage.Length -gt 200) {
$EventMessage = $EventMessage.Substring(0, 200) + "..."
}
}
# Determine severity
$Severity = "Info"
$SeverityClass = "event-info"
# Critical events
if ($EventID -in @(4725, 4726, 4672, 4673, 4719)) {
$Severity = "Critical"
$SeverityClass = "event-critical"
}
# Error events
elseif ($EventID -in @(4625, 5025)) {
$Severity = "Error"
$SeverityClass = "event-error"
}
# Warning events
elseif ($EventID -in @(4720, 4722, 4697, 4698, 4616)) {
$Severity = "Warning"
$SeverityClass = "event-warning"
}
# Success events
elseif ($EventID -in @(4624, 5024)) {
$Severity = "Success"
$SeverityClass = "event-success"
}
$SecurityEvents += [PSCustomObject]@{
Time = $EventTime
EventID = $EventID
Source = $EventSource
Severity = $Severity
Message = $EventMessage
Class = $SeverityClass
}
}
# Also check System and Application logs for security-related events
$OtherLogs = @("System", "Application")
foreach ($LogName in $OtherLogs) {
try {
$LogEvents = Get-WinEvent -LogName $LogName -MaxEvents 20 -ErrorAction SilentlyContinue |
Where-Object {
$_.TimeCreated -ge $StartTime -and
($_.Message -match "error|fail|denied|blocked|attack|malware|virus|threat" -or
$_.Id -in @(1000, 1001, 1002, 1015, 7022, 7023, 7024, 7026, 7031, 7032, 7034))
} |
Select-Object -First 10
foreach ($Event in $LogEvents) {
$EventTime = $Event.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss")
$EventMessage = $Event.Message
if ($EventMessage.Length -gt 150) {
$EventMessage = $EventMessage.Substring(0, 150) + "..."
}
$SecurityEvents += [PSCustomObject]@{
Time = $EventTime
EventID = $Event.Id
Source = $LogName
Severity = "Warning"
Message = $EventMessage
Class = "event-warning"
}
}
} catch {
$errorMsg = "Could not read $LogName log: $_"
Log-Error -FunctionName "Get-SecurityEvents" -ErrorMessage $errorMsg
}
}
} catch {
$errorMsg = "Could not retrieve security events: $_"
Log-Error -FunctionName "Get-SecurityEvents" -ErrorMessage $errorMsg
# Return a placeholder if events can't be accessed
$SecurityEvents = @(
[PSCustomObject]@{
Time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
EventID = "N/A"
Source = "Audit Script"
Severity = "Warning"
Message = "Could not access Windows Event Logs. Run script as Administrator."
Class = "event-warning"
}
)
}
Write-Host " [✓] Found $($SecurityEvents.Count) security events" -ForegroundColor Green
try {
return $SecurityEvents | Sort-Object Time -Descending
} catch {
return $SecurityEvents # Return unsorted if sorting fails
}
}
function Get-SystemInformation {
<#
.SYNOPSIS
Collects comprehensive system information.
#>
try {
$OS = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$ComputerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
if (-not $OS) {
$OS = @{
Caption = "Unknown"
Version = "Unknown"
BuildNumber = "Unknown"
OSArchitecture = "Unknown"
InstallDate = "Unknown"
LastBootUpTime = "Unknown"
}
}
if (-not $ComputerSystem) {
$ComputerSystem = @{
Domain = "Unknown"
Manufacturer = "Unknown"
Model = "Unknown"
TotalPhysicalMemory = 0
}
}
$SysInfo = [PSCustomObject]@{
Hostname = $env:COMPUTERNAME
Domain = $ComputerSystem.Domain
'DC Role' = if ($IsDomainController) { 'Yes' } else { 'No' }
Manufacturer = $ComputerSystem.Manufacturer
Model = $ComputerSystem.Model
'OS Name' = $OS.Caption
'OS Version' = $OS.Version
'OS Build' = $OS.BuildNumber
Architecture = $OS.OSArchitecture
'Install Date' = $OS.InstallDate
'Last Boot Time' = $OS.LastBootUpTime
'System Uptime' = if ($OS.LastBootUpTime -ne "Unknown") {
(New-TimeSpan -Start $OS.LastBootUpTime -End (Get-Date)).ToString("dd\.hh\:mm\:ss")
} else { "Unknown" }
'Total Memory (GB)' = if ($ComputerSystem.TotalPhysicalMemory -gt 0) {
[math]::Round($ComputerSystem.TotalPhysicalMemory / 1GB, 2)
} else { "Unknown" }
'Logged-in User' = "$env:USERDOMAIN\$env:USERNAME"
}
return $SysInfo
} catch {
$errorMsg = "Error collecting system information: $_"
Log-Error -FunctionName "Get-SystemInformation" -ErrorMessage $errorMsg
# Return basic information even if CIM fails
return [PSCustomObject]@{
Hostname = $env:COMPUTERNAME
Domain = "Error retrieving"
'DC Role' = "Unknown"
Manufacturer = "Error retrieving"
Model = "Error retrieving"
'OS Name' = "Windows"
'OS Version' = "Unknown"
'OS Build' = "Unknown"
Architecture = "Unknown"
'Install Date' = "Unknown"
'Last Boot Time' = "Unknown"
'System Uptime' = "Unknown"
'Total Memory (GB)' = "Unknown"
'Logged-in User' = "$env:USERDOMAIN\$env:USERNAME"
}
}
}
function Get-DiskInformation {
<#
.SYNOPSIS
Collects disk usage information.
#>
try {
$Disks = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | Where-Object { $_.Used -or $_.Free } | Select-Object @(
@{N='Drive';E={$_.Name}},
@{N='Description';E={if ($_.DisplayRoot) { $_.DisplayRoot } else { "Local Disk" }}},
@{N='Used (GB)';E={[math]::Round($_.Used / 1GB, 2)}},
@{N='Free (GB)';E={[math]::Round($_.Free / 1GB, 2)}},
@{N='Total (GB)';E={[math]::Round(($_.Used + $_.Free) / 1GB, 2)}},
@{N='Free %';E={if (($_.Used + $_.Free) -gt 0) { [math]::Round(($_.Free / ($_.Used + $_.Free)) * 100, 1) } else { 0 }}}
)
return $Disks
} catch {
$errorMsg = "Error collecting disk information: $_"
Log-Error -FunctionName "Get-DiskInformation" -ErrorMessage $errorMsg
return @(
[PSCustomObject]@{
Drive = "C"
Description = "Local Disk (Error)"
'Used (GB)' = "Error"
'Free (GB)' = "Error"
'Total (GB)' = "Error"
'Free %' = "Error"
}
)
}
}
function Get-NetworkInformation {
<#
.SYNOPSIS
Collects network configuration.
#>
try {
$NetInfo = Get-NetIPConfiguration -Detailed -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
Interface = $_.InterfaceAlias
'IPv4 Address' = ($_.IPv4Address.IPAddress -join ', ')
Subnet = ($_.IPv4Address.PrefixLength -join ', ')
Gateway = if ($_.IPv4DefaultGateway) { $_.IPv4DefaultGateway.NextHop -join ', ' } else { 'None' }
DNS = ($_.DNSServer.ServerAddresses -join ', ')
MAC = $_.NetAdapter.LinkLayerAddress
Status = $_.NetAdapter.Status
}
}
if (-not $NetInfo) {
$NetInfo = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object @(
@{N='Interface';E={$_.InterfaceAlias}},
@{N='IPv4 Address';E={$_.IPAddress}},
@{N='Subnet';E={$_.PrefixLength}},
@{N='MAC';E={(Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue).MacAddress}}
)
}
if (-not $NetInfo) {
$NetInfo = @(
[PSCustomObject]@{
Interface = "Network information unavailable"
'IPv4 Address' = "Error retrieving"
Subnet = "N/A"
Gateway = "N/A"
DNS = "N/A"
MAC = "N/A"
Status = "N/A"
}
)
}
return $NetInfo
} catch {
$errorMsg = "Error collecting network information: $_"
Log-Error -FunctionName "Get-NetworkInformation" -ErrorMessage $errorMsg
return @(
[PSCustomObject]@{
Interface = "Error retrieving network info"
'IPv4 Address' = "Error"
Subnet = "Error"
Gateway = "Error"
DNS = "Error"
MAC = "Error"
Status = "Error"
}
)
}
}
function Get-LocalUserAccounts {
<#
.SYNOPSIS
Collects local user account information.
#>
try {
$Users = Get-LocalUser -ErrorAction SilentlyContinue | Select-Object @(
'Name',
'FullName',
'Description',
@{N='Enabled';E={if ($_.Enabled) { 'Yes' } else { 'No' }}},
@{N='Last Logon';E={if ($_.LastLogon) { $_.LastLogon } else { 'Never' }}},
@{N='Password Changed';E={$_.PasswordLastSet}},
@{N='Password Never Expires';E={if ($_.PasswordNeverExpires) { 'Yes' } else { 'No' }}},
@{N='Account Locked';E={if ($_.Locked) { 'Yes' } else { 'No' }}}
)
if (-not $Users) {
$Users = @(
[PSCustomObject]@{
Name = "Local user information unavailable"
FullName = "N/A"
Description = "N/A"
Enabled = "N/A"
'Last Logon' = "N/A"
'Password Changed' = "N/A"
'Password Never Expires' = "N/A"
'Account Locked' = "N/A"
}
)
}
return $Users
} catch {
$errorMsg = "Error collecting local user accounts: $_"
Log-Error -FunctionName "Get-LocalUserAccounts" -ErrorMessage $errorMsg
return @(
[PSCustomObject]@{
Name = "Error retrieving user accounts"
FullName = "Error"
Description = "Error"
Enabled = "Error"
'Last Logon' = "Error"
'Password Changed' = "Error"
'Password Never Expires' = "Error"
'Account Locked' = "Error"
}
)
}
}
function Get-SecurityConfiguration {
<#
.SYNOPSIS
Collects security-related configuration.
#>
$Firewall = $null
$Defender = $null
$SMB = $null
$UAC = $null
$AuditPolicy = $null
# Firewall
try {
$Firewall = Get-NetFirewallProfile -ErrorAction SilentlyContinue | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction
} catch {
$errorMsg = "Error collecting firewall information: $_"
Log-Error -FunctionName "Get-SecurityConfiguration" -ErrorMessage $errorMsg
$Firewall = [PSCustomObject]@{
Name = "Firewall information unavailable"
Enabled = "Error"
DefaultInboundAction = "Error"
DefaultOutboundAction = "Error"
}
}
# Windows Defender
try {
if (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue) {
$DefStatus = Get-MpComputerStatus -ErrorAction SilentlyContinue
$Defender = [PSCustomObject]@{
'Antivirus Enabled' = if ($DefStatus.AntivirusEnabled) { 'Yes' } else { 'No' }
'Real-time Protection' = if ($DefStatus.RealTimeProtectionEnabled) { 'Yes' } else { 'No' }
'Antivirus Signature Age' = if ($DefStatus.AntivirusSignatureAge) { "$($DefStatus.AntivirusSignatureAge) days" } else { 'N/A' }
'Last Quick Scan' = if ($DefStatus.LastQuickScanDateTime) { $DefStatus.LastQuickScanDateTime } else { 'N/A' }
'Last Full Scan' = if ($DefStatus.LastFullScanDateTime) { $DefStatus.LastFullScanDateTime } else { 'N/A' }
}
} else {
$Defender = [PSCustomObject]@{
'Antivirus Enabled' = 'N/A (Defender not available)'
'Real-time Protection' = 'N/A'
'Antivirus Signature Age' = 'N/A'
'Last Quick Scan' = 'N/A'
'Last Full Scan' = 'N/A'
}
}
} catch {
$errorMsg = "Error collecting Defender information: $_"
Log-Error -FunctionName "Get-SecurityConfiguration" -ErrorMessage $errorMsg
$Defender = [PSCustomObject]@{
'Antivirus Enabled' = 'Error retrieving'
'Real-time Protection' = 'Error'
'Antivirus Signature Age' = 'Error'
'Last Quick Scan' = 'Error'
'Last Full Scan' = 'Error'
}
}
# SMB Configuration
try {
$SMB = Get-SmbServerConfiguration -ErrorAction SilentlyContinue | Select-Object @(
@{N='SMBv1 Enabled';E={if ($_.EnableSMB1Protocol) { 'Yes' } else { 'No' }}},
@{N='SMBv2 Enabled';E={if ($_.EnableSMB2Protocol) { 'Yes' } else { 'No' }}},
@{N='SMB Signing Required';E={if ($_.RequireSecuritySignature) { 'Yes' } else { 'No' }}},
@{N='Inactive Sessions Timeout (min)';E={$_.AutoDisconnectTimeout}}
)
} catch {
$errorMsg = "Error collecting SMB configuration: $_"
Log-Error -FunctionName "Get-SecurityConfiguration" -ErrorMessage $errorMsg
$SMB = [PSCustomObject]@{
'SMBv1 Enabled' = "Error"
'SMBv2 Enabled' = "Error"
'SMB Signing Required' = "Error"
'Inactive Sessions Timeout (min)' = "Error"
}
}
# UAC Configuration
try {
$UAC = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -ErrorAction SilentlyContinue | Select-Object @(
@{N='UAC Enabled';E={if ($_.EnableLUA -eq 1) { 'Yes' } else { 'No' }}},
@{N='Admin Approval Mode';E={if ($_.ConsentPromptBehaviorAdmin -ne 0) { 'Yes' } else { 'No' }}},
@{N='Prompt on Elevation';E={if ($_.PromptOnSecureDesktop -eq 1) { 'Yes' } else { 'No' }}}
)
} catch {
$errorMsg = "Error collecting UAC configuration: $_"
Log-Error -FunctionName "Get-SecurityConfiguration" -ErrorMessage $errorMsg
$UAC = [PSCustomObject]@{
'UAC Enabled' = "Error"
'Admin Approval Mode' = "Error"
'Prompt on Elevation' = "Error"
}
}
# Audit Policy
try {
$AuditPolicy = auditpol /get /category:* 2>$null | Select-String "Logon" | Select-Object -First 3 | ForEach-Object {
[PSCustomObject]@{ 'Audit Policy' = $_.ToString().Trim() }
}
} catch {
$AuditPolicy = [PSCustomObject]@{ 'Audit Policy' = "Could not retrieve audit policy" }
}
$SecurityConfig = [PSCustomObject]@{
Firewall = $Firewall
Defender = $Defender
SMB = $SMB
UAC = $UAC
AuditPolicy = $AuditPolicy
}
return $SecurityConfig
}
function Get-InstalledUpdates {
<#
.SYNOPSIS
Collects recently installed updates.
#>
try {
$Updates = Get-HotFix -ErrorAction SilentlyContinue | Sort-Object InstalledOn -Descending | Select-Object -First 20 | Select-Object @(
'HotFixID',
@{N='Installed On';E={$_.InstalledOn}},
'Description',
@{N='Installed By';E={$_.InstalledBy}}
)
if (-not $Updates) {
$Updates = @(
[PSCustomObject]@{
HotFixID = "No update information available"
'Installed On' = "N/A"
Description = "N/A"
'Installed By' = "N/A"
}
)
}
return $Updates
} catch {
$errorMsg = "Error collecting installed updates: $_"
Log-Error -FunctionName "Get-InstalledUpdates" -ErrorMessage $errorMsg
return @(
[PSCustomObject]@{
HotFixID = "Error retrieving updates"
'Installed On' = "Error"
Description = "Error"
'Installed By' = "Error"
}
)
}
}
function Get-SecurityFindings {
<#
.SYNOPSIS
Analyzes collected data and generates security findings.
Now includes EDR and Backup solution checks.
#>
$Findings = @()
try {
# 1. Check for disabled firewall profiles
$FirewallProfiles = $null
try {
$FirewallProfiles = Get-NetFirewallProfile -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq $false }
} catch {}
if ($FirewallProfiles -and $FirewallProfiles.Count -gt 0) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-001'
Title = 'Firewall Disabled on One or More Profiles'
Description = "The following firewall profiles are disabled: $($FirewallProfiles.Name -join ', '). This exposes the system to network-based attacks."
Remediation = 'Enable Windows Firewall for all profiles using: `Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True`'
Category = 'Network Security'
}
}
# 2. Check SMBv1
$SMBConfig = $null
try {
$SMBConfig = Get-SmbServerConfiguration -ErrorAction SilentlyContinue
} catch {}
if ($SMBConfig -and $SMBConfig.EnableSMB1Protocol) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-002'
Title = 'SMBv1 Protocol Enabled'
Description = 'SMBv1 is an outdated and insecure protocol vulnerable to attacks like EternalBlue. It should be disabled.'
Remediation = 'Disable SMBv1 using: `Set-SmbServerConfiguration -EnableSMB1Protocol $false` and reboot.'
Category = 'Protocol Security'
}
}
# 3. Check for outdated Windows Defender signatures
try {
if (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue) {
$DefStatus = Get-MpComputerStatus -ErrorAction SilentlyContinue
if ($DefStatus -and $DefStatus.AntivirusSignatureAge -gt 7) {
$Findings += [PSCustomObject]@{
Severity = 'Medium'
ID = 'SEC-003'
Title = 'Outdated Antivirus Signatures'
Description = "Antivirus signatures are $($DefStatus.AntivirusSignatureAge) days old, reducing protection against new threats."
Remediation = 'Update Windows Defender signatures manually or ensure regular updates via Windows Update.'
Category = 'Malware Protection'
}
}
}
} catch {}
# 4. Check UAC configuration
$UACConfig = $null
try {
$UACConfig = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -ErrorAction SilentlyContinue
} catch {}
if ($UACConfig -and $UACConfig.EnableLUA -eq 0) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-004'
Title = 'User Account Control (UAC) Disabled'
Description = 'UAC is disabled, allowing programs to run with elevated privileges without prompting, increasing malware risk.'
Remediation = 'Enable UAC by setting HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA to 1 and reboot.'
Category = 'Access Control'
}
}
# 5. Check for disk space issues
$LowDisks = $null
try {
$LowDisks = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | Where-Object {
$_.Free -gt 0 -and (($_.Free / ($_.Used + $_.Free)) * 100) -lt 10
}
} catch {}
if ($LowDisks -and $LowDisks.Count -gt 0) {
$Findings += [PSCustomObject]@{
Severity = 'Medium'
ID = 'SEC-005'
Title = 'Low Disk Space on One or More Drives'
Description = "The following drives have less than 10% free space: $($LowDisks.Name -join ', '). This can cause system instability and failed updates."
Remediation = 'Clean up temporary files, uninstall unused applications, or increase disk capacity.'
Category = 'System Health'
}
}
# 6. Check for default administrator account status
$AdminAccount = $null
try {
$AdminAccount = Get-LocalUser -Name "Administrator" -ErrorAction SilentlyContinue
} catch {}
if ($AdminAccount -and $AdminAccount.Enabled -eq $true) {
$Findings += [PSCustomObject]@{
Severity = 'Medium'
ID = 'SEC-006'
Title = 'Default Administrator Account Enabled'
Description = 'The built-in Administrator account is enabled. This is a well-known account name targeted by attackers.'
Remediation = 'Disable the built-in Administrator account or at least rename it to a non-standard name.'
Category = 'Account Security'
}
}
# 7. Check for missing recent updates
$LastUpdate = $null
try {
$LastUpdate = Get-HotFix -ErrorAction SilentlyContinue | Sort-Object InstalledOn -Descending | Select-Object -First 1
} catch {}
if ($LastUpdate -and $LastUpdate.InstalledOn) {
$DaysSinceUpdate = (New-TimeSpan -Start $LastUpdate.InstalledOn -End (Get-Date)).Days
if ($DaysSinceUpdate -gt 30) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-007'
Title = 'System Not Updated Recently'
Description = "Last Windows update was installed $DaysSinceUpdate days ago. The system may be missing critical security patches."
Remediation = 'Run Windows Update immediately to install the latest security patches.'
Category = 'Patch Management'
}
}
}
# 8. Check for risky startup applications
$RiskyStartupCount = 0
try {
if ($Global:StartupApps) {
$RiskyStartupCount = ($Global:StartupApps | Where-Object { $_.RiskLevel -eq "High" }).Count
}
} catch {}
if ($RiskyStartupCount -gt 0) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-008'
Title = 'Potentially Risky Startup Applications'
Description = "Found $RiskyStartupCount startup applications marked as potentially risky. These could be malware or unwanted software."
Remediation = 'Review the Startup Applications section and investigate/remove suspicious entries.'
Category = 'Application Security'
}
}
# 9. Check for low-security score applications
$LowScoreApps = 0
try {
if ($Global:InstalledApps) {
$LowScoreApps = ($Global:InstalledApps | Where-Object { $_.SecurityScore -le 3 }).Count
}
} catch {}
if ($LowScoreApps -gt 5) {
$Findings += [PSCustomObject]@{
Severity = 'Medium'
ID = 'SEC-009'
Title = 'Multiple Low-Security Applications Installed'
Description = "Found $LowScoreApps applications with low security scores (≤3/10). These may pose security risks."
Remediation = 'Review installed applications in the Applications section and consider removing or updating risky software.'
Category = 'Application Security'
}
}
# 10. Check for recent critical security events
$CriticalEvents = 0
try {
if ($Global:SecurityEvents) {
$CriticalEvents = ($Global:SecurityEvents | Where-Object { $_.Severity -eq "Critical" }).Count
}
} catch {}
if ($CriticalEvents -gt 0) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-010'
Title = 'Recent Critical Security Events Detected'
Description = "Found $CriticalEvents critical security events in the logs. These require immediate investigation."
Remediation = 'Review the Security Events section and investigate the critical events listed.'
Category = 'Event Monitoring'
}
}
# 11. Check for missing EDR solution
if ($Global:EDRInfo) {
$NoEDR = $Global:EDRInfo | Where-Object { $_.EDRName -match "No EDR Detected" }
if ($NoEDR) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-011'
Title = 'No EDR Solution Detected'
Description = 'No Endpoint Detection and Response (EDR) solution was found on this system. EDR provides advanced threat detection and response capabilities.'
Remediation = 'Deploy an EDR solution such as Sophos, CrowdStrike, Microsoft Defender for Endpoint, or similar.'
Category = 'Endpoint Security'
}
} else {
# Check if EDR is active
$InactiveEDR = $Global:EDRInfo | Where-Object { $_.Status -notmatch "Active|Running" }
if ($InactiveEDR) {
$Findings += [PSCustomObject]@{
Severity = 'Medium'
ID = 'SEC-012'
Title = 'EDR Solution Not Active'
Description = "EDR solution '$($InactiveEDR.EDRName)' is installed but not active/running. This reduces threat detection capabilities."
Remediation = 'Start the EDR services and verify they are running properly.'
Category = 'Endpoint Security'
}
} else {
# EDR is active - add positive finding
$ActiveEDR = $Global:EDRInfo | Where-Object { $_.Status -match "Active|Running" } | Select-Object -First 1
if ($ActiveEDR) {
$Findings += [PSCustomObject]@{
Severity = 'Low'
ID = 'SEC-013'
Title = 'EDR Solution Active'
Description = "EDR solution '$($ActiveEDR.EDRName)' is active and running. This provides enhanced threat detection and response capabilities."
Remediation = 'Regularly review EDR alerts and ensure it is kept up to date.'
Category = 'Endpoint Security - Positive'
}
}
}
}
}
# 12. Check for missing backup solution
if ($Global:BackupInfo) {
$NoBackup = $Global:BackupInfo | Where-Object { $_.BackupName -match "No Backup Solution Detected" }
if ($NoBackup) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-014'
Title = 'No Backup Solution Detected - CRITICAL'
Description = 'No backup solution was found on this system. Without backups, data loss from ransomware, hardware failure, or user error is catastrophic.'
Remediation = 'Immediately implement a backup solution such as Acronis, Veeam, or Windows Server Backup. Follow 3-2-1 backup rule: 3 copies, 2 different media, 1 offsite.'
Category = 'Data Protection'
}
} else {
# Check if backup solution is active
$InactiveBackup = $Global:BackupInfo | Where-Object { $_.Status -notmatch "Active|Running" }
if ($InactiveBackup) {
$Findings += [PSCustomObject]@{
Severity = 'High'
ID = 'SEC-015'
Title = 'Backup Solution Not Active'
Description = "Backup solution '$($InactiveBackup.BackupName)' is installed but not active/running. Backups may not be occurring."
Remediation = 'Start the backup services and verify backup jobs are running successfully. Test restore procedures regularly.'
Category = 'Data Protection'
}
} else {
# Backup is active - add positive finding
$ActiveBackup = $Global:BackupInfo | Where-Object { $_.Status -match "Active|Running" } | Select-Object -First 1
if ($ActiveBackup) {
$Findings += [PSCustomObject]@{
Severity = 'Low'
ID = 'SEC-016'
Title = 'Backup Solution Active'
Description = "Backup solution '$($ActiveBackup.BackupName)' is active and running. Regular backups help protect against data loss."
Remediation = 'Regularly test backup restores and ensure backups are stored in a secure, offsite location.'
Category = 'Data Protection - Positive'
}
}
}
}
}
# If no critical findings, add a positive note
if ($Findings.Count -eq 0) {
$Findings += [PSCustomObject]@{
Severity = 'Low'
ID = 'SEC-000'
Title = 'No Critical Security Issues Detected'
Description = 'Basic security checks passed. However, a more in-depth manual review is still recommended.'
Remediation = 'Continue regular security maintenance and monitoring.'
Category = 'General'
}
}
} catch {
$errorMsg = "Error generating security findings: $_"
Log-Error -FunctionName "Get-SecurityFindings" -ErrorMessage $errorMsg
# Add at least one finding if there's an error
$Findings += [PSCustomObject]@{
Severity = 'Medium'
ID = 'SEC-ERR'
Title = 'Error in Security Analysis'
Description = "An error occurred during security analysis: $_"
Remediation = 'Check the Execution Errors section for details and rerun the audit.'
Category = 'System Error'
}
}
try {
return $Findings | Sort-Object Severity -Descending
} catch {
return $Findings # Return unsorted if sorting fails
}
}
# ===============================
# REPORT SECTION FUNCTIONS
# ===============================
function Add-ReportSection {
<#
.SYNOPSIS
Adds a collapsible section to the HTML report.
#>
param (
[Parameter(Mandatory = $true)]
[string]$Title,
[Parameter(Mandatory = $false)]
[PSObject]$Data,
[Parameter(Mandatory = $false)]
[string]$Icon = "fas fa-info-circle",
[Parameter(Mandatory = $false)]
[ValidateSet('low', 'medium', 'high', 'info')]
[string]$RiskLevel = 'info',
[Parameter(Mandatory = $false)]
[switch]$Open,
[Parameter(Mandatory = $false)]
[string]$Notes,
[Parameter(Mandatory = $false)]
[string]$TableId
)
$SectionId = ($Title -replace '[^a-zA-Z0-9]', '').ToLower()
$OpenAttr = if ($Open) { ' open' } else { '' }
$TableAttr = if ($TableId) { " id='$TableId'" } else { '' }
$SectionHtml = @"
<div class="card">
<details id="$SectionId"$OpenAttr>
<summary>
<i class="$Icon"></i> $Title
<span class='badge badge-$RiskLevel'>$RiskLevel</span>
</summary>
<div class="summary-content">
"@
if ($Notes) {
$SectionHtml += "<div class='remediation' style='margin-bottom: 15px;'><strong>Note:</strong> $Notes</div>"
}
# Handle different data types
if ($null -eq $Data) {
$SectionHtml += "<p>No data available for this section.</p>"
} elseif ($Data -is [System.Collections.IEnumerable] -and $Data -isnot [string]) {
if ($Data.Count -gt 0) {
$FirstItem = $Data | Select-Object -First 1
if ($FirstItem -is [PSObject] -and ($FirstItem.PSObject.Properties | Measure-Object).Count -gt 0) {
$SectionHtml += "<table$TableAttr>"
$SectionHtml += "<thead><tr>"
$Properties = $FirstItem.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } | Select-Object -ExpandProperty Name
foreach ($Prop in $Properties) {
$SectionHtml += "<th>$Prop</th>"
}
$SectionHtml += "</tr></thead><tbody>"
foreach ($Item in $Data) {
$SectionHtml += "<tr>"
foreach ($Prop in $Properties) {
$Value = $Item.$Prop
if ($null -eq $Value) { $Value = '' }
# Apply special formatting for security scores
if ($Prop -eq "SecurityScore" -and $Value -match "^[0-9]+$") {
$ScoreClass = "score-$([math]::Floor($Value/2)*2)"
if ($ScoreClass -eq "score-12") { $ScoreClass = "score-10" }
$Value = "<span class='score-badge $ScoreClass'>$Value/10</span>"
}
# Apply special formatting for risk levels
if ($Prop -eq "RiskLevel") {
$BadgeClass = "badge-$($Value.ToLower())"
$Value = "<span class='badge $BadgeClass'>$Value</span>"
}
# Apply special formatting for assessments
if ($Prop -eq "Assessment") {
if ($Value -match "Potentially Risky|Suspicious") {
$Value = "<span style='color: var(--danger); font-weight: 600;'>$Value</span>"
} elseif ($Value -match "Likely Safe") {
$Value = "<span style='color: var(--success); font-weight: 600;'>$Value</span>"
}
}
$SectionHtml += "<td>$Value</td>"
}
$SectionHtml += "</tr>"
}
$SectionHtml += "</tbody></table>"
} else {
$SectionHtml += "<ul>"
foreach ($Item in $Data) {
$SectionHtml += "<li>$Item</li>"
}
$SectionHtml += "</ul>"
}
} else {
$SectionHtml += "<p>No items found.</p>"
}
} elseif ($Data -is [PSObject] -and ($Data.PSObject.Properties | Measure-Object).Count -gt 0) {
$SectionHtml += "<table>"
$SectionHtml += "<tbody>"
$Properties = $Data.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' }
foreach ($Prop in $Properties) {
$Value = $Prop.Value
if ($null -eq $Value) { $Value = '' }
$SectionHtml += "<tr><th>$($Prop.Name)</th><td>$Value</td></tr>"
}
$SectionHtml += "</tbody></table>"
} else {
$SectionHtml += "<p>$Data</p>"
}
$SectionHtml += @"
</div>
</details>
</div>
"@
return $SectionHtml
}
function Add-SecurityEventsSection {
<#
.SYNOPSIS
Adds a formatted security events section to the report.
#>
param (
[Parameter(Mandatory = $true)]
[array]$SecurityEvents
)
$CriticalCount = ($SecurityEvents | Where-Object { $_.Severity -eq "Critical" }).Count
$WarningCount = ($SecurityEvents | Where-Object { $_.Severity -eq "Warning" }).Count
$ErrorCount = ($SecurityEvents | Where-Object { $_.Severity -eq "Error" }).Count
$RiskLevel = if ($CriticalCount -gt 0) { "high" } elseif ($ErrorCount -gt 0) { "medium" } else { "low" }
$SectionHtml = @"
<div class="card">
<details>
<summary>
<i class="fas fa-exclamation-circle"></i> Security Events Analysis
<span class='badge badge-$RiskLevel'>$RiskLevel</span>
</summary>
<div class="summary-content">
<p><strong>Event Summary:</strong> Total: $($SecurityEvents.Count) | Critical: $CriticalCount | Errors: $ErrorCount | Warnings: $WarningCount</p>
<p><em>Note: Showing events from the last 7 days. Run as Administrator for full event log access.</em></p>
<div class="event-log">
"@
if ($SecurityEvents.Count -eq 0) {
$SectionHtml += "<p>No security events found or event logs inaccessible.</p>"
} else {
foreach ($Event in $SecurityEvents) {
$SectionHtml += @"
<div class="event-item $($Event.Class)">
<div class="event-time">
<strong>$($Event.Time)</strong> | Source: $($Event.Source) | Event ID: $($Event.EventID)
</div>
<div class="event-source">$($Event.Severity): $($Event.Message)</div>
</div>
"@
}
}
$SectionHtml += @"
</div>
<div class="remediation" style="margin-top: 15px;">
<strong>Recommended Actions:</strong>
<ul style="margin-top: 5px; margin-left: 20px;">
<li>Investigate Critical events immediately</li>
<li>Review failed login attempts (Event ID 4625)</li>
<li>Monitor account changes (Event IDs 4720-4726)</li>
<li>Check for unexpected process creations (Event ID 4688)</li>
</ul>
</div>
</div>
</details>
</div>
"@
return $SectionHtml
}
function Add-StatsSection {
<#
.SYNOPSIS
Adds a statistics overview section to the report.
#>
param (
[Parameter(Mandatory = $true)]
[PSObject]$SystemInfo,
[Parameter(Mandatory = $true)]
[array]$Admins,
[Parameter(Mandatory = $true)]
[array]$RDPUsers,
[Parameter(Mandatory = $true)]
[array]$Findings,
[Parameter(Mandatory = $true)]
[array]$LocalUsers,
[Parameter(Mandatory = $true)]
[array]$InstalledApps,
[Parameter(Mandatory = $true)]
[array]$StartupApps,
[Parameter(Mandatory = $true)]
[array]$SecurityEvents
)
$TotalAdmins = ($Admins | Where-Object { $_.Name -notmatch "not found|No members" }).Count
$TotalRDPUsers = ($RDPUsers | Where-Object { $_.Name -notmatch "not found|No members" }).Count
$EnabledUsers = ($LocalUsers | Where-Object { $_.Enabled -eq 'Yes' }).Count
$CriticalFindings = ($Findings | Where-Object { $_.Severity -eq 'High' }).Count
$LowScoreApps = ($InstalledApps | Where-Object { $_.SecurityScore -le 3 }).Count
$RiskyStartups = ($StartupApps | Where-Object { $_.RiskLevel -eq 'High' }).Count
$CriticalEvents = ($SecurityEvents | Where-Object { $_.Severity -eq 'Critical' }).Count
# Calculate overall security score
$AppScore = 5 # Default if no apps found
if ($InstalledApps.Count -gt 0 -and $InstalledApps[0].PSObject.Properties.Name -contains 'SecurityScore') {
$AppScore = [math]::Round(($InstalledApps | Measure-Object -Property SecurityScore -Average).Average, 1)
}
$OverallScore = [math]::Round((100 - ($CriticalFindings * 5 + $RiskyStartups * 3 + $CriticalEvents * 4)), 0)
if ($OverallScore -lt 0) { $OverallScore = 0 }
if ($OverallScore -gt 100) { $OverallScore = 100 }
$ScoreColor = if ($OverallScore -ge 80) { "var(--success)" } elseif ($OverallScore -ge 60) { "var(--warning)" } else { "var(--danger)" }
$ScoreWidth = "$OverallScore%"
# Get last update date safely
$LastUpdate = "Unknown"
try {
$LastHotFix = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 1 -ErrorAction SilentlyContinue
if ($LastHotFix -and $LastHotFix.InstalledOn) {
$LastUpdate = $LastHotFix.InstalledOn.ToString("yyyy-MM-dd")
}
} catch {
$LastUpdate = "Unknown"
}
$StatsHtml = @"
<div class="card">
<details open>
<summary>
<i class="fas fa-chart-bar"></i> Executive Summary & Statistics
<span class="badge badge-info">Overview</span>
</summary>
<div class="summary-content">
<p>This report provides a comprehensive security assessment of <strong>$($SystemInfo.Hostname)</strong> ($($SystemInfo.'OS Name')). The system is $(if ($IsDomainController) { 'a Domain Controller' } else { 'a member/server' }) in the <strong>$($SystemInfo.Domain)</strong> domain.</p>
<div style="background: var(--light); padding: 20px; border-radius: 10px; margin: 20px 0;">
<h3 style="margin-top: 0; color: var(--primary);">Overall Security Score: $OverallScore/100</h3>
<div class="security-score-bar">
<div class="security-score-fill score-$(if ($OverallScore -ge 80) { 'green' } elseif ($OverallScore -ge 60) { 'yellow' } else { 'red' })" style="width: $ScoreWidth;"></div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.9rem; color: var(--gray);">
<span>Low Risk</span>
<span>Medium Risk</span>
<span>High Risk</span>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<i class="fas fa-users" style="font-size: 2rem; color: var(--secondary);"></i>
<div class="stat-value">$TotalAdmins</div>
<div class="stat-label">Administrators</div>
</div>
<div class="stat-card">
<i class="fas fa-exclamation-triangle" style="font-size: 2rem; color: var(--danger);"></i>
<div class="stat-value">$CriticalFindings</div>
<div class="stat-label">Critical Findings</div>
</div>
<div class="stat-card">
<i class="fas fa-shield-alt" style="font-size: 2rem; color: var(--warning);"></i>
<div class="stat-value">$LowScoreApps</div>
<div class="stat-label">Low-Score Apps</div>
</div>
<div class="stat-card">
<i class="fas fa-play-circle" style="font-size: 2rem; color: var(--danger);"></i>
<div class="stat-value">$RiskyStartups</div>
<div class="stat-label">Risky Startups</div>
</div>
</div>
<table style="margin-top: 30px;">
<tr>
<th>Assessment Item</th>
<th>Status</th>
<th>Details</th>
</tr>
<tr>
<td>System Uptime</td>
<td>$($SystemInfo.'System Uptime')</td>
<td>Time since last reboot</td>
</tr>
<tr>
<td>Application Security</td>
<td>$AppScore/10 Avg Score</td>
<td>$($InstalledApps.Count) applications analyzed</td>
</tr>
<tr>
<td>Startup Security</td>
<td>$(if ($RiskyStartups -gt 0) { '⚠️ Warning' } else { '✅ Good' })</td>
<td>$($StartupApps.Count) startup items checked</td>
</tr>
<tr>
<td>Event Log Security</td>
<td>$(if ($CriticalEvents -gt 0) { '🔴 Critical' } else { '✅ Clean' })</td>
<td>$($SecurityEvents.Count) events analyzed</td>
</tr>
<tr>
<td>Last Windows Update</td>
<td>$LastUpdate</td>
<td>Most recent patch installation</td>
</tr>
<tr>
<td>Total User Accounts</td>
<td>$($LocalUsers.Count)</td>
<td>$EnabledUsers enabled, $($LocalUsers.Count - $EnabledUsers) disabled</td>
</tr>
<tr>
<td>Network Interfaces</td>
<td>$(try { (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Measure-Object).Count } catch { 'N/A' })</td>
<td>Active IPv4 network interfaces</td>
</tr>
</table>
<div style="margin-top: 25px; padding: 15px; background: rgba(59, 130, 246, 0.1); border-radius: 8px; border-left: 4px solid var(--info);">
<h4 style="margin-top: 0; color: var(--info);"><i class="fas fa-lightbulb"></i> Quick Assessment</h4>
<p>
$(if ($OverallScore -ge 80) {
"✅ <strong>Good Security Posture:</strong> System shows strong security configuration with minimal critical issues."
} elseif ($OverallScore -ge 60) {
"⚠️ <strong>Moderate Security Concerns:</strong> Several areas need attention. Review findings and implement remediations."
} else {
"🔴 <strong>Critical Security Issues:</strong> Immediate attention required. Multiple high-risk findings detected."
})
</p>
<p style="margin-top: 10px; font-size: 0.9rem;">
<strong>Priority Actions:</strong>
$(if ($CriticalFindings -gt 0) { "1. Address $CriticalFindings critical findings<br>" })
$(if ($RiskyStartups -gt 0) { "2. Investigate $RiskyStartups risky startup applications<br>" })
$(if ($CriticalEvents -gt 0) { "3. Review $CriticalEvents critical security events<br>" })
$(if ($LowScoreApps -gt 0) { "4. Evaluate $LowScoreApps low-security applications" })
</p>
</div>
</div>
</details>
</div>
"@
return $StatsHtml
}
# ===============================
# NEW: EDR AND BACKUP DETECTION FUNCTIONS
# ===============================
function Get-EDRStatus {
<#
.SYNOPSIS
Detects and reports on EDR (Endpoint Detection and Response) software status.
Currently detects Sophos with extensibility for other EDRs.
#>
Write-Host " [i] Checking for EDR solutions..." -ForegroundColor Cyan
$EDRInfo = @()
# 1. Check for Sophos EDR
try {
# Method 1: Check for Sophos services
$SophosServices = Get-Service -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like "*Sophos*" -or $_.Name -like "*Sophos*"
}
if ($SophosServices) {
$SophosServiceStatus = @()
foreach ($service in $SophosServices) {
$SophosServiceStatus += "$($service.DisplayName): $($service.Status)"
}
# Method 2: Check for Sophos in registry
$SophosRegistryPaths = @(
"HKLM:\SOFTWARE\Sophos",
"HKLM:\SOFTWARE\WOW6432Node\Sophos",
"HKLM:\SOFTWARE\Sophos\Endpoint Defense",
"HKLM:\SOFTWARE\Sophos\Sophos Anti-Virus"
)
$SophosRegistryInfo = @()
foreach ($regPath in $SophosRegistryPaths) {
if (Test-Path $regPath) {
try {
$regItems = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
if ($regItems) {
$SophosRegistryInfo += "Found: $regPath"
}
} catch {
# Silently continue if registry access fails
}
}
}
# Method 3: Check for Sophos processes
$SophosProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object {
$_.ProcessName -like "*Sophos*" -or $_.Path -like "*Sophos*"
} | Select-Object -First 5
$SophosProcessList = if ($SophosProcesses) {
($SophosProcesses | ForEach-Object { $_.ProcessName }) -join ", "
} else {
"No active Sophos processes detected"
}
# Determine overall Sophos status
$RunningServices = $SophosServices | Where-Object { $_.Status -eq "Running" }
$SophosStatus = if ($RunningServices.Count -gt 0) { "Active" } else { "Installed but not running" }
$EDRInfo += [PSCustomObject]@{
EDRName = "Sophos Endpoint Protection"
Status = $SophosStatus
DetectionMethod = "Multiple checks"
Services = $SophosServiceStatus -join "; "
Processes = $SophosProcessList
RegistryPaths = if ($SophosRegistryInfo.Count -gt 0) { $SophosRegistryInfo -join "; " } else { "None found" }
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = if ($SophosStatus -eq "Active") { "EDR is active and can block known malware" } else { "EDR is installed but may not be protecting the system" }
}
Write-Host " [+] Sophos EDR detected: $SophosStatus" -ForegroundColor Green
} else {
# Check for other EDR solutions (extensible)
$OtherEDRServices = Get-Service -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like "*CrowdStrike*" -or
$_.DisplayName -like "*Carbon Black*" -or
$_.DisplayName -like "*SentinelOne*" -or
$_.DisplayName -like "*Microsoft Defender*" -or
$_.DisplayName -like "*McAfee*" -or
$_.DisplayName -like "*Symantec*" -or
$_.DisplayName -like "*Kaspersky*"
}
if ($OtherEDRServices) {
foreach ($service in $OtherEDRServices | Select-Object -First 3) {
$EDRInfo += [PSCustomObject]@{
EDRName = $service.DisplayName
Status = $service.Status
DetectionMethod = "Service detection"
Services = "$($service.DisplayName): $($service.Status)"
Processes = "Check specific EDR console"
RegistryPaths = "Not scanned"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "Third-party EDR detected. Check vendor console for detailed status."
}
}
Write-Host " [+] Other EDR solution(s) detected" -ForegroundColor Green
}
}
} catch {
$errorMsg = "Error checking for EDR solutions: $_"
Log-Error -FunctionName "Get-EDRStatus" -ErrorMessage $errorMsg
}
# If no EDR found, add a placeholder
if ($EDRInfo.Count -eq 0) {
$EDRInfo += [PSCustomObject]@{
EDRName = "No EDR Detected"
Status = "Not Found"
DetectionMethod = "Service and registry scan"
Services = "None"
Processes = "None"
RegistryPaths = "None"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "Consider deploying an EDR solution for advanced threat protection"
}
Write-Host " [-] No EDR solution detected" -ForegroundColor Yellow
}
return $EDRInfo
}
function Get-BackupSolutionStatus {
<#
.SYNOPSIS
Detects and reports on backup solution status.
Currently detects Acronis with extensibility for other backup solutions.
#>
Write-Host " [i] Checking for backup solutions..." -ForegroundColor Cyan
$BackupInfo = @()
# 1. Check for Acronis Backup
try {
# Method 1: Check for Acronis services
$AcronisServices = Get-Service -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like "*Acronis*" -or $_.Name -like "*Acronis*"
}
if ($AcronisServices) {
$AcronisServiceStatus = @()
foreach ($service in $AcronisServices) {
$AcronisServiceStatus += "$($service.DisplayName): $($service.Status)"
}
# Method 2: Check for Acronis in registry
$AcronisRegistryPaths = @(
"HKLM:\SOFTWARE\Acronis",
"HKLM:\SOFTWARE\WOW6432Node\Acronis",
"HKLM:\SOFTWARE\Acronis\BackupAndRecovery",
"HKLM:\SOFTWARE\Acronis\TrueImage"
)
$AcronisRegistryInfo = @()
foreach ($regPath in $AcronisRegistryPaths) {
if (Test-Path $regPath) {
try {
$regItems = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
if ($regItems) {
$AcronisRegistryInfo += "Found: $regPath"
}
} catch {
# Silently continue if registry access fails
}
}
}
# Method 3: Check for Acronis in installed applications
$AcronisApps = @()
$RegPaths = @(
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($Path in $RegPaths) {
if (Test-Path $Path) {
$Apps = Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object {
$null -ne $_.DisplayName -and $_.DisplayName -match "Acronis"
}
foreach ($App in $Apps) {
$AcronisApps += "$($App.DisplayName) v$($App.DisplayVersion)"
}
}
}
# Method 4: Check for Acronis processes
$AcronisProcesses = Get-Process -ErrorAction SilentlyContinue | Where-Object {
$_.ProcessName -like "*Acronis*" -or $_.Path -like "*Acronis*"
} | Select-Object -First 5
$AcronisProcessList = if ($AcronisProcesses) {
($AcronisProcesses | ForEach-Object { $_.ProcessName }) -join ", "
} else {
"No active Acronis processes detected"
}
# Determine overall Acronis status
$RunningServices = $AcronisServices | Where-Object { $_.Status -eq "Running" }
$AcronisStatus = if ($RunningServices.Count -gt 0) { "Active" } else { "Installed but not running" }
$BackupInfo += [PSCustomObject]@{
BackupName = "Acronis Backup"
Status = $AcronisStatus
DetectionMethod = "Multiple checks"
Services = if ($AcronisServiceStatus.Count -gt 0) { $AcronisServiceStatus -join "; " } else { "None found" }
InstalledApps = if ($AcronisApps.Count -gt 0) { $AcronisApps -join "; " } else { "None found" }
Processes = $AcronisProcessList
RegistryPaths = if ($AcronisRegistryInfo.Count -gt 0) { $AcronisRegistryInfo -join "; " } else { "None found" }
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = if ($AcronisStatus -eq "Active") { "Backup solution is active and protecting data" } else { "Backup solution is installed but may not be running backups" }
}
Write-Host " [+] Acronis Backup detected: $AcronisStatus" -ForegroundColor Green
} else {
# Check for other backup solutions (extensible)
$OtherBackupServices = Get-Service -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like "*Veeam*" -or
$_.DisplayName -like "*Backup*" -or
$_.DisplayName -like "*Veritas*" -or
$_.DisplayName -like "*Commvault*" -or
$_.DisplayName -like "*Windows Backup*" -or
$_.DisplayName -like "*Backup Exec*"
}
if ($OtherBackupServices) {
foreach ($service in $OtherBackupServices | Select-Object -First 3) {
$BackupInfo += [PSCustomObject]@{
BackupName = $service.DisplayName
Status = $service.Status
DetectionMethod = "Service detection"
Services = "$($service.DisplayName): $($service.Status)"
InstalledApps = "Check installed programs"
Processes = "Check specific backup console"
RegistryPaths = "Not scanned"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "Third-party backup solution detected. Check backup logs for status."
}
}
Write-Host " [+] Other backup solution(s) detected" -ForegroundColor Green
}
}
} catch {
$errorMsg = "Error checking for backup solutions: $_"
Log-Error -FunctionName "Get-BackupSolutionStatus" -ErrorMessage $errorMsg
}
# Check for Windows built-in backup features
try {
# Check for Windows Server Backup (if server)
$WSBService = Get-Service -Name "wbengine" -ErrorAction SilentlyContinue
if ($WSBService) {
$BackupInfo += [PSCustomObject]@{
BackupName = "Windows Server Backup"
Status = $WSBService.Status
DetectionMethod = "Service detection"
Services = "$($WSBService.DisplayName): $($WSBService.Status)"
InstalledApps = "Windows Server Backup feature"
Processes = "wbengine.exe"
RegistryPaths = "Windows component"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "Built-in Windows Server Backup detected"
}
Write-Host " [+] Windows Server Backup detected" -ForegroundColor Green
}
} catch {
# Silent fail for this check
}
# If no backup solution found, add a placeholder
if ($BackupInfo.Count -eq 0) {
$BackupInfo += [PSCustomObject]@{
BackupName = "No Backup Solution Detected"
Status = "Not Found"
DetectionMethod = "Service and registry scan"
Services = "None"
InstalledApps = "None"
Processes = "None"
RegistryPaths = "None"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "CRITICAL: No backup solution detected. Implement a backup strategy immediately."
}
Write-Host " [-] No backup solution detected - CRITICAL FINDING" -ForegroundColor Red
}
return $BackupInfo
}
# ===============================
# CUSTOM REPORT SECTION FOR EDR AND BACKUP
# ===============================
function Add-EDRSection {
<#
.SYNOPSIS
Adds a formatted EDR status section to the report.
#>
param (
[Parameter(Mandatory = $true)]
[array]$EDRInfo
)
# Determine risk level based on EDR status
$ActiveEDR = $EDRInfo | Where-Object { $_.Status -match "Active|Running" }
$NoEDR = $EDRInfo | Where-Object { $_.EDRName -match "No EDR Detected" }
if ($NoEDR) {
$RiskLevel = "high"
$RiskText = "No EDR"
} elseif (-not $ActiveEDR) {
$RiskLevel = "medium"
$RiskText = "Inactive"
} else {
$RiskLevel = "low"
$RiskText = "Protected"
}
$SectionHtml = @"
<div class="card">
<details>
<summary>
<i class="fas fa-shield-virus"></i> Endpoint Detection & Response (EDR) Status
<span class='badge badge-$RiskLevel'>$RiskText</span>
</summary>
<div class="summary-content">
<p><strong>Importance:</strong> EDR solutions provide advanced threat detection, investigation, and response capabilities beyond traditional antivirus.</p>
<table>
<thead>
<tr>
<th>EDR Name</th>
<th>Status</th>
<th>Detection Method</th>
<th>Services</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
"@
foreach ($EDR in $EDRInfo) {
$StatusClass = if ($EDR.Status -match "Active|Running") { "status-active" }
elseif ($EDR.Status -match "Not Found") { "status-critical" }
else { "status-inactive" }
$SectionHtml += @"
<tr>
<td><strong>$($EDR.EDRName)</strong></td>
<td><span class="$StatusClass">$($EDR.Status)</span></td>
<td>$($EDR.DetectionMethod)</td>
<td>$($EDR.Services)</td>
<td>$($EDR.Notes)</td>
</tr>
"@
}
$SectionHtml += @"
</tbody>
</table>
<div class="remediation" style="margin-top: 15px;">
<strong>EDR Best Practices:</strong>
<ul style="margin-top: 5px; margin-left: 20px;">
<li>Ensure EDR is actively monitoring all endpoints</li>
<li>Regularly review EDR alerts and incidents</li>
<li>Keep EDR signatures and engine updated</li>
<li>Integrate EDR with SIEM for centralized monitoring</li>
<li>Test EDR detection capabilities regularly</li>
</ul>
</div>
<div style="margin-top: 15px; padding: 12px; background: rgba(59, 130, 246, 0.1); border-radius: 8px; border-left: 4px solid var(--info);">
<strong><i class="fas fa-lightbulb"></i> Security Impact:</strong>
<p style="margin-top: 5px;">
$(if ($ActiveEDR) {
"✅ <strong>Enhanced Protection:</strong> Active EDR provides advanced threat detection, behavioral analysis, and incident response capabilities."
} elseif ($NoEDR) {
"🔴 <strong>High Risk:</strong> Without EDR, the system relies only on basic antivirus which may miss advanced threats like fileless malware and zero-day attacks."
} else {
"⚠️ <strong>Reduced Protection:</strong> EDR is installed but not active. Enable it to restore advanced threat protection."
})
</p>
</div>
</div>
</details>
</div>
"@
return $SectionHtml
}
function Add-BackupSection {
<#
.SYNOPSIS
Adds a formatted Backup solution status section to the report.
#>
param (
[Parameter(Mandatory = $true)]
[array]$BackupInfo
)
# Determine risk level based on backup status
$ActiveBackup = $BackupInfo | Where-Object { $_.Status -match "Active|Running" }
$NoBackup = $BackupInfo | Where-Object { $_.BackupName -match "No Backup Solution Detected" }
if ($NoBackup) {
$RiskLevel = "high"
$RiskText = "CRITICAL"
} elseif (-not $ActiveBackup) {
$RiskLevel = "medium"
$RiskText = "Inactive"
} else {
$RiskLevel = "low"
$RiskText = "Protected"
}
$SectionHtml = @"
<div class="card">
<details>
<summary>
<i class="fas fa-database"></i> Backup Solution Status
<span class='badge badge-$RiskLevel'>$RiskText</span>
</summary>
<div class="summary-content">
<p><strong>Importance:</strong> Regular backups are essential for disaster recovery, ransomware protection, and data integrity.</p>
<table>
<thead>
<tr>
<th>Backup Solution</th>
<th>Status</th>
<th>Detection Method</th>
<th>Installed Components</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
"@
foreach ($Backup in $BackupInfo) {
$StatusClass = if ($Backup.Status -match "Active|Running") { "status-active" }
elseif ($Backup.Status -match "Not Found") { "status-critical" }
else { "status-inactive" }
$SectionHtml += @"
<tr>
<td><strong>$($Backup.BackupName)</strong></td>
<td><span class="$StatusClass">$($Backup.Status)</span></td>
<td>$($Backup.DetectionMethod)</td>
<td>$($Backup.InstalledApps)</td>
<td>$($Backup.Notes)</td>
</tr>
"@
}
$SectionHtml += @"
</tbody>
</table>
<div class="remediation" style="margin-top: 15px;">
<strong>Backup Best Practices (3-2-1 Rule):</strong>
<ul style="margin-top: 5px; margin-left: 20px;">
<li><strong>3 Copies:</strong> Keep at least 3 copies of your data</li>
<li><strong>2 Different Media:</strong> Store copies on at least 2 different types of media</li>
<li><strong>1 Offsite:</strong> Keep at least 1 copy offsite (cloud or physical)</li>
<li>Test restore procedures regularly (quarterly minimum)</li>
<li>Encrypt backups containing sensitive data</li>
<li>Monitor backup job success/failure alerts</li>
</ul>
</div>
<div style="margin-top: 15px; padding: 12px; background: rgba(59, 130, 246, 0.1); border-radius: 8px; border-left: 4px solid var(--info);">
<strong><i class="fas fa-lightbulb"></i> Business Impact:</strong>
<p style="margin-top: 5px;">
$(if ($ActiveBackup) {
"✅ <strong>Data Protected:</strong> Active backup solution provides protection against data loss from ransomware, hardware failure, or accidental deletion."
} elseif ($NoBackup) {
"🔴 <strong>Extreme Risk:</strong> NO BACKUPS DETECTED! Data loss would be catastrophic. Implement a backup solution IMMEDIATELY."
} else {
"⚠️ <strong>Risk of Data Loss:</strong> Backup solution is installed but not active. Start backups immediately to protect data."
})
</p>
</div>
<div style="margin-top: 15px; padding: 12px; background: rgba(245, 158, 11, 0.1); border-radius: 8px; border-left: 4px solid var(--warning);">
<strong><i class="fas fa-exclamation-triangle"></i> Ransomware Consideration:</strong>
<p style="margin-top: 5px;">Ensure backups are immutable or air-gapped to prevent ransomware from encrypting backup files. Test that you can restore from backups without the backup software itself.</p>
</div>
</div>
</details>
</div>
"@
return $SectionHtml
}
# ===============================
# HTML REPORT STYLING & HEADER (with error reporting section)
# ===============================
$HtmlHeader = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CloudTechtiq | Security Audit Report</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #0A2540;
--secondary: #0055A4;
--success: #10B981;
--warning: #F59E0B;
--danger: #DC2626;
--info: #3B82F6;
--light: #F5F7FA;
--dark: #1F2937;
--gray: #6B7280;
--border: #E5E7EB;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', system-ui, sans-serif; }
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
color: #333;
line-height: 1.6;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 30px auto;
background: white;
border-radius: 20px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.1);
overflow: hidden;
border: 1px solid var(--border);
}
.header {
background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 40px;
text-align: center;
position: relative;
}
.header h1 {
font-size: 2.8rem;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 5px;
}
.badge {
display: inline-block;
padding: 6px 14px;
border-radius: 30px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.3px;
}
.badge-low { background: var(--success); color: white; }
.badge-medium { background: var(--warning); color: white; }
.badge-high { background: var(--danger); color: white; }
.badge-info { background: var(--info); color: white; }
.score-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
min-width: 50px;
text-align: center;
}
.score-10 { background: #10B981; color: white; }
.score-8 { background: #34D399; color: white; }
.score-6 { background: #F59E0B; color: white; }
.score-4 { background: #F97316; color: white; }
.score-2 { background: #DC2626; color: white; }
.score-0 { background: #6B7280; color: white; }
.controls {
padding: 20px 40px;
background: var(--light);
border-bottom: 1px solid var(--border);
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.btn-primary {
background: var(--secondary);
color: white;
}
.btn-primary:hover { background: var(--primary); transform: translateY(-2px); }
.btn-success {
background: var(--success);
color: white;
}
.btn-warning {
background: var(--warning);
color: white;
}
.content { padding: 30px 40px; }
.card {
background: white;
border-radius: 16px;
padding: 0;
margin-bottom: 30px;
border: 1px solid var(--border);
overflow: hidden;
transition: box-shadow 0.3s;
}
.card:hover { box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); }
details {
border-bottom: 1px solid var(--border);
}
details:last-of-type { border-bottom: none; }
summary {
padding: 24px 30px;
font-size: 1.3rem;
font-weight: 700;
color: var(--primary);
cursor: pointer;
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
}
summary::-webkit-details-marker { display: none; }
summary:hover { background: rgba(10, 37, 64, 0.04); }
summary i { margin-right: 15px; color: var(--secondary); }
.summary-content {
padding: 0 30px 30px 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th {
background: var(--light);
color: var(--dark);
font-weight: 700;
text-align: left;
padding: 16px;
border-bottom: 2px solid var(--border);
}
td {
padding: 16px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
tr:hover { background: rgba(10, 37, 64, 0.02); }
.finding-item {
padding: 20px;
margin-bottom: 15px;
border-radius: 10px;
border-left: 6px solid;
background: var(--light);
}
.finding-high { border-left-color: var(--danger); }
.finding-medium { border-left-color: var(--warning); }
.finding-low { border-left-color: var(--success); }
.finding-title { font-weight: 700; font-size: 1.1rem; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; }
.finding-desc { color: var(--dark); margin-bottom: 10px; }
.remediation { background: rgba(16, 185, 129, 0.1); padding: 15px; border-radius: 8px; margin-top: 10px; border-left: 4px solid var(--success); }
.error-section {
background: rgba(220, 38, 38, 0.1);
border-left: 6px solid var(--danger);
padding: 20px;
margin: 20px 0;
border-radius: 10px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 25px;
margin-top: 20px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 16px;
text-align: center;
border: 1px solid var(--border);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.03);
}
.stat-value {
font-size: 3rem;
font-weight: 800;
color: var(--secondary);
line-height: 1;
margin: 15px 0;
}
.stat-label {
font-size: 1rem;
color: var(--gray);
text-transform: uppercase;
letter-spacing: 1px;
}
.footer {
text-align: center;
padding: 30px;
color: var(--gray);
border-top: 1px solid var(--border);
background: var(--light);
font-size: 0.95rem;
}
.event-log {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px;
margin-top: 15px;
}
.event-item {
padding: 12px;
margin-bottom: 8px;
border-left: 4px solid;
background: var(--light);
border-radius: 6px;
}
.event-critical { border-left-color: var(--danger); }
.event-error { border-left-color: #EF4444; }
.event-warning { border-left-color: var(--warning); }
.event-info { border-left-color: var(--info); }
.event-success { border-left-color: var(--success); }
.event-time { font-size: 0.85rem; color: var(--gray); margin-bottom: 4px; }
.event-source { font-weight: 600; color: var(--dark); }
.event-message { margin-top: 5px; font-size: 0.95rem; }
@media (max-width: 768px) {
.container { margin: 10px; border-radius: 15px; }
.header { padding: 30px 20px; }
.header h1 { font-size: 2rem; flex-direction: column; gap: 10px; }
.controls, .content { padding: 20px; }
.stats-grid { grid-template-columns: 1fr; }
summary { padding: 20px; font-size: 1.1rem; }
.summary-content { padding: 0 20px 20px 20px; }
}
.pulse { animation: pulse 2s infinite; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.security-score-bar {
height: 10px;
background: #E5E7EB;
border-radius: 5px;
margin: 10px 0;
overflow: hidden;
}
.security-score-fill {
height: 100%;
border-radius: 5px;
transition: width 0.5s ease;
}
.score-green { background: var(--success); }
.score-yellow { background: var(--warning); }
.score-red { background: var(--danger); }
.status-active { color: var(--success); font-weight: 600; }
.status-inactive { color: var(--warning); font-weight: 600; }
.status-critical { color: var(--danger); font-weight: 600; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-shield-alt"></i> Security Audit Report</h1>
<p>Comprehensive analysis of system configuration and security posture</p>
<p>Generated: <script>document.write(new Date().toLocaleString());</script></p>
<p>Target System: <span id="targetSystem"></span></p>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="toggleAll(true)">
<i class="fas fa-expand"></i> Expand All
</button>
<button class="btn btn-success" onclick="toggleAll(false)">
<i class="fas fa-compress"></i> Collapse All
</button>
<button class="btn btn-warning" onclick="window.print()">
<i class="fas fa-print"></i> Print Report
</button>
</div>
<div class="content">
<!-- CONTENT WILL BE INJECTED HERE BY POWERSHELL -->
<!-- ERROR SECTION WILL BE INJECTED HERE IF ERRORS OCCURRED -->
</div>
<div class="footer">
<p><i class="fas fa-lock"></i> <strong>CloudTechtiq Pvt. Ltd.</strong> | Confidential Security Audit</p>
<p>© <script>document.write(new Date().getFullYear());</script> | This report is generated automatically. Findings should be reviewed by a security professional.</p>
</div>
</div>
<script>
function toggleAll(state) {
document.querySelectorAll('details').forEach(detail => {
detail.open = state;
});
}
function highlightRows(searchTerm, className) {
const rows = document.querySelectorAll('tr');
rows.forEach(row => {
if (row.textContent.toLowerCase().includes(searchTerm.toLowerCase())) {
row.classList.add(className);
}
});
}
function filterTable(tableId, searchId) {
const input = document.getElementById(searchId);
const filter = input.value.toUpperCase();
const table = document.getElementById(tableId);
const tr = table.getElementsByTagName("tr");
for (let i = 1; i < tr.length; i++) {
const td = tr[i].getElementsByTagName("td");
let showRow = false;
for (let j = 0; j < td.length; j++) {
if (td[j]) {
const txtValue = td[j].textContent || td[j].innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
showRow = true;
break;
}
}
}
tr[i].style.display = showRow ? "" : "none";
}
}
window.addEventListener('load', function() {
setTimeout(() => toggleAll(false), 100);
});
</script>
</body>
</html>
"@
# ===============================
# MAIN EXECUTION WITH ENHANCED ERROR HANDLING
# ===============================
Write-Host "`n"
Write-Host "╔══════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ CloudTechtiq Enhanced Security Auditor v4.2 ║" -ForegroundColor Cyan
Write-Host "║ Now with EDR & Backup Solution Detection ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host "`n"
Write-Host "[i] Starting comprehensive security audit..." -ForegroundColor Yellow
Write-Host "[-] Script will continue even if some components fail." -ForegroundColor Gray
# Initialize global variables with defaults
$Global:InstalledApps = @()
$Global:StartupApps = @()
$Global:SecurityEvents = @()
$Global:EDRInfo = @()
$Global:BackupInfo = @()
# Collect all data with error handling - NOW WITH 12 STEPS
$dataCollectionSteps = @(
@{Name = "system information"; ScriptBlock = { Get-SystemInformation }; Var = "SystemInfo" }
@{Name = "disk usage"; ScriptBlock = { Get-DiskInformation }; Var = "DiskInfo" }
@{Name = "network configuration"; ScriptBlock = { Get-NetworkInformation }; Var = "NetworkInfo" }
@{Name = "local user accounts"; ScriptBlock = { Get-LocalUserAccounts }; Var = "LocalUsers" }
@{Name = "Administrators group members"; ScriptBlock = { Get-EnhancedGroupMembers -GroupName "Administrators" }; Var = "Admins" }
@{Name = "Remote Desktop Users group members"; ScriptBlock = { Get-EnhancedGroupMembers -GroupName "Remote Desktop Users" }; Var = "RDPUsers" }
@{Name = "security configuration"; ScriptBlock = { Get-SecurityConfiguration }; Var = "SecurityConfig" }
@{Name = "installed applications"; ScriptBlock = { Get-InstalledApplications }; Var = "InstalledApps" }
@{Name = "startup applications"; ScriptBlock = { Get-StartupApplications }; Var = "StartupApps" }
@{Name = "security events"; ScriptBlock = { Get-SecurityEvents }; Var = "SecurityEvents" }
@{Name = "EDR solution status"; ScriptBlock = { Get-EDRStatus }; Var = "EDRInfo" }
@{Name = "backup solution status"; ScriptBlock = { Get-BackupSolutionStatus }; Var = "BackupInfo" }
)
$stepCount = 1
foreach ($step in $dataCollectionSteps) {
Write-Host "[$stepCount/$($dataCollectionSteps.Count)] Collecting $($step.Name)..." -ForegroundColor Green
try {
$result = & $step.ScriptBlock
Set-Variable -Name $step.Var -Value $result -Scope Script
if ($step.Var -eq "InstalledApps") { $Global:InstalledApps = $result }
if ($step.Var -eq "StartupApps") { $Global:StartupApps = $result }
if ($step.Var -eq "SecurityEvents") { $Global:SecurityEvents = $result }
if ($step.Var -eq "EDRInfo") { $Global:EDRInfo = $result }
if ($step.Var -eq "BackupInfo") { $Global:BackupInfo = $result }
} catch {
$errorMsg = "Error collecting $($step.Name): $_"
Log-Error -FunctionName "Main-DataCollection" -ErrorMessage $errorMsg
# Set default values
switch ($step.Var) {
"SystemInfo" { $SystemInfo = [PSCustomObject]@{ Hostname = $env:COMPUTERNAME; 'OS Name' = "Unknown" } }
"DiskInfo" { $DiskInfo = @() }
"NetworkInfo" { $NetworkInfo = @() }
"LocalUsers" { $LocalUsers = @() }
"Admins" { $Admins = @() }
"RDPUsers" { $RDPUsers = @() }
"SecurityConfig" { $SecurityConfig = [PSCustomObject]@{} }
"InstalledApps" { $Global:InstalledApps = @() }
"StartupApps" { $Global:StartupApps = @() }
"SecurityEvents" { $Global:SecurityEvents = @() }
"EDRInfo" { $Global:EDRInfo = @(
[PSCustomObject]@{
EDRName = "Error retrieving EDR info"
Status = "Error"
DetectionMethod = "Error"
Services = "Error"
Processes = "Error"
RegistryPaths = "Error"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "Could not retrieve EDR information due to an error"
}
)}
"BackupInfo" { $Global:BackupInfo = @(
[PSCustomObject]@{
BackupName = "Error retrieving backup info"
Status = "Error"
DetectionMethod = "Error"
Services = "Error"
InstalledApps = "Error"
Processes = "Error"
RegistryPaths = "Error"
LastChecked = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Notes = "Could not retrieve backup information due to an error"
}
)}
}
}
$stepCount++
}
Write-Host "`n[i] Analyzing collected data for security findings..." -ForegroundColor Yellow
try {
$SecurityFindings = Get-SecurityFindings
} catch {
$errorMsg = "Error generating security findings: $_"
Log-Error -FunctionName "Main-Findings" -ErrorMessage $errorMsg
$SecurityFindings = @(
[PSCustomObject]@{
Severity = 'High'
ID = 'SEC-ERR'
Title = 'Error in Security Analysis'
Description = "Could not generate security findings due to an error."
Remediation = 'Check system permissions and try running as Administrator.'
Category = 'System Error'
}
)
}
Write-Host "`n[i] Generating HTML report..." -ForegroundColor Yellow
# Build the report content with error section if needed
$ReportContent = ""
# Add error section if there were any errors during execution
if ($Script:ExecutionErrors.Count -gt 0) {
$errorSection = @"
<div class="card">
<details open>
<summary>
<i class="fas fa-exclamation-triangle"></i> Execution Errors & Warnings
<span class='badge badge-high'>Errors: $($Script:ExecutionErrors.Count)</span>
</summary>
<div class="summary-content">
<div class="error-section">
<p><strong>Note:</strong> The following errors occurred during script execution. The report was still generated, but some data may be incomplete.</p>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Function</th>
<th>Error Type</th>
<th>Message</th>
</tr>
</thead>
<tbody>
"@
# Fix: Use a different variable name to avoid conflict with PowerShell's $error variable
foreach ($err in $Script:ExecutionErrors) {
$errorSection += @"
<tr>
<td>$($err.Timestamp)</td>
<td>$($err.Function)</td>
<td><span class='badge badge-$(if ($err.Type -eq 'Error') { 'high' } else { 'medium' })'>$($err.Type)</span></td>
<td>$($err.Message)</td>
</tr>
"@
}
$errorSection += @"
</tbody>
</table>
<div class="remediation" style="margin-top: 15px;">
<strong>Recommended Actions:</strong>
<ul style="margin-top: 5px; margin-left: 20px;">
<li>Run the script as Administrator to access all system information</li>
<li>Check PowerShell execution policy: `Get-ExecutionPolicy`</li>
<li>Ensure you have appropriate permissions on the system</li>
<li>Some errors may be expected on locked-down systems</li>
</ul>
</div>
</div>
</div>
</details>
</div>
"@
$ReportContent += $errorSection
}
# Add stats/executive summary first
try {
$statsSection = Add-StatsSection -SystemInfo $SystemInfo -Admins $Admins -RDPUsers $RDPUsers -Findings $SecurityFindings -LocalUsers $LocalUsers -InstalledApps $Global:InstalledApps -StartupApps $Global:StartupApps -SecurityEvents $Global:SecurityEvents
$ReportContent += $statsSection
} catch {
$errorMsg = "Error generating stats section: $_"
Log-Error -FunctionName "Main-StatsSection" -ErrorMessage $errorMsg
$ReportContent += "<div class='error-section'><p>Error generating executive summary section.</p></div>"
}
# Add security findings
try {
$findingsSection = Add-ReportSection -Title "Security Findings & Recommendations" -Data $SecurityFindings -Icon "fas fa-exclamation-triangle" -RiskLevel "high" -Open
$ReportContent += $findingsSection
} catch {
$errorMsg = "Error generating findings section: $_"
Log-Error -FunctionName "Main-FindingsSection" -ErrorMessage $errorMsg
}
# Add EDR section
try {
$edrSection = Add-EDRSection -EDRInfo $Global:EDRInfo
$ReportContent += $edrSection
} catch {
$errorMsg = "Error generating EDR section: $_"
Log-Error -FunctionName "Main-EDRSection" -ErrorMessage $errorMsg
$ReportContent += "<div class='card'><div class='error-section'><p>Error generating EDR Status section.</p></div></div>"
}
# Add Backup section
try {
$backupSection = Add-BackupSection -BackupInfo $Global:BackupInfo
$ReportContent += $backupSection
} catch {
$errorMsg = "Error generating Backup section: $_"
Log-Error -FunctionName "Main-BackupSection" -ErrorMessage $errorMsg
$ReportContent += "<div class='card'><div class='error-section'><p>Error generating Backup Solution Status section.</p></div></div>"
}
# Try to add all other sections with individual error handling
$sectionDefinitions = @(
@{Title = "Installed Applications Security Analysis"; Data = $Global:InstalledApps; Icon = "fas fa-box"; RiskLevel = "medium"; Open = $true; Notes = "Security score: 10=Excellent, 5=Average, 1=Poor. Review low-score applications."; TableId = "applicationsTable"}
@{Title = "Startup Applications Review"; Data = $Global:StartupApps; Icon = "fas fa-play"; RiskLevel = "medium"; Notes = "Review 'Potentially Risky' startup items. 'Likely Safe' items are from known trusted vendors."; TableId = "startupTable"}
@{Title = "System Information"; Data = $SystemInfo; Icon = "fas fa-desktop"; RiskLevel = "low"}
@{Title = "Disk Information"; Data = $DiskInfo; Icon = "fas fa-hdd"; RiskLevel = "medium"; Notes = "Drives with less than 10% free space may cause system instability."}
@{Title = "Network Configuration"; Data = $NetworkInfo; Icon = "fas fa-network-wired"; RiskLevel = "medium"}
@{Title = "Local User Accounts"; Data = $LocalUsers; Icon = "fas fa-user"; RiskLevel = "medium"; Notes = "Check for disabled accounts and accounts with password never expires set."}
@{Title = "Administrators Group Members"; Data = $Admins; Icon = "fas fa-user-shield"; RiskLevel = "high"; Open = $true; Notes = "All users and groups with administrative privileges."}
@{Title = "Remote Desktop Users"; Data = $RDPUsers; Icon = "fas fa-desktop"; RiskLevel = "medium"; Notes = "Users and groups allowed to connect via Remote Desktop. Review for least privilege."}
)
foreach ($section in $sectionDefinitions) {
try {
$sectionHtml = Add-ReportSection @section
$ReportContent += $sectionHtml
} catch {
$errorMsg = "Error generating section '$($section.Title)': $_"
Log-Error -FunctionName "Main-Section-$($section.Title)" -ErrorMessage $errorMsg
$ReportContent += "<div class='card'><div class='error-section'><p>Error generating section: $($section.Title)</p></div></div>"
}
}
# Add security events section
try {
$eventsSection = Add-SecurityEventsSection -SecurityEvents $Global:SecurityEvents
$ReportContent += $eventsSection
} catch {
$errorMsg = "Error generating security events section: $_"
Log-Error -FunctionName "Main-SecurityEventsSection" -ErrorMessage $errorMsg
}
# Try to add security configuration sections
if ($SecurityConfig) {
$securitySections = @(
@{Title = "Firewall Configuration"; Data = $SecurityConfig.Firewall; Icon = "fas fa-fire"; RiskLevel = "high"}
@{Title = "Windows Defender Status"; Data = $SecurityConfig.Defender; Icon = "fas fa-shield-alt"; RiskLevel = "high"}
@{Title = "SMB Protocol Configuration"; Data = $SecurityConfig.SMB; Icon = "fas fa-exchange-alt"; RiskLevel = "medium"; Notes = "SMBv1 should be disabled due to security vulnerabilities."}
@{Title = "User Account Control (UAC)"; Data = $SecurityConfig.UAC; Icon = "fas fa-user-lock"; RiskLevel = "high"}
)
foreach ($section in $securitySections) {
try {
if ($section.Data) {
$sectionHtml = Add-ReportSection @section
$ReportContent += $sectionHtml
}
} catch {
$errorMsg = "Error generating security config section '$($section.Title)': $_"
Log-Error -FunctionName "Main-SecurityConfig-$($section.Title)" -ErrorMessage $errorMsg
}
}
}
# Add installed updates
try {
$Updates = Get-InstalledUpdates
$updatesSection = Add-ReportSection -Title "Recent Windows Updates" -Data $Updates -Icon "fas fa-sync-alt" -RiskLevel "low"
$ReportContent += $updatesSection
} catch {
$errorMsg = "Error generating updates section: $_"
Log-Error -FunctionName "Main-UpdatesSection" -ErrorMessage $errorMsg
}
# If it's a Domain Controller, add AD-specific checks
if ($IsDomainController) {
Write-Host "[+] Domain Controller detected - adding AD-specific checks..." -ForegroundColor Cyan
try {
$DCFindings = @(
[PSCustomObject]@{
Severity = 'Medium'
ID = 'DC-001'
Title = 'Domain Controller Detected'
Description = 'This system is a Domain Controller. Additional domain-level security review is recommended.'
Remediation = 'Perform comprehensive AD security audit including GPOs, account policies, and privileged groups.'
Category = 'Domain Security'
}
)
$DCSection = Add-ReportSection -Title "Domain Controller Information" -Data $DCFindings -Icon "fas fa-server" -RiskLevel "high" -Open
$ReportContent += $DCSection
} catch {
$errorMsg = "Error generating DC section: $_"
Log-Error -FunctionName "Main-DCSection" -ErrorMessage $errorMsg
}
}
# Combine header, content, and close HTML
try {
$FullHtml = $HtmlHeader -replace '<!-- CONTENT WILL BE INJECTED HERE BY POWERSHELL -->', $ReportContent
$FullHtml = $FullHtml -replace '<span id="targetSystem"></span>', "$($env:COMPUTERNAME) ($(if($SystemInfo.'OS Name') { $SystemInfo.'OS Name' } else { 'Windows' }))"
# Add search functionality for tables
$SearchScript = @"
<script>
// Add search boxes to application and startup tables
document.addEventListener('DOMContentLoaded', function() {
const appsTable = document.querySelector('table[id="applicationsTable"]');
const startupTable = document.querySelector('table[id="startupTable"]');
if (appsTable) {
const appsSearch = document.createElement('div');
appsSearch.innerHTML = '<input type="text" id="searchApps" placeholder="Search applications..." style="padding: 8px; width: 100%; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 5px;">';
appsTable.parentNode.insertBefore(appsSearch, appsTable);
document.getElementById('searchApps').addEventListener('keyup', function() {
filterTable('applicationsTable', 'searchApps');
});
}
if (startupTable) {
const startupSearch = document.createElement('div');
startupSearch.innerHTML = '<input type="text" id="searchStartup" placeholder="Search startup items..." style="padding: 8px; width: 100%; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 5px;">';
startupTable.parentNode.insertBefore(startupSearch, startupTable);
document.getElementById('searchStartup').addEventListener('keyup', function() {
filterTable('startupTable', 'searchStartup');
});
}
});
</script>
"@
$FullHtml = $FullHtml -replace '</body>', "$SearchScript</body>"
# Save the report
$FullHtml | Out-File -FilePath $ReportPath -Encoding UTF8 -ErrorAction Stop
Write-Host "`n" + ("="*70) -ForegroundColor Green
Write-Host "✅ SECURITY AUDIT COMPLETED (WITH ERROR RECOVERY)!" -ForegroundColor Green
Write-Host ("="*70) -ForegroundColor Green
if ($Script:ExecutionErrors.Count -gt 0) {
Write-Host "⚠️ Note: $($Script:ExecutionErrors.Count) errors occurred during execution." -ForegroundColor Yellow
Write-Host " Check the 'Execution Errors & Warnings' section in the report." -ForegroundColor Yellow
}
} catch {
Write-Host "`n" + ("="*70) -ForegroundColor Red
Write-Host "❌ CRITICAL ERROR: Could not generate HTML report!" -ForegroundColor Red
Write-Host ("="*70) -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
# Try to create a simple error report
try {
$ErrorReport = @"
<html>
<head><title>CloudTechtiq - Audit Error</title></head>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h1 style="color: #d00;">❌ Security Audit Failed</h1>
<p>An error occurred while generating the security audit report.</p>
<div style="background: #fcc; padding: 15px; border-left: 5px solid #d00; margin: 20px 0;">
<h3>Error Details:</h3>
<p><strong>Message:</strong> $_</p>
<p><strong>Time:</strong> $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
<p><strong>Computer:</strong> $env:COMPUTERNAME</p>
</div>
<h3>Troubleshooting Steps:</h3>
<ol>
<li>Run PowerShell as Administrator</li>
<li>Check disk space on your Desktop</li>
<li>Verify PowerShell execution policy: <code>Get-ExecutionPolicy</code></li>
<li>Try running from a different location</li>
</ol>
<p>Contact support if the issue persists.</p>
</body>
</html>
"@
$ErrorReportPath = "$env:USERPROFILE\Desktop\CloudTechtiq_Audit_ERROR_$Date.html"
$ErrorReport | Out-File -FilePath $ErrorReportPath -Encoding UTF8
Write-Host "⚠️ Error report saved to: $ErrorReportPath" -ForegroundColor Yellow
} catch {
Write-Host "❌ Could not even create error report!" -ForegroundColor Red
}
exit 1
}
Write-Host "`n📋 REPORT SUMMARY:" -ForegroundColor White
Write-Host " - System: $(if($SystemInfo.Hostname) { $SystemInfo.Hostname } else { $env:COMPUTERNAME })" -ForegroundColor White
Write-Host " - Applications: $(if($Global:InstalledApps) { $Global:InstalledApps.Count } else { 0 }) analyzed" -ForegroundColor White
Write-Host " - Startup Items: $(if($Global:StartupApps) { $Global:StartupApps.Count } else { 0 }) checked" -ForegroundColor White
Write-Host " - Security Events: $(if($Global:SecurityEvents) { $Global:SecurityEvents.Count } else { 0 }) reviewed" -ForegroundColor White
Write-Host " - EDR Status: $(if($Global:EDRInfo -and $Global:EDRInfo[0].EDRName -notmatch 'Error|No EDR') { 'Detected' } else { 'Not detected' })" -ForegroundColor White
Write-Host " - Backup Status: $(if($Global:BackupInfo -and $Global:BackupInfo[0].BackupName -notmatch 'Error|No Backup') { 'Detected' } else { 'Not detected' })" -ForegroundColor White
Write-Host " - Total Findings: $(if($SecurityFindings) { $SecurityFindings.Count } else { 0 })" -ForegroundColor White
Write-Host "`n📄 Report saved to: $ReportPath" -ForegroundColor White
# Check for critical issues
if ($Global:BackupInfo -and $Global:BackupInfo[0].BackupName -match "No Backup Solution Detected") {
Write-Host "`n🔴 CRITICAL ISSUE: No backup solution detected!" -ForegroundColor Red
Write-Host " This is a severe risk. Implement a backup solution immediately." -ForegroundColor Red
}
if ($Global:EDRInfo -and $Global:EDRInfo[0].EDRName -match "No EDR Detected") {
Write-Host "`n⚠️ WARNING: No EDR solution detected" -ForegroundColor Yellow
Write-Host " Consider deploying an EDR for advanced threat protection." -ForegroundColor Yellow
}
Write-Host "`n🚀 NEXT STEPS:" -ForegroundColor White
Write-Host " 1. Open the HTML report for detailed analysis" -ForegroundColor White
Write-Host " 2. Review the 'Execution Errors' section if present" -ForegroundColor White
Write-Host " 3. Address any missing EDR or Backup solutions immediately" -ForegroundColor White
Write-Host " 4. Implement all recommended remediations" -ForegroundColor White
# Optional: Open the report automatically
try {
$OpenReport = Read-Host "`nOpen the report now? (Y/N)"
if ($OpenReport -eq 'Y' -or $OpenReport -eq 'y') {
Start-Process $ReportPath -ErrorAction SilentlyContinue
}
} catch {
Write-Host " [!] Could not open report automatically" -ForegroundColor Yellow
}
Write-Host "`n[i] Audit script completed. Thank you for using CloudTechtiq Security Auditor v4.2!" -ForegroundColor Cyan
Write-Host "[i] Script executed with $($Script:ExecutionErrors.Count) errors/warnings." -ForegroundColor $(if ($Script:ExecutionErrors.Count -gt 0) { "Yellow" } else { "Green" })