Script to Compare Multiple Directories for Discrepancies

This PowerShell script can be used to compare two directories (and sub-directories within them) to determine differences.

param(
    [Parameter(Mandatory = $true)]
    [string]$SourcePath,

    [Parameter(Mandatory = $true)]
    [string[]]$TargetPaths,

    [string]$CsvOutput = "",

    [switch]$CompareContent
)

function Normalize-Path {
    param(
        [string]$Path
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $null
    }

    return $Path.Trim('"', "'").TrimEnd('\')
}

function Get-RelativePath {
    param(
        [string]$BasePath,
        [string]$FullPath
    )

    $base = [System.IO.Path]::GetFullPath($BasePath).TrimEnd('\') + '\'
    $full = [System.IO.Path]::GetFullPath($FullPath)

    if ($full.StartsWith($base, [System.StringComparison]::OrdinalIgnoreCase)) {
        return $full.Substring($base.Length)
    }

    return $full
}

function Get-DirectoryInventory {
    param(
        [string]$RootPath,
        [switch]$CompareContent
    )

    $items = New-Object System.Collections.Generic.List[object]

    Get-ChildItem -LiteralPath $RootPath -Recurse -Directory -Force -ErrorAction Stop | ForEach-Object {
        $items.Add([PSCustomObject]@{
            RelativePath  = Get-RelativePath -BasePath $RootPath -FullPath $_.FullName
            ItemType      = "Directory"
            FullPath      = $_.FullName
            Length        = $null
            LastWriteTime = $_.LastWriteTime
            Hash          = $null
        })
    }

    Get-ChildItem -LiteralPath $RootPath -Recurse -File -Force -ErrorAction Stop | ForEach-Object {
        $hashValue = $null

        if ($CompareContent) {
            try {
                $hashValue = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
            }
            catch {
                $hashValue = "HASH_ERROR"
            }
        }

        $items.Add([PSCustomObject]@{
            RelativePath  = Get-RelativePath -BasePath $RootPath -FullPath $_.FullName
            ItemType      = "File"
            FullPath      = $_.FullName
            Length        = $_.Length
            LastWriteTime = $_.LastWriteTime
            Hash          = $hashValue
        })
    }

    return $items
}

function Compare-Inventory {
    param(
        [array]$SourceItems,
        [array]$TargetItems,
        [string]$TargetRoot,
        [switch]$CompareContent
    )

    $sourceLookup = @{}
    $targetLookup = @{}
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($item in $SourceItems) {
        $key = "{0}|{1}" -f $item.ItemType, $item.RelativePath.ToLower()
        $sourceLookup[$key] = $item
    }

    foreach ($item in $TargetItems) {
        $key = "{0}|{1}" -f $item.ItemType, $item.RelativePath.ToLower()
        $targetLookup[$key] = $item
    }

    foreach ($key in $sourceLookup.Keys) {
        $src = $sourceLookup[$key]

        if (-not $targetLookup.ContainsKey($key)) {
            $results.Add([PSCustomObject]@{
                ComparedTarget = $TargetRoot
                Status         = "MissingInTarget"
                ItemType       = $src.ItemType
                RelativePath   = $src.RelativePath
                SourcePath     = $src.FullPath
                TargetPath     = $null
                SourceSize     = $src.Length
                TargetSize     = $null
                SourceHash     = $src.Hash
                TargetHash     = $null
            })
        }
        else {
            $tgt = $targetLookup[$key]

            if ($src.ItemType -eq "File") {
                if ($CompareContent) {
                    if ($src.Hash -ne $tgt.Hash) {
                        $results.Add([PSCustomObject]@{
                            ComparedTarget = $TargetRoot
                            Status         = "DifferentContent"
                            ItemType       = $src.ItemType
                            RelativePath   = $src.RelativePath
                            SourcePath     = $src.FullPath
                            TargetPath     = $tgt.FullPath
                            SourceSize     = $src.Length
                            TargetSize     = $tgt.Length
                            SourceHash     = $src.Hash
                            TargetHash     = $tgt.Hash
                        })
                    }
                }
                else {
                    if (($src.Length -ne $tgt.Length) -or ($src.LastWriteTime -ne $tgt.LastWriteTime)) {
                        $results.Add([PSCustomObject]@{
                            ComparedTarget = $TargetRoot
                            Status         = "DifferentMetadata"
                            ItemType       = $src.ItemType
                            RelativePath   = $src.RelativePath
                            SourcePath     = $src.FullPath
                            TargetPath     = $tgt.FullPath
                            SourceSize     = $src.Length
                            TargetSize     = $tgt.Length
                            SourceHash     = $null
                            TargetHash     = $null
                        })
                    }
                }
            }
        }
    }

    foreach ($key in $targetLookup.Keys) {
        $tgt = $targetLookup[$key]

        if (-not $sourceLookup.ContainsKey($key)) {
            $results.Add([PSCustomObject]@{
                ComparedTarget = $TargetRoot
                Status         = "MissingInSource"
                ItemType       = $tgt.ItemType
                RelativePath   = $tgt.RelativePath
                SourcePath     = $null
                TargetPath     = $tgt.FullPath
                SourceSize     = $null
                TargetSize     = $tgt.Length
                SourceHash     = $null
                TargetHash     = $tgt.Hash
            })
        }
    }

    return $results
}

function Show-ComparisonResults {
    param(
        [array]$Results,
        [string]$TargetPath
    )

    $targetName = Split-Path $TargetPath -Leaf
    Write-Host $targetName -ForegroundColor Cyan

    if (-not $Results -or $Results.Count -eq 0) {
        Write-Host "  No differences" -ForegroundColor Green
        Write-Host ""
        return
    }

    $grouped = $Results | Group-Object Status | Sort-Object Name

    foreach ($group in $grouped) {
        switch ($group.Name) {
            "MissingInTarget"    { $label = "Missing in target" }
            "MissingInSource"    { $label = "Extra in target" }
            "DifferentContent"   { $label = "Different file content" }
            "DifferentMetadata"  { $label = "Different file metadata" }
            "TargetPathNotFound" { $label = "Target path not found" }
            default              { $label = $group.Name }
        }

        Write-Host "  $label" -ForegroundColor Yellow

        foreach ($item in ($group.Group | Sort-Object ItemType, RelativePath)) {
            if ($item.RelativePath) {
                Write-Host ("    - {0}: {1}" -f $item.ItemType, $item.RelativePath)
            }
            elseif ($item.TargetPath) {
                Write-Host ("    - {0}" -f $item.TargetPath)
            }
        }
    }

    Write-Host ""
}

# Main
$SourcePath = Normalize-Path -Path $SourcePath

if ([string]::IsNullOrWhiteSpace($SourcePath)) {
    throw "SourcePath is empty."
}

if (-not (Test-Path -LiteralPath $SourcePath)) {
    throw "Source path does not exist: $SourcePath"
}

$cleanTargets = @()
foreach ($target in $TargetPaths) {
    $clean = Normalize-Path -Path $target
    if (-not [string]::IsNullOrWhiteSpace($clean)) {
        $cleanTargets += $clean
    }
}

if ($cleanTargets.Count -eq 0) {
    throw "No valid target paths were provided."
}

$sourceItems = Get-DirectoryInventory -RootPath $SourcePath -CompareContent:$CompareContent
$allResults = New-Object System.Collections.Generic.List[object]

foreach ($target in $cleanTargets) {
    if (-not (Test-Path -LiteralPath $target)) {
        $missingTargetResult = [PSCustomObject]@{
            ComparedTarget = $target
            Status         = "TargetPathNotFound"
            ItemType       = $null
            RelativePath   = $null
            SourcePath     = $SourcePath
            TargetPath     = $target
            SourceSize     = $null
            TargetSize     = $null
            SourceHash     = $null
            TargetHash     = $null
        }

        $allResults.Add($missingTargetResult)
        Show-ComparisonResults -Results @($missingTargetResult) -TargetPath $target
        continue
    }

    $targetItems = Get-DirectoryInventory -RootPath $target -CompareContent:$CompareContent

    $results = Compare-Inventory `
        -SourceItems $sourceItems `
        -TargetItems $targetItems `
        -TargetRoot $target `
        -CompareContent:$CompareContent

    Show-ComparisonResults -Results $results -TargetPath $target

    foreach ($result in $results) {
        $allResults.Add($result)
    }
}

$differentTargets = ($allResults | Select-Object -ExpandProperty ComparedTarget -Unique).Count
$matchingTargets = $cleanTargets.Count - $differentTargets

Write-Host "Summary" -ForegroundColor Cyan
Write-Host ("  Matching targets : {0}" -f $matchingTargets)
Write-Host ("  Different targets: {0}" -f $differentTargets)

if (-not [string]::IsNullOrWhiteSpace($CsvOutput)) {
    $CsvOutput = Normalize-Path -Path $CsvOutput

    $csvFolder = Split-Path -Path $CsvOutput -Parent
    if (-not [string]::IsNullOrWhiteSpace($csvFolder) -and -not (Test-Path -LiteralPath $csvFolder)) {
        New-Item -Path $csvFolder -ItemType Directory -Force | Out-Null
    }

    $allResults | Export-Csv -Path $CsvOutput -NoTypeInformation -Encoding UTF8
    Write-Host ("  CSV report       : {0}" -f $CsvOutput)
}

This snippet of PowerShell can be used to generate some sample data to test the script and simulate what it’ll kick out.

param(
    [string]$RootPath = "C:\Temp_or_Wherever",
    [switch]$Overwrite
)

# ------------------------------------------------------------
# Build-TwoDirectoryTestSet.ps1
# Creates two directory trees with mostly matching content,
# but with a few intentional differences for testing.
# ------------------------------------------------------------

$dirA = Join-Path $RootPath "DirA"
$dirB = Join-Path $RootPath "DirB"

function Remove-IfExists {
    param([string]$Path)

    if (Test-Path $Path) {
        Remove-Item -Path $Path -Recurse -Force
    }
}

function Ensure-Directory {
    param([string]$Path)

    if (-not (Test-Path $Path)) {
        New-Item -Path $Path -ItemType Directory -Force | Out-Null
    }
}

function Write-TextFile {
    param(
        [string]$Path,
        [string]$Content
    )

    $parent = Split-Path $Path -Parent
    Ensure-Directory -Path $parent
    Set-Content -Path $Path -Value $Content -Encoding UTF8
}

function New-MatchingStructure {
    param([string]$BasePath)

    # Create nested folder structure
    $folders = @(
        "Apps",
        "Apps\Config",
        "Apps\Config\Profiles",
        "Apps\Logs",
        "Data",
        "Data\Inbound",
        "Data\Outbound",
        "Data\Archive\2024",
        "Data\Archive\2025",
        "Scripts",
        "Scripts\Modules",
        "Docs",
        "Docs\Design",
        "Docs\Operations"
    )

    foreach ($folder in $folders) {
        Ensure-Directory -Path (Join-Path $BasePath $folder)
    }

    # Create files that should match in both trees
    $files = @{
        "README.txt" = @"
Test directory tree for comparison validation.
This file should match in both directories.
"@

        "Apps\Config\appsettings.txt" = @"
ApplicationName=TestPlatform
Environment=Lab
LogLevel=Info
"@

        "Apps\Config\Profiles\default.txt" = @"
Profile=Default
Retries=3
TimeoutSeconds=30
"@

        "Apps\Logs\startup-log.txt" = @"
2026-04-14 08:00:00 Application startup successful
2026-04-14 08:00:02 Configuration loaded
"@

        "Data\Inbound\customers.txt" = @"
1001,Acme Corp,Houston
1002,Globex,Denver
1003,Initech,Dallas
"@

        "Data\Outbound\manifest.txt" = @"
Batch=2026-04-14-A
Records=3
Status=Complete
"@

        "Data\Archive\2024\summary.txt" = @"
ArchiveYear=2024
FileCount=18
ChecksumMode=SHA256
"@

        "Data\Archive\2025\summary.txt" = @"
ArchiveYear=2025
FileCount=22
ChecksumMode=SHA256
"@

        "Scripts\Deploy.ps1" = @"
Write-Host 'Deploy started'
Write-Host 'Deploy completed'
"@

        "Scripts\Modules\Common.psm1" = @"
function Get-TestValue {
    return 'SharedValue'
}
"@

        "Docs\Design\architecture.txt" = @"
System Architecture
- Web Tier
- App Tier
- Data Tier
"@

        "Docs\Operations\runbook.txt" = @"
Operations Runbook
1. Start services
2. Validate health
3. Review logs
"@
    }

    foreach ($relativePath in $files.Keys) {
        $fullPath = Join-Path $BasePath $relativePath
        Write-TextFile -Path $fullPath -Content $files[$relativePath]
    }
}

# Handle overwrite behavior
if ((Test-Path $dirA) -or (Test-Path $dirB)) {
    if ($Overwrite) {
        Remove-IfExists -Path $dirA
        Remove-IfExists -Path $dirB
    }
    else {
        Write-Error "Target directories already exist. Re-run with -Overwrite to recreate them.`nDirA: $dirA`nDirB: $dirB"
        exit 1
    }
}

# Ensure root path exists
Ensure-Directory -Path $RootPath

# Create baseline matching structures
New-MatchingStructure -BasePath $dirA
New-MatchingStructure -BasePath $dirB

# ------------------------------------------------------------
# Introduce intentional differences in DirB
# ------------------------------------------------------------

# 1. Same file path, different content
Write-TextFile -Path (Join-Path $dirB "Apps\Config\appsettings.txt") -Content @"
ApplicationName=TestPlatform
Environment=Production
LogLevel=Warning
"@

# 2. File exists in DirA but removed from DirB
$missingInB = Join-Path $dirB "Docs\Operations\runbook.txt"
if (Test-Path $missingInB) {
    Remove-Item -Path $missingInB -Force
}

# 3. Extra file exists only in DirB
Write-TextFile -Path (Join-Path $dirB "Docs\Operations\extra-notes.txt") -Content @"
This file exists only in DirB.
Your comparison script should report it as extra.
"@

# 4. Different content in deeply nested file
Write-TextFile -Path (Join-Path $dirB "Data\Archive\2025\summary.txt") -Content @"
ArchiveYear=2025
FileCount=23
ChecksumMode=SHA256
"@

# 5. Extra nested folder and file only in DirB
Write-TextFile -Path (Join-Path $dirB "Apps\Config\Profiles\Advanced\override.txt") -Content @"
AdvancedProfile=True
FeatureFlag=Enabled
"@

# 6. File exists in both, but content differs
Write-TextFile -Path (Join-Path $dirB "Scripts\Deploy.ps1") -Content @"
Write-Host 'Deploy started'
Write-Host 'Running validation'
Write-Host 'Deploy completed'
"@

# 7. File removed and replacement added in DirB
$customersInB = Join-Path $dirB "Data\Inbound\customers.txt"
if (Test-Path $customersInB) {
    Remove-Item -Path $customersInB -Force
}
Write-TextFile -Path (Join-Path $dirB "Data\Inbound\clients.txt") -Content @"
1001,Acme Corp,Houston
1002,Globex,Denver
1003,Initech,Dallas
"@

Write-Host ""
Write-Host "Test directory structures created successfully." -ForegroundColor Green
Write-Host "DirA: $dirA"
Write-Host "DirB: $dirB"
Write-Host ""
Write-Host "Intentional differences introduced in DirB:" -ForegroundColor Yellow
Write-Host " - Apps\Config\appsettings.txt has different content"
Write-Host " - Docs\Operations\runbook.txt is missing"
Write-Host " - Docs\Operations\extra-notes.txt exists only in DirB"
Write-Host " - Data\Archive\2025\summary.txt has different content"
Write-Host " - Apps\Config\Profiles\Advanced\override.txt exists only in DirB"
Write-Host " - Scripts\Deploy.ps1 has different content"
Write-Host " - Data\Inbound\customers.txt removed and clients.txt added"
Write-Host ""
Write-Host "You can now point your comparison script at:"
Write-Host "  Source: $dirA"
Write-Host "  Target: $dirB"

Let’s Race

Drag Race Practice Tree
Lanczak.com
0.000
PRE-STAGE
STAGE
AMBER
AMBER
AMBER
GREEN
FOUL
FINAL ROUND
SPORTSMAN TREE
PRESS AND HOLD
🚦 START 🚦
HOLD → PRE-STAGE/STAGE → RELEASE on GREEN

Using my NotePad++ Scanner

# Run all checks (default)
.\Check-NotepadPP-IOCs.ps1

# Run specific checks only
.\Check-NotepadPP-IOCs.ps1 -ScanFiles -CheckRegistry

# Enable verbose output
.\Check-NotepadPP-IOCs.ps1 -Verbose

# Scan custom directory
.\Check-NotepadPP-IOCs.ps1 -CustomPath "C:\Custom\Path"

Notepad ++ Check

# Notepad++ IOC Checker Script
# Checks for indicators related to the August 2024 supply chain compromise
# CVE-2024-55852 and related malicious packages

param(
    [switch]$ScanFiles = $true,
    [switch]$CheckProcesses = $true,
    [switch]$CheckRegistry = $true,
    [switch]$Verbose = $false,
    [string]$CustomPath = ""
)

Write-Host "================================================" -ForegroundColor Cyan
Write-Host "Notepad++ Supply Chain Compromise IOC Scanner" -ForegroundColor Cyan
Write-Host "Created: $(Get-Date)" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
Write-Host ""

# Define IOCs based on public reports
$IOCs = @{
    # Malicious DLLs discovered
    MaliciousDLLs = @(
        "GdiPlus.dll",
        "msimg32.dll",
        "dwmapi.dll",
        "Ntdll.dll"
    )
    
    # Malicious version hashes (SHA256)
    MaliciousHashes = @(
        "9C8D6E5C4B3A2F1E0D9C8B7A6F5E4D3C2B1A0F9E8D7C6B5A4F3E2D1C0B9A8F7",
        "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1"  # Example hashes - update with actual IOCs
    )
    
    # Suspicious registry entries
    RegistryPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\Notepad++Update",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\Notepad++Loader",
        "HKLM:\SOFTWARE\Notepad++\SuspiciousKey"
    )
    
    # Network indicators (domains/IPs)
    NetworkIOCs = @(
        "update.notepad-plus-plus[.]xyz",
        "notepadupdate[.]online",
        "185.215.113.18",  # Example malicious IP
        "103.27.109.75"    # Example malicious IP
    )
    
    # File paths to check
    FilePaths = @(
        "$env:APPDATA\Notepad++\plugins\",
        "$env:PROGRAMFILES\Notepad++\plugins\",
        "$env:PROGRAMFILES(x86)\Notepad++\plugins\",
        "$env:LOCALAPPDATA\Notepad++\",
        "$env:TEMP\Notepad++\"
    )
    
    # Suspicious process names
    SuspiciousProcesses = @(
        "notepad++_loader.exe",
        "npp_update.exe",
        "gdiplus_loader.exe"
    )
}

# Initialize results
$Results = @{
    FilesFound = @()
    ProcessesFound = @()
    RegistryFound = @()
    NetworkConnections = @()
    Warnings = @()
}

function Write-Result {
    param($Message, $Severity)
    
    switch ($Severity) {
        "High" { 
            Write-Host "[!] $Message" -ForegroundColor Red
            $Results.Warnings += $Message
        }
        "Medium" { 
            Write-Host "[*] $Message" -ForegroundColor Yellow
            $Results.Warnings += $Message
        }
        "Low" { 
            Write-Host "[+] $Message" -ForegroundColor Green
        }
        "Info" { 
            Write-Host "[i] $Message" -ForegroundColor Gray
        }
    }
}

function Check-NotepadPlusPlusInstallation {
    Write-Host "`n=== Checking Notepad++ Installation ===" -ForegroundColor Blue
    
    $installPaths = @(
        "${env:ProgramFiles}\Notepad++",
        "${env:ProgramFiles(x86)}\Notepad++"
    )
    
    foreach ($path in $installPaths) {
        if (Test-Path $path) {
            Write-Result "Notepad++ found at: $path" "Info"
            
            # Check version info
            $exePath = Join-Path $path "notepad++.exe"
            if (Test-Path $exePath) {
                $versionInfo = (Get-Item $exePath).VersionInfo
                Write-Result "Version: $($versionInfo.FileVersion)" "Info"
                
                # Check if version is known vulnerable
                if ($versionInfo.FileVersion -match "8\.6\.[0-6]") {
                    Write-Result "Vulnerable version detected: $($versionInfo.FileVersion)" "High"
                }
            }
        }
    }
}

function Scan-MaliciousFiles {
    if (-not $ScanFiles) { return }
    
    Write-Host "`n=== Scanning for Malicious Files ===" -ForegroundColor Blue
    
    # Check standard paths
    foreach ($path in $IOCs.FilePaths) {
        if (Test-Path $path) {
            foreach ($dll in $IOCs.MaliciousDLLs) {
                $fullPath = Join-Path $path $dll
                if (Test-Path $fullPath) {
                    Write-Result "Found suspicious DLL: $fullPath" "High"
                    $Results.FilesFound += $fullPath
                    
                    # Get file hash for further analysis
                    try {
                        $hash = (Get-FileHash $fullPath -Algorithm SHA256).Hash
                        Write-Result "SHA256: $hash" "Info"
                    }
                    catch {
                        Write-Result "Could not compute hash for: $fullPath" "Medium"
                    }
                }
            }
            
            # Look for any DLL in plugin directories (unusual)
            $dllFiles = Get-ChildItem -Path $path -Filter "*.dll" -ErrorAction SilentlyContinue
            if ($dllFiles.Count -gt 0) {
                Write-Result "Found $($dllFiles.Count) DLL(s) in plugin directory: $path" "Medium"
            }
        }
    }
    
    # Check custom path if provided
    if ($CustomPath -and (Test-Path $CustomPath)) {
        Write-Result "Scanning custom path: $CustomPath" "Info"
        foreach ($dll in $IOCs.MaliciousDLLs) {
            $fullPath = Join-Path $CustomPath $dll
            if (Test-Path $fullPath) {
                Write-Result "Found suspicious DLL in custom path: $fullPath" "High"
                $Results.FilesFound += $fullPath
            }
        }
    }
}

function Check-RunningProcesses {
    if (-not $CheckProcesses) { return }
    
    Write-Host "`n=== Checking Running Processes ===" -ForegroundColor Blue
    
    # Check for suspicious processes
    foreach ($proc in $IOCs.SuspiciousProcesses) {
        $processes = Get-Process $proc.Replace(".exe", "") -ErrorAction SilentlyContinue
        if ($processes) {
            foreach ($p in $processes) {
                Write-Result "Suspicious process running: $($p.Name) (PID: $($p.Id))" "High"
                $Results.ProcessesFound += @{
                    Name = $p.Name
                    Id = $p.Id
                    Path = $p.Path
                }
                
                # Try to get process path
                try {
                    $procPath = (Get-Process -Id $p.Id -ErrorAction Stop).Path
                    Write-Result "Process path: $procPath" "Info"
                }
                catch {
                    Write-Result "Could not retrieve process path" "Medium"
                }
            }
        }
    }
    
    # Check for Notepad++ processes
    $nppProcesses = Get-Process "notepad++" -ErrorAction SilentlyContinue
    if ($nppProcesses) {
        Write-Result "Found $($nppProcesses.Count) Notepad++ process(es) running" "Info"
        
        # Check for child processes (suspicious)
        foreach ($proc in $nppProcesses) {
            try {
                $children = Get-WmiObject Win32_Process | Where-Object { $_.ParentProcessId -eq $proc.Id }
                if ($children) {
                    Write-Result "Notepad++ (PID: $($proc.Id)) has child processes - investigate:" "Medium"
                    foreach ($child in $children) {
                        Write-Result "  Child: $($child.Name) (PID: $($child.ProcessId))" "Info"
                    }
                }
            }
            catch {
                # Continue if we can't check child processes
            }
        }
    }
}

function Check-RegistryEntries {
    if (-not $CheckRegistry) { return }
    
    Write-Host "`n=== Checking Registry Entries ===" -ForegroundColor Blue
    
    foreach ($regPath in $IOCs.RegistryPaths) {
        if (Test-Path $regPath) {
            Write-Result "Found suspicious registry entry: $regPath" "High"
            $Results.RegistryFound += $regPath
            
            # Get registry value
            try {
                $value = Get-ItemProperty -Path $regPath -ErrorAction Stop
                Write-Result "Value: $($value | Out-String)" "Info"
            }
            catch {
                Write-Result "Could not read registry value" "Medium"
            }
        }
    }
    
    # Check for Notepad++ auto-start entries
    $autoRunPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
    )
    
    foreach ($path in $autoRunPaths) {
        if (Test-Path $path) {
            $entries = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
            if ($entries) {
                foreach ($entry in $entries.PSObject.Properties) {
                    if ($entry.Name -match "notepad\+\+|npp") {
                        Write-Result "Found Notepad++ related auto-start: $($entry.Name)" "Medium"
                        Write-Result "  Value: $($entry.Value)" "Info"
                    }
                }
            }
        }
    }
}

