tests/Test-Assessment.35022.ps1
|
<#
.SYNOPSIS On-Demand Scans Configured for Sensitive Information Discovery .DESCRIPTION Checks if on-demand scans are configured for sensitive information discovery in SharePoint, OneDrive, and Exchange. Implements dynamic SIT GUID -> friendly name resolution and generates a markdown result suitable for inclusion in test reports. Reference: https://learn.microsoft.com/en-us/purview/on-demand-classification .NOTES Test ID: 35022 Pillar: Data Risk Level: Medium User Impact: Low Implementation Cost: Medium #> function Test-Assessment-35022 { [ZtTest( Category = 'Information Protection', ImplementationCost = 'Medium', MinimumLicense = 'Microsoft 365 E5', Pillar = 'Data', RiskLevel = 'Medium', SfiPillar = 'Protect tenants and production systems', TenantType = 'Workforce', TestId = 35022, Title = 'On-Demand scans configured for sensitive information discovery', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking On-Demand Scans Configured for Sensitive Information Discovery' Write-ZtProgress -Activity $activity -Status 'Getting SIT Catalog' $sitGuidMap = @{} $scansList = $null $errorMsg = $null try { # Build dynamic SIT catalog from tenant $sitCatalog = Get-DlpSensitiveInformationType -ErrorAction Stop foreach ($sit in $sitCatalog) { try { $id = $null $id = $sit.Identity $name = $sit.Name $sitGuidMap[$id] = $name } catch { # Ignore individual SIT failures, continue } } } catch { Write-PSFMessage "Warning: Failed to build SIT catalog from tenant: $($_.Exception.Message)" -Level Warning } # Fallback common SIT mapping $fallbackMap = @{ '50842eb7-edc8-4019-85dd-5a5c1f2bb085' = 'Credit Card Number' 'a44669fe-0d48-453d-a9b1-2cc83f2cba77' = 'U.S. Social Security Number (SSN)' 'ed36cf51-9d63-40f3-a9a6-5a865c418d21' = 'U.S. Bank Account Number' '48ee9090-3f74-4238-89c9-6c0a93767a8f' = 'SWIFT Code' '50f56e32-3a6f-459f-82e9-e2b27b96b430' = 'Drivers License Number (U.S.)' '65ce4b3d-79b3-46c0-ba9d-8226d98130c8' = 'IBAN (International Banking Account Number)' '3b35900d-fd2d-446b-b3ad-b4723419e2d5' = 'ABA Routing Number' 'f3dbc5dd-e2d4-4487-b43c-ebd87f349aa4' = 'Canada Social Insurance Number' 'f87b75b6-570d-465d-a91a-f0d9b9e0b000' = 'U.K. National Insurance Number (NINO)' 'b3a2fd72-cc1b-40fc-b0dc-6c5ca0e00f6f' = 'International Medical Record Number (MRN)' } Write-ZtProgress -Activity $activity -Status 'Getting On-Demand Scans' try { $scansList = Get-SensitiveInformationScan -ErrorAction Stop } catch { $errorMsg = $_ Write-PSFMessage "Error querying on-demand scans: $_" -Level Error } #endregion Data Collection #region Assessment Logic $scanCount = 0 $passed = $false $tableData = @() $statusCounts = @{} $hasSharePoint = 0 $hasOneDrive = 0 $hasExchange = 0 $customStatus = $null $mostRecentScan = $null if ($errorMsg) { $passed = $false } else { $scanCount = @($scansList).Count $passed = $scanCount -ge 1 if ($scanCount -gt 0) { foreach ($scan in $scansList) { # Use scan object directly - already contains full details from Get-SensitiveInformationScan # Normalize fields $name = $scan.Name $status = $scan.SensitiveInformationScanStatus # Workload may be string or array $workload = '' if ($scan.Workload -is [System.Collections.IEnumerable] -and -not ($scan.Workload -is [string])) { $workload = ($scan.Workload -join ', ') } else { $workload = $scan.Workload } # Parse ItemStatistics.SIT $sitDetails = @() # Put into list to simulate cmdlet output try { if ($scan.ItemStatistics -and $scan.ItemStatistics.SIT) { $sits = $scan.ItemStatistics.SIT # Determine SIT keys depending on object type if ($sits -is [System.Collections.IDictionary]) { $sitKeys = $sits.Keys } elseif ($sits -is [PSCustomObject]) { $sitKeys = $sits.PSObject.Properties | ForEach-Object { $_.Name } } else { $sitKeys = @() } foreach ($guid in $sitKeys) { $guidString = $guid.ToString().Trim() # Obtain count for this GUID $count = 0 if ($sits -is [System.Collections.IDictionary]) { $count = $sits[$guid] } else { try { $count = $sits.$guid } catch { $count = 0 } } # 🔹 SPEC RULE: Ignore SITs with zero matches if (-not $count -or $count -le 0) { continue } # Resolve SIT GUID to friendly name $friendlyName = $null if ($sitGuidMap.ContainsKey($guidString)) { $friendlyName = $sitGuidMap[$guidString] } elseif ($fallbackMap.ContainsKey($guidString)) { $friendlyName = $fallbackMap[$guidString] } else { try { $sitObj = Get-DlpSensitiveInformationType -Identity $guidString -ErrorAction SilentlyContinue if ($sitObj) { if ($sitObj.PSObject.Properties['Name']) { $friendlyName = $sitObj.Name } elseif ($sitObj.PSObject.Properties['DisplayName']) { $friendlyName = $sitObj.DisplayName } else { $friendlyName = $sitObj.ToString() } } } catch {} } if (-not $friendlyName) { $friendlyName = "Unknown SIT - $guidString" } $sitDetails += "$friendlyName`: $count matches" } } } catch { } $sitString = if ($sitDetails.Count -gt 0) { $sitDetails -join "; " } else { 'None' } $createdUtc = '' if ($scan.WhenCreatedUTC) { $createdUtc = $scan.WhenCreatedUTC } $lastScanStart = '' if ($scan.LastScanStartTime) { $lastScanStart = $scan.LastScanStartTime } # Build output row $row = [PSCustomObject]@{ Name = $name Status = $status Workload = $workload 'SIT Detected' = $sitString 'Created (UTC)' = $createdUtc 'Last Scan Start' = $lastScanStart } $tableData += $row # Status counts if ($status -ne '') { if ($statusCounts.ContainsKey($status)) { $statusCounts[$status]++ } else { $statusCounts[$status] = 1 } } } } # Workload coverage counts $hasSharePoint = (@($scansList) | Where-Object { $_.Workload -and (($_.Workload -contains 'SharePoint') -or (($_.Workload -join ',') -match 'SharePoint')) }).Count $hasOneDrive = (@($scansList) | Where-Object { $_.Workload -and (($_.Workload -contains 'OneDrive') -or (($_.Workload -join ',') -match 'OneDrive')) }).Count $hasExchange = (@($scansList) | Where-Object { $_.Workload -and (($_.Workload -contains 'Exchange') -or (($_.Workload -join ',') -match 'Exchange')) }).Count # Most recent scan start $mostRecentScan = @($scansList) | Where-Object { $_.LastScanStartTime } | Sort-Object LastScanStartTime -Descending | Select-Object -First 1 | ForEach-Object { $_.LastScanStartTime } } #endregion Assessment Logic #region Report Generation $testResultMarkdown = "" if ($errorMsg) { $testResultMarkdown = "Unable to determine on-demand scan configuration due to permissions issues or query failure.`n`n" $customStatus = 'Investigate' } else { if ($passed) { $testResultMarkdown = "✅ At least one on-demand scan is configured in the organization, enabling discovery and classification of historical sensitive information.`n`n" } else { $testResultMarkdown = "❌ No on-demand scans are configured in the organization; historical sensitive data cannot be discovered.`n`n" } $testResultMarkdown += "### On-Demand scan configuration summary`n`n" if ($scanCount -gt 0 -and $tableData) { $testResultMarkdown += "**Scan details:**`n`n" $testResultMarkdown += "| Name | Sensitive information scan status | Workload | Sensitive information types detected | When created UTC | Last scan start time|`n" $testResultMarkdown += "|------|--------|----------|--------------|---------------|-----------------|`n" foreach ($row in $tableData) { $nameEsc = $row.Name $statusEsc = $row.Status $workEsc = $row.Workload $sitEsc = $row.'SIT Detected' $created = if ($row.'Created (UTC)') { Get-FormattedDate -DateString $row.'Created (UTC)' } else { '' } $last = if ($row.'Last Scan Start') { Get-FormattedDate -DateString $row.'Last Scan Start' } else { '' } $testResultMarkdown += "| $nameEsc | $statusEsc | $workEsc | $sitEsc | $created | $last |`n" } $testResultMarkdown += "`n**Summary:**`n`n" $testResultMarkdown += "* **Total on-demand scans configured:** $scanCount`n" $testResultMarkdown += "* **Scans by status:**`n" foreach ($status in ($statusCounts.Keys | Sort-Object)) { $testResultMarkdown += " * $status`: $($statusCounts[$status])`n" } $testResultMarkdown += "* **Locations scanned:**`n" $testResultMarkdown += " * SharePoint: $(if ($hasSharePoint -gt 0) { 'Yes' } else { 'No' })`n" $testResultMarkdown += " * OneDrive: $(if ($hasOneDrive -gt 0) { 'Yes' } else { 'No' })`n" $testResultMarkdown += " * Exchange: $(if ($hasExchange -gt 0) { 'Yes' } else { 'No' })`n" $testResultMarkdown += "* **Most recent scan completion:** $(if ($mostRecentScan) { $mostRecentScan } else { 'No completed scans' })`n" } else { $testResultMarkdown += "* **Total on-demand scans configured:** 0`n" $testResultMarkdown += "* **Status:** No scans are configured`n" } $testResultMarkdown += "`n[Microsoft Purview Portal > Information Protection > Classifiers > On-demand classification](https://purview.microsoft.com/informationprotection/dataclassification/colddatascans)`n" $testResultMarkdown += "or" $testResultMarkdown += "`n[Microsoft Purview Portal > Data Loss Prevention > Classifiers > On-demand classification](https://purview.microsoft.com/datalossprevention/dataclassification/colddatascans)`n" } #endregion Report Generation $params = @{ TestId = '35022' Title = 'On-Demand scans configured for sensitive information discovery' Status = $passed Result = $testResultMarkdown } if ($null -ne $customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |