Compare commits

..

62 Commits

Author SHA1 Message Date
wh1te909
8eb91c08aa Release 0.4.26 2021-03-17 17:58:29 +00:00
wh1te909
ded5437522 bump versions 2021-03-17 17:50:56 +00:00
wh1te909
9348657951 fix script manager freezing on latest chrome 2021-03-17 17:36:24 +00:00
wh1te909
bca85933f7 make sure postgres is enabled and running. update npm 2021-03-16 23:09:38 +00:00
Dan
4b84062d62 Merge pull request #329 from silversword411/develop
Adding Bluescreen script
2021-03-16 12:04:37 -07:00
silversword411
d6d0f8fa17 fixed description 2021-03-16 14:16:38 -04:00
silversword411
dd72c875d3 Add Bluescreen script
From dinger1986
2021-03-16 14:13:30 -04:00
wh1te909
1a1df50300 show all severity levels closes #326 2021-03-16 17:54:25 +00:00
wh1te909
53cbb527b4 nats 2.2.0 2021-03-16 17:03:09 +00:00
Dan
8b87b2717e Merge pull request #327 from silversword411/develop
Adding AD Recycle Bin script
2021-03-16 09:50:20 -07:00
silversword411
1007d6dac7 Adding AD Recycle Bin script
Check and Enable AD Recycle Bin
2021-03-16 11:39:44 -04:00
Dan
6799fac120 Merge pull request #325 from silversword411/patch-4
Add Chocolatey Update script to community scripts
2021-03-15 10:35:40 -07:00
silversword411
558e6288ca Merge pull request #1 from silversword411/patch-5
Adding Chocolatey updates to community scripts
2021-03-15 05:10:12 -04:00
silversword411
d9cb73291b Adding Chocolatey updates to community scripts 2021-03-15 05:04:32 -04:00
silversword411
d0f7be3ac3 Create Chocolatey_Update_Installed.bat 2021-03-15 04:57:46 -04:00
wh1te909
331e16d3ca bump mesh closes #323 2021-03-13 23:57:46 +00:00
Dan
0db246c311 Merge pull request #324 from silversword411/patch-3
Avoid multiple update file versions
2021-03-13 14:13:03 -08:00
silversword411
94dc62ff58 Avoid multiple update file versions
Kept getting update.sh.1, update.sh.2 etc with each run and then the auto-pasted command wouldn't be running the latest version of the file.
2021-03-13 12:14:53 -05:00
wh1te909
e68ecf6844 update demo link 2021-03-12 08:22:02 +00:00
Dan
5167b0a8c6 Merge pull request #322 from silversword411/patch-3
Removing extra folder
2021-03-12 00:00:06 -08:00
silversword411
77e3d3786d Removing extra folder 2021-03-11 19:16:27 -05:00
wh1te909
708d4d39bc add test 2021-03-11 19:26:36 +00:00
Dan
2a8cda2a1e Merge pull request #321 from silversword411/patch-3
Updating to match install scripts
2021-03-11 10:47:10 -08:00
silversword411
8d783840ad Updating to match install scripts 2021-03-11 12:02:56 -05:00
wh1te909
abe39d5790 remove checks for older agents 2021-03-11 10:53:27 +00:00
wh1te909
d7868e9e5a Release 0.4.25 2021-03-11 10:11:45 +00:00
wh1te909
7b84e36e15 bump versions 2021-03-11 10:11:13 +00:00
wh1te909
6cab6d69d8 Release 0.4.24 2021-03-11 04:36:34 +00:00
wh1te909
87846d7aef bump versions 2021-03-11 04:36:14 +00:00
wh1te909
2557769c6a fix runchecks wh1te909/rmmagent@739e7434ae 2021-03-11 04:20:18 +00:00
wh1te909
48375f3878 Release 0.4.23 2021-03-11 00:35:02 +00:00
wh1te909
176c85d8c1 bump versions 2021-03-11 00:32:31 +00:00
wh1te909
17cad71ede typo 2021-03-10 22:46:11 +00:00
wh1te909
e8bf9d4e6f change thresholds for check run interval 2021-03-10 22:39:16 +00:00
wh1te909
7bdd2038ef enable django admin during install so that it installs properly, disable it at end of install 2021-03-10 22:32:36 +00:00
wh1te909
e9f6e7943a bump mesh 2021-03-10 19:52:37 +00:00
wh1te909
e74ba387ab update reqs 2021-03-10 19:03:11 +00:00
wh1te909
27c79e5b99 refactor method 2021-03-09 09:39:58 +00:00
wh1te909
8170d5ea73 feat: add client tree sorting closes #316 2021-03-09 03:17:43 +00:00
wh1te909
196f73705d isort 2021-03-09 03:14:56 +00:00
wh1te909
ad0bbf5248 add sorting back to status closes #305 2021-03-08 21:17:26 +00:00
wh1te909
4cae9cd90d add hostname to email subject 2021-03-08 06:58:02 +00:00
wh1te909
be7bc55a76 remove redundant buttons that are already in context menus 2021-03-07 10:21:46 +00:00
wh1te909
684b545e8f exclude date 2021-03-07 10:21:08 +00:00
wh1te909
7835cc3b10 update community scripts 2021-03-06 22:11:58 +00:00
Tragic Bronson
f8706b51e8 Merge pull request #314 from nr-plaxon/patch-3
Adding script to create an all-user logon script
2021-03-06 13:56:32 -08:00
nr-plaxon
d97f8fd5da Adding script to create an all-user logon script 2021-03-06 14:40:53 +01:00
sadnub
f8fa87441e black 2021-03-05 23:32:40 -05:00
sadnub
d42537814a sort of addresses #177. Allow ability to override check intervals 2021-03-05 23:27:54 -05:00
sadnub
792421b0e2 adds #66. EventLog Check: Set the number of event logs found before passing/failing 2021-03-05 21:52:08 -05:00
wh1te909
72d55a010b Release 0.4.22 2021-03-05 23:05:17 +00:00
wh1te909
880d8258ce bump versions 2021-03-05 23:02:08 +00:00
wh1te909
b79bf82efb update docs 2021-03-05 22:22:49 +00:00
wh1te909
b3118b6253 add fields to queryset 2021-03-05 09:30:53 +00:00
sadnub
ba172e2e25 fix issue with exception when other pending actions types exists 2021-03-04 16:31:25 -05:00
sadnub
892d53abeb move alert_template to property on agent versus dynamically generating it everytime 2021-03-04 16:27:05 -05:00
sadnub
5cbaa1ce98 fix tests 2021-03-03 22:25:02 -05:00
sadnub
7b35d9ad2e add policy sync to automation manager 2021-03-03 22:03:11 -05:00
wh1te909
8462de7911 fix wording 2021-03-04 02:20:54 +00:00
wh1te909
8721f44298 fix tests 2021-03-04 01:10:52 +00:00
wh1te909
c7a2d69afa rework agent recovery wh1te909/rmmagent@cef1a0efed 2021-03-04 00:51:03 +00:00
wh1te909
0453d81e7a fix pendingactions count 2021-03-03 11:07:20 +00:00
79 changed files with 26439 additions and 5847 deletions

View File

@@ -8,7 +8,7 @@
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
# [LIVE DEMO](https://rmm.xlawgaming.com/)
# [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.*

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-09 02:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0012_user_agents_per_page'),
]
operations = [
migrations.AddField(
model_name='user',
name='client_tree_sort',
field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
),
]

View File

@@ -15,6 +15,11 @@ AGENT_TBL_TAB_CHOICES = [
("mixed", "Mixed"),
]
CLIENT_TREE_SORT_CHOICES = [
("alphafail", "Move failing clients to the top"),
("alpha", "Sort alphabetically"),
]
class User(AbstractUser, BaseAuditModel):
is_active = models.BooleanField(default=True)
@@ -27,7 +32,10 @@ class User(AbstractUser, BaseAuditModel):
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
)
agents_per_page = models.PositiveIntegerField(default=50)
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
client_tree_sort = models.CharField(
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
)
agent = models.OneToOneField(
"agents.Agent",

View File

@@ -4,6 +4,18 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import User
class UserUISerializer(ModelSerializer):
class Meta:
model = User
fields = [
"dark_mode",
"show_community_scripts",
"agent_dblclick_action",
"default_agent_tbl_tab",
"client_tree_sort",
]
class UserSerializer(ModelSerializer):
class Meta:
model = User

View File

@@ -271,19 +271,13 @@ class TestUserAction(TacticalTestCase):
def test_user_ui(self):
url = "/accounts/users/ui/"
data = {"dark_mode": False}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {"show_community_scripts": True}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {
"userui": True,
"dark_mode": True,
"show_community_scripts": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
"agents_per_page": 1000,
"client_tree_sort": "alpha",
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)

View File

@@ -14,7 +14,15 @@ from logs.models import AuditLog
from tacticalrmm.utils import notify_error
from .models import User
from .serializers import TOTPSetupSerializer, UserSerializer
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
def _is_root_user(request, user) -> bool:
return (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
class CheckCreds(KnoxLoginView):
@@ -105,11 +113,7 @@ class GetUpdateDeleteUser(APIView):
def put(self, request, pk):
user = get_object_or_404(User, pk=pk)
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
serializer = UserSerializer(instance=user, data=request.data, partial=True)
@@ -120,11 +124,7 @@ class GetUpdateDeleteUser(APIView):
def delete(self, request, pk):
user = get_object_or_404(User, pk=pk)
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be deleted from the UI")
user.delete()
@@ -137,11 +137,7 @@ class UserActions(APIView):
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
user.set_password(request.data["password"])
@@ -152,11 +148,7 @@ class UserActions(APIView):
# reset two factor token
def put(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
user.totp_key = ""
@@ -184,23 +176,9 @@ class TOTPSetup(APIView):
class UserUI(APIView):
def patch(self, request):
user = request.user
if "dark_mode" in request.data.keys():
user.dark_mode = request.data["dark_mode"]
user.save(update_fields=["dark_mode"])
if "show_community_scripts" in request.data.keys():
user.show_community_scripts = request.data["show_community_scripts"]
user.save(update_fields=["show_community_scripts"])
if "userui" in request.data.keys():
user.agent_dblclick_action = request.data["agent_dblclick_action"]
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
if "agents_per_page" in request.data.keys():
user.agents_per_page = request.data["agents_per_page"]
user.save(update_fields=["agents_per_page"])
serializer = UserUISerializer(
instance=request.user, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-03-04 03:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0006_auto_20210217_1736'),
('agents', '0030_agent_offline_time'),
]
operations = [
migrations.AddField(
model_name='agent',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
),
]

View File

@@ -20,7 +20,6 @@ from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from packaging import version as pyver
from alerts.models import AlertTemplate
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
@@ -64,6 +63,13 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
site = models.ForeignKey(
"clients.Site",
related_name="agents",
@@ -85,7 +91,7 @@ class Agent(BaseAuditModel):
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
# check if new agent has been create
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
if (
@@ -104,14 +110,6 @@ class Agent(BaseAuditModel):
def client(self):
return self.site.client
@property
def has_nats(self):
return pyver.parse(self.version) >= pyver.parse("1.1.0")
@property
def has_gotasks(self):
return pyver.parse(self.version) >= pyver.parse("1.1.1")
@property
def timezone(self):
# return the default timezone unless the timezone is explicity set per agent
@@ -271,6 +269,20 @@ class Agent(BaseAuditModel):
except:
return ["unknown disk"]
def check_run_interval(self) -> int:
interval = self.check_interval
# determine if any agent checks have a custom interval and set the lowest interval
for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore
if check.run_interval and check.run_interval < interval:
# don't allow check runs less than 15s
if check.run_interval < 15:
interval = 15
else:
interval = check.run_interval
return interval
def run_script(
self,
scriptpk: int,
@@ -461,9 +473,9 @@ class Agent(BaseAuditModel):
)
)
# returns alert template assigned in the following order: policy, site, client, global
# will return None if nothing is found
def get_alert_template(self) -> Union[AlertTemplate, None]:
# sets alert template assigned in the following order: policy, site, client, global
# sets None if nothing is found
def set_alert_template(self):
site = self.site
client = self.client
@@ -563,9 +575,16 @@ class Agent(BaseAuditModel):
continue
else:
# save alert_template to agent cache field
self.alert_template = template
self.save()
return template
# no alert templates found or agent has been excluded
self.alert_template = None
self.save()
return None
def generate_checks_from_policies(self):
@@ -703,11 +722,11 @@ class Agent(BaseAuditModel):
# 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.exclude(status="completed"): # type: ignore
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):
def should_create_alert(self, alert_template=None):
return (
self.overdue_dashboard_alert
or self.overdue_email_alert
@@ -726,7 +745,6 @@ class Agent(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail(
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
(
@@ -735,14 +753,13 @@ class Agent(BaseAuditModel):
f"agent {self.hostname} "
"within the expected time."
),
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_recovery_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail(
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
(
@@ -751,27 +768,25 @@ class Agent(BaseAuditModel):
f"agent {self.hostname} "
"after an interruption in data transmission."
),
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_outage_sms(self):
from core.models import CoreSettings
alert_template = self.get_alert_template()
CORE = CoreSettings.objects.first()
CORE.send_sms(
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_recovery_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_sms(
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=alert_template,
alert_template=self.alert_template,
)

View File

@@ -57,16 +57,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
alert_template = obj.get_alert_template()
if not alert_template:
if not obj.alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.agent_always_email,
"always_text": alert_template.agent_always_text,
"always_alert": alert_template.agent_always_alert,
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}
def get_pending_actions(self, obj):

View File

@@ -200,20 +200,6 @@ def agent_outages_task() -> None:
Alert.handle_alert_failure(agent)
@app.task
def handle_agent_recovery_task(pk: int) -> None:
sleep(10)
from agents.models import RecoveryAction
action = RecoveryAction.objects.get(pk=pk)
if action.mode == "command":
data = {"func": "recoverycmd", "recoverycommand": action.command}
else:
data = {"func": "recover", "payload": {"mode": action.mode}}
asyncio.run(action.agent.nats_cmd(data, wait=False))
@app.task
def run_script_email_results_task(
agentpk: int,

View File

@@ -198,11 +198,6 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_get_processes(self, mock_ret):
agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
url_old = f"/agents/{agent_old.pk}/getprocs/"
r = self.client.get(url_old)
self.assertEqual(r.status_code, 400)
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
url = f"/agents/{agent.pk}/getprocs/"
@@ -340,6 +335,7 @@ class TestAgentViews(TacticalTestCase):
"func": "schedtask",
"schedtaskpayload": {
"type": "schedreboot",
"deleteafter": True,
"trigger": "once",
"name": r.data["task_name"], # type: ignore
"year": 2025,
@@ -421,42 +417,69 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url)
def test_recover(self):
@patch("agents.models.Agent.nats_cmd")
def test_recover(self, nats_cmd):
from agents.models import RecoveryAction
self.agent.version = "0.11.1"
self.agent.save(update_fields=["version"])
RecoveryAction.objects.all().delete()
url = "/agents/recover/"
data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
agent = baker.make_recipe("agents.online_agent")
# test mesh realtime
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
)
nats_cmd.reset_mock()
data["mode"] = "mesh"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("pending", r.json())
RecoveryAction.objects.all().delete()
data["mode"] = "command"
data["cmd"] = "ipconfig /flushdns"
# test mesh with agent rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
RecoveryAction.objects.all().delete()
data["cmd"] = None
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 1)
mesh_recovery = RecoveryAction.objects.first()
self.assertEqual(mesh_recovery.mode, "mesh")
nats_cmd.reset_mock()
RecoveryAction.objects.all().delete()
self.agent.version = "0.9.4"
self.agent.save(update_fields=["version"])
data["mode"] = "mesh"
# test tacagent realtime
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
)
nats_cmd.reset_mock()
# test tacagent with rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("0.9.5", r.json())
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.reset_mock()
self.check_not_authenticated("post", url)
# test shell cmd without command
data = {"pk": agent.pk, "cmd": None, "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 0)
# test shell cmd
data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 1)
cmd_recovery = RecoveryAction.objects.first()
self.assertEqual(cmd_recovery.mode, "command")
self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
def test_agents_agent_detail(self):
url = f"/agents/{self.agent.pk}/agentdetail/"

View File

@@ -69,10 +69,9 @@ def update_agents(request):
def ping(request, pk):
agent = get_object_or_404(Agent, pk=pk)
status = "offline"
if agent.has_nats:
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
return Response({"name": agent.hostname, "status": status})
@@ -80,8 +79,7 @@ def ping(request, pk):
@api_view(["DELETE"])
def uninstall(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
name = agent.hostname
agent.delete()
@@ -147,9 +145,6 @@ def agent_detail(request, pk):
@api_view()
def get_processes(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if pyver.parse(agent.version) < pyver.parse("1.2.0"):
return notify_error("Requires agent version 1.2.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout":
return notify_error("Unable to contact the agent")
@@ -159,9 +154,6 @@ def get_processes(request, pk):
@api_view()
def kill_proc(request, pk, pid):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
)
@@ -177,8 +169,6 @@ def kill_proc(request, pk, pid):
@api_view()
def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = 180 if logtype == "Security" else 30
data = {
"func": "eventlog",
@@ -198,8 +188,6 @@ def get_event_log(request, pk, logtype, days):
@api_view(["POST"])
def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = int(request.data["timeout"])
data = {
"func": "rawcmd",
@@ -228,26 +216,28 @@ class AgentsTableList(APIView):
def patch(self, request):
if "sitePK" in request.data.keys():
queryset = (
Agent.objects.select_related("site")
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site_id=request.data["sitePK"])
)
elif "clientPK" in request.data.keys():
queryset = (
Agent.objects.select_related("site")
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site__client_id=request.data["clientPK"])
)
else:
queryset = Agent.objects.select_related("site").prefetch_related(
"agentchecks"
)
queryset = Agent.objects.select_related(
"site", "policy", "alert_template"
).prefetch_related("agentchecks")
queryset = queryset.only(
"pk",
"hostname",
"agent_id",
"site",
"policy",
"alert_template",
"monitoring_type",
"description",
"needs_reboot",
@@ -294,9 +284,6 @@ class Reboot(APIView):
# reboot now
def post(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")
@@ -306,8 +293,6 @@ class Reboot(APIView):
# reboot later
def patch(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
try:
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
@@ -322,6 +307,7 @@ class Reboot(APIView):
"func": "schedtask",
"schedtaskpayload": {
"type": "schedreboot",
"deleteafter": True,
"trigger": "once",
"name": task_name,
"year": int(dt.datetime.strftime(obj, "%Y")),
@@ -332,9 +318,6 @@ class Reboot(APIView):
},
}
if pyver.parse(agent.version) >= pyver.parse("1.1.2"):
nats_data["schedtaskpayload"]["deleteafter"] = True
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
return notify_error(r)
@@ -487,20 +470,12 @@ def recover(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
mode = request.data["mode"]
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
return notify_error("Only available in agent version greater than 0.9.5")
if not agent.has_nats:
if mode == "tacagent" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater")
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats:
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
# attempt a realtime recovery, otherwise fall back to old recovery method
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
return notify_error(
@@ -567,9 +542,6 @@ def run_script(request):
@api_view()
def recover_mesh(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {"func": "recover", "payload": {"mode": "mesh"}}
r = asyncio.run(agent.nats_cmd(data, timeout=45))
if r != "ok":
@@ -749,9 +721,6 @@ def agent_maintenance(request):
class WMI(APIView):
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
if pyver.parse(agent.version) < pyver.parse("1.1.2"):
return notify_error("Requires agent version 1.1.2 or greater")
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
if r != "ok":
return notify_error("Unable to contact the agent")

View File

@@ -158,7 +158,7 @@ class Alert(models.Model):
email_alert = instance.overdue_email_alert
text_alert = instance.overdue_text_alert
dashboard_alert = instance.overdue_dashboard_alert
alert_template = instance.get_alert_template()
alert_template = instance.alert_template
maintenance_mode = instance.maintenance_mode
alert_severity = "error"
agent = instance
@@ -194,7 +194,7 @@ class Alert(models.Model):
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
@@ -227,7 +227,7 @@ class Alert(models.Model):
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
@@ -336,7 +336,7 @@ class Alert(models.Model):
resolved_email_task = agent_recovery_email_task
resolved_text_task = agent_recovery_sms_task
alert_template = instance.get_alert_template()
alert_template = instance.alert_template
alert = cls.objects.get(agent=instance, resolved=False)
maintenance_mode = instance.maintenance_mode
agent = instance
@@ -354,7 +354,7 @@ class Alert(models.Model):
resolved_email_task = handle_resolved_check_email_alert_task
resolved_text_task = handle_resolved_check_sms_alert_task
alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_check=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
@@ -372,7 +372,7 @@ class Alert(models.Model):
resolved_email_task = handle_resolved_task_email_alert
resolved_text_task = handle_resolved_task_sms_alert
alert_template = instance.agent.get_alert_template()
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_task=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent

View File

@@ -12,3 +12,13 @@ def unsnooze_alerts() -> str:
)
return "ok"
@app.task
def cache_agents_alert_template():
from agents.models import Agent
for agent in Agent.objects.only("pk"):
agent.set_alert_template()
return "ok"

View File

@@ -5,6 +5,8 @@ from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker, seq
from alerts.tasks import cache_agents_alert_template
from autotasks.models import AutomatedTask
from core.models import CoreSettings
from tacticalrmm.test import TacticalTestCase
@@ -395,8 +397,8 @@ class TestAlertTasks(TacticalTestCase):
alert_templates = baker.make("alerts.AlertTemplate", _quantity=6)
# should be None
self.assertFalse(workstation.get_alert_template())
self.assertFalse(server.get_alert_template())
self.assertFalse(workstation.set_alert_template())
self.assertFalse(server.set_alert_template())
# assign first Alert Template as to a policy and apply it as default
policy.alert_template = alert_templates[0] # type: ignore
@@ -405,15 +407,15 @@ class TestAlertTasks(TacticalTestCase):
core.server_policy = policy
core.save()
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
# assign second Alert Template to as default alert template
core.alert_template = alert_templates[1] # type: ignore
core.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk) # type: ignore
# assign third Alert Template to client
workstation.client.alert_template = alert_templates[2] # type: ignore
@@ -421,8 +423,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.client.save()
server.client.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) # type: ignore
# apply policy to client and should override
workstation.client.workstation_policy = policy
@@ -430,8 +432,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.client.save()
server.client.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
# assign fouth Alert Template to site
workstation.site.alert_template = alert_templates[3] # type: ignore
@@ -439,8 +441,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.site.save()
server.site.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
# apply policy to site
workstation.site.workstation_policy = policy
@@ -448,8 +450,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.site.save()
server.site.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
# apply policy to agents
workstation.policy = policy
@@ -457,35 +459,35 @@ class TestAlertTasks(TacticalTestCase):
workstation.save()
server.save()
self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
# test disabling alert template
alert_templates[0].is_active = False # type: ignore
alert_templates[0].save() # type: ignore
self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
# test policy exclusions
alert_templates[3].excluded_agents.set([workstation.pk]) # type: ignore
self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
# test workstation exclusions
alert_templates[2].exclude_workstations = True # type: ignore
alert_templates[2].save() # type: ignore
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore
# test server exclusions
alert_templates[3].exclude_servers = True # type: ignore
alert_templates[3].save() # type: ignore
self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) # type: ignore
@patch("agents.tasks.sleep")
@patch("core.models.CoreSettings.send_mail")
@@ -504,6 +506,7 @@ class TestAlertTasks(TacticalTestCase):
send_email,
sleep,
):
from agents.models import Agent
from agents.tasks import (
agent_outage_email_task,
agent_outage_sms_task,
@@ -564,6 +567,8 @@ class TestAlertTasks(TacticalTestCase):
agent_email_alert = baker.make_recipe(
"agents.overdue_agent", overdue_email_alert=True
)
cache_agents_alert_template()
agent_outages_task()
# should have created 6 alerts
@@ -663,6 +668,9 @@ class TestAlertTasks(TacticalTestCase):
alert_template_always_text.agent_text_on_resolved = True # type: ignore
alert_template_always_text.save() # type: ignore
agent_template_text = Agent.objects.get(pk=agent_template_text.pk)
agent_template_email = Agent.objects.get(pk=agent_template_email.pk)
# have the two agents checkin
url = "/api/v3/checkin/"
@@ -719,7 +727,8 @@ class TestAlertTasks(TacticalTestCase):
send_email,
sleep,
):
from alerts.tasks import cache_agents_alert_template
from checks.models import Check
from checks.tasks import (
handle_check_email_alert_task,
handle_check_sms_alert_task,
@@ -800,6 +809,14 @@ class TestAlertTasks(TacticalTestCase):
"checks.script_check", agent=agent_no_settings
)
# update alert template and pull new checks from DB
cache_agents_alert_template()
check_template_email = Check.objects.get(pk=check_template_email.pk)
check_template_dashboard_text = Check.objects.get(
pk=check_template_dashboard_text.pk
)
check_template_blank = Check.objects.get(pk=check_template_blank.pk)
# test agent with check that has alert settings
check_agent.alert_severity = "warning"
check_agent.status = "failing"
@@ -905,11 +922,11 @@ class TestAlertTasks(TacticalTestCase):
Alert.objects.filter(assigned_check=check_template_email).count(), 1
)
alert_template_email.check_periodic_alert_days = 1
alert_template_email.save()
alert_template_email.check_periodic_alert_days = 1 # type: ignore
alert_template_email.save() # type: ignore
alert_template_dashboard_text.check_periodic_alert_days = 1
alert_template_dashboard_text.save()
alert_template_dashboard_text.check_periodic_alert_days = 1 # type: ignore
alert_template_dashboard_text.save() # type: ignore
# set last email time for alert in the past
alert_email = Alert.objects.get(assigned_check=check_template_email)
@@ -921,6 +938,13 @@ class TestAlertTasks(TacticalTestCase):
alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20)
alert_sms.save()
# refresh checks to get alert template changes
check_template_email = Check.objects.get(pk=check_template_email.pk)
check_template_dashboard_text = Check.objects.get(
pk=check_template_dashboard_text.pk
)
check_template_blank = Check.objects.get(pk=check_template_blank.pk)
Alert.handle_alert_failure(check_template_email)
Alert.handle_alert_failure(check_template_dashboard_text)
@@ -939,11 +963,18 @@ class TestAlertTasks(TacticalTestCase):
resolved_email.assert_not_called()
# test resolved notifications
alert_template_email.check_email_on_resolved = True
alert_template_email.save()
alert_template_email.check_email_on_resolved = True # type: ignore
alert_template_email.save() # type: ignore
alert_template_dashboard_text.check_text_on_resolved = True
alert_template_dashboard_text.save()
alert_template_dashboard_text.check_text_on_resolved = True # type: ignore
alert_template_dashboard_text.save() # type: ignore
# refresh checks to get alert template changes
check_template_email = Check.objects.get(pk=check_template_email.pk)
check_template_dashboard_text = Check.objects.get(
pk=check_template_dashboard_text.pk
)
check_template_blank = Check.objects.get(pk=check_template_blank.pk)
Alert.handle_alert_resolve(check_template_email)
@@ -980,7 +1011,8 @@ class TestAlertTasks(TacticalTestCase):
send_email,
sleep,
):
from alerts.tasks import cache_agents_alert_template
from autotasks.models import AutomatedTask
from autotasks.tasks import (
handle_resolved_task_email_alert,
handle_resolved_task_sms_alert,
@@ -1052,8 +1084,14 @@ class TestAlertTasks(TacticalTestCase):
"autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning"
)
# update alert template and pull new checks from DB
cache_agents_alert_template()
task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore
task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore
task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore
# test agent with task that has alert settings
Alert.handle_alert_failure(task_agent)
Alert.handle_alert_failure(task_agent) # type: ignore
# alert should have been created and sms, email notifications sent
self.assertTrue(Alert.objects.filter(assigned_task=task_agent).exists())
@@ -1081,7 +1119,7 @@ class TestAlertTasks(TacticalTestCase):
send_sms.reset_mock()
# test task with an agent that has an email always alert template
Alert.handle_alert_failure(task_template_email)
Alert.handle_alert_failure(task_template_email) # type: ignore
self.assertTrue(Alert.objects.filter(assigned_task=task_template_email))
alertpk = Alert.objects.get(assigned_task=task_template_email).pk
@@ -1099,7 +1137,7 @@ class TestAlertTasks(TacticalTestCase):
send_email.reset_mock()
# test task with an agent that has an email always alert template
Alert.handle_alert_failure(task_template_dashboard_text)
Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore
self.assertTrue(
Alert.objects.filter(assigned_task=task_template_dashboard_text).exists()
@@ -1110,11 +1148,11 @@ class TestAlertTasks(TacticalTestCase):
outage_sms.assert_not_called
# update task alert seveity to error
task_template_dashboard_text.alert_severity = "error"
task_template_dashboard_text.save()
task_template_dashboard_text.alert_severity = "error" # type: ignore
task_template_dashboard_text.save() # type: ignore
# now should trigger alert
Alert.handle_alert_failure(task_template_dashboard_text)
Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore
outage_sms.assert_called_with(pk=alertpk, alert_interval=0)
outage_sms.reset_mock()
@@ -1130,21 +1168,21 @@ class TestAlertTasks(TacticalTestCase):
send_sms.reset_mock()
# test task with an agent that has a blank alert template
Alert.handle_alert_failure(task_template_blank)
Alert.handle_alert_failure(task_template_blank) # type: ignore
self.assertFalse(
Alert.objects.filter(assigned_task=task_template_blank).exists()
)
# test task that has no template and no settings
Alert.handle_alert_failure(task_no_settings)
Alert.handle_alert_failure(task_no_settings) # type: ignore
self.assertFalse(Alert.objects.filter(assigned_task=task_no_settings).exists())
# test periodic notifications
# make sure a failing task won't trigger another notification and only create a single alert
Alert.handle_alert_failure(task_template_email)
Alert.handle_alert_failure(task_template_email) # type: ignore
send_email.assert_not_called()
send_sms.assert_not_called()
@@ -1152,11 +1190,11 @@ class TestAlertTasks(TacticalTestCase):
Alert.objects.filter(assigned_task=task_template_email).count(), 1
)
alert_template_email.task_periodic_alert_days = 1
alert_template_email.save()
alert_template_email.task_periodic_alert_days = 1 # type: ignore
alert_template_email.save() # type: ignore
alert_template_dashboard_text.task_periodic_alert_days = 1
alert_template_dashboard_text.save()
alert_template_dashboard_text.task_periodic_alert_days = 1 # type: ignore
alert_template_dashboard_text.save() # type: ignore
# set last email time for alert in the past
alert_email = Alert.objects.get(assigned_task=task_template_email)
@@ -1168,8 +1206,13 @@ class TestAlertTasks(TacticalTestCase):
alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20)
alert_sms.save()
Alert.handle_alert_failure(task_template_email)
Alert.handle_alert_failure(task_template_dashboard_text)
# refresh automated tasks to get new alert templates
task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore
task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore
task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore
Alert.handle_alert_failure(task_template_email) # type: ignore
Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore
outage_email.assert_called_with(pk=alert_email.pk, alert_interval=1)
outage_sms.assert_called_with(pk=alert_sms.pk, alert_interval=1)
@@ -1177,7 +1220,7 @@ class TestAlertTasks(TacticalTestCase):
outage_sms.reset_mock()
# test resolving alerts
Alert.handle_alert_resolve(task_agent)
Alert.handle_alert_resolve(task_agent) # type: ignore
self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved)
self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved_on)
@@ -1186,19 +1229,24 @@ class TestAlertTasks(TacticalTestCase):
resolved_email.assert_not_called()
# test resolved notifications
alert_template_email.task_email_on_resolved = True
alert_template_email.save()
alert_template_email.task_email_on_resolved = True # type: ignore
alert_template_email.save() # type: ignore
alert_template_dashboard_text.task_text_on_resolved = True
alert_template_dashboard_text.save()
alert_template_dashboard_text.task_text_on_resolved = True # type: ignore
alert_template_dashboard_text.save() # type: ignore
Alert.handle_alert_resolve(task_template_email)
# refresh automated tasks to get new alert templates
task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore
task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore
task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore
Alert.handle_alert_resolve(task_template_email) # type: ignore
resolved_email.assert_called_with(pk=alert_email.pk)
resolved_sms.assert_not_called()
resolved_email.reset_mock()
Alert.handle_alert_resolve(task_template_dashboard_text)
Alert.handle_alert_resolve(task_template_dashboard_text) # type: ignore
resolved_sms.assert_called_with(pk=alert_sms.pk)
resolved_email.assert_not_called()
@@ -1276,6 +1324,8 @@ class TestAlertTasks(TacticalTestCase):
agent.client.alert_template = alert_template
agent.client.save()
agent.set_alert_template()
agent_outages_task()
# this is what data should be

View File

@@ -14,6 +14,7 @@ from .serializers import (
AlertTemplateRelationSerializer,
AlertTemplateSerializer,
)
from .tasks import cache_agents_alert_template
class GetAddAlerts(APIView):
@@ -194,6 +195,9 @@ class GetAddAlertTemplates(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")
@@ -212,11 +216,17 @@ class GetUpdateDeleteAlertTemplate(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(AlertTemplate, pk=pk).delete()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")

View File

@@ -1,9 +1,9 @@
import json
import os
from itertools import cycle
from unittest.mock import patch
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
@@ -18,8 +18,44 @@ class TestAPIv3(TacticalTestCase):
def test_get_checks(self):
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
# add a check
check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
self.assertEqual(len(r.data["checks"]), 1) # type: ignore
# override check run interval
check2 = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=20
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEqual(len(r.data["checks"]), 2) # type: ignore
# Set last_run on both checks and should return an empty list
check1.last_run = djangotime.now()
check1.save()
check2.last_run = djangotime.now()
check2.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertFalse(r.data["checks"]) # type: ignore
# set last_run greater than interval
check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check1.save()
check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check2.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEquals(len(r.data["checks"]), 2) # type: ignore
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
r = self.client.get(url)
@@ -54,6 +90,45 @@ class TestAPIv3(TacticalTestCase):
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
)
# add check to agent with check interval set
check = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=30
)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": 30},
)
# minimum check run interval is 15 seconds
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": 15},
)
def test_run_checks(self):
# force run all checks regardless of interval
agent = baker.make_recipe("agents.online_agent")
baker.make_recipe("checks.ping_check", agent=agent)
baker.make_recipe("checks.diskspace_check", agent=agent)
baker.make_recipe("checks.cpuload_check", agent=agent)
baker.make_recipe("checks.memory_check", agent=agent)
baker.make_recipe("checks.eventlog_check", agent=agent)
for _ in range(10):
baker.make_recipe("checks.script_check", agent=agent)
url = f"/api/v3/{agent.agent_id}/runchecks/"
r = self.client.get(url)
self.assertEqual(r.json()["agent"], agent.pk)
self.assertIsInstance(r.json()["check_interval"], int)
self.assertEqual(len(r.json()["checks"]), 15)
def test_checkin_patch(self):
from logs.models import PendingAction
@@ -89,3 +164,42 @@ class TestAPIv3(TacticalTestCase):
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "completed")
action.delete()
@patch("apiv3.views.reload_nats")
def test_agent_recovery(self, reload_nats):
reload_nats.return_value = "ok"
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
self.assertEqual(r.status_code, 404)
agent = baker.make_recipe("agents.online_agent")
url = f"/api/v3/{agent.agent_id}/recovery/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make(
"agents.RecoveryAction",
agent=agent,
mode="command",
command="shutdown /r /t 5 /f",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
)
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
reload_nats.assert_called_once()

