Stay Current with Microsoft Sentinel Content Hub Updates

Introduction

When you set up Microsoft Sentinel and install items from the Content Hub – such as data connectors, workbooks, and analytics rules – it’s tempting to think it’s a one-and-done job. In reality, these resources evolve. New versions get published to the Microsoft Sentinel Content Hub catalog, updates appear on GitHub repositories, and improved analytics rules help you respond better to evolving threats. If you never review or update what’s installed, you could miss critical enhancements that strengthen your security posture.

I’ve found that regularly checking for new versions of these packages ensures my SIEM environment, driven by Microsoft Sentinel’s SOAR capabilities, remains effective. Stale or outdated connectors and content can limit visibility and responsiveness. Manually sifting through lists is inefficient. That’s why I developed two PowerShell scripts: one to identify what’s installed, and another to pinpoint which solutions need updates. From there, it’s straightforward to act on available updates, ensuring your Microsoft Azure Sentinel setup is always leveraging the latest innovations.

By running these checks on a schedule, you prevent your environment from falling behind. This process becomes even easier if you integrate it into an Azure Automation runbook. Instead of occasionally remembering to run manual queries, you can automate the entire cycle – detecting what needs attention, producing a clear HTML and JSON summary, and then taking action. Over time, this proactive approach helps maintain a healthier Microsoft Sentinel deployment, supports continuous improvement in your analytics rules and data connectors, and ensures that you are never caught off-guard by outdated content.

The Scripts

Below are the two scripts. The first one inventories your current Content Hub solutions. The second compares installed solutions against what’s available and outputs both an HTML report (for human-friendly review) and a JSON file (for programmatic use). With this information, you’ll quickly see which updates are needed – no guesswork or missed opportunities.

Export-InstalledSentinelContentHubPackages

				
					<#
    =================================================================================
    DISCLAIMER:
    This script is provided "as-is" with no warranties. Usage of this script is at
    your own risk. The author is not liable for any damages or losses arising from
    using this script. Please review the full legal disclaimer at:
    https://kaidojarvemets.com/legal-disclaimer/
    =================================================================================
#>
# Variables for Azure environment
$SubscriptionId = ""
$ResourceGroupName = ""
$WorkspaceName = ""
$ApiVersion = "2024-03-01"

# Construct the API URL
$BaseUri = "https://management.azure.com"
$ResourcePath = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.OperationalInsights/workspaces/$WorkspaceName/providers/Microsoft.SecurityInsights"
$Endpoint = "contentPackages"
$ApiVersionParam = "?api-version=$ApiVersion"
$FullUri = "$BaseUri$ResourcePath/$Endpoint$ApiVersionParam"

# Function to get installed Content Hub solutions
function Get-InstalledContentHubSolutions {
    param (
        [string]$Uri
    )
    
    try {
        $Response = Invoke-AzRestMethod -Method GET -Uri $Uri -ErrorAction Stop
        
        if ($Response.StatusCode -eq 200) {
            $Content = $Response.Content | ConvertFrom-Json
            return $Content.value
        }
        else {
            Write-Error "API call failed with status code: $($Response.StatusCode)"
            return $null
        }
    }
    catch {
        Write-Error "An error occurred: $($Error[0])"
        return $null
    }
}

# Get the installed solutions and format output
$InstalledSolutions = Get-InstalledContentHubSolutions -Uri $FullUri

$InstalledSolutions | Select-Object @{
    Name = 'DisplayName'; Expression = { $PSITEM.properties.displayName }
}, @{
    Name = 'ContentId'; Expression = { $PSITEM.properties.contentId }
}, @{
    Name = 'Version'; Expression = { $PSITEM.properties.version }
}, @{
    Name = 'ContentKind'; Expression = { $PSITEM.properties.contentKind }
} | Format-Table -AutoSize
				
			

