├── .gitignore ├── .vscode └── settings.json ├── README.md ├── RunTest-CICD.ps1 ├── RunTest.ps1 ├── images └── SentinelPesterFramework.png ├── src └── WorkspaceHelper.ps1 └── tests ├── AnalyticsRules └── AnalyticsRules.Tests.ps1 ├── CICD ├── AnalyticsRules-CICD.Tests.ps1 ├── AutomationRules-CICD.Tests.ps1 └── Watchlists-CICD.Tests.ps1 ├── Configuration ├── OfficeConsents.Tests.ps1 ├── SecurityMLAnalytics.Tests.ps1 ├── SentinelConfiguration.Tests.ps1 └── WorkspaceConfiguration.Tests.ps1 ├── DataConnectors ├── AzureADIdentityProtection.Tests.ps1 ├── AzureActiveDirectory.Tests.ps1 ├── AzureAudit.Tests.ps1 ├── DNS.Tests.ps1 ├── DataConnectorsCheckRequirements.Tests.ps1 ├── DefenderForCloud.Tests.ps1 ├── Microsoft365Defender.Tests.ps1 ├── MicrosoftDefenderForCloudApps.Tests.ps1 ├── Office365.Tests.ps1 ├── SecurityEvents.Tests.ps1 ├── ThreatIntelligenceIndicator.Tests.ps1 ├── WindowsDNSEventsViaAMA.Tests.ps1 ├── WindowsEvents.Tests.ps1 └── WindowsFirewall.Tests.ps1 └── Watchlists └── Watchlists.Tests.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | testResults.xml 2 | config.json 3 | /Sentinel/ 4 | /src/Get-PesterTag.ps1 5 | /src/Get-PesterTest.ps1 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powershell.codeFormatting.newLineAfterCloseBrace": false, 3 | "powershell.codeFormatting.trimWhitespaceAroundPipe": true, 4 | "powershell.codeFormatting.useCorrectCasing": true, 5 | "powershell.codeFormatting.whitespaceBetweenParameters": true 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentinel Pester Framework 2 | 3 | ![Sentinel Pester Framework](/images/SentinelPesterFramework.png) 4 | 5 | The Sentinel Pester Framework is a community project meant to help you use PowerShell and [Pester](https://pester.dev/) to test your [Microsoft Sentinel](https://learn.microsoft.com/azure/sentinel/) infrastructure. 6 | 7 | You can find additional information in the related [blog Post](https://cloudbrothers.info/en/sentinel-pester-framework) on my website [cloudbrother.info](https://cloudbrothers.info/en/). 8 | 9 | There are multiple tests for different configuration settings of Microsoft Sentinel and the Log Analytics Workspace as well as tests for thing like Analytics Rules, Automation Actions, Dataconnectors and more. 10 | 11 | This is not meant as a ready to execute solution, but must be configured and modified for your specific environment. 12 | 13 | If you already use a [CI/CD pipeline](https://learn.microsoft.com/en-gb/azure/sentinel/ci-cd) to deploy your Microsoft Sentinel configuration, you can use specific CI/CD related tests. Those tests are configured to use the ARM templates used to deploy the artifacts and dynamically create the necessary tests. 14 | 15 | ## Configuration 16 | 17 | All configuration is done in the Pester test files itself with only a few exceptions. 18 | 19 | * `workspaceName` \ 20 | Microsoft Sentinel Workspace Id 21 | * `resourceGroup` \ 22 | Resource group of the Sentinel Workspace 23 | * `subscriptionId` ß 24 | Subscription Id 25 | * `CICDPathRoot` \ 26 | The path to your Sentinel configuration files deployed via CI/CD (must be ARM templates) 27 | 28 | ```powershell 29 | $configRunContainer = New-PesterContainer -Path "*.Tests.ps1" -Data @{ 30 | workspaceName = "SentinelWorkspace" 31 | resourceGroup = "ResourceGroup" 32 | subscriptionId = "SubscriptionId" 33 | } 34 | ``` 35 | 36 | ## Test tags 37 | 38 | All tests are [tagged](https://pester.dev/docs/usage/tags) and this allows you to easily include or exclude certain tests. 39 | 40 | Modify the `RunTests.ps1` accordingly. 41 | 42 | | Tag | Description | 43 | |--------------------|------------------------------------------------------------| 44 | | configuration | Sentinel Configuration: All entries | 45 | | anomalies | Sentinel Configuration: Anomalies | 46 | | diagnosticsettings | Sentinel Configuration: Diagnostic Settings | 47 | | entityanalytics | Sentinel Configuration: Entity Analytics | 48 | | eyeson | Sentinel Configuration: Opt-Out of Microsoft data access | 49 | | ueba | Sentinel Configuration: User and Entity Behavior Analytics | 50 | | analyticsrules | Analytics rules | 51 | | watchlists | Watchlists | 52 | | dataconnector | Test all data connector (Not recommended) | 53 | | aad | Azure Active Directory | 54 | | aadipc | Azure AD Identity Protection | 55 | | azureactivity | Azure Audit | 56 | | DataConnectorsReqs | Data Connectors Check Requirements | 57 | | dfc | Microsoft Defender for Cloud | 58 | | dns | DNS | 59 | | m365d | Microsoft 365 Defender | 60 | | mda | Microsoft Defender for Cloud Apps | 61 | | mda-shadowit | Microsoft Defender for Cloud Apps - Shadow IT Reporting | 62 | | o365 | Office 365 | 63 | | o365-sharepoint | Office 365 - SharePoint and OneDrive | 64 | | o365-exchange | Office 365 - Exchange | 65 | | o365-teams | Office 365 - SharePoint and OneDrive | 66 | | securityevents | Security Events | 67 | | sentinel | Sentinel basic configuration | 68 | | ti | Threat Intelligence Platforms | 69 | | amadns | Windows DNS Events via AMA | 70 | | winevents | Windows Forwarded Events | 71 | | winfirewall | Windows Firewall | 72 | | workspace | Workspace basic configuration | 73 | 74 | ## Manual changes to test files 75 | 76 | Some data connectors ingest data into more than one table and you might not have enabled all. In this case you must comment out the specific line in the test. 77 | 78 | Here is an example of a modified Azure AD test file. 79 | 80 | ```powershell 81 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 82 | Describe "Azure Active Directory should be connected" -Tag "AAD" { 83 | It " should have current data ()" -ForEach @( 84 | @{ Name = "SigninLogs" ; MaxAge = "1d" } 85 | @{ Name = "AuditLogs" ; MaxAge = "1d" } 86 | # @{ Name = "AADNonInteractiveUserSignInLogs" ; MaxAge = "1d" } 87 | # @{ Name = "AADServicePrincipalSignInLogs" ; MaxAge = "1d" } 88 | # @{ Name = "AADManagedIdentitySignInLogs" ; MaxAge = "1d" } 89 | # @{ Name = "AADProvisioningLogs" ; MaxAge = "1d" } 90 | # @{ Name = "ADFSSignInLogs" ; MaxAge = "1d" } 91 | # @{ Name = "AADUserRiskEvents" ; MaxAge = "30d" } 92 | # @{ Name = "AADRiskyUsers" ; MaxAge = "30d" } 93 | # @{ Name = "NetworkAccessTraffic" ; MaxAge = "1d" } 94 | # @{ Name = "AADRiskyServicePrincipals" ; MaxAge = "30d" } 95 | # @{ Name = "AADServicePrincipalRiskEvents" ; MaxAge = "30d" } 96 | ) { 97 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "$name | where TimeGenerated > ago($MaxAge) | summarize max(TimeGenerated)" | Select-Object -First 1 98 | $FirstRowReturned | Should -Not -BeNullOrEmpty 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | In this environment only `SigninLogs` and `AuditLogs` logs are forwarded to Microsoft Sentinel, all other tables are excluded from the test. 105 | 106 | Another example would be if you don't use Microsoft Defender for Identity and don't want to use ActiveDirectory as a source for UEBA. Just remove `"ActiveDirectory"` from the test file. 107 | 108 | ```powershell 109 | It "EntityAnalytics source <_> is enabled" -ForEach "AzureActiveDirectory" -Tag "EntityAnalytics" { 110 | $SentinelSettings | Where-Object { $_.name -eq "EntityAnalytics" } | Select-Object -ExpandProperty properties | Select-Object -ExpandProperty entityProviders | Should -Contain $_ 111 | } 112 | ``` 113 | 114 | ### Run Pester 115 | 116 | There are two example files provided to run Pester. `RunTest.ps1` and `RunTest-CICD.ps1`. 117 | 118 | Those Pester configurations are used to specify the tags that should be used and you can also modify other settings of the Pester test run. For more information consult the official [Pester documentation](https://pester.dev/docs/commands/New-PesterConfiguration). 119 | 120 | ```powershell 121 | Install-Module Az.Accounts -Force 122 | 123 | Connect-AzAccount -DeviceCode 124 | Set-AzContext -SubscriptionId $subscriptionId 125 | 126 | $configRunContainer = New-PesterContainer -Path "*.Tests.ps1" -Data @{ 127 | # Define your environment variables here 128 | workspaceName = "SentinelWorkspaceName" 129 | resourceGroup = "resourceGroup" 130 | subscriptionId = "SubscriptionId" 131 | } 132 | 133 | $config = New-PesterConfiguration -Hashtable @{ 134 | Filter = @{ 135 | # Use the filter configuration to only specify the tests 136 | # This way you can easily remove e.g. specific dataconnectors from the test without mofiying the test itself 137 | # You will always have to modify the tests.ps1 file if you would like to remove specific tables it change the target configuration 138 | Tag = "Configuration", "AnalyticsRules", "Watchlists", "AAD", "AADIPC", "AzureActivity", "DfC", "O365" 139 | } 140 | TestResult = @{ Enabled = $true } 141 | Run = @{ 142 | Exit = $true 143 | Container = $configRunContainer 144 | } 145 | Output = @{ Verbosity = 'Detailed' } 146 | } 147 | Invoke-Pester -Configuration $config 148 | ``` 149 | 150 | ## Currently available tests 151 | 152 | | Test | Regular | CI/CD* | 153 | |---------------------------------------------------------------------------------|--------------------|-------------------------------| 154 | | Analytics rules should not be in state "AUTO DISABLED" | :white_check_mark: | :white_check_mark: | 155 | | Analytics rule <_> is present | :white_check_mark: | :white_check_mark: | 156 | | Analytics rule name is set to | :x: | :white_check_mark: | 157 | | Analytics rule <_> is enabled | :white_check_mark: | :white_check_mark: | 158 | | Automation rule is present | :x: | :white_check_mark: | 159 | | Automation rule order is set to | :x: | :white_check_mark: | 160 | | Automation rule is | :x: | :white_check_mark: | 161 | | UEBA Source <_> is enabled | :white_check_mark: | :negative_squared_cross_mark: | 162 | | EntityAnalytics source <_> is enabled | :white_check_mark: | :negative_squared_cross_mark: | 163 | | Anomalies is enabled | :white_check_mark: | :negative_squared_cross_mark: | 164 | | Microsoft data access is enabled (EyesOn) | :white_check_mark: | :negative_squared_cross_mark: | 165 | | Diagnostic settings are send to the same Log Analytics workspace | :white_check_mark: | :negative_squared_cross_mark: | 166 | | All diagnostic settings are enabled | :white_check_mark: | :negative_squared_cross_mark: | 167 | | SentinelHealth should have current data (1d) | :white_check_mark: | :negative_squared_cross_mark: | 168 | | Workspace should be located in West Europe | :white_check_mark: | :negative_squared_cross_mark: | 169 | | Workspace retention is set to 90 days | :white_check_mark: | :negative_squared_cross_mark: | 170 | | Workspace capping should be disabled | :white_check_mark: | :negative_squared_cross_mark: | 171 | | Workspace access control mode should be "Use resource or workspace permissions" | :white_check_mark: | :negative_squared_cross_mark: | 172 | | Workspace sku should be "PerGB2018" | :white_check_mark: | :negative_squared_cross_mark: | 173 | | Workspace should not have a capacity reservation | :white_check_mark: | :negative_squared_cross_mark: | 174 | | Workspace should not purge data immediately | :white_check_mark: | :negative_squared_cross_mark: | 175 | | Workspace should have a cannot-delete lock | :white_check_mark: | :negative_squared_cross_mark: | 176 | 177 | * If a specific CI/CD tests would not make sense, then it's marked as :negative_squared_cross_mark: 178 | 179 | For data connectors the tests are not listed here but the basic test " should have current data (1d)" checks that there is at least one datapoint ingested within the last 24 hours. 180 | 181 | For tables with more than one datasource the test is named " () should have current data (1d)". 182 | 183 | For data connectors with more than one table, the tables are defined as a hashtable as part of the tests `ForEach`. 184 | 185 | ```powershell 186 | -ForEach @( 187 | @{ Name = "SigninLogs" ; MaxAge = "1d" } 188 | @{ Name = "AuditLogs" ; MaxAge = "1d" } 189 | } 190 | ``` 191 | 192 | The timeframe can be modified to your needs within the test file. 193 | -------------------------------------------------------------------------------- /RunTest-CICD.ps1: -------------------------------------------------------------------------------- 1 | #region Install required modules 2 | if ( -not ( Get-Module -ListAvailable Az.Accounts ) ) { 3 | Install-Module Az.Accounts -Force 4 | } 5 | #endregion 6 | 7 | #region Switch to correct subscription 8 | Set-AzContext -SubscriptionId $Env:subscriptionId | Out-Null 9 | #endregion 10 | 11 | <# 12 | When connecting a GitHub or Azure DevOps repository to Microsoft Sentinel, the artifacts are organized in folders named 13 | \AnalyticsRule 14 | \AutomationRule 15 | \HuntingQuery 16 | \Parser 17 | \Playbook 18 | \Workbook 19 | 20 | Use, where available, the tests with the tag suffix "-CICD" instead of the normal tests. 21 | Those tests will automatically parse the ARM template files in your configuration root and Pester uses this information 22 | to dynamically create the tests. This way you can easily add new configuration without having to modify the tests. 23 | #> 24 | 25 | $configRunContainer = @( 26 | # Add the CI/CD configuration path as parameter to all CI/CD tests 27 | New-PesterContainer -Path "*.Tests.ps1" -Data @{ 28 | workspaceName = $Env:workspaceName 29 | resourceGroup = $Env:resourceGroupName 30 | subscriptionId = $Env:subscriptionId 31 | # This is the path to the root of your Sentinel configuration files 32 | CICDPathRoot = $PWD 33 | } 34 | ) 35 | 36 | $config = New-PesterConfiguration -Hashtable @{ 37 | Filter = @{ 38 | # Use the filter configuration to only specify the tests 39 | # This way you can easily remove e.g. specific dataconnectors from the test without mofiying the test itself 40 | # You will always have to modify the tests.ps1 file if you would like to remove specific tables it change the target configuration 41 | Tag = "AutomationRules-CICD", "AnalyticsRules-CICD", "Configuration" 42 | } 43 | TestResult = @{ Enabled = $true } 44 | Run = @{ 45 | Exit = $false 46 | Container = $configRunContainer 47 | } 48 | Output = @{ Verbosity = 'Detailed' } 49 | } 50 | Invoke-Pester -Configuration $config 51 | -------------------------------------------------------------------------------- /RunTest.ps1: -------------------------------------------------------------------------------- 1 | #region Install required modules 2 | if ( -not ( Get-Module -ListAvailable Az.Accounts ) ) { 3 | Install-Module Az.Accounts -Force 4 | } 5 | #endregion 6 | 7 | $workspaceName = "SentinelWorkspaceName" 8 | $resourceGroup = "resourceGroup" 9 | $subscriptionId = "SubscriptionId" 10 | 11 | $configRunContainer = New-PesterContainer -Path "*.Tests.ps1" -Data @{ 12 | # Define your environment variables here 13 | workspaceName = $workspaceName 14 | resourceGroup = $resourceGroup 15 | subscriptionId = $subscriptionId 16 | } 17 | 18 | Connect-AzAccount -DeviceCode 19 | Set-AzContext -SubscriptionId $subscriptionId | Out-Null 20 | 21 | $config = New-PesterConfiguration -Hashtable @{ 22 | Filter = @{ 23 | # Use the filter configuration to only specify the tests 24 | # This way you can easily remove e.g. specific dataconnectors from the test without mofiying the test itself 25 | # You will always have to modify the tests.ps1 file if you would like to remove specific tables it change the target configuration 26 | Tag = "Configuration", "AnalyticsRules", "Watchlists", "AAD", "AADIPC", "AzureActivity", "DfC", "O365" 27 | } 28 | TestResult = @{ Enabled = $true } 29 | Run = @{ 30 | Exit = $true 31 | Container = $configRunContainer 32 | } 33 | Output = @{ Verbosity = 'Detailed' } 34 | } 35 | Invoke-Pester -Configuration $config 36 | -------------------------------------------------------------------------------- /images/SentinelPesterFramework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-bader/SentinelPesterFramework/395aa13b0927ca2325f7e0dbc9b48c3ee05b0607/images/SentinelPesterFramework.png -------------------------------------------------------------------------------- /src/WorkspaceHelper.ps1: -------------------------------------------------------------------------------- 1 | function Get-WorkspaceQueryUri { 2 | param ( 3 | [Parameter(Mandatory = $true)] 4 | [string]$subscriptionId, 5 | 6 | [Parameter(Mandatory = $true)] 7 | [string]$resourceGroup, 8 | 9 | [Parameter(Mandatory = $true)] 10 | [string]$workspaceName 11 | ) 12 | # Query the workspace to get the workspaceId 13 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}?api-version=2021-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 14 | $WorkspaceProperties = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty properties 15 | $WorkspaceQueryUri = "https://api.loganalytics.io/v1/workspaces/{0}/query" -f $WorkspaceProperties.customerId 16 | return $WorkspaceQueryUri 17 | } 18 | 19 | function Invoke-WorkspaceQuery { 20 | param ( 21 | # KQL query to run 22 | [Parameter(Mandatory = $true)] 23 | [string]$Query, 24 | 25 | # Workspace Uri 26 | [Parameter(Mandatory = $true)] 27 | $WorkspaceQueryUri 28 | ) 29 | $Result = Invoke-AzRestMethod -Method POST -Uri $WorkspaceQueryUri -Payload ( @{ "query" = $Query } | ConvertTo-Json ) 30 | ($Result.Content | ConvertFrom-Json).tables.rows 31 | } -------------------------------------------------------------------------------- /tests/AnalyticsRules/AnalyticsRules.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeDiscovery { 16 | # Define the Analytics rule ids that should be present and enabled 17 | $AnalyticsRuleIds = @( 18 | "BuiltInFusion", 19 | "b38656e6-ee25-48f6-baee-741be171b174", 20 | "0f897683-8af5-4e42-b256-98cc00c3e3c1", 21 | "7a2a0966-8f10-4498-b627-b1dfd8186c83", 22 | "8b713b44-6ee0-4c47-817a-d1b6b4213d8e", 23 | "a4fe163e-aec6-407c-8240-ef280742a5f4" 24 | ) 25 | } 26 | 27 | 28 | BeforeAll { 29 | # More information about the API can be found here: 30 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/stable/alert-rules/list?tabs=HTTP 31 | # Query Analytics rules 32 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/alertRules?api-version=2022-11-01" -f $subscriptionId, $resourceGroup, $workspaceName 33 | $CurrentItems = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 34 | } 35 | 36 | Describe "Analytics Rules" -Tag "AnalyticsRules" { 37 | 38 | It "Analytics rules should not be in state `"AUTO DISABLED`"" { 39 | # https://learn.microsoft.com/en-us/azure/sentinel/detect-threats-custom#issue-a-scheduled-rule-failed-to-execute-or-appears-with-auto-disabled-added-to-the-name 40 | $CurrentItems | Where-Object { $_.properties.displayName -match "AUTO DISABLED" } | Should -BeNullOrEmpty 41 | } 42 | 43 | It "Analytics rule <_> is present" -ForEach @( $AnalyticsRuleIds ) { 44 | $AnalyticsRuleId = $_ 45 | $AnalyticsRule = $CurrentItems | Where-Object { $_.id -match $AnalyticsRuleId } 46 | $AnalyticsRule.id | Should -Match $AnalyticsRuleId 47 | } 48 | 49 | It "Analytics rule <_> is enabled" -ForEach @( $AnalyticsRuleIds ) { 50 | $AnalyticsRuleId = $_ 51 | $AnalyticsRule = $CurrentItems | Where-Object { $_.id -match $AnalyticsRuleId } 52 | $AnalyticsRule.properties.enabled | Should -Be $true 53 | } 54 | } -------------------------------------------------------------------------------- /tests/CICD/AnalyticsRules-CICD.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeDiscovery { 16 | if ( $CICDPathRoot ) { 17 | $AnalyticsRulePath = Join-Path $CICDPathRoot "AnalyticsRule" 18 | if ( Test-Path $AnalyticsRulePath ) { 19 | #region Create a test list of Analytics Rules 20 | $ArmTemplates = Get-ChildItem $AnalyticsRulePath -Recurse -File *.json 21 | $AnalyticsRulesDefinition = New-Object -TypeName 'System.Collections.ArrayList' 22 | 23 | foreach ($ArmTemplate in $ArmTemplates) { 24 | $ArmTemplateJSON = Get-Content $ArmTemplate.FullName | ConvertFrom-Json 25 | if ($ArmTemplateJSON.resources.id -match "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") { 26 | $Id = $Matches[0] 27 | $TempHashTable = @{ 28 | "id" = $Id 29 | "name" = $ArmTemplateJSON.resources.properties.displayName 30 | "query" = $ArmTemplateJSON.resources.properties.query 31 | } 32 | $AnalyticsRulesDefinition.Add($TempHashTable) | Out-Null 33 | } else { 34 | Write-Warning "Could not read Analytics Rules id from file $($ArmTemplate.FullName). Skipping test for this artifact." 35 | } 36 | } 37 | #endregion 38 | } 39 | } 40 | } 41 | 42 | BeforeAll { 43 | # More information about the API can be found here: 44 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/stable/alert-rules/list?tabs=HTTP 45 | # Query Analytics rules 46 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/alertRules?api-version=2022-11-01" -f $subscriptionId, $resourceGroup, $workspaceName 47 | $CurrentItems = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 48 | } 49 | 50 | Describe "Analytics Rules" -Tag "AnalyticsRules-CICD" { 51 | 52 | Context "Analytics rule `"`" ()" -ForEach $AnalyticsRulesDefinition { 53 | 54 | It "Analytics rule is present" { 55 | $Item = $CurrentItems | Where-Object { $_.id -match $id } 56 | $Item.id | Should -Match $id 57 | } 58 | 59 | It "Analytics rule name is set to " { 60 | $Item = $CurrentItems | Where-Object { $_.id -match $id } 61 | $Item.properties.displayName | Should -Be $name 62 | } 63 | 64 | It "Analytics rule should not be in state `"AUTO DISABLED`"" { 65 | # https://learn.microsoft.com/en-us/azure/sentinel/detect-threats-custom#issue-a-scheduled-rule-failed-to-execute-or-appears-with-auto-disabled-added-to-the-name 66 | $Item = $CurrentItems | Where-Object { $_.id -match $id } 67 | $Item.properties.displayName | Should -Not -Match "AUTO DISABLED" 68 | } 69 | 70 | It "Analytics rule is enabled" { 71 | $Item = $CurrentItems | Where-Object { $_.id -match $id } 72 | $Item.properties.enabled | Should -Be $true 73 | } 74 | 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /tests/CICD/AutomationRules-CICD.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string] 13 | $CICDPathRoot 14 | ) 15 | 16 | BeforeDiscovery { 17 | if ($CICDPathRoot) { 18 | $AutomationRulesPath = Join-Path $CICDPathRoot "AutomationRule" 19 | if ( $AutomationRulesPath ) { 20 | #region Create a test list of Automation Rules 21 | $ArmTemplates = Get-ChildItem $AutomationRulesPath -Recurse -File *.json 22 | $AutomationRulesDefinition = New-Object -TypeName 'System.Collections.ArrayList' 23 | 24 | foreach ($ArmTemplate in $ArmTemplates) { 25 | $ArmTemplateJSON = Get-Content $ArmTemplate.FullName | ConvertFrom-Json 26 | $TempHashTable = @{ 27 | "id" = $ArmTemplateJSON.resources.name 28 | "order" = $ArmTemplateJSON.resources.properties.order 29 | "enabled" = $ArmTemplateJSON.resources.properties.triggeringLogic.isEnabled 30 | } 31 | $AutomationRulesDefinition.Add($TempHashTable) | Out-Null 32 | } 33 | #endregion 34 | } 35 | } 36 | } 37 | 38 | BeforeAll { 39 | # More information about the API can be found here: 40 | # 41 | # Query current settings 42 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/automationRules?api-version=2022-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 43 | $CurrentItems = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 44 | } 45 | 46 | Describe "Automation Rules" -Tag "AutomationRules-CICD" { 47 | 48 | It "Automation rule is present" -ForEach $AutomationRulesDefinition { 49 | $Item = $CurrentItems | Where-Object { $_.name -match $id } 50 | $Item.name | Should -Match $id 51 | } 52 | 53 | It "Automation rule order is set to " -ForEach $AutomationRulesDefinition { 54 | $Item = $CurrentItems | Where-Object { $_.name -match $id } 55 | $Item.properties.order | Should -Be $order 56 | } 57 | 58 | It "Automation rule is " -ForEach $AutomationRulesDefinition { 59 | $Item = $CurrentItems | Where-Object { $_.name -match $id } 60 | $Item.properties.triggeringLogic.isEnabled | Should -Be $enabled 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/CICD/Watchlists-CICD.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | 16 | BeforeDiscovery { 17 | if ( $CICDPathRoot ) { 18 | $AnalyticsRulePath = Join-Path $CICDPathRoot "AnalyticsRules" 19 | if ( Test-Path $AnalyticsRulePath ) { 20 | #region Get all needed watchlist names 21 | $UsedWatchlistsObject = Select-String "_GetWatchlist\('.*?'\)" -Path "$AnalyticsRulePath/*" 22 | $UsedWatchlists = @( $UsedWatchlistsObject.Matches.Value -replace "_GetWatchlist\('(.*?)'\)", '$1' | Select-Object -Unique ) 23 | #endregion 24 | } 25 | } 26 | } 27 | 28 | BeforeAll { 29 | # More information about the API can be found here: 30 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/preview/watchlist-items/list 31 | # Query deployed watchlists 32 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/watchlists?api-version=2022-01-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 33 | $CurrentItems = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 34 | } 35 | 36 | Describe "Watchlists" -Tag "Watchlists-CICD" -ForEach $UsedWatchlists { 37 | 38 | BeforeAll { 39 | $CurrentWatchlist = $_ 40 | Write-Host $CurrentWatchlist 41 | } 42 | 43 | It "Watchlist <_> used by Analytics Rules is deployed" { 44 | $Item = $CurrentItems | Where-Object { $_.name -match $CurrentWatchlist } 45 | $Item.name | Should -Be $CurrentWatchlist 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/Configuration/OfficeConsents.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # More information about the setting can be found here: 17 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/preview/security-ml-analytics-settings/list?tabs=HTTP 18 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/officeConsents?api-version=2022-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 19 | $OfficeConsents = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 20 | } 21 | 22 | Describe "Office Consents" -Tag "OfficeConsents" { 23 | # To be defined 24 | } -------------------------------------------------------------------------------- /tests/Configuration/SecurityMLAnalytics.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeDiscovery { 16 | 17 | } 18 | 19 | BeforeAll { 20 | # More information about the setting can be found here: 21 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/preview/security-ml-analytics-settings/list?tabs=HTTP 22 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/securityMLAnalyticsSettings?api-version=2022-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 23 | $SecurityMLAnalyticsSettings = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 24 | } 25 | 26 | Describe "Security ML Analytics Settings" -Tag "SecurityMLAnalytics" { 27 | # To be defined 28 | } -------------------------------------------------------------------------------- /tests/Configuration/SentinelConfiguration.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | 25 | # More information about the setting can be found here: 26 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/preview/product-settings/update?tabs=HTTP 27 | # https://learn.microsoft.com/en-us/azure/templates/microsoft.securityinsights/settings?pivots=deployment-language-arm-template 28 | 29 | # Query UEBA settings 30 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/settings?api-version=2022-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 31 | $SentinelSettings = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 32 | 33 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourcegroups/{1}/providers/microsoft.operationalinsights/workspaces/{2}/providers/Microsoft.SecurityInsights/settings/SentinelHealth/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 34 | $DiagnosticSettings = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 35 | } 36 | 37 | Describe "Sentinel Configuration" -Tag "Configuration", "Sentinel" { 38 | 39 | It "UEBA Source <_> is enabled" -ForEach "AuditLogs", "SecurityEvent", "SigninLogs", "AzureActivity" -Tag "UEBA" { 40 | $SentinelSettings | Where-Object { $_.name -eq "Ueba" } | Select-Object -ExpandProperty properties | Select-Object -ExpandProperty dataSources | Should -Contain $_ 41 | } 42 | 43 | It "EntityAnalytics source <_> is enabled" -ForEach "ActiveDirectory", "AzureActiveDirectory" -Tag "EntityAnalytics" { 44 | $SentinelSettings | Where-Object { $_.name -eq "EntityAnalytics" } | Select-Object -ExpandProperty properties | Select-Object -ExpandProperty entityProviders | Should -Contain $_ 45 | } 46 | 47 | It "Anomalies is enabled" -Tag "Anomalies" { 48 | $SentinelSettings | Where-Object { $_.name -eq "Anomalies" } | Select-Object -ExpandProperty properties | Select-Object -ExpandProperty isEnabled | Should -Be $true 49 | } 50 | 51 | It "Microsoft data access is enabled (EyesOn)" -Tag "EyesOn" { 52 | $SentinelSettings | Where-Object { $_.name -eq "EyesOn" } | Select-Object -ExpandProperty properties | Select-Object -ExpandProperty isEnabled | Should -Be $true 53 | } 54 | 55 | It "Diagnostic settings are send to the same Log Analytics workspace" -Tag "DiagnosticSettings" { 56 | $DiagnosticSettings.id -like "$($DiagnosticSettings.properties.workspaceId)*" | Should -Be $true 57 | } 58 | 59 | It "All diagnostic settings are enabled" -Tag "DiagnosticSettings" { 60 | $DiagnosticSettings.properties.logs | Where-Object { $_.enabled -eq $false } | Should -BeNullOrEmpty 61 | } 62 | 63 | It "SentinelHealth should have current data (1d)" { 64 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "SentinelHealth | where TimeGenerated > ago(1d) | summarize max(TimeGenerated)" | Select-Object -First 1 65 | $FirstRowReturned | Should -Not -BeNullOrEmpty 66 | } 67 | } -------------------------------------------------------------------------------- /tests/Configuration/WorkspaceConfiguration.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # More information about the setting can be found here: 17 | # https://learn.microsoft.com/en-us/rest/api/loganalytics/workspaces/update?tabs=HTTP 18 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}?api-version=2021-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 19 | $WorkspaceProperties = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json 20 | } 21 | 22 | 23 | Describe "Workspace Configuration" -Tag "Configuration", "Workspace" { 24 | 25 | It "Workspace should be located in West Europe" { 26 | $WorkspaceProperties.location | Should -Be "westeurope" 27 | } 28 | 29 | It "Workspace retention is set to 90 days" { 30 | $WorkspaceProperties.properties.retentionInDays | Should -Be 90 31 | } 32 | 33 | It "Workspace capping should be disabled" { 34 | $WorkspaceProperties.properties.workspaceCapping.dailyQuotaGb | Should -Be -1 35 | } 36 | 37 | It "Workspace access control mode should be `"Use resource or workspace permissions`"" { 38 | $WorkspaceProperties.properties.features.enableLogAccessUsingOnlyResourcePermissions | Should -Be $true 39 | } 40 | 41 | It "Workspace sku should be `"PerGB2018`"" { 42 | $WorkspaceProperties.properties.sku.name | Should -Be "PerGB2018" 43 | } 44 | 45 | It "Workspace should not have a capacity reservation" { 46 | $WorkspaceProperties.properties.sku.capacityReservationLevel | Should -BeNullOrEmpty 47 | } 48 | 49 | It "Workspace should not purge data immediately" { 50 | $WorkspaceProperties.properties.purgeDataImmediately | Should -BeNullOrEmpty 51 | } 52 | 53 | It "Workspace should have a cannot-delete lock" { 54 | # A read-only lock on a Log Analytics workspace prevents User and Entity Behavior Analytics (UEBA) from being enabled. 55 | # A cannot-delete lock on a Log Analytics workspace doesn't prevent data purge operations, remove the data purge role from the user instead. 56 | # https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources?tabs=json#:~:text=A%20read%2Donly%20lock%20on%20a%20Log,data%20purge%20role%20from%20the%20user%20instead. 57 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.Authorization/locks?api-version=2016-09-01" -f $subscriptionId, $resourceGroup, $workspaceName 58 | $ResourceLock = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 59 | $ResourceLock.properties.level | Should -Be "CanNotDelete" 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /tests/DataConnectors/AzureADIdentityProtection.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | 17 | # Import the helper functions 18 | . ./src/WorkspaceHelper.ps1 19 | $params = @{ 20 | "subscriptionId" = $subscriptionId 21 | "resourceGroup" = $resourceGroup 22 | "workspaceName" = $workspaceName 23 | } 24 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 25 | } 26 | 27 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 28 | 29 | Describe "Azure AD Identity Protection should be connected" -Tag "AADIPC" { 30 | It "SecurityAlert (AADIPC) should have current data" { 31 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "SecurityAlert | where TimeGenerated > ago(90d) | where ProductName == 'Azure Active Directory Identity Protection' | summarize max(TimeGenerated)" | Select-Object -First 1 32 | $FirstRowReturned | Should -Not -BeNullOrEmpty 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /tests/DataConnectors/AzureActiveDirectory.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | Describe "Azure Active Directory should be connected" -Tag "AAD" { 28 | It " should have current data ()" -ForEach @( 29 | @{ Name = "SigninLogs" ; MaxAge = "1d" } 30 | @{ Name = "AuditLogs" ; MaxAge = "1d" } 31 | @{ Name = "AADNonInteractiveUserSignInLogs" ; MaxAge = "1d" } 32 | @{ Name = "AADServicePrincipalSignInLogs" ; MaxAge = "1d" } 33 | @{ Name = "AADManagedIdentitySignInLogs" ; MaxAge = "1d" } 34 | @{ Name = "AADProvisioningLogs" ; MaxAge = "1d" } 35 | @{ Name = "ADFSSignInLogs" ; MaxAge = "1d" } 36 | @{ Name = "AADUserRiskEvents" ; MaxAge = "30d" } 37 | @{ Name = "AADRiskyUsers" ; MaxAge = "30d" } 38 | @{ Name = "NetworkAccessTraffic" ; MaxAge = "1d" } 39 | @{ Name = "AADRiskyServicePrincipals" ; MaxAge = "30d" } 40 | @{ Name = "AADServicePrincipalRiskEvents" ; MaxAge = "30d" } 41 | ) { 42 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "$name | where TimeGenerated > ago($MaxAge) | summarize max(TimeGenerated)" | Select-Object -First 1 43 | $FirstRowReturned | Should -Not -BeNullOrEmpty 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/DataConnectors/AzureAudit.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | 17 | # Import the helper functions 18 | . ./src/WorkspaceHelper.ps1 19 | $params = @{ 20 | "subscriptionId" = $subscriptionId 21 | "resourceGroup" = $resourceGroup 22 | "workspaceName" = $workspaceName 23 | } 24 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 25 | } 26 | 27 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 28 | 29 | Describe "Azure Audit should be connected" -Tag "AzureActivity" { 30 | It "AzureActivity should have current data (1d)" { 31 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "AzureActivity | where TimeGenerated > ago(1d) | summarize max(TimeGenerated)" | Select-Object -First 1 32 | $FirstRowReturned | Should -Not -BeNullOrEmpty 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /tests/DataConnectors/DNS.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "DNS should be connected" -Tag "DNS" -ForEach @( 29 | @{ Name = "DnsEvents" ; MaxAge = "1d" } 30 | @{ Name = "DnsInventory" ; MaxAge = "1d" } 31 | ) { 32 | It " should have current data ()" { 33 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "$name | where TimeGenerated > ago($MaxAge) | summarize max(TimeGenerated)" | Select-Object -First 1 34 | $FirstRowReturned | Should -Not -BeNullOrEmpty 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /tests/DataConnectors/DataConnectorsCheckRequirements.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeDiscovery { 16 | $DataConnectorsToCheck = @( 17 | #"APIPolling", 18 | "AmazonWebServicesCloudTrail", 19 | "AmazonWebServicesS3", 20 | "AzureActiveDirectory", 21 | "AzureAdvancedThreatProtection", 22 | #"AzureSecurityCenter", 23 | "Dynamics365", 24 | #"GenericUI", 25 | #"IOT", 26 | "MicrosoftCloudAppSecurity", 27 | "MicrosoftDefenderAdvancedThreatProtection", 28 | "MicrosoftThreatIntelligence", 29 | "MicrosoftThreatProtection", 30 | "Office365", 31 | "Office365Project", 32 | "OfficeATP", 33 | "OfficeIRM", 34 | "OfficePowerBI", 35 | "ThreatIntelligence", 36 | "ThreatIntelligenceTaxii" 37 | ) 38 | } 39 | 40 | BeforeAll { 41 | # More information about the setting can be found here: 42 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/preview/data-connectors-check-requirements/post?tabs=HTTP#dataconnectorkind 43 | $TenantId = Get-AzContext | Select-Object -ExpandProperty Tenant | Select-Object -ExpandProperty Id 44 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/dataConnectorsCheckRequirements?api-version=2022-12-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 45 | } 46 | 47 | Describe "Data Connectors Check Requirements" -Tag "DataConnectorsReqs" { 48 | Context "Data Connector <_>" -ForEach @( $DataConnectorsToCheck ) { 49 | 50 | BeforeAll { 51 | $Payload = @{ 52 | "kind" = $_ 53 | "properties" = @{ 54 | "tenantId" = $TenantId 55 | } 56 | } 57 | $dataConnectorsCheckRequirements = Invoke-AzRestMethod -Method POST -Uri $RestUri -Payload ( $Payload | ConvertTo-Json -Depth 3 ) | Select-Object -ExpandProperty Content | ConvertFrom-Json 58 | } 59 | 60 | It "Data Connector $_ should be authorized" { 61 | 62 | $dataConnectorsCheckRequirements.authorizationState | Should -Be "Valid" 63 | } 64 | 65 | It "Data Connector $_ should be licensed" { 66 | $dataConnectorsCheckRequirements.licenseState | Should -Be "Valid" 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /tests/DataConnectors/DefenderForCloud.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "Microsoft Defender for Cloud should be connected" -Tag "DfC" { 29 | It "SecurityAlert (DfC) should have current data" { 30 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "SecurityAlert | where TimeGenerated > ago(90d) | where ProductName == 'Azure Security Center' | summarize max(TimeGenerated)" | Select-Object -First 1 31 | $FirstRowReturned | Should -Not -BeNullOrEmpty 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/DataConnectors/Microsoft365Defender.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "Microsoft 365 Defender should be connected" -Tag "M365D" { 29 | 30 | It "SecurityIncident (M365D) should have current data (14d)" { 31 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "SecurityIncident | where TimeGenerated > ago(14d) | where ProviderName == 'Microsoft 365 Defender' | summarize max(TimeGenerated)" | Select-Object -First 1 32 | $FirstRowReturned | Should -Not -BeNullOrEmpty 33 | } 34 | 35 | It "SecurityAlert (M365D) should have current data (14d)" { 36 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query 'SecurityAlert |where TimeGenerated > ago(14d) | where ProductName in ("Microsoft Defender Advanced Threat Protection","Office 365 Advanced Threat Protection","Azure Advanced Threat Protection","Microsoft Cloud App Security","Microsoft 365 Defender") | summarize max(TimeGenerated)' | Select-Object -First 1 37 | $FirstRowReturned | Should -Not -BeNullOrEmpty 38 | } 39 | 40 | It " should have current data ()" -ForEach @( 41 | @{ Name = "DeviceEvents" ; MaxAge = "1d" } 42 | @{ Name = "DeviceFileEvents" ; MaxAge = "1d" } 43 | @{ Name = "DeviceImageLoadEvents" ; MaxAge = "1d" } 44 | @{ Name = "DeviceInfo" ; MaxAge = "1d" } 45 | @{ Name = "DeviceLogonEvents" ; MaxAge = "1d" } 46 | @{ Name = "DeviceNetworkEvents" ; MaxAge = "1d" } 47 | @{ Name = "DeviceNetworkInfo" ; MaxAge = "1d" } 48 | @{ Name = "DeviceProcessEvents" ; MaxAge = "1d" } 49 | @{ Name = "DeviceRegistryEvents" ; MaxAge = "1d" } 50 | @{ Name = "DeviceFileCertificateInfo" ; MaxAge = "1d" } 51 | @{ Name = "EmailEvents" ; MaxAge = "1d" } 52 | @{ Name = "EmailUrlInfo" ; MaxAge = "1d" } 53 | @{ Name = "EmailAttachmentInfo" ; MaxAge = "1d" } 54 | @{ Name = "EmailPostDeliveryEvents" ; MaxAge = "1d" } 55 | @{ Name = "UrlClickEvents" ; MaxAge = "1d" } 56 | @{ Name = "IdentityLogonEvents" ; MaxAge = "1d" } 57 | @{ Name = "IdentityQueryEvents" ; MaxAge = "1d" } 58 | @{ Name = "IdentityDirectoryEvents" ; MaxAge = "1d" } 59 | @{ Name = "CloudAppEvents" ; MaxAge = "1d" } 60 | @{ Name = "AlertInfo" ; MaxAge = "7d" } 61 | @{ Name = "AlertEvidence" ; MaxAge = "90d" } 62 | ) { 63 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "$name | where TimeGenerated > ago($MaxAge) | summarize max(TimeGenerated)" | Select-Object -First 1 64 | $FirstRowReturned | Should -Not -BeNullOrEmpty 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /tests/DataConnectors/MicrosoftDefenderForCloudApps.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | 17 | # Import the helper functions 18 | . ./src/WorkspaceHelper.ps1 19 | $params = @{ 20 | "subscriptionId" = $subscriptionId 21 | "resourceGroup" = $resourceGroup 22 | "workspaceName" = $workspaceName 23 | } 24 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 25 | } 26 | 27 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 28 | 29 | Describe "Microsoft Defender for Cloud Apps should be connected" -Tag "MDA" { 30 | It "SecurityAlert (MDA) should have current data" { 31 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "SecurityAlert | where TimeGenerated > ago(90d) | where ProductName == 'Microsoft Cloud App Security' | summarize max(TimeGenerated)" | Select-Object -First 1 32 | $FirstRowReturned | Should -Not -BeNullOrEmpty 33 | } 34 | 35 | It "McasShadowItReporting should have current data" -Tag "MDA-ShadowIT" { 36 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "McasShadowItReporting | where TimeGenerated > ago(7d) | summarize max(TimeGenerated)" | Select-Object -First 1 37 | $FirstRowReturned | Should -Not -BeNullOrEmpty 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /tests/DataConnectors/Office365.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | 17 | # Import the helper functions 18 | . ./src/WorkspaceHelper.ps1 19 | $params = @{ 20 | "subscriptionId" = $subscriptionId 21 | "resourceGroup" = $resourceGroup 22 | "workspaceName" = $workspaceName 23 | } 24 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 25 | } 26 | 27 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 28 | 29 | Describe "Office 365 should be connected" -Tag "O365" { 30 | 31 | It "Office 365 OfficeActivity (SharePoint) should have current data (1d)" -Tag "O365-SharePoint" { 32 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query 'OfficeActivity | where TimeGenerated > ago(1d) | where OfficeWorkload == "SharePoint" or OfficeWorkload == "OneDrive" | summarize max(TimeGenerated)' | Select-Object -First 1 33 | $FirstRowReturned | Should -Not -BeNullOrEmpty 34 | } 35 | 36 | It "Office 365 OfficeActivity (Exchange) should have current data (1d)" -Tag "O365-Exchange" { 37 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query 'OfficeActivity | where TimeGenerated > ago(1d) | where OfficeWorkload == "Exchange" | summarize max(TimeGenerated)' | Select-Object -First 1 38 | $FirstRowReturned | Should -Not -BeNullOrEmpty 39 | } 40 | 41 | It "Office 365 OfficeActivity (Teams) should have current data (1d)" -Tag "O365-Teams" { 42 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query 'OfficeActivity | where TimeGenerated > ago(1d) | where OfficeWorkload == "MicrosoftTeams" | summarize max(TimeGenerated)' | Select-Object -First 1 43 | $FirstRowReturned | Should -Not -BeNullOrEmpty 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/DataConnectors/SecurityEvents.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "SecurityEvents should be connected" -Tag "SecurityEvents" { 29 | It "SecurityEvents should have current data" { 30 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "SecurityEvents | where TimeGenerated > ago(1d) | summarize max(TimeGenerated)" | Select-Object -First 1 31 | $FirstRowReturned | Should -Not -BeNullOrEmpty 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/DataConnectors/ThreatIntelligenceIndicator.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "Threat Intelligence Platforms should be connected" -Tag "TI" { 29 | It "ThreatIntelligenceIndicator should have current data (1d)" { 30 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "ThreatIntelligenceIndicator | where TimeGenerated > ago(1d) | where SourceSystem == 'SecurityGraph' | summarize max(TimeGenerated)" | Select-Object -First 1 31 | $FirstRowReturned | Should -Not -BeNullOrEmpty 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/DataConnectors/WindowsDNSEventsViaAMA.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "Windows DNS Events via AMA should be connected" -Tag "AMADNS" { 29 | It "ASimDnsActivityLogs (Microsoft DNS Server) should have current data (1d)" { 30 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query 'ASimDnsActivityLogs | where TimeGenerated > ago(1d) | where EventProduct == "DNS Server" | where EventVendor == "Microsoft" | summarize max(TimeGenerated)' | Select-Object -First 1 31 | $FirstRowReturned | Should -Not -BeNullOrEmpty 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/DataConnectors/WindowsEvents.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "Windows Forwarded Events should be connected" -Tag "WinEvents" { 29 | It "WindowsEvents should have current data (1d)" { 30 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "WindowsEvents | where TimeGenerated > ago(1d) | summarize max(TimeGenerated)" | Select-Object -First 1 31 | $FirstRowReturned | Should -Not -BeNullOrEmpty 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/DataConnectors/WindowsFirewall.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeAll { 16 | # Import the helper functions 17 | . ./src/WorkspaceHelper.ps1 18 | $params = @{ 19 | "subscriptionId" = $subscriptionId 20 | "resourceGroup" = $resourceGroup 21 | "workspaceName" = $workspaceName 22 | } 23 | $WorkspaceQueryUri = Get-WorkspaceQueryUri @params 24 | } 25 | 26 | Describe "Sentinel Dataconnectors" -Tag "DataConnector" { 27 | 28 | Describe "Windows Firewall should be connected" -Tag "WinFirewall" { 29 | It "WindowsFirewall should have current data (1d)" { 30 | $FirstRowReturned = Invoke-WorkspaceQuery -WorkspaceQueryUri $WorkspaceQueryUri -Query "WindowsFirewall | where TimeGenerated > ago(1d) | summarize max(TimeGenerated)" | Select-Object -First 1 31 | $FirstRowReturned | Should -Not -BeNullOrEmpty 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/Watchlists/Watchlists.Tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string]$workspaceName, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string]$resourceGroup, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string]$subscriptionId, 10 | 11 | [Parameter(Mandatory = $false)] 12 | [string]$CICDPathRoot 13 | ) 14 | 15 | BeforeDiscovery { 16 | # Define a list of Watchlists to test 17 | # Each watchlist entry must have the properties name and maxAgeInDays 18 | $WatchListConfigObjects = @( 19 | @{ 20 | "name" = "IPAddresses" 21 | "maxAgeInDays" = "14" 22 | } 23 | @{ 24 | "name" = "HighRiskApps" 25 | "maxAgeInDays" = "365" 26 | } 27 | ) 28 | } 29 | 30 | BeforeAll { 31 | # More information about the API can be found here: 32 | # https://learn.microsoft.com/en-us/rest/api/securityinsights/stable/watchlists/list?tabs=HTTP 33 | # Query Watchlists 34 | $RestUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights/workspaces/{2}/providers/Microsoft.SecurityInsights/watchlists?api-version=2022-01-01-preview" -f $subscriptionId, $resourceGroup, $workspaceName 35 | $CurrentItems = Invoke-AzRestMethod -Method GET -Uri $RestUri | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value 36 | } 37 | 38 | Describe "Watchlist" -Tag "Watchlists" { 39 | 40 | It "Watchlist is present" -ForEach $WatchListConfig { 41 | $WatchlistName = $name 42 | $Watchlist = $CurrentItems | Where-Object { $_.name -eq $WatchlistName } 43 | $Watchlist.name | Should -Match $WatchlistName 44 | } 45 | 46 | It "Watchlist was updated in the last days" -ForEach $WatchListConfig { 47 | $WatchlistName = $name 48 | $Watchlist = $CurrentItems | Where-Object { $_.name -eq $WatchlistName } 49 | $ModifiedTime = New-TimeSpan -Start $watchList.systemData.lastModifiedAt -End (Get-Date) 50 | $ModifiedTime.TotalDays | Should -BeLessOrEqual $maxAgeInDays 51 | } 52 | } --------------------------------------------------------------------------------