Lister les extensions d'un parc Windows avec deux scripts PowerShell.
Deux scripts PowerShell relèvent les identifiants des extensions installées sur un parc Windows. Une GPO lance le premier sur chaque poste. Le second compte les utilisateurs de chaque extension.
Les scripts ci-dessous sont un exemple : ils couvrent Chrome, Edge, Brave et Firefox. Le périmètre de l'audit se définit avec l'équipe Ostral.
Si le parc est géré par Intune ou un autre MDM, la méthode par MDM évite le dossier partagé.
Les scripts lisent seulement les noms des dossiers d'extensions des navigateurs. Ils n'ouvrent aucun fichier, ne modifient rien, ne demandent aucun droit administrateur et ne contactent aucun serveur. Leur code est court et peut être relu avant usage.
1. Créer le dossier partagé
Chaque poste dépose son résultat dans ce dossier. Sur un serveur de fichiers joignable depuis tous les postes (un contrôleur de domaine convient aussi) :
Créer le dossier C:\Inventaire, et dedans le sous-dossier Resultats.
Y enregistrer les deux scripts présentés en bas de page, sous les noms Collect-Extensions.ps1 et Merge-Extensions.ps1.
Partager C:\Inventaire sous le nom Inventaire. Droit de partage : Utilisateurs du domaine : Modifier.
Droits NTFS pour les Utilisateurs du domaine : Lecture sur Inventaire, Modifier sur Resultats.
Le dossier est accessible à l'adresse \\nom-du-serveur\Inventaire. Les postes lisent les scripts et déposent leur résultat dans Resultats, sans pouvoir modifier les scripts.
Un fichier au nom du poste apparaît dans Resultats.
3. Créer la GPO
Créer une GPO liée aux unités d'organisation des utilisateurs.
Ouvrir Configuration utilisateur > Stratégies > Paramètres Windows > Scripts (ouverture/fermeture de session) et double-cliquer sur Ouverture de session.
Ajouter un script : - nom du script : powershell.exe ; - paramètres du script : -NoProfile -ExecutionPolicy Bypass -File \\nom-du-serveur\Inventaire\Collect-Extensions.ps1.
Si une GPO de l'entreprise impose des scripts signés, signer les deux scripts avec le certificat de signature de code de l'entreprise.
4. Rassembler les résultats
Une semaine plus tard, lancer Merge-Extensions.ps1 depuis le dossier partagé. Deux fichiers apparaissent.
Fichier
Contenu
Usage
inventaire_interne.csv
Les identifiants d'extensions de chaque utilisateur
À conserver dans l'entreprise
extensions_pour_ostral.csv
Chaque identifiant et son nombre d'utilisateurs, sans nom de poste ni d'employé
À transmettre à Ostral
Envoyer extensions_pour_ostral.csv à contact@ostral.ai. La GPO peut ensuite être retirée.
Collect-Extensions.ps1
Lancé à chaque ouverture de session. Il relève les identifiants des extensions de l'utilisateur connecté, puis écrit un fichier dans Resultats.
# Collect-Extensions.ps1
#
# Lists the IDs of the extensions installedinChrome,Edge,Brave and Firefox forthe
# signed-inuser,and writes them to Resultats\<COMPUTER>_<USER>.csv.
#
# Thescript only reads folder and file names. Itopens no file,changes nothing on the
# computer and contacts no server.
#Requires -Version 5.1$ErrorActionPreference = 'Stop'
# Chrome,Edge and Brave store each extensionina folder named after its ID,
# inevery browser profile.
$chromiumExtensions = @("$env:LOCALAPPDATA\Google\Chrome\User Data\*\Extensions\*""$env:LOCALAPPDATA\Microsoft\Edge\User Data\*\Extensions\*""$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\*\Extensions\*")
# Firefox stores each extensionas an <ID>.xpifile,inevery profile.
$firefoxExtensions = @("$env:APPDATA\Mozilla\Firefox\Profiles\*\extensions\*.xpi""$env:LOCALAPPDATA\Packages\Mozilla.Firefox_*\LocalCache\Roaming\Mozilla\Firefox\Profiles\*\extensions\*.xpi")
$ids = @()$ids += Get-Item -Path $chromiumExtensions -ErrorAction SilentlyContinue |
Where-Object{$_.PSIsContainer -and $_.Name -match '^[a-p]{32}$'} |
ForEach-Object{$_.Name}$ids += Get-Item -Path $firefoxExtensions -ErrorAction SilentlyContinue |
ForEach-Object{$_.BaseName}$ids = @($ids | Sort-Object -Unique)
# Write the result,one line per extension. Thefile is written under a temporary name
# and then renamed,so the merge never reads an incomplete file.
$here = if($PSScriptRoot){$PSScriptRoot}else{(Get-Location).Path}$folder = Join-Path $here 'Resultats'$name = ('{0}_{1}' -f $env:COMPUTERNAME,$env:USERNAME) -replace '[^\w.-]','_'$date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$lines = @('"Poste";"Utilisateur";"IdExtension";"DateCollecte"')$lines += $ids | ForEach-Object{'"{0}";"{1}";"{2}";"{3}"' -f $env:COMPUTERNAME,$env:USERNAME,$_,$date}
New-Item -ItemType Directory -Path $folder -Force | Out-Null$temp = Join-Path $folder "$name.tmp"[System.IO.File]::WriteAllLines($temp,$lines,(New-Object System.Text.UTF8Encoding($true)))Move-Item -LiteralPath $temp -Destination(Join-Path $folder "$name.csv") -Force
# Collect-Extensions.ps1
#
# Lists the IDs of the extensions installedinChrome,Edge,Brave and Firefox forthe
# signed-inuser,and writes them to Resultats\<COMPUTER>_<USER>.csv.
#
# Thescript only reads folder and file names. Itopens no file,changes nothing on the
# computer and contacts no server.
#Requires -Version 5.1$ErrorActionPreference = 'Stop'
# Chrome,Edge and Brave store each extensionina folder named after its ID,
# inevery browser profile.
$chromiumExtensions = @("$env:LOCALAPPDATA\Google\Chrome\User Data\*\Extensions\*""$env:LOCALAPPDATA\Microsoft\Edge\User Data\*\Extensions\*""$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\*\Extensions\*")
# Firefox stores each extensionas an <ID>.xpifile,inevery profile.
$firefoxExtensions = @("$env:APPDATA\Mozilla\Firefox\Profiles\*\extensions\*.xpi""$env:LOCALAPPDATA\Packages\Mozilla.Firefox_*\LocalCache\Roaming\Mozilla\Firefox\Profiles\*\extensions\*.xpi")
$ids = @()$ids += Get-Item -Path $chromiumExtensions -ErrorAction SilentlyContinue |
Where-Object{$_.PSIsContainer -and $_.Name -match '^[a-p]{32}$'} |
ForEach-Object{$_.Name}$ids += Get-Item -Path $firefoxExtensions -ErrorAction SilentlyContinue |
ForEach-Object{$_.BaseName}$ids = @($ids | Sort-Object -Unique)
# Write the result,one line per extension. Thefile is written under a temporary name
# and then renamed,so the merge never reads an incomplete file.
$here = if($PSScriptRoot){$PSScriptRoot}else{(Get-Location).Path}$folder = Join-Path $here 'Resultats'$name = ('{0}_{1}' -f $env:COMPUTERNAME,$env:USERNAME) -replace '[^\w.-]','_'$date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$lines = @('"Poste";"Utilisateur";"IdExtension";"DateCollecte"')$lines += $ids | ForEach-Object{'"{0}";"{1}";"{2}";"{3}"' -f $env:COMPUTERNAME,$env:USERNAME,$_,$date}
New-Item -ItemType Directory -Path $folder -Force | Out-Null$temp = Join-Path $folder "$name.tmp"[System.IO.File]::WriteAllLines($temp,$lines,(New-Object System.Text.UTF8Encoding($true)))Move-Item -LiteralPath $temp -Destination(Join-Path $folder "$name.csv") -Force
# Collect-Extensions.ps1
#
# Lists the IDs of the extensions installedinChrome,Edge,Brave and Firefox forthe
# signed-inuser,and writes them to Resultats\<COMPUTER>_<USER>.csv.
#
# Thescript only reads folder and file names. Itopens no file,changes nothing on the
# computer and contacts no server.
#Requires -Version 5.1$ErrorActionPreference = 'Stop'
# Chrome,Edge and Brave store each extensionina folder named after its ID,
# inevery browser profile.
$chromiumExtensions = @("$env:LOCALAPPDATA\Google\Chrome\User Data\*\Extensions\*""$env:LOCALAPPDATA\Microsoft\Edge\User Data\*\Extensions\*""$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\*\Extensions\*")
# Firefox stores each extensionas an <ID>.xpifile,inevery profile.
$firefoxExtensions = @("$env:APPDATA\Mozilla\Firefox\Profiles\*\extensions\*.xpi""$env:LOCALAPPDATA\Packages\Mozilla.Firefox_*\LocalCache\Roaming\Mozilla\Firefox\Profiles\*\extensions\*.xpi")
$ids = @()$ids += Get-Item -Path $chromiumExtensions -ErrorAction SilentlyContinue |
Where-Object{$_.PSIsContainer -and $_.Name -match '^[a-p]{32}$'} |
ForEach-Object{$_.Name}$ids += Get-Item -Path $firefoxExtensions -ErrorAction SilentlyContinue |
ForEach-Object{$_.BaseName}$ids = @($ids | Sort-Object -Unique)
# Write the result,one line per extension. Thefile is written under a temporary name
# and then renamed,so the merge never reads an incomplete file.
$here = if($PSScriptRoot){$PSScriptRoot}else{(Get-Location).Path}$folder = Join-Path $here 'Resultats'$name = ('{0}_{1}' -f $env:COMPUTERNAME,$env:USERNAME) -replace '[^\w.-]','_'$date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$lines = @('"Poste";"Utilisateur";"IdExtension";"DateCollecte"')$lines += $ids | ForEach-Object{'"{0}";"{1}";"{2}";"{3}"' -f $env:COMPUTERNAME,$env:USERNAME,$_,$date}
New-Item -ItemType Directory -Path $folder -Force | Out-Null$temp = Join-Path $folder "$name.tmp"[System.IO.File]::WriteAllLines($temp,$lines,(New-Object System.Text.UTF8Encoding($true)))Move-Item -LiteralPath $temp -Destination(Join-Path $folder "$name.csv") -Force
Merge-Extensions.ps1
Lancé une fois, à la fin. Il rassemble les fichiers de Resultats et compte les utilisateurs de chaque extension.
# Merge-Extensions.ps1
#
# Merges the files of the Resultats folder and writes,next to the script:
# inventaire_interne.csvthe extensions of each user,to keep inside the company;
# extensions_pour_ostral.csveach extension and its number of users,to send to Ostral.
#Requires -Version 5.1$ErrorActionPreference = 'Stop'
$here = if($PSScriptRoot){$PSScriptRoot}else{(Get-Location).Path}$rows = @(Get-ChildItem -Path(Join-Path $here'Resultats\*.csv') |
ForEach-Object{Import-Csv -LiteralPath $_.FullName -Delimiter ';'})
# Internal file:one line per user and per extension.
$rows | Sort-Object Poste,Utilisateur,IdExtension |
Select-Object Poste,Utilisateur,IdExtension |
Export-Csv -LiteralPath(Join-Path $here 'inventaire_interne.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
# File for Ostral:one line per extension,withno computer or user name.
$extensions = @($rows | Group-Object IdExtension | ForEach-Object{[pscustomobject][ordered]@{IdExtension = $_.Name;Utilisateurs = $_.Count}} | Sort-Object Utilisateurs -Descending)
$extensions | Export-Csv -LiteralPath(Join-Path $here 'extensions_pour_ostral.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
Write-Host "Distinct extensions: $($extensions.Count)"Write-Host "File to send to Ostral: $(Join-Path $here 'extensions_pour_ostral.csv')"
# Merge-Extensions.ps1
#
# Merges the files of the Resultats folder and writes,next to the script:
# inventaire_interne.csvthe extensions of each user,to keep inside the company;
# extensions_pour_ostral.csveach extension and its number of users,to send to Ostral.
#Requires -Version 5.1$ErrorActionPreference = 'Stop'
$here = if($PSScriptRoot){$PSScriptRoot}else{(Get-Location).Path}$rows = @(Get-ChildItem -Path(Join-Path $here'Resultats\*.csv') |
ForEach-Object{Import-Csv -LiteralPath $_.FullName -Delimiter ';'})
# Internal file:one line per user and per extension.
$rows | Sort-Object Poste,Utilisateur,IdExtension |
Select-Object Poste,Utilisateur,IdExtension |
Export-Csv -LiteralPath(Join-Path $here 'inventaire_interne.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
# File for Ostral:one line per extension,withno computer or user name.
$extensions = @($rows | Group-Object IdExtension | ForEach-Object{[pscustomobject][ordered]@{IdExtension = $_.Name;Utilisateurs = $_.Count}} | Sort-Object Utilisateurs -Descending)
$extensions | Export-Csv -LiteralPath(Join-Path $here 'extensions_pour_ostral.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
Write-Host "Distinct extensions: $($extensions.Count)"Write-Host "File to send to Ostral: $(Join-Path $here 'extensions_pour_ostral.csv')"
# Merge-Extensions.ps1
#
# Merges the files of the Resultats folder and writes,next to the script:
# inventaire_interne.csvthe extensions of each user,to keep inside the company;
# extensions_pour_ostral.csveach extension and its number of users,to send to Ostral.
#Requires -Version 5.1$ErrorActionPreference = 'Stop'
$here = if($PSScriptRoot){$PSScriptRoot}else{(Get-Location).Path}$rows = @(Get-ChildItem -Path(Join-Path $here'Resultats\*.csv') |
ForEach-Object{Import-Csv -LiteralPath $_.FullName -Delimiter ';'})
# Internal file:one line per user and per extension.
$rows | Sort-Object Poste,Utilisateur,IdExtension |
Select-Object Poste,Utilisateur,IdExtension |
Export-Csv -LiteralPath(Join-Path $here 'inventaire_interne.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
# File for Ostral:one line per extension,withno computer or user name.
$extensions = @($rows | Group-Object IdExtension | ForEach-Object{[pscustomobject][ordered]@{IdExtension = $_.Name;Utilisateurs = $_.Count}} | Sort-Object Utilisateurs -Descending)
$extensions | Export-Csv -LiteralPath(Join-Path $here 'extensions_pour_ostral.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
Write-Host "Distinct extensions: $($extensions.Count)"Write-Host "File to send to Ostral: $(Join-Path $here 'extensions_pour_ostral.csv')"