Bonjour à tous, aujourd’hui je voulais vous partager un bout de script que j’ai du faire au travail sur ces 4 mots clés : SEPM, Computers, REST et API.
En effet, pour ceux qui ne le sauraient pas, dans un parc informatique d’envergure construire un inventaire complet des postes est une tâche complexe (voir carrément difficile), et je ne parle pas de la maintenir à jour l’inventaire en question, bref récupérer la liste des ordinateurs dans un SEPM, c’est cool.
C’est là que l’API REST (on a déjà parlé de REST là, là ou encore là) de SEP et sa fonction « computer » entre en jeu, en effet les antivirus font partie des solutions qui sont généralement déployée sur une bonne partie des postes clients d’un parc informatique. Et la bonne nouvelles, c’est que la plupart des antivirus sur le marché remontent plus ou moins une sorte d’inventaire des postes sur lesquels leur client tournent., et c’est le taf de la fonction computer est documentée ici pour obtenir les détails des postes.
Je crois aussi que je vous ai déjà assez parlé de REST et PowerShell sur le blog… Du coup, vous devriez voir venir la suite, non ?
Get-SEPComputers : SEPM, Computers, REST et API.
Sans transition, je vous donne le script magique qui liste les ordinateurs d’une infra SEPM.
#Requires -Module logging
<#
.SYNOPSIS
Create a CSV export of all computers in a list of SEPM Domains
.DESCRIPTION
Leverage the rest API of a SEPM server to create a list of all knows computers in given domains.
.INPUTS
parameters.json file in the current directory.
.OUTPUTS
SEP_computers.csv file
.COMPONENT
This script use the SEPM REST API: docuementation can be found, on the server : https://SEPM_IP:8446/sepm/restapidocs.html
The script also requires the Logging module from the PSGallery.
1. Download : https://www.powershellgallery.com/packages/Logging/4.2.12
2. Unzip and copy content into : C:\Program Files\WindowsPowerShell\Modules\Logging\4.2.12
Doc : https://docs.microsoft.com/en-us/powershell/scripting/gallery/how-to/working-with-packages/manual-download
The script also needs the Logs directory to be created.
.EXAMPLE
.\Get-SEPComputerInfos
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()] [String] $ParamFile = (Split-Path $script:MyInvocation.MyCommand.Path) + "\" + "parameters.json",
[Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()] [String] $OutputFile = (Split-Path $script:MyInvocation.MyCommand.Path) + "\" + "SEP_computers.csv",
[Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()] [String] $logFile = ((Split-Path $script:MyInvocation.MyCommand.Path) + "\" + "Logs\ClientMove.log")
)
$ErrorActionPreference = "Stop"
############################ PARAMETERS #######################################
$SEPMConf = Get-Content $ParamFile | ConvertFrom-Json
############################## CONFIG #########################################
Set-LoggingDefaultLevel -Level 'INFO'
Add-LoggingTarget -Name Console
Add-LoggingTarget -Name File -Configuration @{Path = $logFile}
Write-Log -Level 'INFO' -Message "DEBUT DU SCRIPT"
Write-Log -Level 'INFO' -Message "Loaded Params : $ParamFile"
############################## FUNCTIONS ######################################
Function ConvertFrom-CustomSecureString{
param(
[Parameter(Mandatory=$true)][ValidateNotNull()] [SecureString] $SecureString
)
# Convert a secure string directly to a plaintext string with :
# > ConvertFrom-SecureString -SecureString $secureString -AsPlainText # 'Example
# only exist in PS V7.0+, so we'll use the old fashion way...
return ([System.Runtime.InteropServices.Marshal]::PtrToStringAuto(([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString))))
}
Function Invoke-SEPMRestAPI{
param(
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] [String] $Uri,
[Parameter(Mandatory=$false)] [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = [WebRequestMethod]::Get,
[Parameter(Mandatory=$false)] [System.Collections.IDictionary] $Header,
[Parameter(Mandatory=$false)] [String] $Body,
[Parameter(Mandatory=$false)] [String] $ContentType = 'application/json',
[switch] $IgnoreCert = $false
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$parameters = @{
Uri = $Uri
Method = $Method
ContentType = $ContentType
}
if($Header){ $parameters['Headers'] = $Header }
if($Body){ $parameters['Body'] = $Body }
try {
Write-Log -Level 'DEBUG' -Message "Requesting : $Uri with $Method and $ContentType"
if($IgnoreCert){
$result = Invoke-RestMethod @parameters -SkipCertificateCheck
}else{
$result = Invoke-RestMethod @parameters
}
} catch {
$ErrorMessage = $_.Exception.Message
$ErrorType = $_.exception.GetType().fullname
Write-Log -Level 'ERROR' -Message "Error in request '$Method' to '$Uri', with header '$Header' and body '$Body' ; Error Details: $ErrorType - $ErrorMessage"
}
#Write-Log -Level 'DEBUG' -Message "The Server Response is : $result"
$result
}
Function Get-SEPMAuthToken {
param(
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] [String] $UserName,
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] [SecureString] $Password,
[Parameter(Mandatory=$false)] [String] $SepmServer = $SEPMConf.SEPM_SERVER_IP,
[Parameter(Mandatory=$false)] [String] $SepmApiPort = $SEPMConf.SEPM_SERVER_PORT,
[Parameter(Mandatory=$false)] [String] $SepmDomain = $SEPMConf.SEPM_DOMAIN,
[switch] $IgnoreCert = $false
)
# map cred in Json
$cred= @{
username = $UserName
password = (ConvertFrom-CustomSecureString -SecureString $Password)
domain = $SepmDomain
} | ConvertTo-Json -Compress
$URIString = "https://" +$SepmServer +":" +$SepmApiPort +"/sepm/api/v1/identity/authenticate"
if($IgnoreCert){
$result = Invoke-SEPMRestAPI -Uri $URIString -Method Post -Body $cred -IgnoreCert
}else{
$result = Invoke-SEPMRestAPI -Uri $URIString -Method Post -Body $cred
}
return $result.token
}
function ConvertTo-HashtableFromPsCustomObject {
param (
[Parameter(
Position = 0,
Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true
)] [object] $psCustomObject
);
Write-Log -Level 'DEBUG' -Message "Start ConvertTo-HashtableFromPsCustomObject"
$output = @{};
$psCustomObject | Get-Member -MemberType *Property | Foreach-Object {
$output.($_.name) = $psCustomObject.($_.name);
}
Write-Log -Level 'DEBUG' -Message "Exit ConvertTo-HashtableFromPsCustomObject"
return $output;
}
Function Get-AllClientsList {
param(
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] [String] $AuthToken,
[Parameter(Mandatory=$false)] [String] $PageSize = 10000,
[Parameter(Mandatory=$false)] [String] $SepmServer = $SEPMConf.SEPM_SERVER_IP,
[Parameter(Mandatory=$false)] [String] $SepmApiPort = $SEPMConf.SEPM_SERVER_PORT,
[Parameter(Mandatory=$false)] [String] $SepmDomain = $SEPMConf.SEPM_DOMAIN
)
Write-Log -Level 'INFO' -Message "requesting all clients in $SepmDomain"
$header =@{Authorization="Bearer $AuthToken"}
$PageIndex = 0
$results = @()
do {
$PageIndex++
$URIString = "https://" +$SepmServer +":" +$SepmApiPort +"/sepm/api/v1/computers/?pageSize=" +$PageSize+"&pageIndex="+$PageIndex
Write-Log -Level 'DEBUG' -Message ("crawling element from " + (($PageIndex-1)*$PageSize) +" to "+ (($PageIndex)*$PageSize)+" (from $URIString)" )
$hostlist = Invoke-SEPMRestAPI -Uri $URIString -Header $header -Method Get -IgnoreCert
$TotalHost = 0
if(($hostlist.getType()).Name -eq "PSCustomObject"){
$hostlist = ConvertTo-HashtableFromPsCustomObject $hostlist
}else{
$hostlist = $hostlist | ConvertFrom-Json -AsHashtable
}
$TotalHost = $hostlist.totalElements
$results += @($hostlist.content)
Write-Log -Level 'INFO' -Message ("We have : "+$results.count +" elements yet, over $TotalHost total")
} while ($PageIndex*$PageSize -lt $TotalHost)
Write-Log -Level 'DEBUG' -Message ("We collected : "+$results.count +" elements, and requested "+ $PageIndex + " pages of "+$PageSize+ "elements")
return $results
}
############################## SCRIPT #########################################
#run time : more or less 1h.
$i=0
#Delete restult file if exists
if (Test-Path $OutputFile) {
Remove-Item $OutputFile
}
Foreach ($domain in $SEPMConf.SEPM_DOMAIN) {
$AuthToken = Get-SEPMAuthToken -UserName $SEPMConf.SEPM_USERNAME -Password (ConvertTo-SecureString ($SEPMConf.SEPM_PASSWORD) -AsPlainText -Force) -SepmDomain $domain -IgnoreCert
Write-Log -Level 'INFO' -Message "The SEPM login token is : $AuthToken"
if($AuthToken){
$res = Get-AllClientsList -AuthToken $AuthToken -SepmDomain $domain
Write-Log -Level 'INFO' -Message ("Number of results : " + $res.Count)
}
if($res.Count -gt 0){
try {
Write-Log -Level 'INFO' -Message ("Start Writing in result file : $OutputFile")
$stream = [System.IO.StreamWriter]::new("$OutputFile", $true, [Text.Encoding]::UTF8 )
#if first run, write csv header
if($i -eq 0){
$tmp = $res[0]
if(($res[0].getType()).Name -eq "PSCustomObject"){
$tmp = ConvertTo-HashtableFromPsCustomObject $res[0]
}
foreach($key in $tmp.Keys){
$stream.Write("$key"+',');
}
}
$i++
$stream.WriteLine("");
#Now the data
$text = ""
for($j=0;$j -lt $res.Count; $j++){
$computer = $res[$j]
Write-Progress -Activity "Preparing data for file" -Status ("computer $($j+1) of + " + ($res.Count)) -PercentComplete ((($j+1) / ($res.Count)) * 100)
if(($computer.getType()).Name -eq "PSCustomObject"){
$computer = ConvertTo-HashtableFromPsCustomObject $computer
}
$line=""
foreach($key in $computer.Keys){
$value = ""
if($key -in @("gateways", "winServers","subnetMasks","ipAddresses","macAddresses","dnsServers")){
$value = (($computer.$key) -join '|')
}elseif($key -eq "group"){
$value = "groupName="+ $computer.$key.name + "|groupid="+ $computer.$key.id + "|domainName="+$computer.$key.domain.name +'|domainId' + $computer.$key.domain.id
}elseif($key -eq "quarantineDesc"){
$value = ($null -ne $computer.$key) ? $computer.$key.replace("`n"," ").replace("`r"," ") : ""
}else{
$value = ($null -ne $computer.$key) ? ($computer.$key).toString() : ""
}
#Write-Log -Level 'DEBUG' -Message ("Writing: " + $key + "= value :" + $value)
if($key -eq $computer.Keys[-1]){
$line += ($value)
}else{
$line += ($value + ',')
}
}
$text += "$line`n";
}
Write-Progress -Activity "Preparing data for file" -Completed
Write-Log -Level 'DEBUG' -Message ("Writing File for domaine" + $domain)
$stream.Write("$text");
$stream.Flush();
} Catch {
Write-Warning ("An error occurred : " + $_.Exception.Message)
Write-Warning $_.Exception.ErrorRecord
Write-Warning $_.ScriptStackTrace
} finally {
$stream.close()
}
}
}
Write-Log -Level 'INFO' -Message ("Finish write results.")
return "\o/"
Write-Log -Level 'INFO' -Message "FIN DU SCRIPT"
Wait-Logging # Flush Log File.
Sachant que je charge le fichier de config suivant en haut du script
{
"SEPM_DOMAIN": "GEEK",
"SEPM_SERVER_IP": "1.2.3.4",
"SEPM_SERVER_DNS": "sepm.geekeries.org",
"SEPM_SERVER_PORT": "8446",
"SEPM_USERNAME": "etienne",
"SEPM_PASSWORD": "thisisamoresecurepasswordthanitseems!",
"LOGFILE": "Logs\\ClientMove.log"
}
Documentation
L’autre bonne nouvelle c’est que Symantec Broadcom a documenté, à peu près, son API et qu’on peut faire plein d’autres choses avec. Pour ceux qui cherchent des idées de conneries à faire, c’est par ici :
Permissions
J’en profite pour préciser que le script ne fonctionne pas en tant qu’utilisateur lambda évidement, vous devez avoir des droits suffisant sur l’infra SEPM.
Conclusion
Bon voilà, pas de grands discours aujourd’hui, mais sachez qu’on peut faire quelques trucs fun avec cette API REST : changer des postes de groupe, modifier les politiques, envoyer les commandes antivirus à un client pour télécharger ou uploader un fichier, etc.
C’est très bien pour automatiser des tâches d’administration, ou pour un attaquant qui tomberait sur un accès oublié à cette API. Mon conseil : vérrrouiller les ports vers cette API comme ceux pour l’interface d’admin pour que seul les admins puissent y accéder… et vérifier vos mots de passe aussi, hein 😉
Voilà, c’est cadeau, amusez vous bien avec le script et à la prochaine !