Powershell : Robocopy Log Parser

Cela faisait quelques temps que j’avais cette idée dans mon backlog cérébral, mais je m’y suis vraiment penché aujourd’hui car il a fallu que je contrôle un gros nombre de fichiers de log générés par une instruction Robocopy (350+).

Le principal challenge pour ce script est d’arriver à gérer correctement les lignes de statistiques de Robocopy puisque l’export est au simple format texte :

------------------------------------------------------------------------------

               Total    Copied   Skipped  Mismatch    FAILED    Extras
    Dirs :      3765      3765         0         0         0         0
   Files :    243503    243503         0         0         0         0
   Bytes :  19.238 g  19.238 g         0         0         0         0
   Times :  44:22:58   6:23:45                       0:00:00   0:26:35
   Ended : Monday, February 30, 1998 3:25:57 AM

Ces lignes ne sont pas directement interprétables comme telles par Powershell, il faut donc jouer du regex pour arriver à obtenir les valeurs qui nous intéressent. Si au départ je voulais simplement ressortir les FAILED, l’effort supplémentaire pour sortir l’intégralité des valeurs pour les répertoires et les fichiers est minime, ce script va donc inclure dans le fichier CSV de sortie l’intégralité des statistiques pour les répertoires et fichiers.

function parse {
    param($inputstr)
    $newstr = $inputstr -replace "[^0-9]" , '-'
    $newstr -match '[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)'
    $stats = @($matches[1],$matches[2],$matches[3],$matches[4],$matches[5],$matches[6])
    return $stats
}

$output=@()
$logfiles = Get-ChildItem "*.log" | Select-Object Name
foreach($logfile in $logfiles) {
    Write-Host "Parsing $($logfile.Name)"
    $logcontent = Get-Content $logfile.Name
    $strf = $logcontent[$logcontent.Count-5]
    $strd = $logcontent[$logcontent.Count-6]
    $statf = parse($strf)
    $statd = parse($strd)
    $dump = New-Object PSCustomObject
    $dump | Add-Member -Name "Filename" -Value $($logfile.Name) -MemberType NoteProperty
    $dump | Add-Member -Name "Files Total" -Value $statf[1] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Copied" -Value $statf[2] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Skipped" -Value $statf[3] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Mismatched" -Value $statf[4] -MemberType NoteProperty
    $dump | Add-Member -Name "Files FAILED" -Value $statf[5] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Extras" -Value $statf[6] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Total" -Value $statd[1] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Copied" -Value $statd[2] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Skipped" -Value $statd[3] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Mismatched" -Value $statd[4] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs FAILED" -Value $statd[5] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Extras" -Value $statd[6] -MemberType NoteProperty
    $output+=$dump
}
$output | Export-Csv robocopy_logs_stats.csv -Delimiter ";" -Encoding utf8

Le script récupère la liste des fichiers en .log dans le répertoire d’exécution pour ensuite les lire. Par chance, les logs Robocopy ont toujours la même structure, ce qui permet d’avoir des valeurs fixes pour séparer les colonnes et récupérer les valeurs qui nous intéressent.

Ensuite, le fichier CSV est facilement interprété par Excel et on peut voir en un clin d’oeil les éventuelles erreurs de transfert et regarder dans le log concerné quels fichiers ou répertoires sont en échec.

Une version commentée du script est disponible en téléchargement.

Robocopy KO sur partage Azure Files

Azure Files est une solution proposée par Microsoft via Azure permettant de créer des partages SMB liés à des Storage Account. Ainsi, il est possible de pousser ses répertoires partagés sans avoir de serveur de fichiers en tant que tel.

Je travaille actuellement sur un sujet portant sur le déploiement de plusieurs partages portés par Azure Files. Une fois la solution mise en place, ma tâche aura été de faire un robocopy des données locales vers le partage SMB porté par Azure Files. Cependant, en montant le partage sur le serveur de transfert avec un compte ayant les bons privilèges RBAC et NTFS, Robocopy se heurtait à un manque de permissions, y compris en mode backup :

2021/08/10 15:35:59 ERROR 5 (0x00000005) Copying NTFS Security to Destination Directory \\dundermifflin.inc\shares\iso\
Access is denied.

Voici plus ou moins à quoi ressemblait mon instruction Robocopy exécutée par Powershell.

Start-Process "robocopy.exe" -ArgumentList "$src $dst /S /E /COPY:DATSOU /PURGE /MIR /MT:4 /R:1 /W:1" 

Ce qui cause l’erreur de permissions est l’argument S de /copy, ce qui demande à Robocopy de conserver les permissions NTFS des fichiers lors du transfert.

Il faut savoir que les permissions sont habituellement découpées en 2 couches au niveau d’un partage : les privilèges SMB et NTFS. Les permissions sont indépendantes ; si sur un serveur managé il est possible d’intervenir sur les permissions SMB, ce n’est pas le cas avec Azure Files car l’accès est donné au service portant les partages, pas au serveur en lui-même. Par conséquent, les permissions sont redescendues via RBAC. Il existe des rôles Storage File Data SMB qui permettent de placer des droits en lecture et/ou en écriture sur les partages portés par Azure Files et qui positionnent ensuite au niveau SMB les permissions. Dans ce cas, mon compte ayant le rôle Storage File Data SMB Share Elevated Contributor était celui utilisé.

En fouinant un peu sur le net, je suis tombé sur cette question posée sur la section Q&A du site de Microsoft. Un utilisateur a été très récemment confronté au même problème. Cette erreur de permissions est due au fait que Robocopy a besoin de devenir propriétaire du fichier qu’il vient d’écrire à destination pour lui attribuer les permissions NTFS du fichier d’origine. Or, sur Azure Files, même avec un compte ayant les privilèges RBAC et NTFS au maximum, ce n’est pas possible. Robocopy va donc se heurter à un blocage et va renvoyer le message d’erreur plus haut.

En conséquence, pour contourner le problème qui est reconnu — et pour lequel Microsoft travaille sur une solution qui consisterait à donner à un compte un privilège RBAC permettant d’autoriser de devenir propriétaire d’un fichier — Microsoft conseille de monter le partage distant Azure Files avec un compte local d’administration qui lui, possède tous les droits.

Le partage doit donc être monté en tant que AZURE\NomDuStorageAccount ; le mot de passe est l’Access Key qui est générée et déclarée au niveau du Storage Account.

Au niveau du Storage Account, dans la section Security + networking du blade, il suffit de se rendre dans Access keys pour arriver sur cette page. La première clef est le mot de passe du compte local à utiliser. Ainsi, si le compte de stockage s’appelle storagedundermifflin, le partage doit être monté avec l’utilisateur AZURE\storagedundermifflin.

Ce compte possède tous les droits sur le partage et court-circuite les privilèges RBAC. En utilisant ce compte, Robocopy est donc capable de devenir propriétaire des fichiers copiés et d’y appliquer les permissions NTFS d’origine. En exécutant de nouveau le Robocopy, tout s’est bien déroulé et les permissions NTFS ont pu être conservées entre la source et la destination.

PrintNightmare – CVE-2021-34528

Microsoft a publié une vulnérabilité dans le service spouleur d’impression, dont tous les détails peuvent se retrouver à ce lien. Il est important de prendre cette faille au sérieux car elle permet notamment d’exécuter du code en tant qu’utilisateur SYSTEM ce qui peut être désastreux en fonction de la machine sur laquelle l’exécution est réalisée.

Plusieurs workarounds sont proposés par Microsoft, et ayant mis en place les corrections nécessaires, je vais partager les quelques scripts (Powershell 5.1) que j’ai conçu afin de me faciliter la tâche.

Tout d’abord, à partir d’un fichier CSV d’inventaire, ce script va interroger le système distant pour récupérer l’état complet du service Spooler (état actuel, type de démarrage, etc.). Ainsi, il est possible d’avoir un état des lieux sur l’exécution de ce service.

$inv = Import-Csv inventaire.csv
$output = @()
foreach($srv in $inv){
    Write-Output $srv.hostname
    $output+=Get-Service -Name Spooler -Computer $srv.hostname
}
$output | Export-Csv spooler_status.csv -Delimiter ";" -Encoding UTF8

Ce deuxième script va appliquer le workaround conseillé par Microsoft ; il s’agit de l’extinction du service Spooler et la désactivation du démarrage automatique de celui-ci. Le script interroge ensuite de nouveau le service pour obtenir son état et génère un fichier CSV de retour.

