Private/Scoring.ps1

function Invoke-M365SnapshotScoring {
    param(
        [Parameter(Mandatory=$true)]
        [string]$ModuleRoot,

        [AllowNull()]
        [object[]]$Groups = @(),

        [AllowNull()]
        [object[]]$Sites = @(),

        [AllowNull()]
        [object[]]$AppRegistrations = @(),

        [Parameter(Mandatory=$false)]
        [switch]$IncludeAppRegistrations,

        [AllowNull()]
        [object[]]$SitesWithoutSensitivityLabel = @(),

        [AllowNull()]
        [object[]]$SitesWithEveryoneSharing = @(),

        [AllowNull()]
        [object[]]$SitesWithAnonymousLinks = @(),

        [AllowNull()]
        [object[]]$SitesWithExternalUsers = @(),

        [AllowNull()]
        [object[]]$SitesWithExternalAccess = @(),

        [AllowNull()]
        [object[]]$SitesWithAdHocPermissions = @(),

        [AllowNull()]
        [object[]]$DisabledWithLicenses = @(),

        [AllowNull()]
        [object[]]$DisabledUsers = @(),

        [AllowNull()]
        [object[]]$AppsWithSecretRisk = @(),

        [AllowNull()]
        [object[]]$UnusedAppRegistrations = @(),

        [AllowNull()]
        [object[]]$SensitiveAppRegistrations = @()
    )

    if ($null -eq $Groups) { $Groups = @() }
    if ($null -eq $Sites) { $Sites = @() }
    if ($null -eq $AppRegistrations) { $AppRegistrations = @() }
    if ($null -eq $SitesWithoutSensitivityLabel) { $SitesWithoutSensitivityLabel = @() }
    if ($null -eq $SitesWithEveryoneSharing) { $SitesWithEveryoneSharing = @() }
    if ($null -eq $SitesWithAnonymousLinks) { $SitesWithAnonymousLinks = @() }
    if ($null -eq $SitesWithExternalUsers) { $SitesWithExternalUsers = @() }
    if ($null -eq $SitesWithExternalAccess) { $SitesWithExternalAccess = @() }
    if ($null -eq $SitesWithAdHocPermissions) { $SitesWithAdHocPermissions = @() }
    if ($null -eq $DisabledWithLicenses) { $DisabledWithLicenses = @() }
    if ($null -eq $DisabledUsers) { $DisabledUsers = @() }
    if ($null -eq $AppsWithSecretRisk) { $AppsWithSecretRisk = @() }
    if ($null -eq $UnusedAppRegistrations) { $UnusedAppRegistrations = @() }
    if ($null -eq $SensitiveAppRegistrations) { $SensitiveAppRegistrations = @() }

    function Get-ScorePercentage {
        param(
            [double]$Numerator,
            [double]$Denominator
        )

        if ($Denominator -le 0) {
            return 0
        }

        return [Math]::Min(100, [Math]::Round(($Numerator / $Denominator) * 100, 0))
    }

    $scoringConfigPath = Join-Path $ModuleRoot 'scoring-rules.json'
    $scoringConfig = $null
    if (Test-Path $scoringConfigPath) {
        try {
            $scoringConfig = Get-Content -Path $scoringConfigPath -Raw | ConvertFrom-Json
        } catch {
            Write-Warning "Failed to parse scoring rules at $scoringConfigPath. Falling back to defaults."
        }
    } else {
        Write-Warning "Scoring rules not found at $scoringConfigPath. Falling back to defaults."
    }

    if (-not $scoringConfig) {
        $defaultScoringRulesJson = @'
{
    "description": "M365 Security Assessment Score - Scoring Rules Configuration",
  "baseScore": 100,
  "rules": [
    {
      "id": "sensitivity-labels",
      "name": "Missing Sensitivity Labels",
      "description": "Sites without information protection labels applied",
      "enabled": true,
      "maxPenalty": 15,
      "calculation": "percentage-based",
      "threshold": 50,
      "message": "Missing sensitivity labels on {percentage}% of sites: -{deduction} points"
    },
    {
      "id": "anonymous-sharing",
      "name": "Anonymous Sharing Enabled",
      "description": "Sites that allow anonymous access (anyone with link, no sign-in required)",
      "enabled": true,
      "maxPenalty": 20,
      "calculation": "flat",
      "threshold": 1,
      "message": "Sites with anonymous sharing enabled ({count}): -{deduction} points"
    },
    {
      "id": "anonymous-links",
      "name": "Active Anonymous Sharing Links",
      "description": "Sites with active anonymous sharing links created",
      "enabled": true,
      "maxPenalty": 15,
      "calculation": "flat",
      "threshold": 1,
      "message": "Sites with active anonymous links ({count}): -{deduction} points"
    },
    {
      "id": "external-users",
      "name": "External Users (Guests)",
      "description": "Sites with external users who have been granted access",
      "enabled": true,
      "maxPenalty": 15,
      "calculation": "prevalence-based",
      "threshold": 100,
      "message": "Sites with external users ({count}): -{deduction} points"
    },
    {
      "id": "adhoc-permissions",
      "name": "Ad-Hoc Permissions",
      "description": "Sites where users have direct permissions (not via SharePoint groups)",
      "enabled": true,
      "maxPenalty": 10,
      "calculation": "percentage-based",
      "threshold": 20,
      "message": "Sites with ad-hoc permissions ({count}): -{deduction} points"
    },
    {
      "id": "external-access-prevalence",
      "name": "High External Access Prevalence",
      "description": "Too many sites have external sharing enabled",
      "enabled": true,
      "maxPenalty": 10,
      "calculation": "percentage-above-threshold",
      "threshold": 75,
      "message": "High external access prevalence ({percentage}%): -{deduction} points"
    },
    {
      "id": "disabled-users-licenses",
      "name": "Disabled Users with Licenses",
      "description": "Inactive users still consuming paid licenses (cost optimization)",
      "enabled": true,
      "maxPenalty": 10,
      "calculation": "percentage-based",
      "threshold": 50,
      "message": "Disabled users with licenses ({count}): -{deduction} points"
    },
    {
      "id": "app-secrets-expiring",
      "name": "App Registrations with Expired/Expiring Secrets",
      "description": "Applications that have expired secrets or secrets expiring in the next 30 days",
      "enabled": true,
      "maxPenalty": 15,
      "calculation": "flat",
      "threshold": 1,
      "message": "Apps with expired or soon-to-expire secrets ({count}): -{deduction} points"
    },
    {
      "id": "unused-app-registrations",
      "name": "Unused App Registrations",
      "description": "Applications with no sign-in activity in the last 90 days (for apps where usage data is available)",
      "enabled": true,
      "maxPenalty": 10,
      "calculation": "flat",
      "threshold": 1,
      "message": "Unused app registrations ({count}): -{deduction} points"
    },
    {
      "id": "sensitive-app-registrations",
      "name": "Sensitive App Registrations",
      "description": "Applications with elevated, write-capable, or privilege-granting permissions",
      "enabled": true,
      "maxPenalty": 15,
      "calculation": "flat",
      "threshold": 1,
      "message": "Sensitive app registrations ({count}): -{deduction} points"
    },
    {
      "id": "group-owner-governance",
      "name": "Group Owner Governance Violations",
      "description": "Groups with owner count outside allowed range or group principals as owners",
      "enabled": true,
      "maxPenalty": 10,
      "calculation": "flat",
      "threshold": 1,
      "message": "Group owner governance violations ({count}): -{deduction} points"
    },
    {
      "id": "site-owner-governance",
      "name": "Site Owner Governance Violations",
      "description": "Sites with owner/admin count outside allowed range or group principals with full control",
      "enabled": true,
      "maxPenalty": 15,
      "calculation": "flat",
      "threshold": 1,
      "message": "Site owner governance violations ({count}): -{deduction} points"
    }
  ],
  "ownerGovernance": {
    "enabled": true,
    "minOwners": 2,
    "maxOwners": 5,
    "flagGroupOwners": true
    },
  "privilegedPermissionDetection": {
    "enabled": true,
    "minPrivilegedPermissionCount": 3,
    "excludePermissionPatterns": [
      "^openid$",
      "^profile$",
      "^email$",
      "^offline_access$",
      "^User\\.Read$"
    ],
    "alwaysSensitivePermissionPatterns": [
      "^Directory\\.AccessAsUser\\.All$",
      "^AppRoleAssignment\\.ReadWrite\\.All$",
      "^DelegatedPermissionGrant\\.ReadWrite\\.All$",
      "^RoleManagement\\.ReadWrite\\..*$",
      "^RoleAssignmentSchedule\\.ReadWrite\\..*$",
      "^RoleEligibilitySchedule\\.ReadWrite\\..*$",
      "^Application\\.ReadWrite\\..*$",
      "^Directory\\.ReadWrite\\.All$",
      "^Sites\\.FullControl\\.All$",
      "^Sites\\.Manage\\.All$",
      "^Exchange\\.ManageAsApp$",
      "^GroupMember\\.ReadWrite\\.All$",
      "^Group\\.ReadWrite\\.All$",
      "^User\\.ReadWrite\\.All$",
      "^RoleManagement\\.ReadWrite\\.Directory$"
    ],
    "privilegedPermissionPatterns": [
      "\\b(ReadWrite|Write|Manage|FullControl|Delete|Create|Update|Send|AccessAsUser|PrivilegedOperations|ReadWrite\\.All|ReadWrite\\.OwnedBy)\\b",
      "^(RoleManagement|RoleAssignmentSchedule|RoleEligibilitySchedule|Policy\\.ReadWrite|AppRoleAssignment\\.ReadWrite|DelegatedPermissionGrant\\.ReadWrite|Directory\\.ReadWrite|Application\\.ReadWrite|Sites\\.(Manage|FullControl)|GroupMember\\.ReadWrite|Group\\.ReadWrite|User\\.ReadWrite).*"
    ]
  },
  "grades": [
    { "grade": "Excellent", "minScore": 90, "maxScore": 100, "color": "green" },
    { "grade": "Good", "minScore": 80, "maxScore": 89, "color": "green" },
    { "grade": "Fair", "minScore": 70, "maxScore": 79, "color": "yellow" },
    { "grade": "Poor", "minScore": 60, "maxScore": 69, "color": "yellow" },
    { "grade": "Critical", "minScore": 0, "maxScore": 59, "color": "red" }
  ]
}
'@

        $scoringConfig = $defaultScoringRulesJson | ConvertFrom-Json
    }

    $score = if ($null -ne $scoringConfig.baseScore) { [double]$scoringConfig.baseScore } else { 100 }
    $scoreDetails = @()

    $ownerGovernance = $scoringConfig.ownerGovernance
    if (-not $ownerGovernance) {
        $ownerGovernance = [PSCustomObject]@{
            enabled = $true
            minOwners = 2
            maxOwners = 5
            flagGroupOwners = $true
        }
    }

    $ownerGovernanceEnabled = ($ownerGovernance.enabled -ne $false)
    $minOwnerThreshold = if ($null -ne $ownerGovernance.minOwners) { [int]$ownerGovernance.minOwners } else { 2 }
    $maxOwnerThreshold = if ($null -ne $ownerGovernance.maxOwners) { [int]$ownerGovernance.maxOwners } else { 5 }
    $flagGroupOwnersAsViolation = ($ownerGovernance.flagGroupOwners -ne $false)

    $groupOwnerGovernanceViolations = @()
    $siteOwnerGovernanceViolations = @()

    if ($ownerGovernanceEnabled) {
        foreach ($group in $Groups) {
            $violationReasons = @()

            if ([int]$group.OwnerCount -lt $minOwnerThreshold) { $violationReasons += 'OwnerCountBelowMin' }
            if ([int]$group.OwnerCount -gt $maxOwnerThreshold) { $violationReasons += 'OwnerCountAboveMax' }
            if ($flagGroupOwnersAsViolation -and [bool]$group.OwnerIsGroup) { $violationReasons += 'OwnerContainsGroup' }

            if ($violationReasons.Count -gt 0) {
                $groupOwnerGovernanceViolations += [PSCustomObject]@{
                    DisplayName = $group.DisplayName
                    GroupId = $group.GroupId
                    OwnerCount = [int]$group.OwnerCount
                    OwnerIsGroup = [bool]$group.OwnerIsGroup
                    ViolationReason = ($violationReasons -join ', ')
                }
            }
        }

        foreach ($site in $Sites) {
            if (-not [bool]$site.OwnerDataAvailable) { continue }

            $violationReasons = @()
            if ([int]$site.OwnerAdminCount -lt $minOwnerThreshold) { $violationReasons += 'OwnerCountBelowMin' }
            if ([int]$site.OwnerAdminCount -gt $maxOwnerThreshold) { $violationReasons += 'OwnerCountAboveMax' }
            if ($flagGroupOwnersAsViolation -and [bool]$site.OwnerIsGroup) { $violationReasons += 'OwnerContainsGroup' }

            if ($violationReasons.Count -gt 0) {
                $siteOwnerGovernanceViolations += [PSCustomObject]@{
                    Title = $site.Title
                    Url = $site.Url
                    Group = $site.Group
                    OwnerAdminCount = [int]$site.OwnerAdminCount
                    OwnerIsGroup = [bool]$site.OwnerIsGroup
                    OwnerSource = $site.OwnerSource
                    ViolationReason = ($violationReasons -join ', ')
                }
            }
        }
    }

    $privilegedPermissionDetection = $scoringConfig.privilegedPermissionDetection
    if (-not $privilegedPermissionDetection) {
        $privilegedPermissionDetection = [PSCustomObject]@{
            enabled = $true
            minPrivilegedPermissionCount = 3
            excludePermissionPatterns = @('^openid$','^profile$','^email$','^offline_access$','^User\.Read$')
            alwaysSensitivePermissionPatterns = @(
                '^Directory\.AccessAsUser\.All$','^AppRoleAssignment\.ReadWrite\.All$','^DelegatedPermissionGrant\.ReadWrite\.All$',
                '^RoleManagement\.ReadWrite\..*$','^RoleAssignmentSchedule\.ReadWrite\..*$','^RoleEligibilitySchedule\.ReadWrite\..*$',
                '^Application\.ReadWrite\..*$','^Directory\.ReadWrite\.All$','^Sites\.FullControl\.All$','^Sites\.Manage\.All$',
                '^Exchange\.ManageAsApp$','^GroupMember\.ReadWrite\.All$','^Group\.ReadWrite\.All$','^User\.ReadWrite\.All$','^RoleManagement\.ReadWrite\.Directory$'
            )
            privilegedPermissionPatterns = @(
                '\b(ReadWrite|Write|Manage|FullControl|Delete|Create|Update|Send|AccessAsUser|PrivilegedOperations|ReadWrite\.All|ReadWrite\.OwnedBy)\b',
                '^(RoleManagement|RoleAssignmentSchedule|RoleEligibilitySchedule|Policy\.ReadWrite|AppRoleAssignment\.ReadWrite|DelegatedPermissionGrant\.ReadWrite|Directory\.ReadWrite|Application\.ReadWrite|Sites\.(Manage|FullControl)|GroupMember\.ReadWrite|Group\.ReadWrite|User\.ReadWrite).*'
            )
        }
    }

    $sensitiveAppRegistrationsResult = @($SensitiveAppRegistrations)
    if ($IncludeAppRegistrations -and $AppRegistrations.Count -gt 0 -and $privilegedPermissionDetection.enabled -ne $false) {
        $minPrivilegedPermissionCount = if ($null -ne $privilegedPermissionDetection.minPrivilegedPermissionCount) { [int]$privilegedPermissionDetection.minPrivilegedPermissionCount } else { 3 }
        $excludePermissionPatterns = @($privilegedPermissionDetection.excludePermissionPatterns)
        $alwaysSensitivePermissionPatterns = @($privilegedPermissionDetection.alwaysSensitivePermissionPatterns)
        $privilegedPermissionPatterns = @($privilegedPermissionDetection.privilegedPermissionPatterns)

        foreach ($app in $AppRegistrations) {
            $permissionEntries = @()
            if (-not [string]::IsNullOrWhiteSpace([string]$app.Permissions)) {
                $permissionEntries = @(([string]$app.Permissions).Split(';') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            }

            $matchedHighRisk = @()
            $matchedPrivileged = @()

            foreach ($entry in $permissionEntries) {
                $permissionName = $entry
                if ($entry -match ':\s*(.+?)\s*\[(Scope|Role)\]\s*$') { $permissionName = $matches[1] }

                $isExcluded = $false
                foreach ($excludePattern in $excludePermissionPatterns) {
                    if (-not [string]::IsNullOrWhiteSpace([string]$excludePattern) -and $permissionName -match [string]$excludePattern) { $isExcluded = $true; break }
                }
                if ($isExcluded) { continue }

                $isHighRisk = $false
                foreach ($highRiskPattern in $alwaysSensitivePermissionPatterns) {
                    if (-not [string]::IsNullOrWhiteSpace([string]$highRiskPattern) -and $permissionName -match [string]$highRiskPattern) { $isHighRisk = $true; break }
                }
                if ($isHighRisk) { $matchedHighRisk += $entry; continue }

                $isPrivileged = $false
                foreach ($privilegedPattern in $privilegedPermissionPatterns) {
                    if (-not [string]::IsNullOrWhiteSpace([string]$privilegedPattern) -and $permissionName -match [string]$privilegedPattern) { $isPrivileged = $true; break }
                }
                if ($isPrivileged) { $matchedPrivileged += $entry }
            }

            $matchedHighRisk = @($matchedHighRisk | Sort-Object -Unique)
            $matchedPrivileged = @($matchedPrivileged | Sort-Object -Unique)
            $app.HighRiskPermissionCount = $matchedHighRisk.Count
            $app.PrivilegedPermissionCount = $matchedPrivileged.Count
            $app.SensitivePermissionCount = $app.HighRiskPermissionCount + $app.PrivilegedPermissionCount

            $allSensitivePermissions = @($matchedHighRisk + $matchedPrivileged | Sort-Object -Unique)
            $app.SensitivePermissions = if ($allSensitivePermissions.Count -gt 0) { $allSensitivePermissions -join '; ' } else { '(none)' }

            if ($app.HighRiskPermissionCount -gt 0) { $app.IsSensitive = $true; $app.SensitiveReason = 'Contains high-risk permission(s)' }
            elseif ($app.PrivilegedPermissionCount -ge $minPrivilegedPermissionCount) { $app.IsSensitive = $true; $app.SensitiveReason = "Contains $($app.PrivilegedPermissionCount) privileged permission(s)" }
            else { $app.IsSensitive = $false; $app.SensitiveReason = '(none)' }
        }

        $sensitiveAppRegistrationsResult = @($AppRegistrations | Where-Object { $_.IsSensitive })
    }

    $regularSites = $Sites | Where-Object { $_.Url -notlike '*-my.sharepoint.com/personal/*' -and $_.Url -notlike '*/search' }
    $regularSiteCount = [Math]::Max($regularSites.Count, 1)

    $ruleData = @{
        'sensitivity-labels' = @{ Count = $SitesWithoutSensitivityLabel.Count; Percentage = Get-ScorePercentage $SitesWithoutSensitivityLabel.Count $regularSiteCount }
        'anonymous-sharing' = @{ Count = $SitesWithEveryoneSharing.Count }
        'anonymous-links' = @{ Count = $SitesWithAnonymousLinks.Count }
        'external-users' = @{ Count = $SitesWithExternalUsers.Count; Percentage = Get-ScorePercentage $SitesWithExternalUsers.Count ([Math]::Max($SitesWithExternalAccess.Count, 1)) }
        'adhoc-permissions' = @{ Count = $SitesWithAdHocPermissions.Count; Percentage = Get-ScorePercentage $SitesWithAdHocPermissions.Count $regularSiteCount }
        'external-access-prevalence' = @{ Count = $SitesWithExternalAccess.Count; Percentage = Get-ScorePercentage $SitesWithExternalAccess.Count $regularSiteCount }
        'disabled-users-licenses' = @{ Count = $DisabledWithLicenses.Count; Percentage = Get-ScorePercentage $DisabledWithLicenses.Count ([Math]::Max($DisabledUsers.Count, 1)) }
        'app-secrets-expiring' = @{ Count = $AppsWithSecretRisk.Count; Percentage = Get-ScorePercentage $AppsWithSecretRisk.Count ([Math]::Max($AppRegistrations.Count, 1)) }
        'unused-app-registrations' = @{ Count = $UnusedAppRegistrations.Count; Percentage = Get-ScorePercentage $UnusedAppRegistrations.Count ([Math]::Max($AppRegistrations.Count, 1)) }
        'sensitive-app-registrations' = @{ Count = $sensitiveAppRegistrationsResult.Count; Percentage = Get-ScorePercentage $sensitiveAppRegistrationsResult.Count ([Math]::Max($AppRegistrations.Count, 1)) }
        'group-owner-governance' = @{ Count = $groupOwnerGovernanceViolations.Count; Percentage = Get-ScorePercentage $groupOwnerGovernanceViolations.Count ([Math]::Max($Groups.Count, 1)) }
        'site-owner-governance' = @{ Count = $siteOwnerGovernanceViolations.Count; Percentage = Get-ScorePercentage $siteOwnerGovernanceViolations.Count ([Math]::Max($regularSites.Count, 1)) }
    }

    foreach ($rule in ($scoringConfig.rules | Where-Object { $_.enabled -ne $false })) {
        if (-not $ruleData.ContainsKey($rule.id)) {
            Write-Warning "Scoring rule '$($rule.id)' has no matching dataset and was skipped."
            continue
        }

        $context = $ruleData[$rule.id]
        $count = [int]$context.Count
        $percentage = if ($context.ContainsKey('Percentage')) { [double]$context.Percentage } else { 0 }
        $threshold = if ($null -ne $rule.threshold) { [double]$rule.threshold } else { 0 }
        $maxPenalty = if ($null -ne $rule.maxPenalty) { [double]$rule.maxPenalty } else { 0 }
        $deduction = 0

        switch ($rule.calculation) {
            'flat' { if ($count -ge $threshold -and $maxPenalty -gt 0) { $deduction = $maxPenalty } }
            'percentage-based' { if ($percentage -gt 0 -and $threshold -gt 0 -and $maxPenalty -gt 0) { $deduction = [Math]::Min($maxPenalty, ($percentage / $threshold) * $maxPenalty) } }
            'prevalence-based' { if ($percentage -gt 0 -and $threshold -gt 0 -and $maxPenalty -gt 0) { $deduction = [Math]::Min($maxPenalty, ($percentage / $threshold) * $maxPenalty) } }
            'percentage-above-threshold' { if ($percentage -gt $threshold -and $threshold -lt 100 -and $maxPenalty -gt 0) { $deduction = [Math]::Min($maxPenalty, (($percentage - $threshold) / (100 - $threshold)) * $maxPenalty) } }
        }

        if ($deduction -gt 0) {
            $score -= $deduction
            $detail = $rule.message
            if (-not $detail) { $detail = "$($rule.name): -$([Math]::Round($deduction, 0)) points" }
            $detail = $detail -replace '\{percentage\}', ([Math]::Round($percentage, 0))
            $detail = $detail -replace '\{count\}', $count
            $detail = $detail -replace '\{deduction\}', ([Math]::Round($deduction, 0))
            $scoreDetails += $detail
        }
    }

    $score = [Math]::Max(0, [Math]::Min(100, $score))
    $score = [Math]::Round($score, 0)

    $scoreGrade = $null
    $scoreColor = $null
    if ($scoringConfig.grades -and $scoringConfig.grades.Count -gt 0) {
        $gradeMatch = $scoringConfig.grades | Where-Object { $score -ge $_.minScore -and $score -le $_.maxScore } | Select-Object -First 1
        if ($gradeMatch) {
            $scoreGrade = $gradeMatch.grade
            switch (([string]$gradeMatch.color).ToLower()) {
                'green' { $scoreColor = 'Green' }
                'yellow' { $scoreColor = 'Yellow' }
                'red' { $scoreColor = 'Red' }
                default { $scoreColor = 'White' }
            }
        }
    }

    if (-not $scoreGrade) {
        $scoreGrade = switch ($score) {
            {$_ -ge 90} { 'Excellent'; break }
            {$_ -ge 80} { 'Good'; break }
            {$_ -ge 70} { 'Fair'; break }
            {$_ -ge 60} { 'Poor'; break }
            default { 'Critical' }
        }
    }

    if (-not $scoreColor) {
        $scoreColor = switch ($score) {
            {$_ -ge 80} { 'Green'; break }
            {$_ -ge 60} { 'Yellow'; break }
            default { 'Red' }
        }
    }

    return [PSCustomObject]@{
        Score = $score
        ScoreDetails = $scoreDetails
        ScoreGrade = $scoreGrade
        ScoreColor = $scoreColor
        SensitiveAppRegistrations = $sensitiveAppRegistrationsResult
        GroupOwnerGovernanceViolations = $groupOwnerGovernanceViolations
        SiteOwnerGovernanceViolations = $siteOwnerGovernanceViolations
    }
}