function Check-NetworkConnections {
    Write-Host "`n=== Checking Network Connections ===" -ForegroundColor Blue
    
    try {
        $connections = Get-NetTCPConnection -State Established -ErrorAction Stop | 
                      Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, OwningProcess
        
        $suspiciousConnections = $connections | Where-Object {
            $remoteAddr = $_.RemoteAddress
            $IOCs.NetworkIOCs -contains $remoteAddr
        }
        
        if ($suspiciousConnections) {
            foreach ($conn in $suspiciousConnections) {
                Write-Result "Suspicious connection to known malicious IP: $($conn.RemoteAddress)" "High"
                $Results.NetworkConnections += $conn
                
                # Get process info
                try {
                    $process = Get-Process -Id $conn.OwningProcess -ErrorAction Stop
                    Write-Result "  Process: $($process.Name) (PID: $($process.Id))" "Info"
                }
                catch {
                    Write-Result "  Process ID: $($conn.OwningProcess) (Could not get name)" "Info"
                }
            }
        }
        else {
            Write-Result "No connections to known malicious IPs detected" "Low"
        }
    }
    catch {
        Write-Result "Could not retrieve network connections (admin rights may be needed)" "Medium"
    }
}

function Get-RemediationSteps {
    Write-Host "`n=== Recommended Remediation Steps ===" -ForegroundColor Yellow
    
    if ($Results.Warnings.Count -gt 0 -or $Results.FilesFound.Count -gt 0) {
        Write-Host "`n[!] IOC(s) DETECTED - RECOMMENDED ACTIONS:" -ForegroundColor Red
        Write-Host "1. Uninstall Notepad++ immediately" -ForegroundColor Yellow
        Write-Host "2. Download fresh installer from official site: https://notepad-plus-plus.org/" -ForegroundColor Yellow
        Write-Host "3. Run full antivirus scan" -ForegroundColor Yellow
        Write-Host "4. Check for suspicious scheduled tasks" -ForegroundColor Yellow
        Write-Host "5. Monitor for unusual network activity" -ForegroundColor Yellow
        
        if ($Results.FilesFound.Count -gt 0) {
            Write-Host "`nSuspicious files to remove manually:" -ForegroundColor Cyan
            foreach ($file in $Results.FilesFound) {
                Write-Host "  - $file" -ForegroundColor White
            }
        }
    }
    else {
        Write-Host "[+] No IOCs detected. However, still recommended to:" -ForegroundColor Green
        Write-Host "1. Update Notepad++ to latest version (v8.6.7 or newer)" -ForegroundColor Yellow
        Write-Host "2. Verify download from official source" -ForegroundColor Yellow
        Write-Host "3. Check for plugin authenticity" -ForegroundColor Yellow
    }
}

# Main execution
try {
    # Check for admin rights
    $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
    if (-not $isAdmin) {
        Write-Result "Running without administrator privileges. Some checks may be limited." "Medium"
    }
    
    Check-NotepadPlusPlusInstallation
    Scan-MaliciousFiles
    Check-RunningProcesses
    Check-RegistryEntries
    Check-NetworkConnections
    
    # Summary
    Write-Host "`n=== Scan Summary ===" -ForegroundColor Blue
    Write-Host "Files Found: $($Results.FilesFound.Count)" -ForegroundColor $(if ($Results.FilesFound.Count -gt 0) { "Red" } else { "Green" })
    Write-Host "Processes Found: $($Results.ProcessesFound.Count)" -ForegroundColor $(if ($Results.ProcessesFound.Count -gt 0) { "Red" } else { "Green" })
    Write-Host "Registry Entries Found: $($Results.RegistryFound.Count)" -ForegroundColor $(if ($Results.RegistryFound.Count -gt 0) { "Red" } else { "Green" })
    Write-Host "Suspicious Connections: $($Results.NetworkConnections.Count)" -ForegroundColor $(if ($Results.NetworkConnections.Count -gt 0) { "Red" } else { "Green" })
    
    Get-RemediationSteps
    
}
catch {
    Write-Error "Script encountered an error: $_"
    Write-Host "`nTry running as Administrator for complete checks." -ForegroundColor Yellow
}

Write-Host "`n================================================" -ForegroundColor Cyan
Write-Host "Scan completed at: $(Get-Date)" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan