Compare commits

..

42 Commits

Author SHA1 Message Date
wh1te909
221418120e Release 0.6.5 2021-04-27 16:20:25 +00:00
wh1te909
46f852e26e bump version 2021-04-27 16:20:08 +00:00
sadnub
4234cf0a31 fix policy task deletion 2021-04-27 12:12:04 -04:00
wh1te909
7f3daea648 Release 0.6.4 2021-04-27 15:36:49 +00:00
wh1te909
2eb16c82f4 bump version 2021-04-27 15:36:38 +00:00
sadnub
e00b2ce591 add test for check deletes 2021-04-27 11:04:06 -04:00
sadnub
d71e1311ca fix deleting checks 2021-04-27 10:58:23 -04:00
sadnub
2cf16963e3 fix custom fields on policy tasks 2021-04-27 10:51:29 -04:00
wh1te909
10bf7b7fb4 update restore docs 2021-04-27 06:18:15 +00:00
wh1te909
182c85a228 Release 0.6.3 2021-04-27 06:02:33 +00:00
wh1te909
94b1988b90 don't make description a required field in edit agent model 2021-04-27 06:00:42 +00:00
wh1te909
6f7e62e9a0 remove alpha status 2021-04-27 05:39:52 +00:00
wh1te909
aa7076af04 bump versions 2021-04-27 05:05:57 +00:00
Dan
c928e8f0d4 Merge pull request #436 from silversword411/develop
Updating management commands
2021-04-26 21:04:08 -07:00
sadnub
5c6b106f68 adding docs for Custom Fields, Scripting, Collector Tasks, and KeyStore 2021-04-26 23:16:10 -04:00
sadnub
d45bcea1ff add mkdocs container to docker dev env 2021-04-26 23:16:10 -04:00
wh1te909
6ff2dc79f8 black 2021-04-27 02:31:33 +00:00
silversword411
b752329987 Adding standardized header comments and example 2021-04-26 20:59:39 -04:00
silversword411
f21465335a clarifying vscode instructions 2021-04-26 20:47:23 -04:00
silversword411
0801adfc4b community script consolidating Defender status reports script 2021-04-26 17:45:56 -04:00
silversword411
5bee8052d5 Fix client site 2021-04-25 23:05:03 -04:00
silversword411
68dca5dfef Updating managment commands 2021-04-25 22:56:56 -04:00
Dan
3f51dd1d2f Merge pull request #435 from silversword411/develop
Docs and tips update
2021-04-25 00:20:05 -07:00
Dan
7f80889d77 Merge pull request #422 from sadnub/develop
Policy rework, global keystore, and collector tasks
2021-04-24 23:57:12 -07:00
sadnub
efc61c0222 fix tests 2021-04-24 22:13:02 -04:00
sadnub
6fc0a05d34 allow adding {{alert.property_name}} to resolved and failure alert scripts 2021-04-24 21:59:41 -04:00
sadnub
a9be872d7a make automated task tables sortable #431 2021-04-24 21:25:55 -04:00
sadnub
6ca85f099e fix autotask modals and allow editing the custom field for a collector task 2021-04-24 21:21:37 -04:00
sadnub
86ff677b8a fix styling 2021-04-24 21:06:17 -04:00
sadnub
35e295df86 implement keystore in script substitution with {{global.name}}. Also fixed issue with space in value. 2021-04-24 21:01:55 -04:00
sadnub
cd4d301790 keystore tests 2021-04-24 20:43:11 -04:00
sadnub
93bb329c3d add frontend end and backend for keystore 2021-04-24 20:36:21 -04:00
silversword411
7c1e0f2c30 More hidden dev docs 2021-04-24 20:06:30 -04:00
sadnub
b57f471f44 add ability to hide custom fields in UI if strictly for script usage 2021-04-24 20:01:28 -04:00
sadnub
252a9a2ed6 implement the rest of collector tasks and add tests 2021-04-24 17:40:44 -04:00
sadnub
7258d4d787 add block inheritance tests and fixes 2021-04-24 15:59:04 -04:00
sadnub
75522fa295 implement policy inheritance blocking 2021-04-24 10:07:37 -04:00
sadnub
4ba8f41d95 fixing tests 2021-04-24 10:07:37 -04:00
sadnub
f326f8e4de policy task and check rework. Added basic collector task implementation 2021-04-24 10:07:37 -04:00
sadnub
f863dc058e add UI for blocking policy inheritance on client, site, and agent 2021-04-24 10:07:37 -04:00
silversword411
20891db251 Tooltip on run interval 2021-04-24 08:52:55 -04:00
silversword411
f1d05f1342 Adding extra optional command line args to dialog 2021-04-23 16:08:16 -04:00
79 changed files with 2201 additions and 979 deletions

View File

@@ -8,7 +8,7 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
EXPOSE 8000 8383
EXPOSE 8000 8383 8005
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical

View File

@@ -227,6 +227,21 @@ services:
volumes:
- tactical-data-dev:/opt/tactical
mkdocs-dev:
container_name: trmm-mkdocs-dev
image: api-dev
restart: always
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-mkdocs-dev"]
ports:
- "8005:8005"
volumes:
- ..:/workspace:cached
networks:
- dev
volumes:
tactical-data-dev:
postgres-data-dev:

View File

@@ -170,3 +170,8 @@ if [ "$1" = 'tactical-websockets-dev' ]; then
check_tactical_ready
"${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
fi
if [ "$1" = 'tactical-mkdocs-dev' ]; then
cd "${WORKSPACE_DIR}/docs"
"${VIRTUAL_ENV}"/bin/mkdocs serve
fi

View File

@@ -11,8 +11,6 @@ It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and i
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
### [Discord Chat](https://discord.gg/upGTkWp)
### [Documentation](https://wh1te909.github.io/tacticalrmm/)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-17 01:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0035_auto_20210329_1709'),
]
operations = [
migrations.AddField(
model_name='agent',
name='block_policy_inheritance',
field=models.BooleanField(default=False),
),
]

View File

@@ -63,6 +63,7 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
block_policy_inheritance = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
@@ -96,9 +97,9 @@ class Agent(BaseAuditModel):
# or if site has changed on agent and if so generate-policies
if (
not old_agent
or old_agent
and old_agent.policy != self.policy
or old_agent.site != self.site
or (old_agent and old_agent.policy != self.policy)
or (old_agent.site != self.site)
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
@@ -421,21 +422,34 @@ class Agent(BaseAuditModel):
# check site policy if agent policy doesn't have one
elif site.server_policy and site.server_policy.winupdatepolicy.exists():
patch_policy = site.server_policy.winupdatepolicy.get()
# make sure agent isn;t blocking policy inheritance
if not self.block_policy_inheritance:
patch_policy = site.server_policy.winupdatepolicy.get()
# if site doesn't have a patch policy check the client
elif (
site.client.server_policy
and site.client.server_policy.winupdatepolicy.exists()
):
patch_policy = site.client.server_policy.winupdatepolicy.get()
# make sure agent and site are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
):
patch_policy = site.client.server_policy.winupdatepolicy.get()
# if patch policy still doesn't exist check default policy
elif (
core_settings.server_policy
and core_settings.server_policy.winupdatepolicy.exists()
):
patch_policy = core_settings.server_policy.winupdatepolicy.get()
# make sure agent site and client are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
and not site.client.block_policy_inheritance
):
patch_policy = core_settings.server_policy.winupdatepolicy.get()
elif self.monitoring_type == "workstation":
# check agent policy first which should override client or site policy
@@ -446,21 +460,36 @@ class Agent(BaseAuditModel):
site.workstation_policy
and site.workstation_policy.winupdatepolicy.exists()
):
patch_policy = site.workstation_policy.winupdatepolicy.get()
# make sure agent isn;t blocking policy inheritance
if not self.block_policy_inheritance:
patch_policy = site.workstation_policy.winupdatepolicy.get()
# if site doesn't have a patch policy check the client
elif (
site.client.workstation_policy
and site.client.workstation_policy.winupdatepolicy.exists()
):
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
# make sure agent and site are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
):
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
# if patch policy still doesn't exist check default policy
elif (
core_settings.workstation_policy
and core_settings.workstation_policy.winupdatepolicy.exists()
):
patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
# make sure agent site and client are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
and not site.client.block_policy_inheritance
):
patch_policy = (
core_settings.workstation_policy.winupdatepolicy.get()
)
# if policy still doesn't exist return the agent patch policy
if not patch_policy:
@@ -527,6 +556,7 @@ class Agent(BaseAuditModel):
and site.server_policy
and site.server_policy.alert_template
and site.server_policy.alert_template.is_active
and not self.block_policy_inheritance
):
templates.append(site.server_policy.alert_template)
if (
@@ -534,6 +564,7 @@ class Agent(BaseAuditModel):
and site.workstation_policy
and site.workstation_policy.alert_template
and site.workstation_policy.alert_template.is_active
and not self.block_policy_inheritance
):
templates.append(site.workstation_policy.alert_template)
@@ -547,6 +578,8 @@ class Agent(BaseAuditModel):
and client.server_policy
and client.server_policy.alert_template
and client.server_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
):
templates.append(client.server_policy.alert_template)
if (
@@ -554,15 +587,28 @@ class Agent(BaseAuditModel):
and client.workstation_policy
and client.workstation_policy.alert_template
and client.workstation_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
):
templates.append(client.workstation_policy.alert_template)
# check if alert template is on client and return
if client.alert_template and client.alert_template.is_active:
if (
client.alert_template
and client.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
):
templates.append(client.alert_template)
# check if alert template is applied globally and return
if core.alert_template and core.alert_template.is_active:
if (
core.alert_template
and core.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.alert_template)
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
@@ -571,6 +617,9 @@ class Agent(BaseAuditModel):
and core.server_policy
and core.server_policy.alert_template
and core.server_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.server_policy.alert_template)
if (
@@ -578,6 +627,9 @@ class Agent(BaseAuditModel):
and core.workstation_policy
and core.workstation_policy.alert_template
and core.workstation_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.workstation_policy.alert_template)
@@ -731,36 +783,6 @@ class Agent(BaseAuditModel):
except:
pass
# define how the agent should handle pending actions
def handle_pending_actions(self):
pending_actions = self.pendingactions.filter(status="pending") # type: ignore
for action in pending_actions:
if action.action_type == "taskaction":
from autotasks.tasks import (
create_win_task_schedule,
delete_win_task_schedule,
enable_or_disable_win_task,
)
task_id = action.details["task_id"]
if action.details["action"] == "taskcreate":
create_win_task_schedule.delay(task_id, pending_action=action.id)
elif action.details["action"] == "tasktoggle":
enable_or_disable_win_task.delay(
task_id, action.details["value"], pending_action=action.id
)
elif action.details["action"] == "taskdelete":
delete_win_task_schedule.delay(task_id, pending_action=action.id)
# for clearing duplicate pending actions on agent
def remove_matching_pending_task_actions(self, task_id):
# remove any other pending actions on agent with same task_id
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
if action.details["task_id"] == task_id:
action.delete()
def should_create_alert(self, alert_template=None):
return (
self.overdue_dashboard_alert

View File

@@ -116,6 +116,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
"logged_username",
"italic",
"policy",
"block_policy_inheritance",
]
depth = 2

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Union
from django.conf import settings
@@ -297,7 +298,7 @@ class Alert(models.Model):
if alert_template and alert_template.action and not alert.action_run:
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
wait=True,
full=True,
@@ -406,7 +407,7 @@ class Alert(models.Model):
):
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
@@ -428,6 +429,36 @@ class Alert(models.Model):
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
)
def parse_script_args(self, args: list[str]):
if not args:
return []
temp_args = list()
# pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
for arg in args:
match = pattern.match(arg)
if match:
name = match.group(1)
if hasattr(self, name):
value = getattr(self, name)
else:
continue
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)) # type: ignore
except Exception as e:
logger.error(e)
continue
else:
temp_args.append(arg)
return temp_args
class AlertTemplate(models.Model):
name = models.CharField(max_length=100)

View File

@@ -5,6 +5,7 @@ from unittest.mock import patch
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker
from autotasks.models import AutomatedTask
from tacticalrmm.test import TacticalTestCase
@@ -203,3 +204,138 @@ class TestAPIv3(TacticalTestCase):
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
reload_nats.assert_called_once()
def test_task_runner_get(self):
from autotasks.serializers import TaskGOGetSerializer
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
self.assertEqual(r.status_code, 404)
# setup data
agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskGOGetSerializer(task).data, r.data) # type: ignore
def test_task_runner_results(self):
from agents.models import AgentCustomField
r = self.client.patch("/api/v3/500/asdf9df9dfdf/taskrunner/")
self.assertEqual(r.status_code, 404)
# setup data
agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
# test passing task
data = {
"stdout": "test test \ntestest stdgsd\n",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing") # type: ignore
# test failing task
data = {
"stdout": "test test \ntestest stdgsd\n",
"stderr": "",
"retcode": 1,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
# test collector task
text = baker.make("core.CustomField", model="agent", type="text", name="Test")
boolean = baker.make(
"core.CustomField", model="agent", type="checkbox", name="Test1"
)
multiple = baker.make(
"core.CustomField", model="agent", type="multiple", name="Test2"
)
# test text fields
task.custom_field = text # type: ignore
task.save() # type: ignore
# test failing failing with stderr
data = {
"stdout": "test test \nthe last line",
"stderr": "This is an error",
"retcode": 1,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
# test saving to text field
data = {
"stdout": "test test \nthe last line",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line") # type: ignore
# test saving to checkbox field
task.custom_field = boolean # type: ignore
task.save() # type: ignore
data = {
"stdout": "1",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value) # type: ignore
# test saving to multiple field with commas
task.custom_field = multiple # type: ignore
task.save() # type: ignore
data = {
"stdout": "this,is,an,array",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"]) # type: ignore
# test mutiple with a single value
data = {
"stdout": "this",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"]) # type: ignore

View File

@@ -15,7 +15,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from accounts.models import User
from agents.models import Agent
from agents.models import Agent, AgentCustomField
from agents.serializers import WinAgentSerializer
from autotasks.models import AutomatedTask
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
@@ -65,9 +65,17 @@ class CheckIn(APIView):
if Alert.objects.filter(agent=agent, resolved=False).exists():
Alert.handle_alert_resolve(agent)
# get any pending actions
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
agent.handle_pending_actions()
# sync scheduled tasks
if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore
tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore
for task in tasks:
if task.sync_status == "pendingdeletion":
task.delete_task_on_agent()
elif task.sync_status == "initial":
task.modify_task_on_agent()
elif task.sync_status == "notsynced":
task.create_task_on_agent()
return Response("ok")
@@ -351,11 +359,42 @@ class TaskRunner(APIView):
instance=task, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save(last_run=djangotime.now())
new_task = serializer.save(last_run=djangotime.now())
status = "failing" if task.retcode != 0 else "passing"
# check if task is a collector and update the custom field
if task.custom_field:
if not task.stderr:
if AgentCustomField.objects.filter(
field=task.custom_field, agent=task.agent
).exists():
agent_field = AgentCustomField.objects.get(
field=task.custom_field, agent=task.agent
)
else:
agent_field = AgentCustomField.objects.create(
field=task.custom_field, agent=task.agent
)
# get last line of stdout
value = new_task.stdout.split("\n")[-1].strip()
if task.custom_field.type in ["text", "number", "single", "datetime"]:
agent_field.string_value = value
agent_field.save()
elif task.custom_field.type == "multiple":
agent_field.multiple_value = value.split(",")
agent_field.save()
elif task.custom_field.type == "checkbox":
agent_field.bool_value = bool(value)
agent_field.save()
status = "passing"
else:
status = "failing"
else:
status = "failing" if task.retcode != 0 else "passing"
new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
new_task.status = status
new_task.save()
@@ -393,7 +432,7 @@ class SysInfo(APIView):
class MeshExe(APIView):
""" Sends the mesh exe to the installer """
"""Sends the mesh exe to the installer"""
def post(self, request):
exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"

View File

@@ -1,7 +1,6 @@
from django.db import models
from agents.models import Agent
from core.models import CoreSettings
from django.db import models
from logs.models import BaseAuditModel
@@ -29,7 +28,8 @@ class Policy(BaseAuditModel):
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_from_policies_task
from automation.tasks import generate_agent_checks_task
# get old policy if exists
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
@@ -38,8 +38,8 @@ class Policy(BaseAuditModel):
# generate agent checks only if active and enforced were changed
if old_policy:
if old_policy.active != self.active or old_policy.enforced != self.enforced:
generate_agent_checks_from_policies_task.delay(
policypk=self.pk,
generate_agent_checks_task.delay(
policy=self.pk,
create_tasks=True,
)
@@ -52,7 +52,10 @@ class Policy(BaseAuditModel):
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
super(BaseAuditModel, self).delete(*args, **kwargs)
generate_agent_checks_task.delay(agents, create_tasks=True)
generate_agent_checks_task.delay(agents=agents, create_tasks=True)
def __str__(self):
return self.name
@property
def is_default_server_policy(self):
@@ -62,9 +65,6 @@ class Policy(BaseAuditModel):
def is_default_workstation_policy(self):
return self.default_workstation_policy.exists() # type: ignore
def __str__(self):
return self.name
def is_agent_excluded(self, agent):
return (
agent in self.excluded_agents.all()
@@ -94,20 +94,29 @@ class Policy(BaseAuditModel):
filtered_agents_pks = Policy.objects.none()
filtered_agents_pks |= Agent.objects.filter(
site__in=[
site
for site in explicit_sites
if site.client not in explicit_clients
and site.client not in self.excluded_clients.all()
],
monitoring_type=mon_type,
).values_list("pk", flat=True)
filtered_agents_pks |= (
Agent.objects.exclude(block_policy_inheritance=True)
.filter(
site__in=[
site
for site in explicit_sites
if site.client not in explicit_clients
and site.client not in self.excluded_clients.all()
],
monitoring_type=mon_type,
)
.values_list("pk", flat=True)
)
filtered_agents_pks |= Agent.objects.filter(
site__client__in=[client for client in explicit_clients],
monitoring_type=mon_type,
).values_list("pk", flat=True)
filtered_agents_pks |= (
Agent.objects.exclude(block_policy_inheritance=True)
.exclude(site__block_policy_inheritance=True)
.filter(
site__client__in=[client for client in explicit_clients],
monitoring_type=mon_type,
)
.values_list("pk", flat=True)
)
return Agent.objects.filter(
models.Q(pk__in=filtered_agents_pks)
@@ -123,9 +132,6 @@ class Policy(BaseAuditModel):
@staticmethod
def cascade_policy_tasks(agent):
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
from logs.models import PendingAction
# List of all tasks to be applied
tasks = list()
@@ -154,6 +160,17 @@ class Policy(BaseAuditModel):
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
if (
agent_policy
and agent_policy.active
@@ -200,26 +217,16 @@ class Policy(BaseAuditModel):
if taskpk not in added_task_pks
]
):
delete_win_task_schedule.delay(task.pk)
if task.sync_status == "initial":
task.delete()
else:
task.sync_status = "pendingdeletion"
task.save()
# handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
for action in agent.pendingactions.filter(action_type="taskaction").exclude(
status="completed"
):
task = AutomatedTask.objects.get(pk=action.details["task_id"])
if (
task.parent_task in agent_tasks_parent_pks
and task.parent_task in added_task_pks
):
agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=agent,
action_type="taskaction",
details={"action": "taskcreate", "task_id": task.id},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
# change tasks from pendingdeletion to notsynced if policy was added or changed
agent.autotasks.filter(sync_status="pendingdeletion").filter(
parent_task__in=[taskpk for taskpk in added_task_pks]
).update(sync_status="notsynced")
return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
@@ -251,6 +258,17 @@ class Policy(BaseAuditModel):
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
# Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first
enforced_checks = list()

View File

@@ -1,169 +1,143 @@
from agents.models import Agent
from automation.models import Policy
from autotasks.models import AutomatedTask
from checks.models import Check
from typing import Any, Dict, List, Union
from tacticalrmm.celery import app
@app.task
# generates policy checks on agents affected by a policy and optionally generate automated tasks
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
def generate_agent_checks_task(
policy: int = None,
site: int = None,
client: int = None,
agents: List[int] = list(),
all: bool = False,
create_tasks: bool = False,
) -> Union[str, None]:
from agents.models import Agent
policy = Policy.objects.get(pk=policypk)
from automation.models import Policy
if policy.is_default_server_policy and policy.is_default_workstation_policy:
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation").only(
p = Policy.objects.get(pk=policy) if policy else None
# generate checks on all agents if all is specified or if policy is default server/workstation policy
if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all:
a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
# generate checks on all servers if policy is a default servers policy
elif p and p.is_default_server_policy:
a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type")
# generate checks on all workstations if policy is a default workstations policy
elif p and p.is_default_workstation_policy:
a = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
# generate checks on a list of supplied agents
elif agents:
a = Agent.objects.filter(pk__in=agents)
# generate checks on agents affected by supplied policy
elif policy:
a = p.related_agents().only("pk")
# generate checks that has specified site
elif site:
a = Agent.objects.filter(site_id=site)
# generate checks that has specified client
elif client:
a = Agent.objects.filter(site__client_id=client)
else:
agents = policy.related_agents().only("pk")
a = []
for agent in agents:
for agent in a:
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# generates policy checks on a list of agents and optionally generate automated tasks
def generate_agent_checks_task(agentpks, create_tasks=False):
for agent in Agent.objects.filter(pk__in=agentpks):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# generates policy checks on agent servers or workstations within a certain client or site and optionally generate automated tasks
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# generates policy checks on all agent servers or workstations and optionally generate automated tasks
def generate_all_agent_checks_task(mon_type, create_tasks=False):
for agent in Agent.objects.filter(monitoring_type=mon_type):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# deletes a policy managed check from all agents
def delete_policy_check_task(checkpk):
Check.objects.filter(parent_check=checkpk).delete()
return "ok"
@app.task
# updates policy managed check fields on agents
def update_policy_check_fields_task(checkpk):
def update_policy_check_fields_task(check: int) -> str:
from checks.models import Check
check = Check.objects.get(pk=checkpk)
c: Check = Check.objects.get(pk=check)
update_fields: Dict[Any, Any] = {}
Check.objects.filter(parent_check=checkpk).update(
warning_threshold=check.warning_threshold,
error_threshold=check.error_threshold,
alert_severity=check.alert_severity,
name=check.name,
run_interval=check.run_interval,
disk=check.disk,
fails_b4_alert=check.fails_b4_alert,
ip=check.ip,
script=check.script,
script_args=check.script_args,
info_return_codes=check.info_return_codes,
warning_return_codes=check.warning_return_codes,
timeout=check.timeout,
pass_if_start_pending=check.pass_if_start_pending,
pass_if_svc_not_exist=check.pass_if_svc_not_exist,
restart_if_stopped=check.restart_if_stopped,
log_name=check.log_name,
event_id=check.event_id,
event_id_is_wildcard=check.event_id_is_wildcard,
event_type=check.event_type,
event_source=check.event_source,
event_message=check.event_message,
fail_when=check.fail_when,
search_last_days=check.search_last_days,
number_of_events_b4_alert=check.number_of_events_b4_alert,
email_alert=check.email_alert,
text_alert=check.text_alert,
dashboard_alert=check.dashboard_alert,
)
for field in c.policy_fields_to_copy:
update_fields[field] = getattr(c, field)
Check.objects.filter(parent_check=check).update(**update_fields)
return "ok"
@app.task
# generates policy tasks on agents affected by a policy
def generate_agent_tasks_from_policies_task(policypk):
def generate_agent_autotasks_task(policy: int = None) -> str:
from agents.models import Agent
policy = Policy.objects.get(pk=policypk)
from automation.models import Policy
if policy.is_default_server_policy and policy.is_default_workstation_policy:
p: Policy = Policy.objects.get(pk=policy)
if p and p.is_default_server_policy and p.is_default_workstation_policy:
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy:
elif p and p.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy:
elif p and p.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else:
agents = policy.related_agents().only("pk")
agents = p.related_agents().only("pk")
for agent in agents:
agent.generate_tasks_from_policies()
return "ok"
@app.task
def delete_policy_autotask_task(taskpk):
def delete_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
for task in AutomatedTask.objects.filter(parent_task=taskpk):
delete_win_task_schedule.delay(task.pk)
for t in AutomatedTask.objects.filter(parent_task=task):
t.delete_task_on_agent()
return "ok"
@app.task
def run_win_policy_autotask_task(task_pks):
from autotasks.tasks import run_win_task
def run_win_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask
for task in task_pks:
run_win_task.delay(task)
for t in AutomatedTask.objects.filter(parent_task=task):
t.run_win_task()
return "ok"
@app.task
def update_policy_task_fields_task(taskpk, update_agent=False):
from autotasks.tasks import enable_or_disable_win_task
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
from autotasks.models import AutomatedTask
task = AutomatedTask.objects.get(pk=taskpk)
t = AutomatedTask.objects.get(pk=task)
update_fields: Dict[str, Any] = {}
AutomatedTask.objects.filter(parent_task=taskpk).update(
alert_severity=task.alert_severity,
email_alert=task.email_alert,
text_alert=task.text_alert,
dashboard_alert=task.dashboard_alert,
script=task.script,
script_args=task.script_args,
name=task.name,
timeout=task.timeout,
enabled=task.enabled,
)
for field in t.policy_fields_to_copy:
update_fields[field] = getattr(t, field)
AutomatedTask.objects.filter(parent_task=task).update(**update_fields)
if update_agent:
for task in AutomatedTask.objects.filter(parent_task=taskpk):
enable_or_disable_win_task.delay(task.pk, task.enabled)
for t in AutomatedTask.objects.filter(parent_task=task).exclude(
sync_status="initial"
):
t.modify_task_on_agent()
return "ok"

View File

@@ -52,7 +52,8 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_add_policy(self):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_add_policy(self, create_task):
url = "/automation/policies/"
data = {
@@ -90,8 +91,8 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("post", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_update_policy(self, generate_agent_checks_from_policies_task):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_update_policy(self, generate_agent_checks_task):
# returns 404 for invalid policy pk
resp = self.client.put("/automation/policies/500/", format="json")
self.assertEqual(resp.status_code, 404)
@@ -110,7 +111,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
# only called if active or enforced are updated
generate_agent_checks_from_policies_task.assert_not_called()
generate_agent_checks_task.assert_not_called()
data = {
"name": "Test Policy Update",
@@ -121,8 +122,8 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_checks_from_policies_task.assert_called_with(
policypk=policy.pk, create_tasks=True # type: ignore
generate_agent_checks_task.assert_called_with(
policy=policy.pk, create_tasks=True # type: ignore
)
self.check_not_authenticated("put", url)
@@ -145,7 +146,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
generate_agent_checks_task.assert_called_with(
[agent.pk for agent in agents], create_tasks=True
agents=[agent.pk for agent in agents], create_tasks=True
)
self.check_not_authenticated("delete", url)
@@ -271,7 +272,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
@patch("automation.tasks.run_win_policy_autotask_task.delay")
@patch("automation.tasks.run_win_policy_autotasks_task.delay")
def test_run_win_task(self, mock_task):
# create managed policy tasks
@@ -281,11 +282,12 @@ class TestPolicyViews(TacticalTestCase):
parent_task=1,
_quantity=6,
)
url = "/automation/runwintask/1/"
resp = self.client.put(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_task.assert_called_once_with([task.pk for task in tasks]) # type: ignore
mock_task.assert_called() # type: ignore
self.check_not_authenticated("put", url)
@@ -426,7 +428,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_sync_policy(self, generate_checks):
url = "/automation/sync/"
@@ -441,7 +443,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_checks.assert_called_with(policy.pk, create_tasks=True) # type: ignore
generate_checks.assert_called_with(policy=policy.pk, create_tasks=True) # type: ignore
self.check_not_authenticated("post", url)
@@ -497,7 +499,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
def test_generating_agent_policy_checks(self):
from .tasks import generate_agent_checks_from_policies_task
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -505,7 +507,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", policy=policy)
# test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id) # type: ignore
generate_agent_checks_task(policy=policy.id) # type: ignore
# make sure all checks were created. should be 7
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
@@ -545,7 +547,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(check.event_type, checks[6].event_type)
def test_generating_agent_policy_checks_with_enforced(self):
from .tasks import generate_agent_checks_from_policies_task
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True, enforced=True)
@@ -555,7 +557,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
self.create_checks(agent=agent, script=script)
generate_agent_checks_from_policies_task(policy.id, create_tasks=True) # type: ignore
generate_agent_checks_task(policy=policy.id, create_tasks=True) # type: ignore
# make sure each agent check says overriden_by_policy
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
@@ -566,13 +568,12 @@ class TestPolicyTasks(TacticalTestCase):
7,
)
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_generating_agent_policy_checks_by_location(
self, generate_agent_checks_by_location_task
self, generate_agent_checks_mock, create_task
):
from automation.tasks import (
generate_agent_checks_by_location_task as generate_agent_checks,
)
from automation.tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -596,16 +597,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
client=workstation_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_task(
client=workstation_agent.client.pk,
create_tasks=True,
)
@@ -620,16 +619,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
client=workstation_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_task(
client=workstation_agent.client.pk,
create_tasks=True,
)
@@ -644,16 +641,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
client=server_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_task(
client=server_agent.client.pk,
create_tasks=True,
)
@@ -668,16 +663,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
client=server_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_task(
client=server_agent.client.pk,
create_tasks=True,
)
@@ -692,16 +685,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
site=workstation_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_task(
site=workstation_agent.site.pk,
create_tasks=True,
)
@@ -716,16 +707,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
site=workstation_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_task(
site=workstation_agent.site.pk,
create_tasks=True,
)
@@ -740,16 +729,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
site=server_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_task(
site=server_agent.site.pk,
create_tasks=True,
)
@@ -764,16 +751,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
site=server_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_task(
site=server_agent.site.pk,
create_tasks=True,
)
@@ -783,13 +768,10 @@ class TestPolicyTasks(TacticalTestCase):
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_generating_policy_checks_for_all_agents(
self, generate_all_agent_checks_task
):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
from core.models import CoreSettings
from .tasks import generate_all_agent_checks_task as generate_all_checks
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -801,11 +783,9 @@ class TestPolicyTasks(TacticalTestCase):
core.server_policy = policy
core.save()
generate_all_agent_checks_task.assert_called_with(
mon_type="server", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="server", create_tasks=True)
generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
generate_agent_checks_mock.reset_mock()
generate_agent_checks_task(all=True, create_tasks=True)
# all servers should have 7 checks
for agent in server_agents:
@@ -818,15 +798,9 @@ class TestPolicyTasks(TacticalTestCase):
core.workstation_policy = policy
core.save()
generate_all_agent_checks_task.assert_any_call(
mon_type="workstation", create_tasks=True
)
generate_all_agent_checks_task.assert_any_call(
mon_type="server", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="server", create_tasks=True)
generate_all_checks(mon_type="workstation", create_tasks=True)
generate_agent_checks_mock.assert_any_call(all=True, create_tasks=True)
generate_agent_checks_mock.reset_mock()
generate_agent_checks_task(all=True, create_tasks=True)
# all workstations should have 7 checks
for agent in server_agents:
@@ -838,11 +812,9 @@ class TestPolicyTasks(TacticalTestCase):
core.workstation_policy = None
core.save()
generate_all_agent_checks_task.assert_called_with(
mon_type="workstation", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="workstation", create_tasks=True)
generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
generate_agent_checks_mock.reset_mock()
generate_agent_checks_task(all=True, create_tasks=True)
# nothing should have the checks
for agent in server_agents:
@@ -851,31 +823,8 @@ class TestPolicyTasks(TacticalTestCase):
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
def test_delete_policy_check(self):
from .models import Policy
from .tasks import delete_policy_check_task
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
agent = baker.make_recipe("agents.server_agent", policy=policy)
# make sure agent has 7 checks
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
# pick a policy check and delete it from the agent
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id # type: ignore
delete_policy_check_task(policy_check_id)
# make sure policy check doesn't exist on agent
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 6)
self.assertFalse(
Agent.objects.get(pk=agent.id)
.agentchecks.filter(parent_check=policy_check_id)
.exists()
)
def update_policy_check_fields(self):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def update_policy_check_fields(self, create_task):
from .models import Policy
from .tasks import update_policy_check_fields_task
@@ -905,8 +854,9 @@ class TestPolicyTasks(TacticalTestCase):
"12.12.12.12",
)
def test_generate_agent_tasks(self):
from .tasks import generate_agent_tasks_from_policies_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_generate_agent_tasks(self, create_task):
from .tasks import generate_agent_autotasks_task
# create test data
policy = baker.make("automation.Policy", active=True)
@@ -915,7 +865,7 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_tasks_from_policies_task(policy.id) # type: ignore
generate_agent_autotasks_task(policy=policy.id) # type: ignore
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
@@ -934,56 +884,61 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(task.parent_task, tasks[2].id) # type: ignore
self.assertEqual(task.name, tasks[2].name) # type: ignore
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_delete_policy_tasks(self, delete_win_task_schedule):
from .tasks import delete_policy_autotask_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.delete_task_on_agent")
def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
from .tasks import delete_policy_autotasks_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
agent = baker.make_recipe("agents.server_agent", policy=policy)
baker.make_recipe("agents.server_agent", policy=policy)
delete_policy_autotask_task(tasks[0].id) # type: ignore
delete_policy_autotasks_task(task=tasks[0].id) # type: ignore
delete_win_task_schedule.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id # type: ignore
)
delete_task_on_agent.assert_called()
@patch("autotasks.tasks.run_win_task.delay")
def test_run_policy_task(self, run_win_task):
from .tasks import run_win_policy_autotask_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.run_win_task")
def test_run_policy_task(self, run_win_task, create_task):
from .tasks import run_win_policy_autotasks_task
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
baker.make_recipe("agents.server_agent", policy=policy)
run_win_policy_autotask_task([task.id for task in tasks]) # type: ignore
run_win_policy_autotasks_task(task=tasks[0].id) # type: ignore
run_win_task.side_effect = [task.id for task in tasks] # type: ignore
self.assertEqual(run_win_task.call_count, 3)
for task in tasks: # type: ignore
run_win_task.assert_any_call(task.id) # type: ignore
run_win_task.assert_called_once()
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
def test_update_policy_tasks(self, enable_or_disable_win_task):
from .tasks import update_policy_task_fields_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.modify_task_on_agent")
def test_update_policy_tasks(self, modify_task_on_agent, create_task):
from .tasks import update_policy_autotasks_fields_task
# setup data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
"autotasks.AutomatedTask",
enabled=True,
policy=policy,
_quantity=3,
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
tasks[0].enabled = False # type: ignore
tasks[0].save() # type: ignore
update_policy_task_fields_task(tasks[0].id) # type: ignore
enable_or_disable_win_task.assert_not_called()
update_policy_autotasks_fields_task(task=tasks[0].id) # type: ignore
modify_task_on_agent.assert_not_called()
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore
update_policy_task_fields_task(tasks[0].id, update_agent=True) # type: ignore
enable_or_disable_win_task.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id, False # type: ignore
)
update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore
modify_task_on_agent.assert_not_called()
agent.autotasks.update(sync_status="synced")
update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore
modify_task_on_agent.assert_called_once()
@patch("agents.models.Agent.generate_tasks_from_policies")
@patch("agents.models.Agent.generate_checks_from_policies")
@@ -996,17 +951,19 @@ class TestPolicyTasks(TacticalTestCase):
generate_checks.reset_mock()
generate_tasks.reset_mock()
generate_agent_checks_task([agent.pk for agent in agents])
generate_agent_checks_task(agents=[agent.pk for agent in agents])
self.assertEquals(generate_checks.call_count, 5)
generate_tasks.assert_not_called()
generate_checks.reset_mock()
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
generate_agent_checks_task(
agents=[agent.pk for agent in agents], create_tasks=True
)
self.assertEquals(generate_checks.call_count, 5)
self.assertEquals(generate_checks.call_count, 5)
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_policy_exclusions(self, delete_task):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_policy_exclusions(self, create_task):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
@@ -1028,8 +985,6 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks
agent.autotasks.all().delete()
@@ -1051,8 +1006,6 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks and reset
agent.autotasks.all().delete()
@@ -1074,8 +1027,6 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks and reset
agent.autotasks.all().delete()
@@ -1103,11 +1054,82 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
def test_removing_duplicate_pending_task_actions(self):
pass
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_policy_inheritance_blocking(self, create_task):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
baker.make("autotasks.AutomatedTask", policy=policy)
agent = baker.make_recipe("agents.agent", monitoring_type="server")
def test_creating_checks_with_assigned_tasks(self):
pass
core = CoreSettings.objects.first()
core.server_policy = policy
core.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from default policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
# test client blocking inheritance
agent.site.client.block_policy_inheritance = True
agent.site.client.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertFalse(agent.autotasks.all())
self.assertFalse(agent.agentchecks.all())
agent.site.client.server_policy = policy
agent.site.client.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from client policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
# test site blocking inheritance
agent.site.block_policy_inheritance = True
agent.site.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertFalse(agent.autotasks.all())
self.assertFalse(agent.agentchecks.all())
agent.site.server_policy = policy
agent.site.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from site policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
# test agent blocking inheritance
agent.block_policy_inheritance = True
agent.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertFalse(agent.autotasks.all())
self.assertFalse(agent.agentchecks.all())
agent.policy = policy
agent.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from agent policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())

View File

@@ -1,13 +1,12 @@
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from agents.serializers import AgentHostnameSerializer
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
@@ -22,7 +21,6 @@ from .serializers import (
PolicyTableSerializer,
PolicyTaskStatusSerializer,
)
from .tasks import run_win_policy_autotask_task
class GetAddPolicies(APIView):
@@ -76,10 +74,10 @@ class GetUpdateDeletePolicy(APIView):
class PolicySync(APIView):
def post(self, request):
if "policy" in request.data.keys():
from automation.tasks import generate_agent_checks_from_policies_task
from automation.tasks import generate_agent_checks_task
generate_agent_checks_from_policies_task.delay(
request.data["policy"], create_tasks=True
generate_agent_checks_task.delay(
policy=request.data["policy"], create_tasks=True
)
return Response("ok")
@@ -101,8 +99,9 @@ class PolicyAutoTask(APIView):
# bulk run win tasks associated with policy
def put(self, request, task):
tasks = AutomatedTask.objects.filter(parent_task=task)
run_win_policy_autotask_task.delay([task.id for task in tasks])
from .tasks import run_win_policy_autotasks_task
run_win_policy_autotasks_task.delay(task=task)
return Response("Affected agent tasks will run shortly")

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.7 on 2021-04-04 00:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0019_globalkvstore'),
('scripts', '0007_script_args'),
('autotasks', '0018_automatedtask_run_asap_after_missed'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='custom_field',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotask', to='core.customfield'),
),
migrations.AddField(
model_name='automatedtask',
name='retvalue',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='automatedtask',
name='script',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autoscript', to='scripts.script'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-21 02:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0019_auto_20210404_0032'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='sync_status',
field=models.CharField(choices=[('synced', 'Synced With Agent'), ('notsynced', 'Waiting On Agent Checkin'), ('pendingdeletion', 'Pending Deletion on Agent'), ('initial', 'Initial Task Sync')], default='initial', max_length=100),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-04-27 14:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0021_customfield_hide_in_ui'),
('autotasks', '0020_auto_20210421_0226'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='custom_field',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotasks', to='core.customfield'),
),
]

View File

@@ -1,16 +1,19 @@
import asyncio
import datetime as dt
import random
import string
from typing import List
import pytz
from alerts.models import SEVERITY_CHOICES
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import DateTimeField
from loguru import logger
from alerts.models import SEVERITY_CHOICES
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel
from loguru import logger
from packaging import version as pyver
from tacticalrmm.utils import bitdays_to_string
logger.configure(**settings.LOG_CONFIG)
@@ -36,6 +39,7 @@ SYNC_STATUS_CHOICES = [
("synced", "Synced With Agent"),
("notsynced", "Waiting On Agent Checkin"),
("pendingdeletion", "Pending Deletion on Agent"),
("initial", "Initial Task Sync"),
]
TASK_STATUS_CHOICES = [
@@ -60,12 +64,19 @@ class AutomatedTask(BaseAuditModel):
blank=True,
on_delete=models.CASCADE,
)
custom_field = models.ForeignKey(
"core.CustomField",
related_name="autotasks",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
script = models.ForeignKey(
"scripts.Script",
null=True,
blank=True,
related_name="autoscript",
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
)
script_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
@@ -100,6 +111,7 @@ class AutomatedTask(BaseAuditModel):
parent_task = models.PositiveIntegerField(null=True, blank=True)
win_task_name = models.CharField(max_length=255, null=True, blank=True)
timeout = models.PositiveIntegerField(default=120)
retvalue = models.TextField(null=True, blank=True)
retcode = models.IntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
@@ -110,7 +122,7 @@ class AutomatedTask(BaseAuditModel):
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
)
sync_status = models.CharField(
max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
max_length=100, choices=SYNC_STATUS_CHOICES, default="initial"
)
alert_severity = models.CharField(
max_length=30, choices=SEVERITY_CHOICES, default="info"
@@ -147,6 +159,31 @@ class AutomatedTask(BaseAuditModel):
return self.last_run
# These fields will be duplicated on the agent tasks that are managed by a policy
@property
def policy_fields_to_copy(self) -> List[str]:
return [
"alert_severity",
"email_alert",
"text_alert",
"dashboard_alert",
"script",
"script_args",
"assigned_check",
"name",
"run_time_days",
"run_time_minute",
"run_time_bit_weekdays",
"run_time_date",
"task_type",
"win_task_name",
"timeout",
"enabled",
"remove_if_not_scheduled",
"run_asap_after_missed",
"custom_field",
]
@staticmethod
def generate_task_name():
chars = string.ascii_letters
@@ -160,7 +197,6 @@ class AutomatedTask(BaseAuditModel):
return TaskSerializer(task).data
def create_policy_task(self, agent=None, policy=None):
from .tasks import create_win_task_schedule
# if policy is present, then this task is being copied to another policy
# if agent is present, then this task is being created on an agent from a policy
@@ -177,15 +213,6 @@ class AutomatedTask(BaseAuditModel):
assigned_check = agent.agentchecks.filter(
parent_check=self.assigned_check.pk
).first()
# check was overriden by agent and we need to use that agents check
else:
if agent.agentchecks.filter(
check_type=self.assigned_check.check_type, overriden_by_policy=True
).exists():
assigned_check = agent.agentchecks.filter(
check_type=self.assigned_check.check_type,
overriden_by_policy=True,
).first()
elif policy and self.assigned_check:
if policy.policychecks.filter(name=self.assigned_check.name).exists():
assigned_check = policy.policychecks.filter(
@@ -201,27 +228,175 @@ class AutomatedTask(BaseAuditModel):
policy=policy,
managed_by_policy=bool(agent),
parent_task=(self.pk if agent else None),
alert_severity=self.alert_severity,
email_alert=self.email_alert,
text_alert=self.text_alert,
dashboard_alert=self.dashboard_alert,
script=self.script,
script_args=self.script_args,
assigned_check=assigned_check,
name=self.name,
run_time_days=self.run_time_days,
run_time_minute=self.run_time_minute,
run_time_bit_weekdays=self.run_time_bit_weekdays,
run_time_date=self.run_time_date,
task_type=self.task_type,
win_task_name=self.win_task_name,
timeout=self.timeout,
enabled=self.enabled,
remove_if_not_scheduled=self.remove_if_not_scheduled,
run_asap_after_missed=self.run_asap_after_missed,
)
create_win_task_schedule.delay(task.pk)
for field in self.policy_fields_to_copy:
setattr(task, field, getattr(self, field))
task.save()
task.create_task_on_agent()
def create_task_on_agent(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
if self.task_type == "scheduled":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "weekly",
"weekdays": self.run_time_bit_weekdays,
"pk": self.pk,
"name": self.win_task_name,
"hour": dt.datetime.strptime(self.run_time_minute, "%H:%M").hour,
"min": dt.datetime.strptime(self.run_time_minute, "%H:%M").minute,
},
}
elif self.task_type == "runonce":
# check if scheduled time is in the past
agent_tz = pytz.timezone(agent.timezone)
task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
pytz.utc
)
now = djangotime.now()
if task_time_utc < now:
self.run_time_date = now.astimezone(agent_tz).replace(
tzinfo=pytz.utc
) + djangotime.timedelta(minutes=5)
self.save(update_fields=["run_time_date"])
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "once",
"pk": self.pk,
"name": self.win_task_name,
"year": int(dt.datetime.strftime(self.run_time_date, "%Y")),
"month": dt.datetime.strftime(self.run_time_date, "%B"),
"day": int(dt.datetime.strftime(self.run_time_date, "%d")),
"hour": int(dt.datetime.strftime(self.run_time_date, "%H")),
"min": int(dt.datetime.strftime(self.run_time_date, "%M")),
},
}
if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse(
"1.4.7"
):
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
if self.remove_if_not_scheduled:
nats_data["schedtaskpayload"]["deleteafter"] = True
elif self.task_type == "checkfailure" or self.task_type == "manual":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "manual",
"pk": self.pk,
"name": self.win_task_name,
},
}
else:
return "error"
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
if r != "ok":
self.sync_status = "initial"
self.save(update_fields=["sync_status"])
logger.warning(
f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in."
)
return "timeout"
else:
self.sync_status = "synced"
self.save(update_fields=["sync_status"])
logger.info(f"{agent.hostname} task {self.name} was successfully created")
return "ok"
def modify_task_on_agent(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
nats_data = {
"func": "enableschedtask",
"schedtaskpayload": {
"name": self.win_task_name,
"enabled": self.enabled,
},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
if r != "ok":
self.sync_status = "notsynced"
self.save(update_fields=["sync_status"])
logger.warning(
f"Unable to modify scheduled task {self.name} on {agent.hostname}. It will try again on next agent checkin"
)
return "timeout"
else:
self.sync_status = "synced"
self.save(update_fields=["sync_status"])
logger.info(f"{agent.hostname} task {self.name} was successfully modified")
return "ok"
def delete_task_on_agent(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": self.win_task_name},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if r != "ok" and "The system cannot find the file specified" not in r:
self.sync_status = "pendingdeletion"
self.save(update_fields=["sync_status"])
logger.warning(
f"{agent.hostname} task {self.name} was successfully modified"
)
return "timeout"
else:
self.delete()
logger.info(f"{agent.hostname} task {self.name} was deleted")
return "ok"
def run_win_task(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))
return "ok"
def should_create_alert(self, alert_template=None):
return (

View File

@@ -4,207 +4,46 @@ import random
from time import sleep
from typing import Union
import pytz
from django.conf import settings
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from logs.models import PendingAction
from tacticalrmm.celery import app
from .models import AutomatedTask
from autotasks.models import AutomatedTask
logger.configure(**settings.LOG_CONFIG)
@app.task
def create_win_task_schedule(pk, pending_action=False):
def create_win_task_schedule(pk):
task = AutomatedTask.objects.get(pk=pk)
if task.task_type == "scheduled":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "weekly",
"weekdays": task.run_time_bit_weekdays,
"pk": task.pk,
"name": task.win_task_name,
"hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour,
"min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute,
},
}
elif task.task_type == "runonce":
# check if scheduled time is in the past
agent_tz = pytz.timezone(task.agent.timezone)
task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
now = djangotime.now()
if task_time_utc < now:
task.run_time_date = now.astimezone(agent_tz).replace(
tzinfo=pytz.utc
) + djangotime.timedelta(minutes=5)
task.save(update_fields=["run_time_date"])
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "once",
"pk": task.pk,
"name": task.win_task_name,
"year": int(dt.datetime.strftime(task.run_time_date, "%Y")),
"month": dt.datetime.strftime(task.run_time_date, "%B"),
"day": int(dt.datetime.strftime(task.run_time_date, "%d")),
"hour": int(dt.datetime.strftime(task.run_time_date, "%H")),
"min": int(dt.datetime.strftime(task.run_time_date, "%M")),
},
}
if task.run_asap_after_missed and pyver.parse(
task.agent.version
) >= pyver.parse("1.4.7"):
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
if task.remove_if_not_scheduled:
nats_data["schedtaskpayload"]["deleteafter"] = True
elif task.task_type == "checkfailure" or task.task_type == "manual":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "manual",
"pk": task.pk,
"name": task.win_task_name,
},
}
else:
return "error"
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
# don't create pending action if this task was initiated by a pending action
if not pending_action:
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=task.agent,
action_type="taskaction",
details={"action": "taskcreate", "task_id": task.id},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
logger.error(
f"Unable to create scheduled task {task.win_task_name} on {task.agent.hostname}. It will be created when the agent checks in."
)
return
# clear pending action since it was successful
if pending_action:
pendingaction = PendingAction.objects.get(pk=pending_action)
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
task.sync_status = "synced"
task.save(update_fields=["sync_status"])
logger.info(f"{task.agent.hostname} task {task.name} was successfully created")
task.create_task_on_agent()
return "ok"
@app.task
def enable_or_disable_win_task(pk, action, pending_action=False):
def enable_or_disable_win_task(pk):
task = AutomatedTask.objects.get(pk=pk)
nats_data = {
"func": "enableschedtask",
"schedtaskpayload": {
"name": task.win_task_name,
"enabled": action,
},
}
r = asyncio.run(task.agent.nats_cmd(nats_data))
if r != "ok":
# don't create pending action if this task was initiated by a pending action
if not pending_action:
PendingAction(
agent=task.agent,
action_type="taskaction",
details={
"action": "tasktoggle",
"value": action,
"task_id": task.id,
},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
return
# clear pending action since it was successful
if pending_action:
pendingaction = PendingAction.objects.get(pk=pending_action)
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
task.sync_status = "synced"
task.save(update_fields=["sync_status"])
task.modify_task_on_agent()
return "ok"
@app.task
def delete_win_task_schedule(pk, pending_action=False):
def delete_win_task_schedule(pk):
task = AutomatedTask.objects.get(pk=pk)
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": task.win_task_name},
}
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok" and "The system cannot find the file specified" not in r:
# don't create pending action if this task was initiated by a pending action
if not pending_action:
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=task.agent,
action_type="taskaction",
details={"action": "taskdelete", "task_id": task.id},
).save()
task.sync_status = "pendingdeletion"
task.save(update_fields=["sync_status"])
return "timeout"
# complete pending action since it was successful
if pending_action:
pendingaction = PendingAction.objects.get(pk=pending_action)
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
task.delete()
task.delete_task_on_agent()
return "ok"
@app.task
def run_win_task(pk):
task = AutomatedTask.objects.get(pk=pk)
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
task.run_win_task()
return "ok"

View File

@@ -4,7 +4,6 @@ from unittest.mock import call, patch
from django.utils import timezone as djangotime
from model_bakery import baker
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .models import AutomatedTask
@@ -17,10 +16,10 @@ class TestAutotaskViews(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
@patch("automation.tasks.generate_agent_tasks_from_policies_task.delay")
@patch("automation.tasks.generate_agent_autotasks_task.delay")
@patch("autotasks.tasks.create_win_task_schedule.delay")
def test_add_autotask(
self, create_win_task_schedule, generate_agent_tasks_from_policies_task
self, create_win_task_schedule, generate_agent_autotasks_task
):
url = "/tasks/automatedtasks/"
@@ -84,13 +83,13 @@ class TestAutotaskViews(TacticalTestCase):
"task_type": "manual",
"assigned_check": None,
},
"policy": policy.id,
"policy": policy.id, # type: ignore
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_tasks_from_policies_task.assert_called_with(policy.id)
generate_agent_autotasks_task.assert_called_with(policy=policy.id) # type: ignore
self.check_not_authenticated("post", url)
@@ -106,14 +105,14 @@ class TestAutotaskViews(TacticalTestCase):
serializer = AutoTaskSerializer(agent)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
@patch("automation.tasks.update_policy_task_fields_task.delay")
@patch("automation.tasks.update_policy_autotasks_fields_task.delay")
def test_update_autotask(
self, update_policy_task_fields_task, enable_or_disable_win_task
self, update_policy_autotasks_fields_task, enable_or_disable_win_task
):
# setup data
agent = baker.make_recipe("agents.agent")
@@ -125,32 +124,32 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.patch("/tasks/500/automatedtasks/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/tasks/{agent_task.id}/automatedtasks/"
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
# test editing agent task
data = {"enableordisable": False}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
enable_or_disable_win_task.assert_called_with(pk=agent_task.id, action=False)
enable_or_disable_win_task.assert_called_with(pk=agent_task.id) # type: ignore
url = f"/tasks/{policy_task.id}/automatedtasks/"
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
# test editing policy task
data = {"enableordisable": True}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
update_policy_task_fields_task.assert_called_with(
policy_task.id, update_agent=True
update_policy_autotasks_fields_task.assert_called_with(
task=policy_task.id, update_agent=True # type: ignore
)
self.check_not_authenticated("patch", url)
@patch("autotasks.tasks.delete_win_task_schedule.delay")
@patch("automation.tasks.delete_policy_autotask_task.delay")
@patch("automation.tasks.delete_policy_autotasks_task.delay")
def test_delete_autotask(
self, delete_policy_autotask_task, delete_win_task_schedule
self, delete_policy_autotasks_task, delete_win_task_schedule
):
# setup data
agent = baker.make_recipe("agents.agent")
@@ -163,21 +162,21 @@ class TestAutotaskViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
# test delete agent task
url = f"/tasks/{agent_task.id}/automatedtasks/"
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
delete_win_task_schedule.assert_called_with(pk=agent_task.id)
delete_win_task_schedule.assert_called_with(pk=agent_task.id) # type: ignore
# test delete policy task
url = f"/tasks/{policy_task.id}/automatedtasks/"
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
delete_policy_autotask_task.assert_called_with(policy_task.id)
delete_policy_autotasks_task.assert_called_with(task=policy_task.id) # type: ignore
self.check_not_authenticated("delete", url)
@patch("agents.models.Agent.nats_cmd")
def test_run_autotask(self, nats_cmd):
@patch("autotasks.tasks.run_win_task.delay")
def test_run_autotask(self, run_win_task):
# setup data
agent = baker.make_recipe("agents.agent", version="1.1.0")
task = baker.make("autotasks.AutomatedTask", agent=agent)
@@ -187,11 +186,10 @@ class TestAutotaskViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
# test run agent task
url = f"/tasks/runwintask/{task.id}/"
url = f"/tasks/runwintask/{task.id}/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
nats_cmd.reset_mock()
run_win_task.assert_called()
self.check_not_authenticated("get", url)
@@ -284,9 +282,9 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
run_time_bit_weekdays=127,
run_time_minute="21:55",
)
self.assertEqual(self.task1.sync_status, "notsynced")
self.assertEqual(self.task1.sync_status, "initial")
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
self.assertEqual(nats_cmd.call_count, 1)
nats_cmd.assert_called_with(
{
@@ -301,29 +299,16 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"min": 55,
},
},
timeout=10,
timeout=5,
)
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "synced")
nats_cmd.return_value = "timeout"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
self.assertEqual(ret.status, "SUCCESS")
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "notsynced")
# test pending action
self.pending_action = PendingAction.objects.create(
agent=self.agent, action_type="taskaction"
)
self.assertEqual(self.pending_action.status, "pending")
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(
pk=self.task1.pk, pending_action=self.pending_action.pk
).apply()
self.assertEqual(ret.status, "SUCCESS")
self.pending_action = PendingAction.objects.get(pk=self.pending_action.pk)
self.assertEqual(self.pending_action.status, "completed")
self.assertEqual(self.task1.sync_status, "initial")
# test runonce with future date
nats_cmd.reset_mock()
@@ -337,7 +322,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
run_time_date=run_time_date,
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task2.pk).apply()
nats_cmd.assert_called_with(
{
"func": "schedtask",
@@ -353,7 +338,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")),
},
},
timeout=10,
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")
@@ -369,7 +354,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
run_time_date=run_time_date,
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task3.pk).apply()
self.task3 = AutomatedTask.objects.get(pk=self.task3.pk)
self.assertEqual(ret.status, "SUCCESS")
@@ -385,7 +370,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
assigned_check=self.check,
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task4.pk).apply()
nats_cmd.assert_called_with(
{
"func": "schedtask",
@@ -396,7 +381,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"name": task_name,
},
},
timeout=10,
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")
@@ -410,7 +395,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
task_type="manual",
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task5.pk).apply()
nats_cmd.assert_called_with(
{
"func": "schedtask",
@@ -421,6 +406,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"name": task_name,
},
},
timeout=10,
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")

