API Access¶
Tactical RMM uses a Django Rest Framework backend which is designed to be consumed by a VueJS frontend.
Therefore, anything you can do via the web interface, you can do via the API.
However this makes it difficult to document the API as it has only been designed to be consumed by our vue frontend.
The easiest way to see what endpoint/payload you need to send is to open your browser's developer tools > Network tab. Then, perform the action you wish to do via the api in Tactical's web interface and watch the network tab to see the endpoint and the payload that is generated, and use that as an example of how to structure your api request.
Please note that using an API key will bypass 2FA authentication.
Please note that Tactical RMM, rigorously battle-tested and proven for production readiness, adheres to Semantic Versioning. However, as we have not reached a 1.0.0 release, be advised that the API is still evolving and may experience breaking changes.
When creating the key you'll need to choose a user, which will reflect what permissions the key has based on the user's role.
Navigate to Settings > Global Settings > API Keys to generate a key.
Warning
Pay attention to your trailing /
they matter.
Headers:
{
"Content-Type": "application/json",
"X-API-KEY": "J57BXCFDA2WBCXH0XTELBR5KAI69CNCZ"
}
Example curl request:
curl https://api.example.com/clients/ -H "X-API-KEY: Y57BXCFAA9WBCXH0XTEL6R5KAK69CNCZ"
Enable Swagger¶
This will let you add a browser interface to see how you can use the API better.
Open /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
and add
SWAGGER_ENABLED = True
Restart Django: sudo systemctl restart rmm.service
Then visit https://api.example.com/api/schema/swagger-ui/
to see it in action.
Beta API¶
Version added: Tactical RMM v0.16.5
A beta API is now available at /beta/v1
which supports filtering and pagination.
To activate it, first enable swagger (see above) and then add the following line to /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
:
BETA_API_ENABLED = True
Then restart Django with: sudo systemctl restart rmm.service
and check Swagger for usage.
Querying the API¶
Here are some examples:
Example Code
Requests Windows Update check to run against agent ID
import requests
API = "http://api.example.com"
HEADERS = {
"Content-Type": "application/json",
"X-API-KEY": "DKNRPTHSAPCKT8A36MCAMNZREWWWFPWI",
}
def trigger_update_scan():
agents = requests.get(f"{API}/agents/?detail=false", headers=HEADERS)
for agent in agents.json():
r = requests.post(f"{API}/winupdate/{agent['agent_id']}/scan/", headers=HEADERS)
print(r.json())
if __name__ == "__main__":
trigger_update_scan()
# Example - Get all agents using API
$headers = @{
'X-API-KEY' = 'ABC1234567890987654321'
}
$url = "https://api.yourdomain.com/agents/"
$agentsResult = Invoke-RestMethod -Method 'Get' -Uri $url -Headers $headers -ContentType "application/json"
foreach ($agent in $agentsResult) {
Write-Host $agent
#Write-Host $agent.hostname
}
# Example - Send PowerShell command to agent. Make sure to pass {{agent.agent_id}} as a parameter
param(
$AgentId
)
$headers = @{
'X-API-KEY' = 'ABC1234567890987654321'
}
$url = "https://api.yourdomain.com/agents/$AgentId/cmd/"
$body = @{
"shell" = "powershell"
"cmd" = "dir c:\\users"
"timeout" = 30
}
$commandResult = Invoke-RestMethod -Method 'Post' -Uri $url -Body ($body|ConvertTo-Json) -Headers $headers -ContentType "application/json"
Write-Host $commandResult
Running a Script on an Agent Using the API¶
First get the script ID from the Script library by hovering your mouse over it in the script list and looking at the tooltip.
POST
to the endpoint /agents/<agentid>/runscript/
this:
{
"output": "forget",
"emails": [],
"emailMode": "default",
"custom_field": null,
"save_all_output": false,
"script": 89, // primary key of script in postgres
"args": [
"arg1",
"arg2"
],
"env_vars": [],
"run_as_user": false,
"timeout": 90 // seconds
}
Example: Run a script on an agent
$apiUrl = 'https://api.example.com/agents/CHANGEME/runscript/'
$jsonPayload = @{
output = "wait"
emails = @()
emailMode = "default"
custom_field = $null
save_all_output = $false
script = 89
args = @()
env_vars = @()
run_as_user = $false
timeout = 90
} | ConvertTo-Json
$headers = @{
'Content-Type' = 'application/json'
'X-API-KEY' = 'changeme'
}
$response = Invoke-RestMethod -Uri $apiUrl -Method Post -Body $jsonPayload -Headers $headers
$response
API via CLI¶
https://gitlab.com/NiceGuyIT/trmm-cli
API Examples¶
Listing all software on all agents:¶
import requests
API = "https://api.example.com"
HEADERS = {
"Content-Type": "application/json",
"X-API-KEY": "9SI43IFUMPEVRWOZR4NC8PGP4ZLA9PYX",
}
def get_software():
agents = requests.get(f"{API}/agents/?detail=false", headers=HEADERS) # get a list of all agents
for agent in agents.json(): # loop thru all agents and print list of installed software
r = requests.get(f"{API}/software/{agent['agent_id']}/", headers=HEADERS)
print(r.json())
if __name__ == "__main__":
get_software()
List Agents¶
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Agents_Endpoint = GetURL("agents")
$Agents = Invoke-RestMethod $Agents_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Agents | ConvertTo-Json
$Agents.foreach({ $_ }) | Select-Object Agent_ID, Hostname, Site_Name, Client_Name, Monitoring_Type | Format-Table
List Tasks¶
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Tasks_Endpoint = GetURL("tasks")
$Response = Invoke-RestMethod $Tasks_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Response | ConvertTo-Json
$Response.foreach({ $_ }) | Select-Object ID, Policy, Name, Custom_Field | Format-Table
Get Custom Fields¶
$TRMM_API_Base_Endpoint = Read-Host "Please provide your API domain i.e https://api.domain.com"
$TRMM_API_Key = Read-Host "Please provide your API Key"
$TRMM_API_CustomFields_Endpoint = $TRMM_API_Base_Endpoint + "/core/customfields"
$TRMM_Request_Headers = @{
"X-API-KEY" = $TRMM_API_Key
}
$Request = Invoke-RestMethod $TRMM_API_CustomFields_Endpoint -Headers $TRMM_Request_Headers -Method Get
$TRMM_Agent_ExistingCustomFields = ($Request | ? {$_.model -eq 'Agent'})
# Gets all of the Windows Features and places it in an array to parse later
# This is to ensure that TRMM has all Windows Feature roles available as a Custom Field
$AllWindowsFeatures = Get-WindowsFeature
$TRMM_Role_AllCustomFields = @()
foreach($WindowsFeature in $AllWindowsFeatures){
if($WindowsFeature.Depth -eq 1){
$PrimaryRole = $WindowsFeature.DisplayName
} else {
$TRMM_Role_AllCustomFields += [PSCustomObject]@{
"Role" = $PrimaryRole
"Value" = $WindowsFeature.DisplayName
}
}
}
$Unique_Roles = $TRMM_Role_AllCustomFields | Select -Unique Role
#Loop through each WindowsFeature
foreach($WindowsFeature in $Unique_Roles){
if($TRMM_Agent_ExistingCustomFields.Name -like "*$($WindowsFeature.Role)*"){
# Need to check if it also contains the value
} else {
# Check to see if the Role is an Array or an Object
# Convert to Array otherwise
if((($TRMM_Role_AllCustomFields | ? {$_.Role -eq $WindowsFeature.Role}).Value) -isnot [array]){
$CustomFieldOptions = @(($TRMM_Role_AllCustomFields | ? {$_.Role -eq $WindowsFeature.Role}).Value)
} else {
$CustomFieldOptions = ($TRMM_Role_AllCustomFields | ? {$_.Role -eq $WindowsFeature.Role}).Value
}
# Create the Custom Field body
$CustomFieldData = @{
model = "agent"
name = "Server Role - " + $WindowsFeature.Role
type = "multiple"
options = $CustomFieldOptions
}
$CreateCustomField = Invoke-RestMethod $TRMM_API_CustomFields_Endpoint -Headers $TRMM_Request_Headers -Method Post -Body ($CustomFieldData | ConvertTo-Json) -ContentType "application/json"
}
}
Get Custom Fields¶
https://discord.com/channels/736478043522072608/888474369544847430/1004184193078669522
import requests
import json
API = "https://api.example.com"
HEADERS = {
"Content-Type": "application/json",
"X-API-KEY": "changeme",
}
payload = {"custom_fields": [{"string_value": "66666", "field": 2}]}
r = requests.put(
f"{API}/agents/IcAhWvOHlADwjuYmndAkjbJuhSdWvWjtvYcYjCZk/",
data=json.dumps(payload),
headers=HEADERS,
)
print(r.__dict__)
Create Collector Tasks¶
https://discord.com/channels/736478043522072608/888474369544847430/1004383584070676610
# Query the TRMM API with PowerShell
# Add this to dotenv.ps1
#$TRMM_API_KEY = ""
#$TRMM_API_DOMAIN = ""
. .\dotenv.ps1
function GetURL {
param (
[string] $Path
)
return "https://${TRMM_API_DOMAIN}/${Path}/"
}
$Headers = @{
'X-API-KEY' = $TRMM_API_KEY
}
$Collector = [PSCustomObject]@{
policy = 1
actions = @(@{
name = "Collector - Location - City and State"
type = "script"
script = 121
timeout = 90
script_args = @()
})
assigned_check = $null
custom_field = 14
name = "Get city and state from API"
# expire_date = $null
run_time_date = "2022-08-02T08:00:00Z"
# run_time_bit_weekdays = $null
weekly_interval = 1
daily_interval = 1
# monthly_months_of_year = $null
# monthly_days_of_month = $null
# monthly_weeks_of_month = $null
# task_instance_policy = 0
# task_repetition_interval = $null
# task_repetition_duration = $null
# stop_task_at_duration_end = $false
# random_task_delay = $null
# remove_if_not_scheduled = $false
run_asap_after_missed = $true
task_type = "daily"
alert_severity = "info"
# collector_all_output = $false
# continue_on_error = $false
}
$Tasks_Endpoint = GetURL("tasks")
$Payload = $Collector | ConvertTo-Json
$Response = Invoke-RestMethod $Tasks_Endpoint -Headers $Headers -ContentType "application/json" -Method "Post" -Body $Payload
if (! $?) {
Write-Output "Failed to submit request: $($error[0].ToString() )"
Write-Output "Stacktrace: $($error[0].Exception.ToString() )"
Exit(1)
}
$Response | ConvertTo-Json
Show Tasks¶
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Tasks_Endpoint = GetURL("tasks")
$Response = Invoke-RestMethod $Tasks_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Response | ConvertTo-Json
$Response.foreach({ $_ }) | Select-Object ID, Policy, Name, Custom_Field | Format-Table
Show Task/collector results¶
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Agents_Endpoint = GetURL("agents")
$Agents = Invoke-RestMethod $Agents_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Agents | ConvertTo-Json
# $Agents.foreach({ $_ }) | Select-Object Agent_ID, Hostname, Site_Name, Client_Name, Monitoring_Type | Format-Table
$Agent_ID = "AGENT ID FROM TABLE"
$Agents.foreach({
if ($_.Agent_ID -ne $Agent_ID) {
return
}
$Fields_Endpoint = GetURL("agents/$($_.Agent_ID)/tasks")
$Fields = Invoke-RestMethod $Fields_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Fields | ConvertTo-Json
$Fields.foreach({ $_ }) | Select-Object ID, Check_Name, Name, Actions, Task_Result | Format-Table
})
or to see results
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Agents_Endpoint = GetURL("agents")
$Agents = Invoke-RestMethod $Agents_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Agents | ConvertTo-Json
# $Agents.foreach({ $_ }) | Select-Object Agent_ID, Hostname, Site_Name, Client_Name, Monitoring_Type | Format-Table
$Agent_ID = "AGENT ID FROM TABLE"
$Agents.foreach({
if ($_.Agent_ID -ne $Agent_ID) {
return
}
$Fields_Endpoint = GetURL("agents/$($_.Agent_ID)/tasks")
$Fields = Invoke-RestMethod $Fields_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
# $Fields | ConvertTo-Json
$Fields.foreach({
$_.Task_Result | Format-Table
})
})
List Custom Fields of Agent¶
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Agent_ID = "ENTER AGENT ID HERE"
$Agents_Endpoint = GetURL("agents/$($Agent_ID)")
$Agents = Invoke-RestMethod $Agents_Endpoint -Headers $Headers -ContentType "application/json" -Method "Get"
ConvertTo-Json $Agents.Custom_Fields
$Agents.Custom_Fields
Update Custom Fields¶
https://discord.com/channels/736478043522072608/888474369544847430/1004422340815360111
This will update the custom fields for an agent. The field value come from the custom fields two posts above. Other fields that exist will not be updated. If you want to update only 1 custom field, use only 1 entry in the array.
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Agent_ID = "AGENT ID GOES HERE"
$Endpoint = GetURL("agents/$($Agent_ID)")
$Custom_Fields = [PSCustomObject]@{
custom_fields = @(
@{
field = 13
string_value = "Value updated from the API"
},
@{
field = 14
string_value = "Another value from the API"
}
)
}
$Payload = $Custom_Fields | ConvertTo-Json
$Response = Invoke-RestMethod $Endpoint -Headers $Headers -ContentType "application/json" -Method Put -Body $Payload
if (! $?) {
Write-Output "Failed to submit request: $($error[0].ToString() )"
Write-Output "Stacktrace: $($error[0].Exception.ToString() )"
Exit(1)
}
$Response | ConvertTo-Json
List service information from Agent¶
# Query the TRMM API with PowerShell
. .\dotenv.ps1
$Payload = [PSCustomObject]@{
code = [string](Get-Content -Path .\TRMM-Run-Cmd-Remote.ps1 -Raw -Encoding UTF8)
timeout = 90
args = @()
shell = "powershell"
}
$Agent_ID = "ENTER AGENT ID HERE"
$Rest_Params = @{
Uri = GetURL("scripts/${Agent_ID}/test")
Headers = $Headers
ContentType = "application/json"
Method = "POST"
Body = $Payload | ConvertTo-Json
}
# Write-Output $Rest_Params["Body"]
# Prefer to handle the error ourselves rather than fill the screen with red text.
$ErrorActionPreference = 'SilentlyContinue'
$Response = Invoke-RestMethod @Rest_Params
if (! $?) {
Write-Output "Failed to submit request: $($error[0].ToString() )"
Write-Output "Stacktrace: $($error[0].Exception.ToString() )"
Exit(1)
}
# $Response | ConvertTo-Json
$Services = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Response))
ConvertFrom-Json $Services
Combining scripts¶
This will iterate over all agents, filter out non-Windows and offline agents, run the PowerShell above to the first 5 services that are running, and compile the information in a Servers array.
# Query the TRMM API with PowerShell
. .\dotenv.ps1
# Default parameters. Body is missing because GET does not include the body.
$Rest_Params = @{
Uri = ""
Headers = $Headers
ContentType = "application/json"
Method = "GET"
}
# Prefer to handle the error ourselves rather than fill the screen with red text.
$ErrorActionPreference = 'SilentlyContinue'
$Rest_Params["Uri"] = GetURL("agents")
$Agents = Invoke-RestMethod @Rest_Params
if (! $?) {
Write-Output "Failed to get list of agents from API: $($error[0].ToString() )"
Write-Output "Stacktrace: $($error[0].Exception.ToString() )"
Exit(1)
}
# Remote command to run.
$Payload = [PSCustomObject]@{
# Without the type casting to [string], this was returning properties relating to Get-Content.
code = [string](Get-Content -Path .\TRMM-Run-Cmd-Remote.ps1 -Raw -Encoding UTF8)
timeout = 90
args = @()
shell = "powershell"
}
$Rest_Params["Body"] = [string]($Payload | ConvertTo-Json)
$Rest_Params["Method"] = "POST"
$Servers = @()
foreach($Agent in $Agents) {
# Build this object to match your needs.
$Server = [PSCustomObject]@{
Agent_ID = $Agent.agent_id
# Services = "Error / Unavailable"
Service_Name = "N/A"
Service_Status = "N/A"
Service_DisplayName = "N/A"
Hostname = $Agent.hostname
Platform = $Agent.plat
Status = $Agent.status
}
if ($Agent.plat -ne "windows" -or $Agent.status -ne "online") {
# Only process online Windows agents. Add other checks as necessary.
# Uncomment this if you want to include filtered servers in the output.
# $Servers += $Server.PSObject.Copy()
continue
}
$Rest_Params["Uri"] = GetURL("scripts/$($Agent.Agent_ID)/test")
$Response = Invoke-RestMethod @Rest_Params
if (! $?) {
$Servers += $Server.PSObject.Copy()
continue
}
# Create a new server object for every item returned from the remote agent (Services in this case).
$Services = ConvertFrom-Json ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Response)))
foreach($Service in $Services) {
$Server.Service_Name = $Service.Name
$Server.Service_Status = $Service.Status
$Server.Service_DisplayName = $Service.DisplayName
$Servers += $Server.PSObject.Copy()
}
}
$Servers | Format-Table Agent_ID, Hostname, Status, Service_Name, Service_Status, Service_DisplayName
# $Servers
Pulling Audit Logs¶
<?php
$ApiKey = "XXX";
$BasicUrl = "https://api.trmm.xxx.xxx/";
$Url = "/logs/audit/";
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $BasicUrl.$Url);
$payload = '{"pagination":{"sortBy":"entry_time","descending":true,"page":1,"rowsPerPage":1000,"rowsNumber":2},"agentFilter":["QFhdnzJcFnodMibCGJYmfIwRyjgIazjajjMSvVWX"],"actionFilter":["modify"]}';
curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$headers = ['Content-Type: application/json', 'X-API-KEY: '.$ApiKey];
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PATCH');
$data = curl_exec($curl);
curl_close($curl);
$audit = json_decode($data, true);
var_dump($audit);
?>
Update software using API¶
#!/rmm/api/env/bin/python
import concurrent.futures
import requests
API = "https://api.CHANGEME.com"
API_KEY = "CHANGEME"
HEADERS = {
"Content-Type": "application/json",
"X-API-KEY": API_KEY,
}
MAX_WORKERS = 10
def create_list_of_urls():
ret = []
agents = requests.get(f"{API}/agents/?detail=false", headers=HEADERS)
for agent in agents.json():
ret.append(f"{API}/software/{agent['agent_id']}/") # refresh software endpoint
return ret
def do_software_refresh(url):
r = requests.put(url, headers=HEADERS)
return r.json()
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = []
for url in create_list_of_urls():
futures.append(executor.submit(do_software_refresh, url))
for future in concurrent.futures.as_completed(futures):
print(future.result())