SOC2/Get-SOC2ConfidentialityControls.ps1

<#
.SYNOPSIS
    Assesses SOC 2 Confidentiality trust principle controls against Microsoft 365 configuration.
.DESCRIPTION
    Evaluates Microsoft 365 tenant settings against SOC 2 Trust Service Criteria for
    the Confidentiality principle. Checks SharePoint sharing settings, DLP policies,
    sensitivity labels, retention policies, and guest access governance.
 
    All operations are strictly read-only (Get-* cmdlets and Graph GET requests only).
    Maps each check to an AICPA SOC 2 Trust Service Criterion reference.
 
    DISCLAIMER: This tool assists with SOC 2 readiness assessment. It does not
    constitute a SOC 2 audit or certification.
 
    Requires Microsoft Graph connection and optionally Purview/Compliance connection
    for DLP and label checks.
.PARAMETER OutputPath
    Optional path to export results as CSV. If not specified, results are returned
    to the pipeline.
.EXAMPLE
    PS> . .\Common\Connect-Service.ps1
    PS> Connect-Service -Service Graph
    PS> Connect-Service -Service Purview
    PS> .\SOC2\Get-SOC2ConfidentialityControls.ps1
 
    Displays SOC 2 Confidentiality control assessment results.
.EXAMPLE
    PS> .\SOC2\Get-SOC2ConfidentialityControls.ps1 -OutputPath '.\soc2-confidentiality.csv'
 
    Exports SOC 2 Confidentiality control results to CSV.
.NOTES
    Author: Daren9m
#>

