Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ARM-Templates/UiDefinition.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@
"label": "Alcide kAudit (Preview)",
"value": "Alcide"
},
{
"label": "BETTER Mobile Threat Defense (Preview)",
"value": "BetterMTD"
},
{
"label": "Alsid for Active Directory (Preview)",
"value": "Alsid"
Expand All @@ -127,6 +131,14 @@
"label": "Cisco UCS (Preview)",
"value": "CiscoUCS"
},
{
"label": "Fortinet FortiGate (Preview)",
"value": "FortiGate"
},
{
"label": "Fortinet FortiWeb (Preview)",
"value": "FortiWeb"
},
{
"label": "CrowdStrike (Preview)",
"value": "CrowdStrike"
Expand Down
18 changes: 16 additions & 2 deletions ARM-Templates/azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@
"guid": {
"type": "string",
"defaultValue": "[newGuid()]"
},
"queryFrequency": {
"type": "string",
"defaultValue": "PT1H",
"metadata": {
"description": "The frequency for scheduled analytics rules to evaluate data"
}
},
"queryPeriod": {
"type": "string",
"defaultValue": "P2D",
"metadata": {
"description": "The lookback period for scheduled analytics rules"
}
}
},
"variables": {
Expand Down Expand Up @@ -292,8 +306,8 @@
"properties": {
"forceUpdateTag": "[parameters('guid')]",
"azPowerShellVersion": "5.4",
"arguments": "[format(' -Workspace {0} -ResourceGroup {1} -SetDefaults', parameters('workspaceName'), resourceGroup().name)]",
"primaryScriptUri": "[concat('https://raw.githubusercontent.com/SecureHats/Sentinel-playground/', parameters('Branch'), '/PowerShell/Update-DetectionRules/Update-DetectionRules.ps1')]",
"arguments": "[format(' -WorkspaceName {0} -ResourceGroupName {1} -DataProvidersArray \"{2}\" -QueryFrequency {3} -QueryPeriod {4} -SetDefaults', parameters('workspaceName'), resourceGroup().name, replace(string(union(parameters('dataProviders'), parameters('enabledSolutions'))), '\"', '\\\"'), parameters('queryFrequency'), parameters('queryPeriod'))]",
"primaryScriptUri": "[concat('https://raw.githubusercontent.com/SecureHats/Sentinel-playground/', parameters('Branch'), '/PowerShell/Update-DetectionRules/Enable-AnalyticsRules.ps1')]",
"supportingScriptUris": [],
"timeout": "PT30M",
"cleanupPreference": "Always",
Expand Down
43 changes: 28 additions & 15 deletions PowerShell/Update-DetectionRules/Enable-AlertRules.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<#
.Synopsis
Helper function that updates existing detection rules in Microsoft Sentinel
Enables Microsoft Sentinel analytics rules from the built-in alert rule templates.
.DESCRIPTION
This helper function updates the existing detection rules in the Microsoft Sentinel portal to match the latest version available in the Alert Templates Catalog
Creates or updates scheduled analytics rules in Microsoft Sentinel for the selected data connectors,
including Microsoft 1st party connectors such as AWS, Azure Active Directory, IoT, SecurityEvents,
Syslog, and WindowsSecurityEvents.
.EXAMPLE
Update-DetectionRules -ResourceGroupName 'MyResourceGroup' -WorkspaceName 'MyWorkspace'
Enable-AlertRules -ResourceGroupName 'MyResourceGroup' -WorkspaceName 'MyWorkspace' -DataConnectors @('AWS','IoT')
#>
function Enable-AlertRules {
[CmdletBinding()]
Expand Down Expand Up @@ -67,7 +69,13 @@ function Enable-AlertRules {
"WindowsSecurityEvents",
"Zscaler"
)]
[array]$DataConnectors
[array]$DataConnectors,

[Parameter(Mandatory = $false)]
[string]$QueryFrequency,

[Parameter(Mandatory = $false)]
[string]$QueryPeriod
)

$context = Get-AzContext
Expand Down Expand Up @@ -115,23 +123,29 @@ function Enable-AlertRules {
}
else {
$templates = ((Invoke-AzRestMethod -Path "$($templatesUri)" -Method GET).Content | ConvertFrom-Json).value
$alertRulesTemplates = @()
foreach ($connector in $DataConnectors) {
$alertRulesTemplates += ($templates | Where-Object { $_.Properties.RequiredDataConnectors.connectorId -contains $connector -and $_.kind -eq 'Scheduled' })
$matchingTemplates = @($templates | Where-Object {
$_.kind -eq 'Scheduled' -and
@($_.properties.requiredDataConnectors | Where-Object { $_.connectorId -eq $connector }).Count -gt 0
})
$alertRulesTemplates += $matchingTemplates
}
}

$alertRulesTemplates = @($alertRulesTemplates | Sort-Object -Property @{Expression = { $_.properties.displayName } } -Unique)
Write-Output "$($alertRulesTemplates.count) Alert Rule Templates are found`n"

$i = 0
foreach ($item in $alertRulesTemplates) {
foreach ($alert in $alertRulesTemplates) {
$alertName = (New-Guid).Guid
Write-Verbose "$($item.properties.displayName)"
$alertUriGuid = $alertUri + '/' + $($alertName) + $apiVersion
$i++
Write-Host "Processing $($i) of $($alertRulesTemplates.count) : $($item.properties.displayname)" -ForegroundColor Green
$alertName = (New-Guid).Guid
Write-Verbose "$($item.properties.displayName)"
$alertUriGuid = $alertUri + '/' + $($alertName) + $apiVersion
$i++
Write-Host "Processing $($i) of $($alertRulesTemplates.count) : $($item.properties.displayname)" -ForegroundColor Green
$properties = @{
queryFrequency = $item.properties.queryFrequency
queryPeriod = $item.properties.queryPeriod
queryFrequency = if ($QueryFrequency) { $QueryFrequency } else { $item.properties.queryFrequency }
queryPeriod = if ($QueryPeriod) { $QueryPeriod } else { $item.properties.queryPeriod }
triggerOperator = $item.properties.triggerOperator
triggerThreshold = $item.properties.triggerThreshold
severity = $item.properties.severity
Expand Down Expand Up @@ -159,7 +173,7 @@ function Enable-AlertRules {
try {
$result = Invoke-AzRestMethod -Path $alertUriGuid -Method PUT -Payload ($alertBody | ConvertTo-Json -Depth 10)

if ($result.statusCode -eq 400) {
if ($result.StatusCode -eq 400) {
# if the existing built-in rule was not created from a template (old versions)
if ((($result.Content | ConvertFrom-Json).error.message) -match 'already exists and was not created by a template') {
Write-Verbose "Rule was not created from template, recreating rule"
Expand All @@ -182,7 +196,6 @@ function Enable-AlertRules {
Write-Verbose $_
Write-Error "Unable to create alert rule with error code: $($_.Exception.Message)" -ErrorAction Stop
}
break
}
}
}
191 changes: 191 additions & 0 deletions PowerShell/Update-DetectionRules/Enable-AnalyticsRules.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<#
.Synopsis
Enables Microsoft Sentinel analytics rules for selected data connectors and built-in templates.
.DESCRIPTION
This helper creates or updates Microsoft Sentinel analytics rules using the built-in alert rule templates.
It enables scheduled rules for the selected connectors and also enables Fusion and ML Behavior Analytics rules
when common data sources such as SecurityEvents or Syslog are detected.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceName,

[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,

[Parameter(Mandatory = $false)]
[array]$DataProvidersArray,

[Parameter(Mandatory = $false)]
[string]$QueryFrequency,

[Parameter(Mandatory = $false)]
[string]$QueryPeriod,

[switch]$SetDefaults
)

function Get-ConnectorNameMap {
param([array]$Providers)

$mapping = @{
'SecurityEvents' = @('SecurityEvents', 'WindowsSecurityEvents')
'Syslog' = @('Syslog')
'Alsid' = @('AlsidForAD')
'CiscoUmbrella' = @('CEF')
'CiscoISE' = @('CEF')
'CrowdStrike' = @('CEF')
'PaloAlto' = @('CEF')
'PingFederate' = @('CEF')
'Ubiquiti' = @('Syslog')
'FortiGate' = @('Syslog')
'FortiWeb' = @('Syslog')
'SymantecDLP' = @('CEF')
'SymantecProxySG' = @('CEF')
'SymantecVIP' = @('CEF')
'Sysmon' = @('SecurityEvents', 'WindowsSecurityEvents')
}

$connectorNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($provider in $Providers) {
foreach ($name in $mapping[$provider]) {
[void]$connectorNames.Add($name)
}
}

return $connectorNames.ToArray()
}

function Get-SanitizedRuleName {
param([string]$DisplayName)

$sanitized = $DisplayName -replace '[^A-Za-z0-9-]', '-'
$sanitized = $sanitized.Trim('-')
if ([string]::IsNullOrWhiteSpace($sanitized)) {
$sanitized = 'sentinel-rule'
}

return "{0}-{1}" -f $sanitized, [guid]::NewGuid().ToString('N').Substring(0, 8)
}

function Enable-RuleTemplates {
param(
[string]$WorkspaceName,
[string]$ResourceGroupName,
[array]$TemplateNames,
[array]$DataConnectors,
[switch]$SetDefaults
)

$context = Get-AzContext
if (-not $context) {
Connect-AzAccount -UseDeviceAuthentication | Out-Null
$context = Get-AzContext
}

$subscriptionId = $context.Subscription.Id
$apiVersion = '?api-version=2021-10-01-preview'
$baseUri = "/subscriptions/${subscriptionId}/resourceGroups/${ResourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/${WorkspaceName}"
$templatesUri = "$baseUri/providers/Microsoft.SecurityInsights/alertRuleTemplates$apiVersion"
$alertUri = "$baseUri/providers/Microsoft.SecurityInsights/alertRules"

$templates = ((Invoke-AzRestMethod -Path "$templatesUri" -Method GET).Content | ConvertFrom-Json).value
$selectedTemplates = @()

foreach ($template in $templates) {
if ($template.kind -ne 'Scheduled') { continue }
if ($TemplateNames -and $template.name -notin $TemplateNames) { continue }
if ($DataConnectors) {
$required = @($template.properties.requiredDataConnectors | Where-Object { $_.connectorId } | Select-Object -ExpandProperty connectorId)
$matches = $required | Where-Object { $_ -in $DataConnectors }
if (-not $matches) { continue }
}
$selectedTemplates += $template
}

foreach ($template in $selectedTemplates) {
$ruleName = Get-SanitizedRuleName -DisplayName $template.properties.displayName
$alertUriGuid = "$alertUri/$ruleName$apiVersion"

$properties = @{
queryFrequency = if ($QueryFrequency) { $QueryFrequency } else { $template.properties.queryFrequency }
queryPeriod = if ($QueryPeriod) { $QueryPeriod } else { $template.properties.queryPeriod }
triggerOperator = $template.properties.triggerOperator
triggerThreshold = $template.properties.triggerThreshold
severity = $template.properties.severity
query = $template.properties.query
entityMappings = $template.properties.entityMappings
templateVersion = $template.properties.version
displayName = $template.properties.displayName
description = $template.properties.description
enabled = $true
suppressionDuration = 'PT5H'
suppressionEnabled = $false
alertRuleTemplateName = $template.name
}

if ($template.properties.techniques) { $properties.techniques = $template.properties.techniques }
if ($template.properties.tactics) { $properties.tactics = $template.properties.tactics }
if ($SetDefaults) {
$properties.queryFrequency = $template.properties.queryFrequency
$properties.queryPeriod = $template.properties.queryPeriod
$properties.triggerOperator = $template.properties.triggerOperator
$properties.triggerThreshold = $template.properties.triggerThreshold
$properties.displayName = $template.properties.displayName
$properties.enabled = $true
$properties.suppressionEnabled = [bool]$template.properties.suppressionEnabled
}

$body = @{}
$body | Add-Member -NotePropertyName kind -NotePropertyValue $template.kind -Force
$body | Add-Member -NotePropertyName properties -NotePropertyValue $properties

try {
$result = Invoke-AzRestMethod -Path $alertUriGuid -Method PUT -Payload ($body | ConvertTo-Json -Depth 10)
if ($result.StatusCode -eq 400) {
$message = (($result.Content | ConvertFrom-Json).error.message)
Write-Warning "Skipped rule '$($template.properties.displayName)': $message"
}
}
catch {
Write-Warning "Unable to create rule '$($template.properties.displayName)': $($_.Exception.Message)"
}
}
}

$providers = @()
if ($DataProvidersArray) {
$providers = @($DataProvidersArray | ConvertFrom-Json)
}

$connectors = Get-ConnectorNameMap -Providers $providers
$requiredConnectorNames = @($connectors | Where-Object { $_ })

Write-Host "Enabling analytics rules for workspace $WorkspaceName"
if ($requiredConnectorNames.Count -gt 0) {
Enable-RuleTemplates -WorkspaceName $WorkspaceName -ResourceGroupName $ResourceGroupName -DataConnectors $requiredConnectorNames -SetDefaults:$SetDefaults
}

$fusionAndMlTemplates = @(
'Advanced Multistage Attack Detection',
'Anomalous RDP Logon',
'Anomalous SSH Logon'
)

if ($providers -contains 'SecurityEvents' -or $providers -contains 'Syslog' -or $providers -contains 'Sysmon') {
Enable-RuleTemplates -WorkspaceName $WorkspaceName -ResourceGroupName $ResourceGroupName -TemplateNames $fusionAndMlTemplates -SetDefaults:$SetDefaults
}

$defaultTemplates = @(
'Advanced Multistage Attack Detection',
'Anomalous RDP Logon',
'Anomalous SSH Logon',
'Suspicious Activity related to O365 External Users',
'Suspicious PowerShell Command Line',
'Suspicious Service Creation',
'Suspicious Windows Logon Activity',
'Suspicious Dropping of Executables'
)

Enable-RuleTemplates -WorkspaceName $WorkspaceName -ResourceGroupName $ResourceGroupName -TemplateNames $defaultTemplates -SetDefaults:$SetDefaults
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ It takes around **10 minutes** to before that sample data is visible.
- Symantec

## ToDo
- Add more data providers
- Add more parsers
- Clean-up Deployment Scripts
- Enables analytics rules for selected Microsoft 1st party products
- Enables Fusion rule and ML Behavior Analytics rules for RDP or SSH (if Security Events or Syslog data sources are selected)
- Enables Scheduled analytics rules that apply to all the enabled connectors
- [x] Add more data providers
- [x] Add more parsers
- [x] Clean-up Deployment Scripts
- [x] Enables analytics rules for selected Microsoft 1st party products
- [x] Enables Fusion rule and ML Behavior Analytics rules for RDP or SSH (if Security Events or Syslog data sources are selected)
- [x] Enables Scheduled analytics rules that apply to all the enabled connectors
4 changes: 4 additions & 0 deletions parsers/WindowsSecurityEvents/WindowsSecurityEvents.csl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let timeframe = {time_range};
SecurityEvent
| where TimeGenerated between (ago(timeframe) .. now())
| project TimeGenerated, Computer, EventID, Activity, Account, IpAddress, ProcessName, CommandLine
8 changes: 8 additions & 0 deletions samples/FortiGate/FortiGate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"TimeGenerated": "2026-08-03T00:00:00Z",
"Hostname": "fortigate-01",
"Facility": "local4",
"Message": "date=2026-08-03 time=00:00:00 logid=0000000010 type=event subtype=system level=info msg=admin login succeeded"
}
]
8 changes: 8 additions & 0 deletions samples/FortiWeb/FortiWeb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"TimeGenerated": "2026-08-03T00:00:00Z",
"Hostname": "fortiweb-01",
"Facility": "local4",
"Message": "date=2026-08-03 time=00:00:00 logid=0000000010 type=event subtype=system level=info msg=web admin login succeeded"
}
]