PI Services

Le blog des collaborateurs de PI Services

O365 : Réaliser un hard match (Update)

Dans un précédent article j'expliquais comment réaliser un hard match (https://blog.piservices.fr/post/2021/04/01/o365-realiser-un-hard-match).

J'ai récemment eu à me servir de nouveau des cmdlets et ces dernières ne fonctionnaient pas correctement, il est possible de le faire via API Graph, mais les commande Azure AD permettent encore de le faire si besoin.

 

# Get GUID for User
$User = Get-ADUser jdupont | select ObjectGUID,UserPrincipalName
$Upn = $User.UserPrincipalName
$Guid = $User.ObjectGUID.Guid
 
# Convert GUID to ImmutableID
$ImmutableId = [System.Convert]::ToBase64String(([GUID]($User.ObjectGUID)).tobytearray())
 
# Connect Azure AD
Connect-AzureAD

# Retrieve my user
$User = Get-AzureADUser -SearchString "jdupont @mydomain.com"

# Set ImmutableID to user
Set-AzureADUser -ObjectId $User.ObjectID -ImmutableId $ImmutableId

 

SCOM - Widget Powershell Grid - Exemple pour lister les alertes de Discovery/Monitoring

Sous ce titre, le cas de l'utilisation du Widget Powershell Grid dans la console Monitoring de SCOM pour afficher certaines alertes spécifiques.

On commence par creer une vue Dashboard

 

On choisit le template Grid Layout

 

On nomme le dashboard

 

On selectionne 1 cell

 

Cliquer Create

Cliquer "Click to add widget"

Selectionner Powershell Grid Widget

 

Nommer le widget, par exemple avec le meme nom que le dashboard

 

Copier coller le script suivant dans le champ Script

# Get SCOM Alert Related to Discovery and Monitoring Error
$Alerts = Get-SCOMAlert -Criteria "ResolutionState <> 255 AND Name matches '.*(Discovery|Monitoring) (failed|error).*'" #| select name,MonitoringObjectDisplayName,MonitoringObjectPath,LastModified,Description | ft -AutoSize

foreach ($alert in $Alerts)
{

$AlertName = $alert.name

$AlertLastModified = $alert.lastmodified

$MonObjDisplayName = $alert.MonitoringObjectDisplayName.ToString()

$MonObjPath = $alert.MonitoringObjectPath.ToString()

$AlertDesc = $alert.Description.ToString()

$dataObject = $ScriptContext.CreateInstance('xsd://foo!bar/baz')

#Create an ID used for sorting the output - Must be a string value - Used to sort the grid
$dataObject['Id'] = $MonObjDisplayName
$dataObject['name'] = $AlertName
$dataObject['MonitoringPath'] = $MonObjPath
$dataObject['LastModified'] = $AlertLastModified
$dataObject['Description'] = $AlertDesc


# Add the data object to be displayed in the dashboard
  $ScriptContext.ReturnCollection.Add($dataObject)

}

 

Dans ce script on recupere les alertes dont le nom correspond a des erreur de Discovery ou Monitoring.

Ne pas hesiter a faire évoluer les critères pour inclures d'autres cas propre a votre environnement.

Get-SCOMAlert -Criteria "ResolutionState <> 255 AND Name matches '.*(Discovery|Monitoring) (failed|error).*'"

 

Cliquer Create

Cliquer Close

 

La liste des alertes s'affiche

 

 

Active Directory : Lister tous les comptes dont le mot ne passe n'expire jamais

Dans toute annuaire Active Directory il existe une mauvaise pratique, celle dont je voudrais vous parler aujourd'hui est la non expiration des mots de passe pour un / des comptes du domaine. 

Pourquoi ?

Il existe plusieurs arguments au fait qu'un mot de passe qui n'expire jamais est une mauvaise pratique, je mettrais en avant ici les deux qui me posent le plus problème:

  • Tout d'abord un mot de passe qui n'expire jamais a plus de chance d'être "découvert" dans des attaques de type brute force; si on se concentre sur ces comptes en particulier, le fait qu'il n'y ai pas de rotation de mot de passe, laisse une plus grande période de temps à l'attaquant pour le découvrir.
  • Si le compte est compromis, l'attaquant a par définition un accès "constant" au SI, puisque tant que la rotation du mot de passe n'aura pas lieu, ce dernier conservera son accès.

Que faire ?

Bien qu'il ne soit pas une bonne pratique d'autoriser la non expiration des mots de passe, je les croise tous le jours dans tout Active Directory, avec toujours "une bonne raison" de l'avoir fait.

En revanche, même s'il n'est pas possible de s'en séparer, il est tout de même bon de mettre en oeuvre quelques bonne pratiques lorsque l'on est dans cette situation :

  • Lister les comptes dont le mot de passe n'expire jamais.
  • Documenter la cause de cette configuration.
  • Documenter leur emploi (raison d'utilisation, application dans lesquelles ils sont utilisés, machines sur lesquelles ils sont utilisés).
  • Documenter la date du dernier changement de mot de passe (même si elle peut être retrouvée dans l'AD).
  • Documenter une procédure de changement de mot de passe (documentation applicative, processus de dépendance...).
  • Indiquer une personne / équipe en mesure de pouvoir réaliser le changement de mot de passe (il se peut qu'il y ai des développeurs, prestataires externes, éditeurs qui se servent de ce mot de passe). 
  • Réaliser une rotation du mot de passe manuellement.

Lister les comptes avec Powershell.

# Variables
$RootFolder = "C:\Temp"
$Workefolder = "$RootFolder\NeverExpires"
$LogFolder = "$RootFolder\Logs"
$LogFile = "$LogFolder\NeverExpires.txt"
$AllFolders = $RootFolder, $Workefolder, $LogFolder

# Check and create if needed
foreach ($Folder in $AllFolders) {
    If (!(Test-Path $Folder)) {
        Try {
            New-Item $Folder -ItemType Directory -ErrorAction Stop
            }
        Catch {
            Write-Warning $($_)
            }
        }
    }

# Import module
Try {
    Import-Module ActiveDirectory -ErrorAction Stop
    }
Catch {
    Write-Output "Failed to import module ActiveDirectory" | Add-Content $LogFile
    Exit
    }

# Get Active Directory users that have a password that never expires
Try {
    $AllEnabledUsers = Get-ADUser -Filter {Enabled -eq $true} -Properties PasswordNeverExpires -ErrorAction Stop
    $PasswordNeverExpires = $AllEnabledUsers.Where({$_.PasswordNeverExpires -ne $false})
    }
Catch {
    Write-Output "Failed to collect users" | Add-Content $LogFile
    }

# Export result
$PasswordNeverExpires | Export-Csv "$Workefolder\PasswordNeverExpires.csv" -Delimiter ";" -Encoding UTF8

 

Powershell : Change the owner of my GPO

Pour faire suite au précédent articles Powershell : Who's the owner of my AD objects et Powershell : Change the owner of my AD objects voici comment trouver les GPO dont le propriétaire n'est pas "Domain Admins" et les modifier.

# Get Domain
$Domain = Get-ADDomain | select -ExpandProperty NetBIOSName

# Get all GPO
$AllGPO = Get-GPO -All
$AllADGPO = Get-ADObject -Filter {(ObjectClass -eq "groupPolicyContainer")} -Properties displayName

# Filter GPO that are not owned by Domain Admins
$NoGood = $AllGPO.Where({$_.owner -ne "$Domain\Domain Admins"})

# Change owner of all invalid GPO
$NoGood | select -First 1 | foreach {
    $DisplayName = $_.DisplayName
    $Id = $_.ID
    $Guid = $Id.Guid

    $CurrentGpo = $AllADGPO.Where({$_.DisplayName -eq $DisplayName})
    Write-Host $CurrentGpo
    Write-Host $DisplayName -ForegroundColor Magenta
    
    $adsiTarget = [adsi]"LDAP://$($CurrentGpo.DistinguishedName)"
    $idRef = New-Object System.Security.Principal.NTAccount("$Domain", "Domain Admins")
    $adsiTarget.PSBase.ObjectSecurity.SetOwner($idRef)
    $adsiTarget.PSBase.CommitChanges()
    
    
    $DisplayName = $null
    $Id = $null
    }

 

 

Remonter son interface SD RED Sophos.

  1. Contexte:

    Lors d’une mise à jour Firmware, Les FW XGS4300 redémarrent et deviennent tour à tour master, secondaire. Certaines ressources basculées ne rebasculent pas sur le master et restent en UP sur le secondaire. Les ressources touchées sont APs et boitier SD-RED. Il faut alors supprimer puis ajouter l'interface pour le remonter.

  2. Suppression de l'interface

    Pour faire rebasculer la ressource sur le master il est nécessaire de la supprimer et la remonter. Les reboots ou hard reset ne suffisent pas. La particularité est que le boitier Red fourni un PIN à conserver lors de sa suppression et le demande au moment de l’ajout par la suite. Il faut penser à récupérer la plage DHCP créée pour qu’elle fournisse une IP du scope. Lors de la suppression la plage DHCP disparait. Network > Dhcp

    Mémoriser les paramètres sur le maître et le fameux pin. Network > Interface > RED.
    Le pin viable est le dernier fonctionnel donc RED actif sur le secondaire. Le pin sera donc aussi celui du secondaire. 

  3. Ajout de l'interface

    Cliquer sur Add, renseigner les paramètres mémorisés branch name, Type, Red ID, Tunnel ID, Unlock code, Firewall IP, 2nd FW IP, sans oublier la particularité du pin de verrouillage. Sans bascule c’est celui du primaire. Si bascule et up sur le secondaire utiliser le pin du secondaire; Sauvegarder. 

    Au redémarrage ou reset du boitier Red à confirmer, il s’enregistre sur le Cloud Sophos récupère sa config et apparait en up. 
    Allez dans Network > Dhcp pour ajouter une plage DHCP pour ce Red.

    Les ressources connectées à ce RED seront dans la plage paramétrée.

Script Powershell - Directory-TreeSize

Le script ci-dessous est une version modifiée d'un script qui propose une version ligne de commande de l'outil TreeSize pour afficher les tailles récursives d'une arborescence de dossier/Fichiers.

Le script original est disponible ici: http://aka.ms/directory-treesize.ps1

- Une fonction GetSizeToCSV a été ajouté pour generer un export CSV

- Un bloc ValidateScript a été ajouté pour le paramètre Directory

 

<#
.SYNOPSIS
    powershell script to to enumerate directory summarizing in tree view directories over a given size

.DESCRIPTION
    
    .\directory-treesize.ps1 c:\windows\system32

    To enable script execution, you may need to Set-ExecutionPolicy Bypass -Force
         
.NOTES
    File Name  : directory-treesize.ps1
    Version    : 1.0
    History    : 
                CJOURDAN --> Add Function to collect and generate CSV Files (GetSizeToCsv)
                CJOURDAN --> Add ValidateScript for 'directory' parameter validation


.EXAMPLE
    .\directory-treesize.ps1
    enumerate current working directory

.PARAMETER depth
    number of directory levels to display

.PARAMETER detail
    display additional file / directory detail
    output: path, total size of files in path, files in current directory / sub directories, directories in current directory / sub directories 
    example: g:\ size:184.209 GB files:5/98053 dirs:10/19387

.PARAMETER directory
    directory to enumerate

.PARAMETER logFile
    log output to log file

.PARAMETER minSizeGB
    minimum size of directory / file to display in GB

.PARAMETER noColor
    output in default foreground color only

.PARAMETER noTree
    output complete directory and file paths

.PARAMETER quiet
    do not display output

.PARAMETER showFiles
    output file information

.PARAMETER showPercent
    show percent graph

.PARAMETER uncompressed
    for windows file length is used instead of size on disk. this will show higher disk used but does *not* use pinvoke to kernel32
    uncompressed switch makes script pwsh compatible and is enabled by default when path contains '/'
    
.PARAMETER createperfcsv
    Cree un fichier CSV "date;folder;size"

.PARAMETER CsvPerfFile
    Chemin du fichier CSV


#>

[cmdletbinding()]
param(
    
    [Parameter(Mandatory = $true,
               ValueFromPipeline = $true,
               HelpMessage = 'Provide Directory Full Path (Ex: "C:\Temp") ')]
                [ValidateScript({
                                $regex='^\w(:)(\\{1})[^\\].*$'
                                if($_ -notmatch $regex)
                                {
                                $false
                                write-host -B white -F red "$_ N'EST PAS UN CHEMIN DE DOSSIER VALIDE (Ex: 'C:\Temp')"
                                EXIT 1
                                }
                                elseif(!(test-path $_))
                                {
                                $false
                                write-host -B white -F red "LE REPERTOIRE $_ EST INTROUVABLE - VERIFIER LE CHEMIN DU REPERTOIRE"
                                EXIT 1
                                }
                                Else
                                {
                                $true
                                }
                                })]         
                [string]$directory,
    
    [float]$minSizeGB = .01,
    [int]$depth = 99,
    [switch]$detail=$false,
    [switch]$noColor,
    [switch]$notree,
    [switch]$showFiles,
    [string]$logFile,

    [switch]$quiet,
    [switch]$showPercent=$true,
    [switch]$uncompressed,
    [switch]$createperfcsv,
    [string]$CsvPerfFile


)


$timer = get-date
$error.Clear()
$ErrorActionPreference = "silentlycontinue"
$drive = Get-PSDrive -Name $directory[0]
$writeDebug = $DebugPreference -ine "silentlycontinue"
$script:logStream = $null
$script:directories = @()
$script:directorySizes = @()
$script:foundtreeIndex = 0
$script:progressTimer = get-date
$pathSeparator = [io.path]::DirectorySeparatorChar
$isWin32 = $psversiontable.psversion -lt [version]"6.0.0" -or $global:IsWindows

function main()
{
    log-info "$(get-date) starting"
    log-info "$($directory) drive total: $((($drive.free + $drive.used) / 1GB).ToString(`"F3`")) GB used: $(($drive.used / 1GB).ToString(`"F3`")) GB free: $(($drive.free / 1GB).ToString(`"F3`")) GB"
    log-info "enumerating $($directory) sub directories, please wait..." -ForegroundColor Yellow

    $uncompressed = !$isWin32
    [dotNet]::Start($directory, $minSizeGB, $depth, [bool]$showFiles, [bool]$uncompressed)
    $script:directories = [dotnet]::_directories
    $script:directorySizes = @(([dotnet]::_directories).totalsizeGB)
    $totalFiles = (($script:directories).filesCount | Measure-Object -Sum).Sum
    $totalFilesSize = $script:directories[0].totalsizeGB
    log-info "displaying $($directory) sub directories over -minSizeGB $($minSizeGB): files: $($totalFiles) directories: $($script:directories.Count)"

    # si le parametre $createperfcsv est $true Ajout de la collecte au format CSV des données directory et taille avec la date du jour    
    if($createperfcsv)
    {
    GetSizeToCsv -directory $directory -directories $directories
    }



    $sortedBySize = $script:directorySizes -ge $minSizeGB | Sort-Object
        
    if ($sortedBySize.Count -lt 1)
    {
        log-info "no directories found! exiting" -foregroundColor Yellow
        exit
    }

    $categorySize = [int]([math]::Floor([math]::max(1, $sortedBySize.Count) / 6))
    $redmin = $sortedBySize[($categorySize * 6) - 1]
    $darkredmin = $sortedBySize[($categorySize * 5) - 1]
    $yellowmin = $sortedBySize[($categorySize * 4) - 1]
    $darkyellowmin = $sortedBySize[($categorySize * 3) - 1]
    $greenmin = $sortedBySize[($categorySize * 2) - 1]
    $darkgreenmin = $sortedBySize[($categorySize) - 1]
    $previousDir = $directory.ToLower()
    [int]$i = 0

    for ($directorySizesIndex = 0; $directorySizesIndex -lt $script:directorySizes.Length; $directorySizesIndex++)
    {

        $previousDir = enumerate-directorySizes -directorySizesIndex $directorySizesIndex -previousDir $previousDir
        

    }

    log-info "$(get-date) finished. total time $((get-date) - $timer)"
}

function enumerate-directorySizes($directorySizesIndex, $previousDir)
{
    $currentIndex = $script:directories[$directorySizesIndex]
    $sortedDir = $currentIndex.directory
    log-info -debug -data "checking dir $($currentIndex.directory) previous dir $($previousDir) tree index $($directorySizesIndex)"
    [float]$totalSizeGB = $currentIndex.totalsizeGB
    log-info -debug -data "rollup size: $($sortedDir) $([float]$totalSizeGB)"

    switch ([float]$totalSizeGB)
    {
        {$_ -ge $redmin}
        {
            $foreground = "Red"; 
            break;
        }
        {$_ -gt $darkredmin}
        {
            $foreground = "DarkRed"; 
            break;
        }
        {$_ -gt $yellowmin}
        {
            $foreground = "Yellow"; 
            break;
        }
        {$_ -gt $darkyellowmin}
        {
            $foreground = "DarkYellow"; 
            break;
        }
        {$_ -gt $greenmin}
        {
            $foreground = "Green"; 
            break;
        }
        {$_ -gt $darkgreenmin}
        {
            $foreground = "DarkGreen"; 
        }

        default
        {
            $foreground = "Gray"; 
        }
    }

    if (!$notree)
    {
        while (!$sortedDir.Contains("$($previousDir)$($pathSeparator)"))
        {
            $previousDir = "$([io.path]::GetDirectoryName($previousDir))"
            log-info -debug -data "checking previous dir: $($previousDir)"
        }

        $percent = ""

        if ($showPercent)
        {
            if ($directorySizesIndex -eq 0)
            {
                # set root to files in root dir
                $percentSize = $currentIndex.sizeGB / $totalFilesSize
            }
            else 
            {
                $percentSize = $totalSizeGB / $totalFilesSize
            }

            $percent = "[$(('X' * ($percentSize * 10)).tostring().padright(10))]"
        }

        $output = $percent + $sortedDir.Replace("$($previousDir)$($pathSeparator)", "$(`" `" * $previousDir.Length)$($pathSeparator)")
    }
    else
    {
        $output = $sortedDir
    }

    if ($detail)
    {
        log-info ("$($output)" `
            + "`tsize:$(($totalSizeGB).ToString(`"F3`")) GB" `
            + " files:$($currentIndex.filesCount)/$($currentIndex.totalFilesCount)" `
            + " dirs:$($currentIndex.directoriesCount)/$($currentIndex.totalDirectoriesCount)") -ForegroundColor $foreground
    }
    else
    {
        log-info "$($output) `t$(($totalSizeGB).ToString(`"F3`")) GB" -ForegroundColor $foreground
    }

    if ($showFiles)
    {
        foreach ($file in ($currentIndex.files).getenumerator())
        {
            log-info ("$(' '*($output.length))$([int64]::Parse($file.value).tostring("N0").padleft(15))`t$($file.key)") -foregroundColor cyan
        }
    }

   return $sortedDir
}

function log-info($data, [switch]$debug, $foregroundColor = "White")
{
    if ($debug -and !$writeDebug)
    {
        return
    }

    if ($debug)
    {
        $foregroundColor = "Yellow"
    }

    if($noColor)
    {
        $foregroundColor = "White"
    }

    if (!$quiet)
    {
        write-host $data -ForegroundColor $foregroundColor
    }

    if($InformationPreference -ieq "continue")
    {
        Write-Information $data
    }

    if ($logFile)
    {
        if ($script:logStream -eq $null)
        {
            $script:logStream = new-object System.IO.StreamWriter ($logFile, $true)
        }

        $script:logStream.WriteLine($data)
    }
}


function GetSizeToCsv($directory,$directories,$CsvPerfFolder)
{
# Date actuelle
$Now = get-date -Format "dd-MM-yyyy"
# On crée un fichier avec le nom des colonnes. 
$CsvFile = New-Item -Path $CsvPerfFile -Force


"date;folder;size" | Add-Content -Path $CsvFile.FullName
#Pour chacune des lignes de la variable $directories on construis une ligne CSV "date,repertoire,size" qu'on ajoute au fichier
$directories | foreach {Add-Content -Value "$Now;$($_.directory);$($_.totalSizeGB)`n" -Path $CsvFile.FullName} 
}


$code = @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

public class dotNet
{
    [DllImport("kernel32.dll")]
    private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
        [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh);

    [DllImport("kernel32.dll", SetLastError = true, PreserveSig = true)]
    private static extern int GetDiskFreeSpaceW([In, MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName,
       out uint lpSectorsPerCluster, out uint lpBytesPerSector, out uint lpNumberOfFreeClusters,
       out uint lpTotalNumberOfClusters);

    public static uint _clusterSize;
    public static int _depth;
    public static List<directoryInfo> _directories;
    public static float _minSizeGB;
    public static bool _showFiles;
    public static List<Task> _tasks;
    public static DateTime _timer;
    public static bool _uncompressed;
    public static string _pathSeparator = @"\";

    public static void Main() { }
    public static void Start(string path, float minSizeGB = 0.01f, int depth = 99, bool showFiles = false, bool uncompressed = false)
    {
        _directories = new List<directoryInfo>();
        _timer = DateTime.Now;
        _showFiles = showFiles;
        _tasks = new List<Task>();
        _uncompressed = uncompressed;
        _minSizeGB = minSizeGB;

        if(path.Contains("/"))
        {
            _pathSeparator = "/";
        }

        _depth = depth + path.Split(_pathSeparator.ToCharArray()).Count();

        if (!_uncompressed)
        {
            _clusterSize = GetClusterSize(path);
        }

        // add 'root' path
        directoryInfo rootPath = new directoryInfo() { directory = path.TrimEnd(_pathSeparator.ToCharArray()) };
        _directories.Add(rootPath);
        _tasks.Add(Task.Run(() => { AddFiles(rootPath); }));

        Console.WriteLine("getting directories");
        AddDirectories(path, _directories);
        Console.WriteLine("waiting for task completion");

        while (_tasks.Where(x => !x.IsCompleted).Count() > 0)
        {
            _tasks.RemoveAll(x => x.IsCompleted);
            Thread.Sleep(100);
        }

        Console.WriteLine(string.Format("total files: {0} total directories: {1}", _directories.Sum(x => x.filesCount), _directories.Count));
        Console.WriteLine("sorting directories");
        _directories.Sort();
        Console.WriteLine("rolling up directory sizes");
        TotalDirectories(_directories);
        Console.WriteLine("filtering directory sizes");
        FilterDirectories(_directories);

        // put trailing slash back in case 'root' path is root
        if (path.EndsWith(_pathSeparator))
        {
           _directories.ElementAt(0).directory = path;
        }

        Console.WriteLine(string.Format("Processing complete. minutes: {0:F3} filtered directories: {1}", (DateTime.Now - _timer).TotalMinutes, _directories.Count));
        return;
    }

    private static void AddDirectories(string path, List<directoryInfo> directories)
    {
        try
        {
            List<string> subDirectories = Directory.GetDirectories(path).ToList();

            foreach (string dir in subDirectories)
            {
                FileAttributes att = new DirectoryInfo(dir).Attributes;

                if ((att & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
                {
                    continue;
                }

                directoryInfo directory = new directoryInfo() { directory = dir };
                directories.Add(directory);
                _tasks.Add(Task.Run(() => { AddFiles(directory); }));
                AddDirectories(dir, directories);
            }
        }
        catch { }
    }

    private static void AddFiles(directoryInfo directoryInfo)
    {
        long sum = 0;

        try
        {
            DirectoryInfo dInfo = new DirectoryInfo(directoryInfo.directory);
            List<FileInfo> filesList = dInfo.GetFileSystemInfos().Where(x => (x is FileInfo)).Cast<FileInfo>().ToList();
            directoryInfo.directoriesCount = dInfo.GetDirectories().Count();

            if (_uncompressed)
            {
                sum = filesList.Sum(x => x.Length);
            }
            else
            {
                sum = GetSizeOnDisk(filesList);
            }

            if (sum > 0)
            {
                directoryInfo.sizeGB = (float)sum / (1024 * 1024 * 1024);
                directoryInfo.filesCount = filesList.Count;


                if (_showFiles)
                {
                    foreach (FileInfo file in filesList)
                    {
                        directoryInfo.files.Add(file.Name, file.Length);
                    }

                    directoryInfo.files = directoryInfo.files.OrderByDescending(v => v.Value).ToDictionary(x => x.Key, x => x.Value);
                }
            }
        }
        catch { }
    }

    private static void FilterDirectories(List<directoryInfo> directories)
    {
        _directories = directories.Where(x => x.totalSizeGB >= _minSizeGB & (x.directory.Split(_pathSeparator.ToCharArray()).Count() <= _depth)).ToList();
    }

    private static uint GetClusterSize(string fullName)
    {
        uint dummy;
        uint sectorsPerCluster;
        uint bytesPerSector;
        int result = GetDiskFreeSpaceW(fullName, out sectorsPerCluster, out bytesPerSector, out dummy, out dummy);

        if (result == 0)
        {
            return 0;
        }
        else
        {
            return sectorsPerCluster * bytesPerSector;
        }
    }

    public static long GetFileSizeOnDisk(FileInfo file)
    {
        // https://stackoverflow.com/questions/3750590/get-size-of-file-on-disk
        uint hosize;
        string name = file.FullName.StartsWith("\\\\") ? file.FullName : "\\\\?\\" + file.FullName;
        uint losize = GetCompressedFileSizeW(name, out hosize);
        long size;

        if (losize == 4294967295 && hosize == 0)
        {
            // 0 byte file
            return 0;
        }

        size = (long)hosize << 32 | losize;
        return ((size + _clusterSize - 1) / _clusterSize) * _clusterSize;
    }

    private static long GetSizeOnDisk(List<FileInfo> filesList)
    {
        long result = 0;

        foreach (FileInfo fileInfo in filesList)
        {
            result += GetFileSizeOnDisk(fileInfo);
        }

        return result;
    }

    private static void TotalDirectories(List<directoryInfo> dInfo)
    {
        directoryInfo[] dirEnumerator = dInfo.ToArray();
        int index = 0;
        int firstMatchIndex = 0;

        foreach (directoryInfo directory in dInfo)
        {

            if (directory.totalSizeGB > 0)
            {
                continue;
            }

            bool match = true;
            bool firstmatch = false;

            if (index == dInfo.Count)
            {
                index = 0;
            }

            string pattern = string.Format(@"{0}(\\|/|$)", Regex.Escape(directory.directory));

            while (match && index < dInfo.Count)
            {
                string dirToMatch = dirEnumerator[index].directory;

                if (Regex.IsMatch(dirToMatch, pattern, RegexOptions.IgnoreCase))
                {
                    if (!firstmatch)
                    {
                        firstmatch = true;
                        firstMatchIndex = index;
                    }
                    else
                    {
                        directory.totalDirectoriesCount += dirEnumerator[index].directoriesCount;
                        directory.totalFilesCount += dirEnumerator[index].filesCount;
                    }

                    directory.totalSizeGB += dirEnumerator[index].sizeGB;
                }
                else if (firstmatch)
                {
                    match = false;
                    index = firstMatchIndex;
                }

                index++;
            }
        }
    }

    public class directoryInfo : IComparable<directoryInfo>
    {
        public string directory;
        public int directoriesCount;
        public Dictionary<string, long> files = new Dictionary<string, long>();
        public int filesCount;
        public float sizeGB;
        public int totalDirectoriesCount;
        public int totalFilesCount;
        public float totalSizeGB;

        int IComparable<directoryInfo>.CompareTo(directoryInfo other)
        {
            // fix string sort 'git' vs 'git lb' when there are subdirs comparing space to \ and set \ to 29
            string compareDir = new String(directory.ToCharArray().Select(ch => ch <= (char)47 ? (char)29 : ch).ToArray());
            string otherCompareDir = new String(other.directory.ToCharArray().Select(ch => ch <= (char)47 ? (char)29 : ch).ToArray());
            return String.Compare(compareDir, otherCompareDir, true);
        }
    }
}
'@

try
{
    Add-Type $code
    main
}
catch
{
    write-host "main exception: $($error | out-string)"   
    $error.Clear()
}
finally
{
    [dotnet]::_directories.clear()
    $script.directories = $Null

    if ($script:logStream)
    {
        $script:logStream.Close() 
        $script:logStream = $null
    }
}






 

[Exchange Hybride] Les boîtes aux lettres partagées online sont automatiquement transformées en boîtes aux lettres utilisateurs

Problème

Dans un environnement Exchange Hybride et suite à une synchronisation d’annuaires, les boîtes aux lettres online de type utilisateur et qui ont été converties en boîtes aux lettres partagées peuvent revenir de manière inattendue en boîtes aux lettres normales.

 Cause

Le problème se produit parce que l’attribut "RemoteRecipientType" a été mal défini. Cet attribut doit refléter que la boîte aux lettres a été migrée ou encore qu'il s’agit d’une boîte aux lettres partagée --> Si ce n’est pas le cas, la synchronisation d’annuaire réplique les attributs dans le cloud puis la boîte aux lettres partagée sera convertie en "UserMailbox". Si l’utilisateur n'a pas de licence et si la période de grâce de 30 jours est écoulée, la boîte aux lettres sera déconnectée et convertie en "MailUser".

 Résolution

Pour résoudre ce problème, l’attribut "RemoteRecipientType" doit être mis à jour comme suit :

  1. Si la boîte aux lettres dans Exchange Online est déconnectée, attribuez temporairement une licence à l’utilisateur ce qui reconnectera automatiquement la boîte aux lettres.
  2. Si la boîte aux lettres distante n'existe pas, exécutez la commande suivante Enable-RemoteMailbox -Identity PrimarySmtpAddress -RemoteRoutingAddress TargetAddressDomain 

    (Note: La TargetAddressDomain représente votre domaine de coexistence)

  3.  Exécutez ensuite la commande suivante Set-ADUser -Identity ((Get-Recipient PrimarySmtpAddress).samaccountname) -Replace @{msExchRemoteRecipientType=100;msExchRecipientTypeDetails=34359738368}  ce qui permettra de définir qu'il s'agit d'une boîte aux lettres partagée migrée.

 

[Outlook] Le délégué ne peut pas modifier ou supprimer un élément du calendrier Outlook

Contexte et problème 

Dans Microsoft Office 365/ Office 2016, un délégué dispose des autorisations "Editeur" sur un calendrier d’un autre utilisateur dans Microsoft Outlook.

Le délégué peut créer des rendez-vous/réunions mais lorsqu’il tente de supprimer un élément du calendrier, le message d'erreur suivant s'affiche :

 

Cause

Lorsqu'un élément de calendrier est supprimé, Outlook 2016/365 tente de le déplacer vers le dossier "Éléments supprimés" de la boîte aux lettres d'origine. Si le délégué ne dispose pas des autorisations sur le dossier Éléments supprimés, l'opération échouera.

Solutions de contournement 

  • Utiliser Outlook sur le Web pour supprimer les entrées du calendrier --> Il s’agit de la solution la plus simple
  • Comme solution de contournement dans Outlook 2016/O365, attribuer au délégué au moins des autorisations d'auteur sur le dossier "Éléments supprimés"
  • Dans les versions antérieures d'Outlook, ce problème ne se pose pas puisque les éléments supprimés étaient déplacés vers la boîte aux lettres des délégués. Ce comportement est contrôlé via la clé de registre DelegateWasteBasketStyle qui se trouve sous :Ordinateur\HKEY_CURRENT_USER\Software\Policies\Microsoft\Office\16.0\Outlook\options\general Cette clé peut avoir les valeurs : 4 – déplace les éléments supprimés vers le dossier Éléments supprimés dans la boîte aux lettres d'origine. 8 – déplace les éléments supprimés vers le dossier Éléments supprimés dans la boîte aux lettres du délégué (Par défaut, Outlook 2016 définit cette valeur clé sur 4) --> Comme une autre solution de contournement, il est possible de modifier la valeur de cette clé de registre à 8.

Power BI - Exemple de Tranformation de donnée selon la valeur d'une colonne

Dans cet article, un exemple de modification customisé des champs d’une colonne en fonction de la valeur d’une autre colonne.

Dans cet exemple, celui d’une liste d’utilisateurs et de leur pays, on souhaite ajouter un préfixe au nom de l’utilisateur, en fonction de leur pays.

 

On importe les données (dans notre exemple, celui d’un fichier Texte/CSV)

 

Une fois les données importées, on sélectionne Transformer les données

On sélectionne Utiliser la première ligne pour les en-têtes pour changer le nom des colonnes 

On sélectionne Editeur avancé pour afficher la requête complète

AVANT :

 

let

    source = csv.document(file.contents("c:\users\cjourdan\documents\test_data.csv"),[delimiter=";", columns=2, encoding=1252, quotestyle=quotestyle.none]),

    #"type modifie" = table.transformcolumntypes(source,{{"column1", type text}, {"column2", type text}}),

    #"en-tetes promus" = table.promoteheaders(#"type modifie", [promoteallscalars=true]),

    #"type modifie1" = table.transformcolumntypes(#"en-tetes promus",{{"name", type text}, {"country", type text}})

 in

    #"type modifié1"

 

On ajoute à la requête les lignes ci-dessous

APRES :

 

Requête complète :

let

    Source = Csv.Document(File.Contents("C:\Users\cjourdan\Documents\Test_Data.csv"),[Delimiter=";", Columns=2, Encoding=1252, QuoteStyle=QuoteStyle.None]),

    #"Type modifié" = Table.TransformColumnTypes(Source,{{"Column1", type text}, {"Column2", type text}}),

    #"En-têtes promus" = Table.PromoteHeaders(#"Type modifié", [PromoteAllScalars=true]),

    #"Type modifié1" = Table.TransformColumnTypes(#"En-têtes promus",{{"Name", type text}, {"Country", type text}}),

  

    #"Custo Replace value" = Table.ReplaceValue(#"Type modifié1" ,each [Name],each

        if ([Country]="United State") then "US_" & Text.From([Name])

        else if ([Country]="France") then "FR_" & Text.From([Name])

        else if ([Country]="Brazil") then "BR_" & Text.From([Name])

        else if ([Country]="England") then "UK_" & Text.From([Name])

        else if ([Country]="Germany") then "GER_" & Text.From([Name])

        else if ([Country]="Spain") then "SP_" & Text.From([Name])

        else if ([Country]="Italia") then "IT_" & Text.From([Name])

        else if ([Country]="Japan") then "JP_" & Text.From([Name])

        else if ([Country]="Norway") then "NW_" & Text.From([Name])

        else if ([Country]="Russia") then "RU_" & Text.From([Name])

       

        else [Name],Replacer.ReplaceValue,{"Name"})

in

    #"Custo Replace value"

 

 

Apres application, les noms sont modifiés

 

 

 

 

 

 

 

Powershell - Utiliser ValidateScript pour valider une adresse IP en parametre

ValidateScript est un mot clé dans la declaration de paramètres d'un script ou d'une fonction, permettant de valider la valeur d'un paramètre en excutant un bloc de script qui va tester la valeur passée en paramètre. NB: il complète le mot clé ValidatePattern utilisé lui pour valider le paramètre a l'aide d'une expression regulière.

L'exemple ci-dessous est celui de la validation qu'une addresse IP donnée en paramètre est bien au format IPv4. Un message customisé est renvoyé.

 

[CmdletBinding()]
param(

[Parameter(Mandatory=$true,HelpMessage="IP cible")]
          [ValidateScript({
                           $startchar = "^"  # CARACTERE DE DEBUT DE REGEX
                           $endchar = "$"    # CARACTERE DE FIN DE REGEX
                           $ZeroOrOneTime = '?' # CHARACTERE 0 OU 1 FOIS
                           $byte = "(?:25[0-5]|2[0-4][0-9]|[01]$ZeroOrOneTime[0-9][0-9]$ZeroOrOneTime)" # REGEX CORRESPONDANT A UN NOMBRE D'UNE IP
                           $dot = '\.' # CARACTERE '.' 
                           $IPv4 = "$byte$dot$byte$dot$byte$dot$byte" # REGEX COMPLETE D'UNE IPv4
                           if($_ -match "$startchar$IPv4$endchar")
                                {
                                $true
                                } 
                                else 
                                {
                                write-host -B white -F red "$_ N'EST PAS UNE ADDRESSE IPV4 VALIDE. VEUILLEZ RENSEIGNER UNE ADRESSE AU FORMAT X.X.X.X (Ex: 192.168.0.1)"
                                EXIT 1
                                }
                          })]          
$TargetIP
)

Write-Host -F Green "$TargetIP EST UNE VALEUR CORRECTE"

 

Si la valeur renseignée est une adresse IPv4 correcte:

Si la valeur renseignée n'est pas une adresse IPv4 correcte:

 

Le scriptblock éxécuté par ValidateScript peut bien sur être reutilisé comme une fonction a part entière, en dehors du bloc de paramètres.