View File

@@ -5,6 +5,7 @@ from . import views
urlpatterns = [
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/runchecks/", views.RunChecks.as_view()),
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("meshexe/", views.MeshExe.as_view()),
@@ -18,4 +19,5 @@ urlpatterns = [
path("winupdates/", views.WinUpdates.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
]

View File

@@ -65,13 +65,6 @@ class CheckIn(APIView):
if Alert.objects.filter(agent=agent, resolved=False).exists():
Alert.handle_alert_resolve(agent)
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
handle_agent_recovery_task.delay(pk=recovery.pk) # type: ignore
return Response("ok")
# get any pending actions
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
agent.handle_pending_actions()
@@ -267,14 +260,13 @@ class SupersededWinUpdate(APIView):
return Response("ok")
class CheckRunner(APIView):
class RunChecks(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
@@ -282,6 +274,42 @@ class CheckRunner(APIView):
}
return Response(ret)
class CheckRunner(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore
run_list = [
check
for check in checks
# always run if check hasn't run yet
if not check.last_run
# if a check interval is set, see if the correct amount of seconds have passed
or (
check.run_interval
and (
check.last_run
< djangotime.now()
- djangotime.timedelta(seconds=check.run_interval)
)
# if check interval isn't set, make sure the agent's check interval has passed before running
)
or (
check.last_run
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
)
]
ret = {
"agent": agent.pk,
"check_interval": agent.check_run_interval(),
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
}
return Response(ret)
def patch(self, request):
check = get_object_or_404(Check, pk=request.data["id"])
check.last_run = djangotime.now()
@@ -297,7 +325,10 @@ class CheckRunnerInterval(APIView):
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
return Response(
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
)
class TaskRunner(APIView):
@@ -513,3 +544,27 @@ class ChocoResult(APIView):
action.status = "completed"
action.save(update_fields=["details", "status"])
return Response("ok")
class AgentRecovery(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
ret = {"mode": "pass", "shellcmd": ""}
if recovery is None:
return Response(ret)
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
ret["mode"] = recovery.mode
if recovery.mode == "command":
ret["shellcmd"] = recovery.command
elif recovery.mode == "rpc":
reload_nats()
return Response(ret)

View File

@@ -28,6 +28,7 @@ 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
# get old policy if exists
@@ -42,6 +43,9 @@ class Policy(BaseAuditModel):
create_tasks=True,
)
if old_policy.alert_template != self.alert_template:
cache_agents_alert_template.delay()
def delete(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task

View File

@@ -79,6 +79,7 @@ def update_policy_check_fields_task(checkpk):
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,
@@ -98,6 +99,7 @@ def update_policy_check_fields_task(checkpk):
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,

View File

@@ -426,6 +426,25 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_sync_policy(self, generate_checks):
url = "/automation/sync/"
# test invalid data
data = {"invalid": 7}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
policy = baker.make("automation.Policy", active=True)
data = {"policy": policy.pk} # type: ignore
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
self.check_not_authenticated("post", url)
class TestPolicyTasks(TacticalTestCase):
def setUp(self):

View File

@@ -7,6 +7,7 @@ urlpatterns = [
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
path("policies/overview/", views.OverviewPolicy.as_view()),
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("sync/", views.PolicySync.as_view()),
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),

View File

@@ -8,6 +8,7 @@ from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from tacticalrmm.utils import notify_error
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
@@ -72,6 +73,20 @@ class GetUpdateDeletePolicy(APIView):
return Response("ok")
class PolicySync(APIView):
def post(self, request):
if "policy" in request.data.keys():
from automation.tasks import generate_agent_checks_from_policies_task
generate_agent_checks_from_policies_task.delay(
request.data["policy"], create_tasks=True
)
return Response("ok")
else:
return notify_error("The request was invalid")
class PolicyAutoTask(APIView):
# tasks associated with policy

View File

@@ -223,7 +223,7 @@ class AutomatedTask(BaseAuditModel):
create_win_task_schedule.delay(task.pk)
def should_create_alert(self, alert_template):
def should_create_alert(self, alert_template=None):
return (
self.dashboard_alert
or self.email_alert
@@ -242,7 +242,6 @@ class AutomatedTask(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -254,14 +253,13 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template)
CORE.send_mail(subject, body, self.agent.alert_template)
def send_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -273,13 +271,11 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)
def send_resolved_email(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
@@ -287,16 +283,15 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_resolved_sms(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)

View File

@@ -18,7 +18,7 @@ class TaskSerializer(serializers.ModelSerializer):
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
alert_template = obj.agent.alert_template
else:
alert_template = None

View File

@@ -29,7 +29,6 @@ class TestAutotaskViews(TacticalTestCase):
agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
old_agent = baker.make_recipe("agents.agent", version="1.1.0")
# test script set to invalid pk
data = {"autotask": {"script": 500}}
@@ -52,15 +51,6 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 404)
# test old agent version
data = {
"autotask": {"script": script.id},
"agent": old_agent.id,
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test add task to agent
data = {
"autotask": {
@@ -203,13 +193,6 @@ class TestAutotaskViews(TacticalTestCase):
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
nats_cmd.reset_mock()
old_agent = baker.make_recipe("agents.agent", version="1.0.2")
task2 = baker.make("autotasks.AutomatedTask", agent=old_agent)
url = f"/tasks/runwintask/{task2.id}/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
nats_cmd.assert_not_called()
self.check_not_authenticated("get", url)

View File

@@ -34,9 +34,6 @@ class AddAutoTask(APIView):
parent = {"policy": policy}
else:
agent = get_object_or_404(Agent, pk=data["agent"])
if not agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
parent = {"agent": agent}
check = None
@@ -128,8 +125,5 @@ class AutoTask(APIView):
@api_view()
def run_task(request, pk):
task = get_object_or_404(AutomatedTask, pk=pk)
if not task.agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
return Response(f"{task.name} will now be run on {task.agent.hostname}")

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-06 02:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0021_auto_20210212_1429'),
]
operations = [
migrations.AddField(
model_name='check',
name='number_of_events_b4_alert',
field=models.PositiveIntegerField(blank=True, default=1, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-06 02:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0022_check_number_of_events_b4_alert'),
]
operations = [
migrations.AddField(
model_name='check',
name='run_interval',
field=models.PositiveIntegerField(blank=True, default=0),
),
]

View File

@@ -93,6 +93,7 @@ class Check(BaseAuditModel):
fail_count = models.PositiveIntegerField(default=0)
outage_history = models.JSONField(null=True, blank=True) # store
extra_details = models.JSONField(null=True, blank=True)
run_interval = models.PositiveIntegerField(blank=True, default=0)
# check specific fields
# for eventlog, script, ip, and service alert severity
@@ -181,6 +182,9 @@ class Check(BaseAuditModel):
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
)
search_last_days = models.PositiveIntegerField(null=True, blank=True)
number_of_events_b4_alert = models.PositiveIntegerField(
null=True, blank=True, default=1
)
def __str__(self):
if self.agent:
@@ -259,7 +263,7 @@ class Check(BaseAuditModel):
"modified_time",
]
def should_create_alert(self, alert_template):
def should_create_alert(self, alert_template=None):
return (
self.dashboard_alert
@@ -488,13 +492,13 @@ class Check(BaseAuditModel):
log.append(i)
if self.fail_when == "contains":
if log:
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "failing"
else:
self.status = "passing"
elif self.fail_when == "not_contains":
if log:
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "passing"
else:
self.status = "failing"
@@ -563,6 +567,7 @@ class Check(BaseAuditModel):
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,
@@ -586,6 +591,7 @@ class Check(BaseAuditModel):
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,
)
def is_duplicate(self, check):
@@ -613,11 +619,10 @@ class Check(BaseAuditModel):
def send_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
else:
subject = f"{self} Failed"
@@ -695,12 +700,11 @@ class Check(BaseAuditModel):
except:
continue
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent:
@@ -744,21 +748,21 @@ class Check(BaseAuditModel):
elif self.check_type == "eventlog":
body = subject
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)
def send_resolved_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = f"{self} is now back to normal"
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_resolved_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
CORE.send_sms(subject, alert_template=alert_template)
CORE.send_sms(subject, alert_template=self.agent.alert_template)
class CheckHistory(models.Model):

View File

@@ -25,7 +25,7 @@ class CheckSerializer(serializers.ModelSerializer):
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
alert_template = obj.agent.alert_template
else:
alert_template = None

View File

@@ -310,14 +310,8 @@ class TestCheckViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_run_checks(self, nats_cmd):
agent = baker.make_recipe("agents.agent", version="1.4.1")
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
url = f"/checks/runchecks/{agent_old.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
url = f"/checks/runchecks/{agent_b4_141.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@@ -1003,6 +997,12 @@ class TestCheckTasks(TacticalTestCase):
"source": "source",
"message": "a test message",
},
{
"eventType": "error",
"eventID": 123,
"source": "source",
"message": "a test message",
},
],
}
@@ -1107,3 +1107,61 @@ class TestCheckTasks(TacticalTestCase):
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test multiple events found and contains
# this should pass since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "contains"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
# test not contains
# this should fail since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "not_contains"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")

View File

@@ -161,8 +161,6 @@ class CheckHistory(APIView):
@api_view()
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))

