diff --git a/ARM-Templates/UiDefinition.json b/ARM-Templates/UiDefinition.json index 4d8141c..0dbe792 100644 --- a/ARM-Templates/UiDefinition.json +++ b/ARM-Templates/UiDefinition.json @@ -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" @@ -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" diff --git a/ARM-Templates/azuredeploy.json b/ARM-Templates/azuredeploy.json index 57c53b7..9acf3c9 100644 --- a/ARM-Templates/azuredeploy.json +++ b/ARM-Templates/azuredeploy.json @@ -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": { @@ -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", diff --git a/PowerShell/Update-DetectionRules/Enable-AlertRules.ps1 b/PowerShell/Update-DetectionRules/Enable-AlertRules.ps1 index 12b9785..1936ade 100644 --- a/PowerShell/Update-DetectionRules/Enable-AlertRules.ps1 +++ b/PowerShell/Update-DetectionRules/Enable-AlertRules.ps1 @@ -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()] @@ -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 @@ -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 @@ -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" @@ -182,7 +196,6 @@ function Enable-AlertRules { Write-Verbose $_ Write-Error "Unable to create alert rule with error code: $($_.Exception.Message)" -ErrorAction Stop } - break } } } diff --git a/PowerShell/Update-DetectionRules/Enable-AnalyticsRules.ps1 b/PowerShell/Update-DetectionRules/Enable-AnalyticsRules.ps1 new file mode 100644 index 0000000..2c0ccd2 --- /dev/null +++ b/PowerShell/Update-DetectionRules/Enable-AnalyticsRules.ps1 @@ -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 diff --git a/README.md b/README.md index 4bab76b..98516c3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/parsers/WindowsSecurityEvents/WindowsSecurityEvents.csl b/parsers/WindowsSecurityEvents/WindowsSecurityEvents.csl new file mode 100644 index 0000000..e2be0a5 --- /dev/null +++ b/parsers/WindowsSecurityEvents/WindowsSecurityEvents.csl @@ -0,0 +1,4 @@ +let timeframe = {time_range}; +SecurityEvent +| where TimeGenerated between (ago(timeframe) .. now()) +| project TimeGenerated, Computer, EventID, Activity, Account, IpAddress, ProcessName, CommandLine diff --git a/samples/FortiGate/FortiGate.json b/samples/FortiGate/FortiGate.json new file mode 100644 index 0000000..570b29a --- /dev/null +++ b/samples/FortiGate/FortiGate.json @@ -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" + } +] diff --git a/samples/FortiWeb/FortiWeb.json b/samples/FortiWeb/FortiWeb.json new file mode 100644 index 0000000..5796baf --- /dev/null +++ b/samples/FortiWeb/FortiWeb.json @@ -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" + } +] diff --git a/scripts/Sentinel/GLM_CAPTURE.json b/scripts/Sentinel/GLM_CAPTURE.json new file mode 100644 index 0000000..135baf2 --- /dev/null +++ b/scripts/Sentinel/GLM_CAPTURE.json @@ -0,0 +1,32 @@ +{ + "name": "GLM Capture Metadata", + "version": "1.0", + "description": "Metadata for GLM capture, encryption, and integration with Azure Key Vault and SOAR. This file contains no secrets.", + "encryption": { + "algorithm": "AES-256-GCM", + "kdf": "PBKDF2", + "pbkdf2_iterations": 100000, + "openssl_cmd": "openssl enc -aes-256-gcm -pbkdf2 -iter 100000 -salt -in -out <encrypted>", + "file_permissions": "600" + }, + "key_management": { + "preferred": "azure_keyvault", + "keyvault_format": "<vaultName>:<secretName>", + "notes": "Use managed identities or service principal with minimal access. Do not store secrets in repo." + }, + "soar": { + "integration": "generic_webhook", + "notes": "Provide SOAR webhook URL via runtime secure store or environment injected at runtime. Avoid embedding in files." + }, + "usage": [ + "1) Store workspace key in Azure Key Vault and grant VM/agent access via managed identity, OR", + "2) Encrypt workspace key locally with AES-256-GCM and store encrypted file with mode 600. Decrypt at runtime with passphrase.", + "3) Installer script reads key from Key Vault (preferred) or decrypts file when prompted." + ], + "safety": [ + "Do not commit plaintext secrets.", + "Pin external script URLs and verify checksums before running.", + "Test in an isolated environment before production." + ], + "timestamp": "2026-08-03T00:00:00Z" +}