Introduction
Il est essentiel de savoir quand les appareils se sont connectés pour la dernière fois à Active Directory (AD) pour maintenir un environnement informatique sécurisé et bien géré. Les systèmes dormants ou rarement utilisés peuvent présenter des risques, tels que des terminaux non autorisés ou des inefficacités dans l’allocation des ressources. Le script PowerShell fourni propose une solution efficace aux administrateurs informatiques et aux fournisseurs de services gérés (MSP) afin de récupérer et d’analyser efficacement les dernières données de connexion pour tous les ordinateurs de leur environnement AD.
Contexte
Active Directory est la clé de voûte de la gestion des utilisateurs et des appareils dans de nombreux environnements d’entreprise. Le suivi de l’horodatage de la dernière connexion des ordinateurs est essentiel pour l’audit, la conformité et le maintien d’un répertoire propre. Ce script simplifie le processus traditionnellement lourd de collecte de ces données en exploitant les capacités d’automatisation de PowerShell et en s’intégrant à des outils tels que NinjaOne pour des fonctionnalités étendues. Il ne se contente pas de récupérer les horodatages de la dernière connexion, mais propose également des options permettant d’interroger d’autres données sur les utilisateurs et de visualiser les résultats dans un format personnalisable.
Le script :
#Requires -Version 5.1 <# .SYNOPSIS Gets the last login time for all computers in Active Directory. .DESCRIPTION Gets the last login time for all computers in Active Directory. The last login time is retrieved from the LastLogonTimeStamp property of the computer object. If the user name cannot be retrieved from an offline computer, the script will return Unknown. If the computer name cannot be retrieved, the script will return Unknown. By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .EXAMPLE (No Parameters) ## EXAMPLE OUTPUT WITHOUT PARAMS ## PARAMETER: -WysiwygCustomField "myWysiwygCustomField" Saves results to a WYSIWYG Custom Field. .EXAMPLE -WysiwygCustomField "myWysiwygCustomField" ## EXAMPLE OUTPUT WITH WysiwygCustomField ## [Info] Found 10 computers. [Info] Attempting to set Custom Field 'myWysiwygCustomField'. [Info] Successfully set Custom Field 'myWysiwygCustomField'! PARAMETER: -QueryForLastUserLogon "true" When checked, the script will query for the last user logon time for each computer. Note that this will take longer to run and will try to connect to each computer in the domain. .EXAMPLE -QueryForLastUserLogon "true" ## EXAMPLE OUTPUT WITH QueryForLastUserLogon ## [Warn] Remote computer WIN-1234567891 is not available. [Info] Found 2 computers. Computer Last Logon Date Last Login in Days User -------- --------------- ------------------ ---- WIN-1234567891 2024-04-01 12:00 0 Unknown WIN-1234567890 2024-04-01 12:00 0 Fred WIN-9876543210 2023-04-01 12:00 32 Bob .NOTES Minimum OS Architecture Supported: Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String]$WysiwygCustomField, [Parameter()] [Switch]$QueryForLastUserLogon ) begin { # CIM timeout $CIMTimeout = 10 function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters if ($Characters -ge 10000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.") } # If we're requested to set the field value for a Ninja document we'll specify it here. $DocumentationParams = @{} if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed. $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # The field below requires additional information to be set $NeedsOptions = "Dropdown" if ($DocumentName) { if ($NeedsOptions -contains $Type) { # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong. $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # If an error is received it will have an exception property, the function will exit with that error information. if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports. switch ($Type) { "Checkbox" { # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry. $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID. $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid. $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID if (-not $Selection) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown") } $NinjaValue = $Selection } default { # All the other types shouldn't require additional work on the input. $NinjaValue = $Value } } # We'll need to set the field differently depending on if its a field in a Ninja Document or not. if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 } if ($CustomField.Exception) { throw $CustomField } } } process { if (-not (Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Get Script Variables and override parameters with them if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomField } if ($env:queryForLastUserLogon -and $env:queryForLastUserLogon -notlike "null") { if ($env:queryForLastUserLogon -eq "true") { $QueryForLastUserLogon = $true } else { $QueryForLastUserLogon = $false } } # Check that Active Directory module is available if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { Write-Host "[Error] Active Directory module is not available. Please install it and try again." exit 1 } # Get the computer system from the CIM $ComputerSystem = $(Get-CimInstance -ClassName Win32_ComputerSystem) # Check if this script is running on a domain joined computer if ($ComputerSystem.PartOfDomain -eq $false) { Write-Host "[Error] This script must be run on a domain joined computer." exit 1 } # Check if this script is running on a domain controller switch ($ComputerSystem.DomainRole) { 0 { Write-Host "[Info] Running script on a Standalone Workstation." } 1 { Write-Host "[Info] Running script on a Member Workstation." } 2 { Write-Host "[Info] Running script on a Standalone Server." } 3 { Write-Host "[Info] Running script on a Member Server." } 4 { Write-Host "[Info] Running script on a Backup Domain Controller." } 5 { Write-Host "[Info] Running script on a Primary Domain Controller." } } # Get the SearchBase for the domain $Domain = "DC=$($ComputerSystem.Domain -split "\." -join ",DC=")" # Get Computers from Active Directory try { $Computers = Get-ADComputer -Filter { (Enabled -eq $true) } -Properties Name, LastLogonTimeStamp -SearchBase "$Domain" -ErrorAction Stop } catch { Write-Host "[Error] Failed to get computers. Make sure this is running on a domain controller." exit 1 } $IsFirstError = $true $LastLogonInfo = foreach ($Computer in $Computers) { try { # Get the LastLogonTimeStamp for the computer from Active Directory $PCInfo = Get-ADComputer -Identity $Computer.Name -Properties LastLogonTimeStamp -ErrorAction Stop | Select-Object -Property @( @{Name = "Computer"; Expression = { $_.Name } }, @{Name = "LastLogon"; Expression = { [DateTime]::FromFileTime($_.LastLogonTimeStamp) } } ) } catch { # This should only happen if the script is not running as the system user on a domain controller or not as a domain admin Write-Debug "[Debug] $($_.Exception.Message)" Write-Host "[Warn] Failed to get details for $($Computer.Name) from Active Directory. Skipping." continue } try { if ($QueryForLastUserLogon) { # Get the User Principal Name from the computer $LastUserLogonInfo = Get-CimInstance -ClassName Win32_UserProfile -ComputerName $Computer.name -OperationTimeoutSec $CIMTimeout -ErrorAction Stop | Where-Object { $_.LocalPath -like "*Users*" } | Sort-Object -Property LastUseTime | Select-Object -Last 1 $SecIdentifier = New-Object System.Security.Principal.SecurityIdentifier($LastUserLogonInfo.SID) -ErrorAction Stop $UserName = $SecIdentifier.Translate([System.Security.Principal.NTAccount]) } } catch { if ($null -eq $UserName) { if ($IsFirstError) { # Only show on the first error Write-Debug "[Debug] $($_.Exception.Message)" Write-Host "[Error] Failed to connect to 1 or more computers via Get-CimInstance." $IsFirstError = $false } Write-Host "[Warn] Remote computer $($Computer.Name) is not available or could not be queried." } } if ($null -eq $UserName) { $UserName = [PSCustomObject]@{ value = "Unknown" } } if ($null -eq $PCInfo.LastLogon) { $PCInfo = [PSCustomObject]@{ Computer = $Computer.Name LastLogon = "Unknown" } Write-Host "[Warn] Failed to get LastLogonTimeStamp for $($Computer.Name)." } # Get the number of days since the last login $LastLoginDays = try { 0 - $(Get-Date -Date $PCInfo.LastLogon).Subtract($(Get-Date)).Days } catch { # Return unknown if the date is invalid or does not exist "Unknown" } # Output the results if ($QueryForLastUserLogon) { [PSCustomObject]@{ 'Computer' = $PCInfo.Computer 'Last Logon Date' = $PCInfo.LastLogon 'Last Login in Days' = $LastLoginDays 'User' = $UserName.value } } else { [PSCustomObject]@{ 'Computer' = $PCInfo.Computer 'Last Logon Date' = $PCInfo.LastLogon 'Last Login in Days' = $LastLoginDays } } $PCInfo = $null $LastUserLogonInfo = $null $SecIdentifier = $null $UserName = $null } # Output the number of computers found if ($LastLogonInfo -and $LastLogonInfo.Count -gt 0) { Write-Host "[Info] Found $($LastLogonInfo.Count) computers." } else { Write-Host "[Error] No computers were found." $ExitCode = 1 } function Write-LastLoginInfo { param () $LastLogonInfo | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host } # Save the results to a custom field if ($WysiwygCustomField) { $LastLoginOkayDays = 30 $LastLoginTooOldDays = 90 # Convert the array to an HTML table $HtmlTable = $LastLogonInfo | ConvertTo-Html -Fragment # Set the color of the rows based on the last logon time $HtmlTable = $HtmlTable -split [Environment]::NewLine | ForEach-Object { if ($_ -match "<td>(?'LastLoginDays'\d+)<\/td>") { # Get the last login days from the HTML table [int]$LastLoginDays = $Matches.LastLoginDays if ($LastLoginDays -lt $LastLoginTooOldDays -and $LastLoginDays -ge $LastLoginOkayDays) { # warning = 31 days to 89 days $_ -replace "<tr><td>", '<tr class="warning"><td>' } elseif ($LastLoginDays -ge $LastLoginTooOldDays) { # danger = 90 days or more $_ -replace "<tr><td>", '<tr class="danger"><td>' } else { # success = 30 days or less $_ -replace "<tr><td>", '<tr class="success"><td>' } } else { $_ } } # Set the width of the table to 10% to reduce the width of the table to its minimum possible width $HtmlTable = $HtmlTable -replace "<table>", "<table style='white-space:nowrap;'>" try { Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomField'." Set-NinjaProperty -Name $WysiwygCustomField -Value $($HtmlTable | Out-String) Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomField'!" } catch { Write-Host "[Error] Failed to set Custom Field '$WysiwygCustomField'." Write-LastLoginInfo $ExitCode = 1 } } else { Write-LastLoginInfo } exit $ExitCode } end { }
Gagnez du temps grâce à plus de 300 scripts du Dojo NinjaOne.
Description détaillée
Le script est conçu pour être à la fois puissant et polyvalent, ce qui permet aux professionnels de l’informatique de l’adapter à leurs besoins spécifiques. Vous trouverez ci-dessous une description détaillée de ses fonctionnalités :
1. Vérification des prérequis :
Le script vérifie d’abord l’environnement du système :
a. Confirme qu’il est exécuté sur un ordinateur relié à un domaine.
b. S’assure que le module Active Directory PowerShell est installé.
2. Traitement des paramètres :
Le script accepte deux paramètres clés :
a. -WysiwygCustomField : spécifie un champ personnalisé dans NinjaOne pour enregistrer les résultats.
b. -QueryForLastUserLogon : permet d’interroger les détails de la dernière connexion de l’utilisateur sur chaque ordinateur, bien que cela prolonge le temps d’exécution.
3. Requête Active Directory :
Le script utilise “Get-ADComputer” pour récupérer les objets informatiques activés ainsi que leur “LastLogonTimeStamp” (horodatage indiquant la dernière connexion). Il arrange les données dans un tableau lisible, éventuellement complété par les détails de la dernière connexion de l’utilisateur.
4. Gestion des erreurs et journalisation :
Une gestion complète des erreurs garantit la fiabilité de l’exécution du script. Par exemple :
a. Les ordinateurs hors ligne sont signalés par des avertissements.
b. Les erreurs commises lors de l’interrogation de certains systèmes sont enregistrées, mais le script continue à traiter les autres systèmes.
5. Traitement et visualisation des données :
a. Le script calcule le nombre de jours écoulés depuis la dernière connexion.
b. Si cette option est spécifiée, les résultats sont formatés dans un tableau HTML pour faciliter la création de rapports, avec codes couleur indiquant l’ancienneté de l’activité de connexion.
6. Intégration avec NinjaOne :
Les résultats peuvent être enregistrés dans un champ personnalisé NinjaOne, ce qui les rend accessibles pour des analyses et des rapports ultérieurs.
Cas d’utilisation potentiels
Étude de cas :
L’administrateur informatique d’une entreprise de taille moyenne remarque un pic d’ordinateurs inactifs lors d’un audit annuel. Utilisation du script :
- Il récupère les données de la dernière connexion pour tous les appareils du domaine.
- Les systèmes dormants qui ne se sont pas connectés depuis plus de 90 jours sont signalés et mis hors service.
- Les informations sur l’activité des utilisateurs sont utilisées pour optimiser l’allocation des ressources matérielles.
Cette approche améliore l’état de sécurité de l’entreprise en réduisant les surfaces d’attaque et permet de réaliser des économies en identifiant les ressources sous-utilisées.
Comparaisons
Traditionnellement, les administrateurs informatiques s’appuyaient sur des outils tels que l’interface utilisateur graphique d’Active Directory Users and Computers (ADUC) ou des logiciels d’audit tiers pour collecter les informations de connexion. Ces méthodes manquent souvent d’évolutivité ou nécessitent un effort manuel important. En revanche, ce script PowerShell :
- Automatise le processus de récupération des données, ce qui permet de gagner du temps.
- Offre une grande flexibilité grâce à des paramètres personnalisables.
- S’intègre à des outils tels que NinjaOne pour améliorer les rapports.
FAQ
Q : Le script nécessite-t-il des droits administrateur ?
Oui, il doit être exécuté avec des autorisations élevées pour interroger efficacement les données AD.
Q : Quelle est la précision de la propriété LastLogonTimeStamp ?
Cette propriété peut ne pas refléter l’activité en temps réel, car elle est mise à jour environ tous les 14 jours par défaut. Pour obtenir des données de connexion précises, envisagez d’interroger des contrôleurs de domaine individuels.
Q : Que se passe-t-il si un ordinateur est hors ligne ?
Les systèmes hors ligne sont signalés par des avertissements et leurs coordonnées d’utilisateur sont définies comme “inconnues”
Q : Le script peut-il être personnalisé ?
Oui, il est conçu pour être flexible. Des paramètres tels que WysiwygCustomField et QueryForLastUserLogon permettent de personnaliser les cas d’utilisation.
Implications
Les résultats générés par ce script ont des implications significatives pour la sécurité informatique et l’efficacité opérationnelle :
- Sécurité renforcée : l’identification des appareils inactifs réduit les vecteurs d’attaque potentiels.
- Amélioration de la conformité : les entreprises peuvent répondre aux exigences d’audit en conservant des enregistrements précis sur les appareils.
- Rationalisation des opérations : l’automatisation élimine la charge de travail manuelle liée au suivi de l’activité des appareils.
Recommandations
- Exécutez-le régulièrement : programmez l’exécution du script une fois par mois afin de maintenir à jour l’inventaire des systèmes actifs.
- Associez-le à des stratégies : utilisez les résultats pour appliquer des politiques telles que la mise hors service des appareils inactifs après une période donnée.
- Utilisez des identifiants sécurisées : veillez à ce que le script soit exécuté dans un environnement sécurisé afin de protéger les données AD sensibles.
Conclusion
Pour les administrateurs informatiques et les MSP, l’automatisation de la récupération des données de dernière connexion est essentielle pour maintenir un environnement AD sécurisé et efficace. Ce script PowerShell simplifie non seulement le processus, mais s’intègre également de manière transparente à des plateformes telles que NinjaOne, ce qui permet d’obtenir des rapports et une gestion avancés. En tirant parti d’outils de ce type, les entreprises peuvent optimiser leurs opérations informatiques et renforcer leurs cadres de sécurité.