> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neoagent.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Patch RoboShadow CVEs with your RMM

> Add three RMM components so a Neo agent can patch the CVEs RoboShadow finds, rescan the device and confirm the fix

Neo reads RoboShadow and changes nothing there, because RoboShadow's API only reads. The fix runs on the device through your RMM. Add the three components below to your RMM once, and an agent that has RoboShadow and your RMM's script tool can patch a CVE end to end:

1. It reads the client's CVEs in RoboShadow, ranks them, and reads the fix for each one.
2. It matches each RoboShadow device to the device in your RMM, by name inside the client's company, then by serial number when two devices share a name. When it cannot find exactly one device, it patches nothing there and tells you.
3. It upgrades the vulnerable application with **\[Neo Script] WinGet Upgrade**, or installs the missing update with **\[Neo Script] Install Windows Update**: one package or one KB per component run, under the approval rules of your RMM script tool.
4. It runs **\[Neo Script] RoboShadow Rescan**, waits for RoboShadow's next scan, and calls a CVE resolved only when that scan no longer shows it.
5. It writes what it did, device by device, in an internal note on the ticket. A weekly agent with no ticket writes it on the client's **RoboShadow CVE remediation** ticket, and creates that ticket when there is none.

The rescan also fires your RoboShadow AutoFix rules. When an AutoFix rule already fixed the same application on other devices, the agent runs the rescan alone.

<Note>
  Some runtimes, such as .NET and the Visual C++ Redistributables, keep the older version beside the new one, so RoboShadow can still list the CVE after a successful upgrade. The agent reports it and does not patch again. RoboShadow's AutoFix rule for the application has an **Uninstall Before Upgrade** option; test it on one device first, because other applications may need the older version.
</Note>

## Add the components to your RMM

Create each component as a PowerShell script that runs as the local system account, with the exact name below. Neo finds the components by name, so a renamed copy is never used. Each one reads its input from an environment variable, which is how Datto RMM passes a component variable, or from a `PackageId=<value>` or `KB=<value>` argument, which is how NinjaOne passes a script variable. The patch components are checked against how Datto RMM and NinjaOne pass a variable. On another RMM, a component that does not receive its input reports `MISSING_INPUT` and changes nothing, and the agent stops patching on that RMM and tells you. The agent runs them through Neo's RMM script tool, which N-central, ScreenConnect and Syncro do not use, so on those RMMs the agent reads and reports only.

In Datto RMM: open **Automation** > **Components**, click **New Component**, choose the **Scripts** category and **PowerShell**, paste the script, and add the variable the table names as a **String** variable. In NinjaOne, add a script variable with the same name. The agent runs a patch component only when its synced record declares that variable. Then click the sync button under [Integrations > RMM](https://dashboard.neoagent.io/integrations) in Neo, so Neo finds the new components.

| Component | Variable | What it does |
| - | - | - |
| `[Neo Script] WinGet Upgrade` | `PackageId` | Upgrades one WinGet package, such as `Google.Chrome`, to the newest version WinGet offers. Nothing else is upgraded. |
| `[Neo Script] Install Windows Update` | `KB` | Installs one missing Windows update, such as `KB5034441`. It never restarts the device and reports when a restart is needed. |
| `[Neo Script] RoboShadow Rescan` | none | Restarts the `RoboShadowAgent` service, which makes RoboShadow rescan the device, and prints the device's RoboShadow id. |

The WinGet component needs App Installer (WinGet) installed for all users, as it is on current Windows 10 and 11 builds, x64 or ARM64. On a device without it, the component reports `NO_WINGET` and changes nothing.

### \[Neo Script] WinGet Upgrade

```powershell theme={null}
# [Neo Script] WinGet Upgrade
# Upgrades one WinGet package, named by its exact WinGet id, to the newest version WinGet offers.
# Variable: PackageId, a WinGet package id such as Google.Chrome. Never upgrades anything else.
# Reads PackageId from an environment variable (Datto RMM) or a PackageId=<value> argument (NinjaOne).
# Runs as SYSTEM, so it calls winget.exe from the App Installer folder, not from the PATH.
$ErrorActionPreference = 'Stop'
trap { Write-Output "RESULT: FAILED ($($_.Exception.Message))"; exit 1 }

$packageId = "$env:PackageId"
if (-not $packageId) { $packageId = "$($args | Where-Object { "$_" -like 'PackageId=*' } | Select-Object -First 1)" -replace '^PackageId=', '' }
$packageId = $packageId.Trim()
if (-not $packageId) {
    Write-Output 'RESULT: MISSING_INPUT (the RMM passed no PackageId)'
    exit 1
}
if ($packageId -notmatch '^[A-Za-z0-9][A-Za-z0-9._+-]{1,127}$') {
    Write-Output "RESULT: BAD_INPUT (PackageId '$packageId' is not a WinGet package id)"
    exit 1
}

# The 64-bit Program Files and the device's own architecture, also from a 32-bit PowerShell.
$programFiles = if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }
$arch = if (@($env:PROCESSOR_ARCHITEW6432, $env:PROCESSOR_ARCHITECTURE) -contains 'ARM64') { 'arm64' } else { 'x64' }
$winget = Get-ChildItem -Path "$programFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_${arch}__8wekyb3d8bbwe\winget.exe" -ErrorAction SilentlyContinue |
    Sort-Object -Property FullName -Descending | Select-Object -First 1
if ($null -eq $winget) {
    Write-Output 'RESULT: NO_WINGET (App Installer is not installed for all users on this device)'
    exit 1
}

$common = @('--id', $packageId, '--exact', '--accept-source-agreements', '--disable-interactivity')
Write-Output 'Before:'
& $winget.FullName list @common 2>&1 | Out-String | Write-Output

& $winget.FullName upgrade @common --silent --accept-package-agreements 2>&1 | Out-String | Write-Output
$code = $LASTEXITCODE

Write-Output 'After:'
& $winget.FullName list @common 2>&1 | Out-String | Write-Output

# 0x8A15002B: no newer version applies; 0x8A150014: the package is not installed.
switch ($code) {
    0 { Write-Output 'RESULT: UPGRADED'; exit 0 }
    -1978335189 { Write-Output 'RESULT: NO_UPGRADE_AVAILABLE'; exit 0 }
    -1978335212 { Write-Output 'RESULT: NOT_INSTALLED'; exit 1 }
    default { Write-Output "RESULT: FAILED (winget exit code $code)"; exit 1 }
}
```

### \[Neo Script] Install Windows Update

```powershell theme={null}
# [Neo Script] Install Windows Update
# Installs one missing Windows update, named by its KB number, through the Windows Update Agent.
# Variable: KB, such as KB5034441. Never installs any other update and never restarts the device.
# Reads KB from an environment variable (Datto RMM) or a KB=<value> argument (NinjaOne).
$ErrorActionPreference = 'Stop'
trap { Write-Output "RESULT: FAILED ($($_.Exception.Message))"; exit 1 }

$kb = "$env:KB"
if (-not $kb) { $kb = "$($args | Where-Object { "$_" -like 'KB=*' } | Select-Object -First 1)" -replace '^KB=', '' }
$kb = $kb.Trim().ToUpper()
if (-not $kb) {
    Write-Output 'RESULT: MISSING_INPUT (the RMM passed no KB)'
    exit 1
}
if ($kb -notmatch '^(KB)?\d{6,8}$') {
    Write-Output "RESULT: BAD_INPUT (KB '$kb' is not a KB number)"
    exit 1
}
$kbNumber = $kb -replace '^KB', ''

$session = New-Object -ComObject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
$found = $searcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0")
$wanted = @($found.Updates | Where-Object { $_.KBArticleIDs -contains $kbNumber })
if ($wanted.Count -eq 0) {
    Write-Output "RESULT: NOT_FOUND (KB$kbNumber is installed, superseded or not offered to this device)"
    exit 0
}

$updates = New-Object -ComObject Microsoft.Update.UpdateColl
foreach ($update in $wanted) {
    if (-not $update.EulaAccepted) { $update.AcceptEula() }
    [void]$updates.Add($update)
    Write-Output "Update: $($update.Title)"
}

$downloader = $session.CreateUpdateDownloader()
$downloader.Updates = $updates
[void]$downloader.Download()

$installer = $session.CreateUpdateInstaller()
$installer.Updates = $updates
$result = $installer.Install()

# OperationResultCode: 2 succeeded, 3 succeeded with errors, 4 failed, 5 aborted. Read each update's
# own code, because 3 means some of them installed.
$installed = 0
$withErrors = 0
for ($i = 0; $i -lt $updates.Count; $i++) {
    $title = $updates.Item($i).Title
    $updateCode = $result.GetUpdateResult($i).ResultCode
    Write-Output "Update result: $title = $updateCode"
    if ($updateCode -eq 2) { $installed++ }
    if ($updateCode -eq 3) { $withErrors++ }
}
Write-Output "Reboot required: $($result.RebootRequired)"
if ($installed -eq $updates.Count) {
    Write-Output 'RESULT: INSTALLED'
    exit 0
}
if ($installed + $withErrors -gt 0) {
    Write-Output "RESULT: PARTIAL ($installed of $($updates.Count) installed, $withErrors with errors)"
    exit 1
}
Write-Output "RESULT: FAILED (result code $($result.ResultCode))"
exit 1
```

### \[Neo Script] RoboShadow Rescan

```powershell theme={null}
# [Neo Script] RoboShadow Rescan
# Restarts the RoboShadowAgent service, which makes the RoboShadow agent rescan this device.
# A rescan also fires the RoboShadow AutoFix rules that match the device's applications.
# No variables. Prints the device's RoboShadow DeviceId so Neo can check it ran on the right device.
$ErrorActionPreference = 'Stop'
trap { Write-Output "RESULT: FAILED ($($_.Exception.Message))"; exit 1 }

$service = Get-Service -Name 'RoboShadowAgent' -ErrorAction SilentlyContinue
if ($null -eq $service) {
    Write-Output 'RESULT: NO_AGENT (the RoboShadowAgent service is not installed)'
    exit 1
}

$control = Get-ItemProperty -Path 'HKLM:\SOFTWARE\RoboShadowLtd\Rubicon\Control' -ErrorAction SilentlyContinue
if ($null -ne $control) {
    Write-Output "DeviceId: $($control.DeviceId)"
}

$started = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
Restart-Service -Name 'RoboShadowAgent' -Force
Start-Sleep -Seconds 15
$status = (Get-Service -Name 'RoboShadowAgent').Status
Write-Output "Service: $status"
Write-Output "Started (UTC): $started"
if ($status -ne 'Running') {
    Write-Output 'RESULT: FAILED (the service did not start again)'
    exit 1
}
Write-Output 'RESULT: RESCAN_STARTED'
```

## Set up the agent

Give the agent RoboShadow with the **Vulnerabilities and Updates** and **Organisations, Devices and Inventory** areas on, script execution on your RMM, and an internal-note permission on your PSA. A weekly agent with no ticket also needs ticket create on your PSA for the **RoboShadow CVE remediation** ticket.

Each patch run follows the agent's approval setting for RMM scripts, and runs without a request when an admin pre-approved that exact script and value. While you test, turn on approval for RMM scripts on the agent: the agent then sends the plan, with the device, the package or KB and the CVEs, and runs it once a technician approves.

See [Connecting RoboShadow to Neo](/integrations/roboshadow/connecting-to-neo) to connect RoboShadow first.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.