Topic
This script imports a CSV file of vulnerabilities exported from various vulnerability scan tools and platforms.
Environment
NinjaOne Automation
Description
To use this script, save it as a batch file and upload it to your Script Library. You can then run it as needed from the play button on the device Detail page or from the device Search view). See Automating the Vulnerability Import Process for instructions and resources.
<#
.SYNOPSIS
This script is used to import a vulnerability csv file exported from various vulnerability scan tools/platforms.
.DESCRIPTION
This script is used to import a vulnerability csv file exported from various vulnerability scan tools/platforms.
Once the file is successfully imported, it will delete the csv file. If the csv fails to import because of an issue
with the CSV, it will append "FAILED_" to the CSV name. If the initial import call goes through but subsequent status
checks fail it will append "UNKNOWN_" to the CSV name. You will need to check the NinjaOne UI for status at that point.
# ---------------------------------------------------------------
# Author: Mark Giordano
# Date: 03/05/2025
# Description: NinjaOne vulnerability Import and Log
# ---------------------------------------------------------------
.NOTES
Standard API URLs are:
app.ninjarmm.com
eu.ninjarmm.com
oc.ninjarmm.com
ca.ninjarmm.com
us2.ninjarmm.com
If you want to set this up a recurring job in NinjaOne, it's recommended it be done on a secure device that is not
used for everyday purposes. The credentials should be stored in secure custom fields and retrieved from there.
See Luke Whitelock's recommended setup (Getting Started) here: https://docs.mspp.io/ninjaone/getting-started
#>
<# Uncomment this section if you plan to pull your API credentials from NinjaOne Secure Custom Fields
$ClientID = Ninja-Property-Get NinjaOneAPIClientID
$ClientSecret = Ninja-Property-Get NinjaOneAPISecret
#>
# Adjust BaseURL to applicable instance #
$BaseURL = 'ca.ninjarmm.com'
# Specify the Scan Group ID #
$ScanGroupID = 'ENTER SCANGROUP ID NUMBER'
# Directory to the CSV files for import #
$PathtoCSV = "$env:SystemDrivetemp"
# Beginning pattern of CSV filenames to match #
$CSVName = 'VulnExport_'
## Function Setup ##
function Get-NinjaToken {
$Headers = @{
"Content-Type" = "application/x-www-form-urlencoded"
}
$Scope = "monitoring management offline_access"
if ([string]::IsNullOrWhiteSpace($ClientID) -or [string]::IsNullOrWhiteSpace($ClientSecret)) {
if ($($PSVersionTable.PSVersion -lt [version]'7.0')) {
$ClientID = Read-Host -Prompt "ClientID"
$ClientSecret = Read-Host -Prompt "ClientSecret"
}
else {
$ClientID = Read-Host -MaskInput "ClientID"
$ClientSecret = Read-Host -MaskInput "ClientSecret"
}
}
$GrantType = 'client_credentials'
$params = @{
Uri = "https://$($BaseURL)/ws/oauth/token"
Method = 'POST'
Body = "grant_type=$GrantType&client_id=$ClientID&client_secret=$ClientSecret&scope=$Scope"
}
$Response = Invoke-RestMethod @Params -Headers $Headers
return $Response.access_token
}
function Get-BaseSettings {
[CmdletBinding()]
param (
[Parameter()]
[String]$Method = 'Get',
[Parameter()]
[String]$Body,
[switch]$Paginate,
[string]$After,
[string]$ContentType
)
if ($Paginate) {
$URL = "https://$($BaseURL)/api/v2/$($Request)?pageSize=$($PageSize)&after=$($After)"
}
else {
$URL = "https://$($BaseURL)/api/v2/$($Request)"
}
$Params = @{
Uri = "$URL"
Method = "$Method"
Headers = @{
'Authorization' = 'Bearer {0}' -f "$Token"
"Content-Type" = 'application/json'
}
}
if ($Body) {
$Params['Body'] = $Body
}
return $Params
}
function Get-VulScanGroups {
[CmdletBinding()]
param (
[Parameter()]
[int]$SGID
)
if ($SGID) {
$Request = "/vulnerability/scan-groups/$SGID"
}
else {
$Request = '/vulnerability/scan-groups'
}
$Params = Get-BaseSettings
try {
$ScanGroupRaw = Invoke-WebRequest @Params
return $ScanGroupRaw.Content | ConvertFrom-Json
}
catch {
return $false
}
}
function New-VulScanImport {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[int]$SGID,
[Parameter(Mandatory = $true)]
[string]$CSV
)
if ($($PSVersionTable.PSVersion -ge [version]'7.0')) {
$Request = "vulnerability/scan-groups/$SGID/upload"
$Params = Get-BaseSettings -Method 'Post'
$Form = @{
"csv" = Get-Item $CSV
}
try {
$Response = Invoke-WebRequest @Params -Form $Form
Write-LogEntry -Message ($Response.Content | Out-String)
}
catch {
Write-LogEntry -Message 'Failed to import CSV.'
Write-LogEntry -Message "$($_.Exception.Message)"
exit 1
}
}
else {
$APIURL = "https://$BaseUrl/api/v2/vulnerability/scan-groups/$SGID/upload"
Add-Type -AssemblyName System.Net.Http
$HTTPClient = New-Object System.Net.Http.HttpClient
$HTTPClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $Token)
# Create the multipart form-data content
$MPFormData = New-Object System.Net.Http.MultipartFormDataContent
# Open the CSV file as a stream and prepare the StreamContent
$FileStream = [System.IO.File]::OpenRead($CSV)
$FileName = [System.IO.Path]::GetFileName($CSV)
$StreamContent = New-Object System.Net.Http.StreamContent($FileStream)
$StreamContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/csv")
# Add the CSV file content with the form field name "csv"
$MPFormData.Add($StreamContent, "csv", $FileName)
# Post the multipart content and wait for the result
$APICall = $HTTPClient.PostAsync($APIURL, $MPFormData)
$APICall.Wait()
$Response = $APICall.Result
# Read the response content
$APIResponse = $Response.Content.ReadAsStringAsync()
$APIResponse.Wait()
if (!($Response.IsSuccessStatusCode)) {
Write-LogEntry -Message 'Failed to import CSV.'
Write-LogEntry -Message ($APIResponse.Result | Out-String)
$FileStream.Close()
$HTTPClient.Dispose()
exit 1
}
Write-LogEntry -Message ($APIResponse.Result | Out-String)
$FileStream.Close()
$HTTPClient.Dispose()
}
}
function Write-LogEntry {
param (
[Parameter(Mandatory = $true)]
[string]$Message
)
$LogPath = "$PathtoCSVNinjaOneVulnerabiltyImport.log"
$TimeStamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogPath -Value "$TimeStamp $Message"
Write-Host "$TimeStamp $Message"
}
## End Function Setup ##
$Token = Get-NinjaToken
if (!(Test-Path $PathtoCSV)) {
Write-LogEntry -Message 'Entered path to CSV files does not exist. Exiting.'
exit 1
}
$CSVFiles = Get-ChildItem $PathtoCSV | Where-Object { $_.Extension -eq '.csv' -and $_.Name -match "^$CSVName" }
if (!($CSVFiles)) {
Write-LogEntry -Message 'No CSV file present to import. Exiting.'
exit 0
}
foreach ($CSV in $CSVFiles) {
Write-LogEntry -Message "Found CSV: $($CSV.Name)"
Write-LogEntry -Message "Importing to ScanGroup $ScanGroupID"
try {
New-VulScanImport -SGID $ScanGroupID -CSV $($CSV.FullName)
}
catch {
Write-LogEntry -Message 'Failed to import csv.'
Write-LogEntry -Message ($_.Exception.Message)
exit 1
}
$ImportStatus = (Get-VulScanGroups -SGID $ScanGroupID).Status
if (!($ImportStatus)) {
Write-LogEntry -Message 'Unable to retrieve import status, but the import may still succeed.'
Write-LogEntry -Message 'Please check the NinjaOne UI for vulnerability import status.'
Write-LogEntry -Message 'Appending CSV filename with UNKNOWN_'
try {
Rename-Item -Path $($CSV.FullName) "UNKNOWN_$($CSV.Name)" -ErrorAction Stop
Write-LogEntry -Message "Successfully renamed to UNKNOWN_$($CSV.Name)."
exit 1
}
catch {
Write-LogEntry -Message "Failed to rename CSV."
exit 1
}
}
while ($ImportStatus -ne 'Complete') {
Write-LogEntry -Message "Import Status: $ImportStatus"
Start-Sleep 10
$ImportStatus = (Get-VulScanGroups -SGID $ScanGroupID).Status
}
if ($ImportStatus -eq 'Complete' ) {
Write-LogEntry -Message "Import Status: $ImportStatus"
Write-LogEntry -Message "Import Successful"
Write-LogEntry -Message "Deleting $($CSV.Name)..."
try {
Remove-Item -Path $($CSV.FullName) -Force -ErrorAction Stop
Write-LogEntry -Message "Successfully deleted."
exit 0
}
catch {
Write-LogEntry -Message "Failed to delete CSV."
exit 1
}
}
else {
Write-LogEntry -Message "Import Status: $ImportStatus"
Write-LogEntry -Message "Import failed!"
Write-LogEntry -Message "Renaming $($CSV.Name)..."
try {
Rename-Item -Path $($CSV.FullName) "FAILED_$($CSV.Name)" -ErrorAction Stop
Write-LogEntry -Message "Successfully renamed to FAILED_$($CSV.Name)."
exit 1
}
catch {
Write-LogEntry -Message "Failed to rename CSV."
exit 1
}
}
}