├── Create-AnalyticRulesFromTemplates.ps1 ├── IngestionPerTablePerDay.kql ├── CfS ├── AzureSubscriptions-Specification.yaml ├── LogAnalytics-Specification.yaml ├── SOC-Optimization-Specification.yaml └── SOC-Optimization-Manifest.yaml ├── export-incident-to-event-hub ├── readme.md └── template.json ├── README.md ├── VerifyConditionalAccessImpact ├── IngestionPerTablePerDayPerUserOnSpecificTables.kql ├── incident-lifecycle-automation └── fixed-state-diagram │ ├── readme.md │ └── incident-lifecycle-NewIncidentFlow-azuredeploy.json ├── SentinelAnalyticRulesMassiveCreationScript.ps1 └── SentinelAnalyticRulesManagementScript.ps1 /Create-AnalyticRulesFromTemplates.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Moved to https://github.com/stefanpems/sentinel-utilities/blob/main/SentinelAnalyticRulesMassiveCreationScript.ps1 3 | #> 4 | -------------------------------------------------------------------------------- /IngestionPerTablePerDay.kql: -------------------------------------------------------------------------------- 1 | let total_days = 30d; //--> Change as needed 2 | let startdate = now(-total_days); 3 | let enddate = now(); //--> Change as needed 4 | let total_days_num = todouble(datetime_diff('day', enddate, startdate)); 5 | Usage 6 | | where TimeGenerated between (startdate .. enddate) 7 | | summarize BillableDataMB = sum(Quantity) by DataType, bin(TimeGenerated, 1d) 8 | | project TimeGenerated, DataType, BillableDataMB 9 | | render timechart 10 | 11 | -------------------------------------------------------------------------------- /CfS/AzureSubscriptions-Specification.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | title: Azure Subscriptions APIs 5 | description: Skills for retrieving the list of Azure Subscriptions and the details of each single Azure Subscription 6 | version: "0.0.1" 7 | 8 | paths: 9 | /subscriptions?api-version=2016-06-01: 10 | get: 11 | operationId: ListSubscriptions 12 | summary: Gets a list of all Microsoft Azure Subscriptions. 13 | responses: 14 | 200: 15 | description: Successful authentication. 16 | 401: 17 | description: Unsuccessful authentication. -------------------------------------------------------------------------------- /CfS/LogAnalytics-Specification.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | title: Azure Log Analytics APIs 5 | description: Skills for retrieving the list of Azure Log Analytics and the details of each single Azure Log Analytics workspace 6 | version: "0.0.1" 7 | 8 | paths: 9 | /workspaces?api-version=2023-09-01: 10 | get: 11 | operationId: ListLogAnalytics 12 | summary: Gets a list of all Microsoft Log Analytics workspaces. 13 | responses: 14 | 200: 15 | description: Successful authentication. 16 | 401: 17 | description: Unsuccessful authentication. -------------------------------------------------------------------------------- /CfS/SOC-Optimization-Specification.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | title: Sentinel SOC Optimization 5 | description: Skills to retrieve the recommendations provided by the "SOC Optimization" feature in Microsoft Sentinel. 6 | version: "0.0.1" 7 | 8 | paths: 9 | /recommendations?api-version=2024-01-01-preview: 10 | get: 11 | operationId: GetRecommendations 12 | summary: Gets a list of all recommendations made available by the "SOC Optimization" experience in Microsoft Sentinel. 13 | responses: 14 | 200: 15 | description: Successful authentication. 16 | 401: 17 | description: Unsuccessful authentication. -------------------------------------------------------------------------------- /export-incident-to-event-hub/readme.md: -------------------------------------------------------------------------------- 1 | This Logic App exports the triggering Sentinel incident to Event Hub. Before the export, the Logic App adds into the JSON the URL of the incident in the new Unified portal. The JSON of the incident includes the information related to the alerts and the entities of the incident. 2 | 3 | All the details on how to deploy, configure and test this Logic App can be found here: https://www.linkedin.com/pulse/simple-logic-app-export-sentinel-incidents-event-hub-pescosolido-qs7wf/ 4 | 5 | ## Deployment button 6 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fstefanpems%2Fsentinel-utilities%2Frefs%2Fheads%2Fmain%2Fexport-incident-to-event-hub%2Ftemplate.json) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | My stuff for Microsoft Sentinel. 2 | 3 | 𝐒𝐞𝐧𝐭𝐢𝐧𝐞𝐥𝐀𝐧𝐚𝐥𝐲𝐭𝐢𝐜𝐑𝐮𝐥𝐞𝐬𝐌𝐚𝐧𝐚𝐠𝐞𝐦𝐞𝐧𝐭𝐒𝐜𝐫𝐢𝐩𝐭.𝐩𝐬1 is a script containing cmdlets that automates the massive creation, backup, deletion and update of Analytic Rules in Microsoft Sentinel. 4 | Ideas for its improvement / evolution: 5 | 1. Change the authentication flow (do not use Device Code flow) 6 | 2. Export as json ARM template files any kind of rule - Not only the rules related to the templates installed from Content Hub solutions 7 | 3. Restore rules from their json ARM template files 8 | 4. Update installed solutions in Content Hub 9 | 5. Install specified solutions in Content Hub 10 | 11 | ... 12 | 13 | 𝐕𝐞𝐫𝐢𝐟𝐲𝐂𝐨𝐧𝐝𝐢𝐭𝐢𝐨𝐧𝐚𝐥𝐀𝐜𝐜𝐞𝐬𝐬𝐈𝐦𝐩𝐚𝐜𝐭 is a KQL query to list which Conditional Access Policies in "Report-only" mode would have forced MFA or blocked the sign-ins if they were set to "On". 14 | It requires the SigninLogs from Microsoft Entra to be collected in Sentinel 15 | -------------------------------------------------------------------------------- /VerifyConditionalAccessImpact: -------------------------------------------------------------------------------- 1 | // This KQL query lists which Conditional Access Policies in "Report-only" mode would have forced MFA or blocked the sign-ins if they were set to "On". 2 | // It requires the SigninLogs from Microsoft Entra to be collected in Sentinel 3 | 4 | SigninLogs 5 | | where tostring(parse_json(Status).errorCode) == 0 //Select only the successful sign-ins 6 | | mv-expand ConditionalAccessPolicies 7 | | extend caPolicyResult = tostring(parse_json(ConditionalAccessPolicies).result) 8 | | where caPolicyResult == "reportOnlyFailure" or caPolicyResult == "reportOnlyInterrupted" // they would have been blocked or impacted (MFA) 9 | | extend caPolicyDisplayName = tostring(parse_json(ConditionalAccessPolicies).displayName) 10 | | summarize count() by caPolicyDisplayName, caPolicyResult 11 | | order by count_ 12 | 13 | // Remove the last 2 lines above and uncomment the following one if you want to see the details of each identified event 14 | //| project TimeGenerated, caPolicyDisplayName, UserDisplayName, UserPrincipalName, Location, IPAddress, AppDisplayName, ConditionalAccessStatus, IsInteractive, RiskLevelAggregated, RiskDetail, DeviceDetail, ResultType, ResultDescription, Status 15 | -------------------------------------------------------------------------------- /IngestionPerTablePerDayPerUserOnSpecificTables.kql: -------------------------------------------------------------------------------- 1 | let total_days = 30d; //--> Change as needed 2 | let startdate = now(-total_days); 3 | let enddate = now(); //--> Change as needed 4 | let total_days_num = todouble(datetime_diff('day', enddate, startdate)); 5 | let number_of_entra_users = toscalar( 6 | SigninLogs | where TimeGenerated between (startdate .. enddate) | distinct UserPrincipalName | count 7 | ); 8 | let number_of_office_users = toscalar( 9 | OfficeActivity | where TimeGenerated between (startdate .. enddate) | distinct UserId | count 10 | ); 11 | Usage 12 | | where TimeGenerated between (startdate .. enddate) 13 | | where DataType in ("AADManagedIdentitySignInLogs", 14 | "AADNonInteractiveUserSignInLogs", 15 | "AADProvisioningLogs", 16 | "AADRiskyUsers", 17 | "AADServicePrincipalSignInLogs", 18 | "AADUserRiskEvents", 19 | "ADFSSignInLogs", 20 | "AuditLogs", 21 | "O365API_CL", 22 | "OfficeActivity", 23 | "SigninLogs", 24 | "EmailAttachmentInfo", 25 | "EmailEvents", 26 | "EmailPostDeliveryEvents", 27 | "EmailUrlInfo")   28 | | summarize BillableDataMB = sum(Quantity) by DataType, bin(TimeGenerated, 1d) 29 | | summarize AvgDailyIngestionKB = avg(BillableDataMB * 1024) by DataType 30 | | extend AvgDailyIngestionKBperUser = round(AvgDailyIngestionKB/todouble(number_of_entra_users),0) 31 | | extend Scope = iif(DataType in ("AADManagedIdentitySignInLogs", 32 | "AADNonInteractiveUserSignInLogs", 33 | "AADProvisioningLogs", 34 | "AADRiskyUsers", 35 | "AADServicePrincipalSignInLogs", 36 | "AADUserRiskEvents", 37 | "ADFSSignInLogs", 38 | "AuditLogs", 39 | "SigninLogs"), 40 | "Entra",iif(DataType in ("O365API_CL", 41 | "OfficeActivity"), 42 | "O365",iif(DataType in ("EmailAttachmentInfo", 43 | "EmailEvents", 44 | "EmailPostDeliveryEvents", 45 | "EmailUrlInfo"), 46 | "MDO","n.a."))) 47 | | project Scope, DataType, AvgDailyIngestionKBperUser 48 | //| summarize AvgDailyIngestionKBperUserPerScope = sum(AvgDailyIngestionKBperUser) by Scope 49 | 50 | -------------------------------------------------------------------------------- /CfS/SOC-Optimization-Manifest.yaml: -------------------------------------------------------------------------------- 1 | Descriptor: 2 | Name: Microsoft Sentinel SOC Optimization 3 | DisplayName: Microsoft Sentinel SOC Optimization (Community release) 4 | Description: SOC Optimization is a feature of Microsoft Sentinel that provides three kind of recommendations. 5 | Firstly, it provides daily adaptive strategies for optimal data utilization and attack detection. 6 | Secondly, it provides actionable insights into data usage patterns for threat protection and cost optimization. 7 | Lastly, it provides threat-based recommendations using the MITRE ATT&CK framework. 8 | The following are examples of recommendations provided by "SOC Optimization" 9 | "Low usage of table (Table wasn't queried in the last 30 days)" 10 | "Coverage improvement against AiTM (Adversary in the Middle) 11 | "Coverage improvement against BEC (Financial Fraud) 12 | "Coverage improvement against ERP (SAP) Financial Process Manipulation" 13 | "Coverage improvement against BEC (Mass Credential Harvest)" 14 | "Coverage improvement against Human Operated Ransomware" 15 | "Coverage improvement against IaaS Resource Theft" 16 | DescriptionForModel: Skills for getting a GET REST API call reflection. Uses ReflectionData as operationId 17 | Settings: 18 | - Name: SentinelApiUrl 19 | Label: Microsoft Sentinel APIs URL 20 | Description: The URL of the Microsoft Sentinel APIs. Specify your subscriptionId, resourceGroupName and workspaceName. 21 | HintText: "Set your subscriptionId, resourceGroupName and workspaceName" 22 | DefaultValue: "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces//providers/Microsoft.SecurityInsights" 23 | SettingType: String 24 | Required: true 25 | Authorization: 26 | #Type: OAuthAuthorizationCodeFlow 27 | Type: OAuthClientCredentialsFlow 28 | ClientId: f0b91c6e-a082-4be9-9d27-7266265c83d5 # 29 | TokenEndpoint: https://login.microsoftonline.com/common/oauth2/authorize 30 | Scopes: user_impersonation 31 | AuthorizationContentType: application/x-www-form-urlencoded 32 | SkillGroups: 33 | - Format: API 34 | Settings: 35 | # Replace this with your own URL where the OpenAPI spec file is located. 36 | OpenApiSpecUrl: http://172.13.112.25:5000/file/API_Plugin_Reflection_OAI_GET_Simple_Header.yaml 37 | EndpointUrlSettingName: SentinelApiUrl 38 | -------------------------------------------------------------------------------- /incident-lifecycle-automation/fixed-state-diagram/readme.md: -------------------------------------------------------------------------------- 1 | # Incident Lifecycle Automation - Fixed State Diagram - Sample Prototype 2 | 3 | This folder contains a prototype of incident lifecycle automation for Sentinel incidents. Specifically, the folder contains 2 Workflow templates (Azure Logic App) to implement the automation described in [Incident Lifecycle Automation in the Microsoft Unified Security Operations Platform](https://www.linkedin.com/pulse/incident-lifecycle-automation-microsoft-unified-stefano-pescosolido-yro9f/) 4 | 5 | This is an automation based on a fixed state diagram, as described in the article. 6 | 7 | ## Installation Steps 8 | 9 | * If not already available, create 3 Entra groups representing SOC Tiers 3, 2, and 1. 10 | 11 | NOTE: In this implementation, Tier 3 is considered the entry level of the SOC, i.e., the lowest in the hierarchy. Escalation proceeds to Tier 2 and then Tier 1. If you wish to change the numerical order of escalation for the 3 tiers, you must modify the logic app `UpdatedInvestigationFlow`, specifically the switch condition on the current Tier in the logical branch related to the escalate action. 12 | 13 | * Prepare the SharePoint list used as storage for investigations. Columns: 14 | - Title (Single line of text) 15 | - Main Incident (Hyperlink or Picture) 16 | - Status (Choice - Possible values: New, Assigned, Closed) 17 | - Tier (Choice - Possible values: T3, T2, T1) 18 | - Assigned to (Person or Group) 19 | - Tasks (Lookup - Allow Multiple Choice) 20 | - Action (Choice - Possible values: None, Assign, Escalate, Close, Reopen) 21 | - Notes (Multiple lines of text) 22 | - Incident ARM ID (Single line of text) 23 | - Classification (Choice - Possible values: BenignPositive - SuspiciousButExpected, FalsePositive - InaccurateData, FalsePositive - IncorrectAlertLogic, TruePositive - SuspiciousActivity, Undetermined) 24 | 25 | NOTES: 26 | - The Tasks column of type Lookup must point to the Tasks list. Therefore, it can only be created after the Tasks list has been created. 27 | - For the colums of type 'Choice' use exactly the values listed here above. If you want to change some of them, you also need to modify the Azure Logic Apps accordingly. 28 | - In this list, create 3 custom views filtering by Status not equal to Closed and Tier equal to T3, T2, and T1 respectively. Name these views T3, T2, and T1. The corresponding URL must be saved as the access URL to the investigations list for Tier 3, 2, and 1 operators respectively. 29 | - You may, if desired, change the field order as they appear in the T3, T2, and T1 views and in the data entry forms. 30 | - It is possible and recommended to modify the data display form (Configure Layout / Body) so that the fields Status, Title, Main Incident, and Incident ARM ID are read-only. 31 | 32 | * In the same SharePoint site collection, prepare the SharePoint list used as storage for tasks. Columns: 33 | - Title (Single line of text) 34 | - Assigned to (Person or Group) 35 | - Due date (Date and Time) 36 | - Instructions (Multiple lines of text) 37 | - Notes (Multiple lines of text) 38 | - Completed (Yes/No) 39 | - Investigation (Lookup) 40 | 41 | NOTES: 42 | - The Investigation column of type Lookup must point to the Investigations list. Therefore, it can only be created after the Investigations list has been created. 43 | - In this list, create 3 custom views filtering by appropriate criteria. 44 | - You may, if desired, change the field order as they appear in the views and in the data entry forms. 45 | 46 | * Install the two logic apps using the templates published here - Use these two links: 47 | 48 | 1. [![Deploy NewIncidentFlow to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fstefanpems%2Fsentinel-utilities%2Frefs%2Fheads%2Fmain%2Fincident-lifecycle-automation%2Ffixed-state-diagram%2Fincident-lifecycle-NewIncidentFlow-azuredeploy.json) 49 | 50 | 2. [![Deploy UpdatedInvestigationFlow to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fstefanpems%2Fsentinel-utilities%2Frefs%2Fheads%2Fmain%2Fincident-lifecycle-automation%2Ffixed-state-diagram%2Fincident-lifecycle-UpdatedInvestigationFlow-azuredeploy.json) 51 | 52 | NOTE: In the deployment parameters of the two logic apps, use the references to the groups and lists created as described above. 53 | 54 | * For each of the two Azure Logic Apps just deployed: 55 | - Assign the corresponding Managed Identity the role of Microsoft Sentinel Responder on the resource group or Sentinel workspace (go to "Identity" Azure role assignments" / "Add role assignment" / select Scope = Resource group", select the subscription & resource group used for Sentinel, select Role = "Microsoft Sentinel Responder") 56 | - Authorize the API connection to Office 365 / SharePoint Online (go to "API connections" / "Sharepointonline-" / Edit API connection / Authorize / authenticate with credentials that are members on both Lists; do not forget to save) 57 | - Edit the workflow and verify the correctness of the parameters (they have the values specified during the deployment); modify their values if necessary. 58 | 59 | * Authorize Sentinel / Defender XDR on the Resource Group of the newly deployed Logic App "NewIncidentFlow": in Defender XDR (with Sentinel integrated), selct an incidet, go to "Run playbook", go to the bottom of the list, identify the newly deployed Logic App "NewIncidentFlow", click on "Grant permissions". 60 | 61 | * Test the solution by manually running the newly deployed Logic App "NewIncidentFlow": in Defender XDR (with Sentinel integrated), selct an incidet, go to "Run playbook", identify the newly deployed Logic App "NewIncidentFlow", click on "Run playbook". Go to the list of the investigations and check for the existance of the investigation created by the workflow. Proceed as shown in the demo (see the reference article in LinkedIn). 62 | 63 | * In Defender XDR / Sentinel, if desired, create an Automation Rule to run the playbook automatically on new incidents. 64 | -------------------------------------------------------------------------------- /incident-lifecycle-automation/fixed-state-diagram/incident-lifecycle-NewIncidentFlow-azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "title": "NewIncidentFlow", 6 | "description": "This workflow can be launched manually or automatically on new incident created in Sentinel. It creates the corresponding investigation list and two sample tasks in the related SharePoint lists", 7 | "prerequisites": "SharePoint lists for storing Investigations and Tasks", 8 | "postDeployment": [ 9 | "Assign the role of Microsoft Sentinel Reponder to the workflow's managed identity at the Sentinel resource group or workspace level", 10 | "Autorize the API for the connection to SharePoint", 11 | "Set the workflow parameters", 12 | "Authorize Sentinel/Defender XDR on the Resource Group of the Logic App", 13 | "If desired, create an Automation Rule to run the playbook automatically" 14 | ], 15 | "prerequisitesDeployTemplateFile": "", 16 | "lastUpdateTime": "", 17 | "entities": [ 18 | ], 19 | "tags": [ 20 | ], 21 | "support": { 22 | "tier": "community", 23 | "armtemplate": "Generated from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Playbook-ARM-Template-Generator" 24 | }, 25 | "author": { 26 | "name": "Stefano Pescosolido - https://www.linkedin.com/in/stefanopescosolido/" 27 | } 28 | }, 29 | "parameters": { 30 | "PlaybookName": { 31 | "defaultValue": "NewIncidentFlow", 32 | "type": "string" 33 | }, 34 | "T3GroupID": { 35 | "type": "String", 36 | "metadata": { 37 | "description": "Enter the Object GUID for the Entra Group representing Tier 3 - NOTE: in this implementation, Tier 3 is the entry level (lower in hierarchy)" 38 | } 39 | }, 40 | "SharePointSiteUrl": { 41 | "type": "String", 42 | "metadata": { 43 | "description": "Enter the URL of the SharePoint site collection hosting the lists for storing Investigations and Tasks" 44 | } 45 | }, 46 | "SharePointListNameForTasks": { 47 | "type": "String", 48 | "metadata": { 49 | "description": "Enter name of the SharePoint list for storing Tasks" 50 | } 51 | }, 52 | "SharePointListNameForInvestigations": { 53 | "type": "String", 54 | "metadata": { 55 | "description": "Enter name of the SharePoint list for storing Investigations" 56 | } 57 | } 58 | }, 59 | "variables": { 60 | "MicrosoftSentinelConnectionName": "[concat('MicrosoftSentinel-', parameters('PlaybookName'))]", 61 | "SharepointonlineConnectionName": "[concat('Sharepointonline-', parameters('PlaybookName'))]" 62 | }, 63 | "resources": [ 64 | { 65 | "properties": { 66 | "provisioningState": "Succeeded", 67 | "state": "Enabled", 68 | "definition": { 69 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 70 | "contentVersion": "1.0.0.0", 71 | "parameters": { 72 | "$connections": { 73 | "defaultValue": { 74 | }, 75 | "type": "Object" 76 | }, 77 | "T3GroupID": { 78 | "defaultValue": "[parameters('T3GroupID')]", 79 | "type": "String" 80 | }, 81 | "SharePointSiteUrl": { 82 | "defaultValue": "[parameters('SharePointSiteUrl')]", 83 | "type": "String" 84 | }, 85 | "SharePointListNameForTasks": { 86 | "defaultValue": "[parameters('SharePointListNameForTasks')]", 87 | "type": "String" 88 | }, 89 | "SharePointListNameForInvestigations": { 90 | "defaultValue": "[parameters('SharePointListNameForInvestigations')]", 91 | "type": "String" 92 | } 93 | }, 94 | "triggers": { 95 | "Microsoft_Sentinel_incident": { 96 | "type": "ApiConnectionWebhook", 97 | "inputs": { 98 | "host": { 99 | "connection": { 100 | "name": "@parameters('$connections')['azuresentinel']['connectionId']" 101 | } 102 | }, 103 | "body": { 104 | "callback_url": "@listCallbackUrl()" 105 | }, 106 | "path": "/incident-creation" 107 | } 108 | } 109 | }, 110 | "actions": { 111 | "Create_investigation": { 112 | "runAfter": { 113 | "Create_fake_task_T3.2": [ 114 | "Succeeded" 115 | ] 116 | }, 117 | "type": "ApiConnection", 118 | "inputs": { 119 | "host": { 120 | "connection": { 121 | "name": "@parameters('$connections')['sharepointonline']['connectionId']" 122 | } 123 | }, 124 | "method": "post", 125 | "body": { 126 | "Title": "Incident: @{triggerBody()?['object']?['properties']?['providerIncidentId']} - @{formatDateTime(addHours(utcNow(),2),'yyyy.MM.dd HH:mm')} (Initial title: @{triggerBody()?['object']?['properties']?['title']})", 127 | "Status": { 128 | "Value": "New" 129 | }, 130 | "Tier": { 131 | "Value": "T3" 132 | }, 133 | "Assigned_x0020_to": { 134 | "Claims": "i:0#.f|membership|@{parameters('T3GroupID')}" 135 | }, 136 | "Tasks": [ 137 | { 138 | "Id": "@body('Create_fake_task_T3.1')?['ID']" 139 | }, 140 | { 141 | "Id": "@body('Create_fake_task_T3.2')?['ID']" 142 | } 143 | ], 144 | "Main_x0020_Incident": "https://security.microsoft.com/incident2/@{triggerBody()?['object']?['properties']?['providerIncidentId']}", 145 | "Notes": "\u003cp class=\"editor-paragraph\"\u003eIncident created. Summary by Security Copilot:\u003c/p\u003e\u003cp class=\"editor-paragraph\"\u003e\u0026lt;to-be-added-here\u0026gt;\u003c/p\u003e", 146 | "Incident_x0020_ARM_x0020_ID": "@triggerBody()?['object']?['id']" 147 | }, 148 | "path": "/datasets/@{encodeURIComponent(encodeURIComponent(parameters('SharePointSiteUrl')))}/tables/@{encodeURIComponent(encodeURIComponent(parameters('SharePointListNameForInvestigations')))}/items" 149 | } 150 | }, 151 | "Create_fake_task_T3.1": { 152 | "runAfter": { 153 | }, 154 | "type": "ApiConnection", 155 | "inputs": { 156 | "host": { 157 | "connection": { 158 | "name": "@parameters('$connections')['sharepointonline']['connectionId']" 159 | } 160 | }, 161 | "method": "post", 162 | "body": { 163 | "Title": "Task T3.1 - Incident: @{triggerBody()?['object']?['properties']?['providerIncidentId']} - Created: @{formatDateTime(addHours(utcNow(),2),'yyyy.MM.dd HH:mm')}", 164 | "Assigned_x0020_to": { 165 | "Claims": "i:0#.f|membership|@{parameters('T3GroupID')}" 166 | }, 167 | "Due_x0020_date": "@addDays(utcNow(),7)", 168 | "Instructions": "\u003cp class=\"editor-paragraph\"\u003eLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\u0026nbsp;\u003c/p\u003e\u003cul class=\"editor-list-ul\"\u003e\u003cli class=\"editor-listitem\"\u003e\u003cb\u003e\u003cstrong class=\"editor-text-bold\"\u003eFirst\u003c/strong\u003e\u003c/b\u003e item\u003c/li\u003e\u003cli class=\"editor-listitem\"\u003e\u003ci\u003e\u003cem class=\"editor-text-italic\"\u003eSecond\u003c/em\u003e\u003c/i\u003e item\u003c/li\u003e\u003cli class=\"editor-listitem\"\u003e\u003ci\u003e\u003cb\u003e\u003cstrong class=\"editor-text-bold editor-text-italic\"\u003eThird\u003c/strong\u003e\u003c/b\u003e\u003c/i\u003e item\u003c/li\u003e\u003c/ul\u003e\u003cp class=\"editor-paragraph\"\u003eUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\u003c/p\u003e" 169 | }, 170 | "path": "/datasets/@{encodeURIComponent(encodeURIComponent(parameters('SharePointSiteUrl')))}/tables/@{encodeURIComponent(encodeURIComponent(parameters('SharePointListNameForTasks')))}/items" 171 | } 172 | }, 173 | "Create_fake_task_T3.2": { 174 | "runAfter": { 175 | "Create_fake_task_T3.1": [ 176 | "Succeeded" 177 | ] 178 | }, 179 | "type": "ApiConnection", 180 | "inputs": { 181 | "host": { 182 | "connection": { 183 | "name": "@parameters('$connections')['sharepointonline']['connectionId']" 184 | } 185 | }, 186 | "method": "post", 187 | "body": { 188 | "Title": "Task T3.2 - Incident: @{triggerBody()?['object']?['properties']?['providerIncidentId']} - Created: @{formatDateTime(addHours(utcNow(),2),'yyyy.MM.dd HH:mm')}", 189 | "Assigned_x0020_to": { 190 | "Claims": "i:0#.f|membership|@{parameters('T3GroupID')}" 191 | }, 192 | "Due_x0020_date": "@addDays(utcNow(),7)", 193 | "Completed": false, 194 | "Instructions": "\u003cp class=\"editor-paragraph\"\u003eDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\u003c/p\u003e" 195 | }, 196 | "path": "/datasets/@{encodeURIComponent(encodeURIComponent(parameters('SharePointSiteUrl')))}/tables/@{encodeURIComponent(encodeURIComponent(parameters('SharePointListNameForTasks')))}/items" 197 | } 198 | }, 199 | "Update_incident_as_Active": { 200 | "runAfter": { 201 | "Update_fake_task_T3.2": [ 202 | "Succeeded" 203 | ] 204 | }, 205 | "type": "ApiConnection", 206 | "inputs": { 207 | "host": { 208 | "connection": { 209 | "name": "@parameters('$connections')['azuresentinel']['connectionId']" 210 | } 211 | }, 212 | "method": "put", 213 | "body": { 214 | "incidentArmId": "@triggerBody()?['object']?['id']", 215 | "ownerAction": "Assign", 216 | "owner": "@parameters('T3GroupID')", 217 | "status": "Active" 218 | }, 219 | "path": "/Incidents" 220 | } 221 | }, 222 | "Update_fake_task_T3.1": { 223 | "runAfter": { 224 | "Create_investigation": [ 225 | "Succeeded" 226 | ] 227 | }, 228 | "type": "ApiConnection", 229 | "inputs": { 230 | "host": { 231 | "connection": { 232 | "name": "@parameters('$connections')['sharepointonline']['connectionId']" 233 | } 234 | }, 235 | "method": "patch", 236 | "body": { 237 | "Completed": false, 238 | "Investigation": { 239 | "Id": "@body('Create_investigation')?['ID']" 240 | } 241 | }, 242 | "path": "/datasets/@{encodeURIComponent(encodeURIComponent(parameters('SharePointSiteUrl')))}/tables/@{encodeURIComponent(encodeURIComponent(parameters('SharePointListNameForTasks')))}/items/@{encodeURIComponent(body('Create_fake_task_T3.1')?['ID'])}" 243 | } 244 | }, 245 | "Update_fake_task_T3.2": { 246 | "runAfter": { 247 | "Update_fake_task_T3.1": [ 248 | "Succeeded" 249 | ] 250 | }, 251 | "type": "ApiConnection", 252 | "inputs": { 253 | "host": { 254 | "connection": { 255 | "name": "@parameters('$connections')['sharepointonline']['connectionId']" 256 | } 257 | }, 258 | "method": "patch", 259 | "body": { 260 | "Completed": false, 261 | "Investigation": { 262 | "Id": "@body('Create_investigation')?['ID']" 263 | } 264 | }, 265 | "path": "/datasets/@{encodeURIComponent(encodeURIComponent(parameters('SharePointSiteUrl')))}/tables/@{encodeURIComponent(encodeURIComponent(parameters('SharePointListNameForTasks')))}/items/@{encodeURIComponent(body('Create_fake_task_T3.2')?['ID'])}" 266 | } 267 | } 268 | }, 269 | "outputs": { 270 | } 271 | }, 272 | "parameters": { 273 | "$connections": { 274 | "value": { 275 | "azuresentinel": { 276 | "connectionId": "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", 277 | "connectionName": "[variables('MicrosoftSentinelConnectionName')]", 278 | "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/Azuresentinel')]", 279 | "connectionProperties": { 280 | "authentication": { 281 | "type": "ManagedServiceIdentity" 282 | } 283 | } 284 | }, 285 | "sharepointonline": { 286 | "connectionId": "[resourceId('Microsoft.Web/connections', variables('SharepointonlineConnectionName'))]", 287 | "connectionName": "[variables('SharepointonlineConnectionName')]", 288 | "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/Sharepointonline')]" 289 | } 290 | } 291 | } 292 | } 293 | }, 294 | "name": "[parameters('PlaybookName')]", 295 | "type": "Microsoft.Logic/workflows", 296 | "location": "[resourceGroup().location]", 297 | "identity": { 298 | "type": "SystemAssigned" 299 | }, 300 | "tags": { 301 | "hidden-SentinelTemplateName": "NewIncidentFlow", 302 | "hidden-SentinelTemplateVersion": "1.0" 303 | }, 304 | "apiVersion": "2017-07-01", 305 | "dependsOn": [ 306 | "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", 307 | "[resourceId('Microsoft.Web/connections', variables('SharepointonlineConnectionName'))]" 308 | ] 309 | }, 310 | { 311 | "type": "Microsoft.Web/connections", 312 | "apiVersion": "2016-06-01", 313 | "name": "[variables('MicrosoftSentinelConnectionName')]", 314 | "location": "[resourceGroup().location]", 315 | "kind": "V1", 316 | "properties": { 317 | "displayName": "[variables('MicrosoftSentinelConnectionName')]", 318 | "customParameterValues": { 319 | }, 320 | "parameterValueType": "Alternative", 321 | "api": { 322 | "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/Azuresentinel')]" 323 | } 324 | } 325 | }, 326 | { 327 | "type": "Microsoft.Web/connections", 328 | "apiVersion": "2016-06-01", 329 | "name": "[variables('SharepointonlineConnectionName')]", 330 | "location": "[resourceGroup().location]", 331 | "kind": "V1", 332 | "properties": { 333 | "displayName": "[variables('SharepointonlineConnectionName')]", 334 | "customParameterValues": { 335 | }, 336 | "api": { 337 | "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/Sharepointonline')]" 338 | } 339 | } 340 | } 341 | ] 342 | } 343 | -------------------------------------------------------------------------------- /export-incident-to-event-hub/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "title": "Export a Sentinel incident to Event Hub", 6 | "description": "This Logic App exports the triggering Sentinel incident to Event Hub. Before the export, the Logic App adds into the JSON the URL of the incident in the new Unified portal. The JSON of the incident includes the information related to the alerts and the entities of the incident", 7 | "prerequisites": "", 8 | "postDeployment": ["1. Authorize Microsoft Sentinel on the resource group of the Logic App.", "2. Authorize the Managed Identity of the Logic App to send data to Event Hub."], 9 | "prerequisitesDeployTemplateFile": "", 10 | "lastUpdateTime": "", 11 | "entities": [], 12 | "tags": [], 13 | "author": { 14 | "name": "Stefano Pescosolido" 15 | } 16 | }, 17 | "parameters": { 18 | "logicAppName": { 19 | "type": "string", 20 | "defaultValue": "Sentinel-ExportIncidentWithUrlInUnifiedPortal", 21 | "metadata": { 22 | "description": "Name of the Logic App." 23 | } 24 | }, 25 | "eventHubName": { 26 | "type": "string", 27 | "defaultValue": "write-your-event-hub-name-here", 28 | "metadata": { 29 | "description": "Name of the Event Hub." 30 | } 31 | } 32 | }, 33 | "variables": { 34 | "MicrosoftSentinelConnectionName": "[concat('MicrosoftSentinel-', parameters('logicAppName'))]", 35 | "EventHubConnectionName": "[concat('EventHub-', parameters('logicAppName'))]" 36 | }, 37 | "resources": [ 38 | { 39 | "type": "Microsoft.Logic/workflows", 40 | "apiVersion": "2019-05-01", 41 | "name": "[parameters('logicAppName')]", 42 | "location": "[resourceGroup().location]", 43 | "tags": { 44 | "LogicAppsCategory": "security", 45 | "hidden-SentinelTemplateName": "[parameters('logicAppName')]", 46 | "hidden-SentinelTemplateVersion": "1.0" 47 | }, 48 | "properties": { 49 | "definition": { 50 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 51 | "contentVersion": "1.0.0.0", 52 | "parameters": { 53 | "EventHub Name": { 54 | "defaultValue": "[parameters('eventHubName')]", 55 | "type": "String" 56 | }, 57 | "$connections": { 58 | "defaultValue": {}, 59 | "type": "Object" 60 | } 61 | }, 62 | "triggers": { 63 | "Microsoft_Sentinel_incident": { 64 | "type": "ApiConnectionWebhook", 65 | "inputs": { 66 | "host": { 67 | "connection": { 68 | "name": "@parameters('$connections')['microsoftsentinel-connection']['connectionId']" 69 | } 70 | }, 71 | "body": { 72 | "callback_url": "@{listCallbackUrl()}" 73 | }, 74 | "path": "/incident-creation" 75 | } 76 | } 77 | }, 78 | "actions": { 79 | "Read_and_parse_Incident": { 80 | "runAfter": {}, 81 | "type": "ParseJson", 82 | "inputs": { 83 | "content": "@triggerBody()", 84 | "schema": { 85 | "type": "object", 86 | "properties": { 87 | "headers": { 88 | "type": "object", 89 | "properties": { 90 | "Accept-Encoding": { 91 | "type": "string" 92 | }, 93 | "Host": { 94 | "type": "string" 95 | }, 96 | "Max-Forwards": { 97 | "type": "string" 98 | }, 99 | "Correlation-Context": { 100 | "type": "string" 101 | }, 102 | "traceparent": { 103 | "type": "string" 104 | }, 105 | "x-ms-client-tracking-id": { 106 | "type": "string" 107 | }, 108 | "x-ms-correlation-request-id": { 109 | "type": "string" 110 | }, 111 | "x-ms-forward-internal-correlation-id": { 112 | "type": "string" 113 | }, 114 | "X-ARR-LOG-ID": { 115 | "type": "string" 116 | }, 117 | "CLIENT-IP": { 118 | "type": "string" 119 | }, 120 | "DISGUISED-HOST": { 121 | "type": "string" 122 | }, 123 | "X-SITE-DEPLOYMENT-ID": { 124 | "type": "string" 125 | }, 126 | "WAS-DEFAULT-HOSTNAME": { 127 | "type": "string" 128 | }, 129 | "X-Forwarded-Proto": { 130 | "type": "string" 131 | }, 132 | "X-AppService-Proto": { 133 | "type": "string" 134 | }, 135 | "X-ARR-SSL": { 136 | "type": "string" 137 | }, 138 | "X-Forwarded-TlsVersion": { 139 | "type": "string" 140 | }, 141 | "X-Forwarded-For": { 142 | "type": "string" 143 | }, 144 | "X-Original-URL": { 145 | "type": "string" 146 | }, 147 | "X-WAWS-Unencoded-URL": { 148 | "type": "string" 149 | }, 150 | "Content-Length": { 151 | "type": "string" 152 | }, 153 | "Content-Type": { 154 | "type": "string" 155 | } 156 | } 157 | }, 158 | "body": { 159 | "type": "object", 160 | "properties": { 161 | "eventUniqueId": { 162 | "type": "string" 163 | }, 164 | "objectSchemaType": { 165 | "type": "string" 166 | }, 167 | "objectEventType": { 168 | "type": "string" 169 | }, 170 | "workspaceInfo": { 171 | "type": "object", 172 | "properties": { 173 | "SubscriptionId": { 174 | "type": "string" 175 | }, 176 | "ResourceGroupName": { 177 | "type": "string" 178 | }, 179 | "WorkspaceName": { 180 | "type": "string" 181 | } 182 | } 183 | }, 184 | "workspaceId": { 185 | "type": "string" 186 | }, 187 | "object": { 188 | "type": "object", 189 | "properties": { 190 | "id": { 191 | "type": "string" 192 | }, 193 | "name": { 194 | "type": "string" 195 | }, 196 | "etag": { 197 | "type": "string" 198 | }, 199 | "type": { 200 | "type": "string" 201 | }, 202 | "properties": { 203 | "type": "object", 204 | "properties": { 205 | "title": { 206 | "type": "string" 207 | }, 208 | "severity": { 209 | "type": "string" 210 | }, 211 | "status": { 212 | "type": "string" 213 | }, 214 | "owner": { 215 | "type": "object", 216 | "properties": { 217 | "objectId": {}, 218 | "email": {}, 219 | "assignedTo": {}, 220 | "userPrincipalName": {} 221 | } 222 | }, 223 | "labels": { 224 | "type": "array" 225 | }, 226 | "firstActivityTimeUtc": { 227 | "type": "string" 228 | }, 229 | "lastActivityTimeUtc": { 230 | "type": "string" 231 | }, 232 | "lastModifiedTimeUtc": { 233 | "type": "string" 234 | }, 235 | "createdTimeUtc": { 236 | "type": "string" 237 | }, 238 | "incidentNumber": { 239 | "type": "integer" 240 | }, 241 | "additionalData": { 242 | "type": "object", 243 | "properties": { 244 | "alertsCount": { 245 | "type": "integer" 246 | }, 247 | "bookmarksCount": { 248 | "type": "integer" 249 | }, 250 | "commentsCount": { 251 | "type": "integer" 252 | }, 253 | "alertProductNames": { 254 | "type": "array", 255 | "items": { 256 | "type": "string" 257 | } 258 | }, 259 | "tactics": { 260 | "type": "array", 261 | "items": { 262 | "type": "string" 263 | } 264 | }, 265 | "techniques": { 266 | "type": "array", 267 | "items": { 268 | "type": "string" 269 | } 270 | } 271 | } 272 | }, 273 | "relatedAnalyticRuleIds": { 274 | "type": "array", 275 | "items": { 276 | "type": "string" 277 | } 278 | }, 279 | "incidentUrl": { 280 | "type": "string" 281 | }, 282 | "providerName": { 283 | "type": "string" 284 | }, 285 | "providerIncidentId": { 286 | "type": "string" 287 | }, 288 | "alerts": { 289 | "type": "array", 290 | "items": { 291 | "type": "object", 292 | "properties": { 293 | "id": { 294 | "type": "string" 295 | }, 296 | "name": { 297 | "type": "string" 298 | }, 299 | "type": { 300 | "type": "string" 301 | }, 302 | "kind": { 303 | "type": "string" 304 | }, 305 | "properties": { 306 | "type": "object", 307 | "properties": { 308 | "systemAlertId": { 309 | "type": "string" 310 | }, 311 | "tactics": { 312 | "type": "array", 313 | "items": { 314 | "type": "string" 315 | } 316 | }, 317 | "alertDisplayName": { 318 | "type": "string" 319 | }, 320 | "description": { 321 | "type": "string" 322 | }, 323 | "confidenceLevel": { 324 | "type": "string" 325 | }, 326 | "severity": { 327 | "type": "string" 328 | }, 329 | "vendorName": { 330 | "type": "string" 331 | }, 332 | "productName": { 333 | "type": "string" 334 | }, 335 | "productComponentName": { 336 | "type": "string" 337 | }, 338 | "alertType": { 339 | "type": "string" 340 | }, 341 | "processingEndTime": { 342 | "type": "string" 343 | }, 344 | "status": { 345 | "type": "string" 346 | }, 347 | "endTimeUtc": { 348 | "type": "string" 349 | }, 350 | "startTimeUtc": { 351 | "type": "string" 352 | }, 353 | "timeGenerated": { 354 | "type": "string" 355 | }, 356 | "providerAlertId": { 357 | "type": "string" 358 | }, 359 | "resourceIdentifiers": { 360 | "type": "array", 361 | "items": { 362 | "type": "object", 363 | "properties": { 364 | "type": { 365 | "type": "string" 366 | }, 367 | "workspaceId": { 368 | "type": "string" 369 | } 370 | }, 371 | "required": [ 372 | "type", 373 | "workspaceId" 374 | ] 375 | } 376 | }, 377 | "additionalData": { 378 | "type": "object", 379 | "properties": { 380 | "ProcessedBySentinel": { 381 | "type": "string" 382 | }, 383 | "Alert generation status": { 384 | "type": "string" 385 | }, 386 | "Query Period": { 387 | "type": "string" 388 | }, 389 | "Trigger Operator": { 390 | "type": "string" 391 | }, 392 | "Trigger Threshold": { 393 | "type": "string" 394 | }, 395 | "Correlation Id": { 396 | "type": "string" 397 | }, 398 | "Analytics Template Id": { 399 | "type": "string" 400 | }, 401 | "Search Query Results Overall Count": { 402 | "type": "string" 403 | }, 404 | "Data Sources": { 405 | "type": "string" 406 | }, 407 | "Query": { 408 | "type": "string" 409 | }, 410 | "Query Start Time UTC": { 411 | "type": "string" 412 | }, 413 | "Query End Time UTC": { 414 | "type": "string" 415 | }, 416 | "Analytic Rule Ids": { 417 | "type": "string" 418 | }, 419 | "Event Grouping": { 420 | "type": "string" 421 | }, 422 | "Analytic Rule Name": { 423 | "type": "string" 424 | } 425 | } 426 | }, 427 | "friendlyName": { 428 | "type": "string" 429 | } 430 | } 431 | } 432 | }, 433 | "required": [ 434 | "id", 435 | "name", 436 | "type", 437 | "kind", 438 | "properties" 439 | ] 440 | } 441 | }, 442 | "bookmarks": { 443 | "type": "array" 444 | }, 445 | "relatedEntities": { 446 | "type": "array", 447 | "items": { 448 | "type": "object", 449 | "properties": { 450 | "id": { 451 | "type": "string" 452 | }, 453 | "name": { 454 | "type": "string" 455 | }, 456 | "type": { 457 | "type": "string" 458 | }, 459 | "kind": { 460 | "type": "string" 461 | }, 462 | "properties": { 463 | "type": "object", 464 | "properties": { 465 | "accountName": { 466 | "type": "string" 467 | }, 468 | "upnSuffix": { 469 | "type": "string" 470 | }, 471 | "aadTenantId": { 472 | "type": "string" 473 | }, 474 | "aadUserId": { 475 | "type": "string" 476 | }, 477 | "isDomainJoined": { 478 | "type": "boolean" 479 | }, 480 | "displayName": { 481 | "type": "string" 482 | }, 483 | "additionalData": { 484 | "type": "object", 485 | "properties": { 486 | "Sources": { 487 | "type": "string" 488 | }, 489 | "GivenName": { 490 | "type": "string" 491 | }, 492 | "IsDeleted": { 493 | "type": "string" 494 | }, 495 | "IsEnabled": { 496 | "type": "string" 497 | }, 498 | "Surname": { 499 | "type": "string" 500 | }, 501 | "TransitiveDirectoryRoles": { 502 | "type": "string" 503 | }, 504 | "UserType": { 505 | "type": "string" 506 | }, 507 | "UpnName": { 508 | "type": "string" 509 | }, 510 | "SyncFromAad": { 511 | "type": "string" 512 | }, 513 | "Country": { 514 | "type": "string" 515 | }, 516 | "MailAddress": { 517 | "type": "string" 518 | }, 519 | "PhoneNumber": { 520 | "type": "string" 521 | }, 522 | "AdditionalMailAddresses": { 523 | "type": "string" 524 | } 525 | } 526 | }, 527 | "friendlyName": { 528 | "type": "string" 529 | } 530 | } 531 | } 532 | }, 533 | "required": [ 534 | "id", 535 | "name", 536 | "type", 537 | "kind", 538 | "properties" 539 | ] 540 | } 541 | }, 542 | "comments": { 543 | "type": "array" 544 | } 545 | } 546 | } 547 | } 548 | } 549 | } 550 | } 551 | } 552 | } 553 | } 554 | }, 555 | "Initialize_and_set_variable_OriginalIncident": { 556 | "runAfter": { 557 | "Read_and_parse_Incident": [ 558 | "Succeeded" 559 | ] 560 | }, 561 | "type": "InitializeVariable", 562 | "inputs": { 563 | "variables": [ 564 | { 565 | "name": "OriginalIncident", 566 | "type": "object", 567 | "value": "@body('Read_and_parse_Incident')" 568 | } 569 | ] 570 | } 571 | }, 572 | "Add_property_unifiedIncidentUrl_to_ModifiedIncident": { 573 | "runAfter": { 574 | "Initialize_variable_ModifiedIncident": [ 575 | "Succeeded" 576 | ] 577 | }, 578 | "type": "SetVariable", 579 | "inputs": { 580 | "name": "ModifiedIncident", 581 | "value": "@setProperty(variables('OriginalIncident')['object'],'unifiedIncidentUrl',concat('https://security.microsoft.com/incident2/',variables('OriginalIncident')['object']['properties']['providerIncidentId'],'/overview'))" 582 | } 583 | }, 584 | "Initialize_variable_ModifiedIncident": { 585 | "runAfter": { 586 | "Initialize_and_set_variable_OriginalIncident": [ 587 | "Succeeded" 588 | ] 589 | }, 590 | "type": "InitializeVariable", 591 | "inputs": { 592 | "variables": [ 593 | { 594 | "name": "ModifiedIncident", 595 | "type": "object" 596 | } 597 | ] 598 | } 599 | }, 600 | "Send_ModifiedIncident_to_EventHub": { 601 | "runAfter": { 602 | "Add_property_unifiedIncidentUrl_to_ModifiedIncident": [ 603 | "Succeeded" 604 | ] 605 | }, 606 | "type": "ApiConnection", 607 | "inputs": { 608 | "host": { 609 | "connection": { 610 | "name": "@parameters('$connections')['eventhub-connection']['connectionId']" 611 | } 612 | }, 613 | "method": "post", 614 | "body": { 615 | "ContentData": "@base64(variables('ModifiedIncident'))" 616 | }, 617 | "path": "/@{encodeURIComponent(parameters('EventHub Name'))}/events" 618 | } 619 | } 620 | }, 621 | "outputs": {} 622 | }, 623 | "parameters": { 624 | "$connections": { 625 | "value": { 626 | "microsoftsentinel-connection": { 627 | "id": "[concat('/subscriptions/',subscription().subscriptionId,'/providers/Microsoft.Web/locations/',resourceGroup().location,'/managedApis/Azuresentinel')]", 628 | "connectionId": "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", 629 | "connectionName": "[variables('MicrosoftSentinelConnectionName')]", 630 | "connectionProperties": { 631 | "authentication": { 632 | "type": "ManagedServiceIdentity" 633 | } 634 | } 635 | }, 636 | "eventhub-connection": { 637 | "id": "[concat('/subscriptions/',subscription().subscriptionId,'/providers/Microsoft.Web/locations/',resourceGroup().location,'/managedApis/eventhubs')]", 638 | "connectionId": "[resourceId('Microsoft.Web/connections', variables('EventHubConnectionName'))]", 639 | "connectionName": "[variables('EventHubConnectionName')]" 640 | } 641 | } 642 | } 643 | } 644 | }, 645 | "identity": { 646 | "type": "SystemAssigned" 647 | } 648 | }, 649 | { 650 | "type": "Microsoft.Web/connections", 651 | "apiVersion": "2016-06-01", 652 | "location": "[resourceGroup().location]", 653 | "name": "[variables('EventHubConnectionName')]", 654 | "kind": "V1", 655 | "properties": { 656 | "api": { 657 | "id": "[concat('/subscriptions/',subscription().subscriptionId,'/providers/Microsoft.Web/locations/',resourceGroup().location,'/managedApis/eventhubs')]" 658 | }, 659 | "customParameterValues": {}, 660 | "displayName": "[variables('EventHubConnectionName')]" 661 | } 662 | }, 663 | { 664 | "type": "Microsoft.Web/connections", 665 | "apiVersion": "2016-06-01", 666 | "location": "[resourceGroup().location]", 667 | "name": "[variables('MicrosoftSentinelConnectionName')]", 668 | "kind": "V1", 669 | "properties": { 670 | "api": { 671 | "id": "[concat('/subscriptions/',subscription().subscriptionId,'/providers/Microsoft.Web/locations/',resourceGroup().location,'/managedApis/Azuresentinel')]" 672 | }, 673 | "customParameterValues": {}, 674 | "parameterValueType": "Alternative", 675 | "displayName": "[variables('MicrosoftSentinelConnectionName')]" 676 | } 677 | } 678 | ], 679 | "outputs": {} 680 | } 681 | -------------------------------------------------------------------------------- /SentinelAnalyticRulesMassiveCreationScript.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | --> Abandoned: please use SentinelAnalyticRulesManagementScript.ps1 4 | 5 | This script contains two cmdlets: 6 | 1. The cmdlet Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates automates the creation of Analytic Rules in Microsoft Sentinel starting from existing Templates. 7 | 2. The cmdlet Get-SentinelInstalledTemplatesAsCsv extracts in a CSV file the names of the Analytic Rules Templates installed in the workspace. 8 | 9 | .DESCRIPTION 10 | Version: 1.0 11 | Release Date: 2024-02-19 12 | 13 | The cmdlet Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates creates the Analytic Rules (aka Rules) based on the Analytic Rules Templates (aka Templates) available 14 | in the Content Hub Solutions (aka Packages or Solutions) already installed in the Sentinel workspace. 15 | Before using this cmdlet, ensure to have the desired Solutions installed and the related Connectors active. 16 | If required, it is highly recommended to manually update the installed Solutions before creating the Rules (otherwise the Rules will be created by using possibly old Templates!). 17 | The Sentinel out-of-the-box Templates that are not part of any already installed Solutions are not considered by the cmdlet. 18 | The Templates that have already one or more active Rule associated to them are skipped: no additional Rules are created for them, so you can safely run the cmdlet multiple times 19 | without causing duplications. 20 | It is possible to further filter-out the Templates to be considered by Severities (passed as input parameter) and/or by DisplayNames (specified in a CSV file specified as input parameter). 21 | The execution can be simulated, so that the cmdlet only logs what it would do but without doing any real change to Sentinel. 22 | The log file is created in the same local directory from where the script is launched. 23 | The creation of some Rules may terminate with an error, typically because of missing content in Sentinel (e.g. missing tables). These errors are simply logged and the execution continues. 24 | 25 | The cmdlet Get-SentinelInstalledTemplatesAsCsv extracts in a CSV file the names of the Analytic Rules Templates installed in the workspace. 26 | You can edit this file manually to remove the undesired rules and then use the edited file as input file for 27 | the cmdlet Invoke-SentinelAnalyticRulesCreationFromInstalledTemplate. 28 | 29 | ############################# 30 | How to launch it and what to expect? 31 | 1. Download this script file from here 32 | 2. Search for the "Launching section" just at the end of the script file. 33 | Uncomment it by removing the opening "minor_char + sharp_char" and closing "sharp_char + major_char" surrounding that section. 34 | 3. Set the parameters and choose the cmdlet to launch based on your environment and needs (comment out or delete what is not needed). 35 | See also the parameters explaination and the many examples here below in this comment section. 36 | 4. In the powershell window, launch the script by calling its file name: it will cause the execution of the launching section. 37 | Note: before launching, I recommend to change the current directory to the directory containing the script. The directory from where you launch the script 38 | will contain the output log file. 39 | 5. If you are not blocked by the initial checks that verify the launching conditions, soon you'll see the classical "device login" message. Proceed with the authentication 40 | to Azure accordingly. 41 | Note: you need to authenticate with a user having the rights to read and create Analytic Rules and modify Solutions in Sentinel (min. role: Microsoft Sentinel Contributor). 42 | 6. Wait for the end of the execution while reading the output messages. 43 | 7. Read the final statistics. If needed, read the content of the log file. 44 | ############################# 45 | 46 | .PARAMETER SubscriptionId 47 | (Mandatory, string) ID of the Azure Subscription containing the Sentinel workspace. 48 | 49 | .PARAMETER ResourceGroup 50 | (Mandatory, string) Name of the Azure Resource Group containing the Sentinel workspace. 51 | 52 | .PARAMETER Workspace 53 | (Mandatory, string) Name of the Azure Sentinel workspace. 54 | 55 | .PARAMETER Region 56 | (Mandatory, string) Azure region where the Sentinel workspace is located. E.g.: westeurope 57 | 58 | .PARAMETER SaveExistingAnalyticRuleTemplatesToCsvFile 59 | (Optional, bool) Flag to save existing Analytic Rule Templates to a CSV file. Default is false. 60 | When it is specified and set to true, OutputAnalyticRuleTemplatesCsvFile must be specified and 61 | no Analytic Rule will be created (not even in a simulated loop). 62 | The value of the paramters 'SimulateOnly', 'LimitToMaxNumberOfRules', 'InputCsvFile' and 63 | 'SeveritiesToInclude' will then be ignored. 64 | 65 | .PARAMETER OutputAnalyticRuleTemplatesCsvFile 66 | (Optional, string) Path to the CSV file where the cmdlet writes the information of the Analytic 67 | Rule Templates existing in the Sentinel workspace (all and only the Templates existing in the 68 | ContentHub solutions installed in the workspace). 69 | The CSV will contain the following colums: 70 | "DisplayName","Severity","AtLeastOneRuleAlreadyExists","Package" 71 | 72 | .PARAMETER CsvSeparatorChar 73 | (Optional, char) Character used as a separator in the CSV file. 74 | The char ',' (comma) is used if not specified differently. 75 | 76 | .PARAMETER SimulateOnly 77 | (Optional, bool) Flag to simulate the operation without making any changes in Sentinel. Default is false (= no simulation: Rules are created). 78 | 79 | .PARAMETER LimitToMaxNumberOfRules 80 | (Optional, int) Limit the maximum number of Rules that will be created. Default is 0 (no limit). 81 | To be used typically onbly for the first tests. 82 | 83 | .PARAMETER InputCsvFile 84 | (Optional, string) Path to the CSV file containing the DisplayName of the Templates to be considered. 85 | NOTE: it requires a column named DisplayName, where it expects to find the name of the Templates to be considered. 86 | If the column is not found, the execution ends with an error. 87 | If the file is not found, the execution ends with an error. 88 | 89 | .PARAMETER SeveritiesToInclude 90 | (Optional, string array) Array of severities to include in the operation. Default is all severities ("Informational", "Low", "Medium", "High"). 91 | 92 | 93 | .EXAMPLE 94 | ################ 95 | # Exports the names of the existing Templates in a CSV file. 96 | # Edit the file as needed (filter and remove the unwanted lines) and use it as input for the where you can remove the Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates cmdlet. 97 | # No change is made to Sentinel 98 | ################ 99 | $SubscriptionId = "<...>" 100 | $ResourceGroup = "<...>" 101 | $Workspace = "<...>" 102 | $Region = "<...>" #e.g. westeurope 103 | $SaveExistingAnalyticRuleTemplatesToCsvFile = $true 104 | $OutputAnalyticRuleTemplatesCsvFile = "\art.csv" 105 | $CsvSeparatorChar = ';' 106 | 107 | Get-SentinelInstalledTemplatesAsCsv -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 108 | -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile -CsvSeparatorChar $CsvSeparatorChar 109 | 110 | # NOTE: the above command is equivalent (same code executed and same behavior) to the following one: 111 | 112 | Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 113 | -SaveExistingAnalyticRuleTemplatesToCsvFile $true -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile ` 114 | -CsvSeparatorChar $CsvSeparatorChar 115 | 116 | .EXAMPLE 117 | ################ 118 | # Creates the Rules from the Templates existing in the Solutions installed in Sentinel. Among these, the only Templates considered are those 119 | # with the 'DisplayName' specified in the input CSV file and with 'Severity' High or Medium. 120 | ################ 121 | $SubscriptionId = "<...>" 122 | $ResourceGroup = "<...>" 123 | $Workspace = "<...>" 124 | $Region = "<...>" #e.g. westeurope 125 | $CsvSeparatorChar = ';' 126 | $SimulateOnly = $false 127 | $InputCsvFile = "\.csv" 128 | $SeveritiesToInclude = @("High","Medium") 129 | 130 | Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 131 | -CsvSeparatorChar $CsvSeparatorChar -InputCsvFile $InputCsvFile ` 132 | -SeveritiesToInclude $SeveritiesToInclude -Simulate $SimulateOnly #-verbose 133 | 134 | .EXAMPLE 135 | ################ 136 | # Simulates the creation of the Rules from the Templates existing in the Solutions installed in Sentinel. Among these, the only Templates 137 | # considered are those with the 'DisplayName' specified in the input CSV file and with 'Severity' High or Medium or Informational. 138 | ################ 139 | $SubscriptionId = "<...>" 140 | $ResourceGroup = "<...>" 141 | $Workspace = "<...>" 142 | $Region = "<...>" #e.g. westeurope 143 | $CsvSeparatorChar = ';' 144 | $SimulateOnly = $true 145 | $InputCsvFile = "\.csv" 146 | $SeveritiesToInclude = @("High","Medium","Informational") 147 | 148 | Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 149 | -CsvSeparatorChar $CsvSeparatorChar -InputCsvFile $InputCsvFile ` 150 | -SeveritiesToInclude $SeveritiesToInclude -Simulate $SimulateOnly #-verbose 151 | 152 | .EXAMPLE 153 | ################ 154 | # Simulates the creation of the Rules from the Templates existing in the Solutions installed in Sentinel. Among these, the only Templates 155 | # considered are those with 'Severity' High. 156 | # The execution is stopped after the first 10 Rules virtually added. 157 | ################ 158 | $SubscriptionId = "<...>" 159 | $ResourceGroup = "<...>" 160 | $Workspace = "<...>" 161 | $Region = "<...>" #e.g. westeurope 162 | $SimulateOnly = $true 163 | $LimitToMaxNumberOfRules = 10 164 | $SeveritiesToInclude = @("High") 165 | 166 | Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 167 | -SeveritiesToInclude $SeveritiesToInclude -Simulate $SimulateOnly -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules #-verbose 168 | 169 | .NOTES 170 | The script requires PowerShell 7. 171 | * Check the version of your powershell by using: $PSVersionTable.PSVersion 172 | * Install it by launching: winget search Microsoft.PowerShell 173 | (https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4) 174 | 175 | .AUTHOR 176 | Stefano Pescosolido (https://www.linkedin.com/in/stefanopescosolido/) 177 | Part of the code is taken from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Sentinel-All-In-One 178 | 179 | #> 180 | 181 | function CreateAuthenticationHeader { 182 | param ( 183 | [Parameter(Mandatory = $true)][string]$TenantId, 184 | [Parameter(Mandatory = $false)][string]$PrefixInDisplayName 185 | ) 186 | $instanceProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile 187 | $profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($instanceProfile) 188 | $token = $profileClient.AcquireAccessToken($TenantId) 189 | $authNHeader = @{ 190 | 'Content-Type' = 'application/json' 191 | 'Authorization' = 'Bearer ' + $token.AccessToken 192 | } 193 | 194 | return $authNHeader 195 | } 196 | 197 | function CreateAnalyticRule { 198 | param ( 199 | [Parameter(Mandatory = $true)][string]$BaseUri, 200 | [Parameter(Mandatory = $true)][object]$Template, 201 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $true 202 | ) 203 | 204 | $alertUri = "$BaseUri/providers/Microsoft.SecurityInsights/alertRules/" 205 | $BaseAlertUri = $BaseUri + "/providers/Microsoft.SecurityInsights/alertRules/" 206 | 207 | $kind = $Template.properties.mainTemplate.resources.kind 208 | $displayName = $Template.properties.mainTemplate.resources.properties[0].displayName 209 | $eventGroupingSettings = $Template.properties.mainTemplate.resources.properties[0].eventGroupingSettings 210 | if ($null -eq $eventGroupingSettings) { 211 | $eventGroupingSettings = [ordered]@{aggregationKind = "SingleAlert" } 212 | } 213 | $body = "" 214 | $properties = $Template.properties.mainTemplate.resources[0].properties 215 | $properties.enabled = $true 216 | #Add the field to link this rule with the rule template so that the rule template will show up as used 217 | #We had to use the "Add-Member" command since this field does not exist in the rule template that we are copying from. 218 | $properties | Add-Member -NotePropertyName "alertRuleTemplateName" -NotePropertyValue $Template.properties.mainTemplate.resources[0].name 219 | $properties | Add-Member -NotePropertyName "templateVersion" -NotePropertyValue $Template.properties.mainTemplate.resources[1].properties.version 220 | 221 | 222 | #Depending on the type of alert we are creating, the body has different parameters 223 | switch ($kind) { 224 | "MicrosoftSecurityIncidentCreation" { 225 | $body = @{ 226 | "kind" = "MicrosoftSecurityIncidentCreation" 227 | "properties" = $properties 228 | } 229 | } 230 | "NRT" { 231 | $body = @{ 232 | "kind" = "NRT" 233 | "properties" = $properties 234 | } 235 | } 236 | "Scheduled" { 237 | $body = @{ 238 | "kind" = "Scheduled" 239 | "properties" = $properties 240 | } 241 | 242 | } 243 | Default { } 244 | } 245 | #If we have created the body... 246 | if ("" -ne $body) { 247 | #Create the GUId for the alert and create it. 248 | $guid = (New-Guid).Guid 249 | #Create the URI we need to create the alert. 250 | $alertUri = $BaseAlertUri + $guid + "?api-version=2022-12-01-preview" 251 | try { 252 | Write-Verbose -Message "Template: $displayName - Creating the rule...." 253 | 254 | if(-not($SimulateOnly)){ 255 | $rule = Invoke-RestMethod -Uri $alertUri -Method Put -Headers $authHeader -Body ($body | ConvertTo-Json -EnumsAsStrings -Depth 50) 256 | Write-Host -Message "Template: $displayName - Creating the rule - Succeeded" -ForegroundColor Green 257 | #This pauses for 1 second so that we don't overload the workspace. 258 | Start-Sleep -Seconds 1 259 | } 260 | else { 261 | Write-Host -Message "Template: $displayName - Creating the rule - Succeeded (SIMULATED)" -ForegroundColor Green 262 | } 263 | 264 | } 265 | catch { 266 | Write-Verbose "Template: $displayName - ERROR while creating the rule:" 267 | Write-Verbose $_ 268 | #Write-Host -Message "Template: $displayName - ERROR while creating the rule: $(($_).Exception.Message)" -ForegroundColor Red 269 | Write-Host -Message "Template: $displayName - ERROR while creating the rule" -ForegroundColor Red 270 | throw 271 | } 272 | } 273 | 274 | return $rule 275 | } 276 | 277 | function LinkAnalyticRuleToSolution { 278 | param ( 279 | [Parameter(Mandatory = $true)][string]$BaseUri, 280 | [Parameter(Mandatory = $true)][object]$Rule, 281 | [Parameter(Mandatory = $true)][object]$Template, 282 | [Parameter(Mandatory = $true)][object]$Solution, 283 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $true 284 | ) 285 | 286 | $baseMetaURI = $BaseUri + "/providers/Microsoft.SecurityInsights/metadata/analyticsrule-" 287 | 288 | $metabody = @{ 289 | "apiVersion" = "2022-01-01-preview" 290 | "name" = "analyticsrule-" + $Rule.name 291 | "type" = "Microsoft.OperationalInsights/workspaces/providers/metadata" 292 | "id" = $null 293 | "properties" = @{ 294 | "contentId" = $Template.properties.mainTemplate.resources[0].name 295 | "parentId" = $Rule.id 296 | "kind" = "AnalyticsRule" 297 | "version" = $Template.properties.mainTemplate.resources.properties[1].version 298 | "source" = $Solution.source 299 | "author" = $Solution.author 300 | "support" = $Solution.support 301 | } 302 | } 303 | Write-Verbose -Message "Rule: $(($Rule).displayName) - Updating metadata...." 304 | $metaURI = $baseMetaURI + $Rule.name + "?api-version=2022-01-01-preview" 305 | try { 306 | if(-not($SimulateOnly)){ 307 | $metaVerdict = Invoke-RestMethod -Uri $metaURI -Method Put -Headers $authHeader -Body ($metabody | ConvertTo-Json -EnumsAsStrings -Depth 5) 308 | Write-Host -Message "Rule: $(($Rule).properties.displayName) - Updating metadata - Succeeded" -ForegroundColor Green 309 | #This pauses for 1 second so that we don't overload the workspace. 310 | Start-Sleep -Seconds 1 311 | } else { 312 | Write-Host -Message "Rule: $(($Rule).properties.displayName) - Updating metadata - Succeeded (SIMULATED)" -ForegroundColor Green 313 | } 314 | 315 | } 316 | catch { 317 | Write-Verbose "Rule: $(($Rule).displayName) - ERROR while updating metadata:" 318 | Write-Verbose $_ 319 | #Write-Host -Message "Rule: $(($Rule).displayName) - ERROR while updating metadata: $(($_).Exception.Message)" -ForegroundColor Red 320 | Write-Host -Message "Rule: $(($Rule).displayName) - ERROR while updating metadata" -ForegroundColor Red 321 | throw 322 | } 323 | return $metaVerdict 324 | 325 | } 326 | 327 | function CheckIfAnAnalyticRuleAssociatedToTemplateExist { 328 | param ( 329 | [Parameter(Mandatory = $true)][string]$BaseUri, 330 | [Parameter(Mandatory = $true)][object]$Template 331 | ) 332 | 333 | $uri = $BaseUri + "/providers/Microsoft.SecurityInsights/alertRules?api-version=2022-01-01-preview" 334 | 335 | $allRules = (Invoke-RestMethod -Uri $uri -Method Get -Headers $authHeader).value 336 | 337 | $found = $false 338 | foreach($rule in $allRules){ 339 | if($rule.properties.alertRuleTemplateName -eq $Template.properties.mainTemplate.resources[0].name){ 340 | $found = $true 341 | break 342 | } 343 | } 344 | 345 | return $found 346 | 347 | } 348 | 349 | function ExecuteRequest { 350 | param( 351 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 352 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 353 | [Parameter(Mandatory = $true)][string]$Workspace, 354 | [Parameter(Mandatory = $true)][string]$Region, 355 | [Parameter(Mandatory = $false)][bool]$SaveExistingAnalyticRuleTemplatesToCsvFile = $false, 356 | [Parameter(Mandatory = $false)][string]$OutputAnalyticRuleTemplatesCsvFile, 357 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar, 358 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $false, 359 | [Parameter(Mandatory = $false)][int]$LimitToMaxNumberOfRules = 0, 360 | [Parameter(Mandatory = $false)][string]$InputCsvFile, 361 | [Parameter(Mandatory = $false)][string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High"), 362 | [Parameter(Mandatory = $false)][bool]$SuppressWarningForExportCsv = $false 363 | ) 364 | 365 | # Check installed PowerShell version 366 | if($PSVersionTable.PSVersion.Major -lt 7){ 367 | Write-Host "This cmdlet requires PowerShell 7. Exiting..." -ForegroundColor Red 368 | exit 369 | } 370 | 371 | #Check if Az.Accounts is installed 372 | $module = Get-Module -ListAvailable -Name Az.Accounts 373 | if($null -eq $module){ 374 | Write-Host "The module 'Az.Accounts' is required and is not installed." -ForegroundColor Red 375 | Write-Host "To install it, open PowerShell as and Administrator and execute the following command: " -ForegroundColor Red 376 | Write-Host "Install-Module -Name Az.Accounts" -ForegroundColor Red 377 | Write-Host "Exiting..." -ForegroundColor Red 378 | exit 379 | } 380 | 381 | # Set default values for some parameters 382 | if( ([string]::IsNullOrEmpty($CsvSeparatorChar)) -or (([byte]$CsvSeparatorChar) -eq 0) ) { 383 | $CsvSeparatorChar = ',' 384 | } 385 | 386 | if((-not($SaveExistingAnalyticRuleTemplatesToCsvFile))-and([string]::IsNullOrEmpty($SeveritiesToInclude))){ 387 | $SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 388 | } 389 | 390 | # Check the coherence of the input parameters 391 | $askForConfirmation = $false 392 | 393 | if(($SaveExistingAnalyticRuleTemplatesToCsvFile) -and ([string]::IsNullOrEmpty($OutputAnalyticRuleTemplatesCsvFile))){ 394 | Write-Host "When the input parameter 'SaveExistingAnalyticRuleTemplatesToCsvFile' is set to 'true' it is necessary to specify a value also for the input parameter 'OutputAnalyticRuleTemplatesCsvFile'. Exiting..." -ForegroundColor Red 395 | exit 396 | } 397 | 398 | if(-not($SuppressWarningForExportCsv)){ 399 | if($SaveExistingAnalyticRuleTemplatesToCsvFile){ 400 | Write-Host "NOTE: when the input parameter 'SaveExistingAnalyticRuleTemplatesToCsvFile' is set to 'true', no Analytic Rule will be created (not even in a simulated loop). The value of the paramters 'SimulateOnly', 'LimitToMaxNumberOfRules', 'InputCsvFile' and 'SeveritiesToInclude' will then be ignored." -ForegroundColor Blue -BackgroundColor Yellow 401 | $askForConfirmation = $true 402 | } 403 | } 404 | 405 | if(($SaveExistingAnalyticRuleTemplatesToCsvFile) -and (-not([string]::IsNullOrEmpty($OutputAnalyticRuleTemplatesCsvFile)))){ 406 | if(Test-Path($OutputAnalyticRuleTemplatesCsvFile)){ 407 | Write-Host "NOTE: The file '$OutputAnalyticRuleTemplatesCsvFile' already exists and will be overwritten." -ForegroundColor Blue -BackgroundColor Yellow 408 | $askForConfirmation = $true 409 | } 410 | $folder = Split-Path -Parent $OutputAnalyticRuleTemplatesCsvFile 411 | if(-not(Test-Path($folder))){ 412 | Write-Host "The folder '$folder' specified in the input parameter 'OutputAnalyticRuleTemplatesCsvFile' does not exist. Exiting..." -ForegroundColor Red 413 | exit 414 | } 415 | } 416 | 417 | if(-not($SuppressWarningForExportCsv)){ 418 | if((-not($SaveExistingAnalyticRuleTemplatesToCsvFile))-and($SimulateOnly)){ 419 | Write-Host "NOTE: when the input parameter 'SimulateOnly' is set to 'true', no Analytic Rule will be created but you can see - in the output messages and in the log file - what rule would be created" -ForegroundColor Blue -BackgroundColor Yellow 420 | $askForConfirmation = $true 421 | } 422 | } 423 | 424 | $inCsvContent = $null 425 | $filterByTemplateDisplayName = $null 426 | if(-not([string]::IsNullOrEmpty($InputCsvFile))){ 427 | if(-not(Test-Path($InputCsvFile))){ 428 | Write-Host "The input file specified in the input parameter 'InputCsvFile' does not exist. Exiting..." -ForegroundColor Red 429 | exit 430 | } else { 431 | try { 432 | $inCsvContent = Import-Csv $InputCsvFile -Delimiter $CsvSeparatorChar 433 | if($inCsvContent | Get-Member -Name "DisplayName" -MemberType Properties){ 434 | $filterByTemplateDisplayName = $inCsvContent | Select-Object -ExpandProperty "DisplayName" 435 | } else { 436 | Write-Host "Cannot find the column 'DisplayName' in the CSV content of the file '$InputCsvFile' with separator '$CsvSeparatorChar'" -ForegroundColor Red 437 | exit 438 | } 439 | } 440 | catch { 441 | Write-Host "Cannot read the CSV content of the file '$InputCsvFile' with separator '$CsvSeparatorChar' - ERROR: " $_.Exception.Message -ForegroundColor Red 442 | Write-Debug $_ 443 | exit 444 | } 445 | } 446 | } 447 | 448 | if($askForConfirmation){ 449 | Write-Host " " 450 | if((Read-Host "Type 'y' if you want to continue...") -ne 'y'){ 451 | Write-Host "Exiting..." 452 | exit 453 | } 454 | Write-Host " " 455 | } 456 | 457 | Write-Verbose "---------------------- START OF EXECUTION - $(Get-Date)" 458 | Write-Verbose "SubscriptionId: $SubscriptionId" 459 | Write-Verbose "ResourceGroup: $ResourceGroup" 460 | Write-Verbose "Workspace: $Workspace" 461 | Write-Verbose "Region: $Region" 462 | Write-Verbose "SaveExistingAnalyticRuleTemplatesToCsvFile: $SaveExistingAnalyticRuleTemplatesToCsvFile" 463 | Write-Verbose "OutputAnalyticRuleTemplatesCsvFile: $OutputAnalyticRuleTemplatesCsvFile" 464 | Write-Verbose "CsvSeparatorChar: $CsvSeparatorChar" 465 | Write-Verbose "Simulate: $SimulateOnly" 466 | Write-Verbose "LimitToMaxNumberOfRules: $LimitToMaxNumberOfRules" 467 | Write-Verbose "InputCsvFile: $InputCsvFile" 468 | Write-Verbose "SeveritiesToInclude: $SeveritiesToInclude" 469 | 470 | # Initialize log file 471 | $LogStartTime = Get-Date -Format "yyyy-MM-dd_hh.mm.ss" 472 | $oLogFile = "log_$LogStartTime.log" 473 | "EXECUTION STARTED - $LogStartTime" | Out-File $oLogFile 474 | 475 | # Authenticate to Azure 476 | Connect-AzAccount -DeviceCode | out-null 477 | Write-Verbose "Connected to Azure" 478 | Write-Host "Execution started. Please wait..." 479 | 480 | # Set the current subscription 481 | $context = Set-AzContext -SubscriptionId $subscriptionId 482 | Write-Verbose "Azure Context set successfully" 483 | #Write-Debug "context: $context" 484 | 485 | # Get the Authentication Header for calling the REST APIs 486 | $authHeader = CreateAuthenticationHeader($context.Subscription.TenantId) 487 | Write-Verbose "Authentication header created successfully" 488 | #Write-Debug "authHeader: $authHeader" 489 | 490 | # List all Solutions in Content Hub 491 | $baseUri = "https://management.azure.com/subscriptions/${SubscriptionId}/resourceGroups/${ResourceGroup}/providers/Microsoft.OperationalInsights/workspaces/${Workspace}" 492 | $packagesUrl = $baseUri + "/providers/Microsoft.SecurityInsights/contentProductPackages?api-version=2023-04-01-preview" 493 | #Write-Debug "packagesUrl: $packagesUrl" 494 | $allSolutions = (Invoke-RestMethod -Method "Get" -Uri $packagesUrl -Headers $authHeader ).value 495 | Write-Verbose -Message "Number of Solutions found: $(($allSolutions).Count)"; "Number of Solutions found: $(($allSolutions).Count)" | Out-File $oLogFile -Append 496 | 497 | # List all Analytic Rule Templates which are part of the installed solutions 498 | $templatesUrl = $baseUri + "/providers/Microsoft.SecurityInsights/contentTemplates?api-version=2023-05-01-preview&%24filter=(properties%2FcontentKind%20eq%20'AnalyticsRule')" 499 | #Write-Debug "templatesUrl: $templatesUrl" 500 | $allTemplates = (Invoke-RestMethod -Uri $templatesUrl -Method Get -Headers $authHeader).value 501 | if($SaveExistingAnalyticRuleTemplatesToCsvFile){ 502 | #Initialize CSV file 503 | "DisplayName","Severity","AtLeastOneRuleAlreadyExists","Package" -join $CsvSeparatorChar | Out-File $OutputAnalyticRuleTemplatesCsvFile 504 | } 505 | Write-Verbose -Message "Number of Templates found: $(($allTemplates).Count)"; "Number of Templates found: $(($allTemplates).Count)" | Out-File $oLogFile -Append 506 | 507 | # Iterate through all the Analytic Rule Templates 508 | $NumberOfConsideredTemplates = 0 509 | $NumberOfSkippedTemplates = 0 510 | $NumberOfCreatedRules = 0 511 | $NumberOfErrors = 0 512 | $loopIndex = 0 513 | foreach ($template in $allTemplates ) { 514 | $loopIndex++ | Out-Null 515 | Write-Host "Processing template ($loopIndex)/$(($allTemplates).Count)..." 516 | $NumberOfConsideredTemplates++ | out-null 517 | 518 | # If the Template should be filtered by display name, do it now 519 | if((-not($null -eq $filterByTemplateDisplayName)) -and (-not($filterByTemplateDisplayName.Contains($(($template).properties.displayName))))){ 520 | Write-Verbose "Template skipped (display name not in the input CSV file): '$(($template).properties.displayName)'" 521 | "Template skipped (display name not in the input CSV file): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 522 | $NumberOfSkippedTemplates++ | out-null 523 | continue 524 | } 525 | 526 | # Make sure that the Template's severity is one we want to include 527 | $severity = $template.properties.mainTemplate.resources.properties[0].severity 528 | if ( ($SeveritiesToInclude.Contains($severity)) -or ($SaveExistingAnalyticRuleTemplatesToCsvFile) ) { 529 | try { 530 | #Check if at least an Analytic Rule associated at this templates already exists 531 | Write-Verbose "Template: '$(($template).properties.displayName)' - Searching for existing rules..." 532 | $found = CheckIfAnAnalyticRuleAssociatedToTemplateExist -BaseUri $baseUri -Template $template 533 | if(($found) -and (-not($SaveExistingAnalyticRuleTemplatesToCsvFile))){ 534 | Write-Verbose "Template '$(($template).properties.displayName)' - A rule already exists based on this template" 535 | "Template '$(($template).properties.displayName)' - A rule already exists based on this template" | Out-File $oLogFile -Append 536 | $NumberOfSkippedTemplates++ | out-null 537 | continue #goto next Template in the foreach loop 538 | } 539 | 540 | # Search for the solution containing the Template 541 | Write-Verbose "Template: '$(($template).properties.displayName)' - Searching for containing solution..." 542 | $solution = $allSolutions.properties | Where-Object -Property "contentId" -Contains $template.properties.packageId 543 | #Write-Debug "solution: $solution" 544 | if(($null -eq $solution) -and (-not($SaveExistingAnalyticRuleTemplatesToCsvFile))){ 545 | Write-Verbose "Template '$(($template).properties.displayName)' - UNEXPECTED: solution not found" 546 | "Template '$(($template).properties.displayName)' - UNEXPECTED: solution not found" | Out-File $oLogFile -Append 547 | 548 | $NumberOfErrors++ | out-null 549 | continue #goto next Template in the foreach loop 550 | } 551 | 552 | if($SaveExistingAnalyticRuleTemplatesToCsvFile){ 553 | # Write Template info in CSV file 554 | $(($template).properties.displayName),$severity,$found,$(($solution).displayName) -join $CsvSeparatorChar | Out-File $OutputAnalyticRuleTemplatesCsvFile -Append 555 | continue #goto next Template in the foreach loop 556 | } 557 | 558 | # Create the Analytic Rule from the Template - NOTE: at this point it will have "Source name" = "Gallery Content" 559 | Write-Verbose "Template '$(($template).properties.displayName)' - About to create rule" 560 | $analyticRule = CreateAnalyticRule -BaseUri $baseUri -Template $template -SimulateOnly $SimulateOnly 561 | #Write-Debug "analyticRule: $analyticRule" 562 | "Template '$(($template).properties.displayName)' - Rule created sucessfully" | Out-File $oLogFile -Append 563 | 564 | if($SimulateOnly){ 565 | # Simulate the result of the above command (it is needed in order to simulate the following command) 566 | Write-Verbose "Template '$(($template).properties.displayName)' - SIMULATED - Creating a fake rule" 567 | $analyticRule = New-Object -TypeName PSObject -Property @{ 568 | name = "" 569 | id = "" 570 | displayName = $template.properties.mainTemplate.resources.properties[0].displayName 571 | } 572 | } 573 | 574 | # Modify the metadata of the Analytic Rule so that it is linked as "In use" in the Solution - NOTE: at this point it will have "Source name" = 575 | Write-Verbose "Template '$(($template).properties.displayName)' - About to modify metadata" 576 | $metadataChangeResult = LinkAnalyticRuleToSolution -BaseUri $baseUri -Rule $analyticRule -Template $template -Solution $solution -SimulateOnly $SimulateOnly 577 | #Write-Debug "metadataChangeResult: $metadataChangeResult" 578 | "Template '$(($template).properties.displayName)' - Metadata modified successfully" | Out-File $oLogFile -Append 579 | 580 | $NumberOfCreatedRules++ | out-null 581 | } 582 | catch { 583 | "Template '$(($template).properties.displayName)' - ERROR while creating the rule" | Out-File $oLogFile -Append 584 | "-------------" | Out-File $oLogFile -Append 585 | $_ | Out-File $oLogFile -Append 586 | "-------------" | Out-File $oLogFile -Append 587 | $NumberOfErrors++ | out-null 588 | } 589 | 590 | if(($LimitToMaxNumberOfRules -gt 0) -and ($NumberOfCreatedRules -ge $LimitToMaxNumberOfRules)){ 591 | break 592 | } 593 | } else { 594 | Write-Verbose "Template skipped (severity: '$severity'): '$(($template).properties.displayName)'" 595 | "Template skipped (severity: '$severity'): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 596 | $NumberOfSkippedTemplates++ | out-null 597 | } 598 | } 599 | 600 | 601 | Write-Verbose "---------------------- END OF EXECUTION - $(Get-Date)" 602 | 603 | Write-Host (" ") ; " " | Out-File $oLogFile -Append 604 | Write-Host ("### Summary:") -ForegroundColor Blue; "### Summary:" | Out-File $oLogFile -Append 605 | Write-Host ("") -ForegroundColor Blue 606 | Write-Host (" # of template processed: $NumberOfConsideredTemplates") -ForegroundColor Blue; " # of template processed: $NumberOfConsideredTemplates" | Out-File $oLogFile -Append 607 | if($SaveExistingAnalyticRuleTemplatesToCsvFile){ 608 | Write-Host (" # of template processed with errors: $NumberOfErrors") -ForegroundColor Red; " # of rules processed with errors: $NumberOfErrors" | Out-File $oLogFile -Append 609 | } else { 610 | if(-not($SimulateOnly)){ 611 | Write-Host (" # of rules created: $NumberOfCreatedRules") -ForegroundColor Green; " # of rules created: $NumberOfCreatedRules" | Out-File $oLogFile -Append 612 | } else { 613 | Write-Host (" # of rules created (SIMULATED): $NumberOfCreatedRules") -ForegroundColor Green; " # of rules created (SIMULATED): $NumberOfCreatedRules" | Out-File $oLogFile -Append 614 | } 615 | Write-Host (" # of rules not created because of errors: $NumberOfErrors") -ForegroundColor Red; " # of rules not created because of errors: $NumberOfErrors" | Out-File $oLogFile -Append 616 | Write-Host (" # of templates skipped: $NumberOfSkippedTemplates") -ForegroundColor Gray; " # of template skipped: $NumberOfSkippedTemplates" | Out-File $oLogFile -Append 617 | } 618 | Write-Host ("") -ForegroundColor Blue 619 | 620 | 621 | "EXECUTION ENDED - $LogStartTime" | Out-File $oLogFile -Append 622 | Write-Host "Please check the log file for details: '.\$oLogFile'" -ForegroundColor Blue 623 | 624 | } 625 | 626 | function Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates { 627 | param( 628 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 629 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 630 | [Parameter(Mandatory = $true)][string]$Workspace, 631 | [Parameter(Mandatory = $true)][string]$Region, 632 | [Parameter(Mandatory = $false)][bool]$SaveExistingAnalyticRuleTemplatesToCsvFile = $false, 633 | [Parameter(Mandatory = $false)][string]$OutputAnalyticRuleTemplatesCsvFile, 634 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar, 635 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $false, 636 | [Parameter(Mandatory = $false)][int]$LimitToMaxNumberOfRules = 0, 637 | [Parameter(Mandatory = $false)][string]$InputCsvFile, 638 | [Parameter(Mandatory = $false)][string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 639 | ) 640 | 641 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 642 | -SaveExistingAnalyticRuleTemplatesToCsvFile $SaveExistingAnalyticRuleTemplatesToCsvFile -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile ` 643 | -CsvSeparatorChar $CsvSeparatorChar ` 644 | -Simulate $SimulateOnly ` 645 | -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 646 | -InputCsvFile $InputCsvFile ` 647 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 648 | } 649 | 650 | function Get-SentinelInstalledTemplatesAsCsv{ 651 | param ( 652 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 653 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 654 | [Parameter(Mandatory = $true)][string]$Workspace, 655 | [Parameter(Mandatory = $true)][string]$Region, 656 | [Parameter(Mandatory = $false)][string]$OutputAnalyticRuleTemplatesCsvFile, 657 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar 658 | ) 659 | 660 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 661 | -SaveExistingAnalyticRuleTemplatesToCsvFile $true -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile ` 662 | -CsvSeparatorChar $CsvSeparatorChar -SuppressWarningForExportCsv $true 663 | } 664 | 665 | <# 666 | ############################################################################### 667 | # Launching section - UNCOMMENT AS NEEDED 668 | 669 | ######################## 670 | # Extract the names of the installed Analytic Rules Templates - BEFORE LAUNCHING, set the parameters according to your environment! 671 | # (See also the description of the parameters and the many examples in the initial part of this script file) 672 | ######################## 673 | $SubscriptionId = "" 674 | $ResourceGroup = "" 675 | $Workspace = "" 676 | $Region = "" #(e.g, westeurope) 677 | $OutputAnalyticRuleTemplatesCsvFile = "\.csv" 678 | $CsvSeparatorChar = ';' 679 | 680 | Get-SentinelInstalledTemplatesAsCsv -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 681 | -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile -CsvSeparatorChar $CsvSeparatorChar 682 | 683 | 684 | ######################## 685 | # Create the Analytic Rules or simulate their creation - BEFORE LAUNCHING, set the parameters according to your environment! 686 | # (See also the description of the parameters and the many examples in the initial part of this script file) 687 | ######################## 688 | $SubscriptionId = "" 689 | $ResourceGroup = "" 690 | $Workspace = "" 691 | $Region = "" #(e.g, westeurope) 692 | $SimulateOnly = $false 693 | $LimitToMaxNumberOfRules = 10 694 | $InputCsvFile = "\.csv" 695 | $CsvSeparatorChar = ';' 696 | $SeveritiesToInclude = @("High","Medium") 697 | 698 | Invoke-SentinelAnalyticRulesCreationFromInstalledTemplates -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 699 | -Simulate $SimulateOnly -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 700 | -InputCsvFile $InputCsvFile -CsvSeparatorChar $CsvSeparatorChar ` 701 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 702 | #> 703 | 704 | 705 | 706 | -------------------------------------------------------------------------------- /SentinelAnalyticRulesManagementScript.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script contains cmdlets that automates the massive creation, backup, deletion and update of Analytic Rules in Microsoft Sentinel. 4 | 5 | .DESCRIPTION 6 | Version: 1.0 7 | Release Date: 2024-03-03 8 | Author: Stefano Pescosolido (https://www.linkedin.com/in/stefanopescosolido/) 9 | Short Link: https://aka.ms/sarms 10 | Presentation & demo: https://www.youtube.com/watch?v=5WO6bfpkrTI <--- WATCH IT FOR SEEING THIS SCRIPT IN ACTION!!! 11 | 12 | ############################# 13 | 14 | >> HOW TO LAUNCH IT AND WHAT TO EXPECT? 15 | 16 | 1. Download this script file from GitHub 17 | 2. Search for the string "LAUNCHING SECTION"; it's the final section of this script file. 18 | 3. Set the input parameters and choose the cmdlet to launch based on your environment and needs. 19 | Note: the "LAUNCHING SECTION" has detailed comments on how to set the parameters. 20 | 4. When the parameters are set, open a powershell window and launch the script by calling its file name. 21 | Note: The directory from where you launch the script will contain the output log file. 22 | 5. If you are not blocked by the initial checks of the launching conditions, soon you'll see the classical "device login" message. 23 | Proceed with the authentication to Azure accordingly: open a browser, put the specified device code, authenticate with a valid user. 24 | Note: you need to authenticate with a user having the rights to read and create Analytic Rules and modify Solutions in Sentinel 25 | (min. role: Microsoft Sentinel Contributor). Rights on the local computer may be required to read and write the input/output CSV files. 26 | 6. Wait for the end of the execution while reading the output messages. 27 | 7. Read the final statistics. If needed, read the content of the log file. 28 | ############################# 29 | 30 | .NOTES 31 | The script requires PowerShell 7. 32 | * Check the version of your powershell by using: $PSVersionTable.PSVersion 33 | * Install it by launching: winget search Microsoft.PowerShell 34 | (https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows) 35 | 36 | The script also requires the powershell module Az.Accounts 37 | (https://learn.microsoft.com/en-us/powershell/module/az.accounts) 38 | 39 | Part of the code in this script is taken from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Sentinel-All-In-One 40 | 41 | .DISCLAIMER 42 | This script is provided "as is", without warranty of any kind. 43 | No extensive testing as been made. Use with caution and at your own risk. 44 | 45 | #> 46 | 47 | 48 | 49 | ################################################################################################## 50 | # CMDLET section 51 | ################################################################################################## 52 | 53 | function New-SentinelRules { 54 | param( 55 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 56 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 57 | [Parameter(Mandatory = $true)][string]$Workspace, 58 | [Parameter(Mandatory = $true)][string]$Region, 59 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $false, 60 | [Parameter(Mandatory = $false)][int]$LimitToMaxNumberOfRules = 0, 61 | [Parameter(Mandatory = $false)][string]$InputCsvFile, 62 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar, 63 | [Parameter(Mandatory = $false)][string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 64 | ) 65 | 66 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 67 | -Mode ([ExecutionMode]::NewRules) ` 68 | -Simulate $SimulateOnly ` 69 | -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 70 | -InputCsvFile $InputCsvFile ` 71 | -CsvSeparatorChar $CsvSeparatorChar ` 72 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 73 | } 74 | 75 | function Get-SentinelTemplates{ 76 | param ( 77 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 78 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 79 | [Parameter(Mandatory = $true)][string]$Workspace, 80 | [Parameter(Mandatory = $true)][string]$Region, 81 | [Parameter(Mandatory = $false)][string]$OutputAnalyticRuleTemplatesCsvFile, 82 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar 83 | ) 84 | 85 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 86 | -Mode ([ExecutionMode]::GetTemplates) -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile ` 87 | -CsvSeparatorChar $CsvSeparatorChar -SuppressWarningForExportCsv $true 88 | } 89 | 90 | function Get-SentinelRules{ 91 | param ( 92 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 93 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 94 | [Parameter(Mandatory = $true)][string]$Workspace, 95 | [Parameter(Mandatory = $true)][string]$Region, 96 | [Parameter(Mandatory = $false)][string]$OutputRulesCsvFile, 97 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar 98 | ) 99 | 100 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 101 | -Mode ([ExecutionMode]::GetRules) -OutputRulesCsvFile $OutputRulesCsvFile ` 102 | -CsvSeparatorChar $CsvSeparatorChar -SuppressWarningForExportCsv $true 103 | } 104 | 105 | function Export-SentinelRules{ 106 | param ( 107 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 108 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 109 | [Parameter(Mandatory = $true)][string]$Workspace, 110 | [Parameter(Mandatory = $true)][string]$Region, 111 | [Parameter(Mandatory = $true)][string]$BackupFolder, 112 | [Parameter(Mandatory = $false)][string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 113 | ) 114 | 115 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 116 | -Mode ([ExecutionMode]::BackupRules) ` 117 | -BackupFolder $BackupFolder ` 118 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 119 | } 120 | 121 | function Remove-SentinelRules{ 122 | param ( 123 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 124 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 125 | [Parameter(Mandatory = $true)][string]$Workspace, 126 | [Parameter(Mandatory = $true)][string]$Region, 127 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $false, 128 | [Parameter(Mandatory = $false)][int]$LimitToMaxNumberOfRules = 0, 129 | [Parameter(Mandatory = $false)][string]$BackupFolder, 130 | [Parameter(Mandatory = $false)][string]$InputCsvFile, 131 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar, 132 | [Parameter(Mandatory = $false)][string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 133 | ) 134 | 135 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 136 | -Mode ([ExecutionMode]::DeleteRules) ` 137 | -Simulate $SimulateOnly ` 138 | -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 139 | -BackupFolder $BackupFolder ` 140 | -InputCsvFile $InputCsvFile ` 141 | -CsvSeparatorChar $CsvSeparatorChar ` 142 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 143 | } 144 | 145 | function Update-SentinelRules{ 146 | param ( 147 | [Parameter(Mandatory = $true)][string]$SubscriptionId, 148 | [Parameter(Mandatory = $true)][string]$ResourceGroup, 149 | [Parameter(Mandatory = $true)][string]$Workspace, 150 | [Parameter(Mandatory = $true)][string]$Region, 151 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $false, 152 | [Parameter(Mandatory = $false)][int]$LimitToMaxNumberOfRules = 0, 153 | [Parameter(Mandatory = $false)][string]$BackupFolder, 154 | [Parameter(Mandatory = $false)][string]$InputCsvFile, 155 | [Parameter(Mandatory = $false)][char]$CsvSeparatorChar, 156 | [Parameter(Mandatory = $false)][string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 157 | ) 158 | 159 | ExecuteRequest -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 160 | -Mode ([ExecutionMode]::UpdateRules) ` 161 | -Simulate $SimulateOnly ` 162 | -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 163 | -BackupFolder $BackupFolder ` 164 | -InputCsvFile $InputCsvFile ` 165 | -CsvSeparatorChar $CsvSeparatorChar ` 166 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 167 | } 168 | 169 | ################################################################################################## 170 | # Helper functions section 171 | ################################################################################################## 172 | 173 | function CreateAuthenticationHeader { 174 | param ( 175 | [Parameter(Mandatory = $true)][string]$TenantId, 176 | [Parameter(Mandatory = $false)][string]$PrefixInDisplayName 177 | ) 178 | $instanceProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile 179 | $profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($instanceProfile) 180 | $token = $profileClient.AcquireAccessToken($TenantId) 181 | $authNHeader = @{ 182 | 'Content-Type' = 'application/json' 183 | 'Authorization' = 'Bearer ' + $token.AccessToken 184 | } 185 | 186 | return $authNHeader 187 | } 188 | 189 | function CreateAnalyticRule { 190 | param ( 191 | [Parameter(Mandatory = $true)][object]$AuthHeader, 192 | [Parameter(Mandatory = $true)][string]$BaseUri, 193 | [Parameter(Mandatory = $true)][object]$Template, 194 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $true 195 | ) 196 | 197 | #Ref. https://learn.microsoft.com/en-us/rest/api/securityinsights/alert-rules/create-or-update?view=rest-securityinsights-2023-02-01&tabs=HTTP 198 | 199 | #$alertUri = "$BaseUri/providers/Microsoft.SecurityInsights/alertRules/" 200 | $BaseAlertUri = $BaseUri + "/providers/Microsoft.SecurityInsights/alertRules/" 201 | 202 | $kind = $Template.properties.mainTemplate.resources.kind 203 | $displayName = $Template.properties.mainTemplate.resources.properties[0].displayName 204 | $eventGroupingSettings = $Template.properties.mainTemplate.resources.properties[0].eventGroupingSettings 205 | if ($null -eq $eventGroupingSettings) { 206 | $eventGroupingSettings = [ordered]@{aggregationKind = "SingleAlert" } 207 | } 208 | $body = "" 209 | $properties = $Template.properties.mainTemplate.resources[0].properties 210 | $properties.enabled = $true 211 | #Add the field to link this rule with the rule template so that the rule template will show up as used 212 | #We had to use the "Add-Member" command since this field does not exist in the rule template that we are copying from. 213 | $properties | Add-Member -NotePropertyName "alertRuleTemplateName" -NotePropertyValue $Template.properties.mainTemplate.resources[0].name 214 | $properties | Add-Member -NotePropertyName "templateVersion" -NotePropertyValue $Template.properties.mainTemplate.resources[1].properties.version 215 | 216 | 217 | #Depending on the type of alert we are creating, the body has different parameters 218 | switch ($kind) { 219 | "MicrosoftSecurityIncidentCreation" { 220 | $body = @{ 221 | "kind" = "MicrosoftSecurityIncidentCreation" 222 | "properties" = $properties 223 | } 224 | } 225 | "NRT" { 226 | $body = @{ 227 | "kind" = "NRT" 228 | "properties" = $properties 229 | } 230 | } 231 | "Scheduled" { 232 | $body = @{ 233 | "kind" = "Scheduled" 234 | "properties" = $properties 235 | } 236 | 237 | } 238 | Default { } 239 | } 240 | #If we have created the body... 241 | if ("" -ne $body) { 242 | #Create the GUId for the alert and create it. 243 | $guid = (New-Guid).Guid 244 | #Create the URI we need to create the alert. 245 | $alertUri = $BaseAlertUri + $guid + "?api-version=2022-12-01-preview" 246 | try { 247 | Write-Verbose -Message "Template: $displayName - Creating the rule...." 248 | 249 | if(-not($SimulateOnly)){ 250 | $rule = Invoke-RestMethod -Uri $alertUri -Method Put -Headers $AuthHeader -Body ($body | ConvertTo-Json -EnumsAsStrings -Depth 50) 251 | Write-Host -Message "Template: $displayName - Creating the rule - Succeeded" -ForegroundColor Green 252 | #This pauses for 1 second so that we don't overload the workspace. 253 | Start-Sleep -Seconds 1 254 | } 255 | else { 256 | Write-Host -Message "Template: $displayName - Creating the rule - Succeeded (SIMULATED)" -ForegroundColor Green 257 | } 258 | 259 | } 260 | catch { 261 | Write-Verbose "Template: $displayName - ERROR while creating the rule:" 262 | Write-Verbose $_ 263 | #Write-Host -Message "Template: $displayName - ERROR while creating the rule: $(($_).Exception.Message)" -ForegroundColor Red 264 | Write-Host -Message "Template: $displayName - ERROR while creating the rule" -ForegroundColor Red 265 | throw 266 | } 267 | } 268 | 269 | return $rule 270 | } 271 | 272 | function LinkAnalyticRuleToSolution { 273 | param ( 274 | [Parameter(Mandatory = $true)][object]$AuthHeader, 275 | [Parameter(Mandatory = $true)][string]$BaseUri, 276 | [Parameter(Mandatory = $true)][object]$Rule, 277 | [Parameter(Mandatory = $true)][object]$Template, 278 | [Parameter(Mandatory = $true)][object]$Solution, 279 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $true 280 | ) 281 | 282 | $baseMetaURI = $BaseUri + "/providers/Microsoft.SecurityInsights/metadata/analyticsrule-" 283 | 284 | $metabody = @{ 285 | "apiVersion" = "2023-02-01" 286 | "name" = "analyticsrule-" + $Rule.name 287 | "type" = "Microsoft.OperationalInsights/workspaces/providers/metadata" 288 | "id" = $null 289 | "properties" = @{ 290 | "contentId" = $Template.properties.mainTemplate.resources[0].name 291 | "parentId" = $Rule.id 292 | "kind" = "AnalyticsRule" 293 | "version" = $Template.properties.mainTemplate.resources.properties[1].version 294 | "source" = $Solution.source 295 | "author" = $Solution.author 296 | "support" = $Solution.support 297 | } 298 | } 299 | Write-Verbose -Message "Rule: $(($Rule).displayName) - Updating metadata...." 300 | $metaURI = $baseMetaURI + $Rule.name + "?api-version=2023-02-01" 301 | try { 302 | if(-not($SimulateOnly)){ 303 | $metaVerdict = Invoke-RestMethod -Uri $metaURI -Method Put -Headers $AuthHeader -Body ($metabody | ConvertTo-Json -EnumsAsStrings -Depth 5) 304 | Write-Host -Message "Rule: $(($Rule).properties.displayName) - Updating metadata - Succeeded" -ForegroundColor Green 305 | #This pauses for 1 second so that we don't overload the workspace. 306 | Start-Sleep -Seconds 1 307 | } else { 308 | Write-Host -Message "Rule: $(($Rule).properties.displayName) - Updating metadata - Succeeded (SIMULATED)" -ForegroundColor Green 309 | } 310 | 311 | } 312 | catch { 313 | Write-Verbose "Rule: $(($Rule).displayName) - ERROR while updating metadata:" 314 | Write-Verbose $_ 315 | #Write-Host -Message "Rule: $(($Rule).displayName) - ERROR while updating metadata: $(($_).Exception.Message)" -ForegroundColor Red 316 | Write-Host -Message "Rule: $(($Rule).displayName) - ERROR while updating metadata" -ForegroundColor Red 317 | throw 318 | } 319 | return $metaVerdict 320 | 321 | } 322 | 323 | function DeleteRule { 324 | param ( 325 | [Parameter(Mandatory = $true)][object]$AuthHeader, 326 | [Parameter(Mandatory = $true)][string]$BaseUri, 327 | [Parameter(Mandatory = $true)][object]$Rule, 328 | [Parameter(Mandatory = $false)][bool]$SimulateOnly = $true 329 | ) 330 | 331 | $baseMetaURI = $BaseUri + "/providers/Microsoft.SecurityInsights/alertRules/" 332 | 333 | Write-Verbose -Message "Rule: $(($Rule).displayName) - About to be deleted...." 334 | $fullURI = $baseMetaURI + $Rule.name + "?api-version=2023-02-01" 335 | try { 336 | if(-not($SimulateOnly)){ 337 | Invoke-RestMethod -Uri $fullURI -Method Delete -Headers $AuthHeader 338 | Write-Host -Message "Rule: $(($Rule).properties.displayName) - Delete - Succeeded" -ForegroundColor Green 339 | #This pauses for 1 second so that we don't overload the workspace. 340 | Start-Sleep -Seconds 1 341 | } else { 342 | Write-Host -Message "Rule: $(($Rule).properties.displayName) - Delete - Succeeded (SIMULATED)" -ForegroundColor Green 343 | } 344 | } 345 | catch { 346 | Write-Verbose "Rule: $(($Rule).displayName) - ERROR while deleting the rule:" 347 | Write-Verbose $_ 348 | #Write-Host -Message "Rule: $(($Rule).displayName) - ERROR while deleting the rule: $(($_).Exception.Message)" -ForegroundColor Red 349 | Write-Host -Message "Rule: $(($Rule).displayName) - ERROR while deleting the rule" -ForegroundColor Red 350 | throw 351 | } 352 | } 353 | 354 | function IsFirstVersionLowerThanSecondVersion { 355 | param ( 356 | [Parameter(Mandatory = $true)][string]$v1, 357 | [Parameter(Mandatory = $true)][string]$v2 358 | ) 359 | 360 | $res = $false 361 | 362 | $a1 = $v1.Split(".") | ForEach-Object { [int]$_ } 363 | $a2 = $v2.Split(".") | ForEach-Object { [int]$_ } 364 | 365 | # Get the length of the shortest array 366 | $minLength = [Math]::Min($a1.Length, $a2.Length) 367 | 368 | # Compare the elements of the two arrays 369 | for ($i = 0; $i -lt $minLength; $i++) { 370 | if ($a1[$i] -lt $a2[$i]) { 371 | $res = $true 372 | break; 373 | } 374 | } 375 | 376 | # If all elements are equal, compare the length of the arrays 377 | if ($i -eq $minLength) { 378 | if ($a1.Length -lt $a2.Length) { 379 | $res = $true 380 | } 381 | } 382 | 383 | return $res 384 | } 385 | 386 | function CheckIfAnAnalyticRuleAssociatedToTemplateExist { 387 | param ( 388 | [Parameter(Mandatory = $true)][object]$AuthHeader, 389 | [Parameter(Mandatory = $true)][string]$BaseUri, 390 | [Parameter(Mandatory = $true)][object]$Template 391 | ) 392 | 393 | $uri = $BaseUri + "/providers/Microsoft.SecurityInsights/alertRules?api-version=2023-02-01" 394 | 395 | $allRules = (Invoke-RestMethod -Uri $uri -Method Get -Headers $AuthHeader).value 396 | 397 | $found = $false 398 | foreach($rule in $allRules){ 399 | if($rule.properties.alertRuleTemplateName -eq $Template.properties.mainTemplate.resources[0].name){ 400 | $found = $true 401 | break 402 | } 403 | } 404 | 405 | return $found 406 | 407 | } 408 | 409 | function GetAnalyticRulesAssociatedToTemplate { 410 | param ( 411 | [Parameter(Mandatory = $true)][object]$AuthHeader, 412 | [Parameter(Mandatory = $true)][string]$BaseUri, 413 | [Parameter(Mandatory = $true)][object]$Template 414 | ) 415 | 416 | $res = $null 417 | $uri = $BaseUri + "/providers/Microsoft.SecurityInsights/alertRules?api-version=2023-02-01" 418 | 419 | $allRules = (Invoke-RestMethod -Uri $uri -Method Get -Headers $AuthHeader).value 420 | 421 | foreach($rule in $allRules){ 422 | if($rule.properties.alertRuleTemplateName -eq $Template.properties.mainTemplate.resources[0].name){ 423 | if($null -eq $res){ 424 | $res = New-Object System.Collections.ArrayList 425 | } 426 | $res.Add($rule) 427 | } 428 | } 429 | 430 | return $res 431 | 432 | } 433 | 434 | function ExportAnalyticRulesAsArmTemplate { 435 | param ( 436 | [Parameter(Mandatory = $true)][object]$Rule, 437 | [Parameter(Mandatory = $true)][string]$filePath 438 | ) 439 | 440 | $jsonRule = $Rule | ConvertTo-Json -Depth 100 441 | 442 | #Add the outer elements to ensure that the file can be processed in the Azure portal as valid custom template (https://portal.azure.com/#create/Microsoft.Template) 443 | $ruleAsJsonArmTemplate = @" 444 | { 445 | `"`$schema`": `"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#`", 446 | `"contentVersion`": `"1.0.0.0`", 447 | `"resources`": [ 448 | RULE_PLACEHOLDER 449 | ] 450 | } 451 | "@ 452 | 453 | $finalJson = $ruleAsJsonArmTemplate -replace "RULE_PLACEHOLDER", $jsonRule 454 | 455 | #Adjust to ensure that the file can be processed in the Azure portal as valid custom template (https://portal.azure.com/#create/Microsoft.Template) 456 | $o = $finalJson | ConvertFrom-Json 457 | $ruleId = $o.resources[0].name 458 | $o.resources[0].name = "sentinel-central/Microsoft.SecurityInsights/$ruleId" 459 | $o.resources[0].type = "Microsoft.OperationalInsights/workspaces/providers/alertRules" 460 | $o.resources[0] | Add-Member -Type NoteProperty -Name "apiVersion" -Value "2022-11-01-preview" 461 | $o.resources[0].PSObject.Properties.Remove("etag") 462 | $o.resources[0].properties.PSObject.Properties.Remove("lastModifiedUtc") 463 | $finalJson = $o | ConvertTo-Json -Depth 100 464 | 465 | #Write to file 466 | $finalJson | Out-File $filePath 467 | 468 | Write-Host -Message "Rule: $($o.resources[0].properties.displayName) - Backup - Succeeded" -ForegroundColor Green 469 | } 470 | 471 | enum ExecutionMode{ 472 | GetTemplates 473 | GetRules 474 | NewRules 475 | BackupRules 476 | DeleteRules 477 | UpdateRules 478 | } 479 | 480 | function ExecuteRequest 481 | ( 482 | [string]$SubscriptionId, 483 | [string]$ResourceGroup, 484 | [string]$Workspace, 485 | [string]$Region, 486 | [ExecutionMode] $Mode, 487 | [string]$OutputAnalyticRuleTemplatesCsvFile, 488 | [string]$OutputRulesCsvFile, 489 | [char]$CsvSeparatorChar, 490 | [bool]$SimulateOnly = $false, 491 | [int]$LimitToMaxNumberOfRules = 0, 492 | [string]$BackupFolder, 493 | [string]$InputCsvFile, 494 | [string[]]$SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 495 | ) 496 | { 497 | ########### Check of the launch conditiona and of input parameters ########### 498 | 499 | # Check installed PowerShell version 500 | if($PSVersionTable.PSVersion.Major -lt 7){ 501 | Write-Host "This cmdlet requires PowerShell 7. Exiting..." -ForegroundColor Red 502 | exit 503 | } 504 | 505 | #Check if Az.Accounts is installed 506 | $module = Get-Module -ListAvailable -Name Az.Accounts 507 | if($null -eq $module){ 508 | Write-Host "The module 'Az.Accounts' is required and is not installed." -ForegroundColor Red 509 | Write-Host "To install it, open PowerShell as and Administrator and execute the following command: " -ForegroundColor Red 510 | Write-Host "Install-Module -Name Az.Accounts" -ForegroundColor Red 511 | Write-Host "Exiting..." -ForegroundColor Red 512 | exit 513 | } 514 | 515 | # Set default values for $CsvSeparatorChar if not set as parameter 516 | if( ([string]::IsNullOrEmpty($CsvSeparatorChar)) -or (([byte]$CsvSeparatorChar) -eq 0) ) { 517 | try { 518 | $CsvSeparatorChar = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ListSeparator 519 | } 520 | catch { 521 | $CsvSeparatorChar = ',' 522 | } 523 | } 524 | 525 | # Set default values for $SeveritiesToInclude if not set as parameter 526 | if(($Mode -ne ([ExecutionMode]::GetTemplates)) -and ($Mode -ne ([ExecutionMode]::GetRules)) -and ([string]::IsNullOrEmpty($SeveritiesToInclude))){ 527 | # By default, all severities are included 528 | $SeveritiesToInclude = @("Informational", "Low", "Medium", "High") 529 | } 530 | 531 | # Check the coherence of the input parameters 532 | $askForConfirmation = $false 533 | 534 | if(($Mode -eq ([ExecutionMode]::GetTemplates)) -and ([string]::IsNullOrEmpty($OutputAnalyticRuleTemplatesCsvFile))){ 535 | Write-Host "Missing value for the input parameter 'OutputAnalyticRuleTemplatesCsvFile'. Exiting..." -ForegroundColor Red 536 | exit 537 | } 538 | 539 | if(($Mode -eq ([ExecutionMode]::GetRules)) -and ([string]::IsNullOrEmpty($OutputRulesCsvFile))){ 540 | Write-Host "Missing value for the input parameter 'OutputRulesCsvFile'. Exiting..." -ForegroundColor Red 541 | exit 542 | } 543 | 544 | if(($Mode -eq ([ExecutionMode]::GetTemplates)) -and (-not([string]::IsNullOrEmpty($OutputAnalyticRuleTemplatesCsvFile)))){ 545 | if(Test-Path($OutputAnalyticRuleTemplatesCsvFile)){ 546 | Write-Host "NOTE: The file '$OutputAnalyticRuleTemplatesCsvFile' already exists and will be overwritten." -ForegroundColor Blue -BackgroundColor Yellow 547 | $askForConfirmation = $true 548 | } 549 | $folder = Split-Path -Parent $OutputAnalyticRuleTemplatesCsvFile 550 | if(-not(Test-Path($folder))){ 551 | Write-Host "The folder '$folder' specified in the input parameter 'OutputAnalyticRuleTemplatesCsvFile' does not exist. Exiting..." -ForegroundColor Red 552 | exit 553 | } 554 | } 555 | 556 | if(($Mode -eq ([ExecutionMode]::GetRules)) -and (-not([string]::IsNullOrEmpty($OutputRulesCsvFile)))){ 557 | if(Test-Path($OutputRulesCsvFile)){ 558 | Write-Host "NOTE: The file '$OutputRulesCsvFile' already exists and will be overwritten." -ForegroundColor Blue -BackgroundColor Yellow 559 | $askForConfirmation = $true 560 | } 561 | $folder = Split-Path -Parent $OutputRulesCsvFile 562 | if(-not(Test-Path($folder))){ 563 | Write-Host "The folder '$folder' specified in the input parameter 'OutputRulesCsvFile' does not exist. Exiting..." -ForegroundColor Red 564 | exit 565 | } 566 | } 567 | 568 | if(($Mode -eq ([ExecutionMode]::UpdateRules)) -or ($Mode -eq ([ExecutionMode]::DeleteRules)) -or ($Mode -eq ([ExecutionMode]::BackupRules))){ 569 | if(-not([string]::IsNullOrEmpty($BackupFolder))){ 570 | if(-not(Test-Path($BackupFolder) -PathType Container)){ 571 | Write-Host "The folder '$BackupFolder' specified in the input parameter 'BackupFolder' does not exist. Exiting..." -ForegroundColor Red 572 | exit 573 | } 574 | } 575 | } 576 | 577 | if($SimulateOnly){ 578 | if($Mode -eq ([ExecutionMode]::NewRules)){ 579 | Write-Host "NOTE: when the input parameter 'SimulateOnly' is set to 'true', no Analytic Rule will be created but you can see - in the output messages and in the log file - what rule would be created" -ForegroundColor Blue -BackgroundColor Yellow 580 | } 581 | if($Mode -eq ([ExecutionMode]::DeleteRules)){ 582 | Write-Host "NOTE: when the input parameter 'SimulateOnly' is set to 'true', no Analytic Rule will be deleted but you can see - in the output messages and in the log file - what rule would be deleted" -ForegroundColor Blue -BackgroundColor Yellow 583 | } 584 | if($Mode -eq ([ExecutionMode]::UpdateRules)){ 585 | Write-Host "NOTE: when the input parameter 'SimulateOnly' is set to 'true', no Analytic Rule will be updated but you can see - in the output messages and in the log file - what rule would be updated" -ForegroundColor Blue -BackgroundColor Yellow 586 | } 587 | $askForConfirmation = $true 588 | } 589 | 590 | $inCsvContent = $null 591 | $filterByTemplateDisplayName = $null 592 | $filterByRuleID = $null 593 | if(-not([string]::IsNullOrEmpty($InputCsvFile))){ 594 | if(-not(Test-Path($InputCsvFile))){ 595 | Write-Host "The input file specified in the input parameter 'InputCsvFile' does not exist. Exiting..." -ForegroundColor Red 596 | exit 597 | } else { 598 | try { 599 | $inCsvContent = Import-Csv $InputCsvFile -Delimiter $CsvSeparatorChar 600 | 601 | if($Mode -eq ([ExecutionMode]::NewRules)){ 602 | if($inCsvContent | Get-Member -Name "DisplayName" -MemberType Properties){ 603 | $filterByTemplateDisplayName = $inCsvContent | Select-Object -ExpandProperty "DisplayName" 604 | }else{ 605 | Write-Host "Cannot find the column 'DisplayName' in the CSV content of the file '$InputCsvFile' with separator '$CsvSeparatorChar'" -ForegroundColor Red 606 | exit 607 | } 608 | 609 | } 610 | 611 | if( ($Mode -eq ([ExecutionMode]::DeleteRules)) -or ($Mode -eq ([ExecutionMode]::UpdateRules)) ){ 612 | if($inCsvContent | Get-Member -Name "RuleID" -MemberType Properties){ 613 | $filterByRuleID = $inCsvContent | Select-Object -ExpandProperty "RuleID" 614 | }else{ 615 | Write-Host "Cannot find the column 'RuleID' in the CSV content of the file '$InputCsvFile' with separator '$CsvSeparatorChar'" -ForegroundColor Red 616 | exit 617 | } 618 | 619 | } 620 | 621 | } 622 | catch { 623 | Write-Host "Cannot read the CSV content of the file '$InputCsvFile' with separator '$CsvSeparatorChar' - ERROR: " $_.Exception.Message -ForegroundColor Red 624 | Write-Debug $_ 625 | exit 626 | } 627 | } 628 | } 629 | 630 | if($askForConfirmation){ 631 | Write-Host " " 632 | if((Read-Host "Type 'y' if you want to continue...") -ne 'y'){ 633 | Write-Host "Exiting..." 634 | exit 635 | } 636 | Write-Host " " 637 | } 638 | 639 | ########### Start of execution ########### 640 | 641 | # Initialize log file 642 | $LogStartTime = Get-Date -Format "yyyy-MM-dd_HH.mm.ss" 643 | $oLogFile = "log_$LogStartTime.log" 644 | 645 | Write-Verbose "---------------------- EXECUTION STARTED - $(Get-Date)" ; "---------------------- EXECUTION STARTED - $LogStartTime" | Out-File $oLogFile 646 | Write-Verbose "SubscriptionId: $SubscriptionId"; "SubscriptionId: $SubscriptionId" | Out-File $oLogFile -Append 647 | Write-Verbose "ResourceGroup: $ResourceGroup"; "ResourceGroup: $ResourceGroup" | Out-File $oLogFile -Append 648 | Write-Verbose "Workspace: $Workspace"; "Workspace: $Workspace" | Out-File $oLogFile -Append 649 | Write-Verbose "Region: $Region"; "Region: $Region" | Out-File $oLogFile -Append 650 | Write-Verbose "Mode: $Mode"; "Mode: $Mode" | Out-File $oLogFile -Append 651 | Write-Verbose "OutputAnalyticRuleTemplatesCsvFile: $OutputAnalyticRuleTemplatesCsvFile"; "OutputAnalyticRuleTemplatesCsvFile: $OutputAnalyticRuleTemplatesCsvFile" | Out-File $oLogFile -Append 652 | Write-Verbose "OutputRulesCsvFile: $OutputRulesCsvFile"; "OutputRulesCsvFile: $OutputRulesCsvFile" | Out-File $oLogFile -Append 653 | Write-Verbose "CsvSeparatorChar: $CsvSeparatorChar"; "CsvSeparatorChar: $CsvSeparatorChar" | Out-File $oLogFile -Append 654 | Write-Verbose "Simulate: $SimulateOnly"; "Simulate: $SimulateOnly" | Out-File $oLogFile -Append 655 | Write-Verbose "LimitToMaxNumberOfRules: $LimitToMaxNumberOfRules"; "LimitToMaxNumberOfRules: $LimitToMaxNumberOfRules" | Out-File $oLogFile -Append 656 | Write-Verbose "InputCsvFile: $InputCsvFile"; "InputCsvFile: $InputCsvFile" | Out-File $oLogFile -Append 657 | Write-Verbose "SeveritiesToInclude: $SeveritiesToInclude"; "SeveritiesToInclude: $SeveritiesToInclude" | Out-File $oLogFile -Append 658 | 659 | 660 | # Authenticate to Azure 661 | try { 662 | Connect-AzAccount -DeviceCode -ErrorAction Stop | out-null 663 | } 664 | catch { 665 | Write-Host "Could not connect to Azure with the provided Device Code. Please retry.... - ERROR: " $_.Exception.Message -ForegroundColor Red 666 | Write-Debug $_ 667 | exit 668 | } 669 | 670 | Write-Verbose "Connected to Azure" 671 | Write-Host "Execution started. Please wait..." 672 | 673 | # Set the current subscription 674 | $context = Set-AzContext -SubscriptionId $subscriptionId 675 | Write-Verbose "Azure Context set successfully" 676 | #Write-Debug "context: $context" 677 | 678 | # Get the Authentication Header for calling the REST APIs 679 | $authHeader = $null 680 | 681 | try { 682 | $authHeader = CreateAuthenticationHeader($context.Subscription.TenantId) 683 | if($null -eq $authHeader){ 684 | throw "Authentication Header is null" 685 | } 686 | Write-Verbose "Authentication header created successfully" 687 | #Write-Debug "authHeader: $authHeader" 688 | } 689 | catch { 690 | Write-Host "Could not create the Authentication Header - ERROR: " $_.Exception.Message -ForegroundColor Red 691 | Write-Debug $_ 692 | Write-Host "Exiting..." 693 | exit 694 | } 695 | 696 | if($Mode -eq ([ExecutionMode]::GetTemplates)){ 697 | #Initialize CSV file 698 | "DisplayName","Severity","AtLeastOneRuleAlreadyExists","Package" -join $CsvSeparatorChar | Out-File $OutputAnalyticRuleTemplatesCsvFile 699 | } 700 | 701 | if($Mode -eq ([ExecutionMode]::GetRules)){ 702 | #Initialize CSV file 703 | "RuleDisplayName","RuleID","Enabled","UpdateAvailable","RuleTemplateVersion","TemplatePackage","TemplateDisplayName","TemplateSeverity","TemplateVersion" -join $CsvSeparatorChar | Out-File $OutputRulesCsvFile 704 | } 705 | 706 | 707 | # List all Solutions in Content Hub 708 | $baseUri = "https://management.azure.com/subscriptions/${SubscriptionId}/resourceGroups/${ResourceGroup}/providers/Microsoft.OperationalInsights/workspaces/${Workspace}" 709 | $packagesUrl = $baseUri + "/providers/Microsoft.SecurityInsights/contentProductPackages?api-version=2023-04-01-preview" 710 | #Write-Debug "packagesUrl: $packagesUrl" 711 | $allSolutions = (Invoke-RestMethod -Method "Get" -Uri $packagesUrl -Headers $authHeader ).value 712 | Write-Verbose -Message "Number of Solutions found: $(($allSolutions).Count)"; "Number of Solutions found: $(($allSolutions).Count)" | Out-File $oLogFile -Append 713 | 714 | # List all Analytic Rule Templates which are part of the installed solutions 715 | $templatesUrl = $baseUri + "/providers/Microsoft.SecurityInsights/contentTemplates?api-version=2023-05-01-preview&%24filter=(properties%2FcontentKind%20eq%20'AnalyticsRule')" 716 | #Write-Debug "templatesUrl: $templatesUrl" 717 | $allTemplates = (Invoke-RestMethod -Uri $templatesUrl -Method Get -Headers $authHeader).value 718 | 719 | Write-Verbose -Message "Number of Templates found: $(($allTemplates).Count)"; "Number of Templates found: $(($allTemplates).Count)" | Out-File $oLogFile -Append 720 | 721 | # Iterate through all the Analytic Rule Templates 722 | $NumberOfConsideredTemplates = 0 723 | $NumberOfSkippedTemplates = 0 724 | $NumberOfSkippedRules = 0 725 | $NumberOfCreatedRules = 0 726 | $NumberOfDeletedRules = 0 727 | $NumberOfUpdatedRules = 0 728 | $NumberOfErrors = 0 729 | $loopIndex = 0 730 | 731 | #Initializing dictionary of rules to be updated 732 | $allRulesNeedingUpdates = $null 733 | 734 | foreach ($template in $allTemplates ) { 735 | $loopIndex++ | Out-Null 736 | Write-Host "Processing template ($loopIndex)/$(($allTemplates).Count)..." 737 | $NumberOfConsideredTemplates++ | out-null 738 | 739 | # If the Template should be filtered by display name, do it now 740 | if((-not($null -eq $filterByTemplateDisplayName)) -and (-not($filterByTemplateDisplayName.Contains($(($template).properties.displayName))))){ 741 | Write-Verbose "Template skipped (display name not in the input CSV file): '$(($template).properties.displayName)'" 742 | "Template skipped (display name not in the input CSV file): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 743 | $NumberOfSkippedTemplates++ | out-null 744 | continue #goto next Template in the foreach loop 745 | } 746 | 747 | # Make sure that the Template's severity is one we want to include 748 | $severity = $template.properties.mainTemplate.resources.properties[0].severity 749 | if ( ($SeveritiesToInclude.Contains($severity)) -or ($Mode -eq ([ExecutionMode]::GetTemplates)) -or ($Mode -eq ([ExecutionMode]::GetRules)) ) { 750 | try { 751 | 752 | if( ($Mode -eq ([ExecutionMode]::NewRules)) -or ($Mode -eq ([ExecutionMode]::GetTemplates)) ) { 753 | #Check if at least an Analytic Rule associated to this templates already exists. 754 | Write-Verbose "Template: '$(($template).properties.displayName)' - Searching for existing rules..." 755 | $found = CheckIfAnAnalyticRuleAssociatedToTemplateExist -AuthHeader $authHeader -BaseUri $baseUri -Template $template 756 | if(($found) -and ($Mode -ne ([ExecutionMode]::GetTemplates)) -and ($Mode -ne ([ExecutionMode]::GetRules)) ){ 757 | Write-Verbose "Template '$(($template).properties.displayName)' - A rule already exists based on this template" 758 | "Template '$(($template).properties.displayName)' - A rule already exists based on this template" | Out-File $oLogFile -Append 759 | $NumberOfSkippedTemplates++ | out-null 760 | 761 | #No need to find all the rules associated to this template: the evidence that at least one exists is enough to understand that no new rules should be created for this template 762 | continue #goto next Template in the foreach loop 763 | } 764 | } 765 | 766 | # Search for the solution containing the Template 767 | Write-Verbose "Template: '$(($template).properties.displayName)' - Searching for the solution containing the template..." 768 | $solution = $allSolutions.properties | Where-Object -Property "contentId" -Contains $template.properties.packageId 769 | #Write-Debug "solution: $solution" 770 | 771 | if($null -eq $solution){ 772 | Write-Verbose "Template '$(($template).properties.displayName)' - UNEXPECTED: solution not found" 773 | "Template '$(($template).properties.displayName)' - UNEXPECTED: solution not found" | Out-File $oLogFile -Append 774 | 775 | $NumberOfErrors++ | out-null 776 | continue #goto next Template in the foreach loop 777 | } 778 | 779 | if($Mode -eq ([ExecutionMode]::GetTemplates)){ 780 | # Write Template info in CSV file 781 | $(($template).properties.displayName),$severity,$found,$(($solution).displayName) -join $CsvSeparatorChar | Out-File $OutputAnalyticRuleTemplatesCsvFile -Append 782 | continue #goto next Template in the foreach loop 783 | } 784 | 785 | if($Mode -eq ([ExecutionMode]::GetRules)){ 786 | # Search for any existing Rule associated to this Template. If the Rule require update, add it to the dictionary of Rules requiring updates 787 | $rulesAssociatedToThisTemplate = $null 788 | $rulesAssociatedToThisTemplate = GetAnalyticRulesAssociatedToTemplate -AuthHeader $authHeader -BaseUri $baseUri -Template $template 789 | 790 | if($null -ne $rulesAssociatedToThisTemplate){ 791 | if($null -eq $allRulesNeedingUpdates){ 792 | $allRulesNeedingUpdates = New-Object 'System.Collections.Generic.Dictionary[String,System.Collections.ArrayList]' 793 | } 794 | $allRulesNeedingUpdates.Add(($template).properties.displayName,$rulesAssociatedToThisTemplate) 795 | 796 | # Write Rules info in CSV file 797 | $rulesAssociatedToThisTemplate.ForEach({ 798 | if(-not([string]::IsNullOrEmpty(($_).properties.displayName))){ 799 | $toBeUpdated = IsFirstVersionLowerThanSecondVersion -v1 ($_).properties.templateVersion -v2 $template.properties.version 800 | 801 | (($_).properties.displayName),(($_).name),(($_).properties.enabled),$toBeUpdated,(($_).properties.templateVersion),$(($solution).displayName),$(($template).properties.displayName),$severity,$(($template).properties.version) -join $CsvSeparatorChar | Out-File $OutputRulesCsvFile -Append 802 | } 803 | }) 804 | } 805 | continue #goto next Template in the foreach loop 806 | } 807 | 808 | if($Mode -eq [ExecutionMode]::NewRules){ 809 | # Create the Analytic Rule from the Template - NOTE: at this point it will have "Source name" = "Gallery Content" 810 | Write-Verbose "Template '$(($template).properties.displayName)' - About to create rule" 811 | $analyticRule = CreateAnalyticRule -AuthHeader $authHeader -BaseUri $baseUri -Template $template -SimulateOnly $SimulateOnly 812 | #Write-Debug "analyticRule: $analyticRule" 813 | "Template '$(($template).properties.displayName)' - Rule created sucessfully" | Out-File $oLogFile -Append 814 | 815 | if($SimulateOnly){ 816 | # Simulate the result of the above command (it is needed in order to simulate the following command) 817 | Write-Verbose "Template '$(($template).properties.displayName)' - SIMULATED - Creating a fake rule" 818 | $analyticRule = New-Object -TypeName PSObject -Property @{ 819 | name = "" 820 | id = "" 821 | displayName = $template.properties.mainTemplate.resources.properties[0].displayName 822 | } 823 | } 824 | 825 | # Modify the metadata of the Analytic Rule so that it is linked as "In use" in the Solution - NOTE: at this point it will have "Source name" = 826 | Write-Verbose "Template '$(($template).properties.displayName)' - About to modify metadata" 827 | LinkAnalyticRuleToSolution -AuthHeader $authHeader -BaseUri $baseUri -Rule $analyticRule -Template $template -Solution $solution -SimulateOnly $SimulateOnly 828 | #Write-Debug "metadataChangeResult: $metadataChangeResult" 829 | "Template '$(($template).properties.displayName)' - Metadata modified successfully" | Out-File $oLogFile -Append 830 | 831 | $NumberOfCreatedRules++ | out-null 832 | } 833 | 834 | if($Mode -eq [ExecutionMode]::BackupRules){ 835 | # Backup the Analytic Rules 836 | 837 | $rulesAssociatedToThisTemplate = $null 838 | $rulesAssociatedToThisTemplate = GetAnalyticRulesAssociatedToTemplate -AuthHeader $authHeader -BaseUri $baseUri -Template $template 839 | 840 | if($null -eq $rulesAssociatedToThisTemplate){ 841 | Write-Verbose "Template skipped (no rules exist): '$(($template).properties.displayName)'" 842 | "Template skipped (no rules exist): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 843 | $NumberOfSkippedTemplates++ | out-null 844 | } 845 | else{ 846 | $rulesAssociatedToThisTemplate.ForEach({ 847 | 848 | if(-not([string]::IsNullOrEmpty(($_).properties.displayName))){ 849 | 850 | if((-not($null -eq $filterByRuleID)) -and (-not($filterByRuleID.Contains( (($_).name) )))){ 851 | Write-Verbose "Rule skipped (ID not in the input CSV file): '$(($_).properties.displayName)','$(($_).name)','$(($template).properties.displayName)'" 852 | "Rule skipped (ID not in the input CSV file): '$(($_).properties.displayName)','$(($_).name)','$(($template).properties.displayName)'" | Out-File $oLogFile -Append 853 | $NumberOfSkippedRules++ | out-null 854 | } else { 855 | 856 | if(-not([string]::IsNullOrEmpty($BackupFolder))){ 857 | try { 858 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Backup - About to backup the rule" | Out-File $oLogFile -Append 859 | $ruleFilePath = Join-Path -Path $BackupFolder -ChildPath "$(($_).name)_$LogStartTime.json" 860 | ExportAnalyticRulesAsArmTemplate -Rule $_ -filePath $ruleFilePath 861 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Backup - Rule backup completed sucessfully" | Out-File $oLogFile -Append 862 | } 863 | catch { 864 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Backup - Backup step - ERROR " | Out-File $oLogFile -Append 865 | "-------------" | Out-File $oLogFile -Append 866 | $_ | Out-File $oLogFile -Append 867 | "-------------" | Out-File $oLogFile -Append 868 | $NumberOfErrors++ | out-null 869 | } 870 | } 871 | } 872 | } 873 | }) 874 | } 875 | } 876 | 877 | if($Mode -eq [ExecutionMode]::DeleteRules){ 878 | # Delete the Analytic Rules 879 | 880 | $rulesAssociatedToThisTemplate = $null 881 | $rulesAssociatedToThisTemplate = GetAnalyticRulesAssociatedToTemplate -AuthHeader $authHeader -BaseUri $baseUri -Template $template 882 | 883 | if($null -eq $rulesAssociatedToThisTemplate){ 884 | Write-Verbose "Template skipped (no rules exist): '$(($template).properties.displayName)'" 885 | "Template skipped (no rules exist): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 886 | $NumberOfSkippedTemplates++ | out-null 887 | } 888 | else{ 889 | $rulesAssociatedToThisTemplate.ForEach({ 890 | 891 | if(-not([string]::IsNullOrEmpty(($_).properties.displayName))){ 892 | 893 | if((-not($null -eq $filterByRuleID)) -and (-not($filterByRuleID.Contains( (($_).name) )))){ 894 | Write-Verbose "Rule skipped (ID not in the input CSV file): '$(($_).properties.displayName)','$(($_).name)','$(($template).properties.displayName)'" 895 | "Rule skipped (ID not in the input CSV file): '$(($_).properties.displayName)','$(($_).name)','$(($template).properties.displayName)'" | Out-File $oLogFile -Append 896 | $NumberOfSkippedRules++ | out-null 897 | } else { 898 | $backupError = $true 899 | if([string]::IsNullOrEmpty($BackupFolder)){ 900 | $backupError = $false 901 | } 902 | else{ 903 | try { 904 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Delete - About to backup the rule" | Out-File $oLogFile -Append 905 | $ruleFilePath = Join-Path -Path $BackupFolder -ChildPath "$(($_).name)_$LogStartTime.json" 906 | ExportAnalyticRulesAsArmTemplate -Rule $_ -filePath $ruleFilePath 907 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Delete - Rule backup completed sucessfully" | Out-File $oLogFile -Append 908 | $backupError = $false 909 | } 910 | catch { 911 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Delete - Backup step - ERROR " | Out-File $oLogFile -Append 912 | "-------------" | Out-File $oLogFile -Append 913 | $_ | Out-File $oLogFile -Append 914 | "-------------" | Out-File $oLogFile -Append 915 | $NumberOfErrors++ | out-null 916 | } 917 | } 918 | 919 | if(-not($backupError)){ 920 | Write-Verbose "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Delete - About to delete rule" 921 | try { 922 | DeleteRule -AuthHeader $authHeader -BaseUri $BaseUri -Rule ($_) -SimulateOnly $SimulateOnly 923 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Delete - Rule deleted sucessfully" | Out-File $oLogFile -Append 924 | $NumberOfDeletedRules++ | Out-Null 925 | } 926 | catch { 927 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Delete - ERROR " | Out-File $oLogFile -Append 928 | "-------------" | Out-File $oLogFile -Append 929 | $_ | Out-File $oLogFile -Append 930 | "-------------" | Out-File $oLogFile -Append 931 | $NumberOfErrors++ | out-null 932 | } 933 | } 934 | } 935 | } 936 | }) 937 | } 938 | } 939 | 940 | if($Mode -eq [ExecutionMode]::UpdateRules){ 941 | # Delete the Analytic Rules 942 | 943 | $rulesAssociatedToThisTemplate = $null 944 | $rulesAssociatedToThisTemplate = GetAnalyticRulesAssociatedToTemplate -AuthHeader $authHeader -BaseUri $baseUri -Template $template 945 | 946 | if($null -eq $rulesAssociatedToThisTemplate){ 947 | Write-Verbose "Template skipped (no rules exist): '$(($template).properties.displayName)'" 948 | "Template skipped (no rules exist): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 949 | $NumberOfSkippedTemplates++ | out-null 950 | } 951 | else{ 952 | $rulesAssociatedToThisTemplate.ForEach({ 953 | 954 | if(-not([string]::IsNullOrEmpty(($_).name))){ 955 | 956 | if((-not($null -eq $filterByRuleID)) -and (-not($filterByRuleID.Contains( (($_).name) )))){ 957 | Write-Verbose "Rule skipped (ID not in the input CSV file): '$(($_).properties.displayName)','$(($_).name)','$(($template).properties.displayName)'" 958 | "Rule skipped (ID not in the input CSV file): '$(($_).properties.displayName)','$(($_).name)','$(($template).properties.displayName)'" | Out-File $oLogFile -Append 959 | $NumberOfSkippedRules++ | out-null 960 | } else { 961 | $toBeUpdated = IsFirstVersionLowerThanSecondVersion -v1 ($_).properties.templateVersion -v2 $template.properties.version 962 | if(-not($toBeUpdated)){ 963 | Write-Verbose "Rule skipped (update not needed): '$(($_).properties.displayName)','$(($_).name)','$(($_).properties.templateVersion)','$(($template).properties.displayName)','$($template.properties.version)'" 964 | "Rule skipped (update not needed): '$(($_).properties.displayName)','$(($_).name)','$(($_).properties.templateVersion)','$(($template).properties.displayName)','$($template.properties.version)'" | Out-File $oLogFile -Append 965 | $NumberOfSkippedRules++ | out-null 966 | } 967 | else{ 968 | 969 | #Update Rule section: Backup, Delete, Re-Create 970 | $backupError = $true 971 | if([string]::IsNullOrEmpty($BackupFolder)){ 972 | $backupError = $false 973 | } 974 | else{ 975 | try { 976 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - About to backup the rule" | Out-File $oLogFile -Append 977 | $ruleFilePath = Join-Path -Path $BackupFolder -ChildPath "$(($_).name)_$LogStartTime.json" 978 | ExportAnalyticRulesAsArmTemplate -Rule $_ -filePath $ruleFilePath 979 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Rule backup completed sucessfully" | Out-File $oLogFile -Append 980 | $backupError = $false 981 | } 982 | catch { 983 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Backup step - ERROR " | Out-File $oLogFile -Append 984 | "-------------" | Out-File $oLogFile -Append 985 | $_ | Out-File $oLogFile -Append 986 | "-------------" | Out-File $oLogFile -Append 987 | $NumberOfErrors++ | out-null 988 | } 989 | } 990 | 991 | if(-not($backupError)){ 992 | Write-Verbose "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - About to update rule in two steps: delete the old one, add the updated one" 993 | try { 994 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - About to delete the rule" | Out-File $oLogFile -Append 995 | DeleteRule -AuthHeader $authHeader -BaseUri $BaseUri -Rule ($_) -SimulateOnly $SimulateOnly 996 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Rule deleted sucessfully" | Out-File $oLogFile -Append 997 | } 998 | catch { 999 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Delete step - ERROR " | Out-File $oLogFile -Append 1000 | "-------------" | Out-File $oLogFile -Append 1001 | $_ | Out-File $oLogFile -Append 1002 | "-------------" | Out-File $oLogFile -Append 1003 | $NumberOfErrors++ | out-null 1004 | } 1005 | 1006 | try { 1007 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - About to recreate the rule" | Out-File $oLogFile -Append 1008 | # Create the Analytic Rule from the Template - NOTE: at this point it will have "Source name" = "Gallery Content" 1009 | Write-Verbose "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - About to recreate the rule" 1010 | $analyticRule = CreateAnalyticRule -AuthHeader $authHeader -BaseUri $baseUri -Template $template -SimulateOnly $SimulateOnly 1011 | #Write-Debug "analyticRule: $analyticRule" 1012 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Rule recreated sucessfully" | Out-File $oLogFile -Append 1013 | 1014 | if($SimulateOnly){ 1015 | # Simulate the result of the above command (it is needed in order to simulate the following command) 1016 | $analyticRule = ($_) 1017 | } 1018 | 1019 | # Modify the metadata of the Analytic Rule so that it is linked as "In use" in the Solution - NOTE: at this point it will have "Source name" = 1020 | Write-Verbose "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - About to modify metadata" 1021 | LinkAnalyticRuleToSolution -AuthHeader $authHeader -BaseUri $baseUri -Rule $analyticRule -Template $template -Solution $solution -SimulateOnly $SimulateOnly 1022 | #Write-Debug "metadataChangeResult: $metadataChangeResult" 1023 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Rule metadata modified sucessfully" | Out-File $oLogFile -Append 1024 | } 1025 | catch { 1026 | "Template '$(($template).properties.displayName)' - Rule '$(($_).name)' / '$(($_).properties.displayName)' - Update - Re-create step - ERROR " | Out-File $oLogFile -Append 1027 | "-------------" | Out-File $oLogFile -Append 1028 | $_ | Out-File $oLogFile -Append 1029 | "-------------" | Out-File $oLogFile -Append 1030 | $NumberOfErrors++ | out-null 1031 | } 1032 | 1033 | $NumberOfUpdatedRules++ | out-null 1034 | } 1035 | } 1036 | } 1037 | } 1038 | }) 1039 | } 1040 | } 1041 | } 1042 | catch { 1043 | "Template '$(($template).properties.displayName)' - ERROR " | Out-File $oLogFile -Append 1044 | "-------------" | Out-File $oLogFile -Append 1045 | $_ | Out-File $oLogFile -Append 1046 | "-------------" | Out-File $oLogFile -Append 1047 | $NumberOfErrors++ | out-null 1048 | } 1049 | 1050 | if(($LimitToMaxNumberOfRules -gt 0) -and ( ($NumberOfCreatedRules -ge $LimitToMaxNumberOfRules) -or ($NumberOfDeletedRules -ge $LimitToMaxNumberOfRules) -or ($NumberOfUpdatedRules -ge $LimitToMaxNumberOfRules))){ 1051 | break 1052 | } 1053 | } else { 1054 | Write-Verbose "Template skipped (severity: '$severity'): '$(($template).properties.displayName)'" 1055 | "Template skipped (severity: '$severity'): '$(($template).properties.displayName)'" | Out-File $oLogFile -Append 1056 | $NumberOfSkippedTemplates++ | out-null 1057 | } 1058 | } 1059 | 1060 | ######## Final logs and messages 1061 | 1062 | if($null -ne $allRulesNeedingUpdates){ 1063 | Write-Host (" ") ; " " | Out-File $oLogFile -Append 1064 | Write-Host ("### Rules to be updated:") -ForegroundColor Blue 1065 | $allRulesNeedingUpdates.Keys.ForEach({ 1066 | Write-Host " " 1067 | Write-Host "......................................................." 1068 | Write-Host "Template: $_" 1069 | Write-Host "Rules:" 1070 | $allRulesNeedingUpdates[$_].ForEach({ 1071 | if(-not([string]::IsNullOrEmpty(($_).properties.displayName))){ 1072 | Write-Host "> " ($_).properties.displayName 1073 | } 1074 | }) 1075 | }) 1076 | Write-Host "......................................................." 1077 | Write-Host (" ") ; " " | Out-File $oLogFile -Append 1078 | } 1079 | 1080 | Write-Host (" ") ; " " | Out-File $oLogFile -Append 1081 | Write-Host ("### Summary:") -ForegroundColor Blue; "### Summary:" | Out-File $oLogFile -Append 1082 | Write-Host ("") -ForegroundColor Blue 1083 | Write-Host (" # of templates processed: $NumberOfConsideredTemplates") -ForegroundColor Blue; " # of templates processed: $NumberOfConsideredTemplates" | Out-File $oLogFile -Append 1084 | if( ($Mode -eq ([ExecutionMode]::GetTemplates)) -or ($Mode -eq ([ExecutionMode]::GetRules)) ) { 1085 | Write-Host (" # of templates processed with errors: $NumberOfErrors") -ForegroundColor Red; " # of rules processed with errors: $NumberOfErrors" | Out-File $oLogFile -Append 1086 | } elseif ($Mode -eq ([ExecutionMode]::NewRules)) { 1087 | if(-not($SimulateOnly)){ 1088 | Write-Host (" # of rules created: $NumberOfCreatedRules") -ForegroundColor Green; " # of rules created: $NumberOfCreatedRules" | Out-File $oLogFile -Append 1089 | } else { 1090 | Write-Host (" # of rules created (SIMULATED): $NumberOfCreatedRules") -ForegroundColor Green; " # of rules created (SIMULATED): $NumberOfCreatedRules" | Out-File $oLogFile -Append 1091 | } 1092 | Write-Host (" # of rules not created because of errors: $NumberOfErrors") -ForegroundColor Red; " # of rules not created because of errors: $NumberOfErrors" | Out-File $oLogFile -Append 1093 | }elseif ($Mode -eq ([ExecutionMode]::DeleteRules)) { 1094 | if(-not($SimulateOnly)){ 1095 | Write-Host (" # of rules deleted: $NumberOfDeletedRules") -ForegroundColor Green; " # of rules deleted: $NumberOfDeletedRules" | Out-File $oLogFile -Append 1096 | } else { 1097 | Write-Host (" # of rules deleted (SIMULATED): $NumberOfDeletedRules") -ForegroundColor Green; " # of rules deleted (SIMULATED): $NumberOfDeletedRules" | Out-File $oLogFile -Append 1098 | } 1099 | Write-Host (" # of rules not deleted because of errors: $NumberOfErrors") -ForegroundColor Red; " # of rules not deleted because of errors: $NumberOfErrors" | Out-File $oLogFile -Append 1100 | }elseif ($Mode -eq ([ExecutionMode]::UpdateRules)) { 1101 | if(-not($SimulateOnly)){ 1102 | Write-Host (" # of rules updated: $NumberOfUpdatedRules") -ForegroundColor Green; " # of rules updated: $NumberOfUpdatedRules" | Out-File $oLogFile -Append 1103 | } else { 1104 | Write-Host (" # of rules updated (SIMULATED): $NumberOfUpdatedRules") -ForegroundColor Green; " # of rules updated (SIMULATED): $NumberOfUpdatedRules" | Out-File $oLogFile -Append 1105 | } 1106 | Write-Host (" # of rules not updated because of errors: $NumberOfErrors") -ForegroundColor Red; " # of rules not updated because of errors: $NumberOfErrors" | Out-File $oLogFile -Append 1107 | } 1108 | Write-Host (" # of templates skipped: $NumberOfSkippedTemplates") -ForegroundColor Gray; " # of template skipped: $NumberOfSkippedTemplates" | Out-File $oLogFile -Append 1109 | Write-Host (" # of rules skipped: $NumberOfSkippedRules") -ForegroundColor Gray; " # of rules skipped: $NumberOfSkippedRules" | Out-File $oLogFile -Append 1110 | Write-Host ("") -ForegroundColor Blue 1111 | Write-Host "Please check the log file for details: '.\$oLogFile'" -ForegroundColor Blue 1112 | $LogEndTime = Get-Date -Format "yyyy-MM-dd_hh.mm.ss" 1113 | Write-Verbose "---------------------- EXECUTION COMPLETE - $(Get-Date)"; "---------------------- EXECUTION COMPLETE - $LogEndTime" | Out-File $oLogFile -Append 1114 | } 1115 | 1116 | 1117 | 1118 | 1119 | ################################################################################################## 1120 | # 1121 | # >> LAUNCHING SECTION << 1122 | # 1123 | # CONSIDER THIS SECTION AS A SET OF EXAMPLES ON HOW TO CALL THE DIFFERENT CMDLETs 1124 | # 1125 | # BEFORE LAUNCHING, set the all the parameters according to your environment and needs! 1126 | # 1127 | # --> Step 1: Set the common environment parameter and chose the cmdlet to be executed 1128 | # 1129 | # --> Step 2: Set the input parameter specific for the chosen cmdlet 1130 | # 1131 | ################################################################################################## 1132 | 1133 | 1134 | # --> Step 1. SET THE COMMON ENVIRONMENT PARAMETER AND THE CHOOSE THE CMDLET BY SETTING THE "EXECUTION MODE" VARIABLE 1135 | 1136 | $SubscriptionId = "" # Mandatory (GUID as string) 1137 | $ResourceGroup = "" # Mandatory (string) 1138 | $Workspace = "" # Mandatory (string) 1139 | $Region = "" # Mandatory (string) - E.g.: westeurope 1140 | $CsvSeparatorChar = $null # Optional (char) - Default: separator for the local culture - E.g. ';' 1141 | 1142 | $execMode = $null # Set as "safe default" --> When launching the script without setting $execMode, nothing happens! 1143 | 1144 | # IMPORTANT: uncomment (only!) one of the following five lines for setting the desired execution mode (the last one uncommented will be executed!) 1145 | #$execMode = [ExecutionMode]::GetTemplates # Set the additional parameters for Get-SentinelTemplates (see below) 1146 | #$execMode = [ExecutionMode]::GetRules # Set the additional parameters for Get-SentinelRules (see below) 1147 | #$execMode = [ExecutionMode]::NewRules # Set the additional parameters for New-SentinelRules (see below) 1148 | #$execMode = [ExecutionMode]::BackupRules # Set the additional parameters for Remove-SentinelRules (see below) 1149 | #$execMode = [ExecutionMode]::DeleteRules # Set the additional parameters for Remove-SentinelRules (see below) 1150 | #$execMode = [ExecutionMode]::UpdateRules # Set the additional parameters for Update-SentinelRules (see below) 1151 | 1152 | 1153 | # --> Step 2. SET THE SPECIFIC PARAMETER FOR THE CHOOSEN CMDLET (EXECUTION MODE) 1154 | 1155 | ######################## --> Execution Mode: GetTemplates 1156 | # The Get-SentinelTemplates cmdlet exports in a CSV file the details of the Analytic Rules Templates existing in the workspace. 1157 | # NOTE: the cmdlet considers only the Templates installed by Content Hub Solutions. 1158 | # Templates installed directly from the gallery (old approach in Sentinel) are ignored. 1159 | ######################## 1160 | $OutputAnalyticRuleTemplatesCsvFile = "" # Mandatory (string) - File local full path 1161 | 1162 | if(($execMode) -eq ([ExecutionMode]::GetTemplates)){ 1163 | Get-SentinelTemplates -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 1164 | -OutputAnalyticRuleTemplatesCsvFile $OutputAnalyticRuleTemplatesCsvFile -CsvSeparatorChar $CsvSeparatorChar 1165 | } 1166 | 1167 | ######################## --> Execution Mode: GetRules 1168 | # The Get-SentinelRules cmdlet exports in a CSV file the details of the Analytic Rules that have been created in the workspace. 1169 | # NOTE: the script considers only the Rules created from the Templates installed by Content Hub Solutions. 1170 | ######################## 1171 | $OutputRulesCsvFile = "" # Mandatory (string) - File local full path 1172 | 1173 | if(($execMode) -eq ([ExecutionMode]::GetRules)){ 1174 | 1175 | Get-SentinelRules -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 1176 | -OutputRulesCsvFile $OutputRulesCsvFile -CsvSeparatorChar $CsvSeparatorChar 1177 | 1178 | } 1179 | 1180 | ######################## --> Execution Mode: NewRules 1181 | # The New-SentinelRules cmdlet creates one Analytic Rule for each of the Templates identified by the input parameters. 1182 | # NOTES: 1183 | # - The execution can be simulated (no Rules are created but their simulated creation is traced in the log file) 1184 | # - No Rules are created for the Templates that have already at least a Rule associated 1185 | # - The Templates to be considered can be filtered by Severity (one or more) and/or by DisplayNames (in an input CSV file) 1186 | # - The (optional) input CSV file can be created starting from the result of Get-SentinelTemplates 1187 | # - The execution can be interrupted after a maximum number of Rules created (useful for testing purposes) 1188 | # 1189 | # IMPORTANT: before launching the New-SentinelRules cmdlet, it is recommended to update all the relevant Solutions in Content Hub. 1190 | ######################## 1191 | $SimulateOnly = $true # Optional (boolean) - Default value: $false (Pay attention!) 1192 | $LimitToMaxNumberOfRules = 1 # Optional (integer) - Use $null or a negative number for unlimited execution 1193 | $InputCsvFile = "" # Optional (string) - File local full path 1194 | $SeveritiesToInclude = @("Low") # Optional (array of strings) - Default: @("Informational", "Low", "Medium", "High") 1195 | 1196 | if(($execMode) -eq ([ExecutionMode]::NewRules)){ 1197 | 1198 | New-SentinelRules -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 1199 | -Simulate $SimulateOnly -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 1200 | -InputCsvFile $InputCsvFile -CsvSeparatorChar $CsvSeparatorChar ` 1201 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 1202 | 1203 | } 1204 | 1205 | ######################## --> Execution Mode: BackupRules 1206 | # The Export-SentinelRules cmdlet saves the json files of the Rules associated to the Templates identified by one or more Severity values. 1207 | # NOTES: 1208 | # - The Templates to be considered can be filtered by Severity (one or more). 1209 | # If not filtered, all the "installed Templates" will be considered (the installed Templates are the ones of the installed Content Hub Solutions) 1210 | # - It is necessary to specify a folder where the script has to backup (as json ARM template) the Rules. 1211 | # 1212 | ######################## 1213 | $BackupFolder = "" # Mandatory (string) - Full path of a local existing folder 1214 | $SeveritiesToInclude = $null # @("Low","Medium") # Optional (array of strings) - Default: @("Informational", "Low", "Medium", "High") 1215 | 1216 | if(($execMode) -eq ([ExecutionMode]::BackupRules)){ 1217 | 1218 | Export-SentinelRules -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 1219 | -BackupFolder $BackupFolder ` 1220 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 1221 | 1222 | } 1223 | 1224 | ######################## --> Execution Mode: DeleteRules 1225 | # The Remove-SentinelRules cmdlet deletes all or some of the Analytic Rules for each of the Templates identified by one or more Severity values. 1226 | # NOTES: 1227 | # - The execution can be simulated (no Rules are deleted but their simulated deletion is traced in the log file) 1228 | # - The Templates to be considered can be filtered by Severity (one or more). 1229 | # If not filtered, all the "installed Templates" will be considered (the installed Templates are the ones of the installed Content Hub Solutions) 1230 | # - The Rules to be considered for each of these Templates can be filtered by their ID (in the input CSV file) 1231 | # If not filtered, all the "existing Rules" for these Templates will be considered (the existing Rules are the ones referring to these Templates) 1232 | # - The (optional) input CSV file can be created starting from the result of Get-SentinelRules 1233 | # - The execution can be interrupted after a maximum number of Rules deleted (useful for testing purposes) 1234 | # - It is possible to specify a folder where the script has to backup (as json ARM template) the Rules before their deletion. 1235 | # In that case, if the backup of a Rule fails for any reason, the Rule is not deleted. 1236 | # 1237 | # IMPORTANT: it is highly recommended to pass the optional input CSV file as parameter to have a better control 1238 | # on what will be deleted! 1239 | ######################## 1240 | $SimulateOnly = $true # Optional (boolean) - Default value: $false (Pay attention!) 1241 | $LimitToMaxNumberOfRules = $null # Optional (integer) - Use $null or a negative number for unlimited execution 1242 | $BackupFolder = "" # Mandatory (string) - Full path of a local existing folder 1243 | $InputCsvFile = "" # Optional (string) - File local full path 1244 | $SeveritiesToInclude = @("Low","Medium") # Optional (array of strings) - Default: @("Informational", "Low", "Medium", "High") 1245 | 1246 | if(($execMode) -eq ([ExecutionMode]::DeleteRules)){ 1247 | 1248 | Remove-SentinelRules -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 1249 | -Simulate $SimulateOnly -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 1250 | -BackupFolder $BackupFolder ` 1251 | -InputCsvFile $InputCsvFile -CsvSeparatorChar $CsvSeparatorChar ` 1252 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 1253 | 1254 | } 1255 | 1256 | 1257 | ######################## --> Execution Mode: UpdateRules 1258 | # The Update-SentinelRules cmdlet updates all or some of the Analytic Rules for each of the Templates identified by one or more Severity values. 1259 | # NOTES: 1260 | # - The execution can be simulated (no Rules are updated but their simulated update is traced in the log file) 1261 | # - The Templates to be considered can be filtered by Severity (one or more) 1262 | # If not filtered, all the "installed Templates" will be considered (the installed Templates are the ones of the installed Content Hub Solutions) 1263 | # - The Rules to be considered for each of these Templates can be filtered by their ID (in the input CSV file) 1264 | # If not filtered, all the "existing Rules" for these Templates will be considered (the existing Rules are the ones referring to these Templates) 1265 | # - The execution can be interrupted after a maximum number of Rules updated (useful for testing purposes) 1266 | # - The update is practically executed as a sequence of a delete of the existing Rule and a creation of the new Rule based 1267 | # on the updated version of the same Template. 1268 | # - It is possible to specify a folder where the script has to backup (as json ARM template) the Rules before their deletion (update). 1269 | # In that case, if the backup of a Rule fails for any reason, the Rule is not deleted (updated). 1270 | # 1271 | # IMPORTANT: it is highly recommended to pass the optional input CSV file as parameter to have a better control 1272 | # on what will be updated! 1273 | # 1274 | # SUPER IMPORTANT: if you need to update a Rule that you have customized somehow, you should keep track of your customizations 1275 | # becase you'll need to re-apply them manually. 1276 | # In that case, you can specify the backup folder so that you have also a backup of the Rulesfrom the script. 1277 | # DO NOT UPDATE CUSTOMIZED RULES IF YOU DO NOT HAVE A BACKUP OF YOUR CUSTOMIZATION! 1278 | ######################## 1279 | $SimulateOnly = $true # Optional (boolean) - Default value: $false (Pay attention!) 1280 | $LimitToMaxNumberOfRules = $null # Optional (integer) - Use $null or a negative number for unlimited execution 1281 | $BackupFolder = "" # Mandatory (string) - Full path of a local existing folder 1282 | $InputCsvFile = "" # Optional (string) - File local full path 1283 | $SeveritiesToInclude = @("Medium") # Optional (array of strings) - Default: @("Informational", "Low", "Medium", "High") 1284 | 1285 | if(($execMode) -eq ([ExecutionMode]::UpdateRules)){ 1286 | 1287 | Update-SentinelRules -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Workspace $Workspace -Region $Region ` 1288 | -Simulate $SimulateOnly -LimitToMaxNumberOfRules $LimitToMaxNumberOfRules ` 1289 | -BackupFolder $BackupFolder ` 1290 | -InputCsvFile $InputCsvFile -CsvSeparatorChar $CsvSeparatorChar ` 1291 | -SeveritiesToInclude $SeveritiesToInclude #-verbose 1292 | 1293 | } 1294 | --------------------------------------------------------------------------------