SEPM Computers REST API

SEPM Computers REST API

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 , ou encore ) 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 :

https://apidocs.symantec.com/

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 !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.