[CmdletBinding()]
param(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

$ErrorActionPreference = 'Stop'

$results = [System.Collections.Generic.List[PSCustomObject]]::new()

# Helper to add a control result
function Add-ControlResult {
    param(
        [string]$TrustPrinciple,
        [string]$TSCReference,
        [string]$ControlId,
        [string]$ControlName,
        [string]$CurrentValue,
        [string]$ExpectedValue,
        [string]$Status,
        [string]$Severity,
        [string]$Evidence = '',
        [string]$Remediation = ''
    )
    $results.Add([PSCustomObject]@{
        TrustPrinciple = $TrustPrinciple
        TSCReference   = $TSCReference
        ControlId      = $ControlId
        ControlName    = $ControlName
        CurrentValue   = $CurrentValue
        ExpectedValue  = $ExpectedValue
        Status         = $Status
        Severity       = $Severity
        Evidence       = $Evidence
        Remediation    = $Remediation
    })
}

# ------------------------------------------------------------------
# SPO availability probe — distinguishes module not installed vs not connected
# ------------------------------------------------------------------
$spoAvailable = $false
$spoTenant = $null
$script:spoMessage = $null

try {
    $null = Get-Command -Name Get-SPOTenant -ErrorAction Stop
    # Command exists — module is installed; now verify we are connected
    try {
        $spoTenant = Get-SPOTenant -ErrorAction Stop
        $spoAvailable = $true
    }
    catch {
        $script:spoMessage = 'SharePoint Online connection required - run Connect-SPOService first'
        Write-Warning "SPO probe: $($script:spoMessage)"
    }
}
catch {
    $script:spoMessage = 'Requires Microsoft.Online.SharePoint.PowerShell module'
    Write-Warning "SPO probe: $($script:spoMessage)"
}

# ------------------------------------------------------------------
# C-01: SharePoint Sites Not Publicly Shared (C1.1)
# ------------------------------------------------------------------
Write-Verbose "C-01: Checking SharePoint tenant sharing capability..."
if ($spoAvailable) {
    $sharingCapability = $spoTenant.SharingCapability.ToString()

    # ExternalUserAndGuestSharing = most permissive (Anyone links)
    # ExternalUserSharingOnly = authenticated guests only
    # ExistingExternalUserSharingOnly = existing guests only
    # Disabled = no external sharing
    $hasAnonymousLinks = $sharingCapability -eq 'ExternalUserAndGuestSharing'

    $currentValue = "SharingCapability: $sharingCapability"
    $status = if ($hasAnonymousLinks) { 'Fail' } else { 'Pass' }

    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-01' `
        -ControlName 'SharePoint Sites Not Publicly Shared' `
        -CurrentValue $currentValue -ExpectedValue 'SharingCapability not set to ExternalUserAndGuestSharing' `
        -Status $status -Severity 'High' `
        -Evidence "Tenant sharing level: $sharingCapability; Anonymous links: $hasAnonymousLinks" `
        -Remediation 'In SharePoint admin center > Policies > Sharing, restrict to Existing guests or more restrictive.'
}
else {
    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-01' `
        -ControlName 'SharePoint Sites Not Publicly Shared' `
        -CurrentValue $script:spoMessage -ExpectedValue 'SharingCapability not set to ExternalUserAndGuestSharing' `
        -Status 'Review' -Severity 'High' `
        -Remediation 'Connect to SharePoint Online to evaluate this control.'
}

# ------------------------------------------------------------------
# C-02: External Sharing Restricted (C1.1)
# ------------------------------------------------------------------
Write-Verbose "C-02: Checking external sharing restrictions..."
if ($spoAvailable) {
    $sharingCapability = $spoTenant.SharingCapability.ToString()
    $isMostPermissive = $sharingCapability -eq 'ExternalUserAndGuestSharing'

    $currentValue = "SharingCapability: $sharingCapability"
    $status = if ($isMostPermissive) { 'Fail' } else { 'Pass' }

    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-02' `
        -ControlName 'External Sharing Restricted' `
        -CurrentValue $currentValue -ExpectedValue 'Not set to most permissive level (ExternalUserAndGuestSharing)' `
        -Status $status -Severity 'High' `
        -Evidence "Sharing capability: $sharingCapability" `
        -Remediation 'In SharePoint admin center > Policies > Sharing, set to New and existing guests or more restrictive.'
}
else {
    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-02' `
        -ControlName 'External Sharing Restricted' `
        -CurrentValue $script:spoMessage -ExpectedValue 'Not set to most permissive level (ExternalUserAndGuestSharing)' `
        -Status 'Review' -Severity 'High' `
        -Remediation 'Connect to SharePoint Online to evaluate this control.'
}

# ------------------------------------------------------------------
# C-03: DLP Policies Active and Enforcing (C1.2)
# ------------------------------------------------------------------
try {
    Write-Verbose "C-03: Checking DLP policies..."
    $null = Get-Command -Name Get-DlpCompliancePolicy -ErrorAction Stop
    $dlpPolicies = @(Get-DlpCompliancePolicy -ErrorAction Stop)

    $enforcingPolicies = @($dlpPolicies | Where-Object { $_.Mode -eq 'Enable' })
    $testPolicies = @($dlpPolicies | Where-Object { $_.Mode -like 'Test*' })

    $currentValue = if ($dlpPolicies.Count -eq 0) {
        'No DLP policies found'
    } elseif ($enforcingPolicies.Count -gt 0) {
        "$($enforcingPolicies.Count) enforcing, $($testPolicies.Count) in test mode (of $($dlpPolicies.Count) total)"
    } else {
        "$($dlpPolicies.Count) policies found but none in enforce mode"
    }

    $status = if ($enforcingPolicies.Count -gt 0) { 'Pass' } elseif ($dlpPolicies.Count -gt 0) { 'Fail' } else { 'Fail' }

    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -ControlId 'C-03' `
        -ControlName 'DLP Policies Active and Enforcing' `
        -CurrentValue $currentValue -ExpectedValue 'At least one DLP policy in enforcement mode' `
        -Status $status -Severity 'High' `
        -Evidence "Total DLP policies: $($dlpPolicies.Count); Enforcing: $($enforcingPolicies.Count); Test: $($testPolicies.Count)" `
        -Remediation 'In Purview compliance portal > DLP > Policies, create or enable a DLP policy in enforcement mode.'
}
catch {
    Write-Warning "C-03: DLP cmdlets not available: $_"
    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -ControlId 'C-03' `
        -ControlName 'DLP Policies Active and Enforcing' `
        -CurrentValue 'Unable to check (Purview not connected)' -ExpectedValue 'DLP policies enforcing' `
        -Status 'Review' -Severity 'High' `
        -Remediation 'Connect to Purview (Security & Compliance) to evaluate DLP policies.'
}

# ------------------------------------------------------------------
# C-04: Sensitivity Labels Published (C1.2)
# ------------------------------------------------------------------
try {
    Write-Verbose "C-04: Checking sensitivity labels..."
    $null = Get-Command -Name Get-Label -ErrorAction Stop
    $labels = @(Get-Label -ErrorAction Stop)

    $enabledLabels = @($labels | Where-Object { $null -eq $_.Disabled -or -not $_.Disabled })

    $currentValue = if ($labels.Count -eq 0) {
        'No sensitivity labels found'
    } else {
        "$($enabledLabels.Count) enabled labels (of $($labels.Count) total)"
    }

    $status = if ($enabledLabels.Count -gt 0) { 'Pass' } else { 'Fail' }

    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -ControlId 'C-04' `
        -ControlName 'Sensitivity Labels Published' `
        -CurrentValue $currentValue -ExpectedValue 'At least one sensitivity label enabled and published' `
        -Status $status -Severity 'Medium' `
        -Evidence "Total labels: $($labels.Count); Enabled: $($enabledLabels.Count)" `
        -Remediation 'In Purview compliance portal > Information protection > Labels, create and publish sensitivity labels.'
}
catch {
    Write-Warning "C-04: Sensitivity label cmdlets not available: $_"
    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -ControlId 'C-04' `
        -ControlName 'Sensitivity Labels Published' `
        -CurrentValue 'Unable to check (Purview not connected)' -ExpectedValue 'Sensitivity labels published' `
        -Status 'Review' -Severity 'Medium' `
        -Remediation 'Connect to Purview to evaluate sensitivity labels.'
}

# ------------------------------------------------------------------
# C-05: Encryption in Transit Enforced (C1.1)
# ------------------------------------------------------------------
# Microsoft 365 enforces TLS 1.2 by default — this is an informational check
Write-Verbose "C-05: Checking encryption in transit (TLS)..."
Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-05' `
    -ControlName 'Encryption in Transit Enforced' `
    -CurrentValue 'TLS 1.2+ enforced by Microsoft 365 (platform default)' -ExpectedValue 'TLS 1.2 or higher' `
    -Status 'Pass' -Severity 'Medium' `
    -Evidence 'Microsoft 365 enforces TLS 1.2 by default since October 2020. TLS 1.0/1.1 are deprecated.' `
    -Remediation 'Verify no legacy applications use TLS 1.0/1.1 to connect to M365 services.'

# ------------------------------------------------------------------
# C-06: Data Retention Policies Configured (C1.2)
# ------------------------------------------------------------------
try {
    Write-Verbose "C-06: Checking retention policies..."
    $null = Get-Command -Name Get-RetentionCompliancePolicy -ErrorAction Stop
    $retentionPolicies = @(Get-RetentionCompliancePolicy -ErrorAction Stop)

    $enabledRetention = @($retentionPolicies | Where-Object { $_.Enabled -eq $true -or $null -eq $_.Enabled })

    $currentValue = if ($retentionPolicies.Count -eq 0) {
        'No retention policies found'
    } else {
        "$($enabledRetention.Count) active retention policies (of $($retentionPolicies.Count) total)"
    }

    $status = if ($enabledRetention.Count -gt 0) { 'Pass' } else { 'Fail' }

    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -ControlId 'C-06' `
        -ControlName 'Data Retention Policies Configured' `
        -CurrentValue $currentValue -ExpectedValue 'At least one retention policy active' `
        -Status $status -Severity 'Medium' `
        -Evidence "Total retention policies: $($retentionPolicies.Count); Active: $($enabledRetention.Count)" `
        -Remediation 'In Purview compliance portal > Data lifecycle management > Retention policies, create retention policies.'
}
catch {
    Write-Warning "C-06: Retention policy cmdlets not available: $_"
    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.2' -ControlId 'C-06' `
        -ControlName 'Data Retention Policies Configured' `
        -CurrentValue 'Unable to check (Purview not connected)' -ExpectedValue 'Retention policies active' `
        -Status 'Review' -Severity 'Medium' `
        -Remediation 'Connect to Purview to evaluate retention policies.'
}

# ------------------------------------------------------------------
# C-07: Guest Access Governance (C1.1)
# ------------------------------------------------------------------
try {
    Write-Verbose "C-07: Checking guest invitation settings..."
    $authPolicy = Invoke-MgGraphRequest -Method GET -Uri '/v1.0/policies/authorizationPolicy' -ErrorAction Stop

    $allowInvitesFrom = $authPolicy['allowInvitesFrom']
    # Values: everyone, adminsAndGuestInviters, adminsGuestInvitersAndAllMembers, none

    $currentValue = "allowInvitesFrom: $allowInvitesFrom"
    $isRestricted = $allowInvitesFrom -eq 'adminsAndGuestInviters' -or $allowInvitesFrom -eq 'none'
    $status = if ($isRestricted) { 'Pass' } else { 'Fail' }

    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-07' `
        -ControlName 'Guest Access Governance' `
        -CurrentValue $currentValue -ExpectedValue "allowInvitesFrom set to 'adminsAndGuestInviters' or 'none'" `
        -Status $status -Severity 'Medium' `
        -Evidence "Guest invitation policy: $allowInvitesFrom" `
        -Remediation 'In Entra admin center > External Identities > External collaboration settings, restrict guest invitations to admins.'
}
catch {
    Write-Warning "C-07: Failed to check guest access settings: $_"
    Add-ControlResult -TrustPrinciple 'Confidentiality' -TSCReference 'C1.1' -ControlId 'C-07' `
        -ControlName 'Guest Access Governance' `
        -CurrentValue "Error: $_" -ExpectedValue 'Guest invitations restricted to admins' `
        -Status 'Error' -Severity 'Medium'
}

# ------------------------------------------------------------------
# Output results
# ------------------------------------------------------------------
if ($results.Count -eq 0) {
    Write-Warning "No SOC 2 Confidentiality control results were generated."
    return
}

Write-Verbose "SOC 2 Confidentiality controls assessed: $($results.Count)"

if ($OutputPath) {
    $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported $($results.Count) SOC 2 Confidentiality controls to $OutputPath"
}
else {
    Write-Output $results
}