Gorstaks Patcher

 avatar
unknown
powershell
2 months ago
22 kB
8
No Index
<#
.SYNOPSIS
  Fetches CISA Known Exploited Vulnerabilities (KEV), detects new CVEs, and applies
  scriptable mitigations where configured—without waiting for Windows Update.

.DESCRIPTION
  - Pulls CISA KEV catalog (JSON).
  - Tracks which CVEs were already seen (state file) so only NEW entries trigger actions.
  - For each new CVE, if a scriptable mitigation exists in CVE-MitigationActions.json,
    runs it (registry, services, firewall, etc.). Otherwise reports with advisory links.
  - Default is -WhatIf (no changes). Use -Apply to actually apply mitigations.
  - Use -RegisterSchedule to install to C:\ProgramData\CVE-MitigationPatcher and run hourly as SYSTEM (fire-and-forget). Log: same folder, CVE-MitigationPatcher.log.

.NOTES
  Requires PowerShell 5.1+. Run as Administrator to apply service/registry mitigations.
  Mitigations are workarounds; install Windows Updates when available.
#>

[CmdletBinding(SupportsShouldProcess)]
param(
  [switch] $Apply,           # Actually apply mitigations (default: WhatIf)
  [switch] $Reapply,         # Process all relevant CVEs (incl. previously seen) - use after adding config mappings
  [switch] $ReportOnly,      # Only list new CVEs and links, do not run any mitigation
  [switch] $RegisterSchedule,   # Create a scheduled task to run every hour
  [switch] $UnregisterSchedule, # Remove the scheduled task
  [switch] $ScheduleApply,   # With -RegisterSchedule: task runs with -Apply (else -ReportOnly)
  [string] $FilterVendor,    # e.g. "Microsoft" - only process these CVEs
  [string] $FilterProduct,   # e.g. "Windows" - product name contains this
  [string] $StatePath,      # Where to store "last seen" CVE IDs (default: script dir)
  [string] $ConfigPath,     # Path to CVE-MitigationActions.json
  [string] $LogPath         # Optional log file
)

$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$InstallDir = "C:\ProgramData\CVE-MitigationPatcher"
$TaskName = "CVE-MitigationPatcher"

$StatePath  = if ($StatePath)  { $StatePath }  else { Join-Path $ScriptDir "CVE-PatcherState.json" }
$ConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $ScriptDir "CVE-MitigationActions.json" }
$LogPath    = if ($LogPath)    { $LogPath }    else { Join-Path $ScriptDir "CVE-MitigationPatcher.log" }

# --------------- Embedded config (used when no external JSON found - single-file distribution) ---------------
$EmbeddedConfigJson = @'
{"templates":{"Set-Registry-DWord":{"type":"registry","path":"{{path}}","name":"{{name}}","value":"{{value}}","valueType":"DWord"},"Delete-RegistryKey":{"type":"deleteRegistryKey","path":"{{path}}"},"Disable-Service":{"type":"service","name":"{{name}}","startupType":"Disabled"},"Block-Firewall-Port":{"type":"firewall","displayName":"{{displayName}}","port":"{{port}}","protocol":"TCP","direction":"Inbound","action":"Block"}},"defaultActionsForVendorProduct":[{"vendor":"Microsoft","productPattern":"Windows","description":"Generic Windows hardening","run":["Disable-SMBv1","Block-MSDTProtocol","Disable-WebClient"]}],"actions":{"CVE-2017-0143":{"description":"SMBv1 RCE","run":["Disable-SMBv1"]},"CVE-2017-0144":{"description":"EternalBlue","run":["Disable-SMBv1"]},"CVE-2017-0145":{"description":"EternalRomance","run":["Disable-SMBv1"]},"CVE-2017-0146":{"description":"SMBv1 RCE","run":["Disable-SMBv1"]},"CVE-2017-0147":{"description":"SMBv1 info disclosure","run":["Disable-SMBv1"]},"CVE-2017-0148":{"description":"SMBv1 server RCE","run":["Disable-SMBv1"]},"CVE-2020-0796":{"description":"SMBGhost","run":["Disable-SMBv3Compression"]},"CVE-2019-0708":{"description":"BlueKeep","run":["Enable-RDPNLA"]},"CVE-2020-1350":{"description":"SigRed","run":["Mitigate-SigRed"]},"CVE-2022-30190":{"description":"Follina","run":["Block-MSDTProtocol"]},"CVE-2022-34713":{"description":"MSDT RCE","run":["Block-MSDTProtocol"]},"CVE-2021-34527":{"description":"PrintNightmare","run":["Disable-PrintSpooler"]},"CVE-2021-1675":{"description":"PrintNightmare LPE","run":["Disable-PrintSpooler"]},"CVE-2024-38063":{"description":"IPv6 RCE","run":["Disable-IPv6"]},"CVE-2022-26923":{"description":"PetitPotam","run":["Disable-WebClient"]},"CVE-2022-30136":{"description":"NFSv4 RCE","run":["Disable-NFS"]},"CVE-2022-26937":{"description":"NFS RCE","run":["Disable-NFS"]}}}
'@

function Write-Log {
  param([string]$Message, [string]$Level = "INFO")
  $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
  Add-Content -Path $LogPath -Value $line -ErrorAction SilentlyContinue
  if ($Level -eq "ERROR") { Write-Error $Message } else { Write-Host $Message }
}

# --------------- Install self to ProgramData (for fire-and-forget) ---------------
function Install-Self {
  if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null }
  $scriptName = Split-Path -Leaf $MyInvocation.MyCommand.Path
  $destScript = Join-Path $InstallDir $scriptName
  $srcScript = $MyInvocation.MyCommand.Path
  Copy-Item -Path $srcScript -Destination $destScript -Force
  $configName = "CVE-MitigationActions.json"
  $srcConfig = Join-Path (Split-Path -Parent $srcScript) $configName
  if (Test-Path $srcConfig) { Copy-Item -Path $srcConfig -Destination (Join-Path $InstallDir $configName) -Force }
  $mitDir = Join-Path (Split-Path -Parent $srcScript) "Mitigations"
  if (Test-Path $mitDir) {
    $destMit = Join-Path $InstallDir "Mitigations"
    if (-not (Test-Path $destMit)) { New-Item -ItemType Directory -Path $destMit -Force | Out-Null }
    Copy-Item -Path "$mitDir\*" -Destination $destMit -Recurse -Force -ErrorAction SilentlyContinue
  }
  return $destScript
}

# --------------- Task scheduling ---------------
if ($UnregisterSchedule) {
  $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
  if ($t) {
    Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
    Write-Host "Scheduled task '$TaskName' removed. To remove files: delete $InstallDir"
  } else {
    Write-Host "Scheduled task '$TaskName' not found."
  }
  exit 0
}
if ($RegisterSchedule) {
  $installedScript = Install-Self
  $workDir = $InstallDir
  $argList = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installedScript`""
  if ($ScheduleApply) { $argList += " -Apply" } else { $argList += " -ReportOnly" }
  if ($FilterVendor) { $argList += " -FilterVendor `"$FilterVendor`"" }
  if ($FilterProduct) { $argList += " -FilterProduct `"$FilterProduct`"" }
  $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $argList -WorkingDirectory $workDir
  $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Hours 1) -RepetitionDuration ([TimeSpan]::MaxValue)
  $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
  $principal = if ($ScheduleApply) {
    New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
  } else {
    New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
  }
  $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
  $params = @{ TaskName = $TaskName; Action = $action; Trigger = $trigger; Settings = $settings; Principal = $principal }
  if ($task) { Set-ScheduledTask @params } else { Register-ScheduledTask @params }
  Write-Host "Fire-and-forget: task '$TaskName' runs every hour as SYSTEM. Log: $InstallDir\CVE-MitigationPatcher.log"
  exit 0
}

$KevUrl = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"

# --------------- Built-in mitigation actions (scriptable) ---------------
$MitigationActions = @{
  "Disable-SMBv1" = {
    # SMBv1 - EternalBlue, EternalChampion, etc. (CVE-2017-0143, 2017-0144, 2017-0145, 2017-0146)
    $path = "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters"
    if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
    Set-ItemProperty -Path $path -Name "SMB1" -Value 0 -Type DWord -Force
    $fs = Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction SilentlyContinue
    if ($fs -and $fs.State -eq "Enabled") {
      Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart -ErrorAction SilentlyContinue
    }
  }
  "Disable-SMBv3Compression" = {
    # CVE-2020-0796 SMBGhost - disables SMBv3 compression on server
    $path = "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters"
    if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
    Set-ItemProperty -Path $path -Name "DisableCompression" -Value 1 -Type DWord -Force
  }
  "Disable-PrintSpooler" = {
    # PrintNightmare and similar (CVE-2021-34527, CVE-2021-1675)
    Stop-Service -Name Spooler -Force -ErrorAction SilentlyContinue
    Set-Service -Name Spooler -StartupType Disabled
  }
  "Block-MSDTProtocol" = {
    # CVE-2022-30190 Follina, CVE-2022-34713 - removes ms-msdt URL protocol handler
    $path = "HKCR:\ms-msdt"
    if (Test-Path $path) {
      Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue
    }
  }
  "Mitigate-SigRed" = {
    # CVE-2020-1350 SigRed - DNS server only; restricts TCP response size
    $svc = Get-Service -Name DNS -ErrorAction SilentlyContinue
    if (-not $svc) { return }
    $path = "HKLM:\SYSTEM\CurrentControlSet\Services\DNS\Parameters"
    if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
    Set-ItemProperty -Path $path -Name "TcpReceivePacketSize" -Value 0xFF00 -Type DWord -Force
    Restart-Service -Name DNS -Force -ErrorAction SilentlyContinue
  }
  "Enable-RDPNLA" = {
    # CVE-2019-0708 BlueKeep - requires NLA before RDP session
    $path = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
    if (Test-Path $path) {
      Set-ItemProperty -Path $path -Name "UserAuthentication" -Value 1 -Type DWord -Force
    }
  }
  "Disable-WebClient" = {
    # NTLM relay, WebDAV abuse vectors
    Stop-Service -Name WebClient -Force -ErrorAction SilentlyContinue
    Set-Service -Name WebClient -StartupType Disabled
  }
  "Disable-IPv6" = {
    # CVE-2024-38063 workaround: disable IPv6 via registry
    $path = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters"
    if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
    Set-ItemProperty -Path $path -Name "DisabledComponents" -Value 0xFF -Type DWord -Force
  }
  "Disable-LLMNR" = {
    # NTLM relay / LLMNR poisoning: disable multicast name resolution
    $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient"
    if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
    Set-ItemProperty -Path $path -Name "EnableMulticast" -Value 0 -Type DWord -Force
  }
  "Disable-NBT-NS" = {
    # NTLM relay / NetBIOS poisoning: disable NetBIOS over TCP/IP on all interfaces
    $basePath = "HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces"
    if (Test-Path $basePath) {
      Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue | ForEach-Object {
        Set-ItemProperty -Path $_.PSPath -Name "NetbiosOptions" -Value 2 -Type DWord -Force -ErrorAction SilentlyContinue
      }
    }
  }
  "Disable-RemoteRegistry" = {
    # Reduces lateral movement; attackers often use Remote Registry for recon
    Stop-Service -Name RemoteRegistry -Force -ErrorAction SilentlyContinue
    Set-Service -Name RemoteRegistry -StartupType Disabled -ErrorAction SilentlyContinue
  }
  "Disable-NFS" = {
    # CVE-2022-30136: Windows NFSv4 RCE - disable NFS services (Server/Client/Redirector)
    Get-Service -Name *Nfs* -ErrorAction SilentlyContinue | ForEach-Object {
      Stop-Service -Name $_.Name -Force -ErrorAction SilentlyContinue
      Set-Service -Name $_.Name -StartupType Disabled -ErrorAction SilentlyContinue
    }
  }
}

# --------------- Fetch CISA KEV and parse ---------------
function Get-CisaKevCatalog {
  Write-Log "Fetching CISA KEV catalog..."
  try {
    $r = Invoke-RestMethod -Uri $KevUrl -Method Get -UseBasicParsing
    return $r
  } catch {
    Write-Log "Failed to fetch KEV: $_" "ERROR"
    throw
  }
}

function Get-RelevantCves {
  param($Catalog, [string]$Vendor, [string]$Product)
  $list = @($Catalog.vulnerabilities)
  if ($Vendor) {
    $list = $list | Where-Object { $_.vendorProject -eq $Vendor }
  }
  if ($Product) {
    $list = $list | Where-Object { $_.product -like "*$Product*" }
  }
  return $list
}

# --------------- State: last seen CVE IDs ---------------
function Get-SeenCveIds {
  if (-not (Test-Path $StatePath)) { return @{} }
  try {
    $o = Get-Content $StatePath -Raw | ConvertFrom-Json
    $h = @{}
    $o.seenCveIds.PSObject.Properties | ForEach-Object { $h[$_.Name] = $true }
    return $h
  } catch {
    return @{}
  }
}

function Save-SeenCveIds {
  param([string[]]$CveIds)
  $seen = Get-SeenCveIds
  foreach ($id in $CveIds) { $seen[$id] = $true }
  @{ seenCveIds = $seen; lastUpdate = (Get-Date -Format "o") } | ConvertTo-Json -Depth 3 | Set-Content $StatePath -Encoding UTF8
}

# --------------- Load config: CVE -> actions, templates (file or embedded) ---------------
$script:ConfigDir = $ScriptDir   # fallback for embedded config / relative script paths
$script:Templates = @{}

function Get-MitigationConfig {
  $cfg = $null
  if (Test-Path $ConfigPath) {
    try {
      $cfg = Get-Content $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
      $script:ConfigDir = Split-Path -Parent $ConfigPath
    } catch {
      Write-Log "Error loading config file: $_" "ERROR"
    }
  }
  if (-not $cfg) {
    try {
      $cfg = $EmbeddedConfigJson | ConvertFrom-Json
      Write-Log "Using embedded config (no file at $ConfigPath)"
      $script:ConfigDir = $ScriptDir
    } catch {
      Write-Log "Error loading embedded config: $_" "ERROR"
      return @{ actions = @{}; defaultActionsForVendorProduct = @() }
    }
  }
  $out = @{}
  if ($cfg.actions) {
    $cfg.actions.PSObject.Properties | ForEach-Object {
      $out[$_.Name] = $_.Value
    }
  }
  if ($cfg.templates) {
    $script:Templates = @{}
    $cfg.templates.PSObject.Properties | ForEach-Object {
      $script:Templates[$_.Name] = $_.Value
    }
  }
  $defaults = @()
  if ($cfg.defaultActionsForVendorProduct) {
    $defaults = @($cfg.defaultActionsForVendorProduct)
  }
  return @{ actions = $out; defaultActionsForVendorProduct = $defaults }
}

# --------------- Expand template with params ({{key}} -> params.key) ---------------
function Expand-Template {
  param($Template, [hashtable]$Params)
  if (-not $Template) { return $null }
  $json = ($Template | ConvertTo-Json -Depth 10 -Compress)
  foreach ($k in $Params.Keys) {
    $val = [string]$Params[$k]
    $val = $val -replace '\\','\\\\' -replace '"','\"'
    $json = $json -replace [regex]::Escape("{{$k}}"), $val
  }
  try { return $json | ConvertFrom-Json } catch { return $null }
}

# --------------- Execute generic action (registry, service, firewall, deleteRegistryKey, script) ---------------
function Invoke-GenericAction {
  param($Action, [string]$CveId)
  $type = if ($Action.PSObject.Properties['type']) { $Action.type } else { $null }
  if (-not $type) {
    Write-Log "Generic action missing 'type' (CVE: $CveId)" "ERROR"
    return $false
  }
  $desc = "$type for $CveId"
  if ($ReportOnly) { return $false }
  if (-not $Apply) {
    Write-Log "[WhatIf] Would apply: $desc"
    return $true
  }
  try {
    switch ($type) {
      "registry" {
        $path = $Action.path
        $name = $Action.name
        $val = if ($null -ne $Action.value) { $Action.value } else { $Action.Value }
        $vtype = if ($Action.valueType) { $Action.valueType } else { "DWord" }
        if ($vtype -eq "DWord" -or $vtype -eq "QWord") { $val = [int]$val }
        if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
        Set-ItemProperty -Path $path -Name $name -Value $val -Type $vtype -Force
      }
      "deleteRegistryKey" {
        $path = $Action.path
        if (Test-Path $path) { Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue }
      }
      "service" {
        $svcName = $Action.name
        $startup = if ($Action.startupType) { $Action.startupType } else { "Disabled" }
        Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue
        Set-Service -Name $svcName -StartupType $startup
      }
      "firewall" {
        $dn = $Action.displayName
        $port = [int]$Action.port
        $proto = if ($Action.protocol) { $Action.protocol } else { "TCP" }
        $dir = if ($Action.direction) { $Action.direction } else { "Inbound" }
        $act = if ($Action.action) { $Action.action } else { "Block" }
        $rule = Get-NetFirewallRule -DisplayName $dn -ErrorAction SilentlyContinue
        if (-not $rule) {
          New-NetFirewallRule -DisplayName $dn -Direction $dir -Protocol $proto -LocalPort $port -Action $act -ErrorAction Stop
        }
      }
      "script" {
        $relPath = $Action.path
        $fullPath = if ([System.IO.Path]::IsPathRooted($relPath)) { $relPath } else { Join-Path $script:ConfigDir $relPath }
        $args = if ($Action.arguments) { @($Action.arguments) } else { @() }
        if (-not (Test-Path $fullPath)) { Write-Log "Script not found: $fullPath" "ERROR"; return $false }
        & $fullPath @args
      }
      default { Write-Log "Unknown generic action type: $type" "ERROR"; return $false }
    }
    Write-Log "Applied mitigation: $desc"
    return $true
  } catch {
    Write-Log "Failed to apply $desc : $_" "ERROR"
    return $false
  }
}

# --------------- Resolve run entry: string (named action) or object (generic/template/script) ---------------
function Invoke-RunEntry {
  param($Entry, [string]$CveId)
  if ($Entry -is [string]) {
    if (-not $MitigationActions[$Entry]) {
      Write-Log "Unknown named action: $Entry (CVE: $CveId)" "ERROR"
      return $false
    }
    if ($ReportOnly) { return $false }
    if ($Apply) {
      try {
        & $MitigationActions[$Entry]
        Write-Log "Applied mitigation: $Entry for $CveId"
        return $true
      } catch {
        Write-Log "Failed to apply $Entry for $CveId : $_" "ERROR"
        return $false
      }
    } else {
      Write-Log "[WhatIf] Would apply: $Entry for $CveId"
      return $true
    }
  }
  if ($Entry.PSObject.Properties['template']) {
    $tplName = $Entry.template
    $params = @{}
    if ($Entry.params) {
      $Entry.params.PSObject.Properties | ForEach-Object { $params[$_.Name] = $_.Value }
    }
    $tpl = $script:Templates[$tplName]
    if (-not $tpl) { Write-Log "Template not found: $tplName (CVE: $CveId)" "ERROR"; return $false }
    $expanded = Expand-Template -Template $tpl -Params $params
    if ($expanded) { return Invoke-GenericAction -Action $expanded -CveId $CveId }
    return $false
  }
  if ($Entry.PSObject.Properties['type']) {
    return Invoke-GenericAction -Action $Entry -CveId $CveId
  }
  Write-Log "Invalid run entry (CVE: $CveId)" "ERROR"
  return $false
}

# --------------- Apply one mitigation by name (legacy) ---------------
function Invoke-MitigationAction {
  param([string]$ActionName, [string]$CveId)
  return Invoke-RunEntry -Entry $ActionName -CveId $CveId
}

# --------------- Main ---------------
try {
  $mode = if ($ReportOnly) { "ReportOnly" } elseif ($Apply) { "Apply" } else { "WhatIf" }
  $filter = if ($FilterVendor) { "Vendor=$FilterVendor" } elseif ($FilterProduct) { "Product=$FilterProduct" } else { "All" }
  Write-Log "Run started. Mode=$mode Filter=$filter"
  $catalog = Get-CisaKevCatalog
  $cves = Get-RelevantCves -Catalog $catalog -Vendor $FilterVendor -Product $FilterProduct
  if (-not $FilterVendor -and -not $FilterProduct) {
    $cves = @($catalog.vulnerabilities)
  }
  Write-Log "Relevant CVEs in KEV: $($cves.Count)"

  $seen = Get-SeenCveIds
  $config = Get-MitigationConfig
  $newCves = if ($Reapply) { @($cves) } else { @($cves | Where-Object { -not $seen[$_.cveID] }) }
  $newIds = @($newCves | ForEach-Object { $_.cveID })

  if ($newIds.Count -eq 0) {
    Write-Log "No CVEs to process. (Use -Reapply to process previously seen CVEs with updated config.)"
    exit 0
  }

  if ($Reapply) {
    Write-Log "Reapply mode: processing all $($newIds.Count) relevant CVEs (including previously seen)"
  } else {
    Write-Log "New CVEs (since last run): $($newIds.Count)"
  }
  foreach ($cve in $newCves) {
    $id = $cve.cveID
    $vendor = $cve.vendorProject
    $product = $cve.product
    $name = $cve.vulnerabilityName
    $notes = $cve.notes
    $link = ($notes -split ' ' | Where-Object { $_ -match '^https?://' } | Select-Object -First 1) -replace '[,;]$',''

    Write-Log "  $id | $vendor | $product | $name"
    Write-Log "    Link: $link"

    $actionConfig = $config.actions[$id]
    $runList = $null
    if ($actionConfig -and $actionConfig.run) {
      $runList = @($actionConfig.run)
    } elseif ($config.defaultActionsForVendorProduct -and $config.defaultActionsForVendorProduct.Count -gt 0) {
      # Vendor/product-based defaults: apply when no explicit CVE config
      $matched = $config.defaultActionsForVendorProduct | Where-Object {
        $v = $_.vendor; $p = $_.productPattern
        (-not $v -or $vendor -eq $v) -and (-not $p -or $product -like "*$p*")
      } | Select-Object -First 1
      if ($matched -and $matched.run) {
        $runList = @($matched.run)
        Write-Log "    Using vendor/product defaults for $vendor | $product"
      }
    }
    if ($runList) {
      foreach ($entry in $runList) {
        $null = Invoke-RunEntry -Entry $entry -CveId $id
      }
    } else {
      Write-Log "    No scriptable mitigation in config. Apply workarounds from the link above."
    }
  }

  if (-not $Reapply) { Save-SeenCveIds -CveIds $newIds }
  Write-Log "State updated. Done."
} catch {
  Write-Log "Script failed: $_" "ERROR"
  exit 1
}
Editor is loading...
Leave a Comment