Skip to content

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

Open
LukeEvansTech wants to merge 3 commits into
mainfrom
chore/add-super-linter
Open

ci: add super-linter (soft launch)#1
LukeEvansTech wants to merge 3 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
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

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
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

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
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

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
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

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
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

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%)

The default 10% threshold catches intentional duplication in test/runbook
templates. Raising to 30% with explicit ignore patterns for tests/.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

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%)

… 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.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

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%)

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