# Automated Software Vulnerability Notification

Today, I’ll guide you through setting up an automation system that notifies your users about vulnerable software detected on their devices. This information will be sourced from the Microsoft Defender API.

In this post, I’ll demonstrate how to create this automation within your Azure tenant and provide all the necessary PowerShell code.

Below is an example of the email your user will receive if a vulnerable **.NET** app is found on their device 👇

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729081016818/2d057f14-9e9d-41ee-b6c7-9f3434c55403.png align="center")

As shown, the email includes a list of vulnerable apps, their versions, clickable CVEs, and details on how each app was detected.

## But why should we make users update their apps in the first place?

* **Users can install software on their own avoiding any interruptions**
    
* **Automatic update of the app can cause compatibility issues**
    
    * this applies mostly to developer tools like Visual Studio, Python, .Net, NodeJS, etc
        

These are some of the reasons why involving your users in the app update process can be necessary.

But just informing the users without any sort of "punishment" wouldn't be sufficient. That is the reason why we are sending a second email in case the user doesn't fix the issue in time. This second email can be sent to the user manager, security team, etc. Or another more brutal approach can be chosen like isolating the device 😎.

---

# Summary

After you finish this tutorial you will have:

* Azure Automation that will monitor vulnerabilities on company devices and send email notifications based on the set thresholds to the device owner
    
    * first email by default 30 days after the first vuln. detection
        
        * TIP: A list of apps that should trigger immediate notification is supported
            
    * second email by default 14 days after the first one
        
* (optional) Azure KeyVault that will safely store SendGrid API key
    
* Azure Storage blob that will be used as persistent storage for Azure Automation runbook
    

---

# Prerequisites

