Last active
February 16, 2022 08:44
-
-
Save isjamesalive/7d67a0fecb2ae1b42f4142a9f1077331 to your computer and use it in GitHub Desktop.
PowerShell functions for interacting with the NVD API.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<#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