From ff65e92f60a8a9d0c206491e0b71a18963d9d69c Mon Sep 17 00:00:00 2001 From: Manoj K Date: Tue, 26 May 2026 12:32:01 +0530 Subject: [PATCH 1/2] Feature-61002 --- src/powershell/tests/Test-Assessment.61002.md | 13 ++ .../tests/Test-Assessment.61002.ps1 | 181 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.61002.md create mode 100644 src/powershell/tests/Test-Assessment.61002.ps1 diff --git a/src/powershell/tests/Test-Assessment.61002.md b/src/powershell/tests/Test-Assessment.61002.md new file mode 100644 index 000000000..9a6f0ccf0 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.61002.md @@ -0,0 +1,13 @@ +Microsoft Sentinel is a cloud-native SIEM that correlates security signals from across your environment into incidents your SOC can act on. AI workloads generate events across identity, cloud posture, and threat protection simultaneously — a central workspace is the only place those signals can be assembled into a coherent incident. This check verifies Sentinel is onboarded to at least one Log Analytics workspace, which every other AI threat detection control in this pillar depends on. + +When Microsoft Sentinel is not onboarded to a Log Analytics workspace, security signals from AI workloads land in isolated product portals with no central point of correlation. Threat actors who compromise an agent identity can exploit this fragmentation because each product sees only its own slice of the attack — an anomalous Entra sign-in, a bulk Graph API call, and a Defender for AI Services alert each get triaged in isolation with no shared context. Without Sentinel, the organization cannot assemble the cross-product pattern that would reveal the full attack chain and trigger an automated response. + +**Remediation action** + +- [What is Microsoft Sentinel?](https://learn.microsoft.com/azure/sentinel/overview) +- [Quickstart: Onboard Microsoft Sentinel](https://learn.microsoft.com/azure/sentinel/quickstart-onboard) +- [Design your Microsoft Sentinel workspace architecture](https://learn.microsoft.com/azure/sentinel/design-your-workspace-architecture) +- [Sentinel onboarding states — Create (REST API)](https://learn.microsoft.com/rest/api/securityinsights/sentinel-onboarding-states/create) + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.61002.ps1 b/src/powershell/tests/Test-Assessment.61002.ps1 new file mode 100644 index 000000000..56be2d1be --- /dev/null +++ b/src/powershell/tests/Test-Assessment.61002.ps1 @@ -0,0 +1,181 @@ +<# +.SYNOPSIS + Checks whether Microsoft Sentinel is onboarded on at least one Log Analytics workspace. + +.DESCRIPTION + This test enumerates all Log Analytics workspaces across in-scope Azure subscriptions and + verifies that at least one has Microsoft Sentinel onboarded. Sentinel is required as a central + SIEM before any other AI threat detection control in this pillar can correlate signals across + the environment. + + Evaluation steps: + 1. Use Azure Resource Graph to enumerate all Log Analytics workspaces (combining subscription + listing and workspace listing in one query). + 2. For each workspace, query the Sentinel onboarding state resource via the + Microsoft.SecurityInsights/onboardingStates/default ARM endpoint. + 3. Pass if at least one workspace returns HTTP 200 (Sentinel is onboarded). + 4. Fail if no workspaces exist or every workspace returns HTTP 404 (not onboarded). + +.NOTES + Test ID: 61002 + Workshop Task: AI_089 + Pillar: AI + Category: AI Threat Detection + Required permissions: + - Reader on each subscription (for Log Analytics workspace enumeration) + - Microsoft Sentinel Reader on each workspace (for onboarding state query) +#> + +function Test-Assessment-61002 { + + [ZtTest( + Category = 'AI Threat Detection', + ImplementationCost = 'Medium', + Service = ('Azure'), + MinimumLicense = ('Microsoft_Sentinel'), + Pillar = 'AI', + RiskLevel = 'High', + SfiPillar = 'Monitor and detect cyberthreats', + TenantType = ('Workforce'), + TestId = 61002, + Title = 'Microsoft Sentinel is onboarded on at least one Log Analytics workspace', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + $activity = 'Evaluating Microsoft Sentinel onboarding state across Log Analytics workspaces' + + # Verify Azure connection + Write-ZtProgress -Activity $activity -Status 'Checking Azure connection' + $azContext = Get-AzContext -ErrorAction SilentlyContinue + if (-not $azContext) { + Write-PSFMessage 'Not connected to Azure.' -Level Warning + Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure + return + } + + # Q1 + Q2: Use Azure Resource Graph to enumerate Log Analytics workspaces across all + # in-scope subscriptions. ARG respects caller RBAC and returns only accessible resources. + Write-ZtProgress -Activity $activity -Status 'Enumerating Log Analytics workspaces via Resource Graph' + + $argQuery = @" +resources +| where type =~ 'microsoft.operationalinsights/workspaces' +| join kind=leftouter ( + resourcecontainers + | where type =~ 'microsoft.resources/subscriptions' + | project subscriptionName=name, subscriptionId +) on subscriptionId +| project + workspaceName=name, + workspaceId=id, + resourceGroup, + subscriptionId, + subscriptionName +| order by subscriptionName asc, workspaceName asc +"@ + + $allWorkspaces = @() + try { + $allWorkspaces = @(Invoke-ZtAzureResourceGraphRequest -Query $argQuery) + Write-PSFMessage "ARG query returned $($allWorkspaces.Count) Log Analytics workspace(s)." -Tag Test -Level VeryVerbose + } + catch { + Write-PSFMessage "Azure Resource Graph query failed: $($_.Exception.Message)" -Tag Test -Level Warning + Add-ZtTestResultDetail -SkippedBecause NotSupported + return + } + + # Q3: For each workspace, query the Sentinel onboarding state. + # HTTP 200 = Sentinel is onboarded; HTTP 404 = not onboarded. + # -FullResponse is used so that a 404 does not throw and the status code can be inspected. + $workspaceResults = @() + foreach ($workspace in $allWorkspaces) { + Write-ZtProgress -Activity $activity -Status "Checking Sentinel onboarding on workspace '$($workspace.workspaceName)'" + + $sentinelPath = "$($workspace.workspaceId)/providers/Microsoft.SecurityInsights/onboardingStates/default?api-version=2024-03-01" + + $sentinelOnboarded = $false + try { + $sentinelResponse = Invoke-ZtAzureRequest -Path $sentinelPath -FullResponse -ErrorAction Stop + $sentinelOnboarded = ($sentinelResponse.StatusCode -eq 200) + } + catch { + Write-PSFMessage "Error checking Sentinel onboarding for workspace '$($workspace.workspaceName)': $_" -Tag Test -Level Warning + } + + $workspaceResults += [PSCustomObject]@{ + SubscriptionName = $workspace.subscriptionName + WorkspaceName = $workspace.workspaceName + ResourceGroup = $workspace.resourceGroup + WorkspaceId = $workspace.workspaceId + SentinelOnboarded = $sentinelOnboarded + } + } + #endregion Data Collection + + #region Assessment Logic + $onboardedWorkspaces = @($workspaceResults | Where-Object { $_.SentinelOnboarded }) + $passed = $onboardedWorkspaces.Count -ge 1 + + if ($passed) { + $testResultMarkdown = "✅ Microsoft Sentinel is onboarded on at least one Log Analytics workspace.`n`n%TestResult%" + } + else { + $testResultMarkdown = "❌ No Log Analytics workspace in scope has Microsoft Sentinel onboarded.`n`n%TestResult%" + } + #endregion Assessment Logic + + #region Report Generation + $workspacesPortalUrl = 'https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/Microsoft.OperationalInsights%2Fworkspaces' + $workspacePortalTemplate = 'https://portal.azure.com/#resource{0}/overview' + + $formatTemplate = @' + + +### [{0}]({1}) + +| Subscription | Workspace | Resource group | Sentinel onboarded | +| :----------- | :-------- | :------------- | :----------------- | +{2} + +**Summary:** + +- Total workspaces: {3} +- Workspaces with Sentinel onboarded: {4} +'@ + + $onboardedCount = $onboardedWorkspaces.Count + + if ($workspaceResults.Count -gt 0) { + $tableRows = '' + foreach ($result in $workspaceResults) { + $subscriptionName = Get-SafeMarkdown -Text $result.SubscriptionName + $workspaceName = Get-SafeMarkdown -Text $result.WorkspaceName + $resourceGroup = Get-SafeMarkdown -Text $result.ResourceGroup + $workspaceLink = "[$workspaceName]($($workspacePortalTemplate -f $result.WorkspaceId))" + $onboardedLabel = if ($result.SentinelOnboarded) { '✅ Yes' } else { '❌ No' } + $tableRows += "| $subscriptionName | $workspaceLink | $resourceGroup | $onboardedLabel |`n" + } + + $mdInfo = $formatTemplate -f 'Workspaces and their Sentinel onboarding state', $workspacesPortalUrl, $tableRows, $workspaceResults.Count, $onboardedCount + } + else { + $mdInfo = "`nNo Log Analytics workspaces were found across accessible subscriptions.`n" + } + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '61002' + Title = 'Microsoft Sentinel is onboarded on at least one Log Analytics workspace' + Status = $passed + Result = $testResultMarkdown + } + + Add-ZtTestResultDetail @params +} From 92ada763c486e0a6bd10d8287ffde0fef4e4d319 Mon Sep 17 00:00:00 2001 From: Manoj K Date: Fri, 29 May 2026 11:41:51 +0530 Subject: [PATCH 2/2] added private function for sentinal, and refactored test --- .../Get-SentinelWorkspaceData.ps1 | 74 +++++++++++++++ .../tests/Test-Assessment.61002.ps1 | 94 +++++-------------- 2 files changed, 98 insertions(+), 70 deletions(-) create mode 100644 src/powershell/private/tests-shared/Get-SentinelWorkspaceData.ps1 diff --git a/src/powershell/private/tests-shared/Get-SentinelWorkspaceData.ps1 b/src/powershell/private/tests-shared/Get-SentinelWorkspaceData.ps1 new file mode 100644 index 000000000..6082b1006 --- /dev/null +++ b/src/powershell/private/tests-shared/Get-SentinelWorkspaceData.ps1 @@ -0,0 +1,74 @@ +# Enumerates all Log Analytics workspaces across accessible subscriptions using a single +# Azure Resource Graph query (spec Q1+Q2), then checks the Sentinel onboarding state for +# each workspace (spec Q3) via the Microsoft.SecurityInsights/onboardingStates/default endpoint. +# +# Returns an array of [PSCustomObject] (SubscriptionName, WorkspaceName, ResourceGroup, +# WorkspaceId, SentinelOnboarded). +# Returns $null when the ARG query fails so the caller can issue a skip. +function Get-SentinelWorkspaceData { + [CmdletBinding()] + param( + [string]$Activity = 'Fetching Sentinel workspace data' + ) + + # Q1 + Q2: Single ARG query joins subscription names onto workspace records so one + # round-trip covers both steps. ARG respects caller RBAC automatically. + $argQuery = @" +resources +| where type =~ 'microsoft.operationalinsights/workspaces' +| join kind=leftouter ( + resourcecontainers + | where type =~ 'microsoft.resources/subscriptions' + | project subscriptionName=name, subscriptionId +) on subscriptionId +| project + workspaceName=name, + workspaceId=id, + resourceGroup, + subscriptionId, + subscriptionName +| order by subscriptionName asc, workspaceName asc +"@ + + Write-ZtProgress -Activity $Activity -Status 'Enumerating Log Analytics workspaces via Resource Graph' + $allWorkspaces = @() + try { + $allWorkspaces = @(Invoke-ZtAzureResourceGraphRequest -Query $argQuery) + Write-PSFMessage "ARG query returned $($allWorkspaces.Count) Log Analytics workspace(s)." -Tag Test -Level VeryVerbose + } + catch { + Write-PSFMessage "Azure Resource Graph query failed: $($_.Exception.Message)" -Tag Test -Level Warning + return $null + } + + # Q3: For each workspace query the Sentinel onboarding state. + # HTTP 200 = onboarded; HTTP 404 = not onboarded. + # -FullResponse prevents a 404 from throwing so the status code can be inspected. + $results = @() + foreach ($workspace in $allWorkspaces) { + Write-ZtProgress -Activity $Activity -Status "Checking Sentinel onboarding on workspace '$($workspace.workspaceName)'" + + $sentinelOnboarded = $false + try { + $sentinelPath = "$($workspace.workspaceId)/providers/Microsoft.SecurityInsights/onboardingStates/default?api-version=2024-03-01" + $response = Invoke-ZtAzureRequest -Path $sentinelPath -FullResponse -ErrorAction Stop + $sentinelOnboarded = ($response.StatusCode -eq 200) + } + catch { + Write-PSFMessage "Error checking Sentinel onboarding for workspace '$($workspace.workspaceName)': $_" -Tag Test -Level Warning + } + + $results += [PSCustomObject]@{ + SubscriptionName = $workspace.subscriptionName + WorkspaceName = $workspace.workspaceName + ResourceGroup = $workspace.resourceGroup + WorkspaceId = $workspace.workspaceId + SentinelOnboarded = $sentinelOnboarded + } + } + + # Use the unary comma operator so an empty array is preserved as an array + # (not collapsed to $null by the pipeline) and the caller can distinguish + # "no workspaces found" from an ARG failure ($null return above). + return , $results +} diff --git a/src/powershell/tests/Test-Assessment.61002.ps1 b/src/powershell/tests/Test-Assessment.61002.ps1 index 56be2d1be..5484e25b2 100644 --- a/src/powershell/tests/Test-Assessment.61002.ps1 +++ b/src/powershell/tests/Test-Assessment.61002.ps1 @@ -14,7 +14,8 @@ 2. For each workspace, query the Sentinel onboarding state resource via the Microsoft.SecurityInsights/onboardingStates/default ARM endpoint. 3. Pass if at least one workspace returns HTTP 200 (Sentinel is onboarded). - 4. Fail if no workspaces exist or every workspace returns HTTP 404 (not onboarded). + 4. Fail if workspaces exist but every one returns HTTP 404 (Sentinel not onboarded). + 5. Skip if no Log Analytics workspaces are found across accessible subscriptions. .NOTES Test ID: 61002 @@ -57,63 +58,21 @@ function Test-Assessment-61002 { return } - # Q1 + Q2: Use Azure Resource Graph to enumerate Log Analytics workspaces across all - # in-scope subscriptions. ARG respects caller RBAC and returns only accessible resources. - Write-ZtProgress -Activity $activity -Status 'Enumerating Log Analytics workspaces via Resource Graph' - - $argQuery = @" -resources -| where type =~ 'microsoft.operationalinsights/workspaces' -| join kind=leftouter ( - resourcecontainers - | where type =~ 'microsoft.resources/subscriptions' - | project subscriptionName=name, subscriptionId -) on subscriptionId -| project - workspaceName=name, - workspaceId=id, - resourceGroup, - subscriptionId, - subscriptionName -| order by subscriptionName asc, workspaceName asc -"@ - - $allWorkspaces = @() - try { - $allWorkspaces = @(Invoke-ZtAzureResourceGraphRequest -Query $argQuery) - Write-PSFMessage "ARG query returned $($allWorkspaces.Count) Log Analytics workspace(s)." -Tag Test -Level VeryVerbose - } - catch { - Write-PSFMessage "Azure Resource Graph query failed: $($_.Exception.Message)" -Tag Test -Level Warning - Add-ZtTestResultDetail -SkippedBecause NotSupported + # Delegate all data fetching (Q1+Q2+Q3) to the private helper. + # $null return signals an ARG failure; empty array signals no workspaces found. + $workspaceResults = Get-SentinelWorkspaceData -Activity $activity + # $null signals an ARG query failure (e.g. no access to Resource Graph). + # An empty array (Count -eq 0) means no workspaces exist – spec says Skip, not Fail. + if ($null -eq $workspaceResults) { + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess return } - # Q3: For each workspace, query the Sentinel onboarding state. - # HTTP 200 = Sentinel is onboarded; HTTP 404 = not onboarded. - # -FullResponse is used so that a 404 does not throw and the status code can be inspected. - $workspaceResults = @() - foreach ($workspace in $allWorkspaces) { - Write-ZtProgress -Activity $activity -Status "Checking Sentinel onboarding on workspace '$($workspace.workspaceName)'" - - $sentinelPath = "$($workspace.workspaceId)/providers/Microsoft.SecurityInsights/onboardingStates/default?api-version=2024-03-01" - - $sentinelOnboarded = $false - try { - $sentinelResponse = Invoke-ZtAzureRequest -Path $sentinelPath -FullResponse -ErrorAction Stop - $sentinelOnboarded = ($sentinelResponse.StatusCode -eq 200) - } - catch { - Write-PSFMessage "Error checking Sentinel onboarding for workspace '$($workspace.workspaceName)': $_" -Tag Test -Level Warning - } - - $workspaceResults += [PSCustomObject]@{ - SubscriptionName = $workspace.subscriptionName - WorkspaceName = $workspace.workspaceName - ResourceGroup = $workspace.resourceGroup - WorkspaceId = $workspace.workspaceId - SentinelOnboarded = $sentinelOnboarded - } + # Per spec: zero workspaces → Skipped, not Failed. + if ($workspaceResults.Count -eq 0) { + Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions.' -Tag Test -Level VeryVerbose + Add-ZtTestResultDetail -SkippedBecause NotApplicable + return } #endregion Data Collection @@ -150,23 +109,18 @@ resources $onboardedCount = $onboardedWorkspaces.Count - if ($workspaceResults.Count -gt 0) { - $tableRows = '' - foreach ($result in $workspaceResults) { - $subscriptionName = Get-SafeMarkdown -Text $result.SubscriptionName - $workspaceName = Get-SafeMarkdown -Text $result.WorkspaceName - $resourceGroup = Get-SafeMarkdown -Text $result.ResourceGroup - $workspaceLink = "[$workspaceName]($($workspacePortalTemplate -f $result.WorkspaceId))" - $onboardedLabel = if ($result.SentinelOnboarded) { '✅ Yes' } else { '❌ No' } - $tableRows += "| $subscriptionName | $workspaceLink | $resourceGroup | $onboardedLabel |`n" - } - - $mdInfo = $formatTemplate -f 'Workspaces and their Sentinel onboarding state', $workspacesPortalUrl, $tableRows, $workspaceResults.Count, $onboardedCount - } - else { - $mdInfo = "`nNo Log Analytics workspaces were found across accessible subscriptions.`n" + $tableRows = '' + foreach ($result in $workspaceResults) { + $subscriptionName = Get-SafeMarkdown -Text $result.SubscriptionName + $workspaceName = Get-SafeMarkdown -Text $result.WorkspaceName + $resourceGroup = Get-SafeMarkdown -Text $result.ResourceGroup + $workspaceLink = "[$workspaceName]($($workspacePortalTemplate -f $result.WorkspaceId))" + $onboardedLabel = if ($result.SentinelOnboarded) { '✅ Yes' } else { '❌ No' } + $tableRows += "| $subscriptionName | $workspaceLink | $resourceGroup | $onboardedLabel |`n" } + $mdInfo = $formatTemplate -f 'Workspaces and their Sentinel onboarding state', $workspacesPortalUrl, $tableRows, $workspaceResults.Count, $onboardedCount + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation