Skip to content

Instantly share code, notes, and snippets.

@isjamesalive
Last active February 16, 2022 08:44
Show Gist options
  • Save isjamesalive/7d67a0fecb2ae1b42f4142a9f1077331 to your computer and use it in GitHub Desktop.
Save isjamesalive/7d67a0fecb2ae1b42f4142a9f1077331 to your computer and use it in GitHub Desktop.
PowerShell functions for interacting with the NVD API.
<#PSScriptInfo
.VERSION 1.4
.GUID 954be067-cffa-48e2-a3ee-f9536767597d
.AUTHOR James
.TAGS NIST, NVD, CVE, REST
.PROJECTURI https://gist.github.com/isjamesalive/7d67a0fecb2ae1b42f4142a9f1077331
.RELEASENOTES
* 1.0 - 2022-01-26:
* Initial release
* 1.1 - 2022-02-04:
* Get-Cve: Added proxy support
* 1.2 - 2022-02-05:
* Format-Cve: Fixed incorrect 'updated' field
* Get-Cve: Added IncludeNotFound switch
* 1.3 - 2022-02-07
* Get-Cve: Added minimal logging
* Get-Cve: Fixed RuntimeException 'You cannot call a method on a null-valued expression' when Select-CveId doesn't match a line
* 1.4 - 2022-02-16
* Format-Cve: Added computed NistUriField
.LICENSEURI
http://creativecommons.org/publicdomain/zero/1.0/
.TODO
* Get-Cve silently fails if the API key is invalid.
* Get-Cve parameter case must match the API.
* Search API support.
* Fix up proxy parameter handling.
* Write tests.
* Change module name and add a manifest.
* MSRC + RHEL API support
#>
<#
.SYNOPSIS
PowerShell functions for easily fetching CVE data.
.EXAMPLE
"CVE-2021-44228 (Log4Shell), CVE-2021-45046, CVE-2021-45105" | Select-CveId | Get-Cve | Format-Cve
#>
Function Write-Log
{
<#
.SYNOPSIS
Writes log messages to the screen and to a log file.
#>
[cmdletbinding(DefaultParameterSetName = 'Default')]
param(
# The path to the log file
[Parameter(
ParameterSetName = 'File',
Mandatory=$true)]
[String]
$Path,
# The message string to be logged
[Parameter(
ValueFromPipeline,
Mandatory=$true)]
[String]
$Message,
# The classification level of the messsage: INFO (default), WARN, ERROR or SUCCESS
[ValidateSet("INFO","WARN","ERROR","SUCCESS")]
[String]
$Level = "INFO",
# The datetime format to use within logs. Default is 's' yyyy-MM-dd'T'HH:mm:ss
# See: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings
[String]
$DateTimeFormat = "s"
)
Begin {
$ColourMap = @{
SUCCESS = "Green";
WARN = "Yellow";
ERROR = "Red";
INFO = "White";
}
}
Process {
$Now = ([DateTime]::Now).toString($DateTimeFormat)
$LogLine = "{0} - {1} - {2}" -f $Now, $($Level.ToUpper()), $Message
If($Path) {
$LogLine | Out-File -Append $Path -ErrorAction Stop -Encoding utf8
}
$LogLine | Write-Host -ForegroundColor $ColourMap.$Level
}
End { }
}
Function Get-Cve {
[CmdletBinding(DefaultParameterSetName="NoProxyCredentials")]
<#
.DESCRIPTION
Submit a CVE ID to the NVD CVE API and output the response.
.EXAMPLE
Get-CveId CVE-2021-44228
.EXAMPLE
"CVE-2021-45046" | Get-Cve -apiKey 11731919-57c8-4c71-963f-6f2b4b439db1
#>
param(
# The ID of the CVE to look up.
[Parameter(
Position=1,
ValueFromPipeline=$True,
Mandatory=$True
)]
[Parameter(ParameterSetName="NoProxyCredentials")]
[Parameter(ParameterSetName="ProxyUseDefaultCredentials")]
[Parameter(ParameterSetName="ProxyCredential")]
[ValidatePattern('^CVE-\d{4}-\d+$')]
[String]
$cveId,
# See https://nvd.nist.gov/developers/request-an-api-key
[Parameter(ParameterSetName="NoProxyCredentials")]
[Parameter(ParameterSetName="ProxyUseDefaultCredentials")]
[Parameter(ParameterSetName="ProxyCredential")]
[String]$apiKey,
# See https://nvd.nist.gov/developers/vulnerabilities#divGetCVEParameters
[Parameter(ParameterSetName="NoProxyCredentials")]
[Parameter(ParameterSetName="ProxyUseDefaultCredentials")]
[Parameter(ParameterSetName="ProxyCredential")]
[string]$addOns,
[Parameter(ParameterSetName="NoProxyCredentials")]
[Parameter(ParameterSetName="ProxyUseDefaultCredentials")]
[Parameter(ParameterSetName="ProxyCredential")]
[Uri]$Proxy,
[Parameter(ParameterSetName="ProxyCredential")]
[PSCredential]$ProxyCredential,
[Parameter(ParameterSetName="ProxyUseDefaultCredentials")]
[Switch]$ProxyUseDefaultCredentials,
# When no CVE data is found return a record with empty fields instead of throwing an exception.
[Parameter(ParameterSetName="NoProxyCredentials")]
[Parameter(ParameterSetName="ProxyUseDefaultCredentials")]
[Parameter(ParameterSetName="ProxyCredential")]
[Switch]$IncludeNotFound
)
Begin {
$FoundCveCount = 0
$NotFoundCveCount = 0
[Uri]$Endpoint = "https://services.nvd.nist.gov/rest/json/cve/1.0"
[String]$RequestFormat = "{0}/{1}"
[Hashtable]$ApiRequestParameters = $MyInvocation.BoundParameters
('CveId','ProxyCredential','Proxy','ProxyUseDefaultCredentials').ForEach({$ApiRequestParameters.Remove($_)})
}
Process {
$RestRequestParameters = @{
Method = 'Get'
Uri = ($RequestFormat -f $Endpoint, $CveId)
Body = $ApiRequestParameters
}
If($Proxy) {
$RestRequestParameters.Add('Proxy', $Proxy)
}
If($ProxyCredential) {
$RestRequestParameters.Add('ProxyCredential', $ProxyCredential)
}
If($ProxyUseDefaultCredentials) {
$RestRequestParameters.Add('ProxyUseDefaultCredentials', $True)
}
Try {
$Result = (Invoke-RestMethod @RestRequestParameters).Result
$FoundCveCount++
}
Catch [System.Exception] {
$Exception = $_
Switch ($_.Exception.Response.StatusCode) {
{[System.Net.HttpStatusCode]::NotFound} {
$NotFoundCveCount++
Write-Log -Level WARN -Message $Exception.ErrorDetails.Message
If($IncludeNotFound) {
$Result = "{{
'CVE_data_type': 'CVE',
'CVE_data_format': 'MITRE',
'CVE_data_version': '4.0',
'CVE_data_timestamp': '{0}',
'CVE_Items': [
{{
'cve': {{
'data_type': 'CVE',
'data_format': 'MITRE',
'data_version': '4.0',
'CVE_data_meta': {{
'ID': '{1}'
}}
}}
}}
]
}}" -f ([DateTime]::Now).toString('u'), $CveId | ConvertFrom-Json
}
}
Default { Throw $Exception }
}
}
$Result
}
End {
Write-Log -Level INFO -Message "Completed CVE lookup. Processed $($FoundCveCount + $NotFoundCveCount): $FoundCveCount found and $NotFoundCveCount not found."
}
}
Function Select-CveId {
<#
.DESCRIPTION
Accept a string and return all instances of valid CVE IDs.
.EXAMPLE
Get-Content ./UnstructuredText.txt | Select-CveId
#>
param(
[Parameter(Position=1,ValueFromPipeline=$True)]
[String]
$String
)
Begin {
# https://www.cve.org/ResourcesSupport/FAQs#pc_cve_id_requestswhat_is_cve_id
$CveIdPattern = 'CVE-\d{4}-\d{4,}'
}
Process {
($String | Select-String -Pattern $CveIdPattern -AllMatches).Matches.ForEach({$_.Value.toUpper()})
}
End {}
}
Function Format-Cve {
<#
.DESCRIPTION
Accept a response object from Get-Cve, extract frequently referred-to fields and output as psobject.
.EXAMPLE
Get-Cve CVE-2021-44228 | Format-Cve
#>
param(
[Parameter(Position=1,ValueFromPipeline=$True)]
[ValidateScript({
($_.CVE_data_type -eq 'CVE') -and
($_.CVE_data_format -eq 'MITRE') -and
($_.CVE_data_version -eq '4.0')
})]
$Cve,
# The language to select when parsing results.
[String]$Language = "en",
# Include references to third-party sources.
[Switch]$IncludeReferences,
# Include CWE IDs.
[Switch]$IncludeCwe
)
Begin {
[System.Collections.ArrayList]$SelectedFieldSet = @()
[System.Collections.ArrayList]$StandardFieldSet = (
@{Label="CveId"; Expression={$Cve.CVE_Items.cve.CVE_data_meta.id}},
@{Label="Assigner"; Expression={$Cve.CVE_Items.cve.CVE_data_meta.assigner}},
@{Label="Updated"; Expression={$Cve.CVE_Items.lastModifiedDate}},
@{Label="BaseScore"; Expression={$Cve.CVE_Items.impact.baseMetricV3.cvssV3.baseScore}},
@{Label="BaseSeverity"; Expression={$Cve.CVE_Items.impact.baseMetricV3.cvssV3.baseSeverity}},
@{Label="VectorString"; Expression={$Cve.CVE_Items.impact.baseMetricV3.cvssV3.vectorString}},
@{Label="Description"; Expression={$Cve.CVE_Items.cve.description.description_data.where({$_.lang -eq $Language}).value}},
@{Label="NistCveUri"; Expression={"https://nvd.nist.gov/vuln/detail/{0}" -f $Cve.CVE_Items.cve.CVE_data_meta.id}}
)
$StandardFieldSet.ForEach({$SelectedFieldSet.Add($_) | Out-Null})
If($IncludeReferences) {
$SelectedFieldSet.Add(@{Label="References"; Expression={$Cve.CVE_Items.cve.references.reference_data.url -join '; '}}) | Out-Null
}
If($IncludeCwe) {
$SelectedFieldSet.Add(@{Label="CWE"; Expression={$Cve.CVE_Items.cve.problemtype.problemtype_data.description.where({$_.lang -eq $Language}).value -join '; '}}) | Out-Null
}
}
Process {
New-Object -Type psobject | Select-Object -Property $SelectedFieldSet
}
End {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment