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 } } |