Create-InstalledSentinelContentHubPackagesReports

				
					<#
    =================================================================================
    DISCLAIMER:
    This script is provided "as-is" with no warranties. Usage of this script is at
    your own risk. The author is not liable for any damages or losses arising from
    using this script. Please review the full legal disclaimer at:
    https://kaidojarvemets.com/legal-disclaimer/
    =================================================================================
#>
# Variables for Azure environment
$SubscriptionId = ""
$ResourceGroupName = ""
$WorkspaceName = ""
$ApiVersion = "2024-03-01"

# Construct the API URLs
$BaseUri = "https://management.azure.com"
$ResourcePath = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.OperationalInsights/workspaces/$WorkspaceName/providers/Microsoft.SecurityInsights"
$InstalledEndpoint = "contentPackages"
$AvailableEndpoint = "contentProductPackages"
$ApiVersionParam = "?api-version=$ApiVersion"

$InstalledUri = "$BaseUri$ResourcePath/$InstalledEndpoint$ApiVersionParam"
$AvailableUri = "$BaseUri$ResourcePath/$AvailableEndpoint$ApiVersionParam"

function Get-ContentHubSolutions {
    param (
        [string]$Uri
    )
    
    try {
        $Response = Invoke-AzRestMethod -Method GET -Uri $Uri -ErrorAction Stop
        
        if ($Response.StatusCode -eq 200) {
            $Content = $Response.Content | ConvertFrom-Json
            return $Content.value
        }
        else {
            Write-Error "API call failed with status code: $($Response.StatusCode)"
            return $null
        }
    }
    catch {
        Write-Error "An error occurred: $($Error[0])"
        return $null
    }
}

# Get both installed and available solutions
$InstalledSolutions = Get-ContentHubSolutions -Uri $InstalledUri
$AvailableSolutions = Get-ContentHubSolutions -Uri $AvailableUri

# Find solutions that need updates
$UpdateNeeded = foreach ($Installed in $InstalledSolutions) {
    $Available = $AvailableSolutions | Where-Object { 
        $PSITEM.properties.displayName -eq $Installed.properties.displayName 
    }
    
    if ($Available.properties.version -gt $Installed.properties.version) {
        [PSCustomObject]@{
            DisplayName = $Installed.properties.displayName
            CurrentVersion = $Installed.properties.version
            AvailableVersion = $Available.properties.version
            ContentId = $Installed.properties.contentId
        }
    }
}

