Private/Popup-SensitiveContent.ps1

function Popup-SensitiveContent {
    <#
    .SYNOPSIS
    Displays a security warning popup when potential hardcoded sensitive data is detected in files.
 
    .DESCRIPTION
    This function scans the provided files for potentially hardcoded sensitive information such as passwords, secrets,
    API keys, tokens, and connection strings. If any sensitive content is detected, a modern GUI popup is displayed
    showing the affected files, line numbers, and masked values.
 
    The popup allows the user to either:
    - Continue with the operation (acknowledging the risk), or
    - Cancel the operation to prevent accidental exposure.
 
    The function intelligently ignores:
    - Common binary and build artifact file types
    - Azure DevOps variable references like $(VariableName)
    - Azure resource scope identifiers (subscriptions, resourceGroups, etc.)
    - Files listed via extension ignore rules
 
    Additional custom regex patterns can be externally configured using:
        %APPDATA%\Popup-SensitiveContent.config
 
    Key Features:
    - Modern WPF popup UI
    - Masked display of detected secrets
    - Configurable external pattern support
    - Safe handling of DevOps variable references
    - Moderate detection to reduce false positives
 
    Recommended Usage:
    Integrate this function before Git commits, pull request creation, or pipeline execution to prevent
    accidental leakage of credentials or sensitive configuration.
 
    .PARAMETER Files
    An array of file paths to be scanned for sensitive content.
 
    .EXAMPLE
    $files = @(
        "C:\Scripts\Deploy.ps1",
        "C:\Configs\appsettings.json"
    )
    Popup-SensitiveContent -Files $files
 
    Displays a warning popup if sensitive values are detected and returns the user's decision.
 
    .EXAMPLE
    if (-not (Popup-SensitiveContent -Files $changedFiles)) {
        throw "Operation aborted due to detected sensitive content."
    }
 
    Integrates with commit pipelines or automation scripts to block unsafe operations when sensitive data is found.
 
    .OUTPUTS
    System.Boolean
 
    Returns:
    - $true -> User chose to Continue
    - $false -> User chose to Cancel
 
    .NOTES
    Author : Lakshmanachari Panuganti
    Date: 20 th November 2025
 
    #>


    param(
        [array]$Files
    )

    $IgnoreExtensions = @(
        '.exe', '.dll', '.pdb', '.obj', '.class', '.o', '.so', '.a', '.lib', '.dylib', '.bin',
        '.tmp', '.temp', '.bak', '.old', '.swp', '.orig',
        '.lock'
    )

    # Filter files before scanning
    $Files = $Files | Where-Object {
        $_ -and (Test-Path $_) -and
        (-not ($IgnoreExtensions -contains ([IO.Path]::GetExtension($_).ToLower())))
    }

    if ($Files.Count -eq 0) { return $true }

    # Sensitive data patterns
    $Patterns = @(
        # Key-value patterns with = or : (supports JSON quoted keys and unquoted keys)
        '(?i)["`''"]?(password|passwd|pwd|db_password)["`''"]?\s*[:=]\s*["`''"]?[^"`''"\r\n]+["`''"]?',
        '(?i)["`''"]?(secret|clientsecret|client_secret)["`''"]?\s*[:=]\s*["`''"]?[^"`''"\r\n]+["`''"]?',
        '(?i)["`''"]?(apikey|api[_-]?key|api_key)["`''"]?\s*[:=]\s*["`''"]?[^"`''"\r\n]+["`''"]?',
        '(?i)["`''"]?(token|accesstoken|authtoken|access_token)["`''"]?\s*[:=]\s*["`''"]?[^"`''"\r\n]+["`''"]?',
        '(?i)["`''"]?(connectionstring|connstring|db[_-]?conn)["`''"]?\s*[:=]\s*["`''"]?[^"`''"\r\n]+["`''"]?',
        # Specific token/key patterns
        'AKIA[0-9A-Z]{16}',
        'ghp_[A-Za-z0-9]{36}',
        'github_pat_[A-Za-z0-9]{22,64}',
        'AIza[0-9A-Za-z\-_]{35}',
        '(?i)eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+',
        '(?i)bearer\s+[A-Za-z0-9\-._~+/]+=*'
    )

    # External file that users can add customize patterns at anytime!
    $configPath = "$env:APPDATA\Popup-SensitiveContent.config"

    if (Test-Path $configPath) {
        $externalPatterns = Get-Content -Path $configPath -ErrorAction SilentlyContinue |
        ForEach-Object { $_.Trim() } |
        Where-Object { $_ -and -not $_.StartsWith('#') }

        $Patterns += $externalPatterns
    }

    $findings = @()

    foreach ($file in $Files) {
        $content = Get-Content $file -ErrorAction SilentlyContinue

        for ($i = 0; $i -lt $content.Count; $i++) {
            $line = $content[$i]

            # Ignore Azure resource scope identifiers
            if ($line -match '(?i)(subscriptions\/|resourceGroups\/|managementGroups\/|providers\/Microsoft)') { continue }
            # Ignore variable assignments and cmdlets
            if ($line -match '(?i)\$\w+\s*=\s*(\$|Get-|Invoke-|ConvertTo-|ConvertFrom-)') { continue }
            # Ignore PowerShell variable references on token-like keys
            if ($line -match '^\s*["'']?(token|accesstoken|authtoken|access_token)["'']?\s*[:=]\s*\$[A-Za-z0-9:_]+') { continue }
            # Ignore Azure DevOps $(VAR_NAME) references
            if ($line -match '\$\([^)]+\)') { continue }
            # Ignore ARM template parameter references like #{env.variable}#
            if ($line -match '#\{[^}]+\}#') { continue }
            # Ignore Terraform variable references like var.variable_name
            if ($line -match '\bvar\.[A-Za-z0-9_]+') { continue }
            # Ignore placeholder values like <YOUR_KEY>, <your-key>, <YOUR_SECRET>
            if ($line -match '[:=]\s*["'']?<[A-Za-z0-9_-]+>["'']?') { continue }
            # Ignore PowerShell variable assignments for api-key, secret, etc.
            if ($line -match '["'']?(api-key|secret|password)["'']?\s*[=:]\s*\$[A-Za-z0-9_]+') { continue }


            foreach ($pattern in $Patterns) {
                if ($line -match $pattern) {

                    $raw = $Matches[0].Trim()

                    # Ignore Azure DevOps variable references like $(VAR)
                    if ($raw -match '\$\([^)]+\)') { continue }

                    # Extract the secret value after = or : (handles both quoted and unquoted)
                    if ($raw -match '[:=]\s*["`''"]?([^"`''"\r\n]+?)["`''"]?\s*$') {
                        $secret = $Matches[1].Trim().Trim('"').Trim("'")
                        # Mask all but first 2 characters
                        if ($secret.Length -gt 2) {
                            $mask = $secret.Substring(0, 2) + '**********'
                        } else {
                            $mask = '*' * $secret.Length
                        }
                        $masked = $raw -replace [regex]::Escape($secret), $mask
                    } else {
                        # For patterns without separators (like AWS keys, GitHub tokens, JWT)
                        $secret = $raw
                        if ($secret.Length -gt 4) {
                            $mask = $secret.Substring(0, 4) + '********'
                        } else {
                            $mask = '*' * $secret.Length
                        }
                        $masked = $mask
                    }

                    $findings += [pscustomobject]@{
                        File  = $file
                        Line  = $i + 1
                        Match = $masked
                    }
                }
            }
        }
    }

    if ($findings.Count -eq 0) { return $true }

    # Build and show popup
    $groups = $findings | Sort-Object File, Line | Group-Object File

    foreach ($g in $groups) {
        $text += "FILE: $($g.Name)`r`n"
        foreach ($item in $g.Group) {
            $text += " • Line $($item.Line): $($item.Match)`r`n"
        }
        $text += "`r`n"
    }

    Add-Type -AssemblyName PresentationFramework

    $XAML = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="Security Alert"
        Height="650" Width="950"
        WindowStartupLocation="CenterScreen"
        ResizeMode="CanResize"
        Background="#F3F4F6">
 
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
 
        <Border Background="#C62828" Padding="16" CornerRadius="8">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="⚠" FontSize="22" Foreground="White" Margin="0,0,8,0"/>
                <TextBlock Text="SENSITIVE DATA DETECTED"
                           Foreground="White"
                           FontWeight="SemiBold"
                           FontSize="20"/>
            </StackPanel>
        </Border>
 
        <Border Grid.Row="1" Margin="0,16,0,16" Padding="14"
                Background="White" CornerRadius="8"
                BorderBrush="#E5E7EB" BorderThickness="1">
 
            <ScrollViewer VerticalScrollBarVisibility="Auto">
                <TextBox Name="Content"
                         TextWrapping="Wrap"
                         BorderThickness="0"
                         FontFamily="Calibri"
                         FontSize="14"
                         IsReadOnly="True"
                         Background="White"/>
            </ScrollViewer>
        </Border>
 
        <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button Name="ContinueBtn" Content="Continue" Width="140" Height="40" Margin="6"/>
            <Button Name="CancelBtn" Content="Cancel" Width="140" Height="40" Margin="6"/>
        </StackPanel>
    </Grid>
</Window>
"@


    $reader = New-Object System.Xml.XmlNodeReader ([xml]$XAML)
    $window = [Windows.Markup.XamlReader]::Load($reader)

    $window.FindName("Content").Text = $text

    $window.FindName("ContinueBtn").Add_Click({
            $window.Tag = $true
            $window.Close()
        })

    $window.FindName("CancelBtn").Add_Click({
            $window.Tag = $false
            $window.Close()
        })

    $window.Topmost = $true
    $window.ShowInTaskbar = $true

    $window.ShowDialog() | Out-Null
    return $window.Tag
}