View File

@@ -32,6 +32,7 @@ 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
# get old client if exists
@@ -54,6 +55,9 @@ class Client(BaseAuditModel):
create_tasks=True,
)
if old_client and old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -127,6 +131,7 @@ 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
# get old client if exists
@@ -149,6 +154,9 @@ class Site(BaseAuditModel):
create_tasks=True,
)
if old_site and old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)

View File

@@ -78,6 +78,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
if not self.pk and CoreSettings.objects.exists():
@@ -105,6 +106,9 @@ class CoreSettings(BaseAuditModel):
mon_type="workstation", create_tasks=True
)
if old_settings and old_settings.alert_template != self.alert_template:
cache_agents_alert_template.delay()
def __str__(self):
return "Global Site Settings"

View File

@@ -63,7 +63,7 @@ def dashboard_info(request):
"show_community_scripts": request.user.show_community_scripts,
"dbl_click_action": request.user.agent_dblclick_action,
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
"agents_per_page": request.user.agents_per_page,
"client_tree_sort": request.user.client_tree_sort,
}
)

View File

@@ -250,8 +250,10 @@ 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" or self.action_type == "agentupdate":
elif self.action_type == "taskaction":
return "Next agent check-in"
elif self.action_type == "agentupdate":
return "Next update cycle"
elif self.action_type == "chocoinstall":
return "ASAP"