$inv = Import-Csv spooler_off.csv -Delimiter ";"
$output = @()
foreach($srv in $inv){
    Write-Host $srv.MachineName
    Stop-Service -InputObject $(Get-Service -Name Spooler -ComputerName $srv.MachineName)
    Set-Service -Name Spooler -StartupType Disabled -ComputerName $srv.MachineName
    $srvstat = New-Object PSCustomObject
    $srvstat | Add-Member -Name "Hostname" -Value $srv.MachineName -MemberType NoteProperty
    $srvstat | Add-Member -Name "SpoolerStatus" -Value (Get-Service -Name Spooler -ComputerName $srv.MachineName).Status -MemberType NoteProperty
    $output+=$srvstat
}
$output | Export-Csv spooler_processed.csv -Encoding UTF8 -Delimiter ";"

Et enfin, puisque Microsoft a mis à jour aujourd’hui la vulnérabilité en incluant un contrôle de clefs de registre qui n’était pas présent hier, ce troisième script va interroger les serveurs d’une liste au format CSV afin de récupérer l’état des clefs de registre mettant à risque le système.

$output = @()
$inv = Import-Csv inventaire_hostname.csv -Delimiter ";"
foreach($srv in $inv){
    Write-Host $srv.Hostname
    $pnp = New-Object PSCustomObject
    if(Invoke-Command -ComputerName $srv.Hostname { Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\" }){
        $pnp | Add-Member -Name "Hostname" -Value $srv.Hostname -MemberType NoteProperty
        $pnp | Add-Member -Name "PointAndPrintKeyExists" -Value "Yes" -MemberType NoteProperty  
        if((Invoke-Command -ComputerName $srv.Hostname { Get-ItemPropertyValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\" -Name NoWarningNoElevationOnInstall }) -eq 1) {
            $pnp | Add-Member -Name "NoWarningNoElevationOnInstall" -Value "1" -MemberType NoteProperty
        }
        else { $pnp | Add-Member -Name "NoWarningNoElevationOnInstall" -Value "0" -MemberType NoteProperty }
        if((Invoke-Command -ComputerName $srv.Hostname {Get-ItemPropertyValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\" -Name NoWarningNoElevationOnUpdate }) -eq 1) {
            $pnp | Add-Member -Name "NoWarningNoElevationOnUpdate" -Value "1" -MemberType NoteProperty
        }
        else { $pnp | Add-Member -Name "NoWarningNoElevationOnUpdate" -Value "0" -MemberType NoteProperty }
    }
    else {$pnp | Add-Member -Name "Hostname" -Value $srv.Hostname -MemberType NoteProperty ; $pnp | Add-Member -Name "PointAndPrintKeyExists" -Value "No or Server Unreacheable" -MemberType NoteProperty }
    $output+=$pnp
}
$output | Export-Csv pointnprint.csv -Delimiter ";" -Encoding UTF8 

En fonction du nombre de serveurs à interroger, l’exécution du script peut être relativement longue. Par souci de rapidité de développement, je n’ai pas inclus de contrôle de réponse du serveur avant d’utiliser Invoke-Command. Il est donc possible que Powershell retourne une erreur si le serveur ne répond pas à la requête ou si il la rejette, cela n’impacte pas le bon déroulement du script.

Naturellement, une étude d’impact et de faisabilité est à faire avant toute intervention et extinction d’un service, même pour le spouleur qui n’est pas d’une importance capitale sur une grande majorité de machines. Il ne reste donc plus qu’à attendre l’application des correctifs de sécurité publiés par Microsoft.

Mes premiers pas sur Python — acte III

Afin de pratiquer Python, j’ai trouvé quelques pages d’exercices sur w3resource. Je n’ai pas réalisé tous les exercices présentés mais plutôt une sélection d’entre eux. Dans cet article, je vais présenter mon code pour chacun d’entre eux, la solution officielle n’étant pas forcément la seule valable. Nombre de ces scripts utilisent des arguments pour être fonctionnels ; vous pouvez appeler l’aide intégrée au script en passant -h en argument. J’ai codé ces snippets avec Python 3.9.1 sur macOS.

Continue reading