578 lines
19 KiB
Python
578 lines
19 KiB
Python
import asyncio
|
|
|
|
from django.conf import settings
|
|
from django.db.models import Prefetch
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils import timezone as djangotime
|
|
from packaging import version as pyver
|
|
from rest_framework.authentication import TokenAuthentication
|
|
from rest_framework.authtoken.models import Token
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from accounts.models import User
|
|
from agents.models import Agent, AgentHistory
|
|
from agents.serializers import AgentHistorySerializer
|
|
from apiv3.utils import get_agent_config
|
|
from autotasks.models import AutomatedTask, TaskResult
|
|
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
|
|
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
|
|
from checks.models import Check, CheckResult
|
|
from checks.serializers import CheckRunnerGetSerializer
|
|
from core.utils import (
|
|
download_mesh_agent,
|
|
get_core_settings,
|
|
get_mesh_device_id,
|
|
get_mesh_ws_url,
|
|
get_meshagent_url,
|
|
)
|
|
from logs.models import DebugLog, PendingAction
|
|
from software.models import InstalledSoftware
|
|
from tacticalrmm.constants import (
|
|
AGENT_DEFER,
|
|
AgentMonType,
|
|
AgentPlat,
|
|
AuditActionType,
|
|
AuditObjType,
|
|
CheckStatus,
|
|
DebugLogType,
|
|
GoArch,
|
|
MeshAgentIdent,
|
|
PAStatus,
|
|
)
|
|
from tacticalrmm.helpers import notify_error
|
|
from tacticalrmm.utils import reload_nats
|
|
from winupdate.models import WinUpdate, WinUpdatePolicy
|
|
|
|
|
|
class CheckIn(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
# called once during tacticalagent windows service startup
|
|
def post(self, request):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
if not agent.choco_installed:
|
|
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
|
|
|
|
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
|
|
return Response("ok")
|
|
|
|
|
|
class SyncMeshNodeID(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
if agent.mesh_node_id != request.data["nodeid"]:
|
|
agent.mesh_node_id = request.data["nodeid"]
|
|
agent.save(update_fields=["mesh_node_id"])
|
|
|
|
return Response("ok")
|
|
|
|
|
|
class Choco(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
agent.choco_installed = request.data["installed"]
|
|
agent.save(update_fields=["choco_installed"])
|
|
return Response("ok")
|
|
|
|
|
|
class WinUpdates(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def put(self, request):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
|
|
needs_reboot: bool = request.data["needs_reboot"]
|
|
agent.needs_reboot = needs_reboot
|
|
agent.save(update_fields=["needs_reboot"])
|
|
|
|
reboot_policy: str = agent.get_patch_policy().reboot_after_install
|
|
reboot = False
|
|
|
|
if reboot_policy == "always":
|
|
reboot = True
|
|
elif needs_reboot and reboot_policy == "required":
|
|
reboot = True
|
|
|
|
if reboot:
|
|
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
|
|
DebugLog.info(
|
|
agent=agent,
|
|
log_type=DebugLogType.WIN_UPDATES,
|
|
message=f"{agent.hostname} is rebooting after updates were installed.",
|
|
)
|
|
|
|
agent.delete_superseded_updates()
|
|
return Response("ok")
|
|
|
|
def patch(self, request):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore
|
|
if not u:
|
|
raise WinUpdate.DoesNotExist
|
|
|
|
success: bool = request.data["success"]
|
|
if success:
|
|
u.result = "success"
|
|
u.downloaded = True
|
|
u.installed = True
|
|
u.date_installed = djangotime.now()
|
|
u.save(
|
|
update_fields=[
|
|
"result",
|
|
"downloaded",
|
|
"installed",
|
|
"date_installed",
|
|
]
|
|
)
|
|
else:
|
|
u.result = "failed"
|
|
u.save(update_fields=["result"])
|
|
|
|
agent.delete_superseded_updates()
|
|
return Response("ok")
|
|
|
|
def post(self, request):
|
|
updates = request.data["wua_updates"]
|
|
if not updates:
|
|
return notify_error("Empty payload")
|
|
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
|
|
for update in updates:
|
|
if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore
|
|
u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore
|
|
u.downloaded = update["downloaded"]
|
|
u.installed = update["installed"]
|
|
u.save(update_fields=["downloaded", "installed"])
|
|
else:
|
|
try:
|
|
kb = "KB" + update["kb_article_ids"][0]
|
|
except:
|
|
continue
|
|
|
|
WinUpdate(
|
|
agent=agent,
|
|
guid=update["guid"],
|
|
kb=kb,
|
|
title=update["title"],
|
|
installed=update["installed"],
|
|
downloaded=update["downloaded"],
|
|
description=update["description"],
|
|
severity=update["severity"],
|
|
categories=update["categories"],
|
|
category_ids=update["category_ids"],
|
|
kb_article_ids=update["kb_article_ids"],
|
|
more_info_urls=update["more_info_urls"],
|
|
support_url=update["support_url"],
|
|
revision_number=update["revision_number"],
|
|
).save()
|
|
|
|
agent.delete_superseded_updates()
|
|
return Response("ok")
|
|
|
|
|
|
class SupersededWinUpdate(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore
|
|
for u in updates:
|
|
u.delete()
|
|
|
|
return Response("ok")
|
|
|
|
|
|
class RunChecks(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, agentid):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER).prefetch_related(
|
|
Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
|
|
),
|
|
agent_id=agentid,
|
|
)
|
|
checks = agent.get_checks_with_policies(exclude_overridden=True)
|
|
ret = {
|
|
"agent": agent.pk,
|
|
"check_interval": agent.check_interval,
|
|
"checks": CheckRunnerGetSerializer(
|
|
checks, context={"agent": agent}, many=True
|
|
).data,
|
|
}
|
|
return Response(ret)
|
|
|
|
|
|
class CheckRunner(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, agentid):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER).prefetch_related(
|
|
Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
|
|
),
|
|
agent_id=agentid,
|
|
)
|
|
checks = agent.get_checks_with_policies(exclude_overridden=True)
|
|
|
|
run_list = [
|
|
check
|
|
for check in checks
|
|
# always run if check hasn't run yet
|
|
if not isinstance(check.check_result, CheckResult)
|
|
or not check.check_result.last_run
|
|
# see if the correct amount of seconds have passed
|
|
or (
|
|
check.check_result.last_run
|
|
< djangotime.now()
|
|
- djangotime.timedelta(
|
|
seconds=check.run_interval or agent.check_interval
|
|
)
|
|
)
|
|
]
|
|
|
|
ret = {
|
|
"agent": agent.pk,
|
|
"check_interval": agent.check_run_interval(),
|
|
"checks": CheckRunnerGetSerializer(
|
|
run_list, context={"agent": agent}, many=True
|
|
).data,
|
|
}
|
|
return Response(ret)
|
|
|
|
def patch(self, request):
|
|
if "agent_id" not in request.data.keys():
|
|
return notify_error("Agent upgrade required")
|
|
|
|
check = get_object_or_404(
|
|
Check.objects.defer(*CHECK_DEFER),
|
|
pk=request.data["id"],
|
|
)
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
|
|
)
|
|
|
|
# get check result or create if doesn't exist
|
|
check_result, created = CheckResult.objects.defer(
|
|
*CHECK_RESULT_DEFER
|
|
).get_or_create(
|
|
assigned_check=check,
|
|
agent=agent,
|
|
)
|
|
|
|
if created:
|
|
check_result.save()
|
|
|
|
status = check_result.handle_check(request.data, check, agent)
|
|
if status == CheckStatus.FAILING and check.assignedtasks.exists():
|
|
for task in check.assignedtasks.all():
|
|
if task.enabled:
|
|
if task.policy:
|
|
task.run_win_task(agent)
|
|
else:
|
|
task.run_win_task()
|
|
|
|
return Response("ok")
|
|
|
|
|
|
class CheckRunnerInterval(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, agentid):
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER).prefetch_related("agentchecks"),
|
|
agent_id=agentid,
|
|
)
|
|
|
|
return Response(
|
|
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
|
|
)
|
|
|
|
|
|
class TaskRunner(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, pk, agentid):
|
|
agent = get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agentid)
|
|
task = get_object_or_404(AutomatedTask, pk=pk)
|
|
return Response(TaskGOGetSerializer(task, context={"agent": agent}).data)
|
|
|
|
def patch(self, request, pk, agentid):
|
|
from alerts.models import Alert
|
|
|
|
agent = get_object_or_404(
|
|
Agent.objects.defer(*AGENT_DEFER),
|
|
agent_id=agentid,
|
|
)
|
|
task = get_object_or_404(
|
|
AutomatedTask.objects.select_related("custom_field"), pk=pk
|
|
)
|
|
|
|
# get task result or create if doesn't exist
|
|
try:
|
|
task_result = (
|
|
TaskResult.objects.select_related("agent")
|
|
.defer("agent__services", "agent__wmi_detail")
|
|
.get(task=task, agent=agent)
|
|
)
|
|
serializer = TaskResultSerializer(
|
|
data=request.data, instance=task_result, partial=True
|
|
)
|
|
except TaskResult.DoesNotExist:
|
|
serializer = TaskResultSerializer(data=request.data, partial=True)
|
|
|
|
serializer.is_valid(raise_exception=True)
|
|
task_result = serializer.save(last_run=djangotime.now())
|
|
|
|
AgentHistory.objects.create(
|
|
agent=agent,
|
|
type=AuditActionType.TASK_RUN,
|
|
command=task.name,
|
|
script_results=request.data,
|
|
)
|
|
|
|
# check if task is a collector and update the custom field
|
|
if task.custom_field:
|
|
if not task_result.stderr:
|
|
task_result.save_collector_results()
|
|
|
|
status = CheckStatus.PASSING
|
|
else:
|
|
status = CheckStatus.FAILING
|
|
else:
|
|
status = (
|
|
CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING
|
|
)
|
|
|
|
if task_result:
|
|
task_result.status = status
|
|
task_result.save(update_fields=["status"])
|
|
else:
|
|
task_result.status = status
|
|
task.save(update_fields=["status"])
|
|
|
|
if status == CheckStatus.PASSING:
|
|
if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True):
|
|
Alert.handle_alert_resolve(task_result)
|
|
else:
|
|
Alert.handle_alert_failure(task_result)
|
|
|
|
return Response("ok")
|
|
|
|
|
|
class MeshExe(APIView):
|
|
"""Sends the mesh exe to the installer"""
|
|
|
|
def post(self, request):
|
|
match request.data:
|
|
case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}:
|
|
ident = MeshAgentIdent.WIN64
|
|
case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
|
|
ident = MeshAgentIdent.WIN32
|
|
case {"goarch": GoArch.AMD64, "plat": AgentPlat.DARWIN} | {
|
|
"goarch": GoArch.ARM64,
|
|
"plat": AgentPlat.DARWIN,
|
|
}:
|
|
ident = MeshAgentIdent.DARWIN_UNIVERSAL
|
|
case _:
|
|
return notify_error("Arch not supported")
|
|
|
|
core = get_core_settings()
|
|
|
|
try:
|
|
uri = get_mesh_ws_url()
|
|
mesh_device_id: str = asyncio.run(
|
|
get_mesh_device_id(uri, core.mesh_device_group)
|
|
)
|
|
except:
|
|
return notify_error("Unable to connect to mesh to get group id information")
|
|
|
|
dl_url = get_meshagent_url(
|
|
ident=ident,
|
|
plat=request.data["plat"],
|
|
mesh_site=core.mesh_site,
|
|
mesh_device_id=mesh_device_id,
|
|
)
|
|
|
|
try:
|
|
return download_mesh_agent(dl_url)
|
|
except:
|
|
return notify_error("Unable to download mesh agent exe")
|
|
|
|
|
|
class NewAgent(APIView):
|
|
def post(self, request):
|
|
from logs.models import AuditLog
|
|
|
|
""" Creates the agent """
|
|
|
|
if Agent.objects.filter(agent_id=request.data["agent_id"]).exists():
|
|
return notify_error(
|
|
"Agent already exists. Remove old agent first if trying to re-install"
|
|
)
|
|
|
|
agent = Agent(
|
|
agent_id=request.data["agent_id"],
|
|
hostname=request.data["hostname"],
|
|
site_id=int(request.data["site"]),
|
|
monitoring_type=request.data["monitoring_type"],
|
|
description=request.data["description"],
|
|
mesh_node_id=request.data["mesh_node_id"],
|
|
goarch=request.data["goarch"],
|
|
plat=request.data["plat"],
|
|
last_seen=djangotime.now(),
|
|
)
|
|
agent.save()
|
|
|
|
user = User.objects.create_user( # type: ignore
|
|
username=request.data["agent_id"],
|
|
agent=agent,
|
|
password=User.objects.make_random_password(60), # type: ignore
|
|
)
|
|
|
|
token = Token.objects.create(user=user)
|
|
|
|
if agent.monitoring_type == AgentMonType.WORKSTATION:
|
|
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
|
|
else:
|
|
WinUpdatePolicy(agent=agent).save()
|
|
|
|
reload_nats()
|
|
|
|
# create agent install audit record
|
|
AuditLog.objects.create(
|
|
username=request.user,
|
|
agent=agent.hostname,
|
|
object_type=AuditObjType.AGENT,
|
|
action=AuditActionType.AGENT_INSTALL,
|
|
message=f"{request.user} installed new agent {agent.hostname}",
|
|
after_value=Agent.serialize(agent),
|
|
debug_info={"ip": request._client_ip},
|
|
)
|
|
|
|
ret = {"pk": agent.pk, "token": token.key}
|
|
return Response(ret)
|
|
|
|
|
|
class Software(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
|
sw = request.data["software"]
|
|
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
|
InstalledSoftware(agent=agent, software=sw).save()
|
|
else:
|
|
s = agent.installedsoftware_set.first() # type: ignore
|
|
s.software = sw
|
|
s.save(update_fields=["software"])
|
|
|
|
return Response("ok")
|
|
|
|
|
|
class Installer(APIView):
|
|
def get(self, request):
|
|
# used to check if token is valid. will return 401 if not
|
|
return Response("ok")
|
|
|
|
def post(self, request):
|
|
if "version" not in request.data:
|
|
return notify_error("Invalid data")
|
|
|
|
ver = request.data["version"]
|
|
if (
|
|
pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER)
|
|
and "-dev" not in settings.LATEST_AGENT_VER
|
|
):
|
|
return notify_error(
|
|
f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
|
|
)
|
|
|
|
return Response("ok")
|
|
|
|
|
|
class ChocoResult(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def patch(self, request, pk):
|
|
action = get_object_or_404(PendingAction, pk=pk)
|
|
results: str = request.data["results"]
|
|
|
|
software_name = action.details["name"].lower()
|
|
success = [
|
|
"install",
|
|
"of",
|
|
software_name,
|
|
"was",
|
|
"successful",
|
|
"installed",
|
|
]
|
|
duplicate = [software_name, "already", "installed", "--force", "reinstall"]
|
|
installed = False
|
|
|
|
if all(x in results.lower() for x in success):
|
|
installed = True
|
|
elif all(x in results.lower() for x in duplicate):
|
|
installed = True
|
|
|
|
action.details["output"] = results
|
|
action.details["installed"] = installed
|
|
action.status = PAStatus.COMPLETED
|
|
action.save(update_fields=["details", "status"])
|
|
return Response("ok")
|
|
|
|
|
|
class AgentHistoryResult(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def patch(self, request, agentid, pk):
|
|
hist = get_object_or_404(
|
|
AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
|
|
)
|
|
s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
|
|
s.is_valid(raise_exception=True)
|
|
s.save()
|
|
return Response("ok")
|
|
|
|
|
|
class AgentConfig(APIView):
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, agentid):
|
|
ret = get_agent_config()
|
|
return Response(ret._to_dict())
|