View File

@@ -218,8 +218,8 @@ class TestAuditViews(TacticalTestCase):
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
self.assertEqual(r.data["completed_count"], 26) # type: ignore
self.assertEqual(r.data["total"], 26) # type: ignore
self.assertEqual(r.data["completed_count"], 12) # type: ignore
self.assertEqual(r.data["total"], 12) # type: ignore
self.check_not_authenticated("patch", url)

View File

@@ -113,16 +113,24 @@ class PendingActions(APIView):
actions = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status=status_filter
)
total = PendingAction.objects.filter(
agent__pk=request.data["agentPK"]
).count()
completed = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status="completed"
).count()
else:
actions = PendingAction.objects.filter(status=status_filter).select_related(
"agent"
)
total = PendingAction.objects.count()
completed = PendingAction.objects.filter(status="completed").count()
ret = {
"actions": PendingActionSerializer(actions, many=True).data,
"completed_count": PendingAction.objects.filter(status="completed").count(),
"total": PendingAction.objects.count(),
"completed_count": completed,
"total": total,
}
return Response(ret)

View File

@@ -16,7 +16,7 @@ future==0.18.2
kombu==5.0.2
loguru==0.5.3
msgpack==1.0.2
packaging==20.8
packaging==20.9
psycopg2-binary==2.8.6
pycparser==2.20
pycryptodome==3.10.1
@@ -28,10 +28,10 @@ redis==3.5.3
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
twilio==6.52.0
twilio==6.53.0
urllib3==1.26.3
uWSGI==2.0.19.1
validators==0.18.2
vine==5.0.0
websockets==8.1
zipp==3.4.0
zipp==3.4.1

