Questo script estrae dal Unified Audit Log di Microsoft 365 tutte le modifiche effettuate dagli amministratori alle Sensitivity Labels e alle policy di Data Loss Prevention (DLP) del tenant.
L’estrazione è limitata alla giornata precedente alla data di esecuzione e i risultati vengono esportati in un file CSV.
Lo script può essere utilizzato per il monitoraggio delle attività amministrative in contesti in cui non sia disponibile un servizio di Security Operations Center (SOC). I file CSV generati possono, ad esempio, essere archiviati in un repository che garantisca la tracciabilità delle modifiche e degli accessi, rendendoli di fatto immutabili e verificabili nel tempo.
Resta comunque fortemente consigliata l’adozione di una soluzione di SIEM e SOAR (come Microsoft Sentinel) per la raccolta centralizzata degli audit log e per la generazione di notifiche tempestive ogni volta che un amministratore modifica le policy di Data Loss Prevention o le etichette di riservatezza.
<#
Prerequisiti:
avere il modulo di exchange online installato, se non fosse presente usare:
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Avere un account con permessi adeguati per poter accedere alla lettura dei log
#>
# ==========================
# CONFIGURAZIONE ESPORTAZIONE
# ==========================
$Global:ExportFolder = "C:\M365_Audit\Export" # <--- CAMBIA PERCORSO SE NECESSARIO
# ==========================
# OPERAZIONI DA TRACCIARE (modifiche DLP/Etichette)
# ==========================
# DLP (Compliance Center/Exchange DLP) - Set/Remove
$dlpOps = @(
'Set-DlpCompliancePolicy','Remove-DlpCompliancePolicy',
'Set-DlpComplianceRule','Remove-DlpComplianceRule',
'Set-DlpPolicy','Remove-DlpPolicy'
)
# Sensitivity labels & label policies - Set/Remove
$labelOps = @(
'Set-Label','Remove-Label',
'Set-LabelPolicy','Remove-LabelPolicy',
'Set-LabelPolicyRule','Remove-LabelPolicyRule'
)
# ==========================
# FINESTRA TEMPORALE: GIORNO PRECEDENTE
# ==========================
# Inizio del giorno precedente (00:00:00) e fine del giorno precedente (23:59:59)
$yesterdayStart = (Get-Date).AddDays(-1).Date
$yesterdayEnd = (Get-Date).Date.AddSeconds(-1)
$start = $yesterdayStart
$end = $yesterdayEnd
# ==========================
# PREPARAZIONE CARTELLA
# ==========================
if (-not (Test-Path -Path $Global:ExportFolder)) {
New-Item -ItemType Directory -Path $Global:ExportFolder -Force | Out-Null
}
# ==========================
# CONNESSIONE EXCHANGE ONLINE (UAL)
# ==========================
if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
throw "Modulo 'ExchangeOnlineManagement' non trovato. Installa: Install-Module ExchangeOnlineManagement -Scope CurrentUser"
}
try {
$connected = $false
try {
$connInfo = Get-ConnectionInformation -ErrorAction Stop
if ($connInfo | Where-Object { $_.State -eq 'Connected' }) { $connected = $true }
} catch { $connected = $false }
if (-not $connected) {
Connect-ExchangeOnline -ShowBanner:$false -ShowProgress:$false -ErrorAction Stop
}
} catch {
throw "Connessione a Exchange Online fallita: $($_.Exception.Message)"
}
# ==========================
# FUNZIONE: RICERCA UAL
# ==========================
function Invoke-UalSearch {
param(
[Parameter(Mandatory=$true)][datetime]$Start,
[Parameter(Mandatory=$true)][datetime]$End,
[Parameter(Mandatory=$true)][string[]]$Operations
)
$sid = "UAL-{0:yyyyMMddHHmmss}-{1}" -f (Get-Date), (New-Guid)
Search-UnifiedAuditLog `
-StartDate $Start `
-EndDate $End `
-Operations $Operations `
-SessionId $sid `
-SessionCommand ReturnLargeSet `
-ResultSize 5000 `
-ErrorAction Stop
}
# ==========================
# ESECUZIONE DELLE RICERCHE (separate per DLP ed Etichette)
# ==========================
$dlpResults = @()
$labelResults = @()
try { $dlpResults += Invoke-UalSearch -Start $start -End $end -Operations $dlpOps } catch { Write-Warning "Errore ricerca DLP: $($_.Exception.Message)" }
try { $labelResults += Invoke-UalSearch -Start $start -End $end -Operations $labelOps } catch { Write-Warning "Errore ricerca Etichette: $($_.Exception.Message)" }
$allResultsCount = ($dlpResults.Count + $labelResults.Count)
if ($allResultsCount -eq 0) {
Write-Host "Nessuna operazione di modifica DLP/Etichette trovata **ieri** nell'audit log." -ForegroundColor Yellow
return
}
# ==========================
# NORMALIZZAZIONE (assegna Servizio per contesto)
# ==========================
$flattened = @()
# --- DLP ---
foreach ($rec in $dlpResults) {
$data = $null
try { $data = $rec.AuditData | ConvertFrom-Json } catch { continue }
$actor = if ($data.UserId) { $data.UserId } else { ($rec.UserIds -join ';') }
$target = $data.ObjectId
if (-not $target) { $target = $data.PolicyName }
if (-not $target) { $target = $data.LabelName }
if (-not $target) { $target = $data.SiteUrl }
if (-not $target) { $target = $data.Workload }
# (non esportato, solo memorizzato)
$modifiedProps = $null
if ($data.ModifiedProperties) {
$modifiedProps = ($data.ModifiedProperties | ForEach-Object {
$nv = $_.NewValue
if ($nv -is [array]) { $nv = ($nv -join ',') }
"$($_.Name)=$nv"
}) -join ' | '
} elseif ($data.Parameters) {
$modifiedProps = ($data.Parameters | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ' | '
}
$flattened += [pscustomobject]@{
'DataOra (locale)' = [datetime]$rec.CreationDate
'Operazione' = $rec.Operation
'Utente' = $actor
'Workload' = $rec.Workload
'Target' = $target
'Esito' = $data.ResultStatus
'Servizio' = 'DLP'
'ClientIP' = $data.ClientIP
'UserAgent' = $data.UserAgent
'DettaglioModifica' = $modifiedProps
'AuditDataRaw' = $rec.AuditData
'RecordId' = $rec.Id
}
}
# --- Etichette ---
foreach ($rec in $labelResults) {
$data = $null
try { $data = $rec.AuditData | ConvertFrom-Json } catch { continue }
$actor = if ($data.UserId) { $data.UserId } else { ($rec.UserIds -join ';') }
$target = $data.ObjectId
if (-not $target) { $target = $data.PolicyName }
if (-not $target) { $target = $data.LabelName }
if (-not $target) { $target = $data.SiteUrl }
if (-not $target) { $target = $data.Workload }
# (non esportato, solo memorizzato)
$modifiedProps = $null
if ($data.ModifiedProperties) {
$modifiedProps = ($data.ModifiedProperties | ForEach-Object {
$nv = $_.NewValue
if ($nv -is [array]) { $nv = ($nv -join ',') }
"$($_.Name)=$nv"
}) -join ' | '
} elseif ($data.Parameters) {
$modifiedProps = ($data.Parameters | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ' | '
}
$flattened += [pscustomobject]@{
'DataOra (locale)' = [datetime]$rec.CreationDate
'Operazione' = $rec.Operation
'Utente' = $actor
'Workload' = $rec.Workload
'Target' = $target
'Esito' = $data.ResultStatus
'Servizio' = 'Etichetta'
'ClientIP' = $data.ClientIP
'UserAgent' = $data.UserAgent
'DettaglioModifica' = $modifiedProps
'AuditDataRaw' = $rec.AuditData
'RecordId' = $rec.Id
}
}
if (-not $flattened -or $flattened.Count -eq 0) {
Write-Host "Nessuna modifica DLP/Etichette trovata **ieri**." -ForegroundColor Yellow
return
}
# ==========================
# EXPORT CSV (Servizio prima di ClientIP; esclusione colonne non richieste)
# ==========================
$outFile = Join-Path $Global:ExportFolder ("Audit_DLP_Etichette_{0:yyyy-MM-dd}_Ieri.csv" -f (Get-Date))
$flattened |
Sort-Object 'DataOra (locale)' |
Select-Object 'DataOra (locale)','Utente','Target','Esito','Servizio','ClientIP','AuditDataRaw','RecordId' |
Export-Csv -Path $outFile -Encoding UTF8 -NoTypeInformation
Write-Host "Esportazione completata (ieri): $outFile" -ForegroundColor Green