PI Services

Le blog des collaborateurs de PI Services

Module Powershell SecretStore - Exemple avec API Crowdstrike

SecretStore est un module Powershell permettant de facilement stocker et gérér des Credentials.

Dans l’exemple ci-dessous nous le mettons en place et l’utilisons pour demander, dans cet exemple, un token d’acces au cloud de l’EDR Crowdstrike.

NB : Le module SecretStore requiert l’installation du module SecretManagement

Comme tout module Powershell, si ils ne peut pas être directement téléchargé en ligne sur un repository avec la commande Install-Module, il est possible de le(s) récupérer sur Github ou encore sur Powershell Galery

(https://www.powershellgallery.com/packages/Microsoft.PowerShell.SecretStore)

(https://www.powershellgallery.com/packages/Microsoft.PowerShell.SecretManagement)

Une fois les module décompressé et stocké dans un des dossier contenant en standard des modules (ex : «C:\Program Files\WindowsPowerShell\Modules»)

NB : Si le module n’est pas stocké dans un chemin déclaré dans la variable $env:PSModulePath, son chemin complet devra être renseigné lors de l’import. Ceci peut être problématique aussi après l’import, dans le cadre de certaines commandes du module. Pour cela il est préférable qu’il soit stocké dans un chemin reconnu par la variable $env:PSModulePath

NB : Pour qu’un module soit correctement reconnu par Powershell, le nom du dossier le contenant doit être identique au nom du fichier psd1 ou psm1 :

 C:\Program Files\WindowsPowerShell\Modules \microsoft.powershell.secretstore

Dans une fenêtre powershell, exécuter Import-module en spécifiant le chemin d’accès aux fichiers indiqué ci-dessous

*****

Import-module microsoft.powershell.secretmanagement

Import-module microsoft.powershell.secretstore

*****

Par défaut le module SecretStore requiert un password pour accéder a chaque au coffre-fort de mot passe. La commande suivante permet de spécifier que l’authentification ne sera pas demandée , uniquement pour l’utilisation actuel (CurrentUser)

 

Set-SecretStoreConfiguration -Scope CurrentUser -Authentication None -Interaction None

La commande demande un password qui ne sera pas redemandé

La commande suivante permet de créer le coffre-fort (vault). Indiquer un nom explicite pour l’usage de ce coffre. (La commande requiert d’indiquer le module SecretStore)

Register-SecretVault -ModuleName microsoft.powershell.secretstore -Name MyVault

A présent que le coffre est créé, nous allons y stocker les informations requise, dans cet exemple, pour récupérer un token Crowdstrike.

Dans une variable de type hashtable ($ApiClient) On renseigne le Client Id, Le Client Secret, et le Hostname (url de l’api). On ‘range’  ($ApiClient) dans le coffre MYVault. (Set-secret)

$ApiClient = @{
    ClientId     = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    ClientSecret = ' xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    Hostname     = 'https://api.eu-1.crowdstrike.com'
}

Set-Secret -Name MyApiClient -Secret $ApiClient -Vault MyVault

On peut tester a présent que le credential peut être récupéré :

Get-Secret -Name MyApiClient -Vault MyVault -AsPlainText

Et dans notre exemple, on peut maintenant récupérer notre token Crowdstrike

Get-Secret -Name MyApiClient -Vault MyVault -AsPlainText | ForEach-Object { Request-FalconToken @_ }

Notre token doit etre valable

$(Test-FalconToken).token

Les cmdlets du module PSFalcon peuvent maintenant être exécutées, par exemple :

Get-FalconHost -All -Detailed

NB : Les modules powershell SecretStore et SecretManagement peuvent même etre supprimé (Remove-Module). Le coffre crée sera toujours accessible par le compte l’ayant crée.

Script - Exemple de recherche d'ancien fichier de log dans une arborescence de dossier

Le script ci-dessous propose de rechercher dans une liste de dossiers specifiques (expression regulière FolderPattern) des fichiers de logs plus ou moins anciens, selon un seuil prédéfini (TimeDelta), ajoute un statut au tableau selon le cas rencontré, et exporte le resultat en fichier csv.

 

##############################################
### CHECK OLD LOG FILES ###
##############################################


<# 

    .SYNOPSIS 
        VERIFICATION DE LA PRESENCE ET DE L'ANCIENNETE DU LOG LE PLUS RECENT DANS UNE LISTE DE DOSSIER; 
        UN TABLEAU DES DOSSIERS DONT LE LOG LE PLUS RECENT EST PLUS ANCIEN QUE J-$TimeDelta EST AFFICHE ET EXPORTE EN FICHIER CSV.  

    .PARAMETER  
        RootFolder : Chemin du dossier racine
        TimeDelta : Seuil d'ancienneté du fichier de log le plus recent
        FolderPattern: Expression reguliere pour selectionner des noms de sous dossier
        

 
    .EXAMPLE 
     .\Check_Old_Log_Folders.ps1 -RootFolder "C:\MyRootFolder" -TimeDelta 15
#>




[CmdletBinding()]
param(
[Parameter(Mandatory,HelpMessage="Chemin du dossier racine")]
$RootFolder,

[Parameter(Mandatory,HelpMessage="Seuil d'ancienneté du fichier de log le plus recent")]
$TimeDelta,

[Parameter(HelpMessage="Expression reguliere pour selectionner des noms de sous dossier")]
[regex]$FolderPattern = "^(SRV|DEV).*"

)



# Date du jour - TimeDelta
$DateTimeMinus = (get-date).AddDays(-$TimeDelta)

# Verification que RootFolder existe 
if (!(Test-Path -Path $RootFolder))
    {
    write-host -B White -F Red "UNABLE TO FIND $RootFolder - CHECK PATH - END OF SCRIPT"
    EXIT 1
    }


# Creation d'un PSobject pour stocker les futurs valeurs LogFolder,LastLog et Status
$FileTab = @()

# Recuperation des noms de dossier
$Folders = Get-ChildItem -Path $RootFolder -Directory | Where-Object {$_.Name -match $FolderPattern}


# Pour chacun des dossier on cherche le dernier fichier modifié
$Folders | foreach {


$LogFolder = $_.Name
$LastLog = Get-ChildItem -Path $_.FullName | select -Last 1 -Property LastWriteTime -ExpandProperty LastWriteTime

# Si le dossier est vide (pas de Lastlog) le status est KO
 if (!$LastLog) {$status = "KO - DOSSIER VIDE"}

# Si LastLog est plus ancien ou plus recent que $DateTimeMinus  
 switch($LastLog)
    {
        
        {$LastLog -lt $DateTimeMinus} {$status = "KO - OLDER THAN $DateTimeMinus"}
        {$LastLog -gt $DateTimeMinus} {$status = "OK - NEWER THAN $DateTimeMinus"}
        default {$status = "UNKNOWN"}
    }


# remplissage du tableau
$FileTab += New-Object -TypeName psobject -Property @{LogFolder=$LogFolder;LastLog=$LastLog;Status=$status}

}


# Affiche le tableau $FileTab des dossiers dont le fichier le plus recent est plus ancien que $TimeDelta ou pour lequel il n'y a pas de log
Write-Host -F red -B White "LOG FOLDERS DONT LE FICHIER DE LOG LE PLUS RECENT EST PLUS ANCIEN QUE J $TimeDelta OU POUR LESQUELS IL N'EXISTE PAS DE LOG (DOSSIER VIDE)"
$KOFiles = $FileTab | Where-Object {$_.status -like 'KO*'} | select LogFolder,LastLog,Status | Sort LastLog
$KOFiles | ft -AutoSize


# Export CSV du tableau en supprimant les caractere '"'
$KOFilesCSV = $KOFiles | ConvertTo-Csv -Delimiter "," -NoTypeInformation | ForEach {$_.replace('"','')}


# Export du fichier CSV
$KOFilesCSV | Out-File "$($pwd.ProviderPath)\OldLogFolders.csv" -Force
write-host -F Blue -B White "Old Log Folders CSV File Exported as $($pwd.ProviderPath)\OldLogFolders.csv"


# Ouverture du fichier CSV
notepad.exe "$($pwd.ProviderPath)\OldLogFolders.csv"

 

[Powershell] - Autoriser l'envoie sur une liste de distribution dans un environnement en Split Model Permission

Contexte :

Dans le cas ou vous implémentez l'Active Directory Split Model Permission, vous réduisez les permissions de l'Exchange sur L'AD et par conséquent une partie des commandes disponible depuis l'Exchange.

Problème :

La modification doit être faite sur les fiches des objets AD et il faut connaitre l'attribut à modifier.

Par exemple pour autoriser un ou des utilisateur(s) à écrire à une liste de distribution, il faut pour chaque utilisateur ajouter son "DistinguishedName" dans l'attribut "authOrig" de la liste en question.

Solution :

Si vous ne souhaitez pas éditez les fiches AD, aller dans l'onglet éditeur d'attributs et éditer / modifier l'attribut (déjà 4 clics avec la validation) vous pouvez utiliser Powershell avec la ligne de commande suivante :

Set-ADGroup -Identity "SamAccountName_Du_Groupe" -Add @{authOrig=@("DistinguishedName_Du_User")}

Mais si vous devez le faire pour plusieurs utilisateurs et ne pas traiter cela unitairement, il vous est possible d'utiliser la fonction suivante en passant plusieurs utilisateurs pour l'attribut "SamAccountName" séparés par une virgule :

Function Allow-ToSendTo {
    param (
    [Parameter(Mandatory)][String]$GroupName,
    [Parameter(Mandatory)][String[]]$SamAccountName
    )

    Try {
        Get-ADGroup $GroupName -ErrorAction Stop
        foreach ($Sam in $SamAccountName) {
            Try {
                $CurrentUser = Get-ADUser $Sam -ErrorAction Stop
                Try {
                    Set-ADGroup -Identity $GroupName -Add @{authOrig=@($CurrentUser.DistinguishedName)} -ErrorAction Stop
                }
                Catch {
                    Write-Warning $($_)
                    }
            }
            Catch {
                Write-Warning $($_)
            }
        }
    }
    Catch {
        Write-Host "Group Not found sorry" -ForegroundColor Yellow
    }
}

 

PowerShell - créer et déléguer des Administrative Units

Une présentation des Administrative Units (AU) a été réalisée dans l'article suivant : Azure - présentation des Administrative Units

La délégation des Administrative Units en interface graphique a été expliquée dans l'article suivant :  Azure - déléguer les Administrative Units

 

Créer et déléguer des Administrative Units en PowerShell

#Import les modules AzureAD et AzureADPreview qui contient les commandes utilisées pour la création des Administrative Units (AU)
#Si certaines commandes ne sont pas reconnues, assurez vous d'avoir le module à jour avec la commande Update-Module
Import-Module AzureAD
Import-Module AzureADPreview

#Initie une connexion avec AzureAD dans la console PowerShell, un compte avec les droits Global Administrator ou Privileged Role Administrator est nécessaire
Connect-AzureAD

#Création d'une nouvelle AU, la commande est sauvegardé dans une variable car l'objectID doit être utilisé pour la délégation
#Par défaut l'affectation des objets à l'AU sera Assigned, pour faire de l'affectation dynamique utiliser les paramètres MembershipType, MembershipRuleProcessingState et MembershipRule 
New-AzureADMSAdministrativeUnit -Description "Test Administrative Unit created with PowerShell" -DisplayName "AdministrativeUnit-test2"

#Les propriétés de l'AU sont récupérées car l'object ID est nécessaire pour faire l'affectation des droits au groupe de délégation dans PIM
$NewAU = Get-AzureADAdministrativeUnit -Filter "displayname eq 'AdministrativeUnit-test2'"

#Création d'un groupe de délégation qui aura la délégation sur l'AU créée
#C'est un groupe de sécurité, la partie mail est donc désactivé (MailEnabled $False) et la partie sécurité est activé (SecurityEnabled $True)
#Le paramètre MailNickname est obligatoire, il est donc recommandé de rentrer la même valeur que le displayName
#Pour pouvoir affecter des rôles via PIM, il est nécessaire d'activer le paramètre IsAssignableToRole
#Le paramètre Visibility permet de modifier qui est autorisé à voir les membres du groupe, il est recommandé de choisir soit Private soit HiddenMembership
New-AzureADMSGroup -DisplayName "Group-to-manage-AdministrativeUnit-test2" -Description "Group that have the delegation on the AU AdministrativeUnit-test2" `
-MailEnabled $False -MailNickname "Group-to-manage-AdministrativeUnit-test2" -SecurityEnabled $True -IsAssignableToRole $True -Visibility "Private"

#L'affectation des droits au groupe de délégation dans PIM via la commande New-AzureADMSRoleAssignment nécessite l'ID du groupe de délégation
$NewAzureGroup = Get-AzureADGroup -Filter "displayname eq 'Group-to-manage-AdministrativeUnit-test2'"

#L'affectation des droits au groupe de délégation dans PIM via la commande New-AzureADMSRoleAssignment nécessite l'ID du rôle
#Cet ID peut être récupérer depuis portal.azure.com > Azure Active Directory > Administrative Units > {Cliquer sur une AU existante} >
#Roles and admininistrators > {Choisir le rôle a affecté} > Description > Template ID
#Le template ID choisi est celui d'User administrator
$RoleDefinitionID = Get-AzureADMSRoleDefinition -ID "fe930be7-5e62-47db-91af-98c3a49a38b1"

#L'affectation des droits au groupe de délégation dans PIM via la commande New-AzureADMSRoleAssignment nécessite l'ID de l'AU
$DirectoryScopeId = '/administrativeUnits/' + $NewAU.ObjectId

#Création de la délégation PIM du groupe Group-to-manage-AdministrativeUnit-test2 sur l'AU AdministrativeUnit-test2
New-AzureADMSRoleAssignment -DirectoryScopeId $DirectoryScopeId -RoleDefinitionId $RoleDefinitionID.Id -PrincipalId $NewAzureGroup.ObjectId

 

Vérification de la création et de la délégation

L'Administrative Unit a bien été créé

 

Le groupe de délégation ainsi que son affectation dans PIM est effective

 

Remarque : lors de l'installation du module AzureADPreview, celui-ci entre en conflit avec le module AzureAD s'il est déjà installé, lors de l'utilisation de la commande Install-Module, utiliser les paramètres AllowClobber et Force

 

Pour aller plus loin

Documentation officielle de la commande New-AzureADMSAdministrativeUnit
Documentation officielle de la commande Get-AzureADAdministrativeUnit
Documentation officielle de la commande New-AzureADMSGroup
Documentation officielle de la commande Get-AzureADGroup
Documentation officielle de la commande Get-AzureADMSRoleDefinition
Documentation officielle de la commande New-AzureADMSRoleAssignment

Scripting - Exemple de la recuperation de la date de modification du password d'un compte AD

Problématique:  Récuperer et rendre lisible un timestamp représentant la date de modification du password d'un compte AD, renvoyé par l'outil Dsquery (https://ss64.com/nt/dsquery.html)

 

$user = "johndoe"
$domain = "mydomain.com"

# Executer la commande dsquery pour recuperer l'attribut pwdLastSet
$obj = dsquery * -filter "samaccountname=$user" -attr displayName pwdLastSet -d $domain

# Decouper $obj pour ne recuperer que la chaine correspondant au timestamp
$TimeStamp = $($obj[1] -split " ") | Where-Object {$_ -match '^\d+$'}

# Convertir le timestamp en date avec l'outil w32tm.exe
w32tm.exe /ntte $TimeStamp



Powershell : WSUS Report Servers States

Voici un script qui vous permettra de réaliser un état des lieu sur les mises à jour de sécurité nécessaires pour l'ensemble de vos serveurs rattachés à un WSUS.

Attention : Prenez soins de modifier les variables suivantes :

  1. Sur la ligne 12 vous pouvez remplacer le "-30" par un valeur plus élevé si vous souhaitez un delta plus large (le -30 correspondant à M - 1, et ainsi connaitre le nombre de mise à jours manquante à J-30 jours)
  2. Sur la ligne 14 $Alias (doit correspondra à l'alias renseigné dans votre DNS).
  3. Ligne 44 vous pouvez ajouter un -Credential (si vous n'exécutez pas ce script dans un contexte avec les droits nécessaires).

 

Ce dernier est une version pour un WSUS unique en mode autonome ou déconnecté, ne possédant pas de Downstram Server, je publierais une autre version pour ce cas de figure.

#################################
# Step 1 : Variables Definition #
#################################
#region - Variables
$RootFolder = "C:\Temp\WSUS"
$ResultFolder = "$RootFolder\Result"
$LogFolder = "$RootFolder\Logs"
$Archives = "$RootFolder\Archives"
$AllFolders = $RootFolder, $ResultFolder, $LogFolder, $Archives
$LogFile = "$LogFolder\GeneralLog.log"
$Date = Get-Date -Format M
$TargetDate = ((Get-Date).AddDays(-30)).ToString('MM/dd/yyyy')
$Array = @()
$Alias = "wsus"

# Folders creation
foreach ($Folder in $AllFolders) {
    If (!(Test-Path $Folder)) {
        New-Item $Folder -ItemType Directory
    }
}

# Old Files deletion
Get-ChildItem $LogFolder | Remove-Item -Force
Get-ChildItem $ResultFolder | Remove-Item -Force
#endregion - Variables

###############################
# Step 2 : Locate WSUS Server #
###############################
#region - Locate WSUS Server
Write-Output "Trying to locate WSUS server" | Add-Content $LogFile
Try {
    $WSUSServer = Resolve-DnsName $Alias -ErrorAction Stop | select -ExpandProperty Name -Last 1
    Write-Output "Successfully locate WSUS server" | Add-Content $LogFile
}
Catch {
    Write-Output "Failed to locate WSUS server" | Add-Content $LogFile
}

# Wsus Role Check
Write-Output "Verifying WSUS server role installation" | Add-Content $LogFile
Try {
    $WsusRoleInstalled = Invoke-Command -ComputerName $WSUSServer -ScriptBlock {Get-WindowsFeature | Where-Object {($_.InstallState -eq "Installed") -and ($_.Name -like "UpdateServices")}} -ErrorAction Stop | Select-Object -Property Installed
    Write-Output "Successfully Run the query" | Add-Content $LogFile
    If ($WsusRoleInstalled.Installed -ne $true) {
        # End of Script
        Write-Output "WSUS Server not found" | Add-Content $LogFile
        Exit
    }
}
Catch {
    Write-Output "Failed to verify WSUS server role installation" | Add-Content $LogFile
    Exit
}
#endregion - Locate WSUS Server

############################
# Step 3 : WSUS Connection #
############################
#region - WSUS Connection
# Try on default port 8530
Try {
    $WsusPort = "8530"
    [void][reflection.assembly]::loadwithpartialname("microsoft.updateservices.administration")
    $Wsus = [microsoft.updateservices.administration.adminproxy]::getupdateserver($WsusServer,$false,$WsusPort)
    $Connection = $Wsus.Name
}
Catch {
    # Try on port 80
    Try {
        $WsusPort = "80"
        [void][reflection.assembly]::loadwithpartialname("microsoft.updateservices.administration")
        $Wsus = [microsoft.updateservices.administration.adminproxy]::getupdateserver($WsusServer,$false,$WsusPort)
        $Connection = $Wsus.Name
    }
    Catch {
        # Try on port 8531
        Try {
            $WsusPort = "8531"
            [void][reflection.assembly]::loadwithpartialname("microsoft.updateservices.administration")
            $Wsus = [microsoft.updateservices.administration.adminproxy]::getupdateserver($WsusServer,$true,$WsusPort)
            $Connection = $Wsus.Name
        }
        Catch {
            # Try on port 443
            Try {
                $WsusPort = "443"
                [void][reflection.assembly]::loadwithpartialname("microsoft.updateservices.administration")
                $Wsus = [microsoft.updateservices.administration.adminproxy]::getupdateserver($WsusServer,$true,$WsusPort)
                $Connection = $Wsus.Name
            }
            Catch {
                # End of Script
                Write-Output "WSUS Server connection failed on all ports" | Add-Content $LogFile
                Exit
            }
        }
    }
}
#endregion - WSUS Connection

###########################
# Step 4 : WSUS Reporting #
###########################
#region - WSUS Reporting
# Variables Definition
$computerscope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$updatescope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateClassifications = $Wsus.GetUpdateClassifications() | Where-Object {$_.Title -eq "Security Updates"}
$UpdateScope.Classifications.AddRange($updateClassifications)
$Servers = $wsus.GetComputerTargets($computerscope) | Where-Object {($_.ComputerRole -eq "Server")}
$NBServers = $Servers.Count

Write-Output "WSUS Server has $NBServers to manage" | Add-Content $LogFile
$Servers | Foreach {
    $FullDomainName = $_.FullDomainName
    $WorkstationID = $_.Id
    $LastSyncClient = $_.LastSyncTime
    [STRING]$LastSyncResultClient = $_.LastSyncResult
    $LastReportedStatusTime = $_.LastReportedStatusTime
    $IPAddress = ($_.IPAddress).IPAddressToString
    $Id = $_.Id
    $Make = $_.Make
    $Model = $_.Model
    $OSLanguage = ($_.OSInfo).DefaultUILanguage
    $OSArchitecture = $_.OSArchitecture
    $OSFamily = $_.OSFamily
    $OSDescription = $_.OSDescription
    $ComputerTargetGroupIds = $_.ComputerTargetGroupIds
    $SyncsFromDownstreamServer = $_.SyncsFromDownstreamServer
    $UpdateServer = $_.UpdateServer.Name

    # Get AD Information
    Try {
        Write-Output "Trying to retrieve Active Directory Informations for $FullDomainName" | Add-Content $LogFile
        $CurrentServer = Get-ADComputer -Filter {DNSHostName -eq $FullDomainName} -Properties CanonicalName -ErrorAction Stop | Select-Object Enabled,CanonicalName,OperatingSystem,Name
        Write-Output "Successfully retrieve Active Directory Informations for $FullDomainName" | Add-Content $LogFile
    }
    Catch {
        Write-Output "Failed to retrieve Active Directory Informations for $FullDomainName" | Add-Content $LogFile
    }
    # Define state for Begining and End of year, plus a targeted date (here M-30)
    $updatescope.ToCreationDate=[DATETIME]"01/31/2022"
    $KBToInstallJanuary = $wsus.GetSummariesPerComputerTarget($updatescope,$computerscope) | where {($_.ComputerTargetId -eq $WorkstationID)} |Select-Object -ExpandProperty NotInstalledCount
    $updatescope.ToCreationDate=[DATETIME]"12/31/9999"
    $KBToInstallUpToDate = $wsus.GetSummariesPerComputerTarget($updatescope,$computerscope) | where {($_.ComputerTargetId -eq $WorkstationID)} |Select-Object -ExpandProperty NotInstalledCount
    $updatescope.ToCreationDate=[DATETIME]"$TargetDate"
    $KBToInstallJ30 = $wsus.GetSummariesPerComputerTarget($updatescope,$computerscope) | where {($_.ComputerTargetId -eq $WorkstationID)} |Select-Object -ExpandProperty NotInstalledCount
    $IPAddress = ($_."IPAddress").IPAddressToString
    
    $Array += New-Object psobject -Property @{
        FullDomainName = $FullDomainName
        WorkstationID = $Id
        LastSyncClient = $LastSyncClient
        LastSyncResultClient = $LastSyncResultClient
        LastReportedStatusTime = $LastReportedStatusTime
        IPAddress = $IPAddress
        Enabled = $CurrentServer.Enabled
        CanonicalName =$CurrentServer.CanonicalName
        Name = $CurrentServer.Name
        KBNeededInJanuary = $KBToInstallJanuary
        KBNeededLastMonth = $KBToInstallJ30
        KBNeededToBeUpToDate = $KBToInstallUpToDate
        Id = $Id
        Make = $Make
        Model = $Model
        OSLanguage = $OSLanguage
        OSArchitecture = $OSArchitecture
        OSFamily = $OSFamily
        OSDescription = $OSDescription
        ComputerTargetGroupIds = $ComputerTargetGroupIds
        SyncsFromDownstreamServer = $SyncsFromDownstreamServer
        UpdateServer = $UpdateServer
    }
    # Release
    $FullDomainName = $null
    $WorkstationID = $null
    $LastSyncClient = $null
    $LastSyncResultClient = $null
    $LastReportedStatusTime = $null
    $IPAddress = $null
    $Id = $null
    $Make = $null
    $Model = $null
    $OSLanguage = $null
    $OSArchitecture = $null
    $OSFamily = $null
    $OSDescription = $null
    $ComputerTargetGroupIds = $null
    $SyncsFromDownstreamServer = $null
    $CurrentServer = $null
    $UpdateServer = $null
    $KBToInstallJanuary = $null
    $KBToInstallUpToDate = $null
    $KBToInstallJ30 = $null
}
#endregion - WSUS Reporting

########################
# Step 4 : Export Data #
########################
#region - Export Data
$Array | select WorkstationID,Make,Model,OSFamily,OSDescription,OSArchitecture,OSLanguage,IPAddress,FullDomainName,Name,CanonicalName,Enabled,SyncsFromDownstreamServer,UpdateServer,LastReportedStatusTime,LastSyncClient,LastSyncResultClient,KBNeededInJanuary,KBNeededLastMonth,KBNeededToBeUpToDate | Export-Csv $ResultFolder\Report.csv -Delimiter ";" -Encoding UTF8 -NoTypeInformation

#endregion - Export Data

 

 

Powershell : Lister les membres externes des OneDrives

Attention au côté juridique de cette opération. 

Dans certain cas de figure nous sommes amenés à devoir faire une liste de tout accès externe à différents types de ressources, dans le cas présent nous allons lister l'ensemble des OneDrives avec un partage externe, pour ce faire vous aurez besoin de :

  • Powershell 5.1,
  • Le module Sharepoint Online,
  • un accès Sharepoint Admin.

Vous pouvez maintenant lancer le script en prenant soin de modifier les url de connexion (L2 et L59) 

# Connection to Sharepoint Online
Connect-SPOService -Url https://contoso-admin.sharepoint.com # (Please change the connection URL)

# Get the list of All OneDrives
$AllOneDrive = Get-SPOSite -IncludePersonalSite $true -Limit all -Filter "Url -like '-my.sharepoint.com/personal/'"

# Define variables
$ArrayOneDriveMembers = @()
$SortOneDrive = $AllOneDrive | sort Url
$X = 0

$SortOneDrive | foreach {
    $OneDriveUrl = $_.Url
    $OneDriveOwner = $_.Owner
    $OneDriveStorageQuota = $_."Storage Quota"
    $OneDriveTitle = $_.Title
    # Get Member of current OneDrive
    Try {
        $OneDriveMembers = Get-SPOUser -Site $OneDriveUrl -ErrorAction stop
        }
    Catch {
        Write-Warning $($_)
        Write-Output "$OneDriveUrl;$OneDriveTitle" | Add-Content C:\temp\Onedriveerrors.txt
        }
    
    # Get External members
    $OneDriveExternal = $OneDriveMembers.where({$_.Usertype -eq "Guest"})
    If ($OneDriveExternal.count -ne 0) {
        Write-Host "External Users are present in $OneDriveTitle" -ForegroundColor Green
        $OneDriveExternal.count
        $OneDriveExternal | foreach {
            $DisplayName = $_.DisplayName
            $LoginName = $_.LoginName

            # Store Data
            $ArrayOneDriveMembers += New-Object psobject -Property @{
                OneDriveUrl = $OneDriveUrl
                OneDriveOwner = $OneDriveOwner
                OneDriveStorageQuota = $OneDriveStorageQuota
                OneDriveTitle = $OneDriveTitle
                DisplayName = $DisplayName
                LoginName = $LoginName
                }
            # Release
            $DisplayName = $null
            $LoginName = $null
            }
        }
    # Release
    $OneDriveUrl = $null
    $OneDriveOwner = $null
    $OneDriveStorageQuota = $null
    $OneDriveTitle = $null
    $OneDriveMembers = $null
    
    # After 500 connection, reconnect to Sharepoint
    If ($x -eq 500) {
        # Connect to Sharepoint
        Connect-SPOService -Url https://contoso-admin.sharepoint.com 
        $x =0
        }
    $x++
    }

# Export Data
$ArrayOneDriveMembers | Export-Csv c:\temp\ExternalOneDriveMembers.csv -Encoding UTF8 -Delimiter ";" -NoTypeInformation

 

Script - Crowdstrike - Exemple d'utilisation du module PSFalcon

Falcon Crowdsrike est l'un des antivirus next generation (EDR) les plus en vue et les plus performants du marché.  Le script ci-dessous propose un exemple de requetage de l'API de Crowdstrike via l'utilisation du module Powershell PSFalcon (https://www.powershellgallery.com/packages/PSFalcon)

L'acces et l'utilisation de l'Api Crowdstrike requiert bien sur un abonnement Crowdstrike (https://www.crowdstrike.com/)

Le script utilisant pour l'acces a l'API, une clé d'encryptage (AES.key), un fichier contenant le SecID (SecID.txt), et un fichier contenant la passphrase (pass.txt), il est necessaire de recreer ces éléments avec le contexte de l'abonnement auquel on se connecte (SecID).

Le script interroge l'API pour recuperer des éléments tel que les hotes, les comportements (Behaviors), les detections, les incidents.

 

QueryCrowdstrike.ps1 (11,62 kb)

 

##############################################
### QUERY CROWDSTRIKE WITH PSFALCON MODULE ###
##############################################


<# 

    .SYNOPSIS 
        INTERROGATION D'UN TENANT FALCON CROWDSTRIKE VIA L'UTILISATION DU MODULE POWERSHELL PSFALCON
        NB: LES DONNEES SONT EXTRAITES SOUS FORME D'UN FICHIERS CSV 

    .PARAMETER  
        ClientID : Client ID crowdstrike
        Pass : Password du compte d'acces a l'API
        
        ExportFolder : Dossier d'export du fichier CSV
        LogFolder : Chemin du dossier où creer le log du script

 
    .EXAMPLE 
     .\QueryCrowdstrike.ps1 -CloudUrl "https://api.eu-1.crowdstrike.com"  -ClientID <clientid> -Pass <pass> -ExportFolder ./ -LogFolder ./
#>


[CmdletBinding()]
param(
[Parameter(Mandatory,HelpMessage="Cloud URL")]
$CloudUrl,

[Parameter(HelpMessage="Client ID crowdstrike")]
$ClientID,

[Parameter(HelpMessage="Password du compte d'acces a l'API")]
$Pass,

[Parameter(Mandatory,HelpMessage="Dossier d'export du fichier CSV")]
[string]$ExportFolder="./CSV",

[Parameter(Mandatory,HelpMessage="Chemin du dossier où creer le log du script")] 
[string]$LogFolder="./",

[Parameter(Mandatory,HelpMessage="Chemin de la clé utilisée pour l'encryption du CID et de la passphrase")] 
$KeyFile = "D:\MyFolder\CROWDSTRIKE\AES.key",

[Parameter(Mandatory,HelpMessage="Chemin du fichier contenant le SecID du client")] 
$SecIDFile = "D:\MyFolder\CROWDSTRIKE\SecID.txt",

[Parameter(Mandatory,HelpMessage="Chemin du fichier contenant la passphrase")] 
$PassFile = "D:\MyFolder\CROWDSTRIKE\Pass.txt"


)


# TO RENEW KEY FILE USED FOR CLIENT ID AND PASSPHRASE ENCRYPTION
<#
    # Generate Key File
    $Key = New-Object Byte[] 16   # You can use 16, 24, or 32 for AES
    [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
    $Key | out-file $KeyFile

#>


# TO RENEW SECID ENCRYPTION, EXECUTE THE CODE BETWEEN <# #>

<#
    # Create secure string object
    $SecIDFile = "D:\MyFolder\CROWDSTRIKE\SecID.txt"
    $Key = Get-Content $KeyFile
    $SecID = read-host  -Prompt "Enter client SecID" -AsSecureString
    $SecID | ConvertFrom-SecureString -key $Key | Out-File $SecIDFile

#>


# TO RENEW PASSPHRASE ENCRYPTION, EXECUTE THE CODE BETWEEN <# #>

<#
    # Create secure string object
    $PassFile = "D:\MyFolder\CROWDSTRIKE\Pass.txt"
    $Key = Get-Content $KeyFile
    $Pass = read-host  -Prompt "Enter client PassPhrase" -AsSecureString
    $Pass | ConvertFrom-SecureString -key $Key | Out-File $PassFile

#>






# OTHER VARIABLES
# Script Name
$ScriptName = "QueryCrowdstrike.ps1"
# LogName = ScriptName without extension
$Log = $ScriptName.Split('.')[0]


# FUNCTIONS


function Write-Log 
{ 
    <# 
    .SYNOPSIS 
        This function creates or appends a line to a log file. 
 
    .PARAMETER  Message 
        The message parameter is the log message you'd like to record to the log file. 
 
    .EXAMPLE 
        PS C:\> Write-Log -Message 'Value1' 
        This example shows how to call the Write-Log function with named parameters. 
    #> 
    [CmdletBinding()] 
    param ( 
        [Parameter(Mandatory)] 
        [string]$Message,
        [Parameter(Mandatory)] 
        [string]$LogPath, 
        [Parameter(Mandatory)] 
        [string]$LogName
        
    ) 
     
    try 
    { 
        $DateTime = Get-Date -Format ‘MM-dd-yy HH:mm:ss’ 
        Add-Content -Value "$DateTime - $Message" -Path "$LogPath\$LogName.log" 
    } 
    catch 
    { 
        Write-Error $_.Exception.Message 
    } 
} 



            




# Requis si l'import du module echoue en en renvoyant plusieurs messages 'Unable to find [System.Net.Http.(...)*]' 
Add-Type -AssemblyName System.Net.Http 

# Import Module
Import-Module -Name D:\MyFolder\CROWDSTRIKE\Powershell_Module\psfalcon-master\PSFalcon.psm1



$Key = Get-Content $KeyFile

# Get file content and Decrypt ClientID
$ClientID = Get-Content $SecIDFile | ConvertTo-SecureString -Key $key
$ClientID = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientID))

# Get file content and Decrypt Pass
$Pass = Get-Content $PassFile | ConvertTo-SecureString -Key $key
$Pass = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Pass))







# Query for Falcon Token
try
    {
    Request-FalconToken -ClientId $ClientID -ClientSecret $Pass -Hostname $CloudUrl
    }
catch
    {
    $Message = "Error during Request-FalconToken - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }


# Test if Test-FalconToken return True
if ($(Test-FalconToken).token -eq $true)  
      {
      $Message = "Token is valid - OK"
      write-host -F Green $Message
      Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
      }
Else
    {
    $Message = "Problem with Token - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }




# GET DATA

#region Falcon Hosts 

# Get Falcon Hosts (exemple option -filter:  –Filter "hostname:'SRV'")
try
    {
    $FalconHosts = Get-FalconHost -All -Detailed
    #$FalconHosts | select hostname,os_version,agent_version,status | ft -AutoSize
    }
catch
    {
    $Message = "Problem during Get-FalconHost - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }


# Convert To CSV
 try
    {
    $FalconHostsCSV = $($FalconHosts | select `
        hostname,
        os_version,
        platform_name,
        status,
        agent_version,
        config_id_base,
        config_id_build,
        @{Name='First_Seen';Expression={[datetime]::ParseExact($_.first_seen.Replace('T','').Replace('Z',''),'yyyy-MM-ddHH:mm:ss',$null)}},
        @{Name='Last_Seen';Expression={[datetime]::ParseExact($_.last_seen.Replace('T','').Replace('Z',''),'yyyy-MM-ddHH:mm:ss',$null)}},
        reduced_functionality_mode | ConvertTo-Csv -Delimiter ',' -NoTypeInformation).Replace('"','')
    
    # Display CSV File
    $FalconHostsCSV
    }