View File

@@ -1,28 +1,22 @@
import asyncio
from agents.models import Agent
from checks.models import Check
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from checks.models import Check
from scripts.models import Script
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
from .models import AutomatedTask
from .serializers import AutoTaskSerializer, TaskSerializer
from .tasks import (
create_win_task_schedule,
delete_win_task_schedule,
enable_or_disable_win_task,
)
class AddAutoTask(APIView):
def post(self, request):
from automation.models import Policy
from automation.tasks import generate_agent_tasks_from_policies_task
from automation.tasks import generate_agent_autotasks_task
from autotasks.tasks import create_win_task_schedule
data = request.data
script = get_object_or_404(Script, pk=data["autotask"]["script"])
@@ -47,7 +41,7 @@ class AddAutoTask(APIView):
del data["autotask"]["run_time_days"]
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
serializer.is_valid(raise_exception=True)
obj = serializer.save(
task = serializer.save(
**parent,
script=script,
win_task_name=AutomatedTask.generate_task_name(),
@@ -55,11 +49,11 @@ class AddAutoTask(APIView):
run_time_bit_weekdays=bit_weekdays,
)
if not "policy" in data:
create_win_task_schedule.delay(pk=obj.pk)
if task.agent:
create_win_task_schedule.delay(pk=task.pk)
if "policy" in data:
generate_agent_tasks_from_policies_task.delay(data["policy"])
elif task.policy:
generate_agent_autotasks_task.delay(policy=task.policy.pk)
return Response("Task will be created shortly!")
@@ -75,7 +69,7 @@ class AutoTask(APIView):
return Response(AutoTaskSerializer(agent, context=ctx).data)
def put(self, request, pk):
from automation.tasks import update_policy_task_fields_task
from automation.tasks import update_policy_autotasks_fields_task
task = get_object_or_404(AutomatedTask, pk=pk)
@@ -84,39 +78,44 @@ class AutoTask(APIView):
serializer.save()
if task.policy:
update_policy_task_fields_task.delay(task.pk)
update_policy_autotasks_fields_task.delay(task=task.pk)
return Response("ok")
def patch(self, request, pk):
from automation.tasks import update_policy_task_fields_task
from automation.tasks import update_policy_autotasks_fields_task
from autotasks.tasks import enable_or_disable_win_task
task = get_object_or_404(AutomatedTask, pk=pk)
if "enableordisable" in request.data:
action = request.data["enableordisable"]
if not task.policy:
enable_or_disable_win_task.delay(pk=task.pk, action=action)
else:
update_policy_task_fields_task.delay(task.pk, update_agent=True)
task.enabled = action
task.save(update_fields=["enabled"])
action = "enabled" if action else "disabled"
if task.policy:
update_policy_autotasks_fields_task.delay(
task=task.pk, update_agent=True
)
elif task.agent:
enable_or_disable_win_task.delay(pk=task.pk)
return Response(f"Task will be {action} shortly")
else:
return notify_error("The request was invalid")
def delete(self, request, pk):
from automation.tasks import delete_policy_autotask_task
from automation.tasks import delete_policy_autotasks_task
from autotasks.tasks import delete_win_task_schedule
task = get_object_or_404(AutomatedTask, pk=pk)
if not task.policy:
if task.agent:
delete_win_task_schedule.delay(pk=task.pk)
if task.policy:
delete_policy_autotask_task.delay(task.pk)
elif task.policy:
delete_policy_autotasks_task.delay(task=task.pk)
task.delete()
return Response(f"{task.name} will be deleted shortly")
@@ -124,6 +123,8 @@ class AutoTask(APIView):
@api_view()
def run_task(request, pk):
from autotasks.tasks import run_win_task
task = get_object_or_404(AutomatedTask, pk=pk)
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
run_win_task.delay(pk=pk)
return Response(f"{task.name} will now be run on {task.agent.hostname}")

View File

@@ -6,15 +6,14 @@ from statistics import mean
from typing import Any
import pytz
from alerts.models import SEVERITY_CHOICES
from core.models import CoreSettings
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from loguru import logger
from alerts.models import SEVERITY_CHOICES
from core.models import CoreSettings
from logs.models import BaseAuditModel
from loguru import logger
from .utils import bytes2human
@@ -263,6 +262,42 @@ class Check(BaseAuditModel):
"modified_time",
]
@property
def policy_fields_to_copy(self) -> list[str]:
return [
"warning_threshold",
"error_threshold",
"alert_severity",
"name",
"run_interval",
"disk",
"fails_b4_alert",
"ip",
"script",
"script_args",
"info_return_codes",
"warning_return_codes",
"timeout",
"svc_name",
"svc_display_name",
"svc_policy_mode",
"pass_if_start_pending",
"pass_if_svc_not_exist",
"restart_if_stopped",
"log_name",
"event_id",
"event_id_is_wildcard",
"event_type",
"event_source",
"event_message",
"fail_when",
"search_last_days",
"number_of_events_b4_alert",
"email_alert",
"text_alert",
"dashboard_alert",
]
def should_create_alert(self, alert_template=None):
return (
@@ -551,49 +586,23 @@ class Check(BaseAuditModel):
def create_policy_check(self, agent=None, policy=None):
if not agent and not policy or agent and policy:
if (not agent and not policy) or (agent and policy):
return
Check.objects.create(
check = Check.objects.create(
agent=agent,
policy=policy,
managed_by_policy=bool(agent),
parent_check=(self.pk if agent else None),
name=self.name,
alert_severity=self.alert_severity,
check_type=self.check_type,
email_alert=self.email_alert,
dashboard_alert=self.dashboard_alert,
text_alert=self.text_alert,
fails_b4_alert=self.fails_b4_alert,
extra_details=self.extra_details,
run_interval=self.run_interval,
error_threshold=self.error_threshold,
warning_threshold=self.warning_threshold,
disk=self.disk,
ip=self.ip,
script=self.script,
script_args=self.script_args,
timeout=self.timeout,
info_return_codes=self.info_return_codes,
warning_return_codes=self.warning_return_codes,
svc_name=self.svc_name,
svc_display_name=self.svc_display_name,
pass_if_start_pending=self.pass_if_start_pending,
pass_if_svc_not_exist=self.pass_if_svc_not_exist,
restart_if_stopped=self.restart_if_stopped,
svc_policy_mode=self.svc_policy_mode,
log_name=self.log_name,
event_id=self.event_id,
event_id_is_wildcard=self.event_id_is_wildcard,
event_type=self.event_type,
event_source=self.event_source,
event_message=self.event_message,
fail_when=self.fail_when,
search_last_days=self.search_last_days,
number_of_events_b4_alert=self.number_of_events_b4_alert,
)
for field in self.policy_fields_to_copy:
setattr(check, field, getattr(self, field))
check.save()
def is_duplicate(self, check):
if self.check_type == "diskspace":
return self.disk == check.disk

View File

@@ -14,6 +14,22 @@ class TestCheckViews(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
def test_delete_agent_check(self):
# setup data
agent = baker.make_recipe("agents.agent")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
resp = self.client.delete("/checks/500/check/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/checks/{check.pk}/check/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(agent.agentchecks.all())
self.check_not_authenticated("delete", url)
def test_get_disk_check(self):
# setup data
disk_check = baker.make_recipe("checks.diskspace_check")

View File

@@ -1,6 +1,8 @@
import asyncio
from datetime import datetime as dt
from agents.models import Agent
from automation.models import Policy
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
@@ -8,14 +10,6 @@ from packaging import version as pyver
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from automation.models import Policy
from automation.tasks import (
delete_policy_check_task,
generate_agent_checks_from_policies_task,
update_policy_check_fields_task,
)
from scripts.models import Script
from tacticalrmm.utils import notify_error
@@ -25,6 +19,8 @@ from .serializers import CheckHistorySerializer, CheckSerializer
class AddCheck(APIView):
def post(self, request):
from automation.tasks import generate_agent_checks_task
policy = None
agent = None
@@ -53,28 +49,30 @@ class AddCheck(APIView):
data=request.data["check"], partial=True, context=parent
)
serializer.is_valid(raise_exception=True)
obj = serializer.save(**parent, script=script)
new_check = serializer.save(**parent, script=script)
# Generate policy Checks
if policy:
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
generate_agent_checks_task.delay(policy=policy.pk)
elif agent:
checks = agent.agentchecks.filter( # type: ignore
check_type=obj.check_type, managed_by_policy=True
check_type=new_check.check_type, managed_by_policy=True
)
# Should only be one
duplicate_check = [check for check in checks if check.is_duplicate(obj)]
duplicate_check = [
check for check in checks if check.is_duplicate(new_check)
]
if duplicate_check:
policy = Check.objects.get(pk=duplicate_check[0].parent_check).policy
if policy.enforced:
obj.overriden_by_policy = True
obj.save()
new_check.overriden_by_policy = True
new_check.save()
else:
duplicate_check[0].delete()
return Response(f"{obj.readable_desc} was added!")
return Response(f"{new_check.readable_desc} was added!")
class GetUpdateDeleteCheck(APIView):
@@ -83,6 +81,10 @@ class GetUpdateDeleteCheck(APIView):
return Response(CheckSerializer(check).data)
def patch(self, request, pk):
from automation.tasks import (
update_policy_check_fields_task,
)
check = get_object_or_404(Check, pk=pk)
# remove fields that should not be changed when editing a check from the frontend
@@ -105,36 +107,31 @@ class GetUpdateDeleteCheck(APIView):
serializer = CheckSerializer(instance=check, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
# Update policy check fields
if check.policy:
update_policy_check_fields_task(checkpk=pk)
check = serializer.save()
# resolve any alerts that are open
if "check_reset" in request.data.keys():
if obj.alert.filter(resolved=False).exists():
obj.alert.get(resolved=False).resolve()
if check.alert.filter(resolved=False).exists():
check.alert.get(resolved=False).resolve()
return Response(f"{obj.readable_desc} was edited!")
if check.policy:
update_policy_check_fields_task.delay(check=check.pk)
return Response(f"{check.readable_desc} was edited!")
def delete(self, request, pk):
from automation.tasks import generate_agent_checks_task
check = get_object_or_404(Check, pk=pk)
check_pk = check.pk
policy_pk = None
if check.policy:
policy_pk = check.policy.pk
check.delete()
# Policy check deleted
if check.policy:
delete_policy_check_task.delay(checkpk=check_pk)
Check.objects.filter(parent_check=check.pk).delete()
# Re-evaluate agent checks is policy was enforced
if check.policy.enforced:
generate_agent_checks_from_policies_task.delay(policypk=policy_pk)
generate_agent_checks_task.delay(policy=check.policy)
# Agent check deleted
elif check.agent:

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-04-17 01:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0016_auto_20210329_1827'),
]
operations = [
migrations.AddField(
model_name='client',
name='block_policy_inheritance',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='site',
name='block_policy_inheritance',
field=models.BooleanField(default=False),
),
]

View File

@@ -9,6 +9,7 @@ from logs.models import BaseAuditModel
class Client(BaseAuditModel):
name = models.CharField(max_length=255, unique=True)
block_policy_inheritance = models.BooleanField(default=False)
workstation_policy = models.ForeignKey(
"automation.Policy",
related_name="workstation_clients",
@@ -34,30 +35,29 @@ class Client(BaseAuditModel):
def save(self, *args, **kw):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_by_location_task
from automation.tasks import generate_agent_checks_task
# get old client if exists
old_client = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kw)
# check if server polcies have changed and initiate task to reapply policies if so
if old_client and old_client.server_policy != self.server_policy:
generate_agent_checks_by_location_task.delay(
location={"site__client_id": self.pk},
mon_type="server",
create_tasks=True,
)
# check if polcies have changed and initiate task to reapply policies if so
if old_client:
if (
(old_client.server_policy != self.server_policy)
or (old_client.workstation_policy != self.workstation_policy)
or (
old_client.block_policy_inheritance != self.block_policy_inheritance
)
):
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_client and old_client.workstation_policy != self.workstation_policy:
generate_agent_checks_by_location_task.delay(
location={"site__client_id": self.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_task.delay(
client=self.pk,
create_tasks=True,
)
if old_client and old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
if old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -120,6 +120,7 @@ class Client(BaseAuditModel):
class Site(BaseAuditModel):
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
name = models.CharField(max_length=255)
block_policy_inheritance = models.BooleanField(default=False)
workstation_policy = models.ForeignKey(
"automation.Policy",
related_name="workstation_sites",
@@ -145,30 +146,24 @@ class Site(BaseAuditModel):
def save(self, *args, **kw):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_by_location_task
from automation.tasks import generate_agent_checks_task
# get old client if exists
old_site = type(self).objects.get(pk=self.pk) if self.pk else None
super(Site, self).save(*args, **kw)
# check if server polcies have changed and initiate task to reapply policies if so
if old_site and old_site.server_policy != self.server_policy:
generate_agent_checks_by_location_task.delay(
location={"site_id": self.pk},
mon_type="server",
create_tasks=True,
)
# check if polcies have changed and initiate task to reapply policies if so
if old_site:
if (
(old_site.server_policy != self.server_policy)
or (old_site.workstation_policy != self.workstation_policy)
or (old_site.block_policy_inheritance != self.block_policy_inheritance)
):
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_site and old_site.workstation_policy != self.workstation_policy:
generate_agent_checks_by_location_task.delay(
location={"site_id": self.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_task.delay(site=self.pk, create_tasks=True)
if old_site and old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
if old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)

View File

@@ -39,6 +39,7 @@ class SiteSerializer(ModelSerializer):
"client",
"custom_fields",
"agent_count",
"block_policy_inheritance",
)
def validate(self, val):
@@ -80,6 +81,7 @@ class ClientSerializer(ModelSerializer):
"server_policy",
"workstation_policy",
"alert_template",
"block_policy_inheritance",
"sites",
"custom_fields",
"agent_count",

View File

@@ -179,13 +179,9 @@ class TestClientViews(TacticalTestCase):
self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_delete_client(self, task1, task2):
def test_delete_client(self):
from agents.models import Agent
task1.return_value = "ok"
task2.return_value = "ok"
# setup data
client_to_delete = baker.make("clients.Client")
client_to_move = baker.make("clients.Client")
@@ -352,13 +348,9 @@ class TestClientViews(TacticalTestCase):
self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_delete_site(self, task1, task2):
def test_delete_site(self):
from agents.models import Agent
task1.return_value = "ok"
task2.return_value = "ok"
# setup data
client = baker.make("clients.Client")
site_to_delete = baker.make("clients.Site", client=client)

View File

@@ -111,7 +111,7 @@ class GetUpdateClient(APIView):
class DeleteClient(APIView):
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
from automation.tasks import generate_agent_checks_task
client = get_object_or_404(Client, pk=pk)
agents = Agent.objects.filter(site__client=client)
@@ -124,8 +124,7 @@ class DeleteClient(APIView):
site = get_object_or_404(Site, pk=sitepk)
agents.update(site=site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
generate_agent_checks_task.delay(all=True, create_tasks=True)
client.delete()
return Response(f"{client.name} was deleted!")
@@ -207,7 +206,7 @@ class GetUpdateSite(APIView):
class DeleteSite(APIView):
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
from automation.tasks import generate_agent_checks_task
site = get_object_or_404(Site, pk=pk)
if site.client.sites.count() == 1:
@@ -224,8 +223,7 @@ class DeleteSite(APIView):
agents.update(site=agent_site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
generate_agent_checks_task.delay(all=True, create_tasks=True)
site.delete()
return Response(f"{site.name} was deleted!")

View File

@@ -1,12 +1,8 @@
import os
import shutil
import subprocess
import tempfile
from django.core.management.base import BaseCommand
from agents.models import Agent
from scripts.models import Script
from logs.models import PendingAction
class Command(BaseCommand):
@@ -29,5 +25,8 @@ class Command(BaseCommand):
self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
)
# remove task pending actions. deprecated 4/20/2021
PendingAction.objects.filter(action_type="taskaction").delete()
# load community scripts into the db
Script.load_community_scripts()

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.7 on 2021-04-04 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0018_auto_20210329_1709'),
]
operations = [
migrations.CreateModel(
name='GlobalKVStore',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=25)),
('value', models.TextField()),
],
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.1.7 on 2021-04-15 01:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0019_codesigntoken'),
('core', '0019_globalkvstore'),
]
operations = [
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-24 23:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0020_merge_20210415_0132'),
]
operations = [
migrations.AddField(
model_name='customfield',
name='hide_in_ui',
field=models.BooleanField(default=False),
),
]

View File

@@ -79,7 +79,7 @@ class CoreSettings(BaseAuditModel):
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_all_agent_checks_task
from automation.tasks import generate_agent_checks_task
if not self.pk and CoreSettings.objects.exists():
raise ValidationError("There can only be one CoreSettings instance")
@@ -97,14 +97,10 @@ class CoreSettings(BaseAuditModel):
super(BaseAuditModel, self).save(*args, **kwargs)
# check if server polcies have changed and initiate task to reapply policies if so
if old_settings and old_settings.server_policy != self.server_policy:
generate_all_agent_checks_task.delay(mon_type="server", create_tasks=True)
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_settings and old_settings.workstation_policy != self.workstation_policy:
generate_all_agent_checks_task.delay(
mon_type="workstation", create_tasks=True
)
if (old_settings and old_settings.server_policy != self.server_policy) or (
old_settings and old_settings.workstation_policy != self.workstation_policy
):
generate_agent_checks_task.delay(all=True, create_tasks=True)
if old_settings and old_settings.alert_template != self.alert_template:
cache_agents_alert_template.delay()
@@ -251,6 +247,7 @@ class CustomField(models.Model):
blank=True,
default=list,
)
hide_in_ui = models.BooleanField(default=False)
class Meta:
unique_together = (("model", "name"),)
@@ -279,3 +276,56 @@ class CodeSignToken(models.Model):
def __str__(self):
return "Code signing token"
class GlobalKVStore(models.Model):
name = models.CharField(max_length=25)
value = models.TextField()
def __str__(self):
return self.name
RUN_ON_CHOICES = (
("client", "Client"),
("site", "Site"),
("agent", "Agent"),
("once", "Once"),
)
SCHEDULE_CHOICES = (("daily", "Daily"), ("weekly", "Weekly"), ("monthly", "Monthly"))
""" class GlobalTask(models.Model):
script = models.ForeignKey(
"scripts.Script",
null=True,
blank=True,
related_name="script",
on_delete=models.SET_NULL,
)
script_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
custom_field = models.OneToOneField(
"core.CustomField",
related_name="globaltask",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
timeout = models.PositiveIntegerField(default=120)
retcode = models.IntegerField(null=True, blank=True)
retvalue = models.TextField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
execution_time = models.CharField(max_length=100, default="0.0000")
run_schedule = models.CharField(
max_length=25, choices=SCHEDULE_CHOICES, default="once"
)
run_on = models.CharField(
max_length=25, choices=RUN_ON_CHOICES, default="once"
) """

View File

@@ -1,7 +1,7 @@
import pytz
from rest_framework import serializers
from .models import CodeSignToken, CoreSettings, CustomField
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore
class CoreSettingsSerializer(serializers.ModelSerializer):
@@ -33,3 +33,9 @@ class CodeSignTokenSerializer(serializers.ModelSerializer):
class Meta:
model = CodeSignToken
fields = "__all__"
class KeyStoreSerializer(serializers.ModelSerializer):
class Meta:
model = GlobalKVStore
fields = "__all__"

View File

@@ -8,8 +8,8 @@ from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
from .consumers import DashInfo
from .models import CoreSettings, CustomField
from .serializers import CustomFieldSerializer
from .models import CoreSettings, CustomField, GlobalKVStore
from .serializers import CustomFieldSerializer, KeyStoreSerializer
from .tasks import core_maintenance_tasks
@@ -88,8 +88,8 @@ class TestCoreTasks(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_edit_coresettings(self, generate_all_agent_checks_task):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_edit_coresettings(self, generate_agent_checks_task):
url = "/core/editsettings/"
# setup
@@ -106,7 +106,7 @@ class TestCoreTasks(TacticalTestCase):
)
self.assertEqual(CoreSettings.objects.first().mesh_token, data["mesh_token"])
generate_all_agent_checks_task.assert_not_called()
generate_agent_checks_task.assert_not_called()
# test adding policy
data = {
@@ -120,9 +120,9 @@ class TestCoreTasks(TacticalTestCase):
CoreSettings.objects.first().workstation_policy.id, policies[0].id # type: ignore
)
self.assertEqual(generate_all_agent_checks_task.call_count, 2)
generate_agent_checks_task.assert_called_once()
generate_all_agent_checks_task.reset_mock()
generate_agent_checks_task.reset_mock()
# test remove policy
data = {
@@ -132,7 +132,7 @@ class TestCoreTasks(TacticalTestCase):
self.assertEqual(r.status_code, 200)
self.assertEqual(CoreSettings.objects.first().workstation_policy, None)
self.assertEqual(generate_all_agent_checks_task.call_count, 1)
self.assertEqual(generate_agent_checks_task.call_count, 1)
self.check_not_authenticated("patch", url)
@@ -273,3 +273,61 @@ class TestCoreTasks(TacticalTestCase):
self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)
def test_get_keystore(self):
url = "/core/keystore/"
# setup
keys = baker.make("core.GlobalKVStore", _quantity=2)
r = self.client.get(url)
serializer = KeyStoreSerializer(keys, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 2) # type: ignore
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_keystore(self):
url = "/core/keystore/"
data = {"name": "test", "value": "text"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)
def test_update_keystore(self):
# setup
key = baker.make("core.GlobalKVStore")
# test not found
r = self.client.put("/core/keystore/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/keystore/{key.id}/" # type: ignore
data = {"name": "test", "value": "text"}
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
new_key = GlobalKVStore.objects.get(pk=key.id) # type: ignore
self.assertEqual(new_key.name, data["name"])
self.assertEqual(new_key.value, data["value"])
self.check_not_authenticated("put", url)
def test_delete_keystore(self):
# setup
key = baker.make("core.GlobalKVStore")
# test not found
r = self.client.delete("/core/keystore/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/keystore/{key.id}/" # type: ignore
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(GlobalKVStore.objects.filter(pk=key.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)

View File

@@ -13,4 +13,6 @@ urlpatterns = [
path("customfields/", views.GetAddCustomFields.as_view()),
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
path("codesign/", views.CodeSign.as_view()),
path("keystore/", views.GetAddKeyStore.as_view()),
path("keystore/<int:pk>/", views.UpdateDeleteKeyStore.as_view()),
]

View File

@@ -11,11 +11,12 @@ from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import CodeSignToken, CoreSettings, CustomField
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore
from .serializers import (
CodeSignTokenSerializer,
CoreSettingsSerializer,
CustomFieldSerializer,
KeyStoreSerializer,
)
@@ -229,3 +230,32 @@ class CodeSign(APIView):
except:
ret = "Something went wrong"
return notify_error(ret)
class GetAddKeyStore(APIView):
def get(self, request):
keys = GlobalKVStore.objects.all()
return Response(KeyStoreSerializer(keys, many=True).data)
def post(self, request):
serializer = KeyStoreSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class UpdateDeleteKeyStore(APIView):
def put(self, request, pk):
key = get_object_or_404(GlobalKVStore, pk=pk)
serializer = KeyStoreSerializer(instance=key, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(GlobalKVStore, pk=pk).delete()
return Response("ok")

View File

@@ -7,7 +7,7 @@ from tacticalrmm.middleware import get_debug_info, get_username
ACTION_TYPE_CHOICES = [
("schedreboot", "Scheduled Reboot"),
("taskaction", "Scheduled Task Action"),
("taskaction", "Scheduled Task Action"), # deprecated
("agentupdate", "Agent Update"),
("chocoinstall", "Chocolatey Software Install"),
]
@@ -42,13 +42,6 @@ AUDIT_OBJECT_TYPE_CHOICES = [
("bulk", "Bulk"),
]
# taskaction details format
# {
# "action": "taskcreate" | "taskdelete" | "tasktoggle",
# "value": "Enable" | "Disable" # only needed for task toggle,
# "task_id": 1
# }
STATUS_CHOICES = [
("pending", "Pending"),
("completed", "Completed"),
@@ -250,8 +243,6 @@ class PendingAction(models.Model):
if self.action_type == "schedreboot":
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
elif self.action_type == "taskaction":
return "Next agent check-in"
elif self.action_type == "agentupdate":
return "Next update cycle"
elif self.action_type == "chocoinstall":
@@ -268,20 +259,6 @@ class PendingAction(models.Model):
elif self.action_type == "chocoinstall":
return f"{self.details['name']} software install"
elif self.action_type == "taskaction":
if self.details["action"] == "taskdelete":
return "Device pending task deletion"
elif self.details["action"] == "taskcreate":
return "Device pending task creation"
elif self.details["action"] == "tasktoggle":
# value is bool
if self.details["value"]:
action = "enable"
else:
action = "disable"
return f"Device pending task {action}"
class BaseAuditModel(models.Model):
# abstract base class for auditing models

View File

@@ -262,23 +262,13 @@
},
{
"guid": "d980fda3-a068-47eb-8495-1aab07a24e64",
"filename": "Win_Defender_Status_Report_Last24hrs.ps1",
"filename": "Win_Defender_Status_Report.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Defender - Status Report Last 24hr",
"description": "Using Event Viewer, this will check for Malware and Antispyware within the last 24 hours and display, otherwise will report as Healthy",
"name": "Defender - Status Report",
"description": "This will check for Malware and Antispyware within the last 24 hours and display, otherwise will report as Healthy. Command Parameter: (number) if provided will check that number of days back in the log.",
"shell": "powershell",
"category": "TRMM (Win):Security>Antivirus"
},
{
"guid": "6a0202fc-1b27-4905-95c0-0a03bb881893",
"filename": "Win_Defender_Status_Report_LastYear.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Defender - Status Report Last Year",
"description": "Using Event Viewer, this will check for Malware and Antispyware and display, otherwise will report as Healthy",
"shell": "powershell",
"category": "TRMM (Win):Security>Antivirus",
"default_timeout": "300"
},
{
"guid": "9956e936-6fdb-4488-a9d8-8b274658037f",
"filename": "Win_Disable_Fast_Startup.bat",

View File

@@ -196,9 +196,9 @@ class Script(BaseAuditModel):
def parse_script_args(
cls, agent, shell: str, args: List[str] = list()
) -> Union[List[str], None]:
from core.models import CustomField
from core.models import CustomField, GlobalKVStore
if not list:
if not args:
return []
temp_args = list()
@@ -220,6 +220,18 @@ class Script(BaseAuditModel):
# ignore arg since it is invalid
continue
# value is in the global keystore and replace value
if temp[0] == "global":
if GlobalKVStore.objects.filter(name=temp[1]).exists():
value = GlobalKVStore.objects.get(name=temp[1]).value
temp_args.append(
re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)
)
continue
else:
# ignore since value doesn't exist
continue
if temp[0] == "client":
model = "client"
obj = agent.client
@@ -263,7 +275,7 @@ class Script(BaseAuditModel):
# replace the value in the arg and push to array
# log any unhashable type errors
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)) # type: ignore
except Exception as e:
logger.error(e)
continue

View File

@@ -15,16 +15,16 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.6.2"
TRMM_VERSION = "0.6.5"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.131"
APP_VER = "0.0.132"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.5.2"
MESH_VER = "0.8.17"
MESH_VER = "0.8.19"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "15"

View File

@@ -66,8 +66,7 @@ class TestUtils(TestCase):
mock_subprocess.assert_not_called()
@override_settings(
ALLOWED_HOSTS=["api.example.com"],
SECRET_KEY="sekret",
ALLOWED_HOSTS=["api.example.com"], SECRET_KEY="sekret", DOCKER_BUILD=False
)
@patch("subprocess.run")
def test_reload_nats(self, mock_subprocess):

View File

@@ -15,14 +15,14 @@ Needs an official install_devbox.sh script
## Install Ubuntu 20.04 LTS
Don't forget to
```
```bash
sudo apt-get updates && sudo apt-get upgrade
```
### Optional
Set all users in sudo group not to require password every time:
```
```bash
sudo visudo
```
@@ -36,14 +36,14 @@ Add this:
Create folder to dump into
```
```bash
sudo mkdir /rmm
sudo chown ${USER}:${USER} -R /rmm
cd /rmm
```
Get dev install script
```
```bash
wget https://raw.githubusercontent.com/silversword411/tacticalrmm-devdocs/blob/main/install_devbox.sh
```
@@ -53,7 +53,7 @@ and replace with your forked repo URL (example commented out below)
## Run it
```
```bash
./install_devbox.sh
```
## Watch for
@@ -98,7 +98,16 @@ pip install -r requirements.txt
Then run tests
```
```bash
python manage.py test
```
## Misc Notes
### Spinning up front end web interface in development
```bash
cd /web
npm run serve
```

View File

@@ -0,0 +1,48 @@
## Install WSL2
https://docs.microsoft.com/en-us/windows/wsl/install-win10
## Install Docker Desktop
https://www.docker.com/products/docker-desktop
### Configure Docker
Make sure it doesn't look like this
![img](images/docker_WSL2_distros_missing.png)
This is better
![img](images/docker_with_ubuntu-20.04.png)
### Check and make sure WSL is v2 and set Ubuntu as default
[https://docs.microsoft.com/en-us/windows/wsl/install-win10#set-your-distribution-version-to-wsl-1-or-wsl-2](https://docs.microsoft.com/en-us/windows/wsl/install-win10#set-your-distribution-version-to-wsl-1-or-wsl-2)
![img](images/wls2_upgrade_and_set_default.png)
## Create .env file
Under .devcontainer duplicate
```
.env.example
```
as
```
.env
```
Customize to your tastes (it doesn't need to be internet configured, just add records in your `hosts` file) eg
```
127.0.0.1 rmm.example.com
127.0.0.1 api.example.com
127.0.0.1 mesh.example.com
```

View File

@@ -84,6 +84,14 @@ Bring changes from original repo to your local vscode copy so you're current wit
![Sync Fork](images/trmm_need_sync_local_fork.png)
In VSCode open TERMINAL
```
Ctrl+`
```
Tell git to pull from the GitHub upstream repo all new changes into your local directory
```
git pull --rebase upstream develop
```

View File

@@ -0,0 +1,9 @@
# Automated Tasks
## Collector Tasks
Collector tasks allow saving data from script output directly to a custom field. The collector task will only save the last line of standard output of the script.
You can create collector tasks by adding it to an Automation Policy or adding it directly to an agent. During creation, select the **Collector** checkbox and select the custom field to save to. You can only save to agent custom fields at this time.
See [Custom Fields](custom_fields.md) and [Scripting](scripting.md) for more information

View File

@@ -1,16 +1,53 @@
# Custom Fields
**Settings > Global Settings > Custom Fields**
!!!info
v0.5.0 adds support for custom fields to be used in the dashboard and in scripts.
v0.5.0 adds support for custom fields to be used in scripts.
#### Adding Custom Fields
It also exposes some pre-defined fields that are already in the database.
In the dashboard, go to **Settings > Global Settings > Custom Fields** and click **Add Custom Field**.
Please check the following video for examples until proper docs are written:
The following options are available to configure on custom fields:
[https://www.youtube.com/watch?v=0-5jGGL3FOM](https://www.youtube.com/watch?v=0-5jGGL3FOM)
- **Model** - This is the object that the custom field will be added to. The available options are:
- Agent
- Site
- Client
- **Name** - Sets the name of the custom field. This will be used to identify the custom field in the dashboard and in scripts.
- **Field Type** - Sets the type of field. Below are the allowed types.
- Text
- Number
- Single select dropdown
- Multi-select dropdown
- Checkbox
- DateTime
- **Input Options** - *Only available on Single and Multiple-select dropdowns*. Sets the options to choose from.
- **Default Value** - If no value is found when looking up the custom field; this value will instead be supplied.
- **Required** - This makes the field required when adding new Clients, Sites, and Agents. *If this is set a default value will need to be set as well*
- **Hide in Dashboard** - This will not show the custom field in Client, Site, and Agent forms in the dashboard. This is useful if the custom field's value is updated by a collector task and only supplied to scripts.
#### Using Custom Fields in the Dashboard
Once the custom fields are added, they will show up in the Client, Site, and Agent Add/Edit forms.
#### Using Custom Fields in Scripts
Tactical RMM allows for passing various database fields for Clients, Sites, and Agents in scripts. This includes custom fields as well!
!!!warning
The characters within the brackets is case-sensitive!
In your script's arguments, use the notation `{{client.AV_KEY}}`. This will lookup the client for the agent that the script is running on and find the custom field named `AV_KEY` and replace that with the value.
The same is also true for `{{site.no_patching}}` and `{{agent.Another Field}}`
For more information see SCRIPTING PAGE
#### Populating Custom Fields automatically
Tactical RMM supports automatically collecting information and saving them directly to custom fields. This is made possible by creating **Collector Tasks**. These are just normal Automated Tasks, but instead they will save the last line of the standard output to the custom field that is selected.
!!!info
To populate a multiple select custom field, return a string with the options separated by a comma `"This,will,be,an,array"`
For more information See [Collector Tasks](automated_tasks.md#Collector Tasks)

View File

@@ -0,0 +1,9 @@
# Global Key Store
The key store is used to store values that need to be referenced from multiple scripts. This also allows for easy updating of values since scripts reference the values at runtime.
To Add/Edit values in the Global Key Store, browse to **Settings > Global Settings > KeyStore**.
You can reference values from the key store in script arguments by using the {{global.key_name}} syntax.
See [Scripts](scripting.md) for more information.

View File

@@ -0,0 +1,110 @@
# Scripting
Tactical RMM supports uploading existing scripts or adding new scripts right in the dashboard. Languages supported are:
- Powershell
- Windows Batch
- Python
## Adding Scripts
In the dashboard, browse to **Settings > Scripts Manager**. Click the **New** button and select either Upload Script or New Script. The available options for scripts are:
- **Name** - This identifies the script in the dashboard
- **Description** - Optional description for the script
- **Category** - Optional way to group similar scripts together.
- **Type** - This sets the language of the script. Available options are:
- Powershell
- Windows Batch
- Python
- **Script Arguments** - Optional way to set default arguments for scripts. These will autopopulate when running scripts and can be changed at runtime.
- **Default Timeout** - Sets the default timeout of the script and will stop script execution if the duration surpasses the configured timeout. Can be changed at script runtime
- **Favorite** - Favorites the script.
## Downloading Scripts
To download a Tactical RMM Script, click on the script in the Script Manager to select it. Then click the **Download Script** button on the top. You can also right-click on the script and select download
## Community Script
These are script that are built into Tactical RMM. They are provided and mantained by the Tactical RMM community. These scripts are updated whenever Tactical RMM is updated and can't be modified or deleted in the dashboard.
### Hiding Community Scripts
You can choose to hide community script throughout the dashboard by opening **Script Manager** and clicking the **Show/Hide Community Scripts** toggle button.
## Using Scripts
### Manual run on agent
In the **Agent Table**, you can right-click on an agent and select **Run Script**. You have the options of:
- **Wait for Output** - Runs the script and waits for the script to finish running and displays the output.
- **Fire and Forget** - Starts the script and does not wait for output.
- **Email Output** - Starts the script and will email the output. Allows for using the default email address in the global settings or adding a new email address.
There is also an option on the agent context menu called **Run Favorited Script**. This will essentially Fire and Forget the script with default args and timeout.
### Bulk Run on agents
Tactical RMM offers a way to run a script on multiple agents at once. Browse to **Tools > Bulk Script** and select the target for the script to run.
### Automated Tasks
Tactical RMM allows scheduling tasks to run on agents. This leverages the Windows Task Scheduler and has the same scheduling options.
See [Automated Tasks](automated_tasks.md) for configuring automated tasks
### Script Checks
Scripts can also be run periodically on an agent and trigger an alert if it fails.
### Alert Failure/Resolve Actions
Scripts can be triggered when an alert is triggered and resolved. This script will run on any online agent and supports passing the alert information as arguments.
For configuring **Alert Templates**, see [Alerting](../alerting.md)
See below for populating dashboard data in scripts and the available options.
## Using dashboard data in scripts
Tactical RMM allows passing in dashboard data to scripts as arguments. The below powershell arguments will get the client name of the agent and also the agent's public IP address
```
-ClientName {{client.name}} -PublicIP {{agent.public_ip}}
```
!!!info
Script variables are case-sensitive!
See a full list of available options [Here](../script_variables.md)
### Getting Custom Field values
Tactical RMM supports pulling data from custom fields using the {{model.custom_field_name}} syntax.
See [Using Custom Fields in Scripts](custom_fields.md#Using Custom Fields in Scripts)
### Getting values from the Global Keystore
Tactical RMM supports getting values from the global key store using the {{global.key_name}} syntax
See [Global Keystore](keystore.md).
### Example Powershell Script
The below script takes five named values. The arguments will look like this: `-SiteName {{site.name}} -ClientName {{client.name}} -PublicIP {{agent.public_ip}} -CustomField {{client.AV_KEY}} -Global {{global.API_KEY}}`
```powershell
param (
[string] $SiteName,
[string] $ClientName,
[string] $PublicIp,
[string] $CustomField,
[string] $Global
)
Write-Output "Site: $SiteName"
Write-Output "Client: $ClientName"
Write-Output "Public IP: $PublicIp"
Write-Output "Custom Fields: $CustomField"
Write-Output "Global: $Global"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -10,8 +10,6 @@ It uses an [agent](https://github.com/wh1te909/rmmagent) written in Golang and i
## [LIVE DEMO](https://rmm.tacticalrmm.io/)
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
## Features
- Teamviewer-like remote desktop control

View File

@@ -48,18 +48,27 @@ Change offline time on all agents to 5 minutes
python manage.py bulk_change_checkin --offline --all 5
```
Change overdue time on all agents to 10 minutes
```bash
python manage.py bulk_change_checkin --overdue --all 10
```
Change overdue time on all agents in client named *Example Client* to 12 minutes
```bash
python manage.py bulk_change_checkin --overdue --client "Example Client" 12
```
Change offline time on all agents in site named *Example Site* to 2 minutes
```bash
python manage.py bulk_change_checkin --offline --site "Example Site" 2
```
Change offline time on all agents in client named *Example Client* to 12 minutes
```bash
python manage.py bulk_change_checkin --offline --client "Example Client" 12
```
Change overdue time on all agents to 10 minutes
```bash
python manage.py bulk_change_checkin --overdue --all 10
```
Change overdue time on all agents in site named *Example Site* to 4 minutes
```bash
python manage.py bulk_change_checkin --overdue --site "Example Site" 4
```
Change overdue time on all agents in client named *Example Client* to 14 minutes
```bash
python manage.py bulk_change_checkin --overdue --client "Example Client" 14
```

View File

@@ -4,7 +4,9 @@
It is currently not possible to restore to a different domain/subdomain, only to a different physical or virtual server.
!!!danger
You must update your old RMM to the latest version using the `update.sh` script before attempting to restore.
The restore script will always restore to the latest available RMM version on github.
Make sure you update your old RMM to the latest version using the `update.sh` script and then run a fresh backup to use with this restore script.
#### Prepare the new server
Create the same exact linux user account as you did when you installed the original server.

View File

@@ -0,0 +1,39 @@
# Script Variables
Tactical RMM allows passing dashboard data into script as arguments. This uses the syntax `{{client.name}}`.
See below for the available options.
## Agent
- **{{agent.version}}** - Tactical RMM agent version
- **{{agent.operating_system}}** - Agent operating system example: *Windows 10 Pro, 64 bit (build 19042.928)*
- **{{agent.plat}}** - Will show the platform example: *windows*
- **{{agent.hostname}}** - The hostname of the agent
- **{{agent.public_ip}}** - Public IP address of agent
- **{{agent.total_ram}}** - Total RAM on agent. Returns an integer - example: *16*
- **{{agent.boot_time}}** - Uptime of agent. Returns unix timestamp. example: *1619439603.0*
- **{{agent.logged_in_user}}** - Username of logged in user
- **{{agent.monitoring_type}}** - Returns a string of *workstation* or *server*
- **{{agent.description}}** - Description of agent in dashboard
- **{{agent.mesh_node_id}}** - The mesh node id used for linking the tactical agent to mesh.
- **{{agent.choco_installed}}** - Boolean to see if Chocolatey is installed
- **{{agent.patches_last_installed}}** - The date that patches were last installed by Tactical RMM.
- **{{agent.needs_reboot}}** - Returns true if the agent needs a reboot
- **{{agent.time_zone}}** - Returns timezone configured on agent
- **{{agent.maintenance_mode}}** - Returns true if agent is in maintenance mode
## Client
- **{{client.name}}** - Returns name of client
## Site
- **{{site.name}}** - Returns name of Site
## Alert
!!!info
Only available in failure and resolve actions on alert templates!
- **{{alert.alert_time}}** - Time of the alert
- **{{alert.message}}** - Alert message
- **{{alert.severity}}** - Severity of the alert *info, warning, or error*

View File

@@ -12,6 +12,9 @@ nav:
- "Updating the RMM (Docker)": update_docker.md
- "Updating Agents": update_agents.md
- Functionality:
- "Automated Tasks": functions/automated_tasks.md
- "Scripting": functions/scripting.md
- "Global Keystore": functions/keystore.md
- "Custom Fields": functions/custom_fields.md
- "Remote Background": functions/remote_bg.md
- "Maintenance Mode": functions/maintenance_mode.md

View File

@@ -0,0 +1,35 @@
<#
.Synopsis
Defender - Status Report
.DESCRIPTION
This will check Event Log for Windows Defender Malware and Antispyware reports, otherwise will report as Healthy. By default if no command parameter is provided it will check the last 1 day (good for a scheduled daily task).
If a number is provided as a command parameter it will search back that number of days back provided (good for collecting all AV alerts on the computer).
.EXAMPLE
Win_Defender_Status_reports.ps1 365
#>
$param1 = $args[0]
$ErrorActionPreference = 'silentlycontinue'
if ($Args.Count -eq 0) {
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
}
else {
$TimeSpan = (Get-Date) - (New-TimeSpan -Day $param1)
}
if (Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan }) {
Write-Output "Virus Found or Issue with Defender"
Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan }
exit 1
}
else {
Write-Output "No Virus Found, Defender is Healthy"
Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1150', '1001'; StartTime = $TimeSpan }
exit 0
}
Exit $LASTEXITCODE

View File

@@ -1,24 +0,0 @@
# This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours
$ErrorActionPreference= 'silentlycontinue'
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
if (Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational';ID='1116','1118','1015','1006','5010','5012','5001','1123';StartTime=$TimeSpan})
{
Write-Output "Virus Found or Issue with Defender"
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational';ID='1116','1118','1015','1006','5010','5012','5001','1123';StartTime=$TimeSpan}
exit 1
}
else
{
Write-Output "No Virus Found, Defender is Healthy"
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational';ID='1150','1001';StartTime=$TimeSpan}
exit 0
}
Exit $LASTEXITCODE

View File

@@ -1,22 +0,0 @@
# This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours
$ErrorActionPreference = 'silentlycontinue'
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 365)
if (Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan })
{
Write-Output "Virus Found or Issue with Defender"
Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1116', '1118', '1015', '1006', '5010', '5012', '5001', '1123'; StartTime = $TimeSpan }
exit 1
}
else
{
Write-Output "No Virus Found, Defender is Healthy"
Get-WinEvent -FilterHashtable @{LogName = 'Microsoft-Windows-Windows Defender/Operational'; ID = '1150', '1001'; StartTime = $TimeSpan }
exit 0
}
Exit $LASTEXITCODE

View File

@@ -60,6 +60,14 @@
<q-th auto-width :props="props"></q-th>
</template>
<template v-slot:header-cell-collector="props">
<q-th auto-width :props="props">
<q-icon name="mdi-database-arrow-up" size="1.5em">
<q-tooltip>Collector Task</q-tooltip>
</q-icon>
</q-th>
</template>
<template v-slot:header-cell-status="props">
<q-th auto-width :props="props"></q-th>
</template>
@@ -170,6 +178,13 @@
<q-tooltip>This task is managed by a policy</q-tooltip>
</q-icon>
</q-td>
<!-- is collector task -->
<q-td>
<q-icon v-if="!!props.row.custom_field" style="font-size: 1.3rem" name="check">
<q-tooltip>The task updates a custom field on the agent</q-tooltip>
</q-icon>
</q-td>
<!-- status icon -->
<q-td v-if="props.row.status === 'passing'">
<q-icon style="font-size: 1.3rem" color="positive" name="check_circle">
@@ -199,6 +214,8 @@
<q-td v-if="props.row.sync_status === 'notsynced'">Will sync on next agent checkin</q-td>
<q-td v-else-if="props.row.sync_status === 'synced'">Synced with agent</q-td>
<q-td v-else-if="props.row.sync_status === 'pendingdeletion'">Pending deletion on agent</q-td>
<q-td v-else-if="props.row.sync_status === 'initial'">Waiting for task creation on agent</q-td>
<q-td v-else></q-td>
<q-td v-if="props.row.retcode !== null || props.row.stdout || props.row.stderr">
<span
style="cursor: pointer; text-decoration: underline"
@@ -249,36 +266,43 @@ export default {
{ name: "emailalert", field: "email_alert", align: "left" },
{ name: "dashboardalert", field: "dashboard_alert", align: "left" },
{ name: "policystatus", align: "left" },
{ name: "collector", label: "Collector", field: "custom_field", align: "left", sortable: true },
{ name: "status", align: "left" },
{ name: "name", label: "Name", field: "name", align: "left" },
{ name: "sync_status", label: "Sync Status", field: "sync_status", align: "left" },
{ name: "name", label: "Name", field: "name", align: "left", sortable: true },
{ name: "sync_status", label: "Sync Status", field: "sync_status", align: "left", sortable: true },
{
name: "moreinfo",
label: "More Info",
field: "more_info",
align: "left",
sortable: true,
},
{
name: "datetime",
label: "Last Run Time",
field: "last_run",
align: "left",
sortable: true,
},
{
name: "schedule",
label: "Schedule",
field: "schedule",
align: "left",
sortable: true,
},
{
name: "assignedcheck",
label: "Assigned Check",
field: "assigned_check",
align: "left",
sortable: true,
},
],
pagination: {
rowsPerPage: 9999,
sortBy: "name",
descending: false,
},
};
},
@@ -383,7 +407,7 @@ export default {
automatedTasks: state => state.automatedTasks,
}),
tasks() {
return this.automatedTasks.autotasks;
return this.automatedTasks.autotasks.filter(task => task.sync_status !== "pendingdeletion");
},
},
};

View File

@@ -61,6 +61,15 @@
</q-icon>
</q-th>
</template>
<template v-slot:header-cell-collector="props">
<q-th auto-width :props="props">
<q-icon name="mdi-database-arrow-up" size="1.5em">
<q-tooltip>Collector Task</q-tooltip>
</q-icon>
</q-th>
</template>
<!-- body slots -->
<template v-slot:body="props" :props="props">
<q-tr class="cursor-pointer" @dblclick="showEditTask(props.row)">
@@ -130,6 +139,12 @@
v-model="props.row.dashboard_alert"
/>
</q-td>
<!-- is collector task -->
<q-td>
<q-icon v-if="!!props.row.custom_field" style="font-size: 1.3rem" name="check">
<q-tooltip>The task updates a custom field on the agent</q-tooltip>
</q-icon>
</q-td>
<q-td>{{ props.row.name }}</q-td>
<q-td>{{ props.row.schedule }}</q-td>
<q-td>
@@ -182,24 +197,28 @@ export default {
{ name: "smsalert", field: "text_alert", align: "left" },
{ name: "emailalert", field: "email_alert", align: "left" },
{ name: "dashboardalert", field: "dashboard_alert", align: "left" },
{ name: "name", label: "Name", field: "name", align: "left" },
{ name: "collector", label: "Collector", field: "custom_field", align: "left", sortable: true },
{ name: "name", label: "Name", field: "name", align: "left", sortable: true },
{
name: "schedule",
label: "Schedule",
field: "schedule",
align: "left",
sortable: true,
},
{
name: "status",
label: "More Info",
field: "more_info",
align: "left",
sortable: true,
},
{
name: "assignedcheck",
label: "Assigned Check",
field: "assigned_check",
align: "left",
sortable: true,
},
],
pagination: {

View File

@@ -50,6 +50,10 @@
label="Policy"
>
</q-select>
<q-checkbox label="Block policy inheritance" v-model="blockInheritance">
<q-tooltip>This {{ type }} will not inherit from higher policies</q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section v-else>
No Automation Policies have been setup. Go to Settings > Automation Manager
@@ -85,6 +89,7 @@ export default {
selectedWorkstationPolicy: null,
selectedServerPolicy: null,
selectedAgentPolicy: null,
blockInheritance: false,
options: [],
};
},
@@ -94,13 +99,17 @@ export default {
if (this.type === "client" || this.type === "site") {
if (
this.object.workstation_policy === this.selectedWorkstationPolicy &&
this.object.server_policy === this.selectedServerPolicy
this.object.server_policy === this.selectedServerPolicy &&
this.object.blockInheritance === this.blockInheritance
) {
this.hide();
return;
}
} else if (this.type === "agent") {
if (this.object.policy === this.selectedAgentPolicy) {
if (
this.object.policy === this.selectedAgentPolicy &&
this.object.block_policy_inheritance === this.blockInheritance
) {
this.hide();
return;
}
@@ -118,6 +127,7 @@ export default {
pk: this.object.id,
server_policy: this.selectedServerPolicy,
workstation_policy: this.selectedWorkstationPolicy,
block_policy_inheritance: this.blockInheritance,
},
};
} else if (this.type === "site") {
@@ -127,6 +137,7 @@ export default {
pk: this.object.id,
server_policy: this.selectedServerPolicy,
workstation_policy: this.selectedWorkstationPolicy,
block_policy_inheritance: this.blockInheritance,
},
};
} else if (this.type === "agent") {
@@ -134,6 +145,7 @@ export default {
data = {
id: this.object.id,
policy: this.selectedAgentPolicy,
block_policy_inheritance: this.blockInheritance,
};
}
@@ -186,8 +198,10 @@ export default {
if (this.type !== "agent") {
this.selectedServerPolicy = this.object.server_policy;
this.selectedWorkstationPolicy = this.object.workstation_policy;
this.blockInheritance = this.object.blockInheritance;
} else {
this.selectedAgentPolicy = this.object.policy;
this.blockInheritance = this.object.block_policy_inheritance;
}
},
};

View File

@@ -62,6 +62,8 @@
<q-td v-else-if="props.row.sync_status === 'notsynced'">Will sync on next agent checkin</q-td>
<q-td v-else-if="props.row.sync_status === 'synced'">Synced with agent</q-td>
<q-td v-else-if="props.row.sync_status === 'pendingdeletion'">Pending deletion on agent</q-td>
<q-td v-else-if="props.row.sync_status === 'initial'">Waiting for task creation on agent</q-td>
<q-td v-else></q-td>
<!-- more info -->
<q-td v-if="props.row.check_type === 'ping'">
<span

View File

@@ -53,6 +53,12 @@
</q-badge>
<span> To use a domain CA </span>
</div>
<div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>-desc "Desired custom description on agent"</code>
</q-badge>
<span> Set agent description field during install </span>
</div>
</q-expansion-item>
<br />
<p class="text-italic">Note: the auth token above will be valid for {{ info.expires }} hours.</p>

View File

@@ -60,13 +60,7 @@
<q-card-section class="row">
<div class="col-2">Description:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="agent.description"
class="col-8"
:rules="[val => !!val || '*Required']"
/>
<q-input outlined dense v-model="agent.description" class="col-8" />
</q-card-section>
<q-card-section class="row">
<div class="col-2">Timezone:</div>
@@ -266,7 +260,7 @@ export default {
created() {
// Get custom fields
this.getCustomFields("agent").then(r => {
this.customFields = r.data;
this.customFields = r.data.filter(field => !field.hide_in_ui);
});
this.getAgentInfo();
this.getClientsSites();

View File

@@ -29,6 +29,7 @@
/>
</q-card-section>
<div class="text-h6">Custom Fields</div>
<q-card-section v-for="field in customFields" :key="field.id">
<CustomField v-model="custom_fields[field.name]" :field="field" />
</q-card-section>
@@ -175,7 +176,7 @@ export default {
created() {
// Get custom fields
this.getCustomFields("client").then(r => {
this.customFields = r.data;
this.customFields = r.data.filter(field => !field.hide_in_ui);
});
// Copy client prop locally

View File

@@ -33,6 +33,7 @@
/>
</q-card-section>
<div class="text-h6">Custom Fields</div>
<q-card-section v-for="field in customFields" :key="field.id">
<CustomField v-model="custom_fields[field.name]" :field="field" />
</q-card-section>
@@ -199,7 +200,7 @@ export default {
// Get custom fields
this.getCustomFields("site").then(r => {
this.customFields = r.data;
this.customFields = r.data.filter(field => !field.hide_in_ui);
});
// Copy site prop locally

View File

@@ -147,6 +147,7 @@
v-model="localField.required"
color="green"
/>
<q-toggle label="Hide in Dashboard" v-model="localField.hide_in_ui" color="green" />
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
@@ -175,6 +176,7 @@ export default {
default_value_string: "",
default_value_bool: false,
default_values_multiple: [],
hide_in_ui: false,
},
modelOptions: [
{ label: "Client", value: "client" },
@@ -222,7 +224,6 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
this.onOk();
this.notifyError("There was an error editing the custom field");
});
} else {
@@ -261,7 +262,7 @@ export default {
},
},
mounted() {
// If pk prop is set that means we are editting
// If pk prop is set that means we are editing
if (this.field) Object.assign(this.localField, this.field);
// Set model to current tab

View File

@@ -13,12 +13,7 @@
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr
:props="props"
class="cursor-pointer"
@contextmenu="selectedTemplate = props.row"
@dblclick="editCustomField(props.row)"
>
<q-tr :props="props" class="cursor-pointer" @dblclick="editCustomField(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
@@ -50,6 +45,10 @@
<q-td>
{{ capitalize(props.row.type) }}
</q-td>
<!-- hide in ui -->
<q-td>
<q-icon v-if="props.row.hide_in_ui" name="check" />
</q-td>
<!-- default value -->
<q-td v-if="props.row.type === 'checkbox'">
{{ props.row.default_value_bool }}
@@ -101,6 +100,7 @@ export default {
align: "left",
sortable: true,
},
{ name: "hide_in_ui", label: "Hide in UI", field: "hide_in_ui", align: "left", sortable: true },
{ name: "default_value", label: "Default Value", field: "default_value", align: "left", sortable: true },
{ name: "required", label: "Required", field: "required", align: "left", sortable: true },
],
@@ -128,7 +128,7 @@ export default {
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`core/customfields/${field.id}/`)
.delete(`/core/customfields/${field.id}/`)
.then(r => {
this.refresh();
this.$q.loading.hide();

View File

@@ -8,6 +8,7 @@
<q-tab name="smsalerts" label="SMS Alerts" />
<q-tab name="meshcentral" label="MeshCentral" />
<q-tab name="customfields" label="Custom Fields" />
<q-tab name="keystore" label="Key Store" />
</q-tabs>
</template>
<template v-slot:after>
@@ -24,7 +25,9 @@
<div class="text-subtitle2">General</div>
<hr />
<q-card-section class="row">
<q-checkbox v-model="settings.agent_auto_update" label="Enable agent automatic self update" />
<q-checkbox v-model="settings.agent_auto_update" label="Enable agent automatic self update">
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Default agent timezone:</div>
@@ -293,10 +296,14 @@
<q-tab-panel name="customfields">
<CustomFields />
</q-tab-panel>
<q-tab-panel name="keystore">
<KeyStoreTable />
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
<q-card-section class="row items-center">
<q-btn v-show="tab !== 'customfields'" label="Save" color="primary" type="submit" />
<q-btn v-show="tab !== 'customfields' || tab !== 'keystore'" label="Save" color="primary" type="submit" />
<q-btn
v-show="tab === 'emailalerts'"
label="Save and Test"
@@ -316,11 +323,13 @@
import mixins from "@/mixins/mixins";
import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy";
import CustomFields from "@/components/modals/coresettings/CustomFields";
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable";
export default {
name: "EditCoreSettings",
components: {
CustomFields,
KeyStoreTable,
},
mixins: [mixins],
data() {

View File

@@ -0,0 +1,107 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-bar>
{{ title }}
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit="submit">
<!-- name -->
<q-card-section>
<q-input label="Name" outlined dense v-model="localKey.name" :rules="[val => !!val || '*Required']" />
</q-card-section>
<!-- name -->
<q-card-section>
<q-input label="Value" outlined dense v-model="localKey.value" :rules="[val => !!val || '*Required']" />
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Submit" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "KeyStoreForm",
mixins: [mixins],
props: { globalKey: Object },
data() {
return {
localKey: {
name: "",
value: "",
},
};
},
computed: {
title() {
return this.editing ? "Edit Global Key" : "Add Global Key";
},
editing() {
return !!this.globalKey;
},
},
methods: {
submit() {
this.$q.loading.show();
let data = {
...this.localKey,
};
if (this.editing) {
this.$axios
.put(`/core/keystore/${data.id}/`, data)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Key was edited!");
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("There was an error editing the key");
});
} else {
this.$axios
.post("/core/keystore/", data)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Key was added!");
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("There was an error adding the key");
});
}
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
// If pk prop is set that means we are editing
if (this.globalKey) Object.assign(this.localKey, this.globalKey);
},
};
</script>

View File

@@ -0,0 +1,156 @@
<template>
<div>
<div class="row">
<div class="text-subtitle2">Global Key Store</div>
<q-space />
<q-btn size="sm" color="grey-5" icon="fas fa-plus" text-color="black" label="Add key" @click="addKey" />
</div>
<hr />
<q-table
dense
:data="keystore"
:columns="columns"
:pagination.sync="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Keys added yet"
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="editKey(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="editKey(props.row)">
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="deleteKey(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ props.row.name }}
</q-td>
<!-- value -->
<q-td>
{{ props.row.value }}
</q-td>
</q-tr>
</template>
</q-table>
</div>
</template>
<script>
import KeyStoreForm from "@/components/modals/coresettings/KeyStoreForm";
import mixins from "@/mixins/mixins";
export default {
name: "KeyStoreTable",
mixins: [mixins],
data() {
return {
keystore: [],
pagination: {
rowsPerPage: 0,
sortBy: "name",
descending: true,
},
columns: [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{
name: "value",
label: "Value",
field: "value",
align: "left",
sortable: true,
},
],
};
},
methods: {
getKeyStore() {
this.$q.loading.show();
this.$axios
.get("/core/keystore/")
.then(r => {
this.$q.loading.hide();
this.keystore = r.data;
})
.catch(e => {
this.$q.loading.hide();
});
},
addKey() {
this.$q
.dialog({
component: KeyStoreForm,
parent: this,
})
.onOk(() => {
this.getKeyStore();
});
},
editKey(key) {
this.$q
.dialog({
component: KeyStoreForm,
parent: this,
globalKey: key,
})
.onOk(() => {
this.getKeyStore();
});
},
deleteKey(key) {
this.$q
.dialog({
title: `Delete key: ${key.name}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/core/keystore/${key.id}/`)
.then(r => {
this.getKeyStore();
this.$q.loading.hide();
this.notifySuccess(`key: ${key.name} was deleted!`);
})
.catch(error => {
this.$q.loading.hide();
this.notifyError(`An Error occured while deleting key: ${key.name}`);
});
});
},
},
mounted() {
this.getKeyStore();
},
};
</script>

View File

@@ -64,6 +64,28 @@
dense
v-model="autotask.name"
label="Descriptive name of task"
class="q-pb-none"
/>
</q-card-section>
<q-card-section>
<q-checkbox
dense
label="Collector Task"
v-model="collector"
class="q-pb-sm"
@input="autotask.custom_field = null"
/>
<q-select
v-if="collector"
v-model="autotask.custom_field"
:options="customFieldOptions"
dense
label="Custom Field to update"
outlined
map-options
emit-value
options-dense
hint="The last line of script output will be saved to custom field selected"
/>
</q-card-section>
<q-card-section>
@@ -86,6 +108,7 @@
v-model.number="autotask.timeout"
type="number"
label="Maximum permitted execution time (seconds)"
class="q-pb-none"
/>
</q-card-section>
</q-step>
@@ -190,10 +213,13 @@ export default {
return {
step: 1,
scriptOptions: [],
customFieldOptions: [],
collector: false,
autotask: {
script: null,
script_args: [],
assigned_check: null,
custom_field: null,
name: null,
run_time_days: [],
run_time_minute: null,
@@ -319,6 +345,10 @@ export default {
if (this.policypk) {
this.getPolicyChecks();
}
this.getCustomFields("agent").then(r => {
this.customFieldOptions = r.data.map(field => ({ label: field.name, value: field.id }));
});
},
};
</script>

View File

@@ -15,11 +15,12 @@
dense
options-dense
outlined
v-model="localTask.script"
v-model="autotask.script"
:options="scriptOptions"
label="Select script"
map-options
emit-value
@input="setScriptDefaults"
>
<template v-slot:option="scope">
<q-item v-if="!scope.opt.category" v-bind="scope.itemProps" v-on="scope.itemEvents" class="q-pl-lg">
@@ -38,14 +39,13 @@
dense
label="Script Arguments (press Enter after typing each argument)"
filled
v-model="localTask.script_args"
v-model="autotask.script_args"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
@input="setScriptDefaults"
/>
</q-card-section>
<q-card-section>
@@ -53,14 +53,14 @@
:rules="[val => !!val || '*Required']"
outlined
dense
v-model="localTask.name"
v-model="autotask.name"
label="Descriptive name of task"
class="q-pb-none"
/>
</q-card-section>
<q-card-section>
<q-select
v-model="localTask.alert_severity"
v-model="autotask.alert_severity"
:options="severityOptions"
dense
label="Alert Severity"
@@ -70,12 +70,33 @@
options-dense
/>
</q-card-section>
<q-card-section>
<q-checkbox
dense
label="Collector Task"
v-model="collector"
class="q-pb-sm"
@input="autotask.custom_field = null"
/>
<q-select
v-if="collector"
v-model="autotask.custom_field"
:options="customFieldOptions"
dense
label="Custom Field to update"
outlined
map-options
emit-value
options-dense
hint="The return value of script will be saved to custom field selected"
/>
</q-card-section>
<q-card-section>
<q-input
:rules="[val => !!val || '*Required']"
outlined
dense
v-model.number="localTask.timeout"
v-model.number="autotask.timeout"
type="number"
label="Maximum permitted execution time (seconds)"
class="q-pb-none"
@@ -102,14 +123,17 @@ export default {
},
data() {
return {
localTask: {
autotask: {
id: null,
name: "",
script: null,
script_args: [],
alert_severity: null,
timeout: 120,
custom_field: null,
},
collector: false,
customFieldOptions: [],
scriptOptions: [],
severityOptions: [
{ label: "Informational", value: "info" },
@@ -132,7 +156,7 @@ export default {
this.$q.loading.show();
this.$axios
.put(`/tasks/${this.localTask.id}/automatedtasks/`, this.localTask)
.put(`/tasks/${this.autotask.id}/automatedtasks/`, this.autotask)
.then(r => {
this.$q.loading.hide();
this.onOk();
@@ -160,13 +184,20 @@ export default {
mounted() {
this.scriptOptions = this.getScriptOptions(this.showCommunityScripts);
this.getCustomFields("agent").then(r => {
this.customFieldOptions = r.data.map(field => ({ label: field.name, value: field.id }));
});
this.collector = !!this.task.custom_field;
// copy only certain task props locally
this.localTask.id = this.task.id;
this.localTask.name = this.task.name;
this.localTask.script = this.task.script;
this.localTask.script_args = this.task.script_args;
this.localTask.alert_severity = this.task.alert_severity;
this.localTask.timeout = this.task.timeout;
this.autotask.id = this.task.id;
this.autotask.name = this.task.name;
this.autotask.script = this.task.script;
this.autotask.script_args = this.task.script_args;
this.autotask.alert_severity = this.task.alert_severity;
this.autotask.timeout = this.task.timeout;
this.autotask.custom_field = this.task.custom_field;
},
};
</script>

View File

@@ -257,7 +257,8 @@ export default function () {
client: client.id,
server_policy: site.server_policy,
workstation_policy: site.workstation_policy,
alert_template: site.alert_template
alert_template: site.alert_template,
blockInheritance: site.block_policy_inheritance
}
if (site.maintenance_mode) { siteNode["color"] = "green" }
@@ -276,6 +277,7 @@ export default function () {
server_policy: client.server_policy,
workstation_policy: client.workstation_policy,
alert_template: client.alert_template,
blockInheritance: client.block_policy_inheritance,
children: childSites
}