From bcaacb1ac53f3b34319212dafcaa74f5a28ce916 Mon Sep 17 00:00:00 2001 From: Jan Egil Ring Date: Tue, 20 Oct 2026 04:39:38 +0000 Subject: [PATCH] Implement hosted Defender for Servers setup and Endpoint protection in lab automation Signed-off-by: Jan Egil Ring --- .../01_Sovereign_Cloud/Readme.md | 2 +- .../labautomation/hosted-defender.ps1 | 75 ++++++++++ .../labautomation/shared-deploy-lab.ps1 | 5 +- .../resources/hosted-events/readme.md | 27 +++- .../resources/localbox/manual-preparation.md | 28 +++- .../resources/tests/hosted-defender.tests.ps1 | 140 ++++++++++++++++++ .../resources/tests/readme.md | 14 ++ .../walkthrough/challenge-06/solution-06.md | 6 +- 8 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/hosted-defender.ps1 create mode 100644 03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/hosted-defender.tests.ps1 diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md index c57b74ee2..d35e8c490 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/Readme.md @@ -152,7 +152,7 @@ The main cost driver for this Hackathon is virtual machines: - Plan subscription quotas for at least 12 general-purpose vCPUs and 4 confidential vCPUs per participant, plus upgrade headroom. Keep two participants per subscription unless larger aggregate increases have been approved. Challenge 5 application cleanup does not release node quota because Challenge 7 still uses the cluster. For a 50-participant event, this planning range is USD 1,750-2,250 per day, plus LocalBox for each participating subscription if Challenge 6 is enabled. -There will also be smaller costs for services such as Key Vault, storage, and monitoring. Budget separately for the approved Defender for Servers plan used in Challenge 6. +There will also be smaller costs for services such as Key Vault, storage, and monitoring. Hosted Console setup automatically enables **Defender for Servers P2 and Endpoint protection on every event subscription**. Budget separately for P2 charges on covered servers during preparation and delivery; the infrastructure estimates above do not include them. Manual/BYOS organizers enable their approved plan separately. Removing lab resource groups does not reset subscription-level Defender settings. An Azure Pricing Calculator estimate is available [here](https://azure.com/e/1a7aec76a3e049cba57cda6742025373). This estimate can be adjusted for fewer/more students, running the VMs shorter/longer and adding additional services if desired. diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/hosted-defender.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/hosted-defender.ps1 new file mode 100644 index 000000000..fc4bbba98 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/hosted-defender.ps1 @@ -0,0 +1,75 @@ +function Initialize-MhhHostedDefender { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$SubscriptionId + ) + + $request = { + param([string]$Path, [string]$Method, [hashtable]$Body) + Update-MhhToken | Out-Null + $context = Get-AzContext -ErrorAction Stop + if ($context.Subscription.Id -ne $SubscriptionId) { + throw "The Azure context must target hosted subscription $SubscriptionId before configuring Defender." + } + $arguments = @{ Path = $Path; Method = $Method; ErrorAction = 'Stop' } + if ($null -ne $Body) { + $arguments.Payload = $Body | ConvertTo-Json -Depth 30 -Compress + } + $response = Invoke-AzRestMethod @arguments + $expectedStatus = if ($Method -eq 'PUT') { @(200, 201) } else { @(200) } + if ($response.StatusCode -notin $expectedStatus) { + throw "Defender $Method $Path failed (HTTP $($response.StatusCode)): $($response.Content)" + } + $response.Content | ConvertFrom-Json -AsHashtable -ErrorAction Stop + } + + $basePath = "/subscriptions/$SubscriptionId/providers/Microsoft.Security" + $pricingPath = "$basePath/pricings/VirtualMachines?api-version=2024-01-01" + $endpointPath = "$basePath/settings/WDATP?api-version=2021-06-01" + Write-Host "Enabling paid Defender for Servers P2 and Endpoint protection in hosted subscription $SubscriptionId..." + $pricing = & $request $pricingPath 'GET' + if (-not $pricing.properties -or -not $pricing.properties.pricingTier) { + throw 'Defender pricing response is missing properties.pricingTier.' + } + if ($pricing.properties.pricingTier -ne 'Standard' -or $pricing.properties.subPlan -ne 'P2') { + $properties = @{ pricingTier = 'Standard'; subPlan = 'P2' } + if ($pricing.properties.ContainsKey('enforce')) { + $properties.enforce = $pricing.properties.enforce + } + if ($pricing.properties.ContainsKey('extensions')) { + # Preserve configured extensions, excluding read-only operation status. + $properties.extensions = @( + foreach ($extension in $pricing.properties.extensions) { + $item = @{ name = $extension.name; isEnabled = $extension.isEnabled } + if ($extension.ContainsKey('additionalExtensionProperties')) { + $item.additionalExtensionProperties = $extension.additionalExtensionProperties + } + $item + } + ) + } + & $request $pricingPath 'PUT' @{ properties = $properties } | Out-Null + } + + & $request $endpointPath 'PUT' @{ + kind = 'DataExportSettings' + properties = @{ enabled = $true } + } | Out-Null + + for ($attempt = 1; $attempt -le 12; $attempt++) { + $pricing = & $request $pricingPath 'GET' + $endpoint = & $request $endpointPath 'GET' + if ($pricing.properties.pricingTier -eq 'Standard' -and + $pricing.properties.subPlan -eq 'P2' -and + $endpoint.properties.enabled -is [bool] -and $endpoint.properties.enabled) { + Write-Host 'Verified Defender for Servers P2 and Endpoint protection. VM onboarding must still be validated.' -ForegroundColor Green + return + } + if ($attempt -lt 12) { + Write-Host "Waiting for Defender settings propagation ($attempt/12)..." + Start-Sleep -Seconds 10 + } + } + throw "Defender verification failed in subscription $SubscriptionId after 12 attempts: tier=$($pricing.properties.pricingTier), plan=$($pricing.properties.subPlan), Endpoint protection=$($endpoint.properties.enabled). Stop provisioning and inspect Defender environment settings." +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/shared-deploy-lab.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/shared-deploy-lab.ps1 index dce4e831d..866167fba 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/shared-deploy-lab.ps1 +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/shared-deploy-lab.ps1 @@ -7,7 +7,8 @@ required Azure resource providers. It ensures one preferred region has the VM SKUs and compute quota required by the subscription's participant count. It also submits one shared LocalBox environment per subscription for Challenge 6. For Microsoft-hosted environments only: assigns and verifies the MCAPS control-tag -initiative before deploying resources. Do not use this hook for BYOS setup. +initiative and enables paid Defender for Servers P2 with Endpoint protection before +deploying resources. Do not use this hook for BYOS setup. #> param( [Parameter(Mandatory=$true)] @@ -23,6 +24,7 @@ param( . (Join-Path $PSScriptRoot 'quota-helpers.ps1') . (Join-Path $PSScriptRoot 'localbox-credentials.ps1') . (Join-Path $PSScriptRoot 'hosted-tag-policy.ps1') +. (Join-Path $PSScriptRoot 'hosted-defender.ps1') Get-LocalBoxConsoleGroupCredential @@ -155,6 +157,7 @@ Set-MhhConfidentialComputeSelection -SubscriptionId $SubscriptionId -Candidate $ Write-Host "Selected $($selectedCandidate.VmSize) in $($selectedCandidate.Location); compute SKUs and quotas are ready for $participantCount participants." -ForegroundColor Green Initialize-MhhHostedTagPolicy -SubscriptionId $SubscriptionId -Location $selectedCandidate.Location +Initialize-MhhHostedDefender -SubscriptionId $SubscriptionId $localBoxResourceGroupName = 'rg-localbox-shared' $localBoxLocations = @( diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/hosted-events/readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/hosted-events/readme.md index 9c5aa827e..f4e7d84d5 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/hosted-events/readme.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/hosted-events/readme.md @@ -14,6 +14,31 @@ This runbook covers the FY27 EMEA Cloud & AI Hackathons program described in the When automatic provisioning is not scheduled, use the Console's **Create Users**, content-pack license defaults and **Deploy Lab Environment** lifecycle in order. Investigate failures before continuing; do not compensate by running subscription-wide manual utilities. +## Defender for Servers configuration and costs + +The [shared hook](../../labautomation/shared-deploy-lab.ps1) calls +[hosted Defender setup](../../labautomation/hosted-defender.ps1) once per subscription, +after hosted tag-policy verification and before LocalBox deployment. It sets +`Microsoft.Security/pricings/VirtualMachines` to `Standard` / `P2` and +`Microsoft.Security/settings/WDATP` to `enabled=true` (the **Endpoint protection** +toggle). Existing pricing extension and enforcement settings are preserved; other +Defender plans and settings are not changed. Reruns never toggle protection off. + +The Console deployment identity needs permission to read/write these subscription +settings; the hosted subscription Owner identity supplies it. Participants retain +their existing roles, including Security Reader; no participant subscription Owner +or User Access Administrator role is added. Setup verifies both settings by GET, +retrying propagation up to 12 times, 10 seconds apart. Authorization/API failures +or verification timeout stop shared setup rather than report readiness. A failure +does not roll back P2 or disable protection; inspect the settings before retrying. + +**P2 is a paid subscription-wide plan**, not a per-lab-RG setting. Include its +charges for covered servers during preparation and delivery in the approved event +budget, separately from the existing infrastructure estimates. Use only dedicated +hosted lab subscriptions. Deleting lab resource groups does not reset the plan or +Endpoint integration; the subscription owner must review these settings when +releasing a subscription. This hook is not part of manual/BYOS preparation. + ## Complete LocalBox preparation The shared hook submits `localbox-*` in `rg-localbox-shared`, then waits up to six hours for ARM completion, refreshing its Console authentication while polling. It does not publish LocalBox administrator credentials. A Client VM appearing after roughly 15-20 minutes confirms only that deployment has started. Full nested Azure Local provisioning can take 4-6 hours and must still be checked separately. @@ -23,7 +48,7 @@ The shared hook submits `localbox-*` in `rg-localbox-shared`, then waits up to s 3. Copy **Lab Group ObjectId** from the Console's **Credentials** tab for the preparation prompt or `-AksAdminGroupObjectId`. Confirm that intended Azure lab identities/coaches are members; see the group dependency below. 4. Follow [LocalBox preparation](../localbox/readme.md), once per shared subscription, in elevated PowerShell 7. It creates AKS and then installs/verifies the shared MetalLB extension and reserved ARP pool. Azure authentication uses the Client VM's managed identity; preparation constructs the nested Windows credential locally from the installed configuration without printing it. An explicit `-NodeCredential` overrides that value after a nested-account rotation. 5. Run [Pester health checks](../tests/readme.md) for LocalBox and every selected participant lab. A control-plane-only pass does not establish full readiness. -6. Complete the [participant VM readiness exercise](../localbox/manual-preparation.md#step-6-test-the-environment), including guest management, Defender and Update Manager. Subscription-wide paid-plan changes require the authorized owner and approved budget. +6. Console shared setup enables and verifies **Defender for Servers Plan 2** and **Endpoint protection** on every event subscription. Complete the [Defender readiness checklist](../localbox/manual-preparation.md#defender-for-servers-readiness-organizer) and [participant VM readiness exercise](../localbox/manual-preparation.md#step-6-test-the-environment), including guest management, actual Defender onboarding and Update Manager. Verified subscription settings do not prove that a VM has completed onboarding. 7. Verify an intended participant can use Entra-authenticated `az connectedk8s proxy` and a private application port-forward from their Codespace. Participants inspect MetalLB, not install it, and never need LocalBox-Client access. This validates the lab access path, not routing to the MetalLB service VIP. Shared setup registers `Microsoft.KubernetesRuntime`, waits for registration, diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/manual-preparation.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/manual-preparation.md index 2f03c3462..fabfdb0a2 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/manual-preparation.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/manual-preparation.md @@ -82,12 +82,38 @@ Hosted lab automation grants participants access to shared LocalBox resources, * #### Defender for Servers readiness (organizer) +Complete these checks on **every event subscription** before attendees arrive. +Hosted Console setup enables and reads back **Defender for Servers P2** and +**Endpoint protection**, and grants participants Security Reader access. For +manual/BYOS setup, an authorized subscription administrator enables the approved +plan and integration using the steps below. In both paths, verify actual onboarding +of a test participant VM; subscription configuration alone is not exercise readiness. + 1. Confirm the Defender for Servers plan approved for the event, including its subscription scope and charges 2. As an authorized subscription administrator, open **Microsoft Defender for Cloud -> Environment settings** and select the lab subscription 3. Under **Defender plans**, enable **Servers** with the approved plan if it is not already enabled, and save the changes -4. Verify that the [Defender for Endpoint integration](https://learn.microsoft.com/azure/defender-for-cloud/enable-defender-for-endpoint) is enabled and that required onboarding settings and extensions can apply to the participant VMs +4. Open **Settings & monitoring** for Servers and verify **Endpoint protection** is **On**. If enabling it, select **Continue**, then **Save** on the Defender plans page. This [Defender for Endpoint integration](https://learn.microsoft.com/azure/defender-for-cloud/enable-defender-for-endpoint) is separate from the Servers plan status; verify it even when the plan is already enabled. 5. Verify that participants have **Security Reader** access to review coverage and recommendations. Students verify protection on their own VM; subscription-level plan changes remain an organizer responsibility +For a read-only CLI check, use the intended event subscription explicitly: + +```bash +SUBSCRIPTION_ID='' +az rest --method get \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.Security/pricings/VirtualMachines?api-version=2024-01-01" \ + --query 'properties.{tier:pricingTier,plan:subPlan}' +az rest --method get \ + --url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.Security/settings/WDATP?api-version=2021-06-01" \ + --query 'properties.enabled' +``` + +For hosted Console labs, expect `Standard` / `P2` and `true` for `WDATP`. +For manual labs, compare the returned plan with the approved plan. A missing setting +or a failed query is not proof that integration is enabled. These are +subscription-configuration checks only: verify `MDE.Windows` extension success +and actual device onboarding on the test VM below. Allow for onboarding delays; +do not repeatedly toggle integration to force deployment. + ### Step 6: Test the Environment Before participants begin, follow the [Challenge 6 walkthrough](../../walkthrough/challenge-06/solution-06.md) using a normal participant identity: diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/hosted-defender.tests.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/hosted-defender.tests.ps1 new file mode 100644 index 000000000..bfde60a08 --- /dev/null +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/hosted-defender.tests.ps1 @@ -0,0 +1,140 @@ +BeforeAll { + $root = (Resolve-Path "$PSScriptRoot/../..").Path + . "$root/labautomation/hosted-defender.ps1" + function Update-MhhToken {} + function Get-AzContext { [CmdletBinding()] param() } + function Invoke-AzRestMethod { [CmdletBinding()] param($Path, $Method, $Payload) } +} + +Describe 'Hosted Defender setup' { + BeforeEach { + $script:pricing = @{ pricingTier = 'Free' } + $script:endpoint = $false + $script:writes = @() + Mock Update-MhhToken {} + Mock Get-AzContext { @{ Subscription = @{ Id = 'hosted-subscription' } } } + Mock Start-Sleep {} + Mock Invoke-AzRestMethod { + $isPricing = $Path -like '*/pricings/VirtualMachines?*' + if ($Method -eq 'PUT') { + $body = $Payload | ConvertFrom-Json -AsHashtable + $script:writes += @{ Path = $Path; Body = $body } + if ($isPricing) { $script:pricing = $body.properties } + else { $script:endpoint = $body.properties.enabled } + } + $properties = if ($isPricing) { $script:pricing } else { @{ enabled = $script:endpoint } } + @{ StatusCode = 200; Content = (@{ properties = $properties } | ConvertTo-Json -Depth 30) } + } + } + + It 'enables P2 and Endpoint protection only in the requested subscription' { + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + $script:writes.Count | Should -Be 2 + $script:writes[0].Path | Should -Be '/subscriptions/hosted-subscription/providers/Microsoft.Security/pricings/VirtualMachines?api-version=2024-01-01' + $script:writes[0].Body.properties.pricingTier | Should -Be 'Standard' + $script:writes[0].Body.properties.subPlan | Should -Be 'P2' + $script:writes[1].Path | Should -Be '/subscriptions/hosted-subscription/providers/Microsoft.Security/settings/WDATP?api-version=2021-06-01' + $script:writes[1].Body.kind | Should -Be 'DataExportSettings' + $script:writes[1].Body.properties.enabled | Should -BeTrue + Should -Invoke Invoke-AzRestMethod -Times 2 -Exactly -ParameterFilter { $Method -eq 'GET' -and $Path -like '*/pricings/*' } + Should -Invoke Invoke-AzRestMethod -Times 1 -Exactly -ParameterFilter { $Method -eq 'GET' -and $Path -like '*/settings/WDATP?*' } + } + + It 'upgrades P1 while preserving writable extension and enforcement settings' { + $script:pricing = @{ + pricingTier = 'Standard'; subPlan = 'P1'; enforce = 'False' + extensions = @(@{ name = 'AgentlessVmScanning'; isEnabled = 'False'; additionalExtensionProperties = @{ ExclusionTags = '[]' }; operationStatus = @{ code = 'Succeeded' } }) + freeTrialRemainingTime = 'PT0S' + } + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + $properties = $script:writes[0].Body.properties + $properties.enforce | Should -Be 'False' + $properties.extensions.Count | Should -Be 1 + $properties.extensions[0].name | Should -Be 'AgentlessVmScanning' + $properties.extensions[0].isEnabled | Should -Be 'False' + $properties.extensions[0].additionalExtensionProperties.ExclusionTags | Should -Be '[]' + $properties.extensions[0].ContainsKey('operationStatus') | Should -BeFalse + $properties.ContainsKey('freeTrialRemainingTime') | Should -BeFalse + } + + It 'does not rewrite P2 or disable Endpoint protection on reruns' { + $script:pricing = @{ pricingTier = 'Standard'; subPlan = 'P2' } + $script:endpoint = $true + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + Should -Invoke Invoke-AzRestMethod -Times 0 -ParameterFilter { $Method -eq 'PUT' -and $Path -like '*/pricings/*' } + $script:writes.Count | Should -Be 2 + foreach ($write in $script:writes) { $write.Body.properties.enabled | Should -BeTrue } + } + + It 'repairs disabled Endpoint protection when P2 is already enabled' { + $script:pricing = @{ pricingTier = 'Standard'; subPlan = 'P2' } + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + $script:endpoint | Should -BeTrue + $script:writes.Count | Should -Be 1 + } + + It 'refuses a different Azure context before any request' { + { Initialize-MhhHostedDefender -SubscriptionId 'another-subscription' } | Should -Throw '*context must target*' + Should -Invoke Invoke-AzRestMethod -Times 0 + } + + It 'propagates authorization errors and does not claim success' { + Mock Invoke-AzRestMethod { throw 'AuthorizationFailed' } + { Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' } | Should -Throw '*AuthorizationFailed*' + } + + It 'rejects unexpected HTTP status codes' -TestCases @(@{ Status = 403 }, @{ Status = 202 }, @{ Status = 500 }) { + param($Status) + Mock Invoke-AzRestMethod { @{ StatusCode = $Status; Content = 'test failure' } } + { Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' } | Should -Throw '*HTTP*test failure*' + } + + It 'rejects malformed or missing pricing data before writes' -TestCases @(@{ Content = 'not json' }, @{ Content = '{}' }) { + param($Content) + Mock Invoke-AzRestMethod { @{ StatusCode = 200; Content = $Content } } + { Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' } | Should -Throw + Should -Invoke Invoke-AzRestMethod -Times 0 -ParameterFilter { $Method -eq 'PUT' } + } + + It 'accepts a created response to a PUT' { + Mock Invoke-AzRestMethod { @{ StatusCode = 201; Content = '{"properties":{"enabled":true}}' } } -ParameterFilter { $Method -eq 'PUT' -and $Path -like '*/settings/*' } + $script:endpoint = $true + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + } + + It 'waits for read-back convergence' { + $script:reads = 0 + Mock Invoke-AzRestMethod { + $script:reads++ + @{ StatusCode = 200; Content = (@{ properties = @{ enabled = ($script:reads -gt 1) } } | ConvertTo-Json) } + } -ParameterFilter { $Method -eq 'GET' -and $Path -like '*/settings/*' } + Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' + Should -Invoke Start-Sleep -Times 1 -Exactly -ParameterFilter { $Seconds -eq 10 } + } + + It 'fails after bounded verification instead of trusting successful PUTs' -TestCases @( + @{ Content = '{"properties":{"enabled":false}}' } + @{ Content = '{"properties":{"enabled":"true"}}' } + @{ Content = '{}' } + ) { + param($Content) + Mock Invoke-AzRestMethod { @{ StatusCode = 200; Content = $Content } } -ParameterFilter { $Method -eq 'GET' -and $Path -like '*/settings/*' } + { Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' } | Should -Throw '*after 12 attempts*' + Should -Invoke Start-Sleep -Times 11 -Exactly + Should -Invoke Invoke-AzRestMethod -Times 12 -Exactly -ParameterFilter { $Method -eq 'GET' -and $Path -like '*/settings/*' } + } + + It 'fails if the plan does not persist even with Endpoint protection enabled' { + Mock Invoke-AzRestMethod { @{ StatusCode = 200; Content = '{"properties":{"pricingTier":"Standard","subPlan":"P1"}}' } } -ParameterFilter { $Method -eq 'GET' -and $Path -like '*/pricings/*' } + { Initialize-MhhHostedDefender -SubscriptionId 'hosted-subscription' } | Should -Throw '*plan=P1*' + } + + It 'wires Defender after hosted tag setup and before LocalBox deployment' { + $hook = Get-Content "$root/labautomation/shared-deploy-lab.ps1" -Raw + $hook | Should -Match "Join-Path \`$PSScriptRoot 'hosted-defender.ps1'" + $call = $hook.IndexOf('Initialize-MhhHostedDefender -SubscriptionId $SubscriptionId') + $call | Should -BeGreaterThan $hook.IndexOf('Initialize-MhhHostedTagPolicy -SubscriptionId') + $call | Should -BeLessThan $hook.IndexOf("`$localBoxResourceGroupName = 'rg-localbox-shared'") + } +} diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md index e8e5cbe48..07308d2f4 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md @@ -43,6 +43,20 @@ exception while retaining the four European regions. These checks make no Azure changes; a fresh Console deployment and participant VM creation remain required to validate regional service availability and effective inherited policies. +## Hosted Defender unit tests + +Run `Invoke-Pester ./hosted-defender.tests.ps1 -Output Detailed` for the focused +offline suite. It mocks Azure REST calls and covers Free/P1 to P2 setup, Endpoint +protection, subscription-context guards, preservation of existing pricing settings, +reruns, authorization/HTTP errors, malformed responses, independent read-back, +bounded propagation and shared-hook wiring. These tests do not enable paid plans. + +The Endpoint protection API was tested off/on in an authorized test subscription +on 2026-10-19 and restored to enabled, without changing its existing P1 plan. +That does not validate this new P2 automation live. A fresh Console deployment +must verify P2 and Endpoint protection, followed by a normal participant login, +VM onboarding and Challenge 1 modify-policy identity/RBAC/remediation testing. + ## Hosted tag-policy unit tests From this directory, run `Invoke-Pester ./hosted-tag-policy.tests.ps1 -Output Detailed`. These offline tests mock all Azure and Console commands. They cover hosted-only wiring, subscription targeting, tag-policy propagation, both tag values on resources and resource groups, preservation of unrelated tags, bounded failure, and temporary probe cleanup. Compile the initiative separately with `az bicep build --file ../../labautomation/infra/hosted-tag-policy.bicep --stdout`. diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-06/solution-06.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-06/solution-06.md index 2a82706f1..7bf152eac 100644 --- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-06/solution-06.md +++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/walkthrough/challenge-06/solution-06.md @@ -20,7 +20,7 @@ Use your [Sovereign Cloud Codespace](https://github.com/microsoft/MicroHack/blob - **Reader** and **Azure Stack HCI VM Contributor** role permissions on the resource group containing the shared LocalBox resources - Your assigned resource group (for example, `labuser-XX`) with permission to create and manage your VM and its extensions and assess updates. Hosted labs grant **Owner** on your own resource group - **Security Reader** access at subscription scope to review Defender for Cloud coverage and recommendations -- Defender for Servers enabled by the organizer on the lab subscription, using an approved plan and the required Defender for Endpoint integration +- Defender for Servers and Endpoint protection enabled on the lab subscription (P2 is configured by hosted Console setup; manual labs use the organizer-approved plan) - A guest network with a valid IP address, working DNS, and outbound access to the required Azure Arc, Defender, and configured Windows update-source endpoints - For Task 5, the shared `localbox-aks` cluster in `rg-localbox-shared`, with Microsoft Entra authentication, Kubernetes RBAC, and organizer-prepared MetalLB networking - Membership in the cluster's configured Microsoft Entra administrator group, supplied through the workshop Console, and the **Azure Arc Enabled Kubernetes Cluster User Role** already assigned by the organizer for Cluster Connect. These are separate prerequisites; ask the facilitator to verify either if access fails @@ -202,7 +202,7 @@ The screenshots in Tasks 3 and 4 use `labuser23-vm-01` in resource group `rg-lab 1. Navigate to **Microsoft Defender for Cloud** in the Azure Portal 2. Open **Inventory** and filter by your subscription and assigned resource group 3. Search for `labuserXX-vm-01` (using your own suffix). Match its resource ID to the VM recorded in Task 2, rather than selecting a shared machine with a similar name -4. Open the resource details and review its Defender for Servers coverage and onboarding status. The organizer should already have enabled the approved Servers plan and Defender for Endpoint integration +4. Open the resource details and review its Defender for Servers coverage and onboarding status. Hosted Console setup enables Servers P2 and Endpoint protection; manual labs require the organizer to enable the approved plan and integration You can also open your VM resource and select **Settings -> Security** to see its **Microsoft Defender for Servers** status, as illustrated in Task 3.2. The **On** indicator shows the displayed plan status; it does not by itself confirm that Defender for Endpoint onboarding has completed. @@ -214,7 +214,7 @@ Select the subscription and review **Defender plans**, including the **Servers** ![Subscription Defender plans showing Servers Plan 2 and Full monitoring coverage](./images/dfc_03.png) -The example subscription uses **Servers Plan 2** with **Full** monitoring coverage. Your lab's approved plan may differ. These subscription-level settings are context, not evidence that your individual VM has completed onboarding. +The example subscription uses **Servers Plan 2** with **Full** monitoring coverage. Your lab's approved plan may differ. Ask the facilitator to confirm **Settings & monitoring > Endpoint protection** is **On**, even if the Servers plan is enabled. These subscription-level settings are context, not evidence that your individual VM has completed onboarding. > [!NOTE] > A newly created VM can take time to appear and complete onboarding. If it is missing, first recheck **Guest management: Enabled (Connected)** and the selected subscription/resource group, then refresh Inventory. Ask the facilitator to check plan coverage and extension onboarding if it remains missing or unprotected. An Inventory entry alone does not prove that Defender for Servers protection is active. Do not enable a paid subscription-level plan yourself in a hosted lab.