* License for '**Microsoft Defender Vulnerability Management**'
    
    * [Official licensing FAQ](https://learn.microsoft.com/en-us/defender-vulnerability-management/defender-vulnerability-management-faq#defender-vulnerability-management-licensing-faqs)
        
* **Permission** to create Azure Automation and assign required [Microsoft Defender for Endpoint API](https://learn.microsoft.com/en-us/defender-endpoint/api/exposed-apis-list) permissions
    
* Willing to **pay 1$/month** for required Azure services (Storage, Automation, and KeyVault)
    
    * Or rewrite the code and host it on-premises for free
        
* **SendGrid** account (API token) for sending notification emails
    
    * Runbook code can be customized to use any SMTP service, sending Teams messages, etc.
        

---

# Let's go

## Create Automation Account

Create a new Automation Account named **VulnerableApps**.

Make a note of the **ID** of the automatically created System Managed Identity. You will need it later when granting permission.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1717173722311/a76cebe8-24e9-43a7-860f-1b6e1c70f474.png align="center")

### Create a Runbook

Inside the Automation Account create a new PowerShell 5.1 Runbook and copy-paste code from my [VulnerableApps.ps1 script](https://github.com/ztrhgf/azure_automation_runbooks/blob/main/VulnerableApps.ps1)

Now **modify** `#region CORE variables` section **to suit your environment** and **save** the result!

<div data-node-type="callout">
<div data-node-type="callout-emoji">💥</div>
<div data-node-type="callout-text"><strong>This is vital for it to work so read carefully the comments above the variables and set the correct values</strong></div>
</div>

`$sendGridParam` has to contain values set in the section **Create Azure KeyVault for storing SendGrid API key.**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1717173465149/a1990c04-89c7-42d0-87aa-1c3e2b8d1c58.png align="center")

### Grant required permissions for accessing Microsoft Defender API

In order for our automation to be able to read data from the Microsoft Defender API, it is necessary to assign it the following **WindowsDefenderATP** permissions:

* `User.Read.All`
    
* `SecurityRecommendation.Read.All`
    
* `Alert.Read.All`
    
* `Software.Read.All`
    
* `Vulnerability.Read.All`
    
* `Machine.Read.All`
    
* `AdvancedQuery.Read.All`
    

You can do it easily using my function `Grant-AzureServicePrincipalPermission` (part of the [AzureApplicationStuff module](https://www.powershellgallery.com/packages/AzureApplicationStuff)) or manually in the Azure portal

```powershell
# example code, you need to adjust it to match your environment!

Connect-AzAccount 

Grant-AzureServicePrincipalPermission -servicePrincipalId 'yourManagedIdentityId' -permissionType application -permissionList 'User.Read.All','SecurityRecommendation.Read.All','Alert.Read.All','Software.Read.All','Vulnerability.Read.All','Machine.Read.All','AdvancedQuery.Read.All' -resourceAppId 'fc780465-2017-40d4-a0c5-307022471b92'
```

## Import required modules

Runbook code depends on several other modules that I've created, so in order for it to work, we need to import them into our Automation Account environment.

Required modules:

* `AzureResourceStuff`
    
* `M365DefenderStuff`
    
* `CommonStuff`
    
* `PSSendGrid`
    

You can do it easily using my function `New-AzureAutomationModule` or `New-AzureAutomationRuntimeModule` in case you are using Runtime Environments (both functions are part of the [AzureResourceStuff module](https://www.powershellgallery.com/packages/AzureResourceStuff)) which has a huge benefit of importing but just required modules, but also their required dependencies 👍.

Or you can import the modules manually in the Automation account settings.

```powershell
# example code, you need to adjust it to match your environment!

Connect-AzAccount -Tenant "contoso.onmicrosoft.com" -SubscriptionName "AutomationSubscription"

'AzureResourceStuff','M365DefenderStuff','CommonStuff','PSSendGrid' | % {
    New-AzureAutomationModule -resourceGroupName XXX -automationAccountName YYY -moduleName $_
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">For more details on how <code>New-AzureAutomationModule</code> works, check <a target="_blank" rel="noopener noreferrer nofollow" href="https://doitpshway.com/import-new-or-update-existing-powershell-module-including-its-dependencies-into-azure-automation-account-using-powershell" style="pointer-events: none">this article</a></div>
</div>

## Create an Azure Storage Account for storing Runbook persistent data

Because Azure Automation Runbook doesn't have the option to store complex persistent data, we need to store such data in Azure Blob storage.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">For more details about creating an Azure Storage Account for storing Runbook variables check <a target="_blank" rel="noopener noreferrer nofollow" href="https://doitpshway.com/create-persistent-azure-automation-runbook-variables-using-azure-blob-storage" style="pointer-events: none">this article</a></div>
</div>

In the subscription where you host your Runbook please create the following structure:

* Resource Group Name named `PersistentRunbookVariables`
    
    * In that group create a Storage Account named `persistentvariablesstore`
        
        * In that Storage Account create a container named `variables`
            

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can choose your own names, but in such case, you will have to modify parameters when calling my <code>Export-VariableToStorage</code> and <code>Import-VariableFromStorage</code> functions</div>
</div>

### Grant Runbook Managed Identity permissions to the Azure Storage Account container

Grant our Managed Identity `Storage Blob Data Contributor` IAM role over Storage Account container created in the previous step. So it can modify files in this container.

## Create Azure KeyVault for storing SendGrid API key

For sending emails via SendGrid I am using my function `Send-EmailViaSendGrid`. This function supports retrieval of the SendGrid API key directly from Azure KeyVault, which is an ideal solution for Azure Automations.

The only thing you have to do is to create such a secret in the existing KeyVault (or some newly created one) and set `$sendGridParam` hashtable values accordingly in your Runbook code.

### Grant Runbook Managed Identity permissions to read stored SendGrid token

Grant our Managed Identity `Key Vault Secrets User` IAM role over the created secret to allow it to read it.

---

# Gotchas

* Defender vulnerability report doesn't have to be 100% accurate, because if app is incorrectly uninstalled therefore some registry keys or files remain on the device disk, Defender can still detect it even though the app isn't installed. In such rare cases, you need to manually remove some leftovers.
    
* If you use MDT for software installation, apps are installed under a built-in administrator account, which can lead to situations where the vulnerable app is detected in the built-in administrator profile/registry! This can be a significant problem for your users because it can be hard for them to remove such leftover data.
    
    * This can be solved by removing the built-in admin profile (not the account, just the profile data). For example, using Intune remediation script and PSH code
        
        ```powershell
        Get-CimInstance -Class Win32_UserProfile | ? { $_.SID -like "*-500" } | Remove-CimInstance
        ```
        

---

# Tips

* **Inform your users** about those notifications beforehand! So they don't consider them as a scam 😀
    
* You can **exclude the specific application** from this notification automation, for example, because it is the last existing version, so your users cannot update it anyway
    
    * Just update the `$exclusionList` variable in the automation code
        
        ```powershell
        <#
        - 'CveId' and/or 'ProductName' property/ies (string) have to be defined
        - 'ProductVersion' (string) is optional
            - property values can be extracted from $vulnerabilityPerMachine.vulnswdata
        - 'ValidUntil' (datetime) is optional (since this date, exclustion will be ignored)
            - BEWARE that in Azure pipeline EN date has to be used a.k.a. month.day.year!!!
        #>
        
        $exclusionList = @(
            [PSCustomObject]@{
                CveId       = 'CVE-2024-32002'
                ProductName = 'visual_studio_2022'
                ValidUntil  = (Get-Date 8.1.2024) # M.d.yyyy
            }
        )
        ```
        
* Check [this article](https://doitpshway.com/gradual-update-of-all-applications-using-winget-and-custom-azure-ring-groups) if you want to update all installed applications gradually using Winget
    
    * exceptions can be made
        
* If your helpdesk interactively logs in to employees' computers, you can avoid identifying such accounts as device owners by altering the following line of code in the runbook
    
* ```powershell
          $user = Get-M365DefenderMachineUser -header $header -machineId $machineId | ? { $_.logonTypes -like '*Interactive*' -and $_.accountName -ne "administrator" }
    ```
