
How can you discover if a VM in vCenter is not added to Defender? I have created two scripts, one that’s running on-prem which gets a list of all vCenter VMs that’s running any Windows OS and sends it to an Azure Automation web hook. The web hook triggers a runbook that checks if all VMs it get from vCenter is added to Defender. If there is not already a ticket created for a missing VM in Defender, it creates a new one in Jira.
Add this script part as a Scheduled Windows Task, to run for example once a day.
Check here how to do it.
| # This script will get all VMs with the guest OS "Windows Server" and send the details to a webhook URL in JSON format. | |
| $VMs = get-vm | Where-Object{$_.guest -match "Windows Server"} |Select-Object -Property Name,Guest,CreateDate | |
| # Convert the VM details to JSON format | |
| $body = $VMs | ConvertTo-Json | |
| $webhookURI = "Enter your webhook URL here" | |
| # Send the JSON data to the webhook URL for the Azure Automation runbook | |
| Invoke-WebRequest -Method Post -Uri $webhookURI -Body $body -UseBasicParsing |
This part is to be triggered by the script above. The script bellow is to check if there are any VMs created in vCenter that’s 5 days or older and not added to Defender. Add this to an Azure Automation Runbook.
You need to register an Azure App Registration with this API:
https://learn.microsoft.com/en-us/defender-endpoint/api/get-machines
You also need to create a Jira application that has both read and write rights for tickets. Take a look here on how to create ann app with a secret:
https://www.useparagon.com/blog/create-a-jira-app-to-enable-oauth-for-your-integration
Remember to store the variables for the Defender app secret and the Jira app secret. Also remember to create the webhook.
| param | |
| ( | |
| [Parameter(Mandatory=$false)] | |
| [object] $WebhookData | |
| ) | |
| $threshold = 5 | |
| # Jira authentication | |
| $Text = "[email protected]:$(Get-AutomationVariable -Name 'JIRA')" | |
| $bytes = [System.Text.Encoding]::UTF8.GetBytes($text) | |
| $EncodedText = [convert]::ToBase64String($bytes) | |
| $headersJira = @{ | |
| "Content-Type" = "application/json" | |
| "authorization" = "Basic $EncodedText" | |
| } | |
| # Microsoft Defender authentication | |
| $clientID = "xxxxxx | |
| $clientSecret = Get-AutomationVariable -Name 'Machines - VCenterDefender' | |
| $tenantID = "xxxxxx" | |
| $resource = "https://api.securitycenter.microsoft.com" | |
| $body = @{ | |
| grant_type = "client_credentials" | |
| scope = "$resource/.default" | |
| client_id = $clientId | |
| client_secret = $clientSecret | |
| } | |
| $tokenResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body $body | |
| $token = $tokenResponse.access_token | |
| $headers = @{ | |
| "Authorization" = "Bearer $token" | |
| "Content-Type" = "application/json" | |
| } | |
| # Get the list of devices from Microsoft Defender | |
| $url = "https://api.securitycenter.microsoft.com/api/machines" | |
| $DefenderDevices = (Invoke-RestMethod -Method Get -Uri $url -Headers $headers).value | |
| # Get the list of vCenter devices from the webhook data | |
| if ($WebhookData.RequestBody) { | |
| $vCenterDevices = (ConvertFrom-Json -InputObject $WebhookData.RequestBody) | |
| } | |
| else { | |
| Write-Output "No data in requestbody!" | |
| } | |
| # Extract a hashset of all (lowercase) hostnames found in defender devices | |
| $defendernames = $DefenderDevices | ForEach-Object { | |
| # Get only the first part of the DNS name (before the first dot), and lowercase it | |
| $defenderhost = $_.computerDnsName -split '\.' | Select-Object -First 1 | |
| $defenderhost.ToLower() | |
| } | Select-Object -Unique | |
| # Now compare each vCenter name (case-insensitive) against the hashset | |
| $missingInDefender = $vCenterDevices | Where-Object { | |
| $vcenterName = $_.name.ToLower() | |
| -not ($defendernames -contains $vcenterName) | |
| } | |
| #Date formating | |
| $todaystring = Get-Date -Format "dd.MM.yyyy hh:mm:ss" | |
| $today = [datetime]::ParseExact($todaystring, "dd.MM.yyyy HH:mm:ss", $null) | |
| # Loop through each missing device and create a ticket in Jira | |
| foreach($mDefender in $missingInDefender){ | |
| $summarySearch = "$($mDefender.name)" | |
| $jql = [System.Web.HttpUtility]::UrlEncode("summary ~ `"$summarySearch`"") | |
| # Construct the API URL | |
| $uri = "https://contoso.atlassian.net/rest/api/3/search?jql=$jql" | |
| $ticket = @() | |
| $ticket = Invoke-RestMethod -Method get -Uri $uri -Headers $headersJira | Where-Object{$_.issues.fields.status.name -notlike "Done"} | |
| # Check if the ticket already exists | |
| If(!$ticket){ | |
| $startDateString = get-date $mDefender.CreateDate -Format "dd.MM.yyyy hh:mm:ss" | |
| $startDate = [datetime]::ParseExact($startDateString, "dd.MM.yyyy HH:mm:ss", $null) | |
| if ((New-TimeSpan -start $startDate -End $today).days -ge $threshold){ | |
| $body = @" | |
| {"fields":{"project":{"id":"10036"},"issuetype":{"id":"10002"},"summary":"$($mDefender.name) mangler i Defender","description":{"version":1,"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"$($mDefender.name) $($mDefender.createdate) $($mDefender.notes)"}]}]},"customfield_10181":{"id":"10348","value":"Operations Team"},"components":[],"fixVersions":[],"priority":{"id":"3","name":"Medium""},"labels":["VCenterDefender"]},"update":{},"watchers":[]} | |
| "@ | |
| Invoke-RestMethod -Uri "https://contoso.atlassian.net/rest/api/3/issue/" -Method POST -Headers $headersJira -Body $body | |
| } | |
| else{Write-Output "There is already a ticket for $($mDefender.name)"} | |
| } | |
| } |