Skip to content

ci: add super-linter (soft launch) - #1

Merged
LukeEvansTech merged 4 commits into
mainfrom
chore/add-super-linter
Aug 17, 2026
Merged

ci: add super-linter (soft launch)#1
LukeEvansTech merged 4 commits into
mainfrom
chore/add-super-linter

Conversation

@LukeEvansTech

Copy link
Copy Markdown
Owner

Adds soft-launched super-linter via the shared reusable workflow at LukeEvansTech/shared-workflows@v1. Lint findings appear in the workflow step summary and as a PR comment; failures do not block merges. See https://github.com/LukeEvansTech/shared-workflows/blob/main/docs/spec.md.

@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
BIOME_FORMAT Pass ✅
BIOME_LINT Pass ✅
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Fail ❌
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSCPD Fail ❌
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

Super-linter detected linting errors

For more information, see the GitHub Actions workflow run

Powered by Super-linter

GITHUB_ACTIONS_ZIZMOR
�[1m�[91merror[unpinned-uses]�[0m�[1m: unpinned action reference�[0m
  �[1m�[94m--> �[0m/github/workspace/.github/workflows/lint.yml:16:11
   �[1m�[94m|�[0m
�[1m�[94m16�[0m �[1m�[94m|�[0m     uses: LukeEvansTech/shared-workflows/.github/workflows/super-linter.yml@v1
   �[1m�[94m|�[0m           �[1m�[91m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^�[0m �[1m�[91maction is not pinned to a hash (required by blanket policy)�[0m
   �[1m�[94m|�[0m
   �[1m�[94m= �[0m�[1mnote�[0m: audit confidence → High
   �[1m�[94m= �[0m�[1mnote�[0m: this finding has an auto-fix
   �[1m�[94m= �[0m�[1mhelp�[0m: audit documentation → �[32mhttps://docs.zizmor.sh/audits/#unpinned-uses�[39m

�[32m4�[39m findings (�[1m�[93m3�[39m suppressed, �[92m1�[39m fixable�[0m): �[35m0�[39m informational, �[36m0�[39m low, �[33m0�[39m medium, �[31m1�[39m high🌈 zizmor v1.23.1
�[32m INFO�[0m �[1maudit�[0m�[2m:�[0m �[2mzizmor�[0m�[2m:�[0m 🌈 completed /github/workspace/.github/workflows/lint.yml
JSCPD
Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

 39 │ 38 │ [CmdletBinding()]
 40 │ 39 │ param(
 41 │ 40 │     [switch]$SkipConnection,
 42 │ 41 │     [switch]$IncludePassedDetails
 43 │ 42 │ )
 44 │ 43 │
 45 │ 44 │ # Script configuration
 46 │ 45 │ $ErrorActionPreference = 'Stop'
 47 │ 46 │ $ProgressPreference = 'Continue'
 48 │ 47 │
 49 │ 48 │ # Set up paths
 50 │ 49 │ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
 51 │ 50 │ $reportsPath = Join-Path $scriptPath "Reports"
 52 │ 51 │
 53 │ 52 │ # Ensure Reports directory exists
 54 │ 53 │ if (!(Test-Path $reportsPath)) {
 55 │ 54 │     New-Item -ItemType Directory -Path $reportsPath -Force | Out-Null
 56 │ 55 │     Write-Host "Created Reports directory: $reportsPath" -ForegroundColor Green
 57 │ 56 │ }
 58 │ 57 │
 59 │ 58 │ # Import required modules
 60 │ 59 │ Write-Host "`nExchange Online Security Health Check (Simplified)"

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

 81  │ 80  │ $_"
 82  │ 81  │         exit 1
 83  │ 82  │     }
 84  │ 83  │ }
 85  │ 84  │
 86  │ 85  │ # Verify connection
 87  │ 86  │ $connectionInfo = Get-ConnectionInformation | Where-Object { $_.Name -like '*ExchangeOnline*' }
 88  │ 87  │ if (-not $connectionInfo) {
 89  │ 88  │     Write-Error "No active Exchange Online connection found. Please connect first or remove -SkipConnection parameter."
 90  │ 89  │     exit 1
 91  │ 90  │ }
 92  │ 91  │
 93  │ 92  │ Write-Host "`nConnection Details:" -ForegroundColor Cyan
 94  │ 93  │ Write-Host "  Tenant: $($connectionInfo.TenantId)" -ForegroundColor White
 95  │ 94  │ Write-Host "  User: $($connectionInfo.UserPrincipalName)" -ForegroundColor White
 96  │ 95  │ Write-Host "  Environment: $($connectionInfo.ConnectionUri)" -ForegroundColor White
 97  │ 96  │
 98  │ 97  │ # Get Maester module path
 99  │ 98  │ $maesterModule = Get-Module -ListAvailable Maester | Select-Object -First 1
 100 │ 99  │ if (!$maesterModule) {
 101 │ 100 │     Write-Error "Maester module not found. Please install it first: Install-Module Maester"
 102 │ 101 │     exit 1
 103 │ 102 │ }
 104 │ 103 │
 105 │ 104 │ $maesterPath = Split-Path $maesterModule.Path
 106 │ 105 │ $testsPath = Join-Path $maesterPath "maester-tests"
 107 │ 106 │
 108 │ 107 │ Write-Host "`nSearching for Exchange Online tests..." -ForegroundColor Yellow
 109 │ 108 │
 110 │ 109 │ # Find all test files with Exchange-related tags
 111 │ 110 │ $exoTestFiles = @()
 112 │ 111 │ $testPatterns = @(
 113 │ 112 │     '-Tag.*["'']EXO["'']'

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

 114 │ 113 │ # CISA tests use "MS.EXO"
 115 │ 114 │ )
 116 │ 115 │
 117 │ 116 │ # Search in relevant directories
 118 │ 117 │ $searchDirs = Get-ChildItem -Path $testsPath -Directory -Recurse -ErrorAction SilentlyContinue |
 119 │ 118 │     Where-Object {
 120 │ 119 │         $_.Name -in @('exchange', 'orca', 'cis', 'cisa') -or
 121 │ 120 │         $_.FullName -match 'exchange|exo'
 122 │ 121 │     }
 123 │ 122 │
 124 │ 123 │ foreach ($dir in $searchDirs) {
 125 │ 124 │     $files = Get-ChildItem -Path $dir.FullName -Filter "*.Tests.ps1" -File -ErrorAction SilentlyContinue
 126 │ 125 │     foreach ($file in $files) {
 127 │ 126 │         $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
 128 │ 127 │         if ($content) {
 129 │ 128 │             foreach ($pattern in $testPatterns) {
 130 │ 129 │                 if ($content -match $pattern) {
 131 │ 130 │                     if ($file.FullName -notin $exoTestFiles.FullName) {
 132 │ 131 │                         $exoTestFiles += $file
 133 │ 132 │                     }
 134 │ 133 │                     break
 135 │ 134 │                 }
 136 │ 135 │             }
 137 │ 136 │         }
 138 │ 137 │     }
 139 │ 138 │ }
 140 │ 139 │
 141 │ 140 │ $exoTestFiles = $exoTestFiles | Sort-Object DirectoryName, Name
 142 │ 141 │ Write-Host "Found

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

 142 │ 141 │ $($exoTestFiles.Count) Exchange Online test files" -ForegroundColor Green
 143 │ 142 │
 144 │ 143 │ # Group by test type for reporting
 145 │ 144 │ $fileGroups = $exoTestFiles | Group-Object {
 146 │ 145 │     if ($_.DirectoryName -match 'cisa') { 'CISA' }
 147 │ 146 │     elseif ($_.DirectoryName -match 'orca') { 'ORCA' }
 148 │ 147 │     elseif ($_.DirectoryName -match 'cis') { 'CIS' }
 149 │ 148 │     else { 'Other' }
 150 │ 149 │ }
 151 │ 150 │
 152 │ 151 │ Write-Host "`nTest Distribution:" -ForegroundColor Cyan
 153 │ 152 │ foreach ($group in $fileGroups | Sort-Object Name) {
 154 │ 153 │     Write-Host "  $($group.Name): $($group.Count) test files" -ForegroundColor White
 155 │ 154 │ }
 156 │ 155 │
 157 │ 156 │ # Helper function to get test documentation
 158 │ 157 │ function Get-TestDocumentation {
 159 │ 158 │     param(
 160 │ 159 │         [string]$TestName,
 161 │ 160 │         [string]$TestFile
 162 │ 161 │     )
 163 │ 162 │
 164 │ 163 │     $documentation = @{
 165 │ 164 │         Description = ""
 166 │ 165 │         HowToFix = ""
 167 │ 166 │         References = ""
 168 │ 167 │         Impact = ""
 169 │ 168 │         DefaultValue = ""
 170 │ 169 │         Rationale = ""
 171 │ 170 │     }
 172 │ 171 │
 173 │ 172 │     # Try to find corresponding .ps1 file in public folder
 174 │ 173 │     $publicPath = $TestFile -replace 'maester-tests', 'public' -replace '\.Tests\.ps1$', '.ps1'
 175 │ 174 │     if (Test-Path $publicPath) {
 176 │ 175 │         $cmdContent = Get-Content $publicPath -Raw
 177 │ 176 │
 178 │ 177 │         # Extract description from .SYNOPSIS
 179 │ 178 │         if ($cmdContent -match '\.SYNOPSIS\s*\n\s*(.+?)(?=\n\s*\.|#>)') {
 180 │ 179 │             $documentation.Description = $matches[1].Trim()
 181 │ 180 │         }
 182 │ 181 │
 183 │ 182 │         # Extract description from .DESCRIPTION
 184 │ 183 │         if ($cmdContent -match '\.DESCRIPTION\s*\n([\s\S]+?)(?=\n\s*\.|#>)') {
 185 │ 184 │             $documentation.Rationale = $matches[1].Trim() -replace '\s+', ' '
 186 │ 185 │         }
 187 │ 186 │     }
 188 │ 187 │
 189 │ 188 │     # Try to find corresponding .md file
 190 │ 189 │     $mdPath = $publicPath -replace '\.ps1$', '.md'
 191 │ 190 │     if (Test-Path $mdPath) {
 192 │ 191 │         $mdContent = Get-Content $mdPath -Raw
 193 │ 192 │
 194 │ 193 │         # Extract sections from markdown
 195 │ 194 │         if ($mdContent -match '## Description\s*\n([\s\S]+?)(?=\n##|\Z)') {
 196 │ 195 │             $documentation.Description = $matches[1].Trim()
 197 │ 196 │         }
 198 │ 197 │         if ($mdContent -match '## How to fix\s*\n([\s\S]+?)(?=\n##|\Z)') {
 199 │ 198 │             $documentation.HowToFix = $matches[1].Trim()
 200 │ 199 │         }
 201 │ 200 │         if ($mdContent -match '## References?\s*\n([\s\S]+?)(?=\n##|\Z)') {
 202 │ 201 │             $documentation.References = $matches[1].Trim()
 203 │ 202 │         }
 204 │ 203 │         if ($mdContent -match '## Impact\s*\n([\s\S]+?)(?=\n##|\Z)') {
 205 │ 204 │             $documentation.Impact = $matches[1].Trim()
 206 │ 205 │         }
 207 │ 206 │         if ($mdContent -match '## Default Value\s*\n([\s\S]+?)(?=\n##|\Z)') {
 208 │ 207 │             $documentation.DefaultValue = $matches[1].Trim()
 209 │ 208 │         }
 210 │ 209 │     }
 211 │ 210 │
 212 │ 211 │     return $documentation
 213 │ 212 │ }
 214 │ 213 │
 215 │ 214 │ # Helper function to get remediation URL
 216 │ 215 │ function Get-RemediationUrl {
 217 │ 216 │     param(
 218 │ 217 │         [string]$TestName,
 219 │ 218 │         [string]$TestType
 220 │ 219 │     )
 221 │ 220 │
 222 │ 221 │     $baseUrl = "https://security.microsoft.com"
 223 │ 222 │
 224 │ 223 │     switch -Regex ($TestName) {
 225 │ 224 │         'Spam|AntiSpam' { return "$baseUrl/antispam" }
 226 │ 225 │         'Phish|AntiPhish' { return "$baseUrl/antiphishing" }
 227 │ 226 │         'Safe.*Link' { return "$baseUrl/safelinks" }
 228 │ 227 │         'Safe.*Attach' { return "$baseUrl/safeattachments" }
 229 │ 228 │         'Malware' { return "$baseUrl/antimalware" }
 230 │ 229 │         'DKIM|SPF|DMARC' { return "$baseUrl/authentication" }
 231 │ 230 │         'Audit' { return "https://compliance.microsoft.com/auditlogsearch" }
 232 │ 231 │         'DLP' { return "https://compliance.microsoft.com/datalossprevention" }
 233 │ 232 │         'Calendar|Sharing' { return "https://admin.exchange.microsoft.com/#/organizationconfig" }
 234 │ 233 │         'External.*Forward' { return "$baseUrl/antispam" }
 235 │ 234 │         'Outbound.*Spam' { return "$baseUrl/antispam" }
 236 │ 235 │         default { return "https://admin.microsoft.com" }
 237 │ 236 │     }
 238 │ 237 │ }
 239 │ 238 │
 240 │ 239 │ # Initialize results collection
 241 │ 240 │ $detailedResults = @()
 242 │ 241 │ $testStartTime = Get-Date
 243 │ 242 │
 244 │ 243 │ Write-Host "`nRunning Exchange Online security tests..." -ForegroundColor Yellow
 245 │ 244 │ Write-Host "This may take several minutes..." -ForegroundColor DarkGray
 246 │ 245 │
 247 │ 246 │ # Process each test file
 248 │ 247 │ $fileCount = 0
 249 │ 248 │ $totalTestCount = 0
 250 │ 249 │
 251 │ 250 │ foreach ($testFile in $exoTestFiles) {
 252 │ 251 │     $fileCount++
 253 │ 252 │     $percentComplete = [math]::Round(($fileCount / $exoTestFiles.Count) * 100, 0)
 254 │ 253 │     Write-Progress -Activity "Running Exchange Online Security Tests" -Status "Processing $($testFile.Name)" -PercentComplete $percentComplete
 255 │ 254 │
 256 │ 255 │     try {
 257 │ 256 │         # Run the test file
 258 │ 257 │         $container = New-PesterContainer -Path $testFile.FullName
 259 │ 258 │
 260 │ 259 │         $pesterConfig = New-PesterConfiguration
 261 │ 260 │         $pesterConfig.Run.Container = $container
 262 │ 261 │         $pesterConfig.Run.PassThru = $true
 263 │ 262 │         $pesterConfig.Output.Verbosity = 'None'
 264 │ 263 │         $pesterConfig.TestResult.Enabled = $true
 265 │ 264 │
 266 │ 265 │         # Run tests
 267 │ 266 │         $testOutput = Invoke-Pester -Configuration $pesterConfig
 268 │ 267 │
 269 │ 268 │         foreach ($test in $testOutput.Tests) {
 270 │ 269 │             $totalTestCount++
 271 │ 270 │
 272 │ 271 │             # Determine test metadata
 273 │ 272 │             $testType = if ($testFile.DirectoryName -match 'cisa') { 'CISA' }
 274 │ 273 │                        elseif ($testFile.DirectoryName -match 'orca') { 'ORCA' }
 275 │ 274 │                        elseif ($testFile.DirectoryName -match 'cis') { 'CIS' }
 276 │ 275 │                        else { 'Other' }
 277 │ 276 │
 278 │ 277 │             # Get test documentation
 279 │ 278 │             $docs = Get-TestDocumentation -TestName $test.Name -TestFile $testFile.FullName
 280 │ 279 │
 281 │ 280 │             # Get remediation URL
 282 │ 281 │             $remediationUrl = Get-RemediationUrl -TestName $test.Name -TestType $testType
 283 │ 282 │
 284 │ 283 │             # Extract test ID from name
 285 │ 284 │             $testId = if ($test.Name -match '^(ORCA\.\d+|CISA\.MS\.EXO\.\d+\.\d+|CIS\.\d+\.\d+\.\d+)') {
 286 │ 285 │                 $matches[1]
 287 │ 286 │             } else {
 288 │ 287 │                 $test.Name -split ':' | Select-Object -First 1
 289 │ 288 │             }
 290 │ 289 │
 291 │ 290 │             # Build detailed output
 292 │ 291 │             $detailOutput = ""
 293 │ 292 │             if ($test.Result -eq 'Failed' -and $test.ErrorRecord) {
 294 │ 293 │                 $detailOutput = $test.ErrorRecord.Exception.Message
 295 │ 294 │             } elseif ($test.Result -eq 'Skipped' -and $test.SkippedBecause) {
 296 │ 295 │                 $detailOutput = "Skipped: $($test.SkippedBecause)"
 297 │ 296 │             }
 298 │ 297 │
 299 │ 298 │             # Create detailed record
 300 │ 299 │             $record = [PSCustomObject]@{
 301 │ 300 │                 TestFile = $testFile.Name
 302 │ 301 │                 TestType = $testType
 303 │ 302 │                 TestId = $testId
 304 │ 303 │                 TestName = $test.Name
 305 │ 304 │                 Result = $test.Result
 306 │ 305 │                 Executed = $test.Executed
 307 │ 306 │                 Duration = if ($test.Duration) { "{0:N2}" -f $test.Duration.TotalSeconds } else { "0.00" }
 308 │ 307 │
 309 │ 308 │                 # Test details
 310 │ 309 │                 Description = $docs.Description
 311 │ 310 │                 Rationale = $docs.Rationale
 312 │ 311 │                 Impact = $docs.Impact
 313 │ 312 │                 DefaultValue = $docs.DefaultValue
 314 │ 313 │
 315 │ 314 │                 # Result details
 316 │ 315 │                 ResultDetail = $detailOutput
 317 │ 316 │                 ErrorMessage = if ($test.ErrorRecord) { $test.ErrorRecord.Exception.Message } else { "" }
 318 │ 317 │
 319 │ 318 │                 # Remediation
 320 │ 319 │                 HowToFix = $docs.HowToFix
 321 │ 320 │                 RemediationUrl = $remediationUrl
 322 │ 321 │                 References = $docs.References
 323 │ 322 │
 324 │ 323 │                 # Additional context
 325 │ 324 │                 Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 326 │ 325 │             }
 327 │ 326 │
 328 │ 327 │             $detailedResults += $record
 329 │ 328 │         }
 330 │ 329 │     }
 331 │ 330 │     catch {
 332 │ 331 │         Write-Warning "Error processing $($testFile.Name): $_"
 333 │ 332 │     }
 334 │ 333 │ }
 335 │ 334 │
 336 │ 335 │ Write-Progress -Activity "Running Exchange Online Security Tests" -Completed
 337 │ 336 │
 338 │ 337 │ $testEndTime = Get-Date
 339 │ 338 │ $testDuration = $testEndTime - $testStartTime
 340 │ 339 │
 341 │ 340 │ # Generate timestamp for file names
 342 │ 341 │ $reportTimestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
 343 │ 342 │
 344 │ 343 │ # Create comprehensive CSV export
 345 │ 344 │ Write-Host "`nGenerating reports..." -ForegroundColor Yellow
 346 │ 345 │
 347 │ 346 │ $csvPath = Join-Path $reportsPath "ExchangeHealthCheck-Results-$reportTimestamp.csv"
 348 │ 347 │
 349 │ 348 │ # Filter results based on parameter
 350 │ 349 │ $exportResults = if ($IncludePassedDetails) {
 351 │ 350 │     $detailedResults
 352 │ 351 │ } else {
 353 │ 352 │     # For passed tests, include minimal details
 354 │ 353 │     $detailedResults | ForEach-Object {
 355 │ 354 │         if ($_.Result -in @('Failed', 'Skipped')) {
 356 │ 355 │             $_
 357 │ 356 │         } else {
 358 │ 357 │             # Create simplified record for passed tests
 359 │ 358 │             [PSCustomObject]@{
 360 │ 359 │                 TestFile = $_.TestFile
 361 │ 360 │                 TestType = $_.TestType
 362 │ 361 │                 TestId = $_.TestId
 363 │ 362 │                 TestName = $_.TestName
 364 │ 363 │                 Result = $_.Result
 365 │ 364 │                 Executed = $_.Executed
 366 │ 365 │                 Duration = $_.Duration
 367 │ 366 │                 Description = $_.Description
 368 │ 367 │                 Rationale = ""
 369 │ 368 │                 Impact = ""
 370 │ 369 │                 DefaultValue = ""
 371 │ 370 │                 ResultDetail = "Test passed successfully"
 372 │ 371 │                 ErrorMessage = ""
 373 │ 372 │                 HowToFix = ""
 374 │ 373 │                 RemediationUrl = ""
 375 │ 374 │                 References = ""
 376 │ 375 │                 Timestamp = $_.Timestamp
 377 │ 376 │             }
 378 │ 377 │         }
 379 │ 378 │     }
 380 │ 379 │ }
 381 │ 380 │
 382 │ 381 │ $exportResults | Export-Csv -Path $csvPath -NoTypeInformation
 383 │ 382 │ Write-Host "Detailed results exported to:

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

 383 │ 382 │ $(Split-Path $csvPath -Leaf)" -ForegroundColor Green
 384 │ 383 │
 385 │ 384 │ # Create executive summary
 386 │ 385 │ $summaryPath = Join-Path $reportsPath "ExchangeHealthCheck-Summary-$reportTimestamp.csv"
 387 │ 386 │
 388 │ 387 │ $summary = $detailedResults | Group-Object TestType, Result | ForEach-Object {
 389 │ 388 │     $parts = $_.Name -split ', '
 390 │ 389 │     [PSCustomObject]@{
 391 │ 390 │         TestType = $parts[0]
 392 │ 391 │         Result = $parts[1]
 393 │ 392 │         Count = $_.Count
 394 │ 393 │         TestIds = ($_.Group.TestId | Sort-Object -Unique) -join '; '
 395 │ 394 │     }
 396 │ 395 │ } | Sort-Object TestType, Result
 397 │ 396 │
 398 │ 397 │ $summary | Export-Csv -Path $summaryPath -NoTypeInformation
 399 │ 398 │ Write-Host "Executive summary exported to:

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

 404 │ 403 │ $failedTests = $detailedResults | Where-Object { $_.Result -eq 'Failed' } | Sort-Object TestType, TestId
 405 │ 404 │
 406 │ 405 │ # Calculate statistics
 407 │ 406 │ $stats = @{
 408 │ 407 │     Total = $detailedResults.Count
 409 │ 408 │     Passed = ($detailedResults | Where-Object {$_.Result -eq 'Passed'}).Count
 410 │ 409 │     Failed = ($detailedResults | Where-Object {$_.Result -eq 'Failed'}).Count
 411 │ 410 │     Skipped = ($detailedResults | Where-Object {$_.Result -eq 'Skipped'}).Count
 412 │ 411 │ }
 413 │ 412 │
 414 │ 413 │ # Create text report content

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

 481 │ 547 │  -Leaf)" -ForegroundColor Green
 482 │ 548 │
 483 │ 549 │ # Display summary
 484 │ 550 │ Write-Host "`n" + ("="*60) -ForegroundColor Cyan
 485 │ 551 │ Write-Host "SECURITY HEALTH CHECK COMPLETED" -ForegroundColor Cyan
 486 │ 552 │ Write-Host ("="*60) -ForegroundColor Cyan
 487 │ 553 │
 488 │ 554 │ Write-Host "`nTest Results Summary:" -ForegroundColor Yellow
 489 │ 555 │ Write-Host "  Total Tests Run: $($stats.Total)" -ForegroundColor White
 490 │ 556 │ Write-Host "  Passed: $($stats.Passed)" -ForegroundColor Green
 491 │ 557 │ Write-Host "  Failed: $($stats.Failed)" -ForegroundColor Red
 492 │ 558 │ Write-Host "  Skipped: $($stats.Skipped)" -ForegroundColor Yellow
 493 │ 559 │
 494 │ 560 │ Write-Host

Found 7 clones.
Error: ERROR: jscpd found too many duplicates (31.69%) over threshold (0%)
    at ThresholdReporter.report (/node_modules/@jscpd/finder/dist/index.js:615:13)
    at /node_modules/@jscpd/finder/dist/index.js:109:18
    at Array.forEach (<anonymous>)
    at /node_modules/@jscpd/finder/dist/index.js:108:22
    at async /node_modules/jscpd/dist/bin/jscpd.js:9:5ERROR: jscpd found too many duplicates (31.69%) over threshold (0%)

@LukeEvansTech
LukeEvansTech force-pushed the chore/add-super-linter branch from 5b27f39 to 9acaca3 Compare May 3, 2026 22:29
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
BIOME_FORMAT Pass ✅
BIOME_LINT Pass ✅
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSCPD Fail ❌
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

Super-linter detected linting errors

For more information, see the GitHub Actions workflow run

Powered by Super-linter

JSCPD
Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

 39 │ 38 │ [CmdletBinding()]
 40 │ 39 │ param(
 41 │ 40 │     [switch]$SkipConnection,
 42 │ 41 │     [switch]$IncludePassedDetails
 43 │ 42 │ )
 44 │ 43 │
 45 │ 44 │ # Script configuration
 46 │ 45 │ $ErrorActionPreference = 'Stop'
 47 │ 46 │ $ProgressPreference = 'Continue'
 48 │ 47 │
 49 │ 48 │ # Set up paths
 50 │ 49 │ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
 51 │ 50 │ $reportsPath = Join-Path $scriptPath "Reports"
 52 │ 51 │
 53 │ 52 │ # Ensure Reports directory exists
 54 │ 53 │ if (!(Test-Path $reportsPath)) {
 55 │ 54 │     New-Item -ItemType Directory -Path $reportsPath -Force | Out-Null
 56 │ 55 │     Write-Host "Created Reports directory: $reportsPath" -ForegroundColor Green
 57 │ 56 │ }
 58 │ 57 │
 59 │ 58 │ # Import required modules
 60 │ 59 │ Write-Host "`nExchange Online Security Health Check (Simplified)"

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

 81  │ 80  │ $_"
 82  │ 81  │         exit 1
 83  │ 82  │     }
 84  │ 83  │ }
 85  │ 84  │
 86  │ 85  │ # Verify connection
 87  │ 86  │ $connectionInfo = Get-ConnectionInformation | Where-Object { $_.Name -like '*ExchangeOnline*' }
 88  │ 87  │ if (-not $connectionInfo) {
 89  │ 88  │     Write-Error "No active Exchange Online connection found. Please connect first or remove -SkipConnection parameter."
 90  │ 89  │     exit 1
 91  │ 90  │ }
 92  │ 91  │
 93  │ 92  │ Write-Host "`nConnection Details:" -ForegroundColor Cyan
 94  │ 93  │ Write-Host "  Tenant: $($connectionInfo.TenantId)" -ForegroundColor White
 95  │ 94  │ Write-Host "  User: $($connectionInfo.UserPrincipalName)" -ForegroundColor White
 96  │ 95  │ Write-Host "  Environment: $($connectionInfo.ConnectionUri)" -ForegroundColor White
 97  │ 96  │
 98  │ 97  │ # Get Maester module path
 99  │ 98  │ $maesterModule = Get-Module -ListAvailable Maester | Select-Object -First 1
 100 │ 99  │ if (!$maesterModule) {
 101 │ 100 │     Write-Error "Maester module not found. Please install it first: Install-Module Maester"
 102 │ 101 │     exit 1
 103 │ 102 │ }
 104 │ 103 │
 105 │ 104 │ $maesterPath = Split-Path $maesterModule.Path
 106 │ 105 │ $testsPath = Join-Path $maesterPath "maester-tests"
 107 │ 106 │
 108 │ 107 │ Write-Host "`nSearching for Exchange Online tests..." -ForegroundColor Yellow
 109 │ 108 │
 110 │ 109 │ # Find all test files with Exchange-related tags
 111 │ 110 │ $exoTestFiles = @()
 112 │ 111 │ $testPatterns = @(
 113 │ 112 │     '-Tag.*["'']EXO["'']'

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

 114 │ 113 │ # CISA tests use "MS.EXO"
 115 │ 114 │ )
 116 │ 115 │
 117 │ 116 │ # Search in relevant directories
 118 │ 117 │ $searchDirs = Get-ChildItem -Path $testsPath -Directory -Recurse -ErrorAction SilentlyContinue |
 119 │ 118 │     Where-Object {
 120 │ 119 │         $_.Name -in @('exchange', 'orca', 'cis', 'cisa') -or
 121 │ 120 │         $_.FullName -match 'exchange|exo'
 122 │ 121 │     }
 123 │ 122 │
 124 │ 123 │ foreach ($dir in $searchDirs) {
 125 │ 124 │     $files = Get-ChildItem -Path $dir.FullName -Filter "*.Tests.ps1" -File -ErrorAction SilentlyContinue
 126 │ 125 │     foreach ($file in $files) {
 127 │ 126 │         $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
 128 │ 127 │         if ($content) {
 129 │ 128 │             foreach ($pattern in $testPatterns) {
 130 │ 129 │                 if ($content -match $pattern) {
 131 │ 130 │                     if ($file.FullName -notin $exoTestFiles.FullName) {
 132 │ 131 │                         $exoTestFiles += $file
 133 │ 132 │                     }
 134 │ 133 │                     break
 135 │ 134 │                 }
 136 │ 135 │             }
 137 │ 136 │         }
 138 │ 137 │     }
 139 │ 138 │ }
 140 │ 139 │
 141 │ 140 │ $exoTestFiles = $exoTestFiles | Sort-Object DirectoryName, Name
 142 │ 141 │ Write-Host "Found

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

 142 │ 141 │ $($exoTestFiles.Count) Exchange Online test files" -ForegroundColor Green
 143 │ 142 │
 144 │ 143 │ # Group by test type for reporting
 145 │ 144 │ $fileGroups = $exoTestFiles | Group-Object {
 146 │ 145 │     if ($_.DirectoryName -match 'cisa') { 'CISA' }
 147 │ 146 │     elseif ($_.DirectoryName -match 'orca') { 'ORCA' }
 148 │ 147 │     elseif ($_.DirectoryName -match 'cis') { 'CIS' }
 149 │ 148 │     else { 'Other' }
 150 │ 149 │ }
 151 │ 150 │
 152 │ 151 │ Write-Host "`nTest Distribution:" -ForegroundColor Cyan
 153 │ 152 │ foreach ($group in $fileGroups | Sort-Object Name) {
 154 │ 153 │     Write-Host "  $($group.Name): $($group.Count) test files" -ForegroundColor White
 155 │ 154 │ }
 156 │ 155 │
 157 │ 156 │ # Helper function to get test documentation
 158 │ 157 │ function Get-TestDocumentation {
 159 │ 158 │     param(
 160 │ 159 │         [string]$TestName,
 161 │ 160 │         [string]$TestFile
 162 │ 161 │     )
 163 │ 162 │
 164 │ 163 │     $documentation = @{
 165 │ 164 │         Description = ""
 166 │ 165 │         HowToFix = ""
 167 │ 166 │         References = ""
 168 │ 167 │         Impact = ""
 169 │ 168 │         DefaultValue = ""
 170 │ 169 │         Rationale = ""
 171 │ 170 │     }
 172 │ 171 │
 173 │ 172 │     # Try to find corresponding .ps1 file in public folder
 174 │ 173 │     $publicPath = $TestFile -replace 'maester-tests', 'public' -replace '\.Tests\.ps1$', '.ps1'
 175 │ 174 │     if (Test-Path $publicPath) {
 176 │ 175 │         $cmdContent = Get-Content $publicPath -Raw
 177 │ 176 │
 178 │ 177 │         # Extract description from .SYNOPSIS
 179 │ 178 │         if ($cmdContent -match '\.SYNOPSIS\s*\n\s*(.+?)(?=\n\s*\.|#>)') {
 180 │ 179 │             $documentation.Description = $matches[1].Trim()
 181 │ 180 │         }
 182 │ 181 │
 183 │ 182 │         # Extract description from .DESCRIPTION
 184 │ 183 │         if ($cmdContent -match '\.DESCRIPTION\s*\n([\s\S]+?)(?=\n\s*\.|#>)') {
 185 │ 184 │             $documentation.Rationale = $matches[1].Trim() -replace '\s+', ' '
 186 │ 185 │         }
 187 │ 186 │     }
 188 │ 187 │
 189 │ 188 │     # Try to find corresponding .md file
 190 │ 189 │     $mdPath = $publicPath -replace '\.ps1$', '.md'
 191 │ 190 │     if (Test-Path $mdPath) {
 192 │ 191 │         $mdContent = Get-Content $mdPath -Raw
 193 │ 192 │
 194 │ 193 │         # Extract sections from markdown
 195 │ 194 │         if ($mdContent -match '## Description\s*\n([\s\S]+?)(?=\n##|\Z)') {
 196 │ 195 │             $documentation.Description = $matches[1].Trim()
 197 │ 196 │         }
 198 │ 197 │         if ($mdContent -match '## How to fix\s*\n([\s\S]+?)(?=\n##|\Z)') {
 199 │ 198 │             $documentation.HowToFix = $matches[1].Trim()
 200 │ 199 │         }
 201 │ 200 │         if ($mdContent -match '## References?\s*\n([\s\S]+?)(?=\n##|\Z)') {
 202 │ 201 │             $documentation.References = $matches[1].Trim()
 203 │ 202 │         }
 204 │ 203 │         if ($mdContent -match '## Impact\s*\n([\s\S]+?)(?=\n##|\Z)') {
 205 │ 204 │             $documentation.Impact = $matches[1].Trim()
 206 │ 205 │         }
 207 │ 206 │         if ($mdContent -match '## Default Value\s*\n([\s\S]+?)(?=\n##|\Z)') {
 208 │ 207 │             $documentation.DefaultValue = $matches[1].Trim()
 209 │ 208 │         }
 210 │ 209 │     }
 211 │ 210 │
 212 │ 211 │     return $documentation
 213 │ 212 │ }
 214 │ 213 │
 215 │ 214 │ # Helper function to get remediation URL
 216 │ 215 │ function Get-RemediationUrl {
 217 │ 216 │     param(
 218 │ 217 │         [string]$TestName,
 219 │ 218 │         [string]$TestType
 220 │ 219 │     )
 221 │ 220 │
 222 │ 221 │     $baseUrl = "https://security.microsoft.com"
 223 │ 222 │
 224 │ 223 │     switch -Regex ($TestName) {
 225 │ 224 │         'Spam|AntiSpam' { return "$baseUrl/antispam" }
 226 │ 225 │         'Phish|AntiPhish' { return "$baseUrl/antiphishing" }
 227 │ 226 │         'Safe.*Link' { return "$baseUrl/safelinks" }
 228 │ 227 │         'Safe.*Attach' { return "$baseUrl/safeattachments" }
 229 │ 228 │         'Malware' { return "$baseUrl/antimalware" }
 230 │ 229 │         'DKIM|SPF|DMARC' { return "$baseUrl/authentication" }
 231 │ 230 │         'Audit' { return "https://compliance.microsoft.com/auditlogsearch" }
 232 │ 231 │         'DLP' { return "https://compliance.microsoft.com/datalossprevention" }
 233 │ 232 │         'Calendar|Sharing' { return "https://admin.exchange.microsoft.com/#/organizationconfig" }
 234 │ 233 │         'External.*Forward' { return "$baseUrl/antispam" }
 235 │ 234 │         'Outbound.*Spam' { return "$baseUrl/antispam" }
 236 │ 235 │         default { return "https://admin.microsoft.com" }
 237 │ 236 │     }
 238 │ 237 │ }
 239 │ 238 │
 240 │ 239 │ # Initialize results collection
 241 │ 240 │ $detailedResults = @()
 242 │ 241 │ $testStartTime = Get-Date
 243 │ 242 │
 244 │ 243 │ Write-Host "`nRunning Exchange Online security tests..." -ForegroundColor Yellow
 245 │ 244 │ Write-Host "This may take several minutes..." -ForegroundColor DarkGray
 246 │ 245 │
 247 │ 246 │ # Process each test file
 248 │ 247 │ $fileCount = 0
 249 │ 248 │ $totalTestCount = 0
 250 │ 249 │
 251 │ 250 │ foreach ($testFile in $exoTestFiles) {
 252 │ 251 │     $fileCount++
 253 │ 252 │     $percentComplete = [math]::Round(($fileCount / $exoTestFiles.Count) * 100, 0)
 254 │ 253 │     Write-Progress -Activity "Running Exchange Online Security Tests" -Status "Processing $($testFile.Name)" -PercentComplete $percentComplete
 255 │ 254 │
 256 │ 255 │     try {
 257 │ 256 │         # Run the test file
 258 │ 257 │         $container = New-PesterContainer -Path $testFile.FullName
 259 │ 258 │
 260 │ 259 │         $pesterConfig = New-PesterConfiguration
 261 │ 260 │         $pesterConfig.Run.Container = $container
 262 │ 261 │         $pesterConfig.Run.PassThru = $true
 263 │ 262 │         $pesterConfig.Output.Verbosity = 'None'
 264 │ 263 │         $pesterConfig.TestResult.Enabled = $true
 265 │ 264 │
 266 │ 265 │         # Run tests
 267 │ 266 │         $testOutput = Invoke-Pester -Configuration $pesterConfig
 268 │ 267 │
 269 │ 268 │         foreach ($test in $testOutput.Tests) {
 270 │ 269 │             $totalTestCount++
 271 │ 270 │
 272 │ 271 │             # Determine test metadata
 273 │ 272 │             $testType = if ($testFile.DirectoryName -match 'cisa') { 'CISA' }
 274 │ 273 │                        elseif ($testFile.DirectoryName -match 'orca') { 'ORCA' }
 275 │ 274 │                        elseif ($testFile.DirectoryName -match 'cis') { 'CIS' }
 276 │ 275 │                        else { 'Other' }
 277 │ 276 │
 278 │ 277 │             # Get test documentation
 279 │ 278 │             $docs = Get-TestDocumentation -TestName $test.Name -TestFile $testFile.FullName
 280 │ 279 │
 281 │ 280 │             # Get remediation URL
 282 │ 281 │             $remediationUrl = Get-RemediationUrl -TestName $test.Name -TestType $testType
 283 │ 282 │
 284 │ 283 │             # Extract test ID from name
 285 │ 284 │             $testId = if ($test.Name -match '^(ORCA\.\d+|CISA\.MS\.EXO\.\d+\.\d+|CIS\.\d+\.\d+\.\d+)') {
 286 │ 285 │                 $matches[1]
 287 │ 286 │             } else {
 288 │ 287 │                 $test.Name -split ':' | Select-Object -First 1
 289 │ 288 │             }
 290 │ 289 │
 291 │ 290 │             # Build detailed output
 292 │ 291 │             $detailOutput = ""
 293 │ 292 │             if ($test.Result -eq 'Failed' -and $test.ErrorRecord) {
 294 │ 293 │                 $detailOutput = $test.ErrorRecord.Exception.Message
 295 │ 294 │             } elseif ($test.Result -eq 'Skipped' -and $test.SkippedBecause) {
 296 │ 295 │                 $detailOutput = "Skipped: $($test.SkippedBecause)"
 297 │ 296 │             }
 298 │ 297 │
 299 │ 298 │             # Create detailed record
 300 │ 299 │             $record = [PSCustomObject]@{
 301 │ 300 │                 TestFile = $testFile.Name
 302 │ 301 │                 TestType = $testType
 303 │ 302 │                 TestId = $testId
 304 │ 303 │                 TestName = $test.Name
 305 │ 304 │                 Result = $test.Result
 306 │ 305 │                 Executed = $test.Executed
 307 │ 306 │                 Duration = if ($test.Duration) { "{0:N2}" -f $test.Duration.TotalSeconds } else { "0.00" }
 308 │ 307 │
 309 │ 308 │                 # Test details
 310 │ 309 │                 Description = $docs.Description
 311 │ 310 │                 Rationale = $docs.Rationale
 312 │ 311 │                 Impact = $docs.Impact
 313 │ 312 │                 DefaultValue = $docs.DefaultValue
 314 │ 313 │
 315 │ 314 │                 # Result details
 316 │ 315 │                 ResultDetail = $detailOutput
 317 │ 316 │                 ErrorMessage = if ($test.ErrorRecord) { $test.ErrorRecord.Exception.Message } else { "" }
 318 │ 317 │
 319 │ 318 │                 # Remediation
 320 │ 319 │                 HowToFix = $docs.HowToFix
 321 │ 320 │                 RemediationUrl = $remediationUrl
 322 │ 321 │                 References = $docs.References
 323 │ 322 │
 324 │ 323 │                 # Additional context
 325 │ 324 │                 Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 326 │ 325 │             }
 327 │ 326 │
 328 │ 327 │             $detailedResults += $record
 329 │ 328 │         }
 330 │ 329 │     }
 331 │ 330 │     catch {
 332 │ 331 │         Write-Warning "Error processing $($testFile.Name): $_"
 333 │ 332 │     }
 334 │ 333 │ }
 335 │ 334 │
 336 │ 335 │ Write-Progress -Activity "Running Exchange Online Security Tests" -Completed
 337 │ 336 │
 338 │ 337 │ $testEndTime = Get-Date
 339 │ 338 │ $testDuration = $testEndTime - $testStartTime
 340 │ 339 │
 341 │ 340 │ # Generate timestamp for file names
 342 │ 341 │ $reportTimestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
 343 │ 342 │
 344 │ 343 │ # Create comprehensive CSV export
 345 │ 344 │ Write-Host "`nGenerating reports..." -ForegroundColor Yellow
 346 │ 345 │
 347 │ 346 │ $csvPath = Join-Path $reportsPath "ExchangeHealthCheck-Results-$reportTimestamp.csv"
 348 │ 347 │
 349 │ 348 │ # Filter results based on parameter
 350 │ 349 │ $exportResults = if ($IncludePassedDetails) {
 351 │ 350 │     $detailedResults
 352 │ 351 │ } else {
 353 │ 352 │     # For passed tests, include minimal details
 354 │ 353 │     $detailedResults | ForEach-Object {
 355 │ 354 │         if ($_.Result -in @('Failed', 'Skipped')) {
 356 │ 355 │             $_
 357 │ 356 │         } else {
 358 │ 357 │             # Create simplified record for passed tests
 359 │ 358 │             [PSCustomObject]@{
 360 │ 359 │                 TestFile = $_.TestFile
 361 │ 360 │                 TestType = $_.TestType
 362 │ 361 │                 TestId = $_.TestId
 363 │ 362 │                 TestName = $_.TestName
 364 │ 363 │                 Result = $_.Result
 365 │ 364 │                 Executed = $_.Executed
 366 │ 365 │                 Duration = $_.Duration
 367 │ 366 │                 Description = $_.Description
 368 │ 367 │                 Rationale = ""
 369 │ 368 │                 Impact = ""
 370 │ 369 │                 DefaultValue = ""
 371 │ 370 │                 ResultDetail = "Test passed successfully"
 372 │ 371 │                 ErrorMessage = ""
 373 │ 372 │                 HowToFix = ""
 374 │ 373 │                 RemediationUrl = ""
 375 │ 374 │                 References = ""
 376 │ 375 │                 Timestamp = $_.Timestamp
 377 │ 376 │             }
 378 │ 377 │         }
 379 │ 378 │     }
 380 │ 379 │ }
 381 │ 380 │
 382 │ 381 │ $exportResults | Export-Csv -Path $csvPath -NoTypeInformation
 383 │ 382 │ Write-Host "Detailed results exported to:

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

 383 │ 382 │ $(Split-Path $csvPath -Leaf)" -ForegroundColor Green
 384 │ 383 │
 385 │ 384 │ # Create executive summary
 386 │ 385 │ $summaryPath = Join-Path $reportsPath "ExchangeHealthCheck-Summary-$reportTimestamp.csv"
 387 │ 386 │
 388 │ 387 │ $summary = $detailedResults | Group-Object TestType, Result | ForEach-Object {
 389 │ 388 │     $parts = $_.Name -split ', '
 390 │ 389 │     [PSCustomObject]@{
 391 │ 390 │         TestType = $parts[0]
 392 │ 391 │         Result = $parts[1]
 393 │ 392 │         Count = $_.Count
 394 │ 393 │         TestIds = ($_.Group.TestId | Sort-Object -Unique) -join '; '
 395 │ 394 │     }
 396 │ 395 │ } | Sort-Object TestType, Result
 397 │ 396 │
 398 │ 397 │ $summary | Export-Csv -Path $summaryPath -NoTypeInformation
 399 │ 398 │ Write-Host "Executive summary exported to:

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

 404 │ 403 │ $failedTests = $detailedResults | Where-Object { $_.Result -eq 'Failed' } | Sort-Object TestType, TestId
 405 │ 404 │
 406 │ 405 │ # Calculate statistics
 407 │ 406 │ $stats = @{
 408 │ 407 │     Total = $detailedResults.Count
 409 │ 408 │     Passed = ($detailedResults | Where-Object {$_.Result -eq 'Passed'}).Count
 410 │ 409 │     Failed = ($detailedResults | Where-Object {$_.Result -eq 'Failed'}).Count
 411 │ 410 │     Skipped = ($detailedResults | Where-Object {$_.Result -eq 'Skipped'}).Count
 412 │ 411 │ }
 413 │ 412 │
 414 │ 413 │ # Create text report content

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

 481 │ 547 │  -Leaf)" -ForegroundColor Green
 482 │ 548 │
 483 │ 549 │ # Display summary
 484 │ 550 │ Write-Host "`n" + ("="*60) -ForegroundColor Cyan
 485 │ 551 │ Write-Host "SECURITY HEALTH CHECK COMPLETED" -ForegroundColor Cyan
 486 │ 552 │ Write-Host ("="*60) -ForegroundColor Cyan
 487 │ 553 │
 488 │ 554 │ Write-Host "`nTest Results Summary:" -ForegroundColor Yellow
 489 │ 555 │ Write-Host "  Total Tests Run: $($stats.Total)" -ForegroundColor White
 490 │ 556 │ Write-Host "  Passed: $($stats.Passed)" -ForegroundColor Green
 491 │ 557 │ Write-Host "  Failed: $($stats.Failed)" -ForegroundColor Red
 492 │ 558 │ Write-Host "  Skipped: $($stats.Skipped)" -ForegroundColor Yellow
 493 │ 559 │
 494 │ 560 │ Write-Host

Found 7 clones.
Error: ERROR: jscpd found too many duplicates (31.69%) over threshold (0%)
    at ThresholdReporter.report (/node_modules/@jscpd/finder/dist/index.js:615:13)
    at /node_modules/@jscpd/finder/dist/index.js:109:18
    at Array.forEach (<anonymous>)
    at /node_modules/@jscpd/finder/dist/index.js:108:22
    at async /node_modules/jscpd/dist/bin/jscpd.js:9:5ERROR: jscpd found too many duplicates (31.69%) over threshold (0%)

@LukeEvansTech
LukeEvansTech force-pushed the chore/add-super-linter branch from 9acaca3 to 1a5b4ea Compare May 4, 2026 17:57
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSCPD Fail ❌
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

Super-linter detected linting errors

For more information, see the GitHub Actions workflow run

Powered by Super-linter

JSCPD
Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

 39 │ 38 │ [CmdletBinding()]
 40 │ 39 │ param(
 41 │ 40 │     [switch]$SkipConnection,
 42 │ 41 │     [switch]$IncludePassedDetails
 43 │ 42 │ )
 44 │ 43 │
 45 │ 44 │ # Script configuration
 46 │ 45 │ $ErrorActionPreference = 'Stop'
 47 │ 46 │ $ProgressPreference = 'Continue'
 48 │ 47 │
 49 │ 48 │ # Set up paths
 50 │ 49 │ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
 51 │ 50 │ $reportsPath = Join-Path $scriptPath "Reports"
 52 │ 51 │
 53 │ 52 │ # Ensure Reports directory exists
 54 │ 53 │ if (!(Test-Path $reportsPath)) {
 55 │ 54 │     New-Item -ItemType Directory -Path $reportsPath -Force | Out-Null
 56 │ 55 │     Write-Host "Created Reports directory: $reportsPath" -ForegroundColor Green
 57 │ 56 │ }
 58 │ 57 │
 59 │ 58 │ # Import required modules
 60 │ 59 │ Write-Host "`nExchange Online Security Health Check (Simplified)"

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

 81  │ 80  │ $_"
 82  │ 81  │         exit 1
 83  │ 82  │     }
 84  │ 83  │ }
 85  │ 84  │
 86  │ 85  │ # Verify connection
 87  │ 86  │ $connectionInfo = Get-ConnectionInformation | Where-Object { $_.Name -like '*ExchangeOnline*' }
 88  │ 87  │ if (-not $connectionInfo) {
 89  │ 88  │     Write-Error "No active Exchange Online connection found. Please connect first or remove -SkipConnection parameter."
 90  │ 89  │     exit 1
 91  │ 90  │ }
 92  │ 91  │
 93  │ 92  │ Write-Host "`nConnection Details:" -ForegroundColor Cyan
 94  │ 93  │ Write-Host "  Tenant: $($connectionInfo.TenantId)" -ForegroundColor White
 95  │ 94  │ Write-Host "  User: $($connectionInfo.UserPrincipalName)" -ForegroundColor White
 96  │ 95  │ Write-Host "  Environment: $($connectionInfo.ConnectionUri)" -ForegroundColor White
 97  │ 96  │
 98  │ 97  │ # Get Maester module path
 99  │ 98  │ $maesterModule = Get-Module -ListAvailable Maester | Select-Object -First 1
 100 │ 99  │ if (!$maesterModule) {
 101 │ 100 │     Write-Error "Maester module not found. Please install it first: Install-Module Maester"
 102 │ 101 │     exit 1
 103 │ 102 │ }
 104 │ 103 │
 105 │ 104 │ $maesterPath = Split-Path $maesterModule.Path
 106 │ 105 │ $testsPath = Join-Path $maesterPath "maester-tests"
 107 │ 106 │
 108 │ 107 │ Write-Host "`nSearching for Exchange Online tests..." -ForegroundColor Yellow
 109 │ 108 │
 110 │ 109 │ # Find all test files with Exchange-related tags
 111 │ 110 │ $exoTestFiles = @()
 112 │ 111 │ $testPatterns = @(
 113 │ 112 │     '-Tag.*["'']EXO["'']'

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

 114 │ 113 │ # CISA tests use "MS.EXO"
 115 │ 114 │ )
 116 │ 115 │
 117 │ 116 │ # Search in relevant directories
 118 │ 117 │ $searchDirs = Get-ChildItem -Path $testsPath -Directory -Recurse -ErrorAction SilentlyContinue |
 119 │ 118 │     Where-Object {
 120 │ 119 │         $_.Name -in @('exchange', 'orca', 'cis', 'cisa') -or
 121 │ 120 │         $_.FullName -match 'exchange|exo'
 122 │ 121 │     }
 123 │ 122 │
 124 │ 123 │ foreach ($dir in $searchDirs) {
 125 │ 124 │     $files = Get-ChildItem -Path $dir.FullName -Filter "*.Tests.ps1" -File -ErrorAction SilentlyContinue
 126 │ 125 │     foreach ($file in $files) {
 127 │ 126 │         $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
 128 │ 127 │         if ($content) {
 129 │ 128 │             foreach ($pattern in $testPatterns) {
 130 │ 129 │                 if ($content -match $pattern) {
 131 │ 130 │                     if ($file.FullName -notin $exoTestFiles.FullName) {
 132 │ 131 │                         $exoTestFiles += $file
 133 │ 132 │                     }
 134 │ 133 │                     break
 135 │ 134 │                 }
 136 │ 135 │             }
 137 │ 136 │         }
 138 │ 137 │     }
 139 │ 138 │ }
 140 │ 139 │
 141 │ 140 │ $exoTestFiles = $exoTestFiles | Sort-Object DirectoryName, Name
 142 │ 141 │ Write-Host "Found

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

 142 │ 141 │ $($exoTestFiles.Count) Exchange Online test files" -ForegroundColor Green
 143 │ 142 │
 144 │ 143 │ # Group by test type for reporting
 145 │ 144 │ $fileGroups = $exoTestFiles | Group-Object {
 146 │ 145 │     if ($_.DirectoryName -match 'cisa') { 'CISA' }
 147 │ 146 │     elseif ($_.DirectoryName -match 'orca') { 'ORCA' }
 148 │ 147 │     elseif ($_.DirectoryName -match 'cis') { 'CIS' }
 149 │ 148 │     else { 'Other' }
 150 │ 149 │ }
 151 │ 150 │
 152 │ 151 │ Write-Host "`nTest Distribution:" -ForegroundColor Cyan
 153 │ 152 │ foreach ($group in $fileGroups | Sort-Object Name) {
 154 │ 153 │     Write-Host "  $($group.Name): $($group.Count) test files" -ForegroundColor White
 155 │ 154 │ }
 156 │ 155 │
 157 │ 156 │ # Helper function to get test documentation
 158 │ 157 │ function Get-TestDocumentation {
 159 │ 158 │     param(
 160 │ 159 │         [string]$TestName,
 161 │ 160 │         [string]$TestFile
 162 │ 161 │     )
 163 │ 162 │
 164 │ 163 │     $documentation = @{
 165 │ 164 │         Description = ""
 166 │ 165 │         HowToFix = ""
 167 │ 166 │         References = ""
 168 │ 167 │         Impact = ""
 169 │ 168 │         DefaultValue = ""
 170 │ 169 │         Rationale = ""
 171 │ 170 │     }
 172 │ 171 │
 173 │ 172 │     # Try to find corresponding .ps1 file in public folder
 174 │ 173 │     $publicPath = $TestFile -replace 'maester-tests', 'public' -replace '\.Tests\.ps1$', '.ps1'
 175 │ 174 │     if (Test-Path $publicPath) {
 176 │ 175 │         $cmdContent = Get-Content $publicPath -Raw
 177 │ 176 │
 178 │ 177 │         # Extract description from .SYNOPSIS
 179 │ 178 │         if ($cmdContent -match '\.SYNOPSIS\s*\n\s*(.+?)(?=\n\s*\.|#>)') {
 180 │ 179 │             $documentation.Description = $matches[1].Trim()
 181 │ 180 │         }
 182 │ 181 │
 183 │ 182 │         # Extract description from .DESCRIPTION
 184 │ 183 │         if ($cmdContent -match '\.DESCRIPTION\s*\n([\s\S]+?)(?=\n\s*\.|#>)') {
 185 │ 184 │             $documentation.Rationale = $matches[1].Trim() -replace '\s+', ' '
 186 │ 185 │         }
 187 │ 186 │     }
 188 │ 187 │
 189 │ 188 │     # Try to find corresponding .md file
 190 │ 189 │     $mdPath = $publicPath -replace '\.ps1$', '.md'
 191 │ 190 │     if (Test-Path $mdPath) {
 192 │ 191 │         $mdContent = Get-Content $mdPath -Raw
 193 │ 192 │
 194 │ 193 │         # Extract sections from markdown
 195 │ 194 │         if ($mdContent -match '## Description\s*\n([\s\S]+?)(?=\n##|\Z)') {
 196 │ 195 │             $documentation.Description = $matches[1].Trim()
 197 │ 196 │         }
 198 │ 197 │         if ($mdContent -match '## How to fix\s*\n([\s\S]+?)(?=\n##|\Z)') {
 199 │ 198 │             $documentation.HowToFix = $matches[1].Trim()
 200 │ 199 │         }
 201 │ 200 │         if ($mdContent -match '## References?\s*\n([\s\S]+?)(?=\n##|\Z)') {
 202 │ 201 │             $documentation.References = $matches[1].Trim()
 203 │ 202 │         }
 204 │ 203 │         if ($mdContent -match '## Impact\s*\n([\s\S]+?)(?=\n##|\Z)') {
 205 │ 204 │             $documentation.Impact = $matches[1].Trim()
 206 │ 205 │         }
 207 │ 206 │         if ($mdContent -match '## Default Value\s*\n([\s\S]+?)(?=\n##|\Z)') {
 208 │ 207 │             $documentation.DefaultValue = $matches[1].Trim()
 209 │ 208 │         }
 210 │ 209 │     }
 211 │ 210 │
 212 │ 211 │     return $documentation
 213 │ 212 │ }
 214 │ 213 │
 215 │ 214 │ # Helper function to get remediation URL
 216 │ 215 │ function Get-RemediationUrl {
 217 │ 216 │     param(
 218 │ 217 │         [string]$TestName,
 219 │ 218 │         [string]$TestType
 220 │ 219 │     )
 221 │ 220 │
 222 │ 221 │     $baseUrl = "https://security.microsoft.com"
 223 │ 222 │
 224 │ 223 │     switch -Regex ($TestName) {
 225 │ 224 │         'Spam|AntiSpam' { return "$baseUrl/antispam" }
 226 │ 225 │         'Phish|AntiPhish' { return "$baseUrl/antiphishing" }
 227 │ 226 │         'Safe.*Link' { return "$baseUrl/safelinks" }
 228 │ 227 │         'Safe.*Attach' { return "$baseUrl/safeattachments" }
 229 │ 228 │         'Malware' { return "$baseUrl/antimalware" }
 230 │ 229 │         'DKIM|SPF|DMARC' { return "$baseUrl/authentication" }
 231 │ 230 │         'Audit' { return "https://compliance.microsoft.com/auditlogsearch" }
 232 │ 231 │         'DLP' { return "https://compliance.microsoft.com/datalossprevention" }
 233 │ 232 │         'Calendar|Sharing' { return "https://admin.exchange.microsoft.com/#/organizationconfig" }
 234 │ 233 │         'External.*Forward' { return "$baseUrl/antispam" }
 235 │ 234 │         'Outbound.*Spam' { return "$baseUrl/antispam" }
 236 │ 235 │         default { return "https://admin.microsoft.com" }
 237 │ 236 │     }
 238 │ 237 │ }
 239 │ 238 │
 240 │ 239 │ # Initialize results collection
 241 │ 240 │ $detailedResults = @()
 242 │ 241 │ $testStartTime = Get-Date
 243 │ 242 │
 244 │ 243 │ Write-Host "`nRunning Exchange Online security tests..." -ForegroundColor Yellow
 245 │ 244 │ Write-Host "This may take several minutes..." -ForegroundColor DarkGray
 246 │ 245 │
 247 │ 246 │ # Process each test file
 248 │ 247 │ $fileCount = 0
 249 │ 248 │ $totalTestCount = 0
 250 │ 249 │
 251 │ 250 │ foreach ($testFile in $exoTestFiles) {
 252 │ 251 │     $fileCount++
 253 │ 252 │     $percentComplete = [math]::Round(($fileCount / $exoTestFiles.Count) * 100, 0)
 254 │ 253 │     Write-Progress -Activity "Running Exchange Online Security Tests" -Status "Processing $($testFile.Name)" -PercentComplete $percentComplete
 255 │ 254 │
 256 │ 255 │     try {
 257 │ 256 │         # Run the test file
 258 │ 257 │         $container = New-PesterContainer -Path $testFile.FullName
 259 │ 258 │
 260 │ 259 │         $pesterConfig = New-PesterConfiguration
 261 │ 260 │         $pesterConfig.Run.Container = $container
 262 │ 261 │         $pesterConfig.Run.PassThru = $true
 263 │ 262 │         $pesterConfig.Output.Verbosity = 'None'
 264 │ 263 │         $pesterConfig.TestResult.Enabled = $true
 265 │ 264 │
 266 │ 265 │         # Run tests
 267 │ 266 │         $testOutput = Invoke-Pester -Configuration $pesterConfig
 268 │ 267 │
 269 │ 268 │         foreach ($test in $testOutput.Tests) {
 270 │ 269 │             $totalTestCount++
 271 │ 270 │
 272 │ 271 │             # Determine test metadata
 273 │ 272 │             $testType = if ($testFile.DirectoryName -match 'cisa') { 'CISA' }
 274 │ 273 │                        elseif ($testFile.DirectoryName -match 'orca') { 'ORCA' }
 275 │ 274 │                        elseif ($testFile.DirectoryName -match 'cis') { 'CIS' }
 276 │ 275 │                        else { 'Other' }
 277 │ 276 │
 278 │ 277 │             # Get test documentation
 279 │ 278 │             $docs = Get-TestDocumentation -TestName $test.Name -TestFile $testFile.FullName
 280 │ 279 │
 281 │ 280 │             # Get remediation URL
 282 │ 281 │             $remediationUrl = Get-RemediationUrl -TestName $test.Name -TestType $testType
 283 │ 282 │
 284 │ 283 │             # Extract test ID from name
 285 │ 284 │             $testId = if ($test.Name -match '^(ORCA\.\d+|CISA\.MS\.EXO\.\d+\.\d+|CIS\.\d+\.\d+\.\d+)') {
 286 │ 285 │                 $matches[1]
 287 │ 286 │             } else {
 288 │ 287 │                 $test.Name -split ':' | Select-Object -First 1
 289 │ 288 │             }
 290 │ 289 │
 291 │ 290 │             # Build detailed output
 292 │ 291 │             $detailOutput = ""
 293 │ 292 │             if ($test.Result -eq 'Failed' -and $test.ErrorRecord) {
 294 │ 293 │                 $detailOutput = $test.ErrorRecord.Exception.Message
 295 │ 294 │             } elseif ($test.Result -eq 'Skipped' -and $test.SkippedBecause) {
 296 │ 295 │                 $detailOutput = "Skipped: $($test.SkippedBecause)"
 297 │ 296 │             }
 298 │ 297 │
 299 │ 298 │             # Create detailed record
 300 │ 299 │             $record = [PSCustomObject]@{
 301 │ 300 │                 TestFile = $testFile.Name
 302 │ 301 │                 TestType = $testType
 303 │ 302 │                 TestId = $testId
 304 │ 303 │                 TestName = $test.Name
 305 │ 304 │                 Result = $test.Result
 306 │ 305 │                 Executed = $test.Executed
 307 │ 306 │                 Duration = if ($test.Duration) { "{0:N2}" -f $test.Duration.TotalSeconds } else { "0.00" }
 308 │ 307 │
 309 │ 308 │                 # Test details
 310 │ 309 │                 Description = $docs.Description
 311 │ 310 │                 Rationale = $docs.Rationale
 312 │ 311 │                 Impact = $docs.Impact
 313 │ 312 │                 DefaultValue = $docs.DefaultValue
 314 │ 313 │
 315 │ 314 │                 # Result details
 316 │ 315 │                 ResultDetail = $detailOutput
 317 │ 316 │                 ErrorMessage = if ($test.ErrorRecord) { $test.ErrorRecord.Exception.Message } else { "" }
 318 │ 317 │
 319 │ 318 │                 # Remediation
 320 │ 319 │                 HowToFix = $docs.HowToFix
 321 │ 320 │                 RemediationUrl = $remediationUrl
 322 │ 321 │                 References = $docs.References
 323 │ 322 │
 324 │ 323 │                 # Additional context
 325 │ 324 │                 Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 326 │ 325 │             }
 327 │ 326 │
 328 │ 327 │             $detailedResults += $record
 329 │ 328 │         }
 330 │ 329 │     }
 331 │ 330 │     catch {
 332 │ 331 │         Write-Warning "Error processing $($testFile.Name): $_"
 333 │ 332 │     }
 334 │ 333 │ }
 335 │ 334 │
 336 │ 335 │ Write-Progress -Activity "Running Exchange Online Security Tests" -Completed
 337 │ 336 │
 338 │ 337 │ $testEndTime = Get-Date
 339 │ 338 │ $testDuration = $testEndTime - $testStartTime
 340 │ 339 │
 341 │ 340 │ # Generate timestamp for file names
 342 │ 341 │ $reportTimestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
 343 │ 342 │
 344 │ 343 │ # Create comprehensive CSV export
 345 │ 344 │ Write-Host "`nGenerating reports..." -ForegroundColor Yellow
 346 │ 345 │
 347 │ 346 │ $csvPath = Join-Path $reportsPath "ExchangeHealthCheck-Results-$reportTimestamp.csv"
 348 │ 347 │
 349 │ 348 │ # Filter results based on parameter
 350 │ 349 │ $exportResults = if ($IncludePassedDetails) {
 351 │ 350 │     $detailedResults
 352 │ 351 │ } else {
 353 │ 352 │     # For passed tests, include minimal details
 354 │ 353 │     $detailedResults | ForEach-Object {
 355 │ 354 │         if ($_.Result -in @('Failed', 'Skipped')) {
 356 │ 355 │             $_
 357 │ 356 │         } else {
 358 │ 357 │             # Create simplified record for passed tests
 359 │ 358 │             [PSCustomObject]@{
 360 │ 359 │                 TestFile = $_.TestFile
 361 │ 360 │                 TestType = $_.TestType
 362 │ 361 │                 TestId = $_.TestId
 363 │ 362 │                 TestName = $_.TestName
 364 │ 363 │                 Result = $_.Result
 365 │ 364 │                 Executed = $_.Executed
 366 │ 365 │                 Duration = $_.Duration
 367 │ 366 │                 Description = $_.Description
 368 │ 367 │                 Rationale = ""
 369 │ 368 │                 Impact = ""
 370 │ 369 │                 DefaultValue = ""
 371 │ 370 │                 ResultDetail = "Test passed successfully"
 372 │ 371 │                 ErrorMessage = ""
 373 │ 372 │                 HowToFix = ""
 374 │ 373 │                 RemediationUrl = ""
 375 │ 374 │                 References = ""
 376 │ 375 │                 Timestamp = $_.Timestamp
 377 │ 376 │             }
 378 │ 377 │         }
 379 │ 378 │     }
 380 │ 379 │ }
 381 │ 380 │
 382 │ 381 │ $exportResults | Export-Csv -Path $csvPath -NoTypeInformation
 383 │ 382 │ Write-Host "Detailed results exported to:

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

 383 │ 382 │ $(Split-Path $csvPath -Leaf)" -ForegroundColor Green
 384 │ 383 │
 385 │ 384 │ # Create executive summary
 386 │ 385 │ $summaryPath = Join-Path $reportsPath "ExchangeHealthCheck-Summary-$reportTimestamp.csv"
 387 │ 386 │
 388 │ 387 │ $summary = $detailedResults | Group-Object TestType, Result | ForEach-Object {
 389 │ 388 │     $parts = $_.Name -split ', '
 390 │ 389 │     [PSCustomObject]@{
 391 │ 390 │         TestType = $parts[0]
 392 │ 391 │         Result = $parts[1]
 393 │ 392 │         Count = $_.Count
 394 │ 393 │         TestIds = ($_.Group.TestId | Sort-Object -Unique) -join '; '
 395 │ 394 │     }
 396 │ 395 │ } | Sort-Object TestType, Result
 397 │ 396 │
 398 │ 397 │ $summary | Export-Csv -Path $summaryPath -NoTypeInformation
 399 │ 398 │ Write-Host "Executive summary exported to:

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

 404 │ 403 │ $failedTests = $detailedResults | Where-Object { $_.Result -eq 'Failed' } | Sort-Object TestType, TestId
 405 │ 404 │
 406 │ 405 │ # Calculate statistics
 407 │ 406 │ $stats = @{
 408 │ 407 │     Total = $detailedResults.Count
 409 │ 408 │     Passed = ($detailedResults | Where-Object {$_.Result -eq 'Passed'}).Count
 410 │ 409 │     Failed = ($detailedResults | Where-Object {$_.Result -eq 'Failed'}).Count
 411 │ 410 │     Skipped = ($detailedResults | Where-Object {$_.Result -eq 'Skipped'}).Count
 412 │ 411 │ }
 413 │ 412 │
 414 │ 413 │ # Create text report content

Clone found (powershell):
 - /github/workspace/Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   /github/workspace/Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

 481 │ 547 │  -Leaf)" -ForegroundColor Green
 482 │ 548 │
 483 │ 549 │ # Display summary
 484 │ 550 │ Write-Host "`n" + ("="*60) -ForegroundColor Cyan
 485 │ 551 │ Write-Host "SECURITY HEALTH CHECK COMPLETED" -ForegroundColor Cyan
 486 │ 552 │ Write-Host ("="*60) -ForegroundColor Cyan
 487 │ 553 │
 488 │ 554 │ Write-Host "`nTest Results Summary:" -ForegroundColor Yellow
 489 │ 555 │ Write-Host "  Total Tests Run: $($stats.Total)" -ForegroundColor White
 490 │ 556 │ Write-Host "  Passed: $($stats.Passed)" -ForegroundColor Green
 491 │ 557 │ Write-Host "  Failed: $($stats.Failed)" -ForegroundColor Red
 492 │ 558 │ Write-Host "  Skipped: $($stats.Skipped)" -ForegroundColor Yellow
 493 │ 559 │
 494 │ 560 │ Write-Host

Found 7 clones.
Error: ERROR: jscpd found too many duplicates (31.69%) over threshold (0%)
    at ThresholdReporter.report (/node_modules/@jscpd/finder/dist/index.js:615:13)
    at /node_modules/@jscpd/finder/dist/index.js:109:18
    at Array.forEach (<anonymous>)
    at /node_modules/@jscpd/finder/dist/index.js:108:22
    at async /node_modules/jscpd/dist/bin/jscpd.js:9:5ERROR: jscpd found too many duplicates (31.69%) over threshold (0%)

@LukeEvansTech
LukeEvansTech force-pushed the chore/add-super-linter branch from 1a5b4ea to b4dbf2c Compare May 4, 2026 19:46
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

All files and directories linted successfully

For more information, see the GitHub Actions workflow run

Powered by Super-linter

@LukeEvansTech
LukeEvansTech force-pushed the chore/add-super-linter branch from b4dbf2c to c8b4b9d Compare May 4, 2026 19:57
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSCPD Fail ❌
JSON Pass ✅
JSON_PRETTIER Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

Super-linter detected linting errors

For more information, see the GitHub Actions workflow run

Powered by Super-linter

JSCPD
Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

 39 │ 38 │ [CmdletBinding()]
 40 │ 39 │ param(
 41 │ 40 │     [switch]$SkipConnection,
 42 │ 41 │     [switch]$IncludePassedDetails
 43 │ 42 │ )
 44 │ 43 │
 45 │ 44 │ # Script configuration
 46 │ 45 │ $ErrorActionPreference = 'Stop'
 47 │ 46 │ $ProgressPreference = 'Continue'
 48 │ 47 │
 49 │ 48 │ # Set up paths
 50 │ 49 │ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
 51 │ 50 │ $reportsPath = Join-Path $scriptPath "Reports"
 52 │ 51 │
 53 │ 52 │ # Ensure Reports directory exists
 54 │ 53 │ if (!(Test-Path $reportsPath)) {
 55 │ 54 │     New-Item -ItemType Directory -Path $reportsPath -Force | Out-Null
 56 │ 55 │     Write-Host "Created Reports directory: $reportsPath" -ForegroundColor Green
 57 │ 56 │ }
 58 │ 57 │
 59 │ 58 │ # Import required modules
 60 │ 59 │ Write-Host "`nExchange Online Security Health Check (Simplified)"

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

 81  │ 80  │ $_"
 82  │ 81  │         exit 1
 83  │ 82  │     }
 84  │ 83  │ }
 85  │ 84  │
 86  │ 85  │ # Verify connection
 87  │ 86  │ $connectionInfo = Get-ConnectionInformation | Where-Object { $_.Name -like '*ExchangeOnline*' }
 88  │ 87  │ if (-not $connectionInfo) {
 89  │ 88  │     Write-Error "No active Exchange Online connection found. Please connect first or remove -SkipConnection parameter."
 90  │ 89  │     exit 1
 91  │ 90  │ }
 92  │ 91  │
 93  │ 92  │ Write-Host "`nConnection Details:" -ForegroundColor Cyan
 94  │ 93  │ Write-Host "  Tenant: $($connectionInfo.TenantId)" -ForegroundColor White
 95  │ 94  │ Write-Host "  User: $($connectionInfo.UserPrincipalName)" -ForegroundColor White
 96  │ 95  │ Write-Host "  Environment: $($connectionInfo.ConnectionUri)" -ForegroundColor White
 97  │ 96  │
 98  │ 97  │ # Get Maester module path
 99  │ 98  │ $maesterModule = Get-Module -ListAvailable Maester | Select-Object -First 1
 100 │ 99  │ if (!$maesterModule) {
 101 │ 100 │     Write-Error "Maester module not found. Please install it first: Install-Module Maester"
 102 │ 101 │     exit 1
 103 │ 102 │ }
 104 │ 103 │
 105 │ 104 │ $maesterPath = Split-Path $maesterModule.Path
 106 │ 105 │ $testsPath = Join-Path $maesterPath "maester-tests"
 107 │ 106 │
 108 │ 107 │ Write-Host "`nSearching for Exchange Online tests..." -ForegroundColor Yellow
 109 │ 108 │
 110 │ 109 │ # Find all test files with Exchange-related tags
 111 │ 110 │ $exoTestFiles = @()
 112 │ 111 │ $testPatterns = @(
 113 │ 112 │     '-Tag.*["'']EXO["'']'

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

 114 │ 113 │ # CISA tests use "MS.EXO"
 115 │ 114 │ )
 116 │ 115 │
 117 │ 116 │ # Search in relevant directories
 118 │ 117 │ $searchDirs = Get-ChildItem -Path $testsPath -Directory -Recurse -ErrorAction SilentlyContinue |
 119 │ 118 │     Where-Object {
 120 │ 119 │         $_.Name -in @('exchange', 'orca', 'cis', 'cisa') -or
 121 │ 120 │         $_.FullName -match 'exchange|exo'
 122 │ 121 │     }
 123 │ 122 │
 124 │ 123 │ foreach ($dir in $searchDirs) {
 125 │ 124 │     $files = Get-ChildItem -Path $dir.FullName -Filter "*.Tests.ps1" -File -ErrorAction SilentlyContinue
 126 │ 125 │     foreach ($file in $files) {
 127 │ 126 │         $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
 128 │ 127 │         if ($content) {
 129 │ 128 │             foreach ($pattern in $testPatterns) {
 130 │ 129 │                 if ($content -match $pattern) {
 131 │ 130 │                     if ($file.FullName -notin $exoTestFiles.FullName) {
 132 │ 131 │                         $exoTestFiles += $file
 133 │ 132 │                     }
 134 │ 133 │                     break
 135 │ 134 │                 }
 136 │ 135 │             }
 137 │ 136 │         }
 138 │ 137 │     }
 139 │ 138 │ }
 140 │ 139 │
 141 │ 140 │ $exoTestFiles = $exoTestFiles | Sort-Object DirectoryName, Name
 142 │ 141 │ Write-Host "Found

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

 142 │ 141 │ $($exoTestFiles.Count) Exchange Online test files" -ForegroundColor Green
 143 │ 142 │
 144 │ 143 │ # Group by test type for reporting
 145 │ 144 │ $fileGroups = $exoTestFiles | Group-Object {
 146 │ 145 │     if ($_.DirectoryName -match 'cisa') { 'CISA' }
 147 │ 146 │     elseif ($_.DirectoryName -match 'orca') { 'ORCA' }
 148 │ 147 │     elseif ($_.DirectoryName -match 'cis') { 'CIS' }
 149 │ 148 │     else { 'Other' }
 150 │ 149 │ }
 151 │ 150 │
 152 │ 151 │ Write-Host "`nTest Distribution:" -ForegroundColor Cyan
 153 │ 152 │ foreach ($group in $fileGroups | Sort-Object Name) {
 154 │ 153 │     Write-Host "  $($group.Name): $($group.Count) test files" -ForegroundColor White
 155 │ 154 │ }
 156 │ 155 │
 157 │ 156 │ # Helper function to get test documentation
 158 │ 157 │ function Get-TestDocumentation {
 159 │ 158 │     param(
 160 │ 159 │         [string]$TestName,
 161 │ 160 │         [string]$TestFile
 162 │ 161 │     )
 163 │ 162 │
 164 │ 163 │     $documentation = @{
 165 │ 164 │         Description = ""
 166 │ 165 │         HowToFix = ""
 167 │ 166 │         References = ""
 168 │ 167 │         Impact = ""
 169 │ 168 │         DefaultValue = ""
 170 │ 169 │         Rationale = ""
 171 │ 170 │     }
 172 │ 171 │
 173 │ 172 │     # Try to find corresponding .ps1 file in public folder
 174 │ 173 │     $publicPath = $TestFile -replace 'maester-tests', 'public' -replace '\.Tests\.ps1$', '.ps1'
 175 │ 174 │     if (Test-Path $publicPath) {
 176 │ 175 │         $cmdContent = Get-Content $publicPath -Raw
 177 │ 176 │
 178 │ 177 │         # Extract description from .SYNOPSIS
 179 │ 178 │         if ($cmdContent -match '\.SYNOPSIS\s*\n\s*(.+?)(?=\n\s*\.|#>)') {
 180 │ 179 │             $documentation.Description = $matches[1].Trim()
 181 │ 180 │         }
 182 │ 181 │
 183 │ 182 │         # Extract description from .DESCRIPTION
 184 │ 183 │         if ($cmdContent -match '\.DESCRIPTION\s*\n([\s\S]+?)(?=\n\s*\.|#>)') {
 185 │ 184 │             $documentation.Rationale = $matches[1].Trim() -replace '\s+', ' '
 186 │ 185 │         }
 187 │ 186 │     }
 188 │ 187 │
 189 │ 188 │     # Try to find corresponding .md file
 190 │ 189 │     $mdPath = $publicPath -replace '\.ps1$', '.md'
 191 │ 190 │     if (Test-Path $mdPath) {
 192 │ 191 │         $mdContent = Get-Content $mdPath -Raw
 193 │ 192 │
 194 │ 193 │         # Extract sections from markdown
 195 │ 194 │         if ($mdContent -match '## Description\s*\n([\s\S]+?)(?=\n##|\Z)') {
 196 │ 195 │             $documentation.Description = $matches[1].Trim()
 197 │ 196 │         }
 198 │ 197 │         if ($mdContent -match '## How to fix\s*\n([\s\S]+?)(?=\n##|\Z)') {
 199 │ 198 │             $documentation.HowToFix = $matches[1].Trim()
 200 │ 199 │         }
 201 │ 200 │         if ($mdContent -match '## References?\s*\n([\s\S]+?)(?=\n##|\Z)') {
 202 │ 201 │             $documentation.References = $matches[1].Trim()
 203 │ 202 │         }
 204 │ 203 │         if ($mdContent -match '## Impact\s*\n([\s\S]+?)(?=\n##|\Z)') {
 205 │ 204 │             $documentation.Impact = $matches[1].Trim()
 206 │ 205 │         }
 207 │ 206 │         if ($mdContent -match '## Default Value\s*\n([\s\S]+?)(?=\n##|\Z)') {
 208 │ 207 │             $documentation.DefaultValue = $matches[1].Trim()
 209 │ 208 │         }
 210 │ 209 │     }
 211 │ 210 │
 212 │ 211 │     return $documentation
 213 │ 212 │ }
 214 │ 213 │
 215 │ 214 │ # Helper function to get remediation URL
 216 │ 215 │ function Get-RemediationUrl {
 217 │ 216 │     param(
 218 │ 217 │         [string]$TestName,
 219 │ 218 │         [string]$TestType
 220 │ 219 │     )
 221 │ 220 │
 222 │ 221 │     $baseUrl = "https://security.microsoft.com"
 223 │ 222 │
 224 │ 223 │     switch -Regex ($TestName) {
 225 │ 224 │         'Spam|AntiSpam' { return "$baseUrl/antispam" }
 226 │ 225 │         'Phish|AntiPhish' { return "$baseUrl/antiphishing" }
 227 │ 226 │         'Safe.*Link' { return "$baseUrl/safelinks" }
 228 │ 227 │         'Safe.*Attach' { return "$baseUrl/safeattachments" }
 229 │ 228 │         'Malware' { return "$baseUrl/antimalware" }
 230 │ 229 │         'DKIM|SPF|DMARC' { return "$baseUrl/authentication" }
 231 │ 230 │         'Audit' { return "https://compliance.microsoft.com/auditlogsearch" }
 232 │ 231 │         'DLP' { return "https://compliance.microsoft.com/datalossprevention" }
 233 │ 232 │         'Calendar|Sharing' { return "https://admin.exchange.microsoft.com/#/organizationconfig" }
 234 │ 233 │         'External.*Forward' { return "$baseUrl/antispam" }
 235 │ 234 │         'Outbound.*Spam' { return "$baseUrl/antispam" }
 236 │ 235 │         default { return "https://admin.microsoft.com" }
 237 │ 236 │     }
 238 │ 237 │ }
 239 │ 238 │
 240 │ 239 │ # Initialize results collection
 241 │ 240 │ $detailedResults = @()
 242 │ 241 │ $testStartTime = Get-Date
 243 │ 242 │
 244 │ 243 │ Write-Host "`nRunning Exchange Online security tests..." -ForegroundColor Yellow
 245 │ 244 │ Write-Host "This may take several minutes..." -ForegroundColor DarkGray
 246 │ 245 │
 247 │ 246 │ # Process each test file
 248 │ 247 │ $fileCount = 0
 249 │ 248 │ $totalTestCount = 0
 250 │ 249 │
 251 │ 250 │ foreach ($testFile in $exoTestFiles) {
 252 │ 251 │     $fileCount++
 253 │ 252 │     $percentComplete = [math]::Round(($fileCount / $exoTestFiles.Count) * 100, 0)
 254 │ 253 │     Write-Progress -Activity "Running Exchange Online Security Tests" -Status "Processing $($testFile.Name)" -PercentComplete $percentComplete
 255 │ 254 │
 256 │ 255 │     try {
 257 │ 256 │         # Run the test file
 258 │ 257 │         $container = New-PesterContainer -Path $testFile.FullName
 259 │ 258 │
 260 │ 259 │         $pesterConfig = New-PesterConfiguration
 261 │ 260 │         $pesterConfig.Run.Container = $container
 262 │ 261 │         $pesterConfig.Run.PassThru = $true
 263 │ 262 │         $pesterConfig.Output.Verbosity = 'None'
 264 │ 263 │         $pesterConfig.TestResult.Enabled = $true
 265 │ 264 │
 266 │ 265 │         # Run tests
 267 │ 266 │         $testOutput = Invoke-Pester -Configuration $pesterConfig
 268 │ 267 │
 269 │ 268 │         foreach ($test in $testOutput.Tests) {
 270 │ 269 │             $totalTestCount++
 271 │ 270 │
 272 │ 271 │             # Determine test metadata
 273 │ 272 │             $testType = if ($testFile.DirectoryName -match 'cisa') { 'CISA' }
 274 │ 273 │                        elseif ($testFile.DirectoryName -match 'orca') { 'ORCA' }
 275 │ 274 │                        elseif ($testFile.DirectoryName -match 'cis') { 'CIS' }
 276 │ 275 │                        else { 'Other' }
 277 │ 276 │
 278 │ 277 │             # Get test documentation
 279 │ 278 │             $docs = Get-TestDocumentation -TestName $test.Name -TestFile $testFile.FullName
 280 │ 279 │
 281 │ 280 │             # Get remediation URL
 282 │ 281 │             $remediationUrl = Get-RemediationUrl -TestName $test.Name -TestType $testType
 283 │ 282 │
 284 │ 283 │             # Extract test ID from name
 285 │ 284 │             $testId = if ($test.Name -match '^(ORCA\.\d+|CISA\.MS\.EXO\.\d+\.\d+|CIS\.\d+\.\d+\.\d+)') {
 286 │ 285 │                 $matches[1]
 287 │ 286 │             } else {
 288 │ 287 │                 $test.Name -split ':' | Select-Object -First 1
 289 │ 288 │             }
 290 │ 289 │
 291 │ 290 │             # Build detailed output
 292 │ 291 │             $detailOutput = ""
 293 │ 292 │             if ($test.Result -eq 'Failed' -and $test.ErrorRecord) {
 294 │ 293 │                 $detailOutput = $test.ErrorRecord.Exception.Message
 295 │ 294 │             } elseif ($test.Result -eq 'Skipped' -and $test.SkippedBecause) {
 296 │ 295 │                 $detailOutput = "Skipped: $($test.SkippedBecause)"
 297 │ 296 │             }
 298 │ 297 │
 299 │ 298 │             # Create detailed record
 300 │ 299 │             $record = [PSCustomObject]@{
 301 │ 300 │                 TestFile = $testFile.Name
 302 │ 301 │                 TestType = $testType
 303 │ 302 │                 TestId = $testId
 304 │ 303 │                 TestName = $test.Name
 305 │ 304 │                 Result = $test.Result
 306 │ 305 │                 Executed = $test.Executed
 307 │ 306 │                 Duration = if ($test.Duration) { "{0:N2}" -f $test.Duration.TotalSeconds } else { "0.00" }
 308 │ 307 │
 309 │ 308 │                 # Test details
 310 │ 309 │                 Description = $docs.Description
 311 │ 310 │                 Rationale = $docs.Rationale
 312 │ 311 │                 Impact = $docs.Impact
 313 │ 312 │                 DefaultValue = $docs.DefaultValue
 314 │ 313 │
 315 │ 314 │                 # Result details
 316 │ 315 │                 ResultDetail = $detailOutput
 317 │ 316 │                 ErrorMessage = if ($test.ErrorRecord) { $test.ErrorRecord.Exception.Message } else { "" }
 318 │ 317 │
 319 │ 318 │                 # Remediation
 320 │ 319 │                 HowToFix = $docs.HowToFix
 321 │ 320 │                 RemediationUrl = $remediationUrl
 322 │ 321 │                 References = $docs.References
 323 │ 322 │
 324 │ 323 │                 # Additional context
 325 │ 324 │                 Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 326 │ 325 │             }
 327 │ 326 │
 328 │ 327 │             $detailedResults += $record
 329 │ 328 │         }
 330 │ 329 │     }
 331 │ 330 │     catch {
 332 │ 331 │         Write-Warning "Error processing $($testFile.Name): $_"
 333 │ 332 │     }
 334 │ 333 │ }
 335 │ 334 │
 336 │ 335 │ Write-Progress -Activity "Running Exchange Online Security Tests" -Completed
 337 │ 336 │
 338 │ 337 │ $testEndTime = Get-Date
 339 │ 338 │ $testDuration = $testEndTime - $testStartTime
 340 │ 339 │
 341 │ 340 │ # Generate timestamp for file names
 342 │ 341 │ $reportTimestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
 343 │ 342 │
 344 │ 343 │ # Create comprehensive CSV export
 345 │ 344 │ Write-Host "`nGenerating reports..." -ForegroundColor Yellow
 346 │ 345 │
 347 │ 346 │ $csvPath = Join-Path $reportsPath "ExchangeHealthCheck-Results-$reportTimestamp.csv"
 348 │ 347 │
 349 │ 348 │ # Filter results based on parameter
 350 │ 349 │ $exportResults = if ($IncludePassedDetails) {
 351 │ 350 │     $detailedResults
 352 │ 351 │ } else {
 353 │ 352 │     # For passed tests, include minimal details
 354 │ 353 │     $detailedResults | ForEach-Object {
 355 │ 354 │         if ($_.Result -in @('Failed', 'Skipped')) {
 356 │ 355 │             $_
 357 │ 356 │         } else {
 358 │ 357 │             # Create simplified record for passed tests
 359 │ 358 │             [PSCustomObject]@{
 360 │ 359 │                 TestFile = $_.TestFile
 361 │ 360 │                 TestType = $_.TestType
 362 │ 361 │                 TestId = $_.TestId
 363 │ 362 │                 TestName = $_.TestName
 364 │ 363 │                 Result = $_.Result
 365 │ 364 │                 Executed = $_.Executed
 366 │ 365 │                 Duration = $_.Duration
 367 │ 366 │                 Description = $_.Description
 368 │ 367 │                 Rationale = ""
 369 │ 368 │                 Impact = ""
 370 │ 369 │                 DefaultValue = ""
 371 │ 370 │                 ResultDetail = "Test passed successfully"
 372 │ 371 │                 ErrorMessage = ""
 373 │ 372 │                 HowToFix = ""
 374 │ 373 │                 RemediationUrl = ""
 375 │ 374 │                 References = ""
 376 │ 375 │                 Timestamp = $_.Timestamp
 377 │ 376 │             }
 378 │ 377 │         }
 379 │ 378 │     }
 380 │ 379 │ }
 381 │ 380 │
 382 │ 381 │ $exportResults | Export-Csv -Path $csvPath -NoTypeInformation
 383 │ 382 │ Write-Host "Detailed results exported to:

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

 383 │ 382 │ $(Split-Path $csvPath -Leaf)" -ForegroundColor Green
 384 │ 383 │
 385 │ 384 │ # Create executive summary
 386 │ 385 │ $summaryPath = Join-Path $reportsPath "ExchangeHealthCheck-Summary-$reportTimestamp.csv"
 387 │ 386 │
 388 │ 387 │ $summary = $detailedResults | Group-Object TestType, Result | ForEach-Object {
 389 │ 388 │     $parts = $_.Name -split ', '
 390 │ 389 │     [PSCustomObject]@{
 391 │ 390 │         TestType = $parts[0]
 392 │ 391 │         Result = $parts[1]
 393 │ 392 │         Count = $_.Count
 394 │ 393 │         TestIds = ($_.Group.TestId | Sort-Object -Unique) -join '; '
 395 │ 394 │     }
 396 │ 395 │ } | Sort-Object TestType, Result
 397 │ 396 │
 398 │ 397 │ $summary | Export-Csv -Path $summaryPath -NoTypeInformation
 399 │ 398 │ Write-Host "Executive summary exported to:

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

 404 │ 403 │ $failedTests = $detailedResults | Where-Object { $_.Result -eq 'Failed' } | Sort-Object TestType, TestId
 405 │ 404 │
 406 │ 405 │ # Calculate statistics
 407 │ 406 │ $stats = @{
 408 │ 407 │     Total = $detailedResults.Count
 409 │ 408 │     Passed = ($detailedResults | Where-Object {$_.Result -eq 'Passed'}).Count
 410 │ 409 │     Failed = ($detailedResults | Where-Object {$_.Result -eq 'Failed'}).Count
 411 │ 410 │     Skipped = ($detailedResults | Where-Object {$_.Result -eq 'Skipped'}).Count
 412 │ 411 │ }
 413 │ 412 │
 414 │ 413 │ # Create text report content

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

 481 │ 547 │  -Leaf)" -ForegroundColor Green
 482 │ 548 │
 483 │ 549 │ # Display summary
 484 │ 550 │ Write-Host "`n" + ("="*60) -ForegroundColor Cyan
 485 │ 551 │ Write-Host "SECURITY HEALTH CHECK COMPLETED" -ForegroundColor Cyan
 486 │ 552 │ Write-Host ("="*60) -ForegroundColor Cyan
 487 │ 553 │
 488 │ 554 │ Write-Host "`nTest Results Summary:" -ForegroundColor Yellow
 489 │ 555 │ Write-Host "  Total Tests Run: $($stats.Total)" -ForegroundColor White
 490 │ 556 │ Write-Host "  Passed: $($stats.Passed)" -ForegroundColor Green
 491 │ 557 │ Write-Host "  Failed: $($stats.Failed)" -ForegroundColor Red
 492 │ 558 │ Write-Host "  Skipped: $($stats.Skipped)" -ForegroundColor Yellow
 493 │ 559 │
 494 │ 560 │ Write-Host

Found 7 clones.
Error: ERROR: jscpd found too many duplicates (31.36%) over threshold (10%)
    at ThresholdReporter.report (/node_modules/@jscpd/finder/dist/index.js:615:13)
    at /node_modules/@jscpd/finder/dist/index.js:109:18
    at Array.forEach (<anonymous>)
    at /node_modules/@jscpd/finder/dist/index.js:108:22
    at async /node_modules/jscpd/dist/bin/jscpd.js:9:5ERROR: jscpd found too many duplicates (31.36%) over threshold (10%)

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSCPD Fail ❌
JSON Pass ✅
JSON_PRETTIER Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

Super-linter detected linting errors

For more information, see the GitHub Actions workflow run

Powered by Super-linter

JSCPD
Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

 39 │ 38 │ [CmdletBinding()]
 40 │ 39 │ param(
 41 │ 40 │     [switch]$SkipConnection,
 42 │ 41 │     [switch]$IncludePassedDetails
 43 │ 42 │ )
 44 │ 43 │
 45 │ 44 │ # Script configuration
 46 │ 45 │ $ErrorActionPreference = 'Stop'
 47 │ 46 │ $ProgressPreference = 'Continue'
 48 │ 47 │
 49 │ 48 │ # Set up paths
 50 │ 49 │ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
 51 │ 50 │ $reportsPath = Join-Path $scriptPath "Reports"
 52 │ 51 │
 53 │ 52 │ # Ensure Reports directory exists
 54 │ 53 │ if (!(Test-Path $reportsPath)) {
 55 │ 54 │     New-Item -ItemType Directory -Path $reportsPath -Force | Out-Null
 56 │ 55 │     Write-Host "Created Reports directory: $reportsPath" -ForegroundColor Green
 57 │ 56 │ }
 58 │ 57 │
 59 │ 58 │ # Import required modules
 60 │ 59 │ Write-Host "`nExchange Online Security Health Check (Simplified)"

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

 81  │ 80  │ $_"
 82  │ 81  │         exit 1
 83  │ 82  │     }
 84  │ 83  │ }
 85  │ 84  │
 86  │ 85  │ # Verify connection
 87  │ 86  │ $connectionInfo = Get-ConnectionInformation | Where-Object { $_.Name -like '*ExchangeOnline*' }
 88  │ 87  │ if (-not $connectionInfo) {
 89  │ 88  │     Write-Error "No active Exchange Online connection found. Please connect first or remove -SkipConnection parameter."
 90  │ 89  │     exit 1
 91  │ 90  │ }
 92  │ 91  │
 93  │ 92  │ Write-Host "`nConnection Details:" -ForegroundColor Cyan
 94  │ 93  │ Write-Host "  Tenant: $($connectionInfo.TenantId)" -ForegroundColor White
 95  │ 94  │ Write-Host "  User: $($connectionInfo.UserPrincipalName)" -ForegroundColor White
 96  │ 95  │ Write-Host "  Environment: $($connectionInfo.ConnectionUri)" -ForegroundColor White
 97  │ 96  │
 98  │ 97  │ # Get Maester module path
 99  │ 98  │ $maesterModule = Get-Module -ListAvailable Maester | Select-Object -First 1
 100 │ 99  │ if (!$maesterModule) {
 101 │ 100 │     Write-Error "Maester module not found. Please install it first: Install-Module Maester"
 102 │ 101 │     exit 1
 103 │ 102 │ }
 104 │ 103 │
 105 │ 104 │ $maesterPath = Split-Path $maesterModule.Path
 106 │ 105 │ $testsPath = Join-Path $maesterPath "maester-tests"
 107 │ 106 │
 108 │ 107 │ Write-Host "`nSearching for Exchange Online tests..." -ForegroundColor Yellow
 109 │ 108 │
 110 │ 109 │ # Find all test files with Exchange-related tags
 111 │ 110 │ $exoTestFiles = @()
 112 │ 111 │ $testPatterns = @(
 113 │ 112 │     '-Tag.*["'']EXO["'']'

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

 114 │ 113 │ # CISA tests use "MS.EXO"
 115 │ 114 │ )
 116 │ 115 │
 117 │ 116 │ # Search in relevant directories
 118 │ 117 │ $searchDirs = Get-ChildItem -Path $testsPath -Directory -Recurse -ErrorAction SilentlyContinue |
 119 │ 118 │     Where-Object {
 120 │ 119 │         $_.Name -in @('exchange', 'orca', 'cis', 'cisa') -or
 121 │ 120 │         $_.FullName -match 'exchange|exo'
 122 │ 121 │     }
 123 │ 122 │
 124 │ 123 │ foreach ($dir in $searchDirs) {
 125 │ 124 │     $files = Get-ChildItem -Path $dir.FullName -Filter "*.Tests.ps1" -File -ErrorAction SilentlyContinue
 126 │ 125 │     foreach ($file in $files) {
 127 │ 126 │         $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
 128 │ 127 │         if ($content) {
 129 │ 128 │             foreach ($pattern in $testPatterns) {
 130 │ 129 │                 if ($content -match $pattern) {
 131 │ 130 │                     if ($file.FullName -notin $exoTestFiles.FullName) {
 132 │ 131 │                         $exoTestFiles += $file
 133 │ 132 │                     }
 134 │ 133 │                     break
 135 │ 134 │                 }
 136 │ 135 │             }
 137 │ 136 │         }
 138 │ 137 │     }
 139 │ 138 │ }
 140 │ 139 │
 141 │ 140 │ $exoTestFiles = $exoTestFiles | Sort-Object DirectoryName, Name
 142 │ 141 │ Write-Host "Found

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

 142 │ 141 │ $($exoTestFiles.Count) Exchange Online test files" -ForegroundColor Green
 143 │ 142 │
 144 │ 143 │ # Group by test type for reporting
 145 │ 144 │ $fileGroups = $exoTestFiles | Group-Object {
 146 │ 145 │     if ($_.DirectoryName -match 'cisa') { 'CISA' }
 147 │ 146 │     elseif ($_.DirectoryName -match 'orca') { 'ORCA' }
 148 │ 147 │     elseif ($_.DirectoryName -match 'cis') { 'CIS' }
 149 │ 148 │     else { 'Other' }
 150 │ 149 │ }
 151 │ 150 │
 152 │ 151 │ Write-Host "`nTest Distribution:" -ForegroundColor Cyan
 153 │ 152 │ foreach ($group in $fileGroups | Sort-Object Name) {
 154 │ 153 │     Write-Host "  $($group.Name): $($group.Count) test files" -ForegroundColor White
 155 │ 154 │ }
 156 │ 155 │
 157 │ 156 │ # Helper function to get test documentation
 158 │ 157 │ function Get-TestDocumentation {
 159 │ 158 │     param(
 160 │ 159 │         [string]$TestName,
 161 │ 160 │         [string]$TestFile
 162 │ 161 │     )
 163 │ 162 │
 164 │ 163 │     $documentation = @{
 165 │ 164 │         Description = ""
 166 │ 165 │         HowToFix = ""
 167 │ 166 │         References = ""
 168 │ 167 │         Impact = ""
 169 │ 168 │         DefaultValue = ""
 170 │ 169 │         Rationale = ""
 171 │ 170 │     }
 172 │ 171 │
 173 │ 172 │     # Try to find corresponding .ps1 file in public folder
 174 │ 173 │     $publicPath = $TestFile -replace 'maester-tests', 'public' -replace '\.Tests\.ps1$', '.ps1'
 175 │ 174 │     if (Test-Path $publicPath) {
 176 │ 175 │         $cmdContent = Get-Content $publicPath -Raw
 177 │ 176 │
 178 │ 177 │         # Extract description from .SYNOPSIS
 179 │ 178 │         if ($cmdContent -match '\.SYNOPSIS\s*\n\s*(.+?)(?=\n\s*\.|#>)') {
 180 │ 179 │             $documentation.Description = $matches[1].Trim()
 181 │ 180 │         }
 182 │ 181 │
 183 │ 182 │         # Extract description from .DESCRIPTION
 184 │ 183 │         if ($cmdContent -match '\.DESCRIPTION\s*\n([\s\S]+?)(?=\n\s*\.|#>)') {
 185 │ 184 │             $documentation.Rationale = $matches[1].Trim() -replace '\s+', ' '
 186 │ 185 │         }
 187 │ 186 │     }
 188 │ 187 │
 189 │ 188 │     # Try to find corresponding .md file
 190 │ 189 │     $mdPath = $publicPath -replace '\.ps1$', '.md'
 191 │ 190 │     if (Test-Path $mdPath) {
 192 │ 191 │         $mdContent = Get-Content $mdPath -Raw
 193 │ 192 │
 194 │ 193 │         # Extract sections from markdown
 195 │ 194 │         if ($mdContent -match '## Description\s*\n([\s\S]+?)(?=\n##|\Z)') {
 196 │ 195 │             $documentation.Description = $matches[1].Trim()
 197 │ 196 │         }
 198 │ 197 │         if ($mdContent -match '## How to fix\s*\n([\s\S]+?)(?=\n##|\Z)') {
 199 │ 198 │             $documentation.HowToFix = $matches[1].Trim()
 200 │ 199 │         }
 201 │ 200 │         if ($mdContent -match '## References?\s*\n([\s\S]+?)(?=\n##|\Z)') {
 202 │ 201 │             $documentation.References = $matches[1].Trim()
 203 │ 202 │         }
 204 │ 203 │         if ($mdContent -match '## Impact\s*\n([\s\S]+?)(?=\n##|\Z)') {
 205 │ 204 │             $documentation.Impact = $matches[1].Trim()
 206 │ 205 │         }
 207 │ 206 │         if ($mdContent -match '## Default Value\s*\n([\s\S]+?)(?=\n##|\Z)') {
 208 │ 207 │             $documentation.DefaultValue = $matches[1].Trim()
 209 │ 208 │         }
 210 │ 209 │     }
 211 │ 210 │
 212 │ 211 │     return $documentation
 213 │ 212 │ }
 214 │ 213 │
 215 │ 214 │ # Helper function to get remediation URL
 216 │ 215 │ function Get-RemediationUrl {
 217 │ 216 │     param(
 218 │ 217 │         [string]$TestName,
 219 │ 218 │         [string]$TestType
 220 │ 219 │     )
 221 │ 220 │
 222 │ 221 │     $baseUrl = "https://security.microsoft.com"
 223 │ 222 │
 224 │ 223 │     switch -Regex ($TestName) {
 225 │ 224 │         'Spam|AntiSpam' { return "$baseUrl/antispam" }
 226 │ 225 │         'Phish|AntiPhish' { return "$baseUrl/antiphishing" }
 227 │ 226 │         'Safe.*Link' { return "$baseUrl/safelinks" }
 228 │ 227 │         'Safe.*Attach' { return "$baseUrl/safeattachments" }
 229 │ 228 │         'Malware' { return "$baseUrl/antimalware" }
 230 │ 229 │         'DKIM|SPF|DMARC' { return "$baseUrl/authentication" }
 231 │ 230 │         'Audit' { return "https://compliance.microsoft.com/auditlogsearch" }
 232 │ 231 │         'DLP' { return "https://compliance.microsoft.com/datalossprevention" }
 233 │ 232 │         'Calendar|Sharing' { return "https://admin.exchange.microsoft.com/#/organizationconfig" }
 234 │ 233 │         'External.*Forward' { return "$baseUrl/antispam" }
 235 │ 234 │         'Outbound.*Spam' { return "$baseUrl/antispam" }
 236 │ 235 │         default { return "https://admin.microsoft.com" }
 237 │ 236 │     }
 238 │ 237 │ }
 239 │ 238 │
 240 │ 239 │ # Initialize results collection
 241 │ 240 │ $detailedResults = @()
 242 │ 241 │ $testStartTime = Get-Date
 243 │ 242 │
 244 │ 243 │ Write-Host "`nRunning Exchange Online security tests..." -ForegroundColor Yellow
 245 │ 244 │ Write-Host "This may take several minutes..." -ForegroundColor DarkGray
 246 │ 245 │
 247 │ 246 │ # Process each test file
 248 │ 247 │ $fileCount = 0
 249 │ 248 │ $totalTestCount = 0
 250 │ 249 │
 251 │ 250 │ foreach ($testFile in $exoTestFiles) {
 252 │ 251 │     $fileCount++
 253 │ 252 │     $percentComplete = [math]::Round(($fileCount / $exoTestFiles.Count) * 100, 0)
 254 │ 253 │     Write-Progress -Activity "Running Exchange Online Security Tests" -Status "Processing $($testFile.Name)" -PercentComplete $percentComplete
 255 │ 254 │
 256 │ 255 │     try {
 257 │ 256 │         # Run the test file
 258 │ 257 │         $container = New-PesterContainer -Path $testFile.FullName
 259 │ 258 │
 260 │ 259 │         $pesterConfig = New-PesterConfiguration
 261 │ 260 │         $pesterConfig.Run.Container = $container
 262 │ 261 │         $pesterConfig.Run.PassThru = $true
 263 │ 262 │         $pesterConfig.Output.Verbosity = 'None'
 264 │ 263 │         $pesterConfig.TestResult.Enabled = $true
 265 │ 264 │
 266 │ 265 │         # Run tests
 267 │ 266 │         $testOutput = Invoke-Pester -Configuration $pesterConfig
 268 │ 267 │
 269 │ 268 │         foreach ($test in $testOutput.Tests) {
 270 │ 269 │             $totalTestCount++
 271 │ 270 │
 272 │ 271 │             # Determine test metadata
 273 │ 272 │             $testType = if ($testFile.DirectoryName -match 'cisa') { 'CISA' }
 274 │ 273 │                        elseif ($testFile.DirectoryName -match 'orca') { 'ORCA' }
 275 │ 274 │                        elseif ($testFile.DirectoryName -match 'cis') { 'CIS' }
 276 │ 275 │                        else { 'Other' }
 277 │ 276 │
 278 │ 277 │             # Get test documentation
 279 │ 278 │             $docs = Get-TestDocumentation -TestName $test.Name -TestFile $testFile.FullName
 280 │ 279 │
 281 │ 280 │             # Get remediation URL
 282 │ 281 │             $remediationUrl = Get-RemediationUrl -TestName $test.Name -TestType $testType
 283 │ 282 │
 284 │ 283 │             # Extract test ID from name
 285 │ 284 │             $testId = if ($test.Name -match '^(ORCA\.\d+|CISA\.MS\.EXO\.\d+\.\d+|CIS\.\d+\.\d+\.\d+)') {
 286 │ 285 │                 $matches[1]
 287 │ 286 │             } else {
 288 │ 287 │                 $test.Name -split ':' | Select-Object -First 1
 289 │ 288 │             }
 290 │ 289 │
 291 │ 290 │             # Build detailed output
 292 │ 291 │             $detailOutput = ""
 293 │ 292 │             if ($test.Result -eq 'Failed' -and $test.ErrorRecord) {
 294 │ 293 │                 $detailOutput = $test.ErrorRecord.Exception.Message
 295 │ 294 │             } elseif ($test.Result -eq 'Skipped' -and $test.SkippedBecause) {
 296 │ 295 │                 $detailOutput = "Skipped: $($test.SkippedBecause)"
 297 │ 296 │             }
 298 │ 297 │
 299 │ 298 │             # Create detailed record
 300 │ 299 │             $record = [PSCustomObject]@{
 301 │ 300 │                 TestFile = $testFile.Name
 302 │ 301 │                 TestType = $testType
 303 │ 302 │                 TestId = $testId
 304 │ 303 │                 TestName = $test.Name
 305 │ 304 │                 Result = $test.Result
 306 │ 305 │                 Executed = $test.Executed
 307 │ 306 │                 Duration = if ($test.Duration) { "{0:N2}" -f $test.Duration.TotalSeconds } else { "0.00" }
 308 │ 307 │
 309 │ 308 │                 # Test details
 310 │ 309 │                 Description = $docs.Description
 311 │ 310 │                 Rationale = $docs.Rationale
 312 │ 311 │                 Impact = $docs.Impact
 313 │ 312 │                 DefaultValue = $docs.DefaultValue
 314 │ 313 │
 315 │ 314 │                 # Result details
 316 │ 315 │                 ResultDetail = $detailOutput
 317 │ 316 │                 ErrorMessage = if ($test.ErrorRecord) { $test.ErrorRecord.Exception.Message } else { "" }
 318 │ 317 │
 319 │ 318 │                 # Remediation
 320 │ 319 │                 HowToFix = $docs.HowToFix
 321 │ 320 │                 RemediationUrl = $remediationUrl
 322 │ 321 │                 References = $docs.References
 323 │ 322 │
 324 │ 323 │                 # Additional context
 325 │ 324 │                 Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 326 │ 325 │             }
 327 │ 326 │
 328 │ 327 │             $detailedResults += $record
 329 │ 328 │         }
 330 │ 329 │     }
 331 │ 330 │     catch {
 332 │ 331 │         Write-Warning "Error processing $($testFile.Name): $_"
 333 │ 332 │     }
 334 │ 333 │ }
 335 │ 334 │
 336 │ 335 │ Write-Progress -Activity "Running Exchange Online Security Tests" -Completed
 337 │ 336 │
 338 │ 337 │ $testEndTime = Get-Date
 339 │ 338 │ $testDuration = $testEndTime - $testStartTime
 340 │ 339 │
 341 │ 340 │ # Generate timestamp for file names
 342 │ 341 │ $reportTimestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
 343 │ 342 │
 344 │ 343 │ # Create comprehensive CSV export
 345 │ 344 │ Write-Host "`nGenerating reports..." -ForegroundColor Yellow
 346 │ 345 │
 347 │ 346 │ $csvPath = Join-Path $reportsPath "ExchangeHealthCheck-Results-$reportTimestamp.csv"
 348 │ 347 │
 349 │ 348 │ # Filter results based on parameter
 350 │ 349 │ $exportResults = if ($IncludePassedDetails) {
 351 │ 350 │     $detailedResults
 352 │ 351 │ } else {
 353 │ 352 │     # For passed tests, include minimal details
 354 │ 353 │     $detailedResults | ForEach-Object {
 355 │ 354 │         if ($_.Result -in @('Failed', 'Skipped')) {
 356 │ 355 │             $_
 357 │ 356 │         } else {
 358 │ 357 │             # Create simplified record for passed tests
 359 │ 358 │             [PSCustomObject]@{
 360 │ 359 │                 TestFile = $_.TestFile
 361 │ 360 │                 TestType = $_.TestType
 362 │ 361 │                 TestId = $_.TestId
 363 │ 362 │                 TestName = $_.TestName
 364 │ 363 │                 Result = $_.Result
 365 │ 364 │                 Executed = $_.Executed
 366 │ 365 │                 Duration = $_.Duration
 367 │ 366 │                 Description = $_.Description
 368 │ 367 │                 Rationale = ""
 369 │ 368 │                 Impact = ""
 370 │ 369 │                 DefaultValue = ""
 371 │ 370 │                 ResultDetail = "Test passed successfully"
 372 │ 371 │                 ErrorMessage = ""
 373 │ 372 │                 HowToFix = ""
 374 │ 373 │                 RemediationUrl = ""
 375 │ 374 │                 References = ""
 376 │ 375 │                 Timestamp = $_.Timestamp
 377 │ 376 │             }
 378 │ 377 │         }
 379 │ 378 │     }
 380 │ 379 │ }
 381 │ 380 │
 382 │ 381 │ $exportResults | Export-Csv -Path $csvPath -NoTypeInformation
 383 │ 382 │ Write-Host "Detailed results exported to:

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

 383 │ 382 │ $(Split-Path $csvPath -Leaf)" -ForegroundColor Green
 384 │ 383 │
 385 │ 384 │ # Create executive summary
 386 │ 385 │ $summaryPath = Join-Path $reportsPath "ExchangeHealthCheck-Summary-$reportTimestamp.csv"
 387 │ 386 │
 388 │ 387 │ $summary = $detailedResults | Group-Object TestType, Result | ForEach-Object {
 389 │ 388 │     $parts = $_.Name -split ', '
 390 │ 389 │     [PSCustomObject]@{
 391 │ 390 │         TestType = $parts[0]
 392 │ 391 │         Result = $parts[1]
 393 │ 392 │         Count = $_.Count
 394 │ 393 │         TestIds = ($_.Group.TestId | Sort-Object -Unique) -join '; '
 395 │ 394 │     }
 396 │ 395 │ } | Sort-Object TestType, Result
 397 │ 396 │
 398 │ 397 │ $summary | Export-Csv -Path $summaryPath -NoTypeInformation
 399 │ 398 │ Write-Host "Executive summary exported to:

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

 404 │ 403 │ $failedTests = $detailedResults | Where-Object { $_.Result -eq 'Failed' } | Sort-Object TestType, TestId
 405 │ 404 │
 406 │ 405 │ # Calculate statistics
 407 │ 406 │ $stats = @{
 408 │ 407 │     Total = $detailedResults.Count
 409 │ 408 │     Passed = ($detailedResults | Where-Object {$_.Result -eq 'Passed'}).Count
 410 │ 409 │     Failed = ($detailedResults | Where-Object {$_.Result -eq 'Failed'}).Count
 411 │ 410 │     Skipped = ($detailedResults | Where-Object {$_.Result -eq 'Skipped'}).Count
 412 │ 411 │ }
 413 │ 412 │
 414 │ 413 │ # Create text report content

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

 481 │ 547 │  -Leaf)" -ForegroundColor Green
 482 │ 548 │
 483 │ 549 │ # Display summary
 484 │ 550 │ Write-Host "`n" + ("="*60) -ForegroundColor Cyan
 485 │ 551 │ Write-Host "SECURITY HEALTH CHECK COMPLETED" -ForegroundColor Cyan
 486 │ 552 │ Write-Host ("="*60) -ForegroundColor Cyan
 487 │ 553 │
 488 │ 554 │ Write-Host "`nTest Results Summary:" -ForegroundColor Yellow
 489 │ 555 │ Write-Host "  Total Tests Run: $($stats.Total)" -ForegroundColor White
 490 │ 556 │ Write-Host "  Passed: $($stats.Passed)" -ForegroundColor Green
 491 │ 557 │ Write-Host "  Failed: $($stats.Failed)" -ForegroundColor Red
 492 │ 558 │ Write-Host "  Skipped: $($stats.Skipped)" -ForegroundColor Yellow
 493 │ 559 │
 494 │ 560 │ Write-Host

Found 7 clones.
Error: ERROR: jscpd found too many duplicates (31.31%) over threshold (30%)
    at ThresholdReporter.report (/node_modules/@jscpd/finder/dist/index.js:615:13)
    at /node_modules/@jscpd/finder/dist/index.js:109:18
    at Array.forEach (<anonymous>)
    at /node_modules/@jscpd/finder/dist/index.js:108:22
    at async /node_modules/jscpd/dist/bin/jscpd.js:9:5ERROR: jscpd found too many duplicates (31.31%) over threshold (30%)

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSCPD Fail ❌
JSON Pass ✅
JSON_PRETTIER Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

Super-linter detected linting errors

For more information, see the GitHub Actions workflow run

Powered by Super-linter

JSCPD
Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [39:1 - 60:55] (21 lines, 114 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [38:1 - 59:42]

 39 │ 38 │ [CmdletBinding()]
 40 │ 39 │ param(
 41 │ 40 │     [switch]$SkipConnection,
 42 │ 41 │     [switch]$IncludePassedDetails
 43 │ 42 │ )
 44 │ 43 │
 45 │ 44 │ # Script configuration
 46 │ 45 │ $ErrorActionPreference = 'Stop'
 47 │ 46 │ $ProgressPreference = 'Continue'
 48 │ 47 │
 49 │ 48 │ # Set up paths
 50 │ 49 │ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
 51 │ 50 │ $reportsPath = Join-Path $scriptPath "Reports"
 52 │ 51 │
 53 │ 52 │ # Ensure Reports directory exists
 54 │ 53 │ if (!(Test-Path $reportsPath)) {
 55 │ 54 │     New-Item -ItemType Directory -Path $reportsPath -Force | Out-Null
 56 │ 55 │     Write-Host "Created Reports directory: $reportsPath" -ForegroundColor Green
 57 │ 56 │ }
 58 │ 57 │
 59 │ 58 │ # Import required modules
 60 │ 59 │ Write-Host "`nExchange Online Security Health Check (Simplified)"

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [81:73 - 113:22] (32 lines, 214 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [80:40 - 112:24]

 81  │ 80  │ $_"
 82  │ 81  │         exit 1
 83  │ 82  │     }
 84  │ 83  │ }
 85  │ 84  │
 86  │ 85  │ # Verify connection
 87  │ 86  │ $connectionInfo = Get-ConnectionInformation | Where-Object { $_.Name -like '*ExchangeOnline*' }
 88  │ 87  │ if (-not $connectionInfo) {
 89  │ 88  │     Write-Error "No active Exchange Online connection found. Please connect first or remove -SkipConnection parameter."
 90  │ 89  │     exit 1
 91  │ 90  │ }
 92  │ 91  │
 93  │ 92  │ Write-Host "`nConnection Details:" -ForegroundColor Cyan
 94  │ 93  │ Write-Host "  Tenant: $($connectionInfo.TenantId)" -ForegroundColor White
 95  │ 94  │ Write-Host "  User: $($connectionInfo.UserPrincipalName)" -ForegroundColor White
 96  │ 95  │ Write-Host "  Environment: $($connectionInfo.ConnectionUri)" -ForegroundColor White
 97  │ 96  │
 98  │ 97  │ # Get Maester module path
 99  │ 98  │ $maesterModule = Get-Module -ListAvailable Maester | Select-Object -First 1
 100 │ 99  │ if (!$maesterModule) {
 101 │ 100 │     Write-Error "Maester module not found. Please install it first: Install-Module Maester"
 102 │ 101 │     exit 1
 103 │ 102 │ }
 104 │ 103 │
 105 │ 104 │ $maesterPath = Split-Path $maesterModule.Path
 106 │ 105 │ $testsPath = Join-Path $maesterPath "maester-tests"
 107 │ 106 │
 108 │ 107 │ Write-Host "`nSearching for Exchange Online tests..." -ForegroundColor Yellow
 109 │ 108 │
 110 │ 109 │ # Find all test files with Exchange-related tags
 111 │ 110 │ $exoTestFiles = @()
 112 │ 111 │ $testPatterns = @(
 113 │ 112 │     '-Tag.*["'']EXO["'']'

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [114:9 - 142:8] (28 lines, 243 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [113:9 - 141:10]

 114 │ 113 │ # CISA tests use "MS.EXO"
 115 │ 114 │ )
 116 │ 115 │
 117 │ 116 │ # Search in relevant directories
 118 │ 117 │ $searchDirs = Get-ChildItem -Path $testsPath -Directory -Recurse -ErrorAction SilentlyContinue |
 119 │ 118 │     Where-Object {
 120 │ 119 │         $_.Name -in @('exchange', 'orca', 'cis', 'cisa') -or
 121 │ 120 │         $_.FullName -match 'exchange|exo'
 122 │ 121 │     }
 123 │ 122 │
 124 │ 123 │ foreach ($dir in $searchDirs) {
 125 │ 124 │     $files = Get-ChildItem -Path $dir.FullName -Filter "*.Tests.ps1" -File -ErrorAction SilentlyContinue
 126 │ 125 │     foreach ($file in $files) {
 127 │ 126 │         $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
 128 │ 127 │         if ($content) {
 129 │ 128 │             foreach ($pattern in $testPatterns) {
 130 │ 129 │                 if ($content -match $pattern) {
 131 │ 130 │                     if ($file.FullName -notin $exoTestFiles.FullName) {
 132 │ 131 │                         $exoTestFiles += $file
 133 │ 132 │                     }
 134 │ 133 │                     break
 135 │ 134 │                 }
 136 │ 135 │             }
 137 │ 136 │         }
 138 │ 137 │     }
 139 │ 138 │ }
 140 │ 139 │
 141 │ 140 │ $exoTestFiles = $exoTestFiles | Sort-Object DirectoryName, Name
 142 │ 141 │ Write-Host "Found

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [142:8 - 383:32] (241 lines, 1890 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [141:10 - 382:34]

 142 │ 141 │ $($exoTestFiles.Count) Exchange Online test files" -ForegroundColor Green
 143 │ 142 │
 144 │ 143 │ # Group by test type for reporting
 145 │ 144 │ $fileGroups = $exoTestFiles | Group-Object {
 146 │ 145 │     if ($_.DirectoryName -match 'cisa') { 'CISA' }
 147 │ 146 │     elseif ($_.DirectoryName -match 'orca') { 'ORCA' }
 148 │ 147 │     elseif ($_.DirectoryName -match 'cis') { 'CIS' }
 149 │ 148 │     else { 'Other' }
 150 │ 149 │ }
 151 │ 150 │
 152 │ 151 │ Write-Host "`nTest Distribution:" -ForegroundColor Cyan
 153 │ 152 │ foreach ($group in $fileGroups | Sort-Object Name) {
 154 │ 153 │     Write-Host "  $($group.Name): $($group.Count) test files" -ForegroundColor White
 155 │ 154 │ }
 156 │ 155 │
 157 │ 156 │ # Helper function to get test documentation
 158 │ 157 │ function Get-TestDocumentation {
 159 │ 158 │     param(
 160 │ 159 │         [string]$TestName,
 161 │ 160 │         [string]$TestFile
 162 │ 161 │     )
 163 │ 162 │
 164 │ 163 │     $documentation = @{
 165 │ 164 │         Description = ""
 166 │ 165 │         HowToFix = ""
 167 │ 166 │         References = ""
 168 │ 167 │         Impact = ""
 169 │ 168 │         DefaultValue = ""
 170 │ 169 │         Rationale = ""
 171 │ 170 │     }
 172 │ 171 │
 173 │ 172 │     # Try to find corresponding .ps1 file in public folder
 174 │ 173 │     $publicPath = $TestFile -replace 'maester-tests', 'public' -replace '\.Tests\.ps1$', '.ps1'
 175 │ 174 │     if (Test-Path $publicPath) {
 176 │ 175 │         $cmdContent = Get-Content $publicPath -Raw
 177 │ 176 │
 178 │ 177 │         # Extract description from .SYNOPSIS
 179 │ 178 │         if ($cmdContent -match '\.SYNOPSIS\s*\n\s*(.+?)(?=\n\s*\.|#>)') {
 180 │ 179 │             $documentation.Description = $matches[1].Trim()
 181 │ 180 │         }
 182 │ 181 │
 183 │ 182 │         # Extract description from .DESCRIPTION
 184 │ 183 │         if ($cmdContent -match '\.DESCRIPTION\s*\n([\s\S]+?)(?=\n\s*\.|#>)') {
 185 │ 184 │             $documentation.Rationale = $matches[1].Trim() -replace '\s+', ' '
 186 │ 185 │         }
 187 │ 186 │     }
 188 │ 187 │
 189 │ 188 │     # Try to find corresponding .md file
 190 │ 189 │     $mdPath = $publicPath -replace '\.ps1$', '.md'
 191 │ 190 │     if (Test-Path $mdPath) {
 192 │ 191 │         $mdContent = Get-Content $mdPath -Raw
 193 │ 192 │
 194 │ 193 │         # Extract sections from markdown
 195 │ 194 │         if ($mdContent -match '## Description\s*\n([\s\S]+?)(?=\n##|\Z)') {
 196 │ 195 │             $documentation.Description = $matches[1].Trim()
 197 │ 196 │         }
 198 │ 197 │         if ($mdContent -match '## How to fix\s*\n([\s\S]+?)(?=\n##|\Z)') {
 199 │ 198 │             $documentation.HowToFix = $matches[1].Trim()
 200 │ 199 │         }
 201 │ 200 │         if ($mdContent -match '## References?\s*\n([\s\S]+?)(?=\n##|\Z)') {
 202 │ 201 │             $documentation.References = $matches[1].Trim()
 203 │ 202 │         }
 204 │ 203 │         if ($mdContent -match '## Impact\s*\n([\s\S]+?)(?=\n##|\Z)') {
 205 │ 204 │             $documentation.Impact = $matches[1].Trim()
 206 │ 205 │         }
 207 │ 206 │         if ($mdContent -match '## Default Value\s*\n([\s\S]+?)(?=\n##|\Z)') {
 208 │ 207 │             $documentation.DefaultValue = $matches[1].Trim()
 209 │ 208 │         }
 210 │ 209 │     }
 211 │ 210 │
 212 │ 211 │     return $documentation
 213 │ 212 │ }
 214 │ 213 │
 215 │ 214 │ # Helper function to get remediation URL
 216 │ 215 │ function Get-RemediationUrl {
 217 │ 216 │     param(
 218 │ 217 │         [string]$TestName,
 219 │ 218 │         [string]$TestType
 220 │ 219 │     )
 221 │ 220 │
 222 │ 221 │     $baseUrl = "https://security.microsoft.com"
 223 │ 222 │
 224 │ 223 │     switch -Regex ($TestName) {
 225 │ 224 │         'Spam|AntiSpam' { return "$baseUrl/antispam" }
 226 │ 225 │         'Phish|AntiPhish' { return "$baseUrl/antiphishing" }
 227 │ 226 │         'Safe.*Link' { return "$baseUrl/safelinks" }
 228 │ 227 │         'Safe.*Attach' { return "$baseUrl/safeattachments" }
 229 │ 228 │         'Malware' { return "$baseUrl/antimalware" }
 230 │ 229 │         'DKIM|SPF|DMARC' { return "$baseUrl/authentication" }
 231 │ 230 │         'Audit' { return "https://compliance.microsoft.com/auditlogsearch" }
 232 │ 231 │         'DLP' { return "https://compliance.microsoft.com/datalossprevention" }
 233 │ 232 │         'Calendar|Sharing' { return "https://admin.exchange.microsoft.com/#/organizationconfig" }
 234 │ 233 │         'External.*Forward' { return "$baseUrl/antispam" }
 235 │ 234 │         'Outbound.*Spam' { return "$baseUrl/antispam" }
 236 │ 235 │         default { return "https://admin.microsoft.com" }
 237 │ 236 │     }
 238 │ 237 │ }
 239 │ 238 │
 240 │ 239 │ # Initialize results collection
 241 │ 240 │ $detailedResults = @()
 242 │ 241 │ $testStartTime = Get-Date
 243 │ 242 │
 244 │ 243 │ Write-Host "`nRunning Exchange Online security tests..." -ForegroundColor Yellow
 245 │ 244 │ Write-Host "This may take several minutes..." -ForegroundColor DarkGray
 246 │ 245 │
 247 │ 246 │ # Process each test file
 248 │ 247 │ $fileCount = 0
 249 │ 248 │ $totalTestCount = 0
 250 │ 249 │
 251 │ 250 │ foreach ($testFile in $exoTestFiles) {
 252 │ 251 │     $fileCount++
 253 │ 252 │     $percentComplete = [math]::Round(($fileCount / $exoTestFiles.Count) * 100, 0)
 254 │ 253 │     Write-Progress -Activity "Running Exchange Online Security Tests" -Status "Processing $($testFile.Name)" -PercentComplete $percentComplete
 255 │ 254 │
 256 │ 255 │     try {
 257 │ 256 │         # Run the test file
 258 │ 257 │         $container = New-PesterContainer -Path $testFile.FullName
 259 │ 258 │
 260 │ 259 │         $pesterConfig = New-PesterConfiguration
 261 │ 260 │         $pesterConfig.Run.Container = $container
 262 │ 261 │         $pesterConfig.Run.PassThru = $true
 263 │ 262 │         $pesterConfig.Output.Verbosity = 'None'
 264 │ 263 │         $pesterConfig.TestResult.Enabled = $true
 265 │ 264 │
 266 │ 265 │         # Run tests
 267 │ 266 │         $testOutput = Invoke-Pester -Configuration $pesterConfig
 268 │ 267 │
 269 │ 268 │         foreach ($test in $testOutput.Tests) {
 270 │ 269 │             $totalTestCount++
 271 │ 270 │
 272 │ 271 │             # Determine test metadata
 273 │ 272 │             $testType = if ($testFile.DirectoryName -match 'cisa') { 'CISA' }
 274 │ 273 │                        elseif ($testFile.DirectoryName -match 'orca') { 'ORCA' }
 275 │ 274 │                        elseif ($testFile.DirectoryName -match 'cis') { 'CIS' }
 276 │ 275 │                        else { 'Other' }
 277 │ 276 │
 278 │ 277 │             # Get test documentation
 279 │ 278 │             $docs = Get-TestDocumentation -TestName $test.Name -TestFile $testFile.FullName
 280 │ 279 │
 281 │ 280 │             # Get remediation URL
 282 │ 281 │             $remediationUrl = Get-RemediationUrl -TestName $test.Name -TestType $testType
 283 │ 282 │
 284 │ 283 │             # Extract test ID from name
 285 │ 284 │             $testId = if ($test.Name -match '^(ORCA\.\d+|CISA\.MS\.EXO\.\d+\.\d+|CIS\.\d+\.\d+\.\d+)') {
 286 │ 285 │                 $matches[1]
 287 │ 286 │             } else {
 288 │ 287 │                 $test.Name -split ':' | Select-Object -First 1
 289 │ 288 │             }
 290 │ 289 │
 291 │ 290 │             # Build detailed output
 292 │ 291 │             $detailOutput = ""
 293 │ 292 │             if ($test.Result -eq 'Failed' -and $test.ErrorRecord) {
 294 │ 293 │                 $detailOutput = $test.ErrorRecord.Exception.Message
 295 │ 294 │             } elseif ($test.Result -eq 'Skipped' -and $test.SkippedBecause) {
 296 │ 295 │                 $detailOutput = "Skipped: $($test.SkippedBecause)"
 297 │ 296 │             }
 298 │ 297 │
 299 │ 298 │             # Create detailed record
 300 │ 299 │             $record = [PSCustomObject]@{
 301 │ 300 │                 TestFile = $testFile.Name
 302 │ 301 │                 TestType = $testType
 303 │ 302 │                 TestId = $testId
 304 │ 303 │                 TestName = $test.Name
 305 │ 304 │                 Result = $test.Result
 306 │ 305 │                 Executed = $test.Executed
 307 │ 306 │                 Duration = if ($test.Duration) { "{0:N2}" -f $test.Duration.TotalSeconds } else { "0.00" }
 308 │ 307 │
 309 │ 308 │                 # Test details
 310 │ 309 │                 Description = $docs.Description
 311 │ 310 │                 Rationale = $docs.Rationale
 312 │ 311 │                 Impact = $docs.Impact
 313 │ 312 │                 DefaultValue = $docs.DefaultValue
 314 │ 313 │
 315 │ 314 │                 # Result details
 316 │ 315 │                 ResultDetail = $detailOutput
 317 │ 316 │                 ErrorMessage = if ($test.ErrorRecord) { $test.ErrorRecord.Exception.Message } else { "" }
 318 │ 317 │
 319 │ 318 │                 # Remediation
 320 │ 319 │                 HowToFix = $docs.HowToFix
 321 │ 320 │                 RemediationUrl = $remediationUrl
 322 │ 321 │                 References = $docs.References
 323 │ 322 │
 324 │ 323 │                 # Additional context
 325 │ 324 │                 Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
 326 │ 325 │             }
 327 │ 326 │
 328 │ 327 │             $detailedResults += $record
 329 │ 328 │         }
 330 │ 329 │     }
 331 │ 330 │     catch {
 332 │ 331 │         Write-Warning "Error processing $($testFile.Name): $_"
 333 │ 332 │     }
 334 │ 333 │ }
 335 │ 334 │
 336 │ 335 │ Write-Progress -Activity "Running Exchange Online Security Tests" -Completed
 337 │ 336 │
 338 │ 337 │ $testEndTime = Get-Date
 339 │ 338 │ $testDuration = $testEndTime - $testStartTime
 340 │ 339 │
 341 │ 340 │ # Generate timestamp for file names
 342 │ 341 │ $reportTimestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
 343 │ 342 │
 344 │ 343 │ # Create comprehensive CSV export
 345 │ 344 │ Write-Host "`nGenerating reports..." -ForegroundColor Yellow
 346 │ 345 │
 347 │ 346 │ $csvPath = Join-Path $reportsPath "ExchangeHealthCheck-Results-$reportTimestamp.csv"
 348 │ 347 │
 349 │ 348 │ # Filter results based on parameter
 350 │ 349 │ $exportResults = if ($IncludePassedDetails) {
 351 │ 350 │     $detailedResults
 352 │ 351 │ } else {
 353 │ 352 │     # For passed tests, include minimal details
 354 │ 353 │     $detailedResults | ForEach-Object {
 355 │ 354 │         if ($_.Result -in @('Failed', 'Skipped')) {
 356 │ 355 │             $_
 357 │ 356 │         } else {
 358 │ 357 │             # Create simplified record for passed tests
 359 │ 358 │             [PSCustomObject]@{
 360 │ 359 │                 TestFile = $_.TestFile
 361 │ 360 │                 TestType = $_.TestType
 362 │ 361 │                 TestId = $_.TestId
 363 │ 362 │                 TestName = $_.TestName
 364 │ 363 │                 Result = $_.Result
 365 │ 364 │                 Executed = $_.Executed
 366 │ 365 │                 Duration = $_.Duration
 367 │ 366 │                 Description = $_.Description
 368 │ 367 │                 Rationale = ""
 369 │ 368 │                 Impact = ""
 370 │ 369 │                 DefaultValue = ""
 371 │ 370 │                 ResultDetail = "Test passed successfully"
 372 │ 371 │                 ErrorMessage = ""
 373 │ 372 │                 HowToFix = ""
 374 │ 373 │                 RemediationUrl = ""
 375 │ 374 │                 References = ""
 376 │ 375 │                 Timestamp = $_.Timestamp
 377 │ 376 │             }
 378 │ 377 │         }
 379 │ 378 │     }
 380 │ 379 │ }
 381 │ 380 │
 382 │ 381 │ $exportResults | Export-Csv -Path $csvPath -NoTypeInformation
 383 │ 382 │ Write-Host "Detailed results exported to:

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [383:32 - 399:33] (16 lines, 156 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [382:34 - 398:35]

 383 │ 382 │ $(Split-Path $csvPath -Leaf)" -ForegroundColor Green
 384 │ 383 │
 385 │ 384 │ # Create executive summary
 386 │ 385 │ $summaryPath = Join-Path $reportsPath "ExchangeHealthCheck-Summary-$reportTimestamp.csv"
 387 │ 386 │
 388 │ 387 │ $summary = $detailedResults | Group-Object TestType, Result | ForEach-Object {
 389 │ 388 │     $parts = $_.Name -split ', '
 390 │ 389 │     [PSCustomObject]@{
 391 │ 390 │         TestType = $parts[0]
 392 │ 391 │         Result = $parts[1]
 393 │ 392 │         Count = $_.Count
 394 │ 393 │         TestIds = ($_.Group.TestId | Sort-Object -Unique) -join '; '
 395 │ 394 │     }
 396 │ 395 │ } | Sort-Object TestType, Result
 397 │ 396 │
 398 │ 397 │ $summary | Export-Csv -Path $summaryPath -NoTypeInformation
 399 │ 398 │ Write-Host "Executive summary exported to:

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [404:1 - 414:29] (10 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [403:1 - 413:72]

 404 │ 403 │ $failedTests = $detailedResults | Where-Object { $_.Result -eq 'Failed' } | Sort-Object TestType, TestId
 405 │ 404 │
 406 │ 405 │ # Calculate statistics
 407 │ 406 │ $stats = @{
 408 │ 407 │     Total = $detailedResults.Count
 409 │ 408 │     Passed = ($detailedResults | Where-Object {$_.Result -eq 'Passed'}).Count
 410 │ 409 │     Failed = ($detailedResults | Where-Object {$_.Result -eq 'Failed'}).Count
 411 │ 410 │     Skipped = ($detailedResults | Where-Object {$_.Result -eq 'Skipped'}).Count
 412 │ 411 │ }
 413 │ 412 │
 414 │ 413 │ # Create text report content

Clone found (powershell):
 - Run-MaesterExchangeHealthCheck-Simple.ps1 [481:18 - 494:11] (13 lines, 128 tokens)
   Run-MaesterExchangeHealthCheck.ps1 [547:10 - 560:17]

 481 │ 547 │  -Leaf)" -ForegroundColor Green
 482 │ 548 │
 483 │ 549 │ # Display summary
 484 │ 550 │ Write-Host "`n" + ("="*60) -ForegroundColor Cyan
 485 │ 551 │ Write-Host "SECURITY HEALTH CHECK COMPLETED" -ForegroundColor Cyan
 486 │ 552 │ Write-Host ("="*60) -ForegroundColor Cyan
 487 │ 553 │
 488 │ 554 │ Write-Host "`nTest Results Summary:" -ForegroundColor Yellow
 489 │ 555 │ Write-Host "  Total Tests Run: $($stats.Total)" -ForegroundColor White
 490 │ 556 │ Write-Host "  Passed: $($stats.Passed)" -ForegroundColor Green
 491 │ 557 │ Write-Host "  Failed: $($stats.Failed)" -ForegroundColor Red
 492 │ 558 │ Write-Host "  Skipped: $($stats.Skipped)" -ForegroundColor Yellow
 493 │ 559 │
 494 │ 560 │ Write-Host

Found 7 clones.
Error: ERROR: jscpd found too many duplicates (31.26%) over threshold (30%)
    at ThresholdReporter.report (/node_modules/@jscpd/finder/dist/index.js:615:13)
    at /node_modules/@jscpd/finder/dist/index.js:109:18
    at Array.forEach (<anonymous>)
    at /node_modules/@jscpd/finder/dist/index.js:108:22
    at async /node_modules/jscpd/dist/bin/jscpd.js:9:5ERROR: jscpd found too many duplicates (31.26%) over threshold (30%)

The default 10% threshold catches intentional duplication in test/runbook
templates. Raising to 30% with explicit ignore patterns for tests/.
… SHA

- Top-level permissions reduced to 'contents: read' (least-privilege).
- Job-level adds 'statuses: write' + 'pull-requests: write' for super-linter.
- Bumped LukeEvansTech/shared-workflows pin to current v1 SHA.

Resolves zizmor excessive-permissions on the lint.yml caller.
The pinned SHA (57fff6de) predates the 2026-05-07 JSCPD decision, so this
branch was still running a linter that shared-workflows now sets to
VALIDATE_JSCPD: false. JSCPD was the only failing check on the PR.

Repointed at 3771e1f9, the SHA already in use across the estate, and removed
.github/linters/.jscpd.json — with JSCPD disabled centrally the config file
has nothing to configure.

Claude-Session: https://claude.ai/code/session_011TnDL1H3UpksfZa17pUukz
@LukeEvansTech
LukeEvansTech force-pushed the chore/add-super-linter branch from 4b54d7b to e9b73c4 Compare August 17, 2026 08:15
@github-actions

Copy link
Copy Markdown

Super-linter summary

Language Validation result
GITHUB_ACTIONS Pass ✅
GITHUB_ACTIONS_ZIZMOR Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
YAML Pass ✅
YAML_PRETTIER Pass ✅

All files and directories linted successfully

For more information, see the GitHub Actions workflow run

Powered by Super-linter

@LukeEvansTech
LukeEvansTech merged commit 6e4db07 into main Aug 17, 2026
9 checks passed
@LukeEvansTech
LukeEvansTech deleted the chore/add-super-linter branch August 17, 2026 08:18
LukeEvansTech added a commit that referenced this pull request Aug 18, 2026
Clears the Mend repository problem on this repo:

> ⚠️ Hidden Unicode characters have been discovered in file(s) in your
repository. … Please confirm that they are intended to be there, as they
could be an attempt to "smuggle" text into your codebase, or used to
confuse tools like Renovate or Large Language Models (LLMs)

## What it was — benign, but worth removing
A **single U+200B (zero-width space)** at `test_results.csv:2865`,
trailing the end of an Entra recommendation description:

```text
…enhancing overall security posture.<U+200B>",High,Maester|Entra|Security|…
```

That is a copy-paste artifact from the Microsoft source text, not
anything hostile. A full scan (zero-width, BOM, bidi overrides, soft
hyphen, word-joiner ranges) found **no other invisible characters**
anywhere in the repository.

Renovate's warning is still the right call — an invisible character in a
committed file is exactly the shape of a text-smuggling attempt, and it
is worth keeping the detector meaningful rather than muting it.

## How
Removed **binary-safely** so the file's CRLF line endings survive — a
naive text-mode rewrite re-wrote all 4,125 lines. The diff is one line;
340,619 → 340,616 bytes.

Worth a separate decision: `test_results.csv` is a 4,127-line Maester
**run artifact** committed to the repo (added in #1) and nothing in-repo
generates it. If it is meant to be a snapshot, fine; if not, it may
belong in `.gitignore` / a workflow artifact instead. Not changed here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant