Analisi Condivisioni Attive in SharePoint e OneDrive

Questo script PowerShell utilizza Microsoft Graph in modalità app-only per analizzare tutti i file e le cartelle condivise presenti nelle document library di SharePoint Online e OneDrive for Business.

Lo script esegue una scansione di tutti i siti SharePoint e dei drive associati, identificando:

Il risultato è un report CSV dettagliato, utile per attività di audit, security assessment e data governance, che consente di individuare rapidamente esposizioni non intenzionali o potenziali rischi di sicurezza.

Lo script supporta l’analisi ricorsiva delle cartelle fino a una profondità configurabile ed è basato su permessi applicativi Microsoft Entra ID, ben descritti e visibili nell’intestazione dello script.

Nota

Lo script è progettato per un’esecuzione manuale e puntuale.
La pianificazione automatica è sconsigliata, poiché le credenziali di connessione a Microsoft Graph sono dichiarate in chiaro all’interno dello script.


<#
===========================================================
PREREQUISITI PER L’ESECUZIONE DELLO SCRIPT
===========================================================

1) Permessi necessari nell’app registrata in Azure AD
-----------------------------------------------------
Lo script utilizza Microsoft Graph API con autenticazione 
client credentials (app registration). L’app deve avere 
i seguenti permessi **Application**:

   - Sites.Read.All
   - Sites.ReadWrite.All   (opzionale ma consigliato)
   - Files.Read.All
   - Files.ReadWrite.All   (opzionale ma consigliato)
   - User.Read.All
   - Directory.Read.All

Dopo aver assegnato i permessi, è necessario:
   → Cliccare su “Grant admin consent”.

2) Requisiti lato PowerShell
----------------------------
- PowerShell 5.1 o PowerShell 7+
- Modulo "Microsoft.Graph" **NON necessario**
  (lo script usa solo Invoke-RestMethod)
- Esecuzione script abilitata:
      Set-ExecutionPolicy RemoteSigned

3) Requisiti lato tenant
------------------------
- L’account che esegue lo script deve essere:
      → Global Administrator
      oppure
      → SharePoint Administrator


===========================================================
FINE PREREQUISITI
===========================================================
#>


# ==========================
# CONFIGURAZIONE
# ==========================

$TenantId     = "INSERISCI-TENANT-ID-QUI"
$ClientId     = "INSERISCI-APP-ID-QUI"
$ClientSecret = "INSERISCI-CLIENT-SECRET-QUI"

# ==========================
# CONFIGURAZIONE OUTPUT
# ==========================

# Cartella di destinazione
$OutputFolder = "C:\temp"

# Crea la cartella se non esiste
if (-not (Test-Path $OutputFolder)) {
    New-Item -ItemType Directory -Path $OutputFolder | Out-Null
}

# Timestamp per nome file
$timestamp = (Get-Date).ToString("yyyy-MM-dd_HH-mm-ss")

# Percorso completo del CSV
$OutputCsvPath = Join-Path $OutputFolder "sharing-data_$timestamp.csv"

# ==========================
# FUNZIONI DI SUPPORTO
# ==========================

function Get-GraphToken {
    param(
        [string]$TenantId,
        [string]$ClientId,
        [string]$ClientSecret,
        [string]$Scope = "https://graph.microsoft.com/.default"
    )

    $body = @{
        client_id     = $ClientId
        scope         = $Scope
        client_secret = $ClientSecret
        grant_type    = "client_credentials"
    }

    $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

    $response = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body $body -ContentType "application/x-www-form-urlencoded"
    return $response.access_token
}

function Invoke-GraphGet {
    param(
        [string]$Uri,
        [string]$AccessToken
    )

    $headers = @{
        Authorization = "Bearer $AccessToken"
    }

    try {
        $response = Invoke-RestMethod -Method Get -Uri $Uri -Headers $headers
    } catch {
        return @()
    }

    if ($response.value) { return $response.value }
    return @($response)
}

function Get-DriveItemsRecursive {
    param(
        [string]$DriveId,
        [string]$AccessToken
    )

    $items = @()
    $queue = New-Object System.Collections.Queue
    $queue.Enqueue("root")

    while ($queue.Count -gt 0) {
        $itemId = $queue.Dequeue()

        if ($itemId -eq "root") {
            $uri = "https://graph.microsoft.com/v1.0/drives/$DriveId/root/children"
        } else {
            $uri = "https://graph.microsoft.com/v1.0/drives/$DriveId/items/$itemId/children"
        }

        $children = Invoke-GraphGet -Uri $uri -AccessToken $AccessToken

        foreach ($child in $children) {
            $items += $child
            if ($child.folder) {
                $queue.Enqueue($child.id)
            }
        }
    }

    return $items
}

function Get-Permissions {
    param(
        [string]$DriveId,
        [string]$ItemId,
        [string]$AccessToken
    )

    $uri = "https://graph.microsoft.com/v1.0/drives/$DriveId/items/$ItemId/permissions"
    return Invoke-GraphGet -Uri $uri -AccessToken $AccessToken
}

# ==========================
# OTTENIMENTO TOKEN
# ==========================

Write-Host "Ottenimento token..." -ForegroundColor Cyan
$AccessToken = Get-GraphToken -TenantId $TenantId -ClientId $ClientId -ClientSecret $ClientSecret

# ==========================
# RECUPERO DOMINI DEL TENANT
# ==========================

Write-Host "Recupero domini del tenant..." -ForegroundColor Cyan
$domains = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/domains" -AccessToken $AccessToken
$internalDomains = $domains.id | ForEach-Object { $_.ToLower() }

# ==========================
# FUNZIONE: Filtra permessi NON ereditati
# ==========================

function Get-RealSharing {
    param(
        $filePerms,
        $parentPerms
    )

    $real = @()

    foreach ($perm in $filePerms) {

        # Link di condivisione = sempre reale
        if ($perm.link) {
            $real += $perm
            continue
        }

        # Se non esiste nella cartella padre → è reale
        $match = $parentPerms | Where-Object { $_.id -eq $perm.id }

        if (-not $match) {
            $real += $perm
        }
    }

    return $real
}

# ==========================
# ELABORAZIONE
# ==========================

$report = New-Object System.Collections.Generic.List[Object]

# ============================================================
# SHAREPOINT
# ============================================================

Write-Host "Analisi SharePoint..." -ForegroundColor Cyan

$spoSites = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/sites?search=*" -AccessToken $AccessToken

foreach ($site in $spoSites) {

    $siteId = $site.id
    $siteUrl = $site.webUrl
    $sitoNome = $site.name

    Write-Host "Sito SPO: $sitoNome" -ForegroundColor Green

    $drives = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/sites/$siteId/drives" -AccessToken $AccessToken

    foreach ($drive in $drives) {

        $driveId = $drive.id
        $driveName = $drive.name

        $items = Get-DriveItemsRecursive -DriveId $driveId -AccessToken $AccessToken

        foreach ($item in $items) {

            if (-not $item.file) { continue }

            $filePerms = Get-Permissions -DriveId $driveId -ItemId $item.id -AccessToken $AccessToken
            $parentPerms = Get-Permissions -DriveId $driveId -ItemId $item.parentReference.id -AccessToken $AccessToken

            # 🔥 Filtra permessi non ereditati
            $realPerms = Get-RealSharing -filePerms $filePerms -parentPerms $parentPerms
            if ($realPerms.Count -eq 0) { continue }

            # Aggregazione
            $roles = @()
            $emails = @()
            $scopes = @()

            foreach ($perm in $realPerms) {

                if ($perm.roles) { $roles += $perm.roles }

                if ($perm.link -and $perm.link.scope) {
                    $scopes += $perm.link.scope
                }

                if ($perm.grantedTo) {
                    if ($perm.grantedTo.user.email) { $emails += $perm.grantedTo.user.email }
                }

                if ($perm.grantedToIdentitiesV2) {
                    foreach ($g in $perm.grantedToIdentitiesV2) {
                        if ($g.user.email) { $emails += $g.user.email }
                    }
                }
            }

            # Livello condivisione
            $livello = "Utenti specifici"
            if ($scopes -contains "anonymous") { $livello = "Chiunque con il link" }
            elseif ($scopes -contains "organization") { $livello = "Tutta l'organizzazione" }

            # 🔥 Identificazione esterni
            $isExternal = "No"

            foreach ($email in $emails) {
                $domain = $email.Split("@")[1].ToLower()
                if ($internalDomains -notcontains $domain) {
                    $isExternal = "Si"
                }
            }

            # Owner = ultima persona che ha modificato il file
            $owner = if ($item.lastModifiedBy.user.email) { 
                $item.lastModifiedBy.user.email 
            } else { 
                $item.lastModifiedBy.user.displayName 
            }

            # Formato compatto
            $dettaglio = $livello

            if ($roles.Count -gt 0) {
                $dettaglio += " - " + (($roles | Select-Object -Unique) -join ";")
            }

            if ($emails.Count -gt 0) {
                $dettaglio += " - " + (($emails | Select-Object -Unique) -join ";")
            }

            # Oggetto finale
            $obj = [PSCustomObject]@{
                Origine               = "SharePoint"
                SitoNome              = $sitoNome
                SitoRootUrl           = $siteUrl
                DriveName             = $driveName
                FileName              = $item.name
                Owner                 = $owner
                DettaglioCondivisione = $dettaglio
                CondivisioneEsterna   = $isExternal
            }

            $report.Add($obj) | Out-Null
        }
    }
}

# ============================================================
# ONEDRIVE
# ============================================================

Write-Host "Analisi OneDrive..." -ForegroundColor Cyan

foreach ($user in $users) {

    $userId = $user.id
    $upn = $user.userPrincipalName
    $sitoNome = "OneDrive - $($user.displayName)"

    Write-Host "OneDrive utente: $upn" -ForegroundColor Green

    try {
        $drive = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/users/$userId/drive" -AccessToken $AccessToken
    } catch {
        continue
    }

    if (-not $drive -or -not $drive.id) { continue }

    $driveId = $drive.id
    $driveName = $drive.name
    $sitoRootUrl = $drive.webUrl

    $items = Get-DriveItemsRecursive -DriveId $driveId -AccessToken $AccessToken

    foreach ($item in $items) {

        if (-not $item.file) { continue }

        $filePerms = Get-Permissions -DriveId $driveId -ItemId $item.id -AccessToken $AccessToken
        $parentPerms = Get-Permissions -DriveId $driveId -ItemId $item.parentReference.id -AccessToken $AccessToken

        # 🔥 Filtra permessi non ereditati
        $realPerms = Get-RealSharing -filePerms $filePerms -parentPerms $parentPerms
        if ($realPerms.Count -eq 0) { continue }

        # Aggregazione
        $roles = @()
        $emails = @()
        $scopes = @()

        foreach ($perm in $realPerms) {

            if ($perm.roles) { $roles += $perm.roles }

            if ($perm.link -and $perm.link.scope) {
                $scopes += $perm.link.scope
            }

            if ($perm.grantedTo) {
                if ($perm.grantedTo.user.email) { $emails += $perm.grantedTo.user.email }
            }

            if ($perm.grantedToIdentitiesV2) {
                foreach ($g in $perm.grantedToIdentitiesV2) {
                    if ($g.user.email) { $emails += $g.user.email }
                }
            }
        }

        # Livello condivisione
        $livello = "Utenti specifici"
        if ($scopes -contains "anonymous") { $livello = "Chiunque con il link" }
        elseif ($scopes -contains "organization") { $livello = "Tutta l'organizzazione" }

        # 🔥 Identificazione esterni
        $isExternal = "No"

        foreach ($email in $emails) {
            $domain = $email.Split("@")[1].ToLower()
            if ($internalDomains -notcontains $domain) {
                $isExternal = "Si"
            }
        }

        # Owner = ultima persona che ha modificato il file
        $owner = if ($item.lastModifiedBy.user.email) { 
            $item.lastModifiedBy.user.email 
        } else { 
            $item.lastModifiedBy.user.displayName 
        }

        # Formato compatto
        $dettaglio = $livello

        if ($roles.Count -gt 0) {
            $dettaglio += " - " + (($roles | Select-Object -Unique) -join ";")
        }

        if ($emails.Count -gt 0) {
            $dettaglio += " - " + (($emails | Select-Object -Unique) -join ";")
        }

        # Oggetto finale
        $obj = [PSCustomObject]@{
            Origine               = "OneDrive"
            SitoNome              = $sitoNome
            SitoRootUrl           = $sitoRootUrl
            DriveName             = $driveName
            FileName              = $item.name
            Owner                 = $owner
            DettaglioCondivisione = $dettaglio
            CondivisioneEsterna   = $isExternal
        }

        $report.Add($obj) | Out-Null
    }
}

# ==========================
# EXPORT CSV
# ==========================

Write-Host "Esporto CSV..." -ForegroundColor Cyan
$report | Export-Csv -Path $OutputCsvPath -NoTypeInformation -Encoding UTF8

Write-Host "Completato. File generato: $OutputCsvPath" -ForegroundColor Green