Vulnerability/Resolve-CpeName.ps1

function Resolve-CpeName {
    <#
    .SYNOPSIS
        Resolves an installed product to a CPE 2.3 name for version-exact CVE matching
    .DESCRIPTION
        Maps an installed software product (registry DisplayName / Publisher /
        DisplayVersion) to a CPE 2.3 formatted string. Resolution strategy, in order:
        1. A built-in curated mapping table of common products with known
           vendor/product CPE tokens (works fully offline). When a curated
           entry declares a version-scheme translator (Java, .NET Framework,
           Chrome-style, Python, Qt-based applications), the normalized
           registry version is translated into the scheme NVD uses for that
           vendor's CPEs before the CPE is built. A translation only fires
           when the scheme actually changes the value; otherwise the version
           passes through untouched. This prevents downstream NVD queries
           from returning NoMatch purely because of the version format
           (e.g. Java '1.8.0_391' vs NVD '8.0.391'). Translated matches are
           reported with MatchedBy 'SchemeTranslation', Confidence Medium and
           the untouched registry value in OriginalVersion. The translators
           are heuristics based on documented vendor versioning conventions.
        2. An optional online lookup against the NVD CPE API 2.0
           (keywordSearch) when -UseOnlineLookup and -ApiKey are supplied.
           Applies the same TLS 1.2 and rate-limit conventions as
           Get-NVDCveData; failures warn and continue.
        3. A heuristic guess that derives vendor/product tokens from the
           sanitized product and vendor names (always Low confidence).
        The registry version string is normalized (architecture and bitness
        noise such as 'x64' or '(64-bit)' removed, build metadata stripped;
        trailing '.0' segments are preserved so version-exact queries stay
        exact) and embedded as the version component of
        cpe:2.3:a:vendor:product:version:*:*:*:*:*:*:*
        Capability limits: every resolved CPE uses the application part
        ('a'); a few curated entries (e.g. VMware ESXi, which NVD classifies
        as an operating system) may belong to a different CPE part and are
        best-effort hints. Curated vendor/product tokens are maintained by
        hand and can drift from NVD naming over time.
    .PARAMETER ProductName
        Product display name, e.g. the registry DisplayName value
    .PARAMETER Vendor
        Optional vendor/publisher name, e.g. the registry Publisher value
    .PARAMETER Version
        Optional product version string, e.g. the registry DisplayVersion value
    .PARAMETER UseOnlineLookup
        Query the NVD CPE API 2.0 when the curated table has no match (requires -ApiKey)
    .PARAMETER ApiKey
        Optional NVD API key. Sent only as the 'apiKey' request header, never logged.
    .EXAMPLE
        Resolve-CpeName -ProductName "Google Chrome" -Vendor "Google LLC" -Version "120.0.6099.130"
    .EXAMPLE
        Resolve-CpeName -ProductName "7-Zip 23.01 (x64)" -Version "23.01" -UseOnlineLookup -ApiKey $key
    .EXAMPLE
        Resolve-CpeName -ProductName "Java SE Development Kit 8 Update 391" -Vendor "Oracle" -Version "1.8.0_391"
        Translates the legacy Java version scheme and returns
        cpe:2.3:a:oracle:jdk:8.0.391:*:*:*:*:*:*:* with MatchedBy 'SchemeTranslation'
    .OUTPUTS
        PSCustomObject with CpeName, MatchedBy (CuratedTable/SchemeTranslation/
        OnlineLookup/Heuristic/None), Confidence (High/Medium/Low), Version and
        OriginalVersion (the raw registry version string, empty when not supplied)
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ProductName,

        [Parameter()]
        [string]$Vendor,

        [Parameter()]
        [string]$Version,

        [Parameter()]
        [switch]$UseOnlineLookup,

        [Parameter()]
        [string]$ApiKey
    )

    begin {
        Write-Host "=== CPE NAME RESOLUTION ===" -ForegroundColor Cyan
        Write-Host "Product: $ProductName" -ForegroundColor Yellow
        if ($Vendor) { Write-Host "Vendor: $Vendor" -ForegroundColor Yellow }
        if ($Version) { Write-Host "Version: $Version" -ForegroundColor Yellow }
        Write-Host "Online Lookup: $(if($UseOnlineLookup -and $ApiKey){'Enabled'}else{'Disabled (curated table / heuristic only)'})" -ForegroundColor Yellow
        Write-Host "API Key: $(if($ApiKey){'Provided'}else{'Not provided (public rate limits apply)'})" -ForegroundColor Yellow

        # Ensure TLS 1.2 on Windows PowerShell 5.1 (idempotent, only if the type exists)
        try {
            if ($null -ne ([Net.SecurityProtocolType] -as [type])) {
                [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
            }
        }
        catch {
            Write-Verbose "Could not enforce TLS 1.2: $_"
        }

        # Curated mapping table: ordered, most specific first.
        # Patterns are matched as case-insensitive substrings of the product name.
        # 'Scheme' marks entries whose vendor CPE version scheme differs from the
        # raw registry version format; see $schemeTranslators below.
        $curatedTable = @(
            @{ Patterns = @('Adobe Acrobat Reader DC', 'Acrobat Reader DC'); Vendor = 'adobe'; Product = 'acrobat_reader_dc'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe Photoshop'); Vendor = 'adobe'; Product = 'photoshop'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe Illustrator'); Vendor = 'adobe'; Product = 'illustrator'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe Premiere Pro'); Vendor = 'adobe'; Product = 'premiere_pro'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe After Effects'); Vendor = 'adobe'; Product = 'after_effects'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe InDesign'); Vendor = 'adobe'; Product = 'indesign'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe Lightroom'); Vendor = 'adobe'; Product = 'lightroom'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe Creative Cloud'); Vendor = 'adobe'; Product = 'creative_cloud'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Adobe Acrobat'); Vendor = 'adobe'; Product = 'acrobat'; PublisherHints = @('Adobe') }
            @{ Patterns = @('Google Chrome'); Vendor = 'google'; Product = 'chrome'; PublisherHints = @('Google'); Scheme = 'Chrome' }
            @{ Patterns = @('Mozilla Firefox'); Vendor = 'mozilla'; Product = 'firefox'; PublisherHints = @('Mozilla') }
            @{ Patterns = @('Mozilla Thunderbird'); Vendor = 'mozilla'; Product = 'thunderbird'; PublisherHints = @('Mozilla') }
            @{ Patterns = @('Microsoft Edge'); Vendor = 'microsoft'; Product = 'edge'; PublisherHints = @('Microsoft'); Scheme = 'Chrome' }
            @{ Patterns = @('Brave'); Vendor = 'brave'; Product = 'brave'; PublisherHints = @('Brave'); Scheme = 'Chrome' }
            @{ Patterns = @('Opera'); Vendor = 'opera'; Product = 'opera'; PublisherHints = @('Opera'); Scheme = 'Chrome' }
            @{ Patterns = @('Microsoft Office', 'Microsoft 365'); Vendor = 'microsoft'; Product = 'office'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Microsoft SQL Server'); Vendor = 'microsoft'; Product = 'sql_server'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Visual Studio Code'); Vendor = 'microsoft'; Product = 'visual_studio_code'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Microsoft Visual Studio'); Vendor = 'microsoft'; Product = 'visual_studio'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Visual C++'); Vendor = 'microsoft'; Product = 'visual_c++'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Microsoft .NET Framework'); Vendor = 'microsoft'; Product = '.net_framework'; PublisherHints = @('Microsoft'); Scheme = 'DotNet' }
            @{ Patterns = @('Microsoft .NET'); Vendor = 'microsoft'; Product = '.net'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('PowerShell'); Vendor = 'microsoft'; Product = 'powershell'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Microsoft Teams'); Vendor = 'microsoft'; Product = 'teams'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Azure CLI'); Vendor = 'microsoft'; Product = 'azure_cli'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Sysinternals'); Vendor = 'microsoft'; Product = 'sysinternals'; PublisherHints = @('Microsoft') }
            @{ Patterns = @('Microsoft OpenJDK'); Vendor = 'microsoft'; Product = 'openjdk'; PublisherHints = @('Microsoft'); Scheme = 'Java' }
            @{ Patterns = @('Eclipse Temurin', 'AdoptOpenJDK'); Vendor = 'eclipse'; Product = 'temurin'; PublisherHints = @('Eclipse', 'Adoptium'); Scheme = 'Java' }
            @{ Patterns = @('Amazon Corretto'); Vendor = 'amazon'; Product = 'corretto'; PublisherHints = @('Amazon'); Scheme = 'Java' }
            @{ Patterns = @('Azul Zulu'); Vendor = 'azul'; Product = 'zulu'; PublisherHints = @('Azul'); Scheme = 'Java' }
            @{ Patterns = @('Java SE Development Kit', 'JDK'); Vendor = 'oracle'; Product = 'jdk'; PublisherHints = @('Oracle'); Scheme = 'Java' }
            @{ Patterns = @('Java(TM)', 'Java 8', 'Java(TM) SE Runtime', 'Java Runtime'); Vendor = 'oracle'; Product = 'jre'; PublisherHints = @('Oracle'); Scheme = 'Java' }
            @{ Patterns = @('Miniconda'); Vendor = 'anaconda'; Product = 'miniconda'; PublisherHints = @('Anaconda') }
            @{ Patterns = @('Anaconda'); Vendor = 'anaconda'; Product = 'anaconda'; PublisherHints = @('Anaconda') }
            @{ Patterns = @('Python'); Vendor = 'python'; Product = 'python'; PublisherHints = @('Python Software Foundation'); Scheme = 'Python' }
            @{ Patterns = @('Node.js'); Vendor = 'nodejs'; Product = 'node.js'; PublisherHints = @('Node.js', 'OpenJS') }
            @{ Patterns = @('RubyMine'); Vendor = 'jetbrains'; Product = 'rubymine'; PublisherHints = @('JetBrains') }
            @{ Patterns = @('Ruby'); Vendor = 'ruby-lang'; Product = 'ruby'; PublisherHints = @('Ruby') }
            @{ Patterns = @('PhpStorm'); Vendor = 'jetbrains'; Product = 'phpstorm'; PublisherHints = @('JetBrains') }
            @{ Patterns = @('PHP'); Vendor = 'php'; Product = 'php'; PublisherHints = @('The PHP Group') }
            @{ Patterns = @('Strawberry Perl'); Vendor = 'strawberryperl'; Product = 'strawberry_perl'; PublisherHints = @('Strawberry Perl') }
            @{ Patterns = @('Perl'); Vendor = 'perl'; Product = 'perl'; PublisherHints = @('Perl') }
            @{ Patterns = @('Go Programming Language', 'Golang'); Vendor = 'golang'; Product = 'go'; PublisherHints = @('Go') }
            @{ Patterns = @('Rustup', 'Rust Programming'); Vendor = 'rust-lang'; Product = 'rust'; PublisherHints = @('Rust') }
            @{ Patterns = @('IntelliJ IDEA'); Vendor = 'jetbrains'; Product = 'intellij_idea'; PublisherHints = @('JetBrains') }
            @{ Patterns = @('PyCharm'); Vendor = 'jetbrains'; Product = 'pycharm'; PublisherHints = @('JetBrains') }
            @{ Patterns = @('WebStorm'); Vendor = 'jetbrains'; Product = 'webstorm'; PublisherHints = @('JetBrains') }
            @{ Patterns = @('Notepad++'); Vendor = 'notepad-plus-plus'; Product = 'notepad++'; PublisherHints = @('Notepad++') }
            @{ Patterns = @('Sublime Text'); Vendor = 'sublimetext'; Product = 'sublime_text'; PublisherHints = @('Sublime') }
            @{ Patterns = @('7-Zip'); Vendor = '7-zip'; Product = '7-zip'; PublisherHints = @('Igor Pavlov', '7-Zip') }
            @{ Patterns = @('WinRAR'); Vendor = 'rarlab'; Product = 'winrar'; PublisherHints = @('win.rar', 'RARLAB') }
            @{ Patterns = @('PuTTY'); Vendor = 'putty'; Product = 'putty'; PublisherHints = @('Simon Tatham') }
            @{ Patterns = @('KiTTY'); Vendor = 'kitty'; Product = 'kitty'; PublisherHints = @('9bis') }
            @{ Patterns = @('WinSCP'); Vendor = 'winscp'; Product = 'winscp'; PublisherHints = @('Martin Prikryl') }
            @{ Patterns = @('OpenSSH'); Vendor = 'openbsd'; Product = 'openssh'; PublisherHints = @('OpenBSD') }
            @{ Patterns = @('OpenVPN'); Vendor = 'openvpn'; Product = 'openvpn'; PublisherHints = @('OpenVPN') }
            @{ Patterns = @('FortiClient'); Vendor = 'fortinet'; Product = 'forticlient'; PublisherHints = @('Fortinet') }
            @{ Patterns = @('GlobalProtect'); Vendor = 'palo_alto_networks'; Product = 'globalprotect'; PublisherHints = @('Palo Alto Networks') }
            @{ Patterns = @('Cisco AnyConnect'); Vendor = 'cisco'; Product = 'anyconnect_secure_mobility_client'; PublisherHints = @('Cisco') }
            @{ Patterns = @('WireGuard'); Vendor = 'wireguard'; Product = 'wireguard'; PublisherHints = @('WireGuard') }
            @{ Patterns = @('FileZilla Server'); Vendor = 'filezilla'; Product = 'filezilla_server'; PublisherHints = @('FileZilla') }
            @{ Patterns = @('FileZilla'); Vendor = 'filezilla'; Product = 'filezilla'; PublisherHints = @('FileZilla') }
            @{ Patterns = @('TortoiseGit'); Vendor = 'tortoisegit'; Product = 'tortoisegit'; PublisherHints = @('TortoiseGit') }
            @{ Patterns = @('TortoiseSVN'); Vendor = 'tortoisesvn'; Product = 'tortoisesvn'; PublisherHints = @('TortoiseSVN') }
            @{ Patterns = @('GitHub Desktop'); Vendor = 'github'; Product = 'github_desktop'; PublisherHints = @('GitHub') }
            @{ Patterns = @('Git'); Vendor = 'git-scm'; Product = 'git'; PublisherHints = @('The Git Development Community') }
            @{ Patterns = @('VLC media player'); Vendor = 'videolan'; Product = 'vlc_media_player'; PublisherHints = @('VideoLAN') }
            @{ Patterns = @('GIMP'); Vendor = 'gimp'; Product = 'gimp'; PublisherHints = @('GIMP') }
            @{ Patterns = @('Inkscape'); Vendor = 'inkscape'; Product = 'inkscape'; PublisherHints = @('Inkscape') }
            @{ Patterns = @('OBS Studio'); Vendor = 'obsproject'; Product = 'obs_studio'; PublisherHints = @('OBS'); Scheme = 'Qt' }
            @{ Patterns = @('Audacity'); Vendor = 'audacityteam'; Product = 'audacity'; PublisherHints = @('Audacity') }
            @{ Patterns = @('HandBrake'); Vendor = 'handbrake'; Product = 'handbrake'; PublisherHints = @('HandBrake') }
            @{ Patterns = @('LibreOffice'); Vendor = 'libreoffice'; Product = 'libreoffice'; PublisherHints = @('The Document Foundation') }
            @{ Patterns = @('OpenOffice'); Vendor = 'apache'; Product = 'openoffice'; PublisherHints = @('Apache') }
            @{ Patterns = @('Foxit PDF Reader', 'Foxit Reader'); Vendor = 'foxitsoftware'; Product = 'foxit_reader'; PublisherHints = @('Foxit') }
            @{ Patterns = @('iTunes'); Vendor = 'apple'; Product = 'itunes'; PublisherHints = @('Apple') }
            @{ Patterns = @('Skype'); Vendor = 'skype'; Product = 'skype'; PublisherHints = @('Skype', 'Microsoft') }
            @{ Patterns = @('KeePassXC'); Vendor = 'keepassxc'; Product = 'keepassxc'; PublisherHints = @('KeePassXC'); Scheme = 'Qt' }
            @{ Patterns = @('KeePass'); Vendor = 'keepass'; Product = 'keepass'; PublisherHints = @('Dominik Reichl') }
            @{ Patterns = @('VeraCrypt'); Vendor = 'veracrypt'; Product = 'veracrypt'; PublisherHints = @('IDRIX') }
            @{ Patterns = @('1Password'); Vendor = 'agilebits'; Product = '1password'; PublisherHints = @('AgileBits') }
            @{ Patterns = @('Bitwarden'); Vendor = 'bitwarden'; Product = 'bitwarden'; PublisherHints = @('Bitwarden') }
            @{ Patterns = @('LastPass'); Vendor = 'lastpass'; Product = 'lastpass'; PublisherHints = @('LastPass') }
            @{ Patterns = @('Malwarebytes'); Vendor = 'malwarebytes'; Product = 'malwarebytes'; PublisherHints = @('Malwarebytes') }
            @{ Patterns = @('OpenSSL'); Vendor = 'openssl'; Product = 'openssl'; PublisherHints = @('OpenSSL') }
            @{ Patterns = @('Zoom'); Vendor = 'zoom'; Product = 'zoom'; PublisherHints = @('Zoom') }
            @{ Patterns = @('Cisco Webex', 'Webex'); Vendor = 'cisco'; Product = 'webex'; PublisherHints = @('Cisco') }
            @{ Patterns = @('Slack'); Vendor = 'slack'; Product = 'slack'; PublisherHints = @('Slack') }
            @{ Patterns = @('Discord'); Vendor = 'discord'; Product = 'discord'; PublisherHints = @('Discord') }
            @{ Patterns = @('TeamViewer'); Vendor = 'teamviewer'; Product = 'teamviewer'; PublisherHints = @('TeamViewer') }
            @{ Patterns = @('AnyDesk'); Vendor = 'anydesk'; Product = 'anydesk'; PublisherHints = @('AnyDesk') }
            @{ Patterns = @('Splashtop'); Vendor = 'splashtop'; Product = 'splashtop'; PublisherHints = @('Splashtop') }
            @{ Patterns = @('nginx'); Vendor = 'nginx'; Product = 'nginx'; PublisherHints = @('nginx') }
            @{ Patterns = @('Apache HTTP Server', 'Apache HTTPD'); Vendor = 'apache'; Product = 'http_server'; PublisherHints = @('Apache') }
            @{ Patterns = @('PostgreSQL'); Vendor = 'postgresql'; Product = 'postgresql'; PublisherHints = @('PostgreSQL') }
            @{ Patterns = @('MySQL Workbench'); Vendor = 'oracle'; Product = 'mysql_workbench'; PublisherHints = @('Oracle', 'MySQL') }
            @{ Patterns = @('MySQL'); Vendor = 'oracle'; Product = 'mysql'; PublisherHints = @('Oracle', 'MySQL') }
            @{ Patterns = @('MongoDB'); Vendor = 'mongodb'; Product = 'mongodb'; PublisherHints = @('MongoDB') }
            @{ Patterns = @('SQLite'); Vendor = 'sqlite'; Product = 'sqlite'; PublisherHints = @('SQLite') }
            @{ Patterns = @('Redis'); Vendor = 'redis'; Product = 'redis'; PublisherHints = @('Redis') }
            @{ Patterns = @('Elasticsearch'); Vendor = 'elastic'; Product = 'elasticsearch'; PublisherHints = @('Elastic') }
            @{ Patterns = @('Grafana'); Vendor = 'grafana'; Product = 'grafana'; PublisherHints = @('Grafana') }
            @{ Patterns = @('VMware Tools'); Vendor = 'vmware'; Product = 'tools'; PublisherHints = @('VMware') }
            @{ Patterns = @('VMware ESXi'); Vendor = 'vmware'; Product = 'esxi'; PublisherHints = @('VMware') }
            @{ Patterns = @('VMware Workstation'); Vendor = 'vmware'; Product = 'workstation'; PublisherHints = @('VMware') }
            @{ Patterns = @('VirtualBox'); Vendor = 'oracle'; Product = 'vm_virtualbox'; PublisherHints = @('Oracle') }
            @{ Patterns = @('Docker Desktop'); Vendor = 'docker'; Product = 'desktop'; PublisherHints = @('Docker') }
            @{ Patterns = @('Podman'); Vendor = 'podman'; Product = 'podman'; PublisherHints = @('Podman') }
            @{ Patterns = @('Docker'); Vendor = 'docker'; Product = 'docker'; PublisherHints = @('Docker') }
            @{ Patterns = @('kubectl'); Vendor = 'kubernetes'; Product = 'kubectl'; PublisherHints = @('Kubernetes') }
            @{ Patterns = @('Terraform'); Vendor = 'hashicorp'; Product = 'terraform'; PublisherHints = @('HashiCorp') }
            @{ Patterns = @('AWS Command Line Interface', 'AWS CLI'); Vendor = 'amazon'; Product = 'aws_cli'; PublisherHints = @('Amazon') }
            @{ Patterns = @('Wireshark'); Vendor = 'wireshark'; Product = 'wireshark'; PublisherHints = @('Wireshark'); Scheme = 'Qt' }
            @{ Patterns = @('Nmap'); Vendor = 'nmap'; Product = 'nmap'; PublisherHints = @('Nmap') }
            @{ Patterns = @('Citrix Workspace'); Vendor = 'citrix'; Product = 'workspace_app'; PublisherHints = @('Citrix') }
            @{ Patterns = @('Cygwin'); Vendor = 'cygwin'; Product = 'cygwin'; PublisherHints = @('Cygwin') }
            @{ Patterns = @('MSYS2'); Vendor = 'msys2'; Product = 'msys2'; PublisherHints = @('MSYS2') }
            @{ Patterns = @('MinGW'); Vendor = 'mingw'; Product = 'mingw'; PublisherHints = @('MinGW') }
        )

        # Vendor version-scheme translators (heuristics): rewrite a normalized
        # registry version into the scheme NVD uses for that vendor's CPEs.
        # Each returns the input unchanged when the scheme already matches
        # (passthrough), so a translation only fires when the value changes.
        $schemeTranslators = @{
            Java = {
                param([string]$V)
                # Legacy '1.8.0_391' / '8u391' schemes map to NVD '8.0.391';
                # modern schemes ('11.0.x', '17.0.x') pass through unchanged
                if ($V -match '^1\.(\d+)\.0[_\.](\d+)$') { return ('{0}.0.{1}' -f $Matches[1], $Matches[2]) }
                if ($V -match '^(\d+)u(\d+)$') { return ('{0}.0.{1}' -f $Matches[1], $Matches[2]) }
                return $V
            }
            DotNet = {
                param([string]$V)
                # .NET Framework registry versions carry a build ('4.8.09037');
                # NVD tracks the framework as major.minor ('4.8')
                if ($V -match '^(\d+\.\d+)(\.\d+)+$') { return $Matches[1] }
                return $V
            }
            Chrome = {
                param([string]$V)
                # Chrome-style four-part versions are already CPE-compatible
                return $V
            }
            Python = {
                param([string]$V)
                # NVD tracks CPython as major.minor when the patch level is 0
                if ($V -match '^(\d+\.\d+)\.0+$') { return $Matches[1] }
                return $V
            }
            Qt = {
                param([string]$V)
                # Qt-based builds sometimes use underscore-separated versions
                if ($V -match '^\d+(_\d+)+$') { return ($V -replace '_', '.') }
                return $V
            }
        }

        $nvdCpeApiBase = 'https://services.nvd.nist.gov/rest/json/cpes/2.0'
        $requestHeaders = @{}
        if ($ApiKey) {
            $requestHeaders['apiKey'] = $ApiKey
        }

        # NVD rate limits: 5 requests / 30s public, 50 requests / 30s with API key
        $sleepSeconds = if ($ApiKey) { 0.6 } else { 6 }

        $result = $null
    }

    process {
        try {
            function ConvertTo-NormalizedCpeVersion {
                param([string]$RawVersion)

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

                $normalized = $RawVersion.Trim()
                if ($normalized -eq 'Unknown') {
                    return $null
                }

                # Strip parenthetical/bracket noise such as '(64-bit)' or '[x64]'
                $normalized = $normalized -replace '\([^)]*\)', ''
                $normalized = $normalized -replace '\[[^\]]*\]', ''

                # Strip architecture/bitness tokens
                $normalized = $normalized -replace '(?i)\b(x64|x86|amd64|arm64|64-bit|32-bit|64bit|32bit)\b', ''

                # Keep only the first whitespace/comma-separated token
                $normalized = ($normalized -split '[\s,]+' | Where-Object { $_ -ne '' } | Select-Object -First 1)

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

                # Strip a leading 'v' and build metadata / pre-release suffixes
                $normalized = $normalized -replace '^[vV]', ''
                $normalized = $normalized -replace '[+].*$', ''
                $normalized = $normalized -replace '[-].*$', ''

                # Keep only characters valid unescaped in a CPE 2.3 formatted-string
                # component; anything else (e.g. '/', ':', ';') would produce an
                # invalid CPE that the NVD API rejects
                $normalized = $normalized -replace '[^0-9A-Za-z._~]', ''

                # Require at least one digit, otherwise treat as unusable
                if ($normalized -notmatch '\d') {
                    return $null
                }

                return $normalized
            }

            function ConvertTo-CpeToken {
                param([string]$RawValue)

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

                $token = $RawValue.Trim().ToLower()
                $token = $token -replace '\([^)]*\)', ''
                $token = $token -replace '[â„¢®]', ''
                $token = $token -replace '\s+', '_'
                $token = $token -replace '[^a-z0-9._+-]', ''
                $token = $token.Trim('_')

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

                return $token
            }

            function New-CpeName {
                param(
                    [string]$VendorToken,
                    [string]$ProductToken,
                    [string]$VersionToken
                )

                if ([string]::IsNullOrWhiteSpace($VersionToken)) {
                    $VersionToken = '*'
                }

                return "cpe:2.3:a:$($VendorToken):$($ProductToken):$($VersionToken):*:*:*:*:*:*:*"
            }

            $normalizedVersion = ConvertTo-NormalizedCpeVersion -RawVersion $Version

            # 1. CURATED TABLE
            $curatedMatch = $null
            foreach ($entry in $curatedTable) {
                foreach ($pattern in $entry.Patterns) {
                    if ($ProductName.IndexOf($pattern, [StringComparison]::OrdinalIgnoreCase) -ge 0) {
                        $curatedMatch = $entry
                        break
                    }
                }
                if ($null -ne $curatedMatch) {
                    break
                }
            }

            if ($null -ne $curatedMatch) {
                $effectiveVersion = $normalizedVersion
                $matchedBy = 'CuratedTable'
                $confidence = 'Medium'
                if ($null -ne $normalizedVersion) {
                    $confidence = 'High'
                }

                if ($null -ne $normalizedVersion -and $curatedMatch.ContainsKey('Scheme')) {
                    $schemeName = [string]$curatedMatch['Scheme']
                    if ($schemeTranslators.ContainsKey($schemeName)) {
                        $translatedVersion = & $schemeTranslators[$schemeName] $normalizedVersion
                        if ($null -ne $translatedVersion -and
                            $translatedVersion -ne $normalizedVersion -and
                            $translatedVersion -match '^[0-9A-Za-z._~]+$') {
                            Write-Host " Version scheme translation ($schemeName): $normalizedVersion -> $translatedVersion" -ForegroundColor Gray
                            $effectiveVersion = $translatedVersion
                            $matchedBy = 'SchemeTranslation'
                            $confidence = 'Medium'
                        }
                    }
                }

                $cpeName = New-CpeName -VendorToken $curatedMatch.Vendor -ProductToken $curatedMatch.Product -VersionToken $effectiveVersion

                Write-Host " Curated table match: $($curatedMatch.Vendor):$($curatedMatch.Product)" -ForegroundColor Gray

                $result = [PSCustomObject]@{
                    CpeName         = $cpeName
                    MatchedBy       = $matchedBy
                    Confidence      = $confidence
                    Version         = $effectiveVersion
                    OriginalVersion = $Version
                }
                return
            }

            # 2. ONLINE LOOKUP (NVD CPE API 2.0)
            if ($UseOnlineLookup -and $ApiKey) {
                try {
                    Write-Verbose "Rate limit pause: $sleepSeconds second(s) before CPE request"
                    Start-Sleep -Seconds $sleepSeconds

                    $requestUri = "$nvdCpeApiBase`?keywordSearch=$([System.Uri]::EscapeDataString($ProductName))&resultsPerPage=10"
                    Write-Host " Querying NVD CPE API for: $ProductName..." -ForegroundColor Gray

                    $response = $null
                    try {
                        $response = Invoke-RestMethod -Uri $requestUri -Method Get -Headers $requestHeaders -TimeoutSec 60 -ErrorAction Stop
                    }
                    catch {
                        Write-Warning "NVD CPE API request failed: $($_.Exception.Message)"
                        $response = $null
                    }

                    if ($null -ne $response -and
                        $null -ne $response.PSObject.Properties['products'] -and
                        $null -ne $response.products -and
                        @($response.products).Count -gt 0) {

                        $onlineCpeName = $null
                        foreach ($productEntry in @($response.products)) {
                            if ($null -eq $productEntry.PSObject.Properties['cpe'] -or $null -eq $productEntry.cpe) {
                                continue
                            }
                            $cpeObject = $productEntry.cpe

                            $isDeprecated = $false
                            if ($null -ne $cpeObject.PSObject.Properties['deprecated'] -and $true -eq $cpeObject.deprecated) {
                                $isDeprecated = $true
                            }

                            if ($null -ne $cpeObject.PSObject.Properties['cpeName'] -and
                                $null -ne $cpeObject.cpeName -and
                                -not $isDeprecated -and
                                [string]$cpeObject.cpeName -like 'cpe:2.3:a:*') {
                                $onlineCpeName = [string]$cpeObject.cpeName
                                break
                            }
                        }

                        if ($null -ne $onlineCpeName) {
                            $cpeParts = $onlineCpeName.Split(':')
                            if ($cpeParts.Count -ge 6) {
                                $vendorToken = $cpeParts[3]
                                $productToken = $cpeParts[4]

                                $cpeName = New-CpeName -VendorToken $vendorToken -ProductToken $productToken -VersionToken $normalizedVersion
                                $confidence = 'Medium'
                                if ($null -ne $normalizedVersion) {
                                    $confidence = 'High'
                                }

                                $result = [PSCustomObject]@{
                                    CpeName         = $cpeName
                                    MatchedBy       = 'OnlineLookup'
                                    Confidence      = $confidence
                                    Version         = $normalizedVersion
                                    OriginalVersion = $Version
                                }
                                return
                            }
                        }

                        Write-Verbose "NVD CPE API returned no usable application CPE for '$ProductName'"
                    }
                    else {
                        Write-Verbose "NVD CPE API returned no products for '$ProductName'"
                    }
                }
                catch {
                    Write-Warning "Online CPE lookup failed for '$ProductName': $_"
                }
            }
            elseif ($UseOnlineLookup -and -not $ApiKey) {
                Write-Warning "-UseOnlineLookup requires -ApiKey; skipping online CPE lookup for '$ProductName'"
            }

            # 3. HEURISTIC GUESS
            $vendorToken = ConvertTo-CpeToken -RawValue $Vendor
            $productToken = ConvertTo-CpeToken -RawValue $ProductName

            if ($null -ne $productToken) {
                if ($null -eq $vendorToken) {
                    $firstWord = ($ProductName -split '\s+' | Where-Object { $_ -ne '' } | Select-Object -First 1)
                    $vendorToken = ConvertTo-CpeToken -RawValue $firstWord
                }

                if ($null -ne $vendorToken) {
                    $cpeName = New-CpeName -VendorToken $vendorToken -ProductToken $productToken -VersionToken $normalizedVersion

                    Write-Host " Heuristic guess: $($vendorToken):$($productToken) (Low confidence)" -ForegroundColor Gray

                    $result = [PSCustomObject]@{
                        CpeName         = $cpeName
                        MatchedBy       = 'Heuristic'
                        Confidence      = 'Low'
                        Version         = $normalizedVersion
                        OriginalVersion = $Version
                    }
                    return
                }
            }

            Write-Warning "Could not resolve a CPE name for '$ProductName'"
            $result = [PSCustomObject]@{
                CpeName         = $null
                MatchedBy       = 'None'
                Confidence      = 'Low'
                Version         = $normalizedVersion
                OriginalVersion = $Version
            }
        }
        catch {
            Write-Error "Error during CPE name resolution: $_"
            throw
        }
    }

    end {
        Write-Host "=== CPE NAME RESOLUTION COMPLETE ===" -ForegroundColor $(
            if ($null -ne $result -and $null -ne $result.CpeName) { 'Green' } else { 'Gray' }
        )
        if ($null -ne $result) {
            Write-Host " CPE Name: $(if($result.CpeName){$result.CpeName}else{'(none)'})" -ForegroundColor White
            Write-Host " Matched By: $($result.MatchedBy) | Confidence: $($result.Confidence)" -ForegroundColor White
        }
        return $result
    }
}