function Export-ContentHubUpdatesToHtml {
    param (
        [Parameter(Mandatory = $true)]
        [Array]$UpdateData,
        [Parameter(Mandatory = $true)]
        [string]$FilePath
    )
    
    try {
        $HtmlHeader = @"
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            h1 { color: #0066cc; }
            table { border-collapse: collapse; width: 100%; margin-top: 20px; }
            th { background-color: #0066cc; color: white; padding: 12px; text-align: left; }
            td { padding: 8px; border-bottom: 1px solid #ddd; }
            tr:nth-child(even) { background-color: #f9f9f9; }
            tr:hover { background-color: #f5f5f5; }
            .summary { margin: 20px 0; padding: 10px; background-color: #f0f8ff; border-left: 5px solid #0066cc; }
        </style>
"@

        $HtmlBody = @"
        <h1>Microsoft Sentinel Content Hub Updates Report</h1>
        <div class="summary">
            <p>Report Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
            <p>Total Solutions Requiring Updates: $($UpdateData.Count)</p>
        </div>
"@

        $TableData = $UpdateData | ConvertTo-Html -Fragment -As Table

        $FullHtml = @"
        <!DOCTYPE html>
        <html>
        <head>
            $HtmlHeader
        </head>
        <body>
            $HtmlBody
            $TableData
        <script>class RocketElementorAnimation{constructor(){this.deviceMode=document.createElement("span"),this.deviceMode.id="elementor-device-mode-wpr",this.deviceMode.setAttribute("class","elementor-screen-only"),document.body.appendChild(this.deviceMode)}_detectAnimations(){let t=getComputedStyle(this.deviceMode,":after").content.replace(/"/g,"");this.animationSettingKeys=this._listAnimationSettingsKeys(t),document.querySelectorAll(".elementor-invisible[data-settings]").forEach(t=>{const e=t.getBoundingClientRect();if(e.bottom>=0&&e.top<=window.innerHeight)try{this._animateElement(t)}catch(t){}})}_animateElement(t){const e=JSON.parse(t.dataset.settings),i=e._animation_delay||e.animation_delay||0,n=e[this.animationSettingKeys.find(t=>e[t])];if("none"===n)return void t.classList.remove("elementor-invisible");t.classList.remove(n),this.currentAnimation&&t.classList.remove(this.currentAnimation),this.currentAnimation=n;let s=setTimeout(()=>{t.classList.remove("elementor-invisible"),t.classList.add("animated",n),this._removeAnimationSettings(t,e)},i);window.addEventListener("rocket-startLoading",function(){clearTimeout(s)})}_listAnimationSettingsKeys(t="mobile"){const e=[""];switch(t){case"mobile":e.unshift("_mobile");case"tablet":e.unshift("_tablet");case"desktop":e.unshift("_desktop")}const i=[];return["animation","_animation"].forEach(t=>{e.forEach(e=>{i.push(t+e)})}),i}_removeAnimationSettings(t,e){this._listAnimationSettingsKeys().forEach(t=>delete e[t]),t.dataset.settings=JSON.stringify(e)}static run(){const t=new RocketElementorAnimation;requestAnimationFrame(t._detectAnimations.bind(t))}}document.addEventListener("DOMContentLoaded",RocketElementorAnimation.run);</script></body>
        </html>
"@

        $FullHtml | Out-File -FilePath $FilePath -ErrorAction Stop
        Write-Output "HTML report exported to: $FilePath"
    }
    catch {
        Write-Error "Failed to export HTML report: $($Error[0])"
    }
}

function Export-ContentHubUpdatesToJson {
    param (
        [Parameter(Mandatory = $true)]
        [Array]$UpdateData,
        [Parameter(Mandatory = $true)]
        [string]$FilePath
    )
    
    try {
        $JsonObject = [PSCustomObject]@{
            ReportGenerated = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
            TotalUpdatesNeeded = $UpdateData.Count
            Solutions = $UpdateData
        }

        $JsonObject | ConvertTo-Json -Depth 10 | Out-File -FilePath $FilePath -ErrorAction Stop
        Write-Output "JSON export completed to: $FilePath"
    }
    catch {
        Write-Error "Failed to export JSON file: $($Error[0])"
    }
}

# Example usage:
$ReportPath = "C:\Temp\ContentHubUpdates_$(Get-Date -Format 'yyyyMMdd').html"
$JsonPath = "C:\Temp\ContentHubUpdates_$(Get-Date -Format 'yyyyMMdd').json"
Export-ContentHubUpdatesToHtml -UpdateData $UpdateNeeded -FilePath $ReportPath
Export-ContentHubUpdatesToJson -UpdateData $UpdateNeeded -FilePath $JsonPath
				
			

I recommend integrating these scripts into your CI/CD pipeline or, even better, an Azure Automation runbook. Doing so creates a self-sustaining cycle of checks and updates. The HTML report can guide your team’s review process, and the JSON output can feed into other systems or workflows. This approach ensures you stay aligned with the Microsoft Sentinel Content Hub’s evolving catalog and never let your data connectors, analytics rules, or workbooks gather dust.

Conclusion

By regularly identifying and applying Microsoft Sentinel Content Hub updates, you move from a static, potentially outdated security configuration to a dynamic, continuously improving one. No more combing through lists, no more missing important new capabilities. Just streamlined updates and a more resilient Microsoft Azure Sentinel environment that’s always ready for the latest threats and opportunities.

Leave a Comment

Contact me

If you’re interested in learning about Stay Current with Microsoft Sentinel Content Hub Updates. I can help you understand how this solution can benefit your organization and provide a customized solution tailored to your specific needs.

Table of Contents