catch
    {
    $Message = "Error during convert to CSV file for FalconHosts - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }

# Export CSV
 try
    {   
    $FalconHostsCSV | Out-File -FilePath "$ExportFolder`/FalconHosts.csv" -Force
    }
catch
    {
    $Message = "Error during export of CSV file for FalconHosts - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }


#endregion Falcon Hosts 







#region Falcon Detection




#Get-FalconDetection
write-host "FALCON DETECTION EVENTS" -B White -f Blue
try
    {
    $FalconDetection = Get-FalconDetection -Detailed -All  
    $FalconDetection | sort created_timestamp -Descending
    }
catch
    {
    $Message = "Error during Get-FalconDetection - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }


# Convert To CSV
 try
    {
    $FalconDetectionCSV = $($FalconDetection | select `
    detection_id,
    @{Name='Detection_Creation_Date';Expression={[datetime]::ParseExact($_[0].created_timestamp.split('.')[0].Replace('T',''),'yyyy-MM-ddHH:mm:ss',$null)}},
    @{Name='HostName';Expression={$_.device.hostname}},
    @{Name='HostIP';Expression={$_.device.local_ip}},
    @{Name='DeviceID';Expression={$_.device.device_id}},
    @{Name='BehaviorID';Expression={$_.Behaviors.Behavior_id}},
    @{Name='Behavior_FileName';Expression={$_.Behaviors.filename}},
    @{Name='Behavior_FilePath';Expression={$_.Behaviors.filepath}},
    @{Name='Behavior_cmdline';Expression={$_.Behaviors.cmdline}},
    status,
    max_severity_displayname,
    email_sent | ConvertTo-Csv -Delimiter ',' -NoTypeInformation).Replace('"','')
    }
catch
    {
    $Message = "Error during convert to CSV file for FalconDetection - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }

# Export CSV
 try
    {   
    $FalconDetectionCSV | Out-File -FilePath "$ExportFolder`/FalconDetection.csv"
    }
catch
    {
    $Message = "Error during export of CSV file for FalconDetection - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }




#endregion Falcon Detection




#region Falcon Behavior

# Get-FalconBehavior (NB: Transform TimeStamp to more readable format with [datetime]::ParseExact)
write-host "FALCON BEHAVIOR EVENTS" -B White -f Blue

try
    {
    $FalconBehavior = Get-FalconBehavior | foreach {Get-FalconBehavior -Ids $_} | Select -property  behavior_id,display_name,objective,@{Name='BehaviorDate';Expression={[datetime]::ParseExact($_.timestamp.Replace('T','').replace('Z',''),'yyyy-MM-ddHH:mm:ss',$null)}},cmdline,filepath,user_name,incident_id,tactic_id,tactic,technique_id,technique | sort BehaviorDate -Descending
    $FalconBehavior
    }
catch
    {
    $Message = "Error during Get-FalconBehavior - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }


# Convert To CSV
 try
    {
    $FalconBehaviorCSV = $($FalconBehavior | select BehaviorDate,cmdline,filepath,user_name | ConvertTo-Csv -Delimiter ';' -NoTypeInformation).Replace('"','')
    }
catch
    {
    $Message = "Error during convert to CSV file for FalconBehavior - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }

# Export CSV
 try
    {   
    $FalconBehaviorCSV | Out-File -FilePath "$ExportFolder`/FalconBehavior.csv"
    }
catch
    {
    $Message = "Error during export of CSV file for FalconBehavior - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }



#endregion Falcon Behavior





#region Falcon Incident

write-host "FALCON INCIDENTS EVENTS" -B White -f Blue
try
    {
    $FalconIncidents= Get-FalconIncident -Detailed -All  
    $FalconIncidents | sort created -Descending
    }
catch
    {
    $Message = "Error during Get-FalconIncident - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }


# Convert To CSV
 try
    {
    $FalconIncidentsCSV = $($FalconIncidents | select `
    incident_id,
    @{Name='Creation_Date';Expression={[datetime]::ParseExact($_.created.Replace('T','').Replace('Z',''),'yyyy-MM-ddHH:mm:ss',$null)}},
    state,
    email_state  | ConvertTo-Csv -Delimiter ',' -NoTypeInformation).Replace('"','')



        

    }
catch
    {
    $Message = "Error during convert to CSV file for FalconIncident - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }

# Export CSV
 try
    {   
    $FalconIncidentsCSV | Out-File -FilePath "$ExportFolder`/FalconIncidents.csv"
    }
catch
    {
    $Message = "Error during export of CSV file for FalconIncidents - END OF SCRIPT"
    write-host -F Red $Message
    Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
    EXIT 1
    }




#endregion Falcon Incident

 

Active Directory - Comment installer un module PowerShell sans connexion internet ?

Pour limiter la surface d'attaque d'un système d'information, il est courant que les serveurs n'aient pas de connexions internet. C'est un problème pour l'installation de modules PowerShell via la commande Install-Module qui va utiliser une URL internet pour télécharger ses sources. Comment peut-on alors faire pour récupérer ces précieux modules ?

 

Les messages d'erreurs type lorsque le serveur n'est pas autorisé à récupérer un module depuis internet

Message d'erreur type pour Install-Module

 

Message d'erreur type pour Get-PSRepository

 

On peut essayer de réinitialiser les paramètres par défaut du répertoire PowerShell avec la commande Register-PSRepository - Default mais une nouvelle saisie de Get-Repository démontre que rien n'a été changé.

 

Il existe également d'autre paramètres pour la commande Register-PSRepository qui nous permettent par exemple de spécifier nous-mêmes les URL de repository PowerShell mais quel que soient les paramètres entrés sans connexion internet la connexion ne sera jamais établie.

 

Sauvegarder le module PowerShell depuis un ordinateur avec une connexion internet

La commande Save-Module permet de sauvegarder un module PowerShell dans un dossier défini.

Un dossier du module téléchargé est alors dans le dossier spécifié.

Le dossier du module PowerShell doit être ensuite copié sur le serveur qui n'a pas de connexion internet. Le dossier cible doit être celui par défaut pour l'installation de module PowerShell ou s'il a été changé ce dernier.

 

Pour trouver les répertoires de modules PowerShell la variable d'environnement $env:PSModulePath peut être utilisé.

 

Habituellement, il est préférable que le module soit disponible à l'ensemble des utilisateur du serveur, le dossier C:\Program Files\WindowsPowerShell\Modules va donc être utilisé comme cible pour la copie du module PowerShell.

 

La commande Import-Module peut ensuite être utilisée pour chargé le nouveau module dans une session PowerShell. 

Power BI Report Server - Script - Export de dashboards en PBIX

Le script ci-dessous prend en paramètre un fichier contenant une liste de dashboard a exporter au format pbix.

 

#####################################################################
###### PwBI_Export_Dashboards.ps1 - EXPORT DE RAPPORTS EN PBIX ######
#####################################################################


<# 

    .SYNOPSIS 
        CONNEXION A L'API SQL POWERBI REPORTING SERVICES ET EXPORT DE UN OU PLUSIEURS DASHBOARD
 

    .PARAMETER  
        webServiceUrl : Url Racine du serveur de rapport
        CatalogUrl : Url du catalogue des items
        DashboardListFile : Fichier des noms de dashboard a exporter
        BackupFolder : Dossier d'export des dashboards
        LogFolder : Chemin du dossier où creer le log du script

 
    .EXAMPLE 
        .\PwBI_Export_Dashboards.ps1 -webServiceUrl "http://MyServer/Reports"  -DashboardListFile ".\PwBI_Export_Dashboards_List.txt" -BackupFolder ".\BACKUP" -LogFolder ".\BACKUP" 
     
#>


[CmdletBinding()]
param(
[Parameter(Mandatory,HelpMessage="Url Racine du serveur de rapport")]
[string]$webServiceUrl,

[Parameter(Mandatory=$false,HelpMessage="Url du catalogue des items")]
[string]$CatalogUrl = "$webServiceUrl/api/v2.0/CatalogItems",


[Parameter(Mandatory,
HelpMessage="Fichier des noms de dashboard a exporter")]
[ValidateScript({
if( -Not ($_ | Test-Path) ){
                throw "File does not exist"
            }
            return $true 
})]
[string]$DashboardListFile,

[Parameter(Mandatory,HelpMessage="Dossier d'export des dashboards")]
[string]$BackupFolder,

[Parameter(Mandatory,HelpMessage="Chemin du dossier où creer le log du script")]
[string]$LogFolder = "D:\Indicateurs_Securité"

)




# GET DASHBOARD LIST CONTENT
[array]$DashboardList = Get-Content -Path $DashboardListFile



# SCRIPT NAME
$ScriptName = "PwBI_Export_Dashboards.ps1"

# LogName = ScriptName without extension
$Log = $ScriptName.Split('.')[0]

# GET CREDENTIALS TO CONNECT TO REPORT SERVER
$cred = $(Get-Credential -Credential "$env:USERDOMAIN\$env:USERNAME")



function Write-Log 
{ 
    <# 
    .SYNOPSIS 
        This function creates or appends a line to a log file. 
 
    .PARAMETER  Message 
        The message parameter is the log message you'd like to record to the log file. 
 
    .EXAMPLE 
        PS C:\> Write-Log -Message 'Value1' 
        This example shows how to call the Write-Log function with named parameters. 
    #> 
    [CmdletBinding()] 
    param ( 
        [Parameter(Mandatory)] 
        [string]$Message,
        [Parameter(Mandatory)] 
        [string]$LogPath, 
        [Parameter(Mandatory)] 
        [string]$LogName
        
    ) 
     
    try 
    { 
        $DateTime = Get-Date -Format ‘MM-dd-yy HH:mm:ss’ 
        Add-Content -Value "$DateTime - $Message" -Path "$LogPath\$LogName.log" 
    } 
    catch 
    { 
        Write-Error $_.Exception.Message 
    } 
} 




# TABLEAU DES CATALOG ITEMS
$Message = "Recuperation du catalogue des items..."
write-host $Message
Write-Log -Message $Message -LogPath $LogFolder -LogName $Log


            try
            {
            $CatalogItems = Invoke-RestMethod -Uri $CatalogUrl -ContentType 'application/json' -UseDefaultCredentials -Method get
            }
            catch
            {
            $Message = "Error during query of Catalog Items"
            Write-Log -Message "$Message - $($Error[0].Exception)" -LogPath $LogFolder -LogName $Log
            Write-Host -F Yellow $Message
            exit 1
            }



# COMPARAISON DE $CatalogItems ET $ReportList POUR DETERMINER LA LISTE DES DASHBOARDS A EXPORTER

$FoundReport = $CatalogItems.value | Where-Object {$_.name -in $DashboardList}

if (!($FoundReport))
{
$Message = "The required Dashboards have not been found in the catalog of the report server - END OF SCRIPT"
write-host -f Yellow $Message
Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
Exit 1
}



# EXPORT DES DASHBOARDS
try
{

    foreach ($Report in $FoundReport)
    {
    $url = "$CatalogUrl($($Report.Id))/Content/`$value"
    Invoke-WebRequest -UseDefaultCredentials -Uri $url -OutFile "$BackupFolder\$($Report.name).pbix"
    }

}
catch
{
$Message = "KO - The required Dashboards have not been found in the catalog of the report server - END OF SCRIPT"
write-host -f Yellow $Message
Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
Exit 1
}


$Message = "OK - The required Dashboards have been Exported to $BackupFolder - END OF SCRIPT"
write-host -f Green $Message
Write-Log -Message $Message -LogPath $LogFolder -LogName $Log
Exit 0