AzSentinel.psm1
#requires -module @{ModuleName = 'Az.Accounts'; ModuleVersion = '1.5.2'} #requires -version 6.2 enum AggregationKind { SingleAlert AlertPerResult } enum CloseReason { TruePositive FalsePositive } enum DataSourceName { ApplicationInsights AzureActivityLog AzureAuditLog ChangeTrackingContentLocation ChangeTrackingCustomPath ChangeTrackingDataTypeConfiguration ChangeTrackingDefaultRegistry ChangeTrackingLinuxPath ChangeTrackingPath ChangeTrackingRegistry ChangeTrackingServices CustomLog CustomLogCollection DnsAnalytics GenericDataSource IISLogs ImportComputerGroup Itsm LinuxChangeTrackingPath LinuxPerformanceCollection LinuxPerformanceObject LinuxSyslog LinuxSyslogCollection NetworkMonitoring Office365 SecurityCenterSecurityWindowsBaselineConfiguration SecurityEventCollectionConfiguration SecurityInsightsSecurityEventCollectionConfiguration SecurityWindowsBaselineConfiguration SqlDataClassification WindowsEvent WindowsPerformanceCounter WindowsTelemetry } enum ExportType { Alert Hunting All Templates } enum GroupByEntities { Account Ip Host Url FileHash } enum Kind { Scheduled Fusion MLBehaviorAnalytics MicrosoftSecurityIncidentCreation } enum MatchingMethod { All None Custom } enum Severity { Medium High Low Informational } enum Status { New InProgress Closed } enum Tactics { InitialAccess Persistence Execution PrivilegeEscalation DefenseEvasion CredentialAccess LateralMovement Discovery Collection Exfiltration CommandAndControl Impact } enum TriggerOperator { GreaterThan LessThan Equal NotEqual gt lt eq ne } class MicrosoftSecurityIncidentCreation { [string] $DisplayName [string]$Description [bool]$Enabled [string]$ProductFilter [Severity[]]$SeveritiesFilter [string]$DisplayNamesFilter MicrosoftSecurityIncidentCreation ($DisplayName, $Description, $Enabled, $ProductFilter, $SeveritiesFilter, $DisplayNamesFilter) { $this.displayName = $DisplayName $this.description = $Description $this.enabled = $Enabled $this.productFilter = $ProductFilter $this.severitiesFilter = $SeveritiesFilter $this.displayNamesFilter = $DisplayNamesFilter } } class Fusion { [bool]$Enabled [string]$AlertRuleTemplateName Fusion ($Enabled, $AlertRuleTemplateName) { $this.enabled = $Enabled $this.AlertRuleTemplateName = $AlertRuleTemplateName } } class MLBehaviorAnalytics { [bool]$Enabled [string]$AlertRuleTemplateName MLBehaviorAnalytics ($Enabled, $AlertRuleTemplateName) { $this.enabled = $Enabled $this.AlertRuleTemplateName = $AlertRuleTemplateName } } class GroupingConfiguration { [bool]$enabled [bool]$reopenClosedIncident [string]$lookbackDuration [MatchingMethod]$entitiesMatchingMethod [GroupByEntities[]]$groupByEntities # Convert string to ISO_8601 format PdDThHmMsS static [string] TimeString([string]$value) { $value = $value.ToUpper() # Return values already in ISO 8601 format if ($value -match "PT.*|P.*D") { return $value } # Format day time periods if ($value -like "*D") { return "P$value" } # Format hour and minute time periods if ($value -match ".*[HM]") { return "PT$value" } return $value } GroupingConfiguration ($properties) { $this.enabled = $properties.enabled $this.reopenClosedIncident = $properties.reopenClosedIncident $this.lookbackDuration = $properties.lookbackDuration $this.entitiesMatchingMethod = $properties.entitiesMatchingMethod $this.groupByEntities = $properties.groupByEntities } GroupingConfiguration ($enabled, $reopenClosedIncident, $lookbackDuration, $entitiesMatchingMethod, $groupByEntities) { $this.enabled = if ($null -ne $enabled ) { $enabled } else { $false } $this.reopenClosedIncident = if ($null -ne $reopenClosedIncident) { $reopenClosedIncident } else { $false } $this.lookbackDuration = if ($lookbackDuration) { [groupingConfiguration]::TimeString($lookbackDuration) } else { "PT5H" } $this.entitiesMatchingMethod = if ($entitiesMatchingMethod) { $entitiesMatchingMethod } else { "All" } $this.groupByEntities = if ($groupByEntities) { $groupByEntities } else { @( "Account", "Ip", "Host", "Url", "FileHash" ) } } } class IncidentConfiguration { [bool] $CreateIncident [GroupingConfiguration]$GroupingConfiguration IncidentConfiguration ($CreateIncident, $GroupingConfiguration) { $this.createIncident = if ($null -ne $createIncident) { $createIncident } else { $true } $this.groupingConfiguration = $GroupingConfiguration } } class ScheduledAlertProp { [guid] $Name [string] $DisplayName [string] $Description [Severity] $Severity [bool] $Enabled [string] $Query [string] $QueryFrequency [string] $QueryPeriod [TriggerOperator]$TriggerOperator [Int] $TriggerThreshold [string] $SuppressionDuration [bool] $SuppressionEnabled [Tactics[]] $Tactics [string] $PlaybookName [IncidentConfiguration]$IncidentConfiguration $eventGroupingSettings [string] $AlertRuleTemplateName hidden [AggregationKind]$aggregationKind static [string] TriggerOperatorSwitch([string]$value) { switch ($value) { "gt" { $value = "GreaterThan" } "lt" { $value = "LessThan" } "eq" { $value = "Equal" } "ne" { $value = "NotEqual" } default { $value } } return $value } # Convert string to ISO_8601 format PdDThHmMsS static [string] TimeString([string]$value) { $value = $value.ToUpper() # Return values already in ISO 8601 format if ($value -match "PT.*|P.*D") { return $value } # Format day time periods if ($value -like "*D") { return "P$value" } # Format hour and minute time periods if ($value -match ".*[HM]") { return "PT$value" } return $value } ScheduledAlertProp (){ } ScheduledAlertProp ($Name, $DisplayName, $Description, $Severity, $Enabled, $Query, $QueryFrequency, ` $QueryPeriod, $TriggerOperator, $TriggerThreshold, $suppressionDuration, ` $suppressionEnabled, $Tactics, $PlaybookName, $IncidentConfiguration, $aggregationKind) { $this.name = $Name $this.DisplayName = $DisplayName $this.Description = $Description $this.Severity = $Severity $this.Enabled = $Enabled $this.Query = $Query $this.QueryFrequency = [ScheduledAlertProp]::TimeString($QueryFrequency) $this.QueryPeriod = [ScheduledAlertProp]::TimeString($QueryPeriod) $this.TriggerOperator = [ScheduledAlertProp]::TriggerOperatorSwitch($TriggerOperator) $this.TriggerThreshold = $TriggerThreshold $this.SuppressionDuration = if (($null -eq $suppressionDuration) -or ( $false -eq $suppressionEnabled)) { "PT1H" } else { if ( [ScheduledAlertProp]::TimeString($suppressionDuration) -ge [ScheduledAlertProp]::TimeString($QueryFrequency) ) { [ScheduledAlertProp]::TimeString($suppressionDuration) } else { Write-Error "Invalid Properties for Scheduled alert rule: 'suppressionDuration' should be greater than or equal to 'queryFrequency'" -ErrorAction Stop } } $this.SuppressionEnabled = if ($suppressionEnabled) { $suppressionEnabled } else { $false } $this.Tactics = $Tactics if ($PlaybookName) { $this.PlaybookName = if ($PlaybookName.Split('/').count -gt 1){ $PlaybookName.Split('/')[-1] } else { $PlaybookName } } $this.IncidentConfiguration = $IncidentConfiguration $this.eventGroupingSettings = @{ aggregationKind = if ($aggregationKind) { $aggregationKind } else { "SingleAlert" } } } ScheduledAlertProp ($Name, $DisplayName, $Description, $Severity, $Enabled, $Query, $QueryFrequency, ` $QueryPeriod, $TriggerOperator, $TriggerThreshold, $suppressionDuration, ` $suppressionEnabled, $Tactics, $PlaybookName, $IncidentConfiguration, ` $aggregationKind, $AlertRuleTemplateName) { $this.name = $Name $this.DisplayName = $DisplayName $this.Description = $Description $this.Severity = $Severity $this.Enabled = $Enabled $this.Query = $Query $this.QueryFrequency = [ScheduledAlertProp]::TimeString($QueryFrequency) $this.QueryPeriod = [ScheduledAlertProp]::TimeString($QueryPeriod) $this.TriggerOperator = [ScheduledAlertProp]::TriggerOperatorSwitch($TriggerOperator) $this.TriggerThreshold = $TriggerThreshold $this.SuppressionDuration = if (($null -eq $suppressionDuration) -or ( $false -eq $suppressionEnabled)) { "PT1H" } else { if ( [ScheduledAlertProp]::TimeString($suppressionDuration) -ge [ScheduledAlertProp]::TimeString($QueryFrequency) ) { [ScheduledAlertProp]::TimeString($suppressionDuration) } else { Write-Error "Invalid Properties for Scheduled alert rule: 'suppressionDuration' should be greater than or equal to 'queryFrequency'" -ErrorAction Stop } } $this.SuppressionEnabled = if ($suppressionEnabled) { $suppressionEnabled } else { $false } $this.Tactics = $Tactics if ($PlaybookName) { $this.PlaybookName = if ($PlaybookName.Split('/').count -gt 1){ $PlaybookName.Split('/')[-1] } else { $PlaybookName } } $this.IncidentConfiguration = $IncidentConfiguration $this.eventGroupingSettings = @{ aggregationKind = if ($aggregationKind) { $aggregationKind } else { "SingleAlert" } } $this.AlertRuleTemplateName = $AlertRuleTemplateName } } class AlertRule { [string] $Name [string] $Etag [string]$type [Kind]$kind = 'Scheduled' [pscustomobject]$Properties [string]$Id AlertRule ($Name, $Etag, $Properties, $Id, $kind) { $this.id = $Id $this.type = 'Microsoft.SecurityInsights/alertRules' $this.kind = $kind $this.Name = $Name $this.Etag = $Etag $this.Properties = $Properties } } function Compare-Policy { <# .SYNOPSIS Compare PS Objects .DESCRIPTION This function is used for comparison to see if a rule needs to be updated .PARAMETER ReferenceTemplate Reference template is the data of the AlertRule as active on Azure .PARAMETER DifferenceTemplate Difference template is data that is generated and will be uploaded to Azure .EXAMPLE Compare-Policy -ReferenceTemplate -DifferenceTemplate .NOTES NAME: Compare-Policy #> [CmdletBinding()] param ( # Reference value is the Online available [Parameter(Mandatory)] [psobject]$ReferenceTemplate, # Difference template is the template that will be uploaded [Parameter(Mandatory)] [psobject]$DifferenceTemplate ) process { $objprops = $ReferenceTemplate | Get-Member -MemberType Property, NoteProperty | ForEach-Object Name $objprops += $DifferenceTemplate | Get-Member -MemberType Property, NoteProperty | ForEach-Object Name $objprops = $objprops | Sort-Object -Unique | Select-Object $diffs = @() foreach ($objprop in $objprops) { $diff = Compare-Object $ReferenceTemplate $DifferenceTemplate -Property $objprop if ($diff) { $diffprops = @{ PropertyName = $objprop RefValue = ($diff | Where-Object { $_.SideIndicator -eq '<=' } | ForEach-Object $($objprop)) DiffValue = ($diff | Where-Object { $_.SideIndicator -eq '=>' } | ForEach-Object $($objprop)) } $diffs += New-Object PSObject -Property $diffprops } } if ($diffs) { return ($diffs | Select-Object PropertyName, RefValue, DiffValue) } } } function Get-AuthToken { <# .SYNOPSIS Get Authorization Token .DESCRIPTION This function is used to generate the Authtoken for API Calls .EXAMPLE Get-AuthToken #> [CmdletBinding()] param ( ) $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile Write-Verbose -Message "Using Subscription: $($azProfile.DefaultContext.Subscription.Name) from tenant $($azProfile.DefaultContext.Tenant.Id)" $script:subscriptionId = $azProfile.DefaultContext.Subscription.Id $script:tenantId = $azProfile.DefaultContext.Tenant.Id $profileClient = [Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient]::new($azProfile) $script:accessToken = $profileClient.AcquireAccessToken($script:tenantId) $script:authHeader = @{ 'Content-Type' = 'application/json' Authorization = 'Bearer ' + $script:accessToken.AccessToken } } function Get-AzSentinelPlayBook { <# .SYNOPSIS Get Logic App Playbook .DESCRIPTION This function is used for resolving the Logic App and testing the compability with Azure Sentinel .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER Name Enter the Logic App name .EXAMPLE Get-AzSentinelPlayBook -Name "" This example will get search for the Logic app within the current subscripbtio and test to see if it's compatible for Sentinel .NOTES NAME: Get-AzSentinelPlayBook #> param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$SubscriptionId, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Name ) begin { precheck } process { $triggerName = 'When_a_response_to_an_Azure_Sentinel_alert_is_triggered' if ($Name.Split('/').count -gt 1) { $uri = "https://management.azure.com/subscriptions/$($Name.Split('/')[2])/providers/Microsoft.Logic/workflows?api-version=2016-06-01" $Name = $Name.Split('/')[-1] } elseif ($SubscriptionId) { Write-Verbose "Getting LogicApp from Subscription $($subscriptionId)" $uri = "https://management.azure.com/subscriptions/$($subscriptionId)/providers/Microsoft.Logic/workflows?api-version=2016-06-01" } elseif ($script:subscriptionId) { Write-Verbose "Getting LogicApp from Subscription $($script:subscriptionId)" $uri = "https://management.azure.com/subscriptions/$($script:subscriptionId)/providers/Microsoft.Logic/workflows?api-version=2016-06-01" } else { $return = "No SubscriptionID provided" return $return } try { $logicappRaw = (Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader) $logicapp = $logicappRaw.value while ($logicappRaw.nextLink) { $logicappRaw = (Invoke-RestMethod -Uri $($logicappRaw.nextLink) -Headers $script:authHeader -Method Get) $logicapp += $logicappRaw.value } $playBook = $logicapp | Where-Object { $_.name -eq $Name } if ($playBook) { $uri1 = "https://management.azure.com$($playBook.id)/triggers/$($triggerName)/listCallbackUrl?api-version=2016-06-01" try { $playbookTrigger = (Invoke-RestMethod -Uri $uri1 -Method Post -Headers $script:authHeader) $playbookTrigger | Add-Member -NotePropertyName ResourceId -NotePropertyValue $playBook.id -Force return $playbookTrigger } catch { $return = "Playbook $($Name) doesn't start with 'When_a_response_to_an_Azure_Sentinel_alert_is_triggered' step! Error message: $($_.Exception.Message)" Write-Error $return } } else { Write-Warning "Unable to find LogicApp $Name under Subscription Id: $($script:subscriptionId)" } } catch { $return = $_.Exception.Message Write-Error $return } } } function Get-AzSentinelResourceProvider { <# .SYNOPSIS Get AzSentinelResourceProvider .DESCRIPTION This function is used to get status of the required resource providers .PARAMETER NameSpace Enter the name of the namespace without 'Microsoft.' .EXAMPLE Get-AzSentinelResourceProvider -NameSpace 'OperationsManagement' #> param ( [string]$NameSpace ) $uri = "https://management.azure.com/subscriptions/$($script:subscriptionId)/providers/Microsoft.$($NameSpace)?api-version=2019-10-01" try { $invokeReturn = Invoke-RestMethod -Method Get -Uri $uri -Headers $script:authHeader return $invokeReturn } catch { $return = $_.Exception.Message Write-Error $return return $return } } function Get-LogAnalyticWorkspace { <# .SYNOPSIS Get log analytic workspace .DESCRIPTION This function is used by other function for getting the workspace infiormation and seting the right values for $script:workspace and $script:baseUri .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER FullObject If you want to return the full object data .EXAMPLE Get-LogAnalyticWorkspace -WorkspaceName "" This example will get the Workspace and set workspace and baseuri param on Script scope level .EXAMPLE Get-LogAnalyticWorkspace -WorkspaceName "" -FullObject This example will get the Workspace ands return the full data object .EXAMPLE Get-LogAnalyticWorkspace -SubscriptionId "" -WorkspaceName "" This example will get the workspace info from another subscrion than your "Azcontext" subscription .NOTES NAME: Get-LogAnalyticWorkspace #> param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [Switch]$FullObject ) begin { precheck } process { if ($SubscriptionId) { Write-Verbose "Getting Worspace from Subscription $($subscriptionId)" $uri = "https://management.azure.com/subscriptions/$($subscriptionId)/providers/Microsoft.OperationalInsights/workspaces?api-version=2015-11-01-preview" } elseif ($script:subscriptionId) { Write-Verbose "Getting Worspace from Subscription $($script:subscriptionId)" $uri = "https://management.azure.com/subscriptions/$($script:subscriptionId)/providers/Microsoft.OperationalInsights/workspaces?api-version=2015-11-01-preview" } else { Write-Error "No SubscriptionID provided" -ErrorAction Stop } try { $workspaces = Invoke-webrequest -Uri $uri -Method get -Headers $script:authHeader -ErrorAction Stop $workspaceObject = ($workspaces.Content | ConvertFrom-Json).value | Where-Object { $_.name -eq $WorkspaceName } } catch { Write-Error $_.Exception.Message break } if ($workspaceObject) { $Script:workspace = ($workspaceObject.id).trim() $script:workspaceId = $workspaceObject.properties.customerId Write-Verbose "Workspace is: $($Script:workspace)" $script:baseUri = "https://management.azure.com$($Script:workspace)" if ($FullObject) { return $workspaceObject } Write-Verbose ($workspaceObject | Format-List | Format-Table | Out-String) Write-Verbose "Found Workspace $WorkspaceName in RG $($workspaceObject.id.Split('/')[4])" } else { Write-Error "Unable to find workspace $WorkspaceName under Subscription Id: $($script:subscriptionId)" } } } function precheck { <# .SYNOPSIS PreCheck .DESCRIPTION This function is used as a precheck step by all the functions to test all the required authentication and properties. .EXAMPLE precheck Run the test .NOTES NAME: precheck #> $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile if ($azProfile.Contexts.Count -ne 0) { if ($null -eq $script:accessToken ) { Get-AuthToken } elseif ($script:accessToken.ExpiresOn.DateTime - [datetime]::UtcNow.AddMinutes(-5) -le 0) { # if token expires within 5 minutes, request a new one Get-AuthToken } # Set the subscription from AzContext $script:subscriptionId = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile.DefaultContext.Subscription.Id } else { Write-Error 'No subscription available, Please use Connect-AzAccount to login and select the right subscription' break } } function Set-AzSentinelResourceProvider { <# .SYNOPSIS Set AzSentinelResourceProvider .DESCRIPTION This function is enables the required Resource providers .PARAMETER NameSpace Enter the name of the namespace without 'Microsoft.' .EXAMPLE Set-AzSentinelResourceProvider -NameSpace 'OperationsManagementOperationsManagement' #> [OutputType([String])] param ( [string]$NameSpace ) $uri = "https://management.azure.com/subscriptions/$($script:subscriptionId)/providers/Microsoft.$($NameSpace)/register?api-version=2019-10-01" try { $invokeReturn = Invoke-RestMethod -Method Post -Uri $uri -Headers $script:authHeader Write-Verbose $invokeReturn do { $resourceProviderStatus = Get-AzSentinelResourceProvider -NameSpace $NameSpace } until ($resourceProviderStatus.registrationState -eq 'Registered') $return = "Successfully enabled Microsoft.$($NameSpace) on subscription $($script:subscriptionId). Status:$($resourceProviderStatus.registrationState)" return $return } catch { $return = $_.Exception.Message Write-Error $return return $return } } function Add-AzSentinelIncidentComment { <# .SYNOPSIS Add Azure Sentinel Incident comment .DESCRIPTION With this function you can add comment to existing Azure Sentinel Incident. .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER Name Enter the name of the incidnet in GUID format .PARAMETER CaseNumber Enter the case number to get specfiek details of a open case .PARAMETER Comment Enter Comment tekst to add comment to the incident .EXAMPLE Add-AzSentinelIncidentComment -WorkspaceName "" CaseNumber "" -Comment Add a comment to existing incidnet #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [guid]$Name, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [int]$CaseNumber, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Comment ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } Write-Verbose -Message "Using URI: $($uri)" if ($CaseNumber) { $incident = Get-AzSentinelIncident @arguments -CaseNumber $CaseNumber -All } elseif ($Name) { $incident = Get-AzSentinelIncident @arguments -Name $Name } else { Write-Error "Both CaseNumber and Name are empty" -ErrorAction Stop } if ($incident) { $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/Cases/$($incident.name)/comments/$(New-Guid)?api-version=2019-01-01-preview" $body = @{ "properties" = @{ "message" = $Comment } } Write-Verbose "Found incident with case number: $($incident.caseNumber)" try { $return = Invoke-WebRequest -Uri $uri -Method Put -Body ($body | ConvertTo-Json -Depth 99 -EnumsAsStrings) -Headers $script:authHeader return ($return.Content | ConvertFrom-Json).properties } catch { $return = $_.Exception.Message Write-Verbose $_ Write-Error "Unable to update Incident $($incident.caseNumber) with error message $return" return $return } } } } function Rename-AzSentinelAlertRule { <# .SYNOPSIS Rename Azure Sentinel Alert Rule .DESCRIPTION With this function you can rename Azure Sentinel Alert rule .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER CurrentRuleName Enter the current name of the Alert rule .PARAMETER NewRuleName Enter the new name of the Alert rule .EXAMPLE Rename-AzSentinelAlertRule -WorkspaceName "" -CurrentRuleName "" -NewRuleName "" In this example you can rename the alert rule #> [cmdletbinding(SupportsShouldProcess)] [OutputType([String])] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $true, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$CurrentRuleName, [Parameter(Mandatory = $true, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$NewRuleName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { $rule = Get-AzSentinelAlertRule @arguments -RuleName $CurrentRuleName -ErrorAction Stop } catch { $return = $_.Exception.Message Write-Error $return } $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($rule.name)?api-version=2019-01-01-preview" $groupingConfiguration = [GroupingConfiguration]::new( $rule.incidentConfiguration.groupingConfiguration.GroupingConfigurationEnabled, $rule.incidentConfiguration.groupingConfiguration.ReopenClosedIncident, $rule.incidentConfiguration.groupingConfiguration.LookbackDuration, $rule.incidentConfiguration.groupingConfiguration.EntitiesMatchingMethod, $rule.incidentConfiguration.groupingConfiguration.GroupByEntities ) $incidentConfiguration = [IncidentConfiguration]::new( $rule.incidentConfiguration.CreateIncident, $groupingConfiguration ) $bodyAlertProp = [ScheduledAlertProp]::new( $rule.name, $NewRuleName, $rule.Description, $rule.Severity, $rule.Enabled, $rule.Query, $rule.QueryFrequency, $rule.QueryPeriod, $rule.TriggerOperator, $rule.TriggerThreshold, $rule.SuppressionDuration, $rule.SuppressionEnabled, $rule.Tactics, $rule.PlaybookName, $incidentConfiguration, $rule.AggregationKind ) $body = [AlertRule]::new( $rule.name, $rule.etag, $bodyAlertProp, $rule.Id, 'Scheduled') try { $result = Invoke-RestMethod -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) -ErrorAction Stop $return = "Successfully renamed rule $($CurrentRuleName) to $($NewRuleName) with status: $($result.StatusDescription)" return $return } catch { $return = $_.Exception.Message Write-Error "Rename failed with error $return" } } } function Remove-AzSentinelHuntingRule { <# .SYNOPSIS Remove Azure Sentinal Hunting Rules .DESCRIPTION With this function you can remove Azure Sentinal hunting rules from Powershell, if you don't provide andy Hunting rule name all rules will be removed .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the rule that you wnat to remove .EXAMPLE Remove-AzSentinelHuntingRule -WorkspaceName "" -RuleName "" In this example the defined hunting rule will be removed from Azure Sentinel .EXAMPLE Remove-AzSentinelHuntingRule -WorkspaceName "" -RuleName "","", "" In this example you can define multiple hunting rules that will be removed .EXAMPLE Remove-AzSentinelHuntingRule -WorkspaceName "" In this example no hunting rule is specified, all hunting rules will be removed one by one. For each rule you need to confirm the action #> [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$RuleName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } if ($RuleName) { # remove defined rules foreach ($rule in $RuleName) { try { $item = Get-AzSentinelHuntingRule @arguments -RuleName $rule -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } if ($item) { $uri = "$script:baseUri/savedSearches/$($item.name)?api-version=2017-04-26-preview" if ($PSCmdlet.ShouldProcess("Do you want to remove: $rule")) { Write-Output $item try { $result = Invoke-WebRequest -Uri $uri -Method DELETE -Headers $script:authHeader Write-Output "Successfully removed hunting rule: $($rule) with status: $($result.StatusDescription)" } catch { Write-Verbose $_ Write-Error "Unable to remove rule: $($rule) with error message: $($_.Exception.Message)" -ErrorAction Continue } } else { Write-Output "No change have been made for hunting rule: $rule" } } else { Write-Warning "Hunting rule $rule not found in $WorkspaceName" } } } else { Write-Warning "No hunting rule selected, All hunting rules will be removed one by one!" Get-AzSentinelHuntingRule @arguments -Filter "Hunting Queries" | ForEach-Object { $uri = "$script:baseUri/savedSearches/$($_.name)?api-version=2017-04-26-preview" if ($PSCmdlet.ShouldProcess("Do you want to remove: $($_.displayName)")) { try { $result = Invoke-WebRequest -Uri $uri -Method DELETE -Headers $script:authHeader Write-Output "Successfully removed hunting rule: $($_.displayName) with status: $($result.StatusDescription)" } catch { Write-Verbose $_ Write-Error "Unable to remove rule: $($_.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } else { Write-Output "No change have been made for hunting rule: $($_.displayName)" } } } } } function Remove-AzSentinelAlertRuleAction { <# .SYNOPSIS Remove Azure Sentinel Alert rule Action .DESCRIPTION This function can be used to see if an action is attached to the alert rule, if so then the configuration will be returned .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the Alert rule .PARAMETER RuleId Enter the Alert Rule ID that you want to configure .EXAMPLE Remove-AzSentinelAlertRuleAction -WorkspaceName "" -RuleName "AlertRule01" This example will get the Workspace ands return the full data object .NOTES NAME: Remove-AzSentinelAlertRuleAction #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkspaceName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RuleName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RuleId ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } if ($RuleName) { $result = Get-AzSentinelAlertRuleAction @arguments -RuleName $RuleName } elseif ($RuleId) { $result = Get-AzSentinelAlertRuleAction @arguments -RuleId $RuleId } else { Write-Error "No Alert Name or ID is provided" } if ($result) { $uri = "$($Script:baseUri)/providers/Microsoft.SecurityInsights/alertRules/$($result.id.split('asicustomalertsv3_')[-1])?api-version=2019-01-01-preview" Write-Verbose $uri if ($PSCmdlet.ShouldProcess("Do you want to remove Alert Rule action for rule: $($RuleName)")) { try { $return = Invoke-WebRequest -Uri $uri -Method DELETE -Headers $script:authHeader Write-Verbose $return Write-Verbose "Rule action $($result.properties.logicAppResourceId.Split('/')[-1]) removed for rule $($RuleName) with status: $($return.StatusCode)" return $return.StatusCode } catch { Write-Verbose $_ return $_.Exception.Message } } } else { Write-Output "No Alert Action found for Rule: $($RuleName)" } } } function Remove-AzSentinelAlertRule { <# .SYNOPSIS Remove Azure Sentinal Alert Rules .DESCRIPTION With this function you can remove Azure Sentinal Alert rules from Powershell, if you don't provide andy Rule name all rules will be removed .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the rule that you wnat to remove .EXAMPLE Remove-AzSentinelAlertRule -WorkspaceName "" -RuleName "" In this example the defined rule will be removed from Azure Sentinel .EXAMPLE Remove-AzSentinelAlertRule -WorkspaceName "" -RuleName "","", "" In this example you can define multiple rules that will be removed .EXAMPLE Remove-AzSentinelAlertRule -WorkspaceName "" In this example no rule is specified, all rules will be removed one by one. For each rule you need to confirm the action #> [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$RuleName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } if ($RuleName) { # remove defined rules foreach ($rule in $RuleName) { try { $item = Get-AzSentinelAlertRule @arguments -RuleName $rule -WarningAction SilentlyContinue -ErrorAction Stop } catch { $return = $_.Exception.Message Write-Error $return } if ($item) { $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($item.name)?api-version=2019-01-01-preview" if ($PSCmdlet.ShouldProcess("Do you want to remove: $rule")) { Write-Output $item try { $result = Invoke-WebRequest -Uri $uri -Method DELETE -Headers $script:authHeader Write-Output "Successfully removed rule: $($rule) with status: $($result.StatusDescription)" } catch { Write-Verbose $_ Write-Error "Unable to remove rule: $($rule) with error message: $($_.Exception.Message)" -ErrorAction Continue } } else { Write-Output "No change have been made for rule: $rule" } } else { Write-Warning "$rule not found in $WorkspaceName" } } } else { Write-Warning "No Rule selected, All rules will be removed one by one!" Get-AzSentinelAlertRule @arguments | ForEach-Object { $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($_.name)?api-version=2019-01-01-preview" if ($PSCmdlet.ShouldProcess("Do you want to remove: $($_.displayName)")) { try { $result = Invoke-WebRequest -Uri $uri -Method DELETE -Headers $script:authHeader Write-Output "Successfully removed rule: $($_.displayName) with status: $($result.StatusDescription)" } catch { Write-Verbose $_ Write-Error "Unable to remove rule: $($_.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } else { Write-Output "No change have been made for rule: $($_.displayName)" } } } } } function New-AzSentinelHuntingRule { <# .SYNOPSIS Create Azure Sentinal Hunting Rule .DESCRIPTION Use this function to creates Azure Sentinal Hunting rule .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER DisplayName Enter the Display name for the hunting rule .PARAMETER Description Enter the Description for the hunting rule .PARAMETER Tactics Enter the Tactics, valid values: "InitialAccess", "Persistence", "Execution", "PrivilegeEscalation", "DefenseEvasion", "CredentialAccess", "LateralMovement", "Discovery", "Collection", "Exfiltration", "CommandAndControl", "Impact" .PARAMETER Query Enter the querry in KQL format .EXAMPLE New-AzSentinelHuntingRule -WorkspaceName "" -DisplayName "" -Description "" -Tactics "","" -Query '' In this example you create a new hunting rule by defining the rule properties from CMDLET #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkspaceName, [Parameter(Mandatory)] [string] $DisplayName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Query, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Description, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Tactics[]] $Tactics ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } $item = @{ } $content = $null $body = @{ } $compareResult1 = $null $compareResult2 = $null $compareResult = $null Write-Verbose -Message "Creating new Hunting rule: $($DisplayName)" try { Write-Verbose -Message "Get hunting rule $DisplayName" $content = Get-AzSentinelHuntingRule @arguments -RuleName $DisplayName -WarningAction SilentlyContinue if ($content) { Write-Verbose -Message "Hunting rule $($DisplayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.eTag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/savedSearches/$($content.name)?api-version=2017-04-26-preview" } else { Write-Verbose -Message "Hunting rule $($DisplayName) doesn't exists in Azure Sentinel" $guid = (New-Guid).Guid $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/savedSearches/$guid" -Force $uri = "$script:baseUri/savedSearches/$($guid)?api-version=2017-04-26-preview" } } catch { Write-Verbose $_ Write-Error "Unable to connect to APi to get Analytic rules with message: $($_.Exception.Message)" -ErrorAction Stop } [PSCustomObject]$body = @{ "name" = $item.name "eTag" = $item.etag "id" = $item.id "properties" = @{ 'Category' = 'Hunting Queries' 'DisplayName' = $DisplayName 'Query' = $Query [pscustomobject]'Tags' = @( @{ 'Name' = "description" 'Value' = $Description }, @{ "Name" = "tactics" "Value" = $Tactics -join ',' }, @{ "Name" = "createdBy" "Value" = "" }, @{ "Name" = "createdTimeUtc" "Value" = "" } ) } } #return $content if ($content) { $compareResult1 = Compare-Policy -ReferenceTemplate ($content | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id, Tags, Version) -DifferenceTemplate ($body.Properties | Select-Object * -ExcludeProperty name, Tags, Version) $compareResult2 = Compare-Policy -ReferenceTemplate ($content.Tags | Where-Object { $_.name -eq "tactics" }) -DifferenceTemplate ($body.Properties.Tags | Where-Object { $_.name -eq "tactics" }) $compareResult = [PSCustomObject]$compareResult1 + [PSCustomObject]$compareResult2 if ($compareResult) { Write-Output "Found Differences for hunting rule: $($DisplayName)" Write-Output ($compareResult | Format-Table | Out-String) if ($PSCmdlet.ShouldProcess("Do you want to update hunting rule: $($DisplayName)")) { try { Write-Output ($body.properties | Format-Table) $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) Write-Output "Successfully updated hunting rule: $($DisplayName) with status: $($result.StatusDescription)" } catch { Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($_.Exception.Message)" -ErrorAction Stop } } else { Write-Output "No change have been made for rule $($DisplayName), deployment aborted" } } else { Write-Output "Hunting rule $($DisplayName) is compliance, nothing to do" Write-Output ($body.properties | Format-Table) } } else { Write-Verbose "Creating new hunting rule: $($DisplayName)" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) Write-Output "Successfully created hunting rule: $($DisplayName) with status: $($result.StatusDescription)" Write-Output ($body.properties | Format-Table) } catch { Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($_.Exception.Message)" -ErrorAction Stop } } } } function New-AzSentinelAlertRuleAction { <# .SYNOPSIS Create Azure Sentinal Alert Rule Action .DESCRIPTION Use this function to creates Azure Sentinal Alert rule action .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER PlayBookName Enter the Playbook name that you want to assign to the alert rule .PARAMETER RuleName Enter the Alert Rule name that you want to configure .PARAMETER RuleId Enter the Alert Rule ID that you want to configure .EXAMPLE New-AzSentinelAlertRuleAction -WorkspaceName "" -PlayBookName "Playbook01" -RuleName "AlertRule01" New-AzSentinelAlertRuleAction -WorkspaceName "" -PlayBookName "Playbook01" -RuleId 'b6103d42-d2fb-4f35-xxx-c76a7f31ee4e' In this example you you assign the playbook to the Alert rule #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string]$SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$PlayBookName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RuleName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RuleId ) process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } if ($RuleName) { $alertId = (Get-AzSentinelAlertRule @arguments -RuleName $RuleName -ErrorAction SilentlyContinue).name } elseif ($RuleId) { $alertId = $RuleId } else { Write-Error "No Alert Name or ID is provided" } $action = $null if ($SubscriptionId) { $playBook = Get-AzSentinelPlayBook -SubscriptionId $SubscriptionId -Name $PlayBookName } else { $playBook = Get-AzSentinelPlayBook -Name $PlayBookName } $action = Get-AzSentinelAlertRuleAction @arguments -RuleId $alertId -ErrorAction SilentlyContinue if (($null -eq $action) -or ((($action.properties.logicAppResourceId).Split('/')[-1]) -ne $PlayBookName.Split('/')[-1])) { $guid = New-Guid $body = @{ "id" = "$($Script:baseUri)/providers/Microsoft.SecurityInsights/alertRules/$($alertId)/actions/$guid" "name" = $guid "type" = "Microsoft.SecurityInsights/alertRules/actions" "properties" = @{ "ruleId" = $alertId "triggerUri" = $playBook.value "logicAppResourceId" = $playBook.ResourceId } } $uri = "$($Script:baseUri)/providers/Microsoft.SecurityInsights/alertRules/$($alertId)/actions/$($guid)?api-version=2019-01-01-preview" try { $return = Invoke-WebRequest -Method Put -Uri $uri -Headers $Script:authHeader -Body ($body | ConvertTo-Json -Depth 10) Write-Verbose "Successfully created Action for Rule: $($RuleName) with Playbook $($PlayBookName) Status: $($return.StatusDescription)" return $return.StatusDescription } catch { Write-Error "Unable to create Action for Rule: $($RuleName) with Playbook $($PlayBookName) Error: $($_.Exception.Message)" Write-Verbose $_ return $_.Exception.Message } } elseif ((($action.properties.logicAppResourceId).Split('/')[-1]) -eq $PlayBookName) { Write-Output "Alert Rule: $($alertId) is already assigned to playbook: $(($action.properties.logicAppResourceId).Split('/')[-1])" } else { #nothing? } } } function New-AzSentinelAlertRule { <# .SYNOPSIS Create Azure Sentinal Alert Rules .DESCRIPTION Use this function creates Azure Sentinal Alert rules from provided CMDLET .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER Kind The alert rule kind .PARAMETER DisplayName The display name for alerts created by this alert rule. .PARAMETER Description The description of the alert rule. .PARAMETER Severity Enter the Severity, valid values: Medium", "High", "Low", "Informational" .PARAMETER Enabled Determines whether this alert rule is enabled or disabled. .PARAMETER Query The query that creates alerts for this rule. .PARAMETER QueryFrequency Enter the query frequency, example: 5H, 5M, 5D (H stands for Hour, M stands for Minute and D stands for Day) .PARAMETER QueryPeriod Enter the query period, exmaple: 5H, 5M, 5D (H stands for Hour, M stands for Minute and D stands for Day) .PARAMETER TriggerOperator Select the triggert Operator, valid values are: "GreaterThan", "FewerThan", "EqualTo", "NotEqualTo" .PARAMETER TriggerThreshold Enter the trigger treshold .PARAMETER SuppressionDuration Enter the suppression duration, example: 5H, 5M, 5D (H stands for Hour, M stands for Minute and D stands for Day) .PARAMETER SuppressionEnabled Set $true to enable Suppression or $false to disable Suppression .PARAMETER Tactics Enter the Tactics, valid values: "InitialAccess", "Persistence", "Execution", "PrivilegeEscalation", "DefenseEvasion", "CredentialAccess", "LateralMovement", "Discovery", "Collection", "Exfiltration", "CommandAndControl", "Impact" .PARAMETER PlaybookName Enter the Logic App name that you want to configure as playbook trigger .PARAMETER CreateIncident Create incidents from alerts triggered by this analytics rule .PARAMETER GroupingConfigurationEnabled Group related alerts, triggered by this analytics rule, into incidents .PARAMETER ReopenClosedIncident Re-open closed matching incidents .PARAMETER LookbackDuration Limit the group to alerts created within the selected time frame .PARAMETER EntitiesMatchingMethod Group alerts triggered by this analytics rule into a single incident by .PARAMETER GroupByEntities Grouping alerts into a single incident if the selected entities match: .PARAMETER AggregationKind Configure how rule query results are grouped into alerts .PARAMETER AlertRuleTemplateName The Name of the alert rule template used to create this rule .PARAMETER ProductFilter The alerts' productName on which the cases will be generated .PARAMETER SeveritiesFilter The alerts' severities on which the cases will be generated .PARAMETER DisplayNamesFilter The alerts' displayNames on which the cases will be generated .EXAMPLE New-AzSentinelAlertRule -WorkspaceName "" -DisplayName "" -Description "" -Severity -Enabled $true -Query '' -QueryFrequency "" -QueryPeriod "" -TriggerOperator -TriggerThreshold -SuppressionDuration "" -SuppressionEnabled $false -Tactics @("","") -PlaybookName "" Example on how to create a scheduled rule .EXAMPLE New-AzSentinelAlertRule -WorkspaceName "" -Kind Fusion -DisplayName "Advanced Multistage Attack Detection" -Enabled $true -AlertRuleTemplateName "f71aba3d-28fb-450b-b192-4e76a83015c8" Example on how to create a Fusion rule .EXAMPLE New-AzSentinelAlertRule -WorkspaceName "" -Kind MLBehaviorAnalytics -DisplayName "(Preview) Anomalous SSH Login Detection" -Enabled $true -AlertRuleTemplateName "fa118b98-de46-4e94-87f9-8e6d5060b60b" Example on how to create a MLBehaviorAnalytics rule .EXAMPLE New-AzSentinelAlertRule -WorkspaceName "" -Kind MicrosoftSecurityIncidentCreation -DisplayName "" -Description "" -Enabled $true -ProductFilter "" -SeveritiesFilter "","" -DisplayNamesFilter "" Example on how to create a MicrosoftSecurityIncidentCreation rule #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string]$SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false)] [Kind]$Kind = 'Scheduled', [Parameter(Mandatory = $false)] [string]$DisplayName, [Parameter(Mandatory = $false)] [string]$Description, [Parameter(Mandatory = $false)] [Severity]$Severity, [Parameter(Mandatory = $false)] [bool]$Enabled, [Parameter(Mandatory = $false)] [string]$Query, [Parameter(Mandatory = $false)] [string]$QueryFrequency, [parameter(Mandatory = $false)] [string]$QueryPeriod, [Parameter(Mandatory = $false)] [TriggerOperator]$TriggerOperator, [Parameter(Mandatory = $false)] [Int]$TriggerThreshold, [Parameter(Mandatory = $false)] [string]$SuppressionDuration, [Parameter(Mandatory = $false)] [bool]$SuppressionEnabled, [Parameter(Mandatory = $false)] #[Tactics[]] $Tactics, [string[]]$Tactics, [Parameter(Mandatory = $false)] [string[]]$PlaybookName = '', [Parameter(Mandatory = $false)] [bool]$CreateIncident, [Parameter(Mandatory = $false)] [bool]$GroupingConfigurationEnabled, [Parameter(Mandatory = $false)] [bool]$ReopenClosedIncident, [Parameter(Mandatory = $false)] [string]$LookbackDuration, [Parameter(Mandatory = $false)] [MatchingMethod]$EntitiesMatchingMethod, [Parameter(Mandatory = $false)] #[groupByEntities[]]$GroupByEntities, [string[]]$GroupByEntities, [Parameter(Mandatory = $false)] [AggregationKind]$AggregationKind, #Fusion & MLBehaviorAnalytics & Scheduled [Parameter(Mandatory = $false)] [string]$AlertRuleTemplateName, #MicrosoftSecurityIncidentCreation [Parameter(Mandatory = $false)] [string]$ProductFilter, [Parameter(Mandatory = $false)] [Severity[]]$SeveritiesFilter, [Parameter(Mandatory = $false)] [string]$DisplayNamesFilter ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } $item = @{ } Write-Verbose -Message "Creating new rule: $($DisplayName)" try { $content = Get-AzSentinelAlertRule @arguments -RuleName $DisplayName -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } if ($content) { Write-Verbose -Message "Rule $($DisplayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.eTag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($content.name)?api-version=2019-01-01-preview" } else { Write-Verbose -Message "Rule $($DisplayName) doesn't exists in Azure Sentinel" $guid = (New-Guid).Guid $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/providers/Microsoft.SecurityInsights/alertRules/$guid" -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($guid)?api-version=2019-01-01-preview" } if ($Kind -eq 'Scheduled') { try { $groupingConfiguration = [GroupingConfiguration]::new( $GroupingConfigurationEnabled, $ReopenClosedIncident, $LookbackDuration, $EntitiesMatchingMethod, $GroupByEntities ) $incidentConfiguration = [IncidentConfiguration]::new( $CreateIncident, $groupingConfiguration ) if (($AlertRuleTemplateName -and ! $content) -or $content.AlertRuleTemplateName) { if ($content.AlertRuleTemplateName){ <# If alertRule is already created with a TemplateName then Always use template name from existing rule. You can't attach existing scheduled rule to another templatename or remove the link to the template #> $AlertRuleTemplateName = $content.AlertRuleTemplateName } $bodyAlertProp = [ScheduledAlertProp]::new( $item.name, $DisplayName, $Description, $Severity, $Enabled, $Query, $QueryFrequency, $QueryPeriod, $TriggerOperator, $TriggerThreshold, $SuppressionDuration, $SuppressionEnabled, $Tactics, $PlaybookName, $incidentConfiguration, $AggregationKind, $AlertRuleTemplateName ) } else { $bodyAlertProp = [ScheduledAlertProp]::new( $item.name, $DisplayName, $Description, $Severity, $Enabled, $Query, $QueryFrequency, $QueryPeriod, $TriggerOperator, $TriggerThreshold, $SuppressionDuration, $SuppressionEnabled, $Tactics, $PlaybookName, $incidentConfiguration, $AggregationKind ) } $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'Scheduled') } catch { Write-Error "Unable to initiate class with error: $($_.Exception.Message)" -ErrorAction Stop } if ($content) { if ($PlaybookName -or $content.playbookName) { $compareResult = Compare-Policy -ReferenceTemplate ($content | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id) -DifferenceTemplate ($body.Properties | Select-Object * -ExcludeProperty name) } else { $compareResult = Compare-Policy -ReferenceTemplate ($content | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id, PlaybookName) -DifferenceTemplate ($body.Properties | Select-Object * -ExcludeProperty name, PlaybookName) } try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) if (($compareResult | Where-Object PropertyName -eq "playbookName").DiffValue) { foreach ($playbook in ($body.Properties.PlaybookName)) { $PlaybookResult = New-AzSentinelAlertRuleAction @arguments -PlayBookName $playbook -RuleId $($body.Name) -confirm:$false $body.Properties | Add-Member -NotePropertyName PlaybookStatus -NotePropertyValue $PlaybookResult -Force } } elseif (($compareResult | Where-Object PropertyName -eq "playbookName").RefValue) { $PlaybookResult = Remove-AzSentinelAlertRuleAction @arguments -RuleId $($body.Name) -Confirm:$false $body.Properties | Add-Member -NotePropertyName PlaybookStatus -NotePropertyValue $PlaybookResult -Force } else { #nothing } $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties return $return } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties Write-Verbose $_ Write-Error "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue return $return } } else { Write-Verbose "Creating new rule: $($DisplayName)" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) if (($body.Properties.PlaybookName)) { foreach ($playbook in ($body.Properties.PlaybookName)) { New-AzSentinelAlertRuleAction @arguments -PlayBookName $playbook -RuleId $($body.Name) -confirm:$false $body.Properties | Add-Member -NotePropertyName PlaybookStatus -NotePropertyValue $PlaybookResult -Force } } $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties return $return } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties return $return Write-Verbose $_ Write-Error "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } } if ($Kind -eq 'Fusion') { $bodyAlertProp = [Fusion]::new( $Enabled, $AlertRuleTemplateName ) $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'Fusion') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Fusion" -Force $return += $body.Properties return $return } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Fusion" -Force $return += $body.Properties return $return Write-Verbose $_ Write-Verbose "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } if ($Kind -eq 'MLBehaviorAnalytics') { $bodyAlertProp = [MLBehaviorAnalytics]::new( $Enabled, $AlertRuleTemplateName ) $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'MLBehaviorAnalytics') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MLBehaviorAnalytics" -Force $return += $body.Properties return $return } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MLBehaviorAnalytics" -Force $return += $body.Properties return $return Write-Verbose $_ Write-Verbose "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } if ($Kind -eq 'MicrosoftSecurityIncidentCreation') { $bodyAlertProp = [MicrosoftSecurityIncidentCreation]::new( $DisplayName, $Description, $Enabled, $ProductFilter, $SeveritiesFilter, $DisplayNamesFilter ) $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'MicrosoftSecurityIncidentCreation') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MicrosoftSecurityIncidentCreation" -Force $return += $body.Properties return $return } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MicrosoftSecurityIncidentCreation" -Force $return += $body.Properties return $return Write-Verbose $_ Write-Verbose "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } } } function Import-AzSentinelHuntingRule { <# .SYNOPSIS Import Azure Sentinal Hunting rule .DESCRIPTION This function imports Azure Sentinal Hunnting rules from JSON and YAML config files. This way you can manage your Hunting rules dynamic from JSON or multiple YAML files .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER SettingsFile Path to the JSON or YAML file for the Hunting rules .EXAMPLE Import-AzSentinelHuntingRule -WorkspaceName "infr-weu-oms-t-7qodryzoj6agu" -SettingsFile ".\examples\HuntingRules.json" In this example all the rules configured in the JSON file will be created or updated .EXAMPLE Import-AzSentinelHuntingRule -WorkspaceName "" -SettingsFile ".\examples\HuntingRules.yaml" In this example all the rules configured in the YAML file will be created or updated .EXAMPLE Get-Item .\examples\HuntingRules*.json | Import-AzSentinelHuntingRule -WorkspaceName "" In this example you can select multiple JSON files and Pipeline it to the SettingsFile parameter #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkspaceName, [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript( { (Test-Path -Path $_) -and ($_.Extension -in '.json', '.yaml', '.yml') })] [System.IO.FileInfo] $SettingsFile ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } $item = @{ } if ($SettingsFile.Extension -eq '.json') { try { $content = (Get-Content $SettingsFile -Raw | ConvertFrom-Json -ErrorAction Stop) if ($content.analytics){ $hunting = $content.analytics } else { $hunting = $content.Hunting } Write-Verbose -Message "Found $($hunting.count) rules" } catch { Write-Verbose $_ Write-Error -Message 'Unable to convert JSON file' -ErrorAction Stop } } elseif ($SettingsFile.Extension -in '.yaml', 'yml') { try { $hunting = [pscustomobject](Get-Content $SettingsFile -Raw | ConvertFrom-Yaml -ErrorAction Stop) $hunting | Add-Member -MemberType NoteProperty -Name DisplayName -Value $hunting.name Write-Verbose -Message 'Found compatibel yaml file' } catch { Write-Verbose $_ Write-Error -Message 'Unable to convert yaml file' -ErrorAction Stop } } try { $allRulesContent = Get-AzSentinelHuntingRule @arguments -RuleName $($hunting.displayName) -WarningAction SilentlyContinue -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } foreach ($item in $hunting) { Write-Output "Started with Hunting rule: $($item.displayName)" try { Write-Verbose -Message "Get rule $($item.description)" $content = $allRulesContent | Where-Object displayName -eq $item.displayName if ($content) { Write-Verbose -Message "Hunting rule $($item.displayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.etag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/savedSearches/$($content.name)?api-version=2017-04-26-preview" } else { Write-Verbose -Message "Hunting rule $($item.displayName) doesn't exists in Azure Sentinel" $guid = (New-Guid).Guid $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/savedSearches/$guid" -Force $uri = "$script:baseUri/savedSearches/$($guid)?api-version=2017-04-26-preview" } } catch { Write-Verbose $_ Write-Error "Unable to connect to APi to get Analytic rules with message: $($_.Exception.Message)" -ErrorAction Stop } [PSCustomObject]$body = @{ "name" = $item.name "eTag" = $item.etag "id" = $item.id "properties" = @{ 'Category' = 'Hunting Queries' 'DisplayName' = [string]$item.displayName 'Query' = [string]$item.query [pscustomobject]'Tags' = @( @{ 'Name' = "description" 'Value' = [string]$item.description }, @{ "Name" = "tactics" "Value" = [Tactics[]] $item.tactics -join ',' }, @{ "Name" = "createdBy" "Value" = "" }, @{ "Name" = "createdTimeUtc" "Value" = "" } ) } } if ($content) { $compareResult1 = Compare-Policy -ReferenceTemplate ($content | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id, Tags, Version) -DifferenceTemplate ($body.Properties | Select-Object * -ExcludeProperty name, Tags, Version) $compareResult2 = Compare-Policy -ReferenceTemplate ($content.Tags | Where-Object { $_.name -eq "tactics" }) -DifferenceTemplate ($body.Properties.Tags | Where-Object { $_.name -eq "tactics" }) $compareResult = [PSCustomObject]$compareResult1 + [PSCustomObject]$compareResult2 if ($compareResult) { Write-Output "Found Differences for hunting rule: $($item.displayName)" Write-Output ($compareResult | Format-Table | Out-String) if ($PSCmdlet.ShouldProcess("Do you want to update hunting rule: $($body.Properties.DisplayName)")) { try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) Write-Output "Successfully updated hunting rule: $($item.displayName) with status: $($result.StatusDescription)" Write-Output ($body.Properties | Format-List | Format-Table | Out-String) } catch { Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($_.Exception.Message)" -ErrorAction Continue } } else { Write-Output "No change have been made for hunting rule $($item.displayName), deployment aborted" } } else { Write-Output "Hunting rule $($item.displayName) is compliance, nothing to do" Write-Output ($body.Properties | Format-List | Format-Table | Out-String) } } else { Write-Verbose "Creating new rule: $($item.displayName)" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) Write-Output "Successfully created hunting rule: $($item.displayName) with status: $($result.StatusDescription)" Write-Output ($body.Properties | Format-List | Format-Table | Out-String) } catch { Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($_.Exception.Message)" -ErrorAction Continue } } } } } function Import-AzSentinelDataConnector { <# .SYNOPSIS Import Azure Sentinel Data Connectors .DESCRIPTION This function imports Azure Sentinel Data Connectors .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER SettingsFile Path to the JSON file for the Data Connectors .EXAMPLE Import-AzSentinelDataConnector -WorkspaceName "" -SettingsFile ".\examples\DataConnectors.json" In this example all the Data Conenctors configured in the JSON file will be created or updated #> param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkspaceName, [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript( { (Test-Path -Path $_) -and ($_.Extension -in '.json') })] [System.IO.FileInfo] $SettingsFile ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $script:SubscriptionId } } } try { Get-LogAnalyticWorkspace @arguments -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } if ($SettingsFile.Extension -eq '.json') { try { $connectors = Get-Content -Raw $SettingsFile | ConvertFrom-Json -Depth 99 } catch { Write-Verbose $_ Write-Error -Message 'Unable to import JSON file' -ErrorAction Stop } } else { Write-Error -Message 'Unsupported extension for SettingsFile' -ErrorAction Stop } <# Get all the DataConenctors #> $enabledDataConnectors = Get-AzSentinelDataConnector @arguments -ErrorAction SilentlyContinue <# Get AzureActivityLog connector data #> $azureActivityLog = Get-AzSentinelDataConnector @arguments -DataSourceName 'AzureActivityLog' -ErrorAction SilentlyContinue foreach ($item in $connectors.AzureActivityLog) { if ($null -ne $azureActivityLog){ $azureActivity = $azureActivityLog | Where-Object { $_.properties.linkedResourceId.Split('/')[2] -eq $item.subscriptionId } } else { $azureActivity } if ($azureActivity) { Write-Host "AzureActivityLog is already enabled on '$($item.subscriptionId)'" } else { $name = ($item.subscriptionId).Replace('-', '') $connectorBody = @{ id = "$script:Workspace/datasources/$name" name = $name type = 'Microsoft.OperationalInsights/workspaces/datasources' kind = 'AzureActivityLog' properties = @{ linkedResourceId = "/subscriptions/$($item.subscriptionId)/providers/microsoft.insights/eventtypes/management" } } $uri = "$baseUri/datasources/$($name)?api-version=2020-03-01-preview" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($connectorBody | ConvertTo-Json -Depth 4 -EnumsAsStrings) Write-Host "Successfully enabled AzureActivityLog for: $($item.subscriptionId) with status: $($result.StatusDescription)" } catch { $errorReturn = $_.Exception.Message Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($errorReturn)" -ErrorAction Stop } } } #AzureSecurityCenter connector foreach ($item in $connectors.AzureSecurityCenter) { if ($null -ne $enabledDataConnectors){ $azureSecurityCenter = $enabledDataConnectors | Where-Object { $_.Kind -eq "AzureSecurityCenter" -and $_.properties.subscriptionId -eq $item.subscriptionId } } else { $azureSecurityCenter } $skip = $false if ($null -ne $azureSecurityCenter) { if ($azureSecurityCenter.properties.dataTypes.alerts.state -eq $item.state) { Write-Host "AzureSecurityCenter is already '$($item.state)' for subscription '$($azureSecurityCenter.properties.subscriptionId)'" $skip = $true } else { $connectorBody = @{ id = $azureSecurityCenter.id name = $azureSecurityCenter.name etag = $azureSecurityCenter.etag type = 'Microsoft.SecurityInsights/dataConnectors' kind = 'AzureSecurityCenter' properties = @{ subscriptionId = $azureSecurityCenter.properties.subscriptionId dataTypes = @{ alerts = @{ state = $item.state } } } } } } else { $guid = (New-Guid).Guid $connectorBody = @{ id = "$script:Workspace/providers/Microsoft.SecurityInsights/dataConnectors/$guid" name = $guid type = 'Microsoft.SecurityInsights/dataConnectors' kind = 'AzureSecurityCenter' properties = @{ subscriptionId = $item.subscriptionId dataTypes = @{ alerts = @{ state = $item.state } } } } } if ($skip -eq $false) { # Enable or update AzureSecurityCenter with http put method $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/dataConnectors/$($connectorBody.name)?api-version=2020-01-01" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($connectorBody | ConvertTo-Json -Depth 4 -EnumsAsStrings) Write-Host "Successfully enabled AzureSecurityCenter for: $($item.subscriptionId) with status: $($result.StatusDescription)" } catch { $errorReturn = $_ $errorResult = ($errorReturn | ConvertFrom-Json ).error Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($errorResult.message)" -ErrorAction Stop } } } # Office365 connector foreach ($item in $connectors.Office365) { if (-Not (Get-Member -InputObject $item -Name "tenantId" -MemberType Properties)) { Write-Error "TenantId missing" break } if ($null -ne $enabledDataConnectors){ $office365 = $enabledDataConnectors | Where-Object { $_.kind -eq "Office365" -and $_.properties.tenantId -eq $item.tenantId } } else { $office365 } $skip = $false if ($null -ne $office365) { if ($office365) { Write-Host "Office365 is already enabled on tenant '$($office365.properties.tenantId)'" $skip = $true } else { $connectorBody = @{ id = $office365.id name = $office365.name etag = $office365.etag type = 'Microsoft.SecurityInsights/dataConnectors' kind = 'Office365' properties = @{ tenantId = $item.tenantId dataTypes = @{ exchange = @{ state = $item.exchange_state } sharepoint = @{ state = $item.sharepoint_state } teams = @{ state = $item.teams_state } } } } } } else { $guid = (New-Guid).Guid $connectorBody = @{ id = "$script:Workspace/providers/Microsoft.SecurityInsights/dataConnectors/$guid" name = $guid type = 'Microsoft.SecurityInsights/dataConnectors' kind = 'Office365' properties = @{ tenantId = $item.tenantId dataTypes = @{ exchange = @{ state = $item.exchange_state } sharepoint = @{ state = $item.sharepoint_state } teams = @{ state = $item.teams_state } } } } } if ($skip -eq $false) { # Enable or update Office365 with http put method $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/dataConnectors/$($connectorBody.name)?api-version=2020-01-01" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($connectorBody | ConvertTo-Json -Depth 4 -EnumsAsStrings) Write-Host "Successfully enabled Office365 with status: $($result.StatusDescription) for tenant '$($item.tenantId)'" } catch { $errorReturn = $_ $errorResult = ($errorReturn | ConvertFrom-Json ).error Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($errorResult.message)" -ErrorAction Stop } } } #ThreatIntelligenceTaxii foreach ($item in $connectors.ThreatIntelligenceTaxii) { if ($enabledDataConnectors){ $threatIntelligenceTaxii = $enabledDataConnectors | Where-Object { $_.Kind -eq "ThreatIntelligenceTaxii" -and $_.properties.friendlyName -eq $item.friendlyName } } else { $threatIntelligenceTaxii } $skip = $false if ($null -ne $threatIntelligenceTaxii) { if ($threatIntelligenceTaxii.properties.dataTypes.taxiiClient.state -eq $item.state) { Write-Host "ThreatIntelligenceTaxii is already $($item.state) for '$($item.friendlyName)'" $skip = $true } else { # Compose body for connector update scenario $connectorBody = @{ id = $threatIntelligenceTaxii.id name = $threatIntelligenceTaxii.name etag = $threatIntelligenceTaxii.etag type = 'Microsoft.SecurityInsights/dataConnectors' kind = 'ThreatIntelligenceTaxii' properties = @{ tenantId = $script:tenantId workspaceId = $script:workspaceId friendlyName = $item.friendlyName taxiiServer = $item.taxiiServer collectionId = $item.collectionId username = $item.username password = $item.password taxiiClients = $null dataTypes = @{ taxiiClient = @{ state = $item.state } } } } } } else { $guid = (New-Guid).Guid # Compose body for connector enable scenario $connectorBody = @{ id = "$script:Workspace/providers/Microsoft.SecurityInsights/dataConnectors/$guid" name = $guid type = 'Microsoft.SecurityInsights/dataConnectors' kind = 'ThreatIntelligenceTaxii' properties = @{ tenantId = $script:tenantId workspaceId = $script:workspaceId friendlyName = $item.friendlyName taxiiServer = $item.taxiiServer collectionId = $item.collectionId username = $item.username password = $item.password taxiiClients = $null dataTypes = @{ taxiiClient = @{ state = $item.state } } } } } if ($skip -eq $false) { # Enable or update ThreatIntelligenceTaxii $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/dataConnectors/$($connectorBody.name)?api-version=2020-01-01" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($connectorBody | ConvertTo-Json -Depth 4 -EnumsAsStrings) Write-Host "Successfully enabled ThreatIntelligenceTaxii for: $($item.friendlyName) with status: $($result.StatusDescription)" } catch { $errorReturn = $_.Exception.Message Write-Verbose $_ Write-Error "Unable to invoke webrequest with error message: $($errorReturn)" -ErrorAction Stop } } } } } function Import-AzSentinelAlertRule { <# .SYNOPSIS Import Azure Sentinal Alert rule .DESCRIPTION This function imports Azure Sentinal Alert rules from JSON and YAML config files. This way you can manage your Alert rules dynamic from JSON or multiple YAML files .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER SettingsFile Path to the JSON or YAML file for the AlertRules .EXAMPLE Import-AzSentinelAlertRule -WorkspaceName "" -SettingsFile ".\examples\AlertRules.json" In this example all the rules configured in the JSON file will be created or updated Performing the operation "Import-AzSentinelAlertRule" on target "Do you want to update profile: AlertRule01". [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Yes"): Successfully created Action for Rule: with Playbook pkmsentinel Status: Created Created Successfully updated rule: AlertRule01 with status: OK Name : b6103d42-xxx-4f35-xxx-c76a7f31ee4e DisplayName : AlertRule01 Description : Severity : Medium Enabled : True Query : SecurityEvent | where EventID == "4688" | where CommandLine contains "-noni -ep bypass $" QueryFrequency : PT5H QueryPeriod : PT6H TriggerOperator : GreaterThan TriggerThreshold : 5 SuppressionDuration : PT6H SuppressionEnabled : False Tactics : {Persistence, LateralMovement, Collection} PlaybookName : Playbook01 .EXAMPLE Import-AzSentinelAlertRule -WorkspaceName "" -SettingsFile ".\examples\SuspectApplicationConsent.yaml" In this example all the rules configured in the YAML file will be created or updated .EXAMPLE Get-Item .\examples\*.json | Import-AzSentinelAlertRule -WorkspaceName "" In this example you can select multiple JSON files and Pipeline it to the SettingsFile parameter #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $WorkspaceName, [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript( { (Test-Path -Path $_) -and ($_.Extension -in '.json', '.yaml', '.yml') })] [System.IO.FileInfo] $SettingsFile ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } if ($SettingsFile.Extension -eq '.json') { try { $rulesRaw = Get-Content $SettingsFile -Raw $rules = $rulesRaw | ConvertFrom-Json -Depth 99 Write-Verbose -Message "Found $($rules.count) rules" } catch { Write-Verbose $_ Write-Error -Message 'Unable to import JSON file' -ErrorAction Stop } } elseif ($SettingsFile.Extension -in '.yaml', '.yml') { try { $rules = [pscustomobject](Get-Content $SettingsFile -Raw | ConvertFrom-Yaml -ErrorAction Stop) $rules | Add-Member -MemberType NoteProperty -Name DisplayName -Value $rules.name Write-Verbose -Message 'Found compatibel yaml file' } catch { Write-Verbose $_ Write-Error -Message 'Unable to convert yaml file' -ErrorAction Stop } } else { Write-Error -Message 'Unsupported extension for SettingsFile' -ErrorAction Stop } $return = @() <# Test All rules first #> if($rules.analytics -or $rules.Scheduled -or $rules.fusion -or $rules.MLBehaviorAnalytics -or $rules.MicrosoftSecurityIncidentCreation) { $allRules = $rules.analytics + $rules.Scheduled + $rules.fusion + $rules.MLBehaviorAnalytics + $rules.MicrosoftSecurityIncidentCreation | Select-Object displayName try { Write-Verbose -Message "Found $($allRules.displayName.Count) rules in the settings file." $allRulesContent = Get-AzSentinelAlertRule @arguments -RuleName $($allRules.displayName) -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } } <# Analytics rule Take the raw rule configuration if it is not nested in "analytics", "Scheduled", "fusion", "MLBehaviorAnalytics" or "MicrosoftIncidentCreation" #> if (-not $rules.analytics -and -not $rules.Scheduled -and -not $rules.fusion -and -not $rules.MLBehaviorAnalytics -and -not $rules.MicrosoftSecurityIncidentCreation){ Write-Verbose -Message "Settings file is not nested in root schema, using raw configuration." $scheduled = $rules } elseif ($rules.analytics) { $scheduled = $rules.analytics } else{ $scheduled = $rules.Scheduled } foreach ($item in $scheduled) { Write-Verbose -Message "Started with rule: $($item.displayName)" $guid = (New-Guid).Guid if($allRulesContent) { $content = $allRulesContent | Where-Object {$_.kind -eq 'Scheduled' -and $_.displayName -eq $item.displayName} } else{ $content = Get-AzSentinelAlertRule @arguments -RuleName $($item.displayName) -ErrorAction Stop } Write-Verbose -Message "Get rule $($item.description)" if ($content) { Write-Verbose "Rule $($item.displayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.etag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($content.name)?api-version=2019-01-01-preview" } else { Write-Verbose -Message "Rule $($item.displayName) doesn't exist in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/providers/Microsoft.SecurityInsights/alertRules/$guid" -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($guid)?api-version=2019-01-01-preview" } # The official API schema indicates that the grouping configuration is part of the incident configuration try { # Added if/else statement for backwards compatibility if($item.incidentConfiguration){ $groupingConfiguration = [GroupingConfiguration]::new( $item.incidentConfiguration.groupingConfiguration.enabled, $item.incidentConfiguration.groupingConfiguration.reopenClosedIncident, $item.incidentConfiguration.groupingConfiguration.lookbackDuration, $item.incidentConfiguration.groupingConfiguration.entitiesMatchingMethod, $item.incidentConfiguration.groupingConfiguration.groupByEntities ) $incidentConfiguration = [IncidentConfiguration]::new( $item.incidentConfiguration.createIncident, $groupingConfiguration ) } else{ $groupingConfiguration = [GroupingConfiguration]::new( $item.groupingConfiguration.enabled, $item.groupingConfiguration.reopenClosedIncident, $item.groupingConfiguration.lookbackDuration, $item.groupingConfiguration.entitiesMatchingMethod, $item.groupingConfiguration.groupByEntities ) $incidentConfiguration = [IncidentConfiguration]::new( $item.createIncident, $groupingConfiguration ) Write-Warning -Message "`"$($item.displayName)`" configuration is not following the official API schema, consider updating the incident and grouping configuration." } if (($item.AlertRuleTemplateName -and ! $content) -or $content.AlertRuleTemplateName){ if ($content.AlertRuleTemplateName){ <# If alertRule is already created with a TemplateName then Always use template name from existing rule. You can't attach existing scheduled rule to another templatename or remove the link to the template #> $item | Add-Member -NotePropertyName AlertRuleTemplateName -NotePropertyValue $content.AlertRuleTemplateName -Force } $bodyAlertProp = [ScheduledAlertProp]::new( $item.name, $item.displayName, $item.description, $item.severity, $item.enabled, $item.query, $item.queryFrequency, $item.queryPeriod, $item.triggerOperator, $item.triggerThreshold, $item.suppressionDuration, $item.suppressionEnabled, $item.Tactics, $item.playbookName, $incidentConfiguration, $item.aggregationKind, $item.AlertRuleTemplateName ) } else { $bodyAlertProp = [ScheduledAlertProp]::new( $item.name, $item.displayName, $item.description, $item.severity, $item.enabled, $item.query, $item.queryFrequency, $item.queryPeriod, $item.triggerOperator, $item.triggerThreshold, $item.suppressionDuration, $item.suppressionEnabled, $item.Tactics, $item.playbookName, $incidentConfiguration, $item.aggregationKind ) } $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'Scheduled') } catch { Write-Error "Unable to initiate class with error: $($_.Exception.Message)" -ErrorAction Stop } if ($content) { if ($item.playbookName -or $content.playbookName) { $compareResult = Compare-Policy -ReferenceTemplate ($content | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id, incidentConfiguration, queryResultsAggregationSettings) -DifferenceTemplate ($body.Properties | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id, incidentConfiguration, queryResultsAggregationSettings) } else { $compareResult = Compare-Policy -ReferenceTemplate ($content | Select-Object * -ExcludeProperty lastModifiedUtc, alertRuleTemplateName, name, etag, id, PlaybookName, incidentConfiguration, queryResultsAggregationSettings) -DifferenceTemplate ($body.Properties | Select-Object * -ExcludeProperty name, PlaybookName, incidentConfiguration, queryResultsAggregationSettings) } try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | Select-Object * -ExcludeProperty Properties.PlaybookName | ConvertTo-Json -Depth 10 -EnumsAsStrings) if (($compareResult | Where-Object PropertyName -eq "playbookName").DiffValue) { $PlaybookResult = New-AzSentinelAlertRuleAction @arguments -PlayBookName $($item.playbookName) -RuleId $($body.Name) $body.Properties | Add-Member -NotePropertyName PlaybookStatus -NotePropertyValue $PlaybookResult -Force } elseif (($compareResult | Where-Object PropertyName -eq "playbookName").RefValue) { $PlaybookResult = Remove-AzSentinelAlertRuleAction @arguments -RuleId $body.Name -Confirm:$false $body.Properties | Add-Member -NotePropertyName PlaybookStatus -NotePropertyValue $PlaybookResult -Force } else { #nothing } $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties Write-Verbose $_ Write-Error "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } else { Write-Verbose "Creating new rule: $($item.displayName)" try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | Select-Object * -ExcludeProperty Properties.PlaybookName | ConvertTo-Json -Depth 10 -EnumsAsStrings) if ($body.Properties.playbookName) { $PlaybookResult = New-AzSentinelAlertRuleAction @arguments -PlayBookName $($item.playbookName) -RuleId $($body.Name) -confirm:$false $body.Properties | Add-Member -NotePropertyName PlaybookStatus -NotePropertyValue $PlaybookResult -Force } $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Scheduled" -Force $return += $body.Properties Write-Verbose $_ Write-Error "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } } <# Fusion rule #> foreach ($item in $rules.fusion) { Write-Verbose "Rule type is Fusion" $guid = (New-Guid).Guid $content = $allRulesContent | Where-Object {$_.kind -eq 'Fusion' -and $_.displayName -eq $item.displayName} Write-Verbose -Message "Get rule $($item.description)" if ($content) { Write-Verbose "Rule $($item.displayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.etag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($content.name)?api-version=2019-01-01-preview" } else { Write-Verbose -Message "Rule $($item.displayName) doesn't exist in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/providers/Microsoft.SecurityInsights/alertRules/$guid" -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($guid)?api-version=2019-01-01-preview" } $bodyAlertProp = [Fusion]::new( $item.enabled, $item.alertRuleTemplateName ) $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'Fusion') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Fusion" -Force $return += $body.Properties } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "Fusion" -Force $return += $body.Properties Write-Verbose $_ Write-Verbose "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } <# MLBehaviorAnalytics #> foreach ($item in $rules.MLBehaviorAnalytics) { Write-Verbose "Rule type is ML Behavior Analytics" $guid = (New-Guid).Guid $content = $allRulesContent | Where-Object {$_.kind -eq 'MLBehaviorAnalytics' -and $_.displayName -eq $item.displayName} Write-Verbose -Message "Get rule $($item.description)" if ($content) { Write-Verbose "Rule $($item.displayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.etag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($content.name)?api-version=2019-01-01-preview" } else { Write-Verbose -Message "Rule $($item.displayName) doesn't exist in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/providers/Microsoft.SecurityInsights/alertRules/$guid" -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($guid)?api-version=2019-01-01-preview" } $bodyAlertProp = [MLBehaviorAnalytics]::new( $item.enabled, $item.alertRuleTemplateName ) $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'MLBehaviorAnalytics') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MLBehaviorAnalytics" -Force $return += $body.Properties } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MLBehaviorAnalytics" -Force $return += $body.Properties Write-Verbose $_ Write-Verbose "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } <# MicrosoftSecurityIncidentCreation #> foreach ($item in $rules.MicrosoftSecurityIncidentCreation) { Write-Verbose "Rule type is Microsoft Security" $guid = (New-Guid).Guid $content = $allRulesContent | Where-Object {$_.kind -eq 'MicrosoftSecurityIncidentCreation' -and $_.displayName -eq $item.displayName} Write-Verbose -Message "Get rule $($item.description)" $content = Get-AzSentinelAlertRule @arguments -RuleName $($item.displayName) -ErrorAction SilentlyContinue if ($content) { Write-Verbose "Rule $($item.displayName) exists in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $content.name -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $content.etag -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue $content.id -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($content.name)?api-version=2019-01-01-preview" } else { Write-Verbose -Message "Rule $($item.displayName) doesn't exist in Azure Sentinel" $item | Add-Member -NotePropertyName name -NotePropertyValue $guid -Force $item | Add-Member -NotePropertyName etag -NotePropertyValue $null -Force $item | Add-Member -NotePropertyName Id -NotePropertyValue "$script:Workspace/providers/Microsoft.SecurityInsights/alertRules/$guid" -Force $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($guid)?api-version=2019-01-01-preview" } $bodyAlertProp = [MicrosoftSecurityIncidentCreation]::new( $item.displayName, $item.description, $item.enabled, $item.productFilter, $item.severitiesFilter, $item.displayNamesFilter ) $body = [AlertRule]::new( $item.name, $item.etag, $bodyAlertProp, $item.Id, 'MicrosoftSecurityIncidentCreation') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue $($result.StatusDescription) -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MicrosoftSecurityIncidentCreation" -Force $return += $body.Properties } catch { $body.Properties | Add-Member -NotePropertyName status -NotePropertyValue "failed" -Force $body.Properties | Add-Member -NotePropertyName Kind -NotePropertyValue "MicrosoftSecurityIncidentCreation" -Force $return += $body.Properties Write-Verbose $_ Write-Verbose "Unable to invoke webrequest for rule $($item.displayName) with error message: $($_.Exception.Message)" -ErrorAction Continue } } return $return } } function Get-AzSentinelIncident { <# .SYNOPSIS Get Azure Sentinel Incident .DESCRIPTION With this function you can get a list of open incidents from Azure Sentinel. You can can also filter to Incident with speciefiek case namber or Case name .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER IncidentName Enter incident name, this is the same name as the alert rule that triggered the incident .PARAMETER CaseNumber Enter the case number to get specfiek details of a open case .PARAMETER All Use -All switch to get a list of all the incidents .EXAMPLE Get-AzSentinelIncident -WorkspaceName "" Get a list of the last 200 Incidents .EXAMPLE Get-AzSentinelIncident -WorkspaceName "" -All Get a list of all Incidents .EXAMPLE Get-AzSentinelIncident -WorkspaceName "" -CaseNumber Get information of a specifiek incident with providing the casenumber .EXAMPLE Get-AzSentinelIncident -WorkspaceName "" -IncidentName "", "" Get information of one or more incidents with providing a incident name, this is the name of the alert rule that triggered the incident #> [cmdletbinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$IncidentName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [int[]]$CaseNumber, [Parameter(Mandatory = $false, ValueFromPipeline)] [Switch]$All ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { Get-LogAnalyticWorkspace @arguments -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/Cases?api-version=2019-01-01-preview" Write-Verbose -Message "Using URI: $($uri)" try { $incidentRaw = (Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader) $incident += $incidentRaw.value if ($All){ while ($incidentRaw.nextLink) { $incidentRaw = (Invoke-RestMethod -Uri $($incidentRaw.nextLink) -Headers $script:authHeader -Method Get) $incident += $incidentRaw.value } } } catch { Write-Verbose $_ Write-Error "Unable to get incidents with error code: $($_.Exception.Message)" -ErrorAction Stop } $return = @() if ($incident) { Write-Verbose "Found $($incident.count) incidents" if ($IncidentName.Count -ge 1) { foreach ($rule in $IncidentName) { [PSCustomObject]$temp = $incident | Where-Object { $_.properties.title -eq $rule } if ($null -ne $temp) { $temp.properties | Add-Member -NotePropertyName etag -NotePropertyValue $temp.etag -Force $temp.properties | Add-Member -NotePropertyName name -NotePropertyValue $temp.name -Force $return += $temp.properties } else { Write-Error "Unable to find incident: $rule" } } return $return } elseif ($CaseNumber.Count -ge 1) { foreach ($rule in $CaseNumber) { [PSCustomObject]$temp = $incident | Where-Object { $_.properties.caseNumber -eq $rule } if ($null -ne $temp) { $temp.properties | Add-Member -NotePropertyName etag -NotePropertyValue $temp.etag -Force $temp.properties | Add-Member -NotePropertyName name -NotePropertyValue $temp.name -Force $return += $temp.properties } else { Write-Error "Unable to find incident: $rule" } } return $return } else { $incident | ForEach-Object { $_.properties | Add-Member -NotePropertyName etag -NotePropertyValue $_.etag -Force $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force } return $incident.properties } } else { Write-Warning "No incident found on $($WorkspaceName)" } } } function Get-AzSentinelHuntingRule { <# .SYNOPSIS Get Azure Sentinel Hunting rule .DESCRIPTION With this function you can get the configuration of the Azure Sentinel Hunting rule from Azure Sentinel .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the Hunting rule name .PARAMETER Filter Select which type of Hunting rules you want to see. Option: HuntingQueries, GeneralExploration, LogManagement .EXAMPLE Get-AzSentinelHuntingRule -WorkspaceName "" -RuleName "","" In this example you can get configuration of multiple Hunting rules .EXAMPLE Get-AzSentinelHuntingRule -WorkspaceName "" In this example you can get a list of all the Hunting rules in once #> [cmdletbinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$RuleName, [Parameter(Mandatory = $false, ValueFromPipeline)] [validateset("Hunting Queries", "Log Management", "General Exploration")] [string]$Filter ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { Get-LogAnalyticWorkspace @arguments -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } $uri = "$script:baseUri/savedSearches?api-version=2017-04-26-preview" Write-Verbose -Message "Using URI: $($uri)" try { if ($Filter) { $huntingRules = (Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader).value | Where-Object { $_.properties.Category -eq $Filter } } else { $huntingRules = (Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader).value } } catch { Write-Verbose $_ Write-Error "Unable to get hunting rules with error code: $($_.Exception.Message)" -ErrorAction Stop } $return = @() if ($huntingRules) { Write-Verbose "Found $($huntingRules.count) hunting rules" if ($RuleName.Count -ge 1) { foreach ($rule in $RuleName) { $temp = @() [PSCustomObject]$temp = $huntingRules | Where-Object { ($_.properties).DisplayName -eq $rule } if ($null -ne $temp) { $temp.properties | Add-Member -NotePropertyName name -NotePropertyValue $temp.name -Force $temp.properties | Add-Member -NotePropertyName id -NotePropertyValue $temp.id -Force $temp.properties | Add-Member -NotePropertyName etag -NotePropertyValue $temp.etag -Force $return += $temp.Properties } } return $return } else { $huntingRules | ForEach-Object { $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force $_.properties | Add-Member -NotePropertyName id -NotePropertyValue $_.id -Force $_.properties | Add-Member -NotePropertyName etag -NotePropertyValue $_.etag -Force $return += $_.properties } return $return } } else { Write-Verbose "No hunting rules found on $($WorkspaceName)" } } } function Get-AzSentinelDataConnector { <# .SYNOPSIS Get Azure Sentinel Data connector .DESCRIPTION With this function you can get Azure Sentinel data connectors that are enabled on the workspace .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER DataConnectorName Enter the Connector ID .EXAMPLE Get-AzSentinelDataConnector -WorkspaceName "" List all enabled dataconnector .EXAMPLE Get-AzSentinelDataConnector -WorkspaceName "" -DataConnectorName "","" Get specific dataconnectors #> param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$DataConnectorName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [DataSourceName[]]$DataSourceName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { Get-LogAnalyticWorkspace @arguments -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } if ($DataConnectorName) { $dataConnectors = @() foreach ($item in $DataConnectorName){ $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/dataConnectors/$($item)?api-version=2020-01-01" try { $result = Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader $dataConnectors += $result } catch { Write-Verbose $_ Write-Error "Unable to get alert rules with error code: $($_.Exception.Message)" -ErrorAction Stop } } return $dataConnectors } elseif ($DataSourceName) { $dataSources = @() foreach ($dataSource in $DataSourceName){ $uri = $($script:baseUri)+ "/dataSources?"+'$'+"filter=kind+eq+'"+$dataSource+"'&api-version=2020-08-01" try { $result = Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader $dataSources += $result } catch { Write-Verbose $_ Write-Error "Unable to get alert rules with error code: $($_.Exception.Message)" -ErrorAction Stop } } return $dataSources.value } else { $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/dataConnectors?api-version=2020-01-01" try { $result = Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader } catch { Write-Verbose $_ Write-Error "Unable to get alert rules with error code: $($_.Exception.Message)" -ErrorAction Stop } return $result.value } } } function Get-AzSentinelAlertRuleTemplates { <# .SYNOPSIS Get Azure Sentinel Alert Rules Templates .DESCRIPTION With this function you can get the configuration of the Azure Sentinel Alert Rules Templates from Azure Sentinel .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER Kind Enter the Kind to filter on the templates .EXAMPLE Get-AzSentinelAlertRuleTemplates -WorkspaceName "" In this example you can get Sentinel alert rules templates in once .EXAMPLE Get-AzSentinelAlertRuleTemplates -WorkspaceName "" -Kind Fusion, MicrosoftSecurityIncidentCreation Filter on the Kind #> param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [Kind[]]$Kind ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { Get-LogAnalyticWorkspace @arguments -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRuleTemplates?api-version=2019-01-01-preview" Write-Verbose -Message "Using URI: $($uri)" try { $alertRulesTemplates = (Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader).value } catch { Write-Verbose $_ Write-Error "Unable to get alert rules with error code: $($_.Exception.Message)" -ErrorAction Stop } $return = @() if ($alertRulesTemplates) { Write-Verbose "Found $($alertRulesTemplates.count) Alert rules templates" if ($Kind) { foreach ($item in $Kind) { $alertRulesTemplates | Where-Object Kind -eq $item | ForEach-Object { $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force $_.properties | Add-Member -NotePropertyName id -NotePropertyValue $_.id -Force $_.properties | Add-Member -NotePropertyName kind -NotePropertyValue $_.kind -Force $return += $_.properties } } } else { $alertRulesTemplates | ForEach-Object { $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force $_.properties | Add-Member -NotePropertyName id -NotePropertyValue $_.id -Force $_.properties | Add-Member -NotePropertyName kind -NotePropertyValue $_.kind -Force $return += $_.properties } } return $return } else { Write-Host "No rules templates found on $($WorkspaceName)" } } } function Get-AzSentinelAlertRuleAction { <# .SYNOPSIS Get Azure Sentinel Alert rule Action .DESCRIPTION This function can be used to see if an action is attached to the alert rule, if so then the configuration will be returned .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the Alert rule .PARAMETER RuleId Enter the Rule Id to skip Get-AzSentinelAlertRule step .EXAMPLE Get-AzSentinelAlertRuleAction -WorkspaceName "" -RuleName "testrule01" This example will get the Workspace ands return the full data object .NOTES NAME: Get-AzSentinelAlertRuleAction #> param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string]$SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RuleName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$RuleId ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } if ($RuleName) { try { $alertId = (Get-AzSentinelAlertRule @arguments -RuleName $RuleName -ErrorAction Stop).name } catch { Write-Error $_.Exception.Message break } } elseif ($RuleId) { $alertId = $RuleId } else { Write-Error "No Alert Name or ID is provided" } if ($alertId) { $uri = "$($Script:baseUri)/providers/Microsoft.SecurityInsights/alertRules/$($alertId)/actions?api-version=2019-01-01-preview" try { $return = (Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader).value return $return } catch { $return = $_.Exception.Message return $return } } else { $return = "No Alert found with provided: $($alertId)" return $return } } } function Get-AzSentinelAlertRule { <# .SYNOPSIS Get Azure Sentinel Alert Rules .DESCRIPTION With this function you can get the configuration of the Azure Sentinel Alert rule from Azure Sentinel .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the Alert rule .PARAMETER Kind The alert rule kind .PARAMETER LastModified Filter for rules modified after this date/time .PARAMETER SkipPlaybook Use SkipPlaybook switch to only return the rule properties, this skips the Playbook resolve step. .EXAMPLE Get-AzSentinelAlertRule -WorkspaceName "" -RuleName "","" In this example you can get configuration of multiple alert rules in once .EXAMPLE Get-AzSentinelAlertRule -SubscriptionId "" -WorkspaceName "" -LastModified 2020-09-21 In this example you can get configuration of multiple alert rules only if modified after the 21st September 2020. The datetime must be in ISO8601 format. #> [cmdletbinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$RuleName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [Kind[]]$Kind, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [DateTime]$LastModified, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [switch]$SkipPlaybook ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { Get-LogAnalyticWorkspace @arguments -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules?api-version=2020-01-01" Write-Verbose -Message "Using URI: $($uri)" try { $alertRules = Invoke-RestMethod -Uri $uri -Method Get -Headers $script:authHeader } catch { Write-Verbose $_ Write-Error "Unable to get alert rules with error code: $($_.Exception.Message)" -ErrorAction Stop } $return = @() if ($alertRules.value -and $LastModified) { Write-Verbose "Filtering for rules modified after $LastModified" $alertRules.value = $alertRules.value | Where-Object { $_.properties.lastModifiedUtc -gt $LastModified } } if ($alertRules.value) { Write-Verbose "Found $($alertRules.value.count) Alert rules" if ($RuleName.Count -ge 1) { foreach ($rule in $RuleName) { $alertRules.value | Where-Object { $_.properties.displayName -eq $rule } | ForEach-Object { $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force $_.properties | Add-Member -NotePropertyName etag -NotePropertyValue $_.etag -Force $_.properties | Add-Member -NotePropertyName id -NotePropertyValue $_.id -Force $_.properties | Add-Member -NotePropertyName kind -NotePropertyValue $_.kind -Force # Updating incidentConfiguration output to match JSON input if ($_.properties.kind -eq 'Scheduled'){ $_.properties | Add-Member -NotePropertyName createIncident -NotePropertyValue $_.properties.incidentConfiguration.createIncident -Force $_.properties | Add-Member -NotePropertyName groupingConfiguration -NotePropertyValue $_.properties.incidentConfiguration.groupingConfiguration -Force $_.properties.PSObject.Properties.Remove('incidentConfiguration') } if (! $SkipPlaybook) { $playbook = Get-AzSentinelAlertRuleAction @arguments -RuleId $_.name if ($playbook) { $playbookName = ($playbook.properties.logicAppResourceId).Split('/')[-1] } else { $playbookName = "" } $_.properties | Add-Member -NotePropertyName playbookName -NotePropertyValue $playbookName -Force } $return += $_.properties } } return $return } elseif ($Kind.Count -ge 1) { foreach ($rule in $Kind) { $alertRules.value | Where-Object { $_.Kind -eq $rule } | ForEach-Object { $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force $_.properties | Add-Member -NotePropertyName etag -NotePropertyValue $_.etag -Force $_.properties | Add-Member -NotePropertyName id -NotePropertyValue $_.id -Force $_.properties | Add-Member -NotePropertyName kind -NotePropertyValue $_.kind -Force # Updating incidentConfiguration output to match JSON input if ($_.properties.kind -eq 'Scheduled'){ $_.properties | Add-Member -NotePropertyName createIncident -NotePropertyValue $_.properties.incidentConfiguration.createIncident -Force $_.properties | Add-Member -NotePropertyName groupingConfiguration -NotePropertyValue $_.properties.incidentConfiguration.groupingConfiguration -Force $_.properties.PSObject.Properties.Remove('incidentConfiguration') } if (! $SkipPlaybook) { $playbook = Get-AzSentinelAlertRuleAction @arguments -RuleId $_.name if ($playbook) { $playbookName = ($playbook.properties.logicAppResourceId).Split('/')[-1] } else { $playbookName = "" } $_.properties | Add-Member -NotePropertyName playbookName -NotePropertyValue $playbookName -Force } $return += $_.properties } } return $return } else { $alertRules.value | ForEach-Object { $_.properties | Add-Member -NotePropertyName name -NotePropertyValue $_.name -Force $_.properties | Add-Member -NotePropertyName id -NotePropertyValue $_.id -Force $_.properties | Add-Member -NotePropertyName kind -NotePropertyValue $_.kind -Force # Updating incidentConfiguration output to match JSON input if ($_.properties.kind -eq 'Scheduled'){ $_.properties | Add-Member -NotePropertyName createIncident -NotePropertyValue $_.properties.incidentConfiguration.createIncident -Force $_.properties | Add-Member -NotePropertyName groupingConfiguration -NotePropertyValue $_.properties.incidentConfiguration.groupingConfiguration -Force $_.properties.PSObject.Properties.Remove('incidentConfiguration') } if (! $SkipPlaybook) { $playbook = Get-AzSentinelAlertRuleAction @arguments -RuleId $_.name if ($playbook) { $playbookName = ($playbook.properties.logicAppResourceId).Split('/')[-1] } else { $playbookName = "" } $_.properties | Add-Member -NotePropertyName playbookName -NotePropertyValue $playbookName -Force } $return += $_.properties } return $return } } else { Write-Verbose "No rules found on $($WorkspaceName)" } } } function Export-AzSentinel { <# .SYNOPSIS Export Azure Sentinel .DESCRIPTION With this function you can export Azure Sentinel configuration .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER Kind Select what you want to export: Alert, Hunting, Templates or All .PARAMETER OutputFolder The Path where you want to export the JSON files .PARAMETER TemplatesKind Select which Kind of templates you want to export, if empy all Templates will be exported .EXAMPLE Export-AzSentinel -WorkspaceName '' -Path C:\Temp\ -Kind All In this example you export Alert, Hunting and Template rules .EXAMPLE Export-AzSentinel -WorkspaceName '' -Path C:\Temp\ -Kind Templates In this example you export only the Templates .EXAMPLE Export-AzSentinel -WorkspaceName '' -Path C:\Temp\ -Kind Alert In this example you export only the Scheduled Alert rules #> param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory)] [System.IO.FileInfo]$OutputFolder, [Parameter(Mandatory, ValueFromPipeline)] [ExportType[]]$Kind, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [Kind[]]$TemplatesKind ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } $date = Get-Date -Format HHmmss_ddMMyyyy <# Test export path #> if (Test-Path $OutputFolder) { Write-Verbose "Path Exists" } else { try { $null = New-Item -Path $OutputFolder -Force -ItemType Directory -ErrorAction Stop } catch { $ErrorMessage = $_.Exception.Message Write-Error $ErrorMessage Write-Verbose $_ Break } } <# Export Alert rules section #> if (($Kind -like 'Alert') -or ($Kind -like 'All')) { try { $rules = Get-AzSentinelAlertRule @arguments -ErrorAction Stop } catch { $return = $_.Exception.Message Write-Error $return } if ($rules) { $output = @{ Scheduled = @( $rules | Where-Object kind -eq Scheduled ) Fusion = @( $rules | Where-Object kind -eq Fusion ) MLBehaviorAnalytics = @( $rules | Where-Object kind -eq MLBehaviorAnalytics ) MicrosoftSecurityIncidentCreation = @( $rules | Where-Object kind -eq MicrosoftSecurityIncidentCreation ) } try { $fullPath = "$($OutputFolder)AlertRules_$date.json" $output | ConvertTo-Json -EnumsAsStrings -Depth 15 | Out-File $fullPath -ErrorAction Stop Write-Output "Alert rules exported to: $fullPath" } catch { $ErrorMessage = $_.Exception.Message Write-Error $ErrorMessage Write-Verbose $_ Break } } } <# Export Hunting rules section #> if (($Kind -like 'Hunting') -or ($Kind -like 'All')) { try { $rules = Get-AzSentinelHuntingRule @arguments -ErrorAction Stop } catch { $return = $_.Exception.Message Write-Error $return } if ($rules) { $output = @{ Hunting = @() } $output.Hunting += $rules try { $fullPath = "$($OutputFolder)HuntingRules_$date.json" $output | ConvertTo-Json -EnumsAsStrings -Depth 15 | Out-File $fullPath -ErrorAction Stop Write-Output "Hunting rules exported to: $fullPath" } catch { $ErrorMessage = $_.Exception.Message Write-Error $ErrorMessage Write-Verbose $_ Break } } } <# Export Templates section #> if (($Kind -like 'Templates') -or ($Kind -like 'All')) { if ($TemplatesKind) { try { $templates = Get-AzSentinelAlertRuleTemplates @arguments -Kind $TemplatesKind } catch { $return = $_.Exception.Message Write-Error $return } } else { try { $templates = Get-AzSentinelAlertRuleTemplates @arguments } catch { $return = $_.Exception.Message Write-Error $return } } if ($templates) { $output = @{ Scheduled = @( $templates | Where-Object kind -eq Scheduled ) Fusion = @( $templates | Where-Object kind -eq Fusion ) MLBehaviorAnalytics = @( $templates | Where-Object kind -eq MLBehaviorAnalytics ) MicrosoftSecurityIncidentCreation = @( $templates | Where-Object kind -eq MicrosoftSecurityIncidentCreation ) } try { $fullPath = "$($OutputFolder)Templates_$date.json" $output | ConvertTo-Json -EnumsAsStrings -Depth 15 | Out-File $fullPath -ErrorAction Stop Write-Output "Templates xported to: $fullPath" } catch { $ErrorMessage = $_.Exception.Message Write-Error $ErrorMessage Write-Verbose $_ Break } } } } } function Enable-AzSentinelAlertRule { <# .SYNOPSIS Enable Azure Sentinel Alert Rules .DESCRIPTION With this function you can enable Azure Sentinel Alert rule .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the Alert rule .EXAMPLE Enable-AzSentinelAlertRule -WorkspaceName "" -RuleName "","" In this example you can get configuration of multiple alert rules in once #> [cmdletbinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$RuleName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { $rules = Get-AzSentinelAlertRule @arguments -RuleName $RuleName -ErrorAction Stop } catch { $return = $_.Exception.Message Write-Error $return } foreach ($rule in $rules) { if ($rule.enabled -eq $true) { Write-Output "'$($rule.DisplayName)' already has status '$($rule.enabled)'" } else { $rule.enabled = $true $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($rule.name)?api-version=2019-01-01-preview" $groupingConfiguration = [GroupingConfiguration]::new( $rule.incidentConfiguration.groupingConfiguration.GroupingConfigurationEnabled, $rule.incidentConfiguration.groupingConfiguration.ReopenClosedIncident, $rule.incidentConfiguration.groupingConfiguration.LookbackDuration, $rule.incidentConfiguration.groupingConfiguration.EntitiesMatchingMethod, $rule.incidentConfiguration.groupingConfiguration.GroupByEntities ) $incidentConfiguration = [IncidentConfiguration]::new( $rule.incidentConfiguration.CreateIncident, $groupingConfiguration ) $bodyAlertProp = [ScheduledAlertProp]::new( $rule.name, $rule.DisplayName, $rule.Description, $rule.Severity, $rule.Enabled, $rule.Query, $rule.QueryFrequency, $rule.QueryPeriod, $rule.TriggerOperator, $rule.TriggerThreshold, $rule.SuppressionDuration, $rule.SuppressionEnabled, $rule.Tactics, $rule.PlaybookName, $incidentConfiguration, $rule.AggregationKind ) $body = [AlertRule]::new( $rule.name, $rule.etag, $bodyAlertProp, $rule.Id, 'Scheduled') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) Write-Verbose $result Write-Output "Status of '$($rule.DisplayName)' changed to '$($rule.enabled)'" } catch { Write-Error $_.Exception.Message } } } } } function Disable-AzSentinelAlertRule { <# .SYNOPSIS Disable Azure Sentinel Alert Rules .DESCRIPTION With this function you can disbale Azure Sentinel Alert rule .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER RuleName Enter the name of the Alert rule .EXAMPLE Disable-AzSentinelAlertRule -WorkspaceName "" -RuleName "","" In this example you can get configuration of multiple alert rules in once #> [cmdletbinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory = $false, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string[]]$RuleName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { $rules = Get-AzSentinelAlertRule @arguments -RuleName $RuleName -ErrorAction Stop } catch { $return = $_.Exception.Message Write-Error $return } foreach ($rule in $rules) { if ($rule.enabled -eq $false) { Write-Output "'$($rule.DisplayName)' already has status '$($rule.enabled)'" } else { $rule.enabled = $false $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/alertRules/$($rule.name)?api-version=2019-01-01-preview" $groupingConfiguration = [GroupingConfiguration]::new( $rule.incidentConfiguration.groupingConfiguration.GroupingConfigurationEnabled, $rule.incidentConfiguration.groupingConfiguration.ReopenClosedIncident, $rule.incidentConfiguration.groupingConfiguration.LookbackDuration, $rule.incidentConfiguration.groupingConfiguration.EntitiesMatchingMethod, $rule.incidentConfiguration.groupingConfiguration.GroupByEntities ) $incidentConfiguration = [IncidentConfiguration]::new( $rule.incidentConfiguration.CreateIncident, $groupingConfiguration ) $bodyAlertProp = [ScheduledAlertProp]::new( $rule.name, $rule.DisplayName, $rule.Description, $rule.Severity, $rule.Enabled, $rule.Query, $rule.QueryFrequency, $rule.QueryPeriod, $rule.TriggerOperator, $rule.TriggerThreshold, $rule.SuppressionDuration, $rule.SuppressionEnabled, $rule.Tactics, $rule.PlaybookName, $incidentConfiguration, $rule.AggregationKind ) $body = [AlertRule]::new( $rule.name, $rule.etag, $bodyAlertProp, $rule.Id, 'Scheduled') try { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json -Depth 10 -EnumsAsStrings) Write-Verbose $result Write-Output "Status of '$($rule.DisplayName)' changed to '$($rule.enabled)'" } catch { Write-Error $_.Exception.Message } } } } } function Set-AzSentinel { <# .SYNOPSIS Enable Azure Sentinel .DESCRIPTION This function enables Azure Sentinel on a existing Workspace .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .EXAMPLE Set-AzSentinel -WorkspaceName "" This example will enable Azure Sentinel for the provided workspace #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } try { $workspaceResult = Get-LogAnalyticWorkspace @arguments -FullObject -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } # Variables $errorResult = '' if ($workspaceResult.properties.provisioningState -eq 'Succeeded') { <# Testing to see if OperationsManagement resource provider is enabled on subscription #> $operationsManagementProvider = Get-AzSentinelResourceProvider -NameSpace "OperationsManagement" if ($operationsManagementProvider.registrationState -ne 'Registered') { Write-Warning "Resource provider 'Microsoft.OperationsManagement' is not registered" if ($PSCmdlet.ShouldProcess("Do you want to enable 'Microsoft.OperationsManagement' on subscription $($script:subscriptionId)")) { Set-AzSentinelResourceProvider -NameSpace 'OperationsManagement' } else { Write-Output "No change have been." break } } <# Testing to see if SecurityInsights resource provider is enabled on subscription #> $securityInsightsProvider = Get-AzSentinelResourceProvider -NameSpace 'SecurityInsights' if ($securityInsightsProvider.registrationState -ne 'Registered') { Write-Warning "Resource provider 'Microsoft.SecurityInsights' is not registered" if ($PSCmdlet.ShouldProcess("Do you want to enable 'Microsoft.SecurityInsights' on subscription $($script:subscriptionId)")) { Set-AzSentinelResourceProvider -NameSpace 'SecurityInsights' } else { Write-Output "No change have been." break } } $body = @{ 'id' = '' 'etag' = '' 'name' = '' 'type' = '' 'location' = $workspaceResult.location 'properties' = @{ 'workspaceResourceId' = $workspaceResult.id } 'plan' = @{ 'name' = 'SecurityInsights($workspace)' 'publisher' = 'Microsoft' 'product' = 'OMSGallery/SecurityInsights' 'promotionCode' = '' } } $uri = "$(($Script:baseUri).Split('microsoft.operationalinsights')[0])Microsoft.OperationsManagement/solutions/SecurityInsights($WorkspaceName)?api-version=2015-11-01-preview" try { $solutionResult = Invoke-webrequest -Uri $uri -Method Get -Headers $script:authHeader Write-Output "Azure Sentinel is already enabled on $WorkspaceName and status is: $($solutionResult.StatusDescription)" } catch { $errorReturn = $_ $errorResult = ($errorReturn | ConvertFrom-Json ).error if ($errorResult.Code -eq 'ResourceNotFound') { Write-Output "Azure Sentinetal is not enabled on workspace: $($WorkspaceName)" try { if ($PSCmdlet.ShouldProcess("Do you want to enable Sentinel for Workspace: $workspace")) { $result = Invoke-webrequest -Uri $uri -Method Put -Headers $script:authHeader -Body ($body | ConvertTo-Json) Write-Output "Successfully enabled Sentinel on workspae: $WorkspaceName with result code $($result.StatusDescription)" } else { Write-Output "No change have been made for $WorkspaceName, deployment aborted" break } } catch { Write-Verbose $_ Write-Error "Unable to enable Sentinel on $WorkspaceName with error message: $($_.Exception.Message)" } } else { Write-Verbose $_ Write-Error "Unable to Azure Sentinel with error message: $($_.Exception.Message)" -ErrorAction Stop } } } else { Write-Error "Workspace $WorkspaceName is currently in $($workspaceResult.properties.provisioningState) status, setup canceled" } } } function Update-AzSentinelIncident { <# .SYNOPSIS Update Azure Sentinel Incident .DESCRIPTION With this function you can update existing Azure Sentinel Incident. .PARAMETER SubscriptionId Enter the subscription ID, if no subscription ID is provided then current AZContext subscription will be used .PARAMETER WorkspaceName Enter the Workspace name .PARAMETER CaseNumber Enter the case number to get specfiek details of a open case .PARAMETER Severity Enter the Severity, you can choose from Medium, High, Low and Informational .PARAMETER Status Enter the Status of the incident, you can choose from New, InProgress and Closed .PARAMETER Comment Enter Comment tekst to add comment to the incident .PARAMETER Labels Add Lebels to the incident, current configured Labels will be added to the existing Labels .PARAMETER CloseReason When Status is equil to Closed, CloseReason is required. You can select from: TruePositive, FalsePositive .PARAMETER ClosedReasonText When Status is equil to Closed, ClosedReasonText is required to be filled in. .EXAMPLE Update-AzSentinelIncident -WorkspaceName "" Get a list of all open Incidents .EXAMPLE Update-AzSentinelIncident -WorkspaceName '' -CaseNumber 42291 -Labels "NewLabel" Add a new Label to list of Labels for a Incident .EXAMPLE Update-AzSentinelIncident -WorkspaceName '' -CaseNumber 42293 -Status Closed -CloseReason FalsePositive -ClosedReasonText "Your input" Close the Incidnet using status Closed, when status closed is selected then CloseReason and ClosedReasonText prperty are required to be filled in #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $false, ParameterSetName = "Sub")] [ValidateNotNullOrEmpty()] [string] $SubscriptionId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$WorkspaceName, [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [int]$CaseNumber, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Severity, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [Status]$Status, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Comment, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string[]]$Labels, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [CloseReason]$CloseReason, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$ClosedReasonText, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Description ) begin { precheck } process { switch ($PsCmdlet.ParameterSetName) { Sub { $arguments = @{ WorkspaceName = $WorkspaceName SubscriptionId = $SubscriptionId } } default { $arguments = @{ WorkspaceName = $WorkspaceName } } } Write-Verbose -Message "Using URI: $($uri)" try { $incident = Get-AzSentinelIncident @arguments -CaseNumber $CaseNumber -ErrorAction Stop } catch { Write-Error $_.Exception.Message break } if ($incident) { if ($Comment) { $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/Cases/$($incident.name)/comments/$(New-Guid)?api-version=2019-01-01-preview" $body = @{ "properties" = @{ "message" = $Comment } } } else { $uri = "$script:baseUri/providers/Microsoft.SecurityInsights/Cases/$($incident.name)?api-version=2019-01-01-preview" $LabelsUnique = $incident.labels + $Labels | Select-Object -Unique $body = @{ "etag" = $($incident.etag) "properties" = @{ "caseNumber" = $CaseNumber "createdTimeUtc" = $($incident.incidentcreatedTimeUtc) "endTimeUtc" = $($incident.endTimeUtc) "lastUpdatedTimeUtc" = $($incident.lastUpdatedTimeUtc) "lastComment" = "" "totalComments" = $incident.TotalComments "metrics" = $incident.Metrics "relatedAlertIds" = $incident.RelatedAlertIds "relatedAlertProductNames" = $incident.RelatedAlertProductNames "severity" = if ($Severity) { $Severity } else { $incident.severity } "startTimeUtc" = $($incident.startTimeUtc) "status" = if ($Status) { $Status } else { $incident.status } "closeReason" = if ($Status -eq 'Closed') { if ($null -ne [CloseReason]$CloseReason) { $CloseReason } else { Write-Error "No close reason provided" -ErrorAction Stop } } else { $null } "closedReasonText" = if ($Status -eq 'Closed') { if ($ClosedReasonText) { $ClosedReasonText } else { Write-Error 'No closed comment provided' } } else { $null } [pscustomobject]"labels" = @( $LabelsUnique) "title" = $($incident.title) "description" = if ($Description) { $Description } else { $incident.Description } "firstAlertTimeGenerated" = $incident.FirstAlertTimeGenerated "lastAlertTimeGenerated" = $incident.LastAlertTimeGenerated "owner" = @{ "name" = $incident.Owner.Name "email" = $incident.Owner.Email "objectId" = $incident.Owner.ObjectId } } } } Write-Output "Found incident with case number: $($incident.caseNumber)" if ($PSCmdlet.ShouldProcess("Do you want to update Incident: $($body.Properties.DisplayName)")) { try { $return = Invoke-WebRequest -Uri $uri -Method Put -Body ($body | ConvertTo-Json -Depth 99 -EnumsAsStrings) -Headers $script:authHeader return ($return.Content | ConvertFrom-Json).properties } catch { $return = $_.Exception.Message Write-Verbose $_ Write-Error "Unable to update Incident $($incident.caseNumber) with error message $return" return $return } } else { Write-Output "No change have been made for Incident $($incident.caseNumber), update aborted" } } } } |