Public/Test-TenantLens.ps1
|
function Test-TenantLens { <# .SYNOPSIS Evaluates the rule catalog against a snapshot and returns findings. .DESCRIPTION Every rule produces exactly one finding with status Pass, Fail, Manual, Skipped (area not collected) or Error (rule threw). Findings carry rationale, remediation and references so they are useful standalone. .EXAMPLE Test-TenantLens -SnapshotPath .\out\contoso_2026-07-23_1430 #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$SnapshotPath, # Additional rule catalog folders (e.g. a company baseline). Loading a # rule path executes its PSD1 files - only use trusted catalogs. [string[]]$RulePath, # Evaluate only the catalogs given via -RulePath. [switch]$ExcludeDefaultRules, # Only evaluate these rule ids. [string[]]$RuleId, # Only evaluate rules for these areas. [string[]]$Area, [ValidateSet('Critical', 'High', 'Medium', 'Low', 'Info')] [string[]]$Severity, # Write findings.json to this folder (defaults to none). [string]$OutputPath ) $snapshot = Read-TLSnapshot -Path $SnapshotPath $rules = @(Get-TLRuleDefinition -Path $RulePath -IncludeDefault:(-not $ExcludeDefaultRules)) if ($RuleId) { $rules = @($rules | Where-Object { $RuleId -contains $_.Id }) } if ($Area) { $rules = @($rules | Where-Object { $Area -contains $_.Area }) } if ($Severity) { $rules = @($rules | Where-Object { $Severity -contains $_.Severity }) } if ($rules.Count -eq 0) { Write-Warning 'No rules matched the given filters.' return } $validStatuses = @('Pass', 'Fail', 'Manual', 'Skipped', 'Error') $findings = foreach ($rule in ($rules | Sort-Object -Property Id)) { $status = 'Skipped' $evidence = @() $areaProperty = $snapshot.PSObject.Properties[$rule.Area] if ($null -eq $areaProperty) { $evidence = @("Area '$($rule.Area)' was not collected (missing scope or collector skipped).") } else { try { $result = & $rule.Test $snapshot if ($result -is [string]) { $status = $result } elseif ($result -is [hashtable]) { $status = [string]$result['Status'] $evidence = @($result['Evidence']) } elseif ($null -ne $result -and $result.PSObject.Properties['Status']) { $status = [string]$result.Status if ($result.PSObject.Properties['Evidence']) { $evidence = @($result.Evidence) } } else { $status = 'Error' $evidence = @('Rule returned no usable result object.') } if ($validStatuses -notcontains $status) { $evidence = @("Rule returned invalid status '$status'.") + $evidence $status = 'Error' } } catch { $status = 'Error' $evidence = @("Rule evaluation threw: $($_.Exception.Message)") } } [pscustomobject]@{ RuleId = $rule.Id Title = $rule.Title Area = $rule.Area Severity = $rule.Severity Status = $status Evidence = @($evidence | Where-Object { $null -ne $_ } | ForEach-Object { [string]$_ }) Rationale = $rule.Rationale Remediation = $rule.Remediation References = @($rule.References) SnapshotPath = [string]$snapshot._Path } } if ($OutputPath) { $findingsFile = Join-Path -Path $OutputPath -ChildPath 'findings.json' $payload = [ordered]@{ tool = 'TenantLens' version = $script:TLVersion generatedUtc = [DateTime]::UtcNow.ToString('o') snapshot = [string]$snapshot._Path findings = @($findings | Select-Object -Property * -ExcludeProperty SnapshotPath) } Write-TLFile -Path $findingsFile -Content (ConvertTo-Json -InputObject $payload -Depth 8) Write-Verbose ("Findings written to '{0}'." -f $findingsFile) } return $findings } |