Audit sans déploiement

Inventaire des extensions par GPO

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) :

  1. Créer le dossier C:\Inventaire, et dedans le sous-dossier Resultats.

  2. Y enregistrer les deux scripts présentés en bas de page, sous les noms Collect-Extensions.ps1 et Merge-Extensions.ps1.

  3. Partager C:\Inventaire sous le nom Inventaire. Droit de partage : Utilisateurs du domaine : Modifier.

  4. 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.

2. Tester sur un poste

Lancer cette commande sur un poste :

powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\nom-du-serveur\Inventaire\Collect-Extensions.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\nom-du-serveur\Inventaire\Collect-Extensions.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\nom-du-serveur\Inventaire\Collect-Extensions.ps1

Un fichier au nom du poste apparaît dans Resultats.

3. Créer la GPO

  1. Créer une GPO liée aux unités d'organisation des utilisateurs.

  2. Ouvrir Configuration utilisateur > Stratégies > Paramètres Windows > Scripts (ouverture/fermeture de session) et double-cliquer sur Ouverture de session.

  3. 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 installed in Chrome, Edge, Brave and Firefox for the
# signed-in user, and writes them to Resultats\<COMPUTER>_<USER>.csv.
#
# The script only reads folder and file names. It opens no file, changes nothing on the
# computer and contacts no server.
&nbsp;
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
&nbsp;
&nbsp;
# Chrome, Edge and Brave store each extension in a folder named after its ID,
# in every 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\*"
)
&nbsp;
# Firefox stores each extension as an <ID>.xpi file, in every profile.
$firefoxExtensions = @(
    "$env:APPDATA\Mozilla\Firefox\Profiles\*\extensions\*.xpi"
    "$env:LOCALAPPDATA\Packages\Mozilla.Firefox_*\LocalCache\Roaming\Mozilla\Firefox\Profiles\*\extensions\*.xpi"
)
&nbsp;
$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)
&nbsp;
&nbsp;
# Write the result, one line per extension. The file 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'
&nbsp;
$lines  = @('"Poste";"Utilisateur";"IdExtension";"DateCollecte"')
$lines += $ids | ForEach-Object { '"{0}";"{1}";"{2}";"{3}"' -f $env:COMPUTERNAME, $env:USERNAME, $_, $date }
&nbsp;
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 installed in Chrome, Edge, Brave and Firefox for the
# signed-in user, and writes them to Resultats\<COMPUTER>_<USER>.csv.
#
# The script only reads folder and file names. It opens no file, changes nothing on the
# computer and contacts no server.
&nbsp;
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
&nbsp;
&nbsp;
# Chrome, Edge and Brave store each extension in a folder named after its ID,
# in every 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\*"
)
&nbsp;
# Firefox stores each extension as an <ID>.xpi file, in every profile.
$firefoxExtensions = @(
    "$env:APPDATA\Mozilla\Firefox\Profiles\*\extensions\*.xpi"
    "$env:LOCALAPPDATA\Packages\Mozilla.Firefox_*\LocalCache\Roaming\Mozilla\Firefox\Profiles\*\extensions\*.xpi"
)
&nbsp;
$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)
&nbsp;
&nbsp;
# Write the result, one line per extension. The file 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'
&nbsp;
$lines  = @('"Poste";"Utilisateur";"IdExtension";"DateCollecte"')
$lines += $ids | ForEach-Object { '"{0}";"{1}";"{2}";"{3}"' -f $env:COMPUTERNAME, $env:USERNAME, $_, $date }
&nbsp;
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 installed in Chrome, Edge, Brave and Firefox for the
# signed-in user, and writes them to Resultats\<COMPUTER>_<USER>.csv.
#
# The script only reads folder and file names. It opens no file, changes nothing on the
# computer and contacts no server.
&nbsp;
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
&nbsp;
&nbsp;
# Chrome, Edge and Brave store each extension in a folder named after its ID,
# in every 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\*"
)
&nbsp;
# Firefox stores each extension as an <ID>.xpi file, in every profile.
$firefoxExtensions = @(
    "$env:APPDATA\Mozilla\Firefox\Profiles\*\extensions\*.xpi"
    "$env:LOCALAPPDATA\Packages\Mozilla.Firefox_*\LocalCache\Roaming\Mozilla\Firefox\Profiles\*\extensions\*.xpi"
)
&nbsp;
$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)
&nbsp;
&nbsp;
# Write the result, one line per extension. The file 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'
&nbsp;
$lines  = @('"Poste";"Utilisateur";"IdExtension";"DateCollecte"')
$lines += $ids | ForEach-Object { '"{0}";"{1}";"{2}";"{3}"' -f $env:COMPUTERNAME, $env:USERNAME, $_, $date }
&nbsp;
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.csv      the extensions of each user, to keep inside the company;
#   extensions_pour_ostral.csv  each extension and its number of users, to send to Ostral.
&nbsp;
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
&nbsp;
$here = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$rows = @(Get-ChildItem -Path (Join-Path $here 'Resultats\*.csv') |
          ForEach-Object { Import-Csv -LiteralPath $_.FullName -Delimiter ';' })
&nbsp;
&nbsp;
# 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
&nbsp;
# File for Ostral: one line per extension, with no computer or user name.
$extensions = @($rows | Group-Object IdExtension | ForEach-Object {
    [pscustomobject][ordered]@{ IdExtension = $_.Name; Utilisateurs = $_.Count }
} | Sort-Object Utilisateurs -Descending)
&nbsp;
$extensions | Export-Csv -LiteralPath (Join-Path $here 'extensions_pour_ostral.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
&nbsp;
&nbsp;
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.csv      the extensions of each user, to keep inside the company;
#   extensions_pour_ostral.csv  each extension and its number of users, to send to Ostral.
&nbsp;
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
&nbsp;
$here = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$rows = @(Get-ChildItem -Path (Join-Path $here 'Resultats\*.csv') |
          ForEach-Object { Import-Csv -LiteralPath $_.FullName -Delimiter ';' })
&nbsp;
&nbsp;
# 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
&nbsp;
# File for Ostral: one line per extension, with no computer or user name.
$extensions = @($rows | Group-Object IdExtension | ForEach-Object {
    [pscustomobject][ordered]@{ IdExtension = $_.Name; Utilisateurs = $_.Count }
} | Sort-Object Utilisateurs -Descending)
&nbsp;
$extensions | Export-Csv -LiteralPath (Join-Path $here 'extensions_pour_ostral.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
&nbsp;
&nbsp;
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.csv      the extensions of each user, to keep inside the company;
#   extensions_pour_ostral.csv  each extension and its number of users, to send to Ostral.
&nbsp;
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
&nbsp;
$here = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$rows = @(Get-ChildItem -Path (Join-Path $here 'Resultats\*.csv') |
          ForEach-Object { Import-Csv -LiteralPath $_.FullName -Delimiter ';' })
&nbsp;
&nbsp;
# 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
&nbsp;
# File for Ostral: one line per extension, with no computer or user name.
$extensions = @($rows | Group-Object IdExtension | ForEach-Object {
    [pscustomobject][ordered]@{ IdExtension = $_.Name; Utilisateurs = $_.Count }
} | Sort-Object Utilisateurs -Descending)
&nbsp;
$extensions | Export-Csv -LiteralPath (Join-Path $here 'extensions_pour_ostral.csv') -Delimiter ';' -NoTypeInformation -Encoding UTF8
&nbsp;
&nbsp;
Write-Host "Distinct extensions: $($extensions.Count)"
Write-Host "File to send to Ostral: $(Join-Path $here 'extensions_pour_ostral.csv')"