View File

@@ -208,5 +208,33 @@
"name": "Verify Antivirus Status",
"description": "Verify and display status for all installed Antiviruses",
"shell": "powershell"
},
{
"filename": "CreateAllUserLogonScript.ps1",
"submittedBy": "https://github.com/nr-plaxon",
"name": "Create User Logon Script",
"description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.",
"shell": "powershell"
},
{
"filename": "Chocolatey_Update_Installed.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey Update Installed Apps",
"description": "Update all apps that were installed using Chocolatey.",
"shell": "cmd"
},
{
"filename": "AD_Check_And_Enable_AD_Recycle_Bin.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "AD - Check and Enable AD Recycle Bin",
"description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
"shell": "powershell"
},
{
"filename": "Check_Events_for_Bluescreens.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Event Viewer - Check for Bluescreens",
"description": "This will check for Bluescreen events on your system",
"shell": "powershell"
}
]

View File

@@ -7,8 +7,6 @@ from tacticalrmm.celery import app
@app.task
def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
nats_data = {
"func": "rawcmd",
"timeout": timeout,
@@ -17,15 +15,13 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
"shell": shell,
},
}
for agent in agents_nats:
for agent in Agent.objects.filter(pk__in=agentpks):
asyncio.run(agent.nats_cmd(nats_data, wait=False))
@app.task
def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
script = Script.objects.get(pk=scriptpk)
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
nats_data = {
"func": "runscript",
"timeout": timeout,
@@ -35,5 +31,5 @@ def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
"shell": script.shell,
},
}
for agent in agents_nats:
for agent in Agent.objects.filter(pk__in=agentpks):
asyncio.run(agent.nats_cmd(nats_data, wait=False))

View File

@@ -18,8 +18,6 @@ logger.configure(**settings.LOG_CONFIG)
@api_view()
def get_services(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10))
if r == "timeout":
@@ -38,8 +36,6 @@ def default_services(request):
@api_view(["POST"])
def service_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
action = request.data["sv_action"]
data = {
"func": "winsvcaction",
@@ -80,8 +76,6 @@ def service_action(request):
@api_view()
def service_detail(request, pk, svcname):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {"func": "winsvcdetail", "payload": {"name": svcname}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "timeout":
@@ -93,8 +87,6 @@ def service_detail(request, pk, svcname):
@api_view(["POST"])
def edit_service(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {
"func": "editwinsvc",
"payload": {

View File

@@ -63,8 +63,6 @@ def get_installed(request, pk):
@api_view()
def refresh_installed(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
if r == "timeout" or r == "natsdown":

View File

@@ -15,20 +15,20 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.4.21"
TRMM_VERSION = "0.4.26"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.119"
APP_VER = "0.0.122"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.4.8"
LATEST_AGENT_VER = "1.4.12"
MESH_VER = "0.7.79"
MESH_VER = "0.7.88"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "10"
NPM_VER = "9"
PIP_VER = "11"
NPM_VER = "10"
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"

View File

@@ -3,6 +3,7 @@ FROM node:12-alpine AS builder
WORKDIR /home/node/app
COPY ./web/package.json .
RUN npm install -g npm
RUN npm install
COPY ./web .

View File

@@ -1,4 +1,4 @@
FROM nats:2.1-alpine
FROM nats:2.2-alpine
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

View File

@@ -8,7 +8,7 @@
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django, Vue and Golang.
It uses an [agent](https://github.com/wh1te909/rmmagent) written in Golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
## [LIVE DEMO](https://rmm.xlawgaming.com/)
## [LIVE DEMO](https://rmm.tacticalrmm.io/)
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*

View File

@@ -44,7 +44,7 @@ ufw allow proto tcp from any to any port 4222
```
!!!info
SSH is only required for you to remotely login and do basic linux server administration for your rmm. It is not needed for any agent communication.<br/>
SSH (port 22 tcp) is only required for you to remotely login and do basic linux server administration for your rmm. It is not needed for any agent communication.<br/>
Allow ssh from everywhere (__not__ recommended)
```bash
ufw allow ssh

View File

@@ -20,7 +20,7 @@ SSH into your server as the linux user you created during install.<br/><br/>
__Never__ run any update scripts or commands as the `root` user.<br/>This will mess up permissions and break your installation.<br/><br/>
Download the update script and run it:<br/>
```bash
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
wget -N https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
chmod +x update.sh
./update.sh
```

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="41"
SCRIPT_VERSION="43"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
sudo apt install -y curl wget dirmngr gnupg lsb-release
@@ -185,9 +185,9 @@ print_green 'Installing golang'
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.16.linux-amd64.tar.gz -P ${go_tmp}
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.16.linux-amd64.tar.gz -C ${go_tmp}
tar -xzf ${go_tmp}/go1.16.2.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
@@ -195,11 +195,11 @@ rm -rf ${go_tmp}
print_green 'Downloading NATS'
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp}
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
sudo mv ${nats_tmp}/nats-server-v2.2.0-linux-amd64/nats-server /usr/local/bin/
sudo chmod +x /usr/local/bin/nats-server
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
rm -rf ${nats_tmp}
@@ -216,6 +216,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
sudo npm install -g npm
print_green 'Installing MongoDB'
@@ -251,6 +252,10 @@ echo "$postgresql_repo" | sudo tee /etc/apt/sources.list.d/pgdg.list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13
sleep 2
sudo systemctl enable postgresql
sudo systemctl restart postgresql
sleep 5
print_green 'Creating database for the rmm'
@@ -366,7 +371,7 @@ MESH_USERNAME = "${meshusername}"
MESH_SITE = "https://${meshdomain}"
REDIS_HOST = "localhost"
KEEP_SALT = False
ADMIN_ENABLED = False
ADMIN_ENABLED = True
EOF
)"
echo "${localvars}" > /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
@@ -811,6 +816,8 @@ python manage.py reload_nats
deactivate
sudo systemctl start nats.service
## disable django admin
sed -i 's/ADMIN_ENABLED = True/ADMIN_ENABLED = False/g' /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
print_green 'Restarting services'
for i in rmm.service celery.service celerybeat.service natsapi.service

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="20"
SCRIPT_VERSION="21"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
sudo apt update
@@ -108,9 +108,9 @@ print_green 'Installing golang'
sudo apt update
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.16.linux-amd64.tar.gz -P ${go_tmp}
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.16.linux-amd64.tar.gz -C ${go_tmp}
tar -xzf ${go_tmp}/go1.16.2.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
@@ -118,11 +118,11 @@ rm -rf ${go_tmp}
print_green 'Downloading NATS'
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp}
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
sudo mv ${nats_tmp}/nats-server-v2.2.0-linux-amd64/nats-server /usr/local/bin/
sudo chmod +x /usr/local/bin/nats-server
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
rm -rf ${nats_tmp}
@@ -133,6 +133,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
sudo npm install -g npm
print_green 'Restoring Nginx'
@@ -205,9 +206,8 @@ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-
sudo apt update
sudo apt install -y postgresql-13
sleep 2
sudo systemctl enable postgresql
sudo systemctl restart postgresql
print_green 'Restoring MongoDB'

View File

@@ -0,0 +1,17 @@
#Please only run on a domain controller
#This script will first check if there are any AD Recycle Bin scopes set up - if there are no scopes it is assumed recycle bin feature is not enabled for the domain
#The script then pulls the domain that the machine running the script is on - queries the domain for the Infrastructure Master and then will attempt to enable the feature
$adRecycleBinScope = Get-ADOptionalFeature -Identity 'Recycle Bin Feature' | Select -ExpandProperty EnabledScopes
$ADDomain = Get-ADDomain | Select -ExpandProperty Forest
$ADInfraMaster = Get-ADDomain | Select-Object InfrastructureMaster
if ($adRecycleBinScope -eq $null){
Write-Host "Recycle Bin Disabled"
Write-Host "Attempting to enable AD Recycle Bin"
Enable-ADOptionalFeature -Identity 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target $ADDomain -Server $ADInfraMaster.InfrastructureMaster -Confirm:$false
Write-Host "AD Recycle Bin enabled for domain $($ADDomain)"
}
else{
Write-Host "Recycle Bin already Enabled For: $($ADDomain)`n Scope: $($adRecycleBinScope)"
}

View File

@@ -0,0 +1,20 @@
# This will check for Bluescreen events on your system
$ErrorActionPreference= 'silentlycontinue'
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
if (Get-WinEvent -FilterHashtable @{LogName='application';ID='1001';ProviderName='Windows Error Reporting';Level=4;Data='BlueScreen';StartTime=$TimeSpan})
{
Write-Output "There has been bluescreen events detected on your system"
Get-WinEvent -FilterHashtable @{LogName='application';ID='1001';ProviderName='Windows Error Reporting';Level=4;Data='BlueScreen';StartTime=$TimeSpan}
exit 1
}
{
else
Write-Output "No bluescreen events detected"
exit 0
}
Exit $LASTEXITCODE

View File

@@ -0,0 +1 @@
cup -y all

View File

@@ -0,0 +1,33 @@
<#
Creates a powershell script that runs at logon of any user on the machine in the security context of the user.
Useful to set HKCU registry items
Log is written to C:\Users\Public\UserLogonLog.txt
#>
New-Item -ItemType Directory -Force -Path "$ENV:WINDIR\TRMM"
$logonfile = "$ENV:WINDIR\TRMM\logonscript.ps1"
$logfile = "C:\Users\Public\UserLogonLog.txt"
# === LogonScript ===
$logonscript=@'
Start-Transcript -Path $logfile
# Example: Disable Automatically Hide Scrollbars
# $registryPath = "HKCU:\Control Panel\Accessibility"
# $Name = "DynamicScrollbars"
# $value = "0"
# New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
Stop-Transcript
'@
$logonscript | Out-File $logonfile
# === Create a link in all users startup folder ===
$Shell = New-Object -ComObject ("WScript.Shell")
$ShortCut = $Shell.CreateShortcut($env:PROGRAMDATA + "\Microsoft\Windows\Start Menu\Programs\StartUp\UserLogon.lnk")
$ShortCut.TargetPath="%systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe"
$ShortCut.Arguments="-executionpolicy bypass -WindowStyle Hidden -file $logonfile"
$ShortCut.WorkingDirectory = "$ENV:WINDIR\TRMM";
$ShortCut.Save()

View File

@@ -1,5 +1,5 @@
#Windows Defender Exclusions for Tactical
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
Add-MpPreference -ExclusionPath 'C:\salt\*'
Add-MpPreference -ExclusionPath "C:\Program Files\Mesh Agent\*"
Add-MpPreference -ExclusionPath "C:\Program Files\TacticalAgent\*"
Add-MpPreference -ExclusionPath "C:\Windows\Temp\trmm\*"
Add-MpPreference -ExclusionPath "C:\Windows\Temp\winagent-v*.exe"

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="113"
SCRIPT_VERSION="114"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh'
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
YELLOW='\033[1;33m'
@@ -172,6 +172,10 @@ printf >&2 "${GREEN}Stopping ${i} service...${NC}\n"
sudo systemctl stop ${i}
done
printf >&2 "${GREEN}Restarting postgresql database${NC}\n"
sudo systemctl restart postgresql
sleep 5
rm -f /rmm/api/tacticalrmm/app.ini
numprocs=$(nproc)
@@ -242,6 +246,21 @@ if ! [[ $HAS_PY39 ]]; then
sudo rm -rf Python-3.9.2 Python-3.9.2.tgz
fi
HAS_NATS220=$(/usr/local/bin/nats-server -version | grep v2.2.0)
if ! [[ $HAS_NATS220 ]]; then
printf >&2 "${GREEN}Updating nats to v2.2.0${NC}\n"
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
sudo rm -f /usr/local/bin/nats-server
sudo mv ${nats_tmp}/nats-server-v2.2.0-linux-amd64/nats-server /usr/local/bin/
sudo chmod +x /usr/local/bin/nats-server
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
rm -rf ${nats_tmp}
fi
sudo npm install -g npm
cd /rmm
git config user.email "admin@example.com"
git config user.name "Bob"

30773
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,25 +10,25 @@
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
},
"dependencies": {
"@quasar/extras": "^1.9.17",
"axios": "^0.21.1",
"@quasar/extras": "^1.9.19",
"apexcharts": "^3.23.1",
"axios": "^0.21.1",
"dotenv": "^8.2.0",
"qrcode.vue": "^1.7.0",
"quasar": "^1.15.4",
"quasar": "^1.15.5",
"vue-apexcharts": "^1.6.0"
},
"devDependencies": {
"@quasar/app": "^2.1.15",
"@quasar/app": "^2.2.1",
"@quasar/cli": "^1.1.3",
"@quasar/quasar-app-extension-testing": "^1.0.3",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^3.0.1",
"core-js": "^3.8.1",
"eslint-plugin-cypress": "^2.11.1",
"flush-promises": "^1.0.2",
"fs-extra": "^9.0.1",
"prismjs": "^1.22.0",
"vue-prism-editor": "^1.2.2",
"eslint-plugin-cypress": "^2.11.1"
"vue-prism-editor": "^1.2.2"
},
"browserslist": [
"last 4 Chrome versions",

View File

@@ -11,30 +11,6 @@
<div class="q-pa-md">
<div class="q-gutter-sm">
<q-btn ref="new" label="New" dense flat push unelevated no-caps icon="add" @click="showAddUserModal" />
<q-btn
ref="edit"
label="Edit"
:disable="selected.length === 0"
dense
flat
push
unelevated
no-caps
icon="edit"
@click="showEditUserModal(selected[0])"
/>
<q-btn
ref="delete"
label="Delete"
:disable="selected.length === 0 || selected[0].username === logged_in_user"
dense
flat
push
unelevated
no-caps
icon="delete"
@click="deleteUser(selected[0])"
/>
</div>
<q-table
dense
@@ -97,7 +73,7 @@
v-close-popup
@click="deleteUser(props.row)"
id="context-delete"
v-if="props.row.username !== logged_in_user"
:disable="props.row.username === logged_in_user"
>
<q-item-section side>
<q-icon name="delete" />
@@ -226,7 +202,7 @@ export default {
.onOk(() => {
this.$store
.dispatch("admin/deleteUser", data.id)
.then(response => {
.then(() => {
this.$q.notify(notifySuccessConfig(`User ${data.username} was deleted!`));
})
.catch(e => {

View File

@@ -12,30 +12,6 @@
<div class="q-pa-sm" style="min-height: 65vh; max-height: 65vh">
<div class="q-gutter-sm">
<q-btn ref="new" label="New" dense flat push unelevated no-caps icon="add" @click="showAddTemplateModal" />
<q-btn
ref="edit"
label="Edit"
:disable="!selectedTemplate"
dense
flat
push
unelevated
no-caps
icon="edit"
@click="showEditTemplateModal(selectedTemplate)"
/>
<q-btn
ref="delete"
label="Delete"
:disable="!selectedTemplate"
dense
flat
push
unelevated
no-caps
icon="delete"
@click="deleteTemplate(selectedTemplate)"
/>
</div>
<q-table
dense

View File

@@ -30,28 +30,6 @@
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
label="Edit"
:disable="!isRowSelected || isBuiltInScript(selectedScript.id)"
dense
flat
push
unelevated
no-caps
icon="edit"
@click="editScript(selectedScript)"
/>
<q-btn
label="Delete"
:disable="!isRowSelected || isBuiltInScript(selectedScript.id)"
dense
flat
push
unelevated
no-caps
icon="delete"
@click="deleteScript(selectedScript.id)"
/>
<q-btn
label="View Code"
:disable="!isRowSelected"
@@ -148,7 +126,7 @@
clickable
v-close-popup
@click="editScript(props.node)"
v-if="props.node.script_type !== 'builtin'"
:disable="props.node.script_type === 'builtin'"
>
<q-item-section side>
<q-icon name="edit" />
@@ -160,7 +138,7 @@
clickable
v-close-popup
@click="deleteScript(props.node.id)"
v-if="props.node.script_type !== 'builtin'"
:disable="props.node.script_type === 'builtin'"
>
<q-item-section side>
<q-icon name="delete" />
@@ -241,7 +219,7 @@
clickable
v-close-popup
@click="editScript(props.row)"
v-if="props.row.script_type !== 'builtin'"
:disable="props.row.script_type === 'builtin'"
>
<q-item-section side>
<q-icon name="edit" />
@@ -253,7 +231,7 @@
clickable
v-close-popup
@click="deleteScript(props.row.id)"
v-if="props.row.script_type !== 'builtin'"
:disable="props.row.script_type === 'builtin'"
>
<q-item-section side>
<q-icon name="delete" />

View File

@@ -88,7 +88,7 @@ export default {
field: "install_date",
sortable: false,
format: (val, row) => {
return val === "01/01/1" ? "" : val;
return val === "01/01/1" || val === "01-1-01" ? "" : val;
},
},
{

View File

@@ -66,7 +66,7 @@
<q-tooltip>Missing</q-tooltip>
</q-icon>
</q-td>
<q-td>{{ props.row.severity }}</q-td>
<q-td>{{ formatSeverity(props.row.severity) }}</q-td>
<q-td>{{ formatMessage(props.row.title) }}</q-td>
<q-td
@click.native="
@@ -155,6 +155,9 @@ export default {
};
},
methods: {
formatSeverity(severity) {
return !severity ? "Other" : severity;
},
editPolicy(pk, policy) {
const data = { pk: pk, policy: policy };
axios.patch(`/winupdate/editpolicy/`, data).then(r => {

View File

@@ -12,28 +12,6 @@
<q-card-section>
<div class="q-gutter-sm">
<q-btn label="New" dense flat push unelevated no-caps icon="add" @click="showAddPolicyForm" />
<q-btn
label="Edit"
:disable="!selectedPolicy"
dense
flat
push
unelevated
no-caps
icon="edit"
@click="showEditPolicyForm(selectedPolicy)"
/>
<q-btn
label="Delete"
:disable="!selectedPolicy"
dense
flat
push
unelevated
no-caps
icon="delete"
@click="deletePolicy(selectedPolicy)"
/>
<q-btn
label="Policy Overview"
dense
@@ -127,6 +105,13 @@
<q-item-section>Policy Exclusions</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="syncPolicies(props.row)">
<q-item-section side>
<q-icon name="sync" />
</q-item-section>
<q-item-section>Sync Policies</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showPatchPolicyForm(props.row)">
<q-item-section side>
<q-icon name="system_update" />
@@ -450,6 +435,24 @@ export default {
this.refresh();
});
},
syncPolicies(policy) {
this.$q.loading.show();
const data = {
policy: policy.id,
};
this.$axios
.post(`/automation/sync/`, data)
.then(r => {
this.$q.loading.hide();
this.notifySuccess("Sync request sent successfully. The task will be run on all affected agents");
})
.catch(error => {
this.$q.loading.hide();
this.notifyError("An Error occured while sending policy sync request");
});
},
toggleCheckbox(policy, type) {
this.$q.loading.show();
let text = "";

View File

@@ -84,8 +84,8 @@
class="col-2"
:rules="[
val => !!val || '*Required',
val => val >= 60 || 'Minimum is 60 seconds',
val => val <= 3600 || 'Maximum is 3600 seconds',
val => val >= 15 || 'Minimum is 15 seconds',
val => val <= 86400 || 'Maximum is 86400 seconds',
]"
/>
</q-card-section>

View File

@@ -36,6 +36,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
dense
outlined
type="number"
v-model.number="cpuloadcheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -63,6 +73,7 @@ export default {
check_type: "cpuload",
warning_threshold: 70,
error_threshold: 90,
run_interval: 0,
fails_b4_alert: 1,
},
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],

View File

@@ -45,6 +45,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
dense
outlined
type="number"
v-model.number="diskcheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -75,6 +85,7 @@ export default {
warning_threshold: 25,
error_threshold: 10,
fails_b4_alert: 1,
run_interval: 0,
},
diskOptions: [],
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],

View File

@@ -95,6 +95,15 @@
label="Alert Severity"
/>
</q-card-section>
<q-card-section>
<q-input
label="Number of events found before alert"
dense
outlined
type="number"
v-model.number="eventlogcheck.number_of_events_b4_alert"
/>
</q-card-section>
<q-card-section>
<q-select
outlined
@@ -105,6 +114,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
outlined
dense
type="number"
v-model.number="eventlogcheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -137,8 +156,10 @@ export default {
fail_when: "contains",
search_last_days: 1,
fails_b4_alert: 1,
number_of_events_b4_alert: 1,
event_id_is_wildcard: false,
alert_severity: "warning",
run_interval: 0,
},
eventMessage: false,
eventSource: false,

View File

@@ -36,6 +36,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
outlined
dense
type="number"
v-model.number="memcheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -64,6 +74,7 @@ export default {
warning_threshold: 70,
error_threshold: 85,
fails_b4_alert: 1,
run_interval: 0,
},
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
};

View File

@@ -38,6 +38,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
outlined
dense
type="number"
v-model.number="pingcheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -67,6 +77,7 @@ export default {
ip: null,
alert_severity: "warning",
fails_b4_alert: 1,
run_interval: 0,
},
severityOptions: [
{ label: "Informational", value: "info" },

View File

@@ -90,6 +90,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
outlined
dense
type="number"
v-model.number="scriptcheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -120,6 +130,7 @@ export default {
fails_b4_alert: 1,
info_return_codes: [],
warning_return_codes: [],
run_interval: 0,
},
scriptOptions: [],
failOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],

View File

@@ -112,6 +112,16 @@
label="Number of consecutive failures before alert"
/>
</q-card-section>
<q-card-section>
<q-input
dense
outlined
type="number"
v-model.number="winsvccheck.run_interval"
label="Check run interval (s)"
hint="Setting this will override the check run interval on the agent"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-if="mode === 'add'" label="Add" color="primary" type="submit" />
<q-btn v-else-if="mode === 'edit'" label="Edit" color="primary" type="submit" />
@@ -146,6 +156,7 @@ export default {
restart_if_stopped: false,
fails_b4_alert: 1,
alert_severity: "warning",
run_interval: 0,
},
severityOptions: [
{ label: "Informational", value: "info" },

View File

@@ -46,6 +46,20 @@
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Client Sort:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="clientTreeSort"
:options="clientTreeSortOptions"
class="col-8"
/>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
@@ -68,8 +82,19 @@ export default {
return {
agentDblClickAction: "",
defaultAgentTblTab: "",
clientTreeSort: "",
tab: "ui",
splitterModel: 20,
clientTreeSortOptions: [
{
label: "Sort alphabetically, moving failing clients to the top",
value: "alphafail",
},
{
label: "Sort alphabetically only",
value: "alpha",
},
],
agentDblClickOptions: [
{
label: "Edit Agent",
@@ -105,17 +130,19 @@ export default {
this.$axios.get("/core/dashinfo/").then(r => {
this.agentDblClickAction = r.data.dbl_click_action;
this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
this.clientTreeSort = r.data.client_tree_sort;
});
},
editUserPrefs() {
const data = {
userui: true,
agent_dblclick_action: this.agentDblClickAction,
default_agent_tbl_tab: this.defaultAgentTblTab,
client_tree_sort: this.clientTreeSort,
};
this.$axios.patch("/accounts/users/ui/", data).then(r => {
this.notifySuccess("Preferences were saved!");
this.$emit("edited");
this.$store.dispatch("loadTree");
this.$emit("close");
});
},

View File

@@ -254,7 +254,7 @@ export default {
}
.prism-editor__container {
height: 60000em;
height: 30000em;
}
/* optional class for removing the outline */
@@ -264,7 +264,7 @@ export default {
.prism-editor__textarea,
.prism-editor__container {
width: 10000em !important;
width: 1000em !important;
-ms-overflow-style: none;
scrollbar-width: none;
}

View File

@@ -32,6 +32,7 @@ export default function () {
showCommunityScripts: false,
agentDblClickAction: "",
defaultAgentTblTab: "server",
clientTreeSort: "alphafail",
},
getters: {
loggedIn(state) {
@@ -139,6 +140,9 @@ export default function () {
},
SET_DEFAULT_AGENT_TBL_TAB(state, tab) {
state.defaultAgentTblTab = tab
},
SET_CLIENT_TREE_SORT(state, val) {
state.clientTreeSort = val
}
},
actions: {
@@ -215,7 +219,7 @@ export default function () {
loadSites(context) {
return axios.get("/clients/sites/");
},
loadTree({ commit }) {
loadTree({ commit, state }) {
axios.get("/clients/tree/").then(r => {
if (r.data.length === 0) {
@@ -263,9 +267,15 @@ export default function () {
output.push(clientNode);
}
// move failing clients to the top
const sortedByFailing = output.sort(a => a.color === "negative" ? -1 : 1)
commit("loadTree", sortedByFailing);
if (state.clientTreeSort === "alphafail") {
// move failing clients to the top
const sortedByFailing = output.sort(a => a.color === "negative" ? -1 : 1);
commit("loadTree", sortedByFailing);
} else {
commit("loadTree", output);
}
});
},
checkVer(context) {

View File

@@ -504,7 +504,7 @@ export default {
name: "agentstatus",
field: "status",
align: "left",
sortable: false,
sortable: true,
},
{
name: "last_seen",
@@ -712,7 +712,10 @@ export default {
},
getDashInfo(edited = true) {
this.$store.dispatch("getDashInfo").then(r => {
if (edited) this.$store.commit("SET_DEFAULT_AGENT_TBL_TAB", r.data.default_agent_tbl_tab);
if (edited) {
this.$store.commit("SET_DEFAULT_AGENT_TBL_TAB", r.data.default_agent_tbl_tab);
this.$store.commit("SET_CLIENT_TREE_SORT", r.data.client_tree_sort);
}
this.darkMode = r.data.dark_mode;
this.$q.dark.set(this.darkMode);
this.currentTRMMVersion = r.data.trmm_version;