Private/Report/Get-TcProductTrace.ps1
|
function Get-TcProductTrace { <# .SYNOPSIS Looks for the catalog products in the technical tables of each tenant. .DESCRIPTION Tables about people are left out: a first name or an address would read as a product. Rows from a tenant are data, so patterns only ever run on them, never the other way round. The result also says which technical tables were not read: finding nothing in a table nobody read proves nothing. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)][System.Collections.IDictionary]$Snapshot, [Parameter(Mandatory)][System.Collections.IDictionary]$Catalog, [Parameter(Mandatory)][string[]]$Role, [Parameter(Mandatory)][System.Collections.IDictionary]$TableState ) $people = 'users', 'teamsUsers', 'mfaRegistrations', 'contacts', 'mailboxes', 'mailboxUsage', 'userActivity', 'oneDrive', 'oneDriveUsage', 'mailboxPermissions', 'mailboxProtocols', 'signInSummary', 'devices', 'managedDevices', 'mobileDevices', 'sharePointExternalUsers', 'riskyUsers', 'teamComposition', 'teamsSettings', 'exchangeSettings' $nameColumns = 'Name', 'Application', 'Domain', 'Setting', 'Value', 'Title' $options = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::CultureInvariant $searches = @(foreach ($product in $Catalog['products']) { if ($product['detection']) { @{ Id = $product['id']; Regex = [regex]::new($product['detection'], $options) } } }) $traces = @{} foreach ($search in $searches) { $traces[$search['Id']] = [ordered]@{} } $coverage = [ordered]@{} foreach ($tenant in $Role) { $states = $TableState[$tenant] $coverage[$tenant] = [ordered]@{ unread = @($states.Keys | Where-Object { $_ -notin $people -and $states[$_]['state'] -in 'NotCollected', 'NotFullyRead' }) } $tables = Get-TcProperty (Get-TcProperty $Snapshot['tenants'] $tenant) 'tables' if ($null -eq $tables) { continue } $texts = @{} foreach ($name in @($tables.Keys)) { if ($name -in $people) { continue } $rows = @($tables[$name]) if (-not $rows.Count) { continue } $lines = [string[]]::new($rows.Count) for ($i = 0; $i -lt $rows.Count; $i++) { $row = $rows[$i] $values = if ($row -is [System.Collections.IDictionary]) { $row.Values } else { $row } $lines[$i] = @($values) -join ' ' } $texts[$name] = @{ Rows = $rows; Lines = $lines; Blob = $lines -join "`n" } } foreach ($search in $searches) { $regex = $search['Regex'] $byTable = @{} $examples = [System.Collections.Generic.List[string]]::new() foreach ($name in $texts.Keys) { $text = $texts[$name] if (-not $regex.IsMatch($text['Blob'])) { continue } $count = 0 for ($i = 0; $i -lt $text['Lines'].Count; $i++) { if (-not $regex.IsMatch($text['Lines'][$i])) { continue } $count++ if ($examples.Count -ge 40) { continue } foreach ($column in $nameColumns) { $value = Get-TcProperty $text['Rows'][$i] $column if ($value -is [string] -and $value -and $value.Length -lt 90 -and $regex.IsMatch($value)) { $examples.Add($value); break } } } if ($count) { $byTable[$name] = $count } } if (-not $byTable.Count) { continue } $sorted = @($byTable.GetEnumerator() | Sort-Object -Property @{ Expression = 'Value'; Descending = $true }, @{ Expression = 'Key' }) $traces[$search['Id']][$tenant] = [ordered]@{ total = [int](($sorted | Measure-Object -Property Value -Sum).Sum) tables = @($sorted | Select-Object -First 6 | ForEach-Object { , @($_.Key, $_.Value) }) examples = @($examples | Select-Object -Unique -First 5) } } } @{ Traces = $traces; Coverage = $coverage } } |