-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSEP_RemoveDuplicatedClients.ps1
More file actions
201 lines (166 loc) · 8.22 KB
/
SEP_RemoveDuplicatedClients.ps1
File metadata and controls
201 lines (166 loc) · 8.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# =================================================================================
# Author: Pawel Lesniewski
# Created: 2025.08.27
# Description: PowerShell script for removing duplicate SEPM clients via REST API
# Version: 1.0
#
# WARNING: Running this script may cause irreversible changes to the SEPM database.
# Always back up and test in a development environment.
#
# USE AT YOUR OWN RISK
#
# https://{{SEPM_server_address}}:8446/sepm/restapidocs.html
# Keywords: SEP Remove Duplicate Clients, SEPM REST API, Symantec, Broadcom,
# Symantec Endpoint Protection, Symantec Endpoint Security
# =================================================================================
# ============== VARIABLES ==============
# SEPM IP Address or FQDN
$sepmServer = "{{SEPM_server_address}}"
# API Port (default 8446)
$sepmPort = 8446
# SEPM administrators login
$sepmUser = "$($env:USERNAME)"
# Users password (leave blank for security reasons, script will ask for pass)
$sepmPassword = ""
# Dry Run - Set to $true to show what will be deleted
[bool]$dryRun = $true
# Set to $false to PERMANENTLY REMOVE clients
# $dryRun = $false
# =====================================================
function ConvertFrom-UnixTime ([UInt64]$EpochTime){
if ($EpochTime -gt [uint32]::MaxValue) {
$result = ([System.DateTimeOffset]::FromUnixTimeMilliseconds($EpochTime))
} else {
$result = ([System.DateTimeOffset]::FromUnixTimeSeconds($EpochTime))
}
return $result
}
# Ignore SSL certificate errors (usefull for self-signed certs)
add-type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
# --- STEP 1: AUTHENTICATION AND TOKEN DOWNLOAD ---
if (-not $sepmPassword) {
$sepmPassword = Read-Host -AsSecureString -Prompt "Logon as '$sepmUser'"
}
$baseApiUrl = "https://{0}:{1}/sepm/api/v1" -f $sepmServer, $sepmPort
$authUrl = "$baseApiUrl/identity/authenticate"
$headers = @{ "Content-Type" = "application/json" }
$authBody = @{
"username" = $sepmUser
"password" = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sepmPassword))
"domain" = "" # Leave blank if you are using a local SEPM account
} | ConvertTo-Json
Write-Host "Step 1: Authentication on the $sepmServer server..."
try {
$authResponse = Invoke-RestMethod -Uri $authUrl -Method Post -Headers $headers -Body $authBody
$accessToken = $authResponse.token
Write-Host "Authentication successful. Token obtained." -ForegroundColor Green
}
catch {
Write-Error "Error while authenticating: $($_.Exception.Message)"
exit
}
# --- STEP 2: DOWNLOAD THE LIST OF ALL CUSTOMERS ---
$computersUrl = "$baseApiUrl/computers"
$apiHeaders = @{
"Content-Type" = "application/json"
"Authorization" = "Bearer $accessToken"
}
Write-Host "Step 2: Downloading a list of all clients... (this may take a while)"
try {
# The API returns data in pages, the 'do-while' loop will fetch all pages
$allClients = @()
$pageIndex = 1
do {
$pagedUrl = "$($computersUrl)?pageIndex=$($pageIndex)&pageSize=1000"
$pageResponse = Invoke-RestMethod -Uri $pagedUrl -Method Get -Headers $apiHeaders
if ($pageResponse.content) {
$allClients += $pageResponse.content
}
$pageIndex++
Write-Host -NoNewline "."
} while ($pageResponse.totalElements -gt $allClients.Count)
Write-Host "`nA total of $($allClients.Count) clients were downloaded.." -ForegroundColor Green
}
catch {
Write-Error "Error while retrieving customer list: $($_.Exception.Message)"
exit
}
# --- STEP 3: IDENTIFY DUPLICATES ---
Write-Host "Step 3: Analyze and identify duplicates based on the Client Name (Computer Name)..."
# We group clients by 'hardwareKey' and filter those groups that have more than one member
# $duplicates = $allClients | Group-Object -Property hardwareKey | Where-Object { $_.Count -gt 2 }
# We group clients by 'computerName' and filter those groups that have more than one member
$duplicates = $allClients | Group-Object -Property computerName | Where-Object { $_.Count -gt 2 }
if ($duplicates.Count -eq 0) {
Write-Host "No duplicates found. Finishing work." -ForegroundColor Green
exit
}
Write-Host "A $($duplicates.Count) groups of duplicate clients found." -ForegroundColor Yellow
$totalDeletedCount = 0
$duplicates | %{$totalDeletedCount = $totalDeletedCount + $($_.Count) - 1 }
Write-Host "Found $($totalDeletedCount) duplicates to remove." -ForegroundColor Yellow
$RequestTimeout = 0
$New_TimeStamp = (Get-Date -format "yyMMdd_HHmmss").ToString()
$duplicates | select count, name, @{n="Group";e={$_.Group[1].group.name}} | Export-Csv -Delimiter ";" -Path "$env:USERPROFILE\Downloads\SEP-Duplicates_$New_TimeStamp.csv"
# --- STEP 4 and 5: SELECT THE LATEST AND REMOVE THE REST ---
if ($dryRun) {
Write-Host "[TEST MODE (Dry Run)] The script will only display which clients would be deleted." -ForegroundColor Cyan
} else {
Write-Host "[REAL MODE] The script will remove duplicate clients!" -ForegroundColor Red -BackgroundColor White
if ($duplicates.Count -gt 50) {$RequestTimeout = 1100}
}
$totalDeletedCount = 0
foreach ($group in $duplicates) {
Write-Host "--------------------------------------------------------"
Write-Host "Analyzing duplicates by computer name: $($group.Name)"
# We sort clients in a group by their last check-in date, from newest to oldest
$sortedClients = $group.Group | Sort-Object -Property agentTimeStamp -Descending
# The first one on the list is the newest one - we'll leave that one alone
$clientToKeep = $sortedClients | Select-Object -First 1
# The rest are duplicates to be removed
$clientsToDelete = $sortedClients | Select-Object -Skip 1
# Write-Host " > client to leave: $($clientToKeep.computerName) (lastConn: $($clientToKeep.lastCheckinTime))" -ForegroundColor Green
Write-Host " > client to leave: $($clientToKeep.computerName), CompID: $($clientToKeep.uniqueId), HWkey: $($clientToKeep.hardwareKey), (lastConn: $((ConvertFrom-UnixTime $clientToKeep.agentTimeStamp).datetime))" -ForegroundColor Green
foreach ($client in $clientsToDelete) {
Write-Host " > client to REMOVE: $($client.computerName), CompID: $($client.uniqueId), HWkey: $($client.hardwareKey), lastConn: $((ConvertFrom-UnixTime $client.agentTimeStamp).datetime))" -ForegroundColor Yellow
if (-not $dryRun) {
$deleteUrl = "$computersUrl/$($client.uniqueId)"
try {
Invoke-RestMethod -Uri $deleteUrl -Method Delete -Headers $apiHeaders
Write-Host " - SUCCESS: Client removed $($client.computerName)." -ForegroundColor DarkGreen -BackgroundColor Green
$totalDeletedCount++
# The API will execute 50 requests and then reject the next ones with error 429. To prevent this, the script waits 1,3 seconds after each request.
# https://knowledge.broadcom.com/external/article/174399/symantec-endpoint-protection-api-created.html
sleep -Milliseconds $RequestTimeout
}
catch {
Write-Host " - ERROR: Failed to delete client $($client.computerName). Message: $($_.Exception.Message)" -ForegroundColor Red
exit
}
}
}
}
Write-Host "--------------------------------------------------------"
if ($dryRun) {
Write-Host "Test mode completed. No changes made." -ForegroundColor Cyan
} else {
Write-Host "Deletion process completed. A total of $totalDeletedCount clients were deleted." -ForegroundColor Green
}
# --- STEP 6: LOGOUT (INVALIDATE TOKEN) ---
$logoutUrl = "$baseApiUrl/identity/logout"
try {
Invoke-RestMethod -Uri $logoutUrl -Method Post -Headers $apiHeaders
Write-Host "The access token has been invalidated."
}
catch {
Write-Warning "Failed to revoke access token: $($_.Exception.Message)"
}