Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/powershell/private/tests-shared/Get-SentinelWorkspaceData.ps1
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code treats every non-200 Q3 response as “Sentinel not onboarded,” including 403 Forbidden. Because Invoke-ZtAzureRequest -FullResponse returns the response instead of throwing on non-2xx, the catch will not handle a 403. A user lacking Microsoft Sentinel Reader on a workspace can get a false fail row of No instead of the ARM-required NoAzureAccess skip. The helper should explicitly branch on 200, 404, and 403/Forbidden, and surface access failure to the caller.

$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
}
13 changes: 13 additions & 0 deletions src/powershell/tests/Test-Assessment.61002.md
Original file line number Diff line number Diff line change
@@ -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)

<!--- Results --->
%TestResult%
135 changes: 135 additions & 0 deletions src/powershell/tests/Test-Assessment.61002.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<#
.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 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
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'),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we agreed to use CompatibleLicense from now on.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service = ('Azure') is not just decorative metadata. Connect-ZtAssessment adds Azure to the connected-service list only after Connect-AzAccount succeeds, and Invoke-ZtTests skips any test whose Service metadata is not satisfied before the test function runs.

This check is redundant when the test is run through Invoke-ZtAssessment.

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
}

Comment thread
Manoj-Kesana marked this conversation as resolved.
# 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
}

# Per spec: zero workspaces → Skipped, not Failed.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec says:

3. If Q2 returns zero workspaces across all subscriptions, mark the check 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

#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

$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

$params = @{
TestId = '61002'
Title = 'Microsoft Sentinel is onboarded on at least one Log Analytics workspace'
Status = $passed
Result = $testResultMarkdown
}

Add-ZtTestResultDetail @params
}
Loading