Private/Compliance.ps1
|
<#
Rétention, enregistrements et modération. Ces champs figuraient dans la liste de ceux qu'on ne copie jamais ET dans celle que le vérificateur ignore : la perte n'apparaissait donc dans aucun rapport. Un document sous étiquette « Conserver 7 ans » arrivait à destination sans étiquette, donc supprimable. Un document « En attente d'approbation » arrivait dans une bibliothèque sans modération, visible de tous. Rien ne le disait. Le principe retenu : ces états ne se copient pas comme une colonne — ils se RÉAPPLIQUENT après coup, et ce qui ne peut pas l'être doit être nommé. #> # Écrits par le service, jamais par une copie directe. Ils restent hors de # Set-SPMItemMetadata : leur place est ici, sous un traitement propre. $script:SPMComplianceField = @( '_ComplianceTag', '_ComplianceTagWrittenTime', '_ComplianceTagUserId', '_ComplianceFlags', '_IsRecord', '_ModerationStatus', '_ModerationComments' ) function Get-SPMComplianceState { <# .SYNOPSIS Lit l'état de conformité d'un élément : étiquette, enregistrement, modération. .DESCRIPTION Tolérant à l'absence : une bibliothèque sans rétention ni modération ne porte simplement pas ces champs. Absent et « vide » se valent ici, mais ni l'un ni l'autre ne vaut « conforme » — c'est « rien à réappliquer ». .OUTPUTS Objet { Label, IsRecord, ModerationStatus, ModerationComments, HasAny }. #> [CmdletBinding()] [OutputType([pscustomobject])] param([Parameter(Mandatory)]$Item) $valeurs = $null if ($Item.PSObject.Properties['FieldValues']) { $valeurs = $Item.FieldValues } elseif ($Item -is [System.Collections.IDictionary]) { $valeurs = $Item } $lire = { param($nom) if ($null -eq $valeurs) { return $null } if ($valeurs -is [System.Collections.IDictionary] -and -not $valeurs.Contains($nom)) { return $null } try { return $valeurs[$nom] } catch { return $null } } $label = & $lire '_ComplianceTag' $record = & $lire '_IsRecord' $moderation = & $lire '_ModerationStatus' $commentaires = & $lire '_ModerationComments' if ("$label" -eq '') { $label = $null } if ("$commentaires" -eq '') { $commentaires = $null } # 0 = approuvé, c'est-à-dire l'état par défaut d'une bibliothèque sans # modération : le réappliquer n'apprendrait rien et écrirait pour rien. $moderationUtile = $null if ($null -ne $moderation -and "$moderation" -ne '' -and [int]$moderation -ne 0) { $moderationUtile = [int]$moderation } [pscustomobject]@{ Label = $label IsRecord = [bool]$record ModerationStatus = $moderationUtile ModerationComments = $commentaires HasAny = [bool]($label -or $record -or $null -ne $moderationUtile) } } function Get-SPMComplianceInventory { <# .SYNOPSIS Compte, à la source, ce qui est étiqueté, déclaré enregistrement ou en modération. .DESCRIPTION Se mesure AVANT de partir. Découvrir en fin de migration qu'une centaine de documents ont perdu leur étiquette de rétention ne laisse aucune manœuvre : la destination est déjà peuplée, et l'étiquette ne se rattrape pas à l'aveugle. .OUTPUTS Objet { Inspected, Labeled, Records, Pending, Labels, Samples }. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][string]$LibraryTitle, [Parameter(Mandatory)]$Connection, [int]$PageSize = 500 ) $items = @(Invoke-SPMWithRetry -Operation "compliance/scan/$LibraryTitle" -PassThruErrors -ScriptBlock { Get-PnPListItem -List $LibraryTitle -PageSize $PageSize ` -Fields '_ComplianceTag', '_IsRecord', '_ModerationStatus', 'FileRef' ` -Connection $Connection -ErrorAction Stop }) $etiquetes = [System.Collections.Generic.List[object]]::new() $records = [System.Collections.Generic.List[object]]::new() $enAttente = 0 $labels = [System.Collections.Generic.HashSet[string]]::new() foreach ($item in $items) { $etat = Get-SPMComplianceState -Item $item $chemin = $null try { $chemin = "$($item.FieldValues['FileRef'])" } catch { $chemin = '(chemin inconnu)' } if ($etat.Label) { [void]$labels.Add("$($etat.Label)") $etiquetes.Add([pscustomobject]@{ Path = $chemin; Label = "$($etat.Label)" }) } if ($etat.IsRecord) { $records.Add([pscustomobject]@{ Path = $chemin }) } if ($null -ne $etat.ModerationStatus) { $enAttente++ } } [pscustomobject]@{ Inspected = $items.Count Labeled = $etiquetes.Count Records = $records.Count Pending = $enAttente Labels = @($labels) # Nommer les premiers permet d'aller vérifier ; tout lister noierait le # constat. La troncature est dite, jamais silencieuse. Samples = @($etiquetes | Select-Object -First 10) RecordSamples = @($records | Select-Object -First 10) } } function Set-SPMItemCompliance { <# .SYNOPSIS Réapplique à destination l'étiquette et l'état de modération de la source. .DESCRIPTION Trois états, trois traitements distincts : * ÉTIQUETTE DE RÉTENTION — réappliquée par `Set-PnPListItem -Label`. C'est ce qui rend le document à nouveau non supprimable. * MODÉRATION — réécrite par SystemUpdate. Sans modération active à destination, l'écriture échoue : c'est la réplique des réglages de bibliothèque qui la rend possible, pas cette fonction. * ENREGISTREMENT — NON restaurable par ce chemin. Déclarer un enregistrement est une opération de gestion documentaire, pas une écriture de champ. On le NOMME plutôt que de le taire. .OUTPUTS Objet { Label, Moderation, Record, Problems }. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)]$SourceItem, [Parameter(Mandatory)]$TargetItem, [Parameter(Mandatory)][string]$LibraryTitle, [Parameter(Mandatory)]$Connection ) $source = Get-SPMComplianceState -Item $SourceItem $etat = [ordered]@{ Label = 'sans objet' Moderation = 'sans objet' Record = 'sans objet' Problems = @() } if (-not $source.HasAny) { return [pscustomobject]$etat } # ------------------------------------------------------------ étiquette if ($source.Label) { $nomEtiquette = "$($source.Label)" try { Invoke-SPMWithRetry -Operation 'Set-PnPListItem/label' -MaxRetries 1 -ScriptBlock { Set-PnPListItem -List $LibraryTitle -Identity $TargetItem.Id -Label $nomEtiquette ` -Connection $Connection -ErrorAction Stop } | Out-Null $etat.Label = "réappliquée ($nomEtiquette)" } catch { $message = ($_.Exception.Message -split [char]10)[0] $etat.Label = 'NON réappliquée' $etat.Problems += "étiquette « $nomEtiquette » non réappliquée — $message" } } # ----------------------------------------------------------- modération if ($null -ne $source.ModerationStatus) { $valeurs = @{ '_ModerationStatus' = $source.ModerationStatus } if ($source.ModerationComments) { $valeurs['_ModerationComments'] = $source.ModerationComments } $echecs = @(Set-SPMValueSet -LibraryTitle $LibraryTitle -ItemId $TargetItem.Id ` -Values $valeurs -Connection $Connection -Operation 'moderation') if ($echecs.Count) { $etat.Moderation = 'NON réappliquée' $etat.Problems += 'état de modération non réappliqué — la modération est-elle active à destination ?' } else { $etat.Moderation = "réappliquée ($($source.ModerationStatus))" } } # -------------------------------------------------------- enregistrement if ($source.IsRecord) { # Le taire ferait passer pour migré un document qui a perdu son statut # d'enregistrement — exactement le genre de perte qu'un audit révèle # deux ans plus tard. $etat.Record = 'NON restauré' $etat.Problems += 'déclaré enregistrement à la source : à redéclarer à destination, aucune écriture ne le fait' } return [pscustomobject]$etat } |