Compare commits
	
		
			155 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					83ba480863 | ||
| 
						 | 
					f158ea25e9 | ||
| 
						 | 
					0227519eab | ||
| 
						 | 
					616a9685fa | ||
| 
						 | 
					fe61b01320 | ||
| 
						 | 
					7b25144311 | ||
| 
						 | 
					9d42fbbdd7 | ||
| 
						 | 
					39ac5b088b | ||
| 
						 | 
					c14ffd08a0 | ||
| 
						 | 
					6e1239340b | ||
| 
						 | 
					a297dc8b3b | ||
| 
						 | 
					8d4ecc0898 | ||
| 
						 | 
					eae9c04429 | ||
| 
						 | 
					a41c48a9c5 | ||
| 
						 | 
					ff2a94bd9b | ||
| 
						 | 
					4a1f5558b8 | ||
| 
						 | 
					608db9889f | ||
| 
						 | 
					012b697337 | ||
| 
						 | 
					0580506cf3 | ||
| 
						 | 
					ff4ab9b661 | ||
| 
						 | 
					b7ce5fdd3e | ||
| 
						 | 
					a11e617322 | ||
| 
						 | 
					d0beac7e2b | ||
| 
						 | 
					9db497092f | ||
| 
						 | 
					8eb91c08aa | ||
| 
						 | 
					ded5437522 | ||
| 
						 | 
					9348657951 | ||
| 
						 | 
					bca85933f7 | ||
| 
						 | 
					c32bb35f1c | ||
| 
						 | 
					4b84062d62 | ||
| 
						 | 
					d6d0f8fa17 | ||
| 
						 | 
					dd72c875d3 | ||
| 
						 | 
					1a1df50300 | ||
| 
						 | 
					53cbb527b4 | ||
| 
						 | 
					8b87b2717e | ||
| 
						 | 
					1007d6dac7 | ||
| 
						 | 
					6799fac120 | ||
| 
						 | 
					558e6288ca | ||
| 
						 | 
					d9cb73291b | ||
| 
						 | 
					d0f7be3ac3 | ||
| 
						 | 
					331e16d3ca | ||
| 
						 | 
					0db246c311 | ||
| 
						 | 
					94dc62ff58 | ||
| 
						 | 
					e68ecf6844 | ||
| 
						 | 
					5167b0a8c6 | ||
| 
						 | 
					77e3d3786d | ||
| 
						 | 
					708d4d39bc | ||
| 
						 | 
					2a8cda2a1e | ||
| 
						 | 
					8d783840ad | ||
| 
						 | 
					abe39d5790 | ||
| 
						 | 
					d7868e9e5a | ||
| 
						 | 
					7b84e36e15 | ||
| 
						 | 
					6cab6d69d8 | ||
| 
						 | 
					87846d7aef | ||
| 
						 | 
					2557769c6a | ||
| 
						 | 
					48375f3878 | ||
| 
						 | 
					176c85d8c1 | ||
| 
						 | 
					17cad71ede | ||
| 
						 | 
					e8bf9d4e6f | ||
| 
						 | 
					7bdd2038ef | ||
| 
						 | 
					e9f6e7943a | ||
| 
						 | 
					e74ba387ab | ||
| 
						 | 
					27c79e5b99 | ||
| 
						 | 
					8170d5ea73 | ||
| 
						 | 
					196f73705d | ||
| 
						 | 
					ad0bbf5248 | ||
| 
						 | 
					4cae9cd90d | ||
| 
						 | 
					be7bc55a76 | ||
| 
						 | 
					684b545e8f | ||
| 
						 | 
					7835cc3b10 | ||
| 
						 | 
					f8706b51e8 | ||
| 
						 | 
					d97f8fd5da | ||
| 
						 | 
					f8fa87441e | ||
| 
						 | 
					d42537814a | ||
| 
						 | 
					792421b0e2 | ||
| 
						 | 
					72d55a010b | ||
| 
						 | 
					880d8258ce | ||
| 
						 | 
					b79bf82efb | ||
| 
						 | 
					b3118b6253 | ||
| 
						 | 
					ba172e2e25 | ||
| 
						 | 
					892d53abeb | ||
| 
						 | 
					5cbaa1ce98 | ||
| 
						 | 
					7b35d9ad2e | ||
| 
						 | 
					8462de7911 | ||
| 
						 | 
					8721f44298 | ||
| 
						 | 
					c7a2d69afa | ||
| 
						 | 
					0453d81e7a | ||
| 
						 | 
					501c04ac2b | ||
| 
						 | 
					0ef4e9a5c3 | ||
| 
						 | 
					129c50e598 | ||
| 
						 | 
					3e276fc2ac | ||
| 
						 | 
					658d5e05ae | ||
| 
						 | 
					4e7d5d476e | ||
| 
						 | 
					6a55ca20f3 | ||
| 
						 | 
					c56c537f7f | ||
| 
						 | 
					fd7d776121 | ||
| 
						 | 
					1af28190d8 | ||
| 
						 | 
					6b305be567 | ||
| 
						 | 
					3bf70513b7 | ||
| 
						 | 
					7e64404654 | ||
| 
						 | 
					e1b5226f34 | ||
| 
						 | 
					0d7128ad31 | ||
| 
						 | 
					5778626087 | ||
| 
						 | 
					3ff48756ed | ||
| 
						 | 
					0ce9a6eeba | ||
| 
						 | 
					ad527b4aed | ||
| 
						 | 
					6633bb452e | ||
| 
						 | 
					efeb0b4feb | ||
| 
						 | 
					8cc11fc102 | ||
| 
						 | 
					ee6a167220 | ||
| 
						 | 
					8d4ad3c405 | ||
| 
						 | 
					072fbf4d60 | ||
| 
						 | 
					727c41c283 | ||
| 
						 | 
					e2266838b6 | ||
| 
						 | 
					775762d615 | ||
| 
						 | 
					900c3008cb | ||
| 
						 | 
					09379213a6 | ||
| 
						 | 
					ceb97048e3 | ||
| 
						 | 
					4561515517 | ||
| 
						 | 
					a7b285759f | ||
| 
						 | 
					b4531b2a12 | ||
| 
						 | 
					9e1d261c76 | ||
| 
						 | 
					e35fa15cd2 | ||
| 
						 | 
					dbd1f0d4f9 | ||
| 
						 | 
					9ade78b703 | ||
| 
						 | 
					f20e244b5f | ||
| 
						 | 
					0989308b7e | ||
| 
						 | 
					12c7140536 | ||
| 
						 | 
					2a0b605e92 | ||
| 
						 | 
					6978890e6a | ||
| 
						 | 
					561abd6cb9 | ||
| 
						 | 
					4dd6227f0b | ||
| 
						 | 
					1ec314c31c | ||
| 
						 | 
					a2be5a00be | ||
| 
						 | 
					4e2241c115 | ||
| 
						 | 
					8459bca64a | ||
| 
						 | 
					24cb0565b9 | ||
| 
						 | 
					9442acb028 | ||
| 
						 | 
					4f7f181a42 | ||
| 
						 | 
					b7dd8737a7 | ||
| 
						 | 
					2207eeb727 | ||
| 
						 | 
					89dad7dfe7 | ||
| 
						 | 
					e5803d0cf3 | ||
| 
						 | 
					c1fffe9ae6 | ||
| 
						 | 
					9e6cbd3d32 | ||
| 
						 | 
					2ea8742510 | ||
| 
						 | 
					5cfa0254f9 | ||
| 
						 | 
					8cd2544f78 | ||
| 
						 | 
					c03b768364 | ||
| 
						 | 
					d60481ead4 | ||
| 
						 | 
					126be3827d | ||
| 
						 | 
					121274dca2 | ||
| 
						 | 
					0ecf8da27e | ||
| 
						 | 
					4a6bcb525d | ||
| 
						 | 
					83f9ee50dd | 
@@ -100,6 +100,7 @@ MESH_USERNAME = '${MESH_USER}'
 | 
			
		||||
MESH_SITE = 'https://${MESH_HOST}'
 | 
			
		||||
MESH_TOKEN_KEY = '${MESH_TOKEN}'
 | 
			
		||||
REDIS_HOST    = '${REDIS_HOST}'
 | 
			
		||||
ADMIN_ENABLED = True
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,7 +3,7 @@
 | 
			
		||||
github: wh1te909
 | 
			
		||||
patreon: # Replace with a single Patreon username
 | 
			
		||||
open_collective: # Replace with a single Open Collective username
 | 
			
		||||
ko_fi: # Replace with a single Ko-fi username
 | 
			
		||||
ko_fi: tacticalrmm
 | 
			
		||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 | 
			
		||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 | 
			
		||||
liberapay: # Replace with a single Liberapay username
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a bug report
 | 
			
		||||
title: ''
 | 
			
		||||
labels: ''
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Server Info (please complete the following information):**
 | 
			
		||||
 - OS: [e.g. Ubuntu 20.04, Debian 10]
 | 
			
		||||
 - Browser: [e.g. chrome, safari]
 | 
			
		||||
 - RMM Version (as shown in top left of web UI):
 | 
			
		||||
 | 
			
		||||
**Installation Method:**
 | 
			
		||||
  - [ ] Standard
 | 
			
		||||
  - [ ] Docker
 | 
			
		||||
 | 
			
		||||
**Agent Info (please complete the following information):**
 | 
			
		||||
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
 | 
			
		||||
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
A clear and concise description of what the bug is.
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Click on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
 | 
			
		||||
**Expected behavior**
 | 
			
		||||
A clear and concise description of what you expected to happen.
 | 
			
		||||
 | 
			
		||||
**Screenshots**
 | 
			
		||||
If applicable, add screenshots to help explain your problem.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context about the problem here.
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
title: ''
 | 
			
		||||
labels: ''
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Is your feature request related to a problem? Please describe.**
 | 
			
		||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
A clear and concise description of what you want to happen.
 | 
			
		||||
 | 
			
		||||
**Describe alternatives you've considered**
 | 
			
		||||
A clear and concise description of any alternative solutions or features you've considered.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context or screenshots about the feature request here.
 | 
			
		||||
@@ -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.*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								api/tacticalrmm/accounts/management/commands/reset_2fa.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								api/tacticalrmm/accounts/management/commands/reset_2fa.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import pyotp
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Reset 2fa"
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument("username", type=str)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        username = kwargs["username"]
 | 
			
		||||
        try:
 | 
			
		||||
            user = User.objects.get(username=username)
 | 
			
		||||
        except User.DoesNotExist:
 | 
			
		||||
            self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        domain = "Tactical RMM"
 | 
			
		||||
        nginx = "/etc/nginx/sites-available/frontend.conf"
 | 
			
		||||
        found = None
 | 
			
		||||
        if os.path.exists(nginx):
 | 
			
		||||
            try:
 | 
			
		||||
                with open(nginx, "r") as f:
 | 
			
		||||
                    for line in f:
 | 
			
		||||
                        if "server_name" in line:
 | 
			
		||||
                            found = line
 | 
			
		||||
                            break
 | 
			
		||||
 | 
			
		||||
                if found:
 | 
			
		||||
                    rep = found.replace("server_name", "").replace(";", "")
 | 
			
		||||
                    domain = "".join(rep.split())
 | 
			
		||||
            except:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        code = pyotp.random_base32()
 | 
			
		||||
        user.totp_key = code
 | 
			
		||||
        user.save(update_fields=["totp_key"])
 | 
			
		||||
 | 
			
		||||
        url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
 | 
			
		||||
        subprocess.run(f'qr "{url}"', shell=True)
 | 
			
		||||
        self.stdout.write(
 | 
			
		||||
            self.style.WARNING("Scan the barcode above with your authenticator app")
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write(
 | 
			
		||||
            self.style.WARNING(
 | 
			
		||||
                f"If that doesn't work you may manually enter the setup key: {code}"
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write(
 | 
			
		||||
            self.style.SUCCESS(f"2fa was successfully reset for user {username}")
 | 
			
		||||
        )
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Reset password for user"
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument("username", type=str)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        username = kwargs["username"]
 | 
			
		||||
        try:
 | 
			
		||||
            user = User.objects.get(username=username)
 | 
			
		||||
        except User.DoesNotExist:
 | 
			
		||||
            self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        passwd = input("Enter new password: ")
 | 
			
		||||
        user.set_password(passwd)
 | 
			
		||||
        user.save()
 | 
			
		||||
        self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-02-28 06:38
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0011_user_default_agent_tbl_tab'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='agents_per_page',
 | 
			
		||||
            field=models.PositiveIntegerField(default=50),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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,6 +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)  # not currently used
 | 
			
		||||
    client_tree_sort = models.CharField(
 | 
			
		||||
        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    agent = models.OneToOneField(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -271,18 +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",
 | 
			
		||||
            "client_tree_sort": "alpha",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,19 +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"])
 | 
			
		||||
 | 
			
		||||
        serializer = UserUISerializer(
 | 
			
		||||
            instance=request.user, data=request.data, partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,10 @@ workstation_agent = agent.extend(
 | 
			
		||||
 | 
			
		||||
online_agent = agent.extend(last_seen=djangotime.now())
 | 
			
		||||
 | 
			
		||||
offline_agent = agent.extend(
 | 
			
		||||
    last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
overdue_agent = agent.extend(
 | 
			
		||||
    last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,93 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Bulk update agent offline/overdue time"
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument("time", type=int, help="Time in minutes")
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--client",
 | 
			
		||||
            type=str,
 | 
			
		||||
            help="Client Name",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--site",
 | 
			
		||||
            type=str,
 | 
			
		||||
            help="Site Name",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--offline",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="Offline",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--overdue",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="Overdue",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--all",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="All agents",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        time = kwargs["time"]
 | 
			
		||||
        client_name = kwargs["client"]
 | 
			
		||||
        site_name = kwargs["site"]
 | 
			
		||||
        all_agents = kwargs["all"]
 | 
			
		||||
        offline = kwargs["offline"]
 | 
			
		||||
        overdue = kwargs["overdue"]
 | 
			
		||||
        agents = None
 | 
			
		||||
 | 
			
		||||
        if offline and time < 2:
 | 
			
		||||
            self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if overdue and time < 3:
 | 
			
		||||
            self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if client_name:
 | 
			
		||||
            try:
 | 
			
		||||
                client = Client.objects.get(name=client_name)
 | 
			
		||||
            except Client.DoesNotExist:
 | 
			
		||||
                self.stdout.write(
 | 
			
		||||
                    self.style.ERROR(f"Client {client_name} doesn't exist")
 | 
			
		||||
                )
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
 | 
			
		||||
        elif site_name:
 | 
			
		||||
            try:
 | 
			
		||||
                site = Site.objects.get(name=site_name)
 | 
			
		||||
            except Site.DoesNotExist:
 | 
			
		||||
                self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist"))
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            agents = Agent.objects.filter(site=site)
 | 
			
		||||
 | 
			
		||||
        elif all_agents:
 | 
			
		||||
            agents = Agent.objects.all()
 | 
			
		||||
 | 
			
		||||
        if agents:
 | 
			
		||||
            if offline:
 | 
			
		||||
                agents.update(offline_time=time)
 | 
			
		||||
                self.stdout.write(
 | 
			
		||||
                    self.style.SUCCESS(
 | 
			
		||||
                        f"Changed offline time on {len(agents)} agents to {time} minutes"
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if overdue:
 | 
			
		||||
                agents.update(overdue_time=time)
 | 
			
		||||
                self.stdout.write(
 | 
			
		||||
                    self.style.SUCCESS(
 | 
			
		||||
                        f"Changed overdue time on {len(agents)} agents to {time} minutes"
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
@@ -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'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -241,6 +239,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
 | 
			
		||||
            return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
@@ -270,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,
 | 
			
		||||
@@ -295,10 +308,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        running_agent = self
 | 
			
		||||
        if run_on_any:
 | 
			
		||||
            nats_ping = {"func": "ping", "timeout": 1}
 | 
			
		||||
            nats_ping = {"func": "ping"}
 | 
			
		||||
 | 
			
		||||
            # try on self first
 | 
			
		||||
            r = asyncio.run(self.nats_cmd(nats_ping))
 | 
			
		||||
            r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
 | 
			
		||||
 | 
			
		||||
            if r == "pong":
 | 
			
		||||
                running_agent = self
 | 
			
		||||
@@ -312,7 +325,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
                for agent in online:
 | 
			
		||||
                    r = asyncio.run(agent.nats_cmd(nats_ping))
 | 
			
		||||
                    r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
 | 
			
		||||
                    if r == "pong":
 | 
			
		||||
                        running_agent = agent
 | 
			
		||||
                        break
 | 
			
		||||
@@ -460,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
 | 
			
		||||
@@ -562,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):
 | 
			
		||||
@@ -702,165 +722,29 @@ 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 handle_alert(self, checkin: bool = False) -> None:
 | 
			
		||||
        from agents.tasks import (
 | 
			
		||||
            agent_outage_email_task,
 | 
			
		||||
            agent_outage_sms_task,
 | 
			
		||||
            agent_recovery_email_task,
 | 
			
		||||
            agent_recovery_sms_task,
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        return (
 | 
			
		||||
            self.overdue_dashboard_alert
 | 
			
		||||
            or self.overdue_email_alert
 | 
			
		||||
            or self.overdue_text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.agent_always_alert
 | 
			
		||||
                    or alert_template.agent_always_email
 | 
			
		||||
                    or alert_template.agent_always_text
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenace mode
 | 
			
		||||
        if self.maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        alert_template = self.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        # called when agent is back online
 | 
			
		||||
        if checkin:
 | 
			
		||||
            if Alert.objects.filter(agent=self, resolved=False).exists():
 | 
			
		||||
 | 
			
		||||
                # resolve alert if exists
 | 
			
		||||
                alert = Alert.objects.get(agent=self, resolved=False)
 | 
			
		||||
                alert.resolve()
 | 
			
		||||
 | 
			
		||||
                # check if a resolved notification should be emailed
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.agent_email_on_resolved
 | 
			
		||||
                    and not alert.resolved_email_sent
 | 
			
		||||
                ):
 | 
			
		||||
                    agent_recovery_email_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if a resolved notification should be texted
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.agent_text_on_resolved
 | 
			
		||||
                    and not alert.resolved_sms_sent
 | 
			
		||||
                ):
 | 
			
		||||
                    agent_recovery_sms_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if any scripts should be run
 | 
			
		||||
                if not alert.resolved_action_run and (
 | 
			
		||||
                    alert_template and alert_template.resolved_action
 | 
			
		||||
                ):
 | 
			
		||||
                    r = self.run_script(
 | 
			
		||||
                        scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                        args=alert_template.resolved_action_args,
 | 
			
		||||
                        timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                        wait=True,
 | 
			
		||||
                        full=True,
 | 
			
		||||
                        run_on_any=True,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    # command was successful
 | 
			
		||||
                    if type(r) == dict:
 | 
			
		||||
                        alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                        alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                        alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                        alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                            r["execution_time"]
 | 
			
		||||
                        )
 | 
			
		||||
                        alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                        alert.save()
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        # called when agent is offline
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
            # check if alert hasn't been created yet so create it
 | 
			
		||||
            if not Alert.objects.filter(agent=self, resolved=False).exists():
 | 
			
		||||
 | 
			
		||||
                # check if alert should be created and if not return
 | 
			
		||||
                if (
 | 
			
		||||
                    self.overdue_dashboard_alert
 | 
			
		||||
                    or self.overdue_email_alert
 | 
			
		||||
                    or self.overdue_text_alert
 | 
			
		||||
                    or (
 | 
			
		||||
                        alert_template
 | 
			
		||||
                        and (
 | 
			
		||||
                            alert_template.agent_always_alert
 | 
			
		||||
                            or alert_template.agent_always_email
 | 
			
		||||
                            or alert_template.agent_always_text
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                ):
 | 
			
		||||
                    alert = Alert.create_availability_alert(self)
 | 
			
		||||
                else:
 | 
			
		||||
                    return
 | 
			
		||||
 | 
			
		||||
                # add a null check history to allow gaps in graph
 | 
			
		||||
                for check in self.agentchecks.all():  # type: ignore
 | 
			
		||||
                    check.add_check_history(None)
 | 
			
		||||
            else:
 | 
			
		||||
                alert = Alert.objects.get(agent=self, resolved=False)
 | 
			
		||||
 | 
			
		||||
            # create dashboard alert if enabled
 | 
			
		||||
            if self.overdue_dashboard_alert or (
 | 
			
		||||
                alert_template and alert_template.agent_always_alert
 | 
			
		||||
            ):
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
            # send email alert if enabled
 | 
			
		||||
            if self.overdue_email_alert or (
 | 
			
		||||
                alert_template and alert_template.agent_always_email
 | 
			
		||||
            ):
 | 
			
		||||
                agent_outage_email_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.agent_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # send text message if enabled
 | 
			
		||||
            if self.overdue_text_alert or (
 | 
			
		||||
                alert_template and alert_template.agent_always_text
 | 
			
		||||
            ):
 | 
			
		||||
                agent_outage_sms_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.agent_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # check if any scripts should be run
 | 
			
		||||
            if not alert.action_run and alert_template and alert_template.action:
 | 
			
		||||
                r = self.run_script(
 | 
			
		||||
                    scriptpk=alert_template.action.pk,
 | 
			
		||||
                    args=alert_template.action_args,
 | 
			
		||||
                    timeout=alert_template.action_timeout,
 | 
			
		||||
                    wait=True,
 | 
			
		||||
                    full=True,
 | 
			
		||||
                    run_on_any=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # command was successful
 | 
			
		||||
                if isinstance(r, dict):
 | 
			
		||||
                    alert.action_retcode = r["retcode"]
 | 
			
		||||
                    alert.action_stdout = r["stdout"]
 | 
			
		||||
                    alert.action_stderr = r["stderr"]
 | 
			
		||||
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                    alert.action_run = djangotime.now()
 | 
			
		||||
                    alert.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(
 | 
			
		||||
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
    def send_outage_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 overdue",
 | 
			
		||||
            (
 | 
			
		||||
@@ -869,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",
 | 
			
		||||
            (
 | 
			
		||||
@@ -885,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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -183,6 +183,8 @@ def agent_recovery_sms_task(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def agent_outages_task() -> None:
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk",
 | 
			
		||||
        "last_seen",
 | 
			
		||||
@@ -195,21 +197,7 @@ def agent_outages_task() -> None:
 | 
			
		||||
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        if agent.status == "overdue":
 | 
			
		||||
            agent.handle_alert()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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))
 | 
			
		||||
            Alert.handle_alert_failure(agent)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,63 @@ from .serializers import AgentSerializer
 | 
			
		||||
from .tasks import auto_self_agent_update_task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentsList(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_agents_list(self):
 | 
			
		||||
        url = "/agents/listagents/"
 | 
			
		||||
 | 
			
		||||
        # 36 total agents
 | 
			
		||||
        company1 = baker.make("clients.Client")
 | 
			
		||||
        company2 = baker.make("clients.Client")
 | 
			
		||||
        site1 = baker.make("clients.Site", client=company1)
 | 
			
		||||
        site2 = baker.make("clients.Site", client=company1)
 | 
			
		||||
        site3 = baker.make("clients.Site", client=company2)
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent", site=site1, monitoring_type="server", _quantity=15
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site2,
 | 
			
		||||
            monitoring_type="workstation",
 | 
			
		||||
            _quantity=10,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site3,
 | 
			
		||||
            monitoring_type="server",
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site3,
 | 
			
		||||
            monitoring_type="workstation",
 | 
			
		||||
            _quantity=7,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test all agents
 | 
			
		||||
        r = self.client.patch(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 36)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test client1
 | 
			
		||||
        data = {"clientPK": company1.pk}  # type: ignore
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 25)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test site3
 | 
			
		||||
        data = {"sitePK": site3.pk}  # type: ignore
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 11)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -141,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/"
 | 
			
		||||
 | 
			
		||||
@@ -256,7 +308,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        mock_ret.return_value = "nt authority\system"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertIsInstance(r.data, str)
 | 
			
		||||
        self.assertIsInstance(r.data, str)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        mock_ret.return_value = "timeout"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -276,15 +328,16 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
 | 
			
		||||
        self.assertEqual(r.data["agent"], self.agent.hostname)
 | 
			
		||||
        self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["agent"], self.agent.hostname)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "type": "schedreboot",
 | 
			
		||||
                "deleteafter": True,
 | 
			
		||||
                "trigger": "once",
 | 
			
		||||
                "name": r.data["task_name"],
 | 
			
		||||
                "name": r.data["task_name"],  # type: ignore
 | 
			
		||||
                "year": 2025,
 | 
			
		||||
                "month": "August",
 | 
			
		||||
                "day": 29,
 | 
			
		||||
@@ -305,7 +358,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.patch(url, data_invalid, format="json")
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "Invalid date")
 | 
			
		||||
        self.assertEqual(r.data, "Invalid date")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
@@ -316,8 +369,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        data = {
 | 
			
		||||
            "client": site.client.id,
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
            "client": site.client.id,  # type: ignore
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "arch": "64",
 | 
			
		||||
            "expires": 23,
 | 
			
		||||
            "installMethod": "exe",
 | 
			
		||||
@@ -364,50 +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, 400)
 | 
			
		||||
        self.assertIn("0.9.5", r.json())
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_agents_list(self):
 | 
			
		||||
        url = "/agents/listagents/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
        # 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.assertEqual(RecoveryAction.objects.count(), 0)
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # 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/"
 | 
			
		||||
@@ -425,7 +497,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        edit = {
 | 
			
		||||
            "id": self.agent.pk,
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "monitoring_type": "workstation",
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
            "offline_time": 4,
 | 
			
		||||
@@ -456,7 +528,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent = Agent.objects.get(pk=self.agent.pk)
 | 
			
		||||
        data = AgentSerializer(agent).data
 | 
			
		||||
        self.assertEqual(data["site"], site.id)
 | 
			
		||||
        self.assertEqual(data["site"], site.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        policy = WinUpdatePolicy.objects.get(agent=self.agent)
 | 
			
		||||
        data = WinUpdatePolicySerializer(policy).data
 | 
			
		||||
@@ -474,21 +546,21 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        # TODO
 | 
			
		||||
        # decode the cookie
 | 
			
		||||
 | 
			
		||||
        self.assertIn("&viewmode=13", r.data["file"])
 | 
			
		||||
        self.assertIn("&viewmode=12", r.data["terminal"])
 | 
			
		||||
        self.assertIn("&viewmode=11", r.data["control"])
 | 
			
		||||
        self.assertIn("&viewmode=13", r.data["file"])  # type: ignore
 | 
			
		||||
        self.assertIn("&viewmode=12", r.data["terminal"])  # type: ignore
 | 
			
		||||
        self.assertIn("&viewmode=11", r.data["control"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["file"])
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["terminal"])
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["control"])
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["file"])  # type: ignore
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["terminal"])  # type: ignore
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["control"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertIn("?login=", r.data["file"])
 | 
			
		||||
        self.assertIn("?login=", r.data["terminal"])
 | 
			
		||||
        self.assertIn("?login=", r.data["control"])
 | 
			
		||||
        self.assertIn("?login=", r.data["file"])  # type: ignore
 | 
			
		||||
        self.assertIn("?login=", r.data["terminal"])  # type: ignore
 | 
			
		||||
        self.assertIn("?login=", r.data["control"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data["hostname"])
 | 
			
		||||
        self.assertEqual(self.agent.client.name, r.data["client"])
 | 
			
		||||
        self.assertEqual(self.agent.site.name, r.data["site"])
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data["hostname"])  # type: ignore
 | 
			
		||||
        self.assertEqual(self.agent.client.name, r.data["client"])  # type: ignore
 | 
			
		||||
        self.assertEqual(self.agent.site.name, r.data["site"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -498,32 +570,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_by_client(self):
 | 
			
		||||
        url = f"/agents/byclient/{self.agent.client.id}/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(r.data)
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/byclient/500/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertFalse(r.data)  # returns empty list
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_by_site(self):
 | 
			
		||||
        url = f"/agents/bysite/{self.agent.site.id}/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(r.data)
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/bysite/500/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.data, [])
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_overdue_action(self):
 | 
			
		||||
        url = "/agents/overdueaction/"
 | 
			
		||||
 | 
			
		||||
@@ -532,14 +578,14 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        agent = Agent.objects.get(pk=self.agent.pk)
 | 
			
		||||
        self.assertTrue(agent.overdue_email_alert)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        payload = {"pk": self.agent.pk, "overdue_text_alert": False}
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        agent = Agent.objects.get(pk=self.agent.pk)
 | 
			
		||||
        self.assertFalse(agent.overdue_text_alert)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -683,7 +729,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertIn(self.agent.hostname, r.data)
 | 
			
		||||
        self.assertIn(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
 | 
			
		||||
        )
 | 
			
		||||
@@ -800,7 +846,7 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, data)
 | 
			
		||||
        self.assertEqual(r.data, data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -812,14 +858,14 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site)
 | 
			
		||||
 | 
			
		||||
        # Test client toggle maintenance mode
 | 
			
		||||
        data = {"type": "Client", "id": site.client.id, "action": True}
 | 
			
		||||
        data = {"type": "Client", "id": site.client.id, "action": True}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode)
 | 
			
		||||
 | 
			
		||||
        # Test site toggle maintenance mode
 | 
			
		||||
        data = {"type": "Site", "id": site.id, "action": False}
 | 
			
		||||
        data = {"type": "Site", "id": site.id, "action": False}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ urlpatterns = [
 | 
			
		||||
    path("listagents/", views.AgentsTableList.as_view()),
 | 
			
		||||
    path("listagentsnodetail/", views.list_agents_no_detail),
 | 
			
		||||
    path("<int:pk>/agenteditdetails/", views.agent_edit_details),
 | 
			
		||||
    path("byclient/<int:clientpk>/", views.by_client),
 | 
			
		||||
    path("bysite/<int:sitepk>/", views.by_site),
 | 
			
		||||
    path("overdueaction/", views.overdue_action),
 | 
			
		||||
    path("sendrawcmd/", views.send_raw_cmd),
 | 
			
		||||
    path("<pk>/agentdetail/", views.agent_detail),
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework import generics, status
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
@@ -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",
 | 
			
		||||
@@ -224,15 +212,32 @@ def send_raw_cmd(request):
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentsTableList(generics.ListAPIView):
 | 
			
		||||
    queryset = (
 | 
			
		||||
        Agent.objects.select_related("site")
 | 
			
		||||
        .prefetch_related("agentchecks")
 | 
			
		||||
        .only(
 | 
			
		||||
class AgentsTableList(APIView):
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        if "sitePK" in request.data.keys():
 | 
			
		||||
            queryset = (
 | 
			
		||||
                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", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
                .filter(site__client_id=request.data["clientPK"])
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            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",
 | 
			
		||||
@@ -247,11 +252,6 @@ class AgentsTableList(generics.ListAPIView):
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    serializer_class = AgentTableSerializer
 | 
			
		||||
 | 
			
		||||
    def list(self, request):
 | 
			
		||||
        queryset = self.get_queryset()
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        serializer = AgentTableSerializer(queryset, many=True, context=ctx)
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
@@ -269,66 +269,6 @@ def agent_edit_details(request, pk):
 | 
			
		||||
    return Response(AgentEditSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def by_client(request, clientpk):
 | 
			
		||||
    agents = (
 | 
			
		||||
        Agent.objects.select_related("site")
 | 
			
		||||
        .filter(site__client_id=clientpk)
 | 
			
		||||
        .prefetch_related("agentchecks")
 | 
			
		||||
        .only(
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "last_seen",
 | 
			
		||||
            "boot_time",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
    return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def by_site(request, sitepk):
 | 
			
		||||
    agents = (
 | 
			
		||||
        Agent.objects.filter(site_id=sitepk)
 | 
			
		||||
        .select_related("site")
 | 
			
		||||
        .prefetch_related("agentchecks")
 | 
			
		||||
        .only(
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "last_seen",
 | 
			
		||||
            "boot_time",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
    return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def overdue_action(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
@@ -344,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")
 | 
			
		||||
@@ -356,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")
 | 
			
		||||
@@ -372,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")),
 | 
			
		||||
@@ -382,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)
 | 
			
		||||
@@ -537,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(
 | 
			
		||||
@@ -617,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":
 | 
			
		||||
@@ -799,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,20 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from typing import TYPE_CHECKING, Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
SEVERITY_CHOICES = [
 | 
			
		||||
    ("info", "Informational"),
 | 
			
		||||
@@ -78,7 +91,7 @@ class Alert(models.Model):
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def create_availability_alert(cls, agent):
 | 
			
		||||
    def create_or_return_availability_alert(cls, agent):
 | 
			
		||||
        if not cls.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            return cls.objects.create(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
@@ -87,9 +100,11 @@ class Alert(models.Model):
 | 
			
		||||
                message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
 | 
			
		||||
                hidden=True,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return cls.objects.get(agent=agent, resolved=False)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def create_check_alert(cls, check):
 | 
			
		||||
    def create_or_return_check_alert(cls, check):
 | 
			
		||||
 | 
			
		||||
        if not cls.objects.filter(assigned_check=check, resolved=False).exists():
 | 
			
		||||
            return cls.objects.create(
 | 
			
		||||
@@ -99,9 +114,11 @@ class Alert(models.Model):
 | 
			
		||||
                message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
 | 
			
		||||
                hidden=True,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return cls.objects.get(assigned_check=check, resolved=False)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def create_task_alert(cls, task):
 | 
			
		||||
    def create_or_return_task_alert(cls, task):
 | 
			
		||||
 | 
			
		||||
        if not cls.objects.filter(assigned_task=task, resolved=False).exists():
 | 
			
		||||
            return cls.objects.create(
 | 
			
		||||
@@ -111,6 +128,305 @@ class Alert(models.Model):
 | 
			
		||||
                message=f"{task.agent.hostname} has task: {task.name} that failed.",
 | 
			
		||||
                hidden=True,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return cls.objects.get(assigned_task=task, resolved=False)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from autotasks.models import AutomatedTask
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        # set variables
 | 
			
		||||
        dashboard_severities = None
 | 
			
		||||
        email_severities = None
 | 
			
		||||
        text_severities = None
 | 
			
		||||
        always_dashboard = None
 | 
			
		||||
        always_email = None
 | 
			
		||||
        always_text = None
 | 
			
		||||
        alert_interval = None
 | 
			
		||||
        email_task = None
 | 
			
		||||
        text_task = None
 | 
			
		||||
 | 
			
		||||
        # check what the instance passed is
 | 
			
		||||
        if isinstance(instance, Agent):
 | 
			
		||||
            from agents.tasks import agent_outage_email_task, agent_outage_sms_task
 | 
			
		||||
 | 
			
		||||
            email_task = agent_outage_email_task
 | 
			
		||||
            text_task = agent_outage_sms_task
 | 
			
		||||
 | 
			
		||||
            email_alert = instance.overdue_email_alert
 | 
			
		||||
            text_alert = instance.overdue_text_alert
 | 
			
		||||
            dashboard_alert = instance.overdue_dashboard_alert
 | 
			
		||||
            alert_template = instance.alert_template
 | 
			
		||||
            maintenance_mode = instance.maintenance_mode
 | 
			
		||||
            alert_severity = "error"
 | 
			
		||||
            agent = instance
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = ["error"]
 | 
			
		||||
                email_severities = ["error"]
 | 
			
		||||
                text_severities = ["error"]
 | 
			
		||||
                always_dashboard = alert_template.agent_always_alert
 | 
			
		||||
                always_email = alert_template.agent_always_email
 | 
			
		||||
                always_text = alert_template.agent_always_text
 | 
			
		||||
                alert_interval = alert_template.agent_periodic_alert_days
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_availability_alert(instance)
 | 
			
		||||
            else:
 | 
			
		||||
                # check if there is an alert that exists
 | 
			
		||||
                if cls.objects.filter(agent=instance, resolved=False).exists():
 | 
			
		||||
                    alert = cls.objects.get(agent=instance, resolved=False)
 | 
			
		||||
                else:
 | 
			
		||||
                    alert = None
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, Check):
 | 
			
		||||
            from checks.tasks import (
 | 
			
		||||
                handle_check_email_alert_task,
 | 
			
		||||
                handle_check_sms_alert_task,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            email_task = handle_check_email_alert_task
 | 
			
		||||
            text_task = handle_check_sms_alert_task
 | 
			
		||||
 | 
			
		||||
            email_alert = instance.email_alert
 | 
			
		||||
            text_alert = instance.text_alert
 | 
			
		||||
            dashboard_alert = instance.dashboard_alert
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            alert_severity = instance.alert_severity
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.check_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.check_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.check_text_alert_severity
 | 
			
		||||
                always_dashboard = alert_template.check_always_alert
 | 
			
		||||
                always_email = alert_template.check_always_email
 | 
			
		||||
                always_text = alert_template.check_always_text
 | 
			
		||||
                alert_interval = alert_template.check_periodic_alert_days
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_check_alert(instance)
 | 
			
		||||
            else:
 | 
			
		||||
                # check if there is an alert that exists
 | 
			
		||||
                if cls.objects.filter(assigned_check=instance, resolved=False).exists():
 | 
			
		||||
                    alert = cls.objects.get(assigned_check=instance, resolved=False)
 | 
			
		||||
                else:
 | 
			
		||||
                    alert = None
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, AutomatedTask):
 | 
			
		||||
            from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
 | 
			
		||||
 | 
			
		||||
            email_task = handle_task_email_alert
 | 
			
		||||
            text_task = handle_task_sms_alert
 | 
			
		||||
 | 
			
		||||
            email_alert = instance.email_alert
 | 
			
		||||
            text_alert = instance.text_alert
 | 
			
		||||
            dashboard_alert = instance.dashboard_alert
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            alert_severity = instance.alert_severity
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.task_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.task_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.task_text_alert_severity
 | 
			
		||||
                always_dashboard = alert_template.task_always_alert
 | 
			
		||||
                always_email = alert_template.task_always_email
 | 
			
		||||
                always_text = alert_template.task_always_text
 | 
			
		||||
                alert_interval = alert_template.task_periodic_alert_days
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_task_alert(instance)
 | 
			
		||||
            else:
 | 
			
		||||
                # check if there is an alert that exists
 | 
			
		||||
                if cls.objects.filter(assigned_task=instance, resolved=False).exists():
 | 
			
		||||
                    alert = cls.objects.get(assigned_task=instance, resolved=False)
 | 
			
		||||
                else:
 | 
			
		||||
                    alert = None
 | 
			
		||||
        else:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if maintenance_mode or not alert:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # check if alert severity changed on check and update the alert
 | 
			
		||||
        if alert_severity != alert.severity:
 | 
			
		||||
            alert.severity = alert_severity
 | 
			
		||||
            alert.save(update_fields=["severity"])
 | 
			
		||||
 | 
			
		||||
        # create alert in dashboard if enabled
 | 
			
		||||
        if dashboard_alert or always_dashboard:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if alert_template and alert.severity not in dashboard_severities:  # type: ignore
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
        # send email if enabled
 | 
			
		||||
        if email_alert or always_email:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if alert_template and alert.severity not in email_severities:  # type: ignore
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                email_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_interval,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # send text if enabled
 | 
			
		||||
        if text_alert or always_text:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if alert_template and alert.severity not in text_severities:  # type: ignore
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                text_task.delay(pk=alert.pk, alert_interval=alert_interval)
 | 
			
		||||
 | 
			
		||||
        # check if any scripts should be run
 | 
			
		||||
        if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.action.pk,
 | 
			
		||||
                args=alert_template.action_args,
 | 
			
		||||
                timeout=alert_template.action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                full=True,
 | 
			
		||||
                run_on_any=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # command was successful
 | 
			
		||||
            if type(r) == dict:
 | 
			
		||||
                alert.action_retcode = r["retcode"]
 | 
			
		||||
                alert.action_stdout = r["stdout"]
 | 
			
		||||
                alert.action_stderr = r["stderr"]
 | 
			
		||||
                alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                alert.action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from autotasks.models import AutomatedTask
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        # set variables
 | 
			
		||||
        email_on_resolved = False
 | 
			
		||||
        text_on_resolved = False
 | 
			
		||||
        resolved_email_task = None
 | 
			
		||||
        resolved_text_task = None
 | 
			
		||||
 | 
			
		||||
        # check what the instance passed is
 | 
			
		||||
        if isinstance(instance, Agent):
 | 
			
		||||
            from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
 | 
			
		||||
 | 
			
		||||
            resolved_email_task = agent_recovery_email_task
 | 
			
		||||
            resolved_text_task = agent_recovery_sms_task
 | 
			
		||||
 | 
			
		||||
            alert_template = instance.alert_template
 | 
			
		||||
            alert = cls.objects.get(agent=instance, resolved=False)
 | 
			
		||||
            maintenance_mode = instance.maintenance_mode
 | 
			
		||||
            agent = instance
 | 
			
		||||
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.agent_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.agent_text_on_resolved
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, Check):
 | 
			
		||||
            from checks.tasks import (
 | 
			
		||||
                handle_resolved_check_email_alert_task,
 | 
			
		||||
                handle_resolved_check_sms_alert_task,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            resolved_email_task = handle_resolved_check_email_alert_task
 | 
			
		||||
            resolved_text_task = handle_resolved_check_sms_alert_task
 | 
			
		||||
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            alert = cls.objects.get(assigned_check=instance, resolved=False)
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.check_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.check_text_on_resolved
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, AutomatedTask):
 | 
			
		||||
            from autotasks.tasks import (
 | 
			
		||||
                handle_resolved_task_email_alert,
 | 
			
		||||
                handle_resolved_task_sms_alert,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            resolved_email_task = handle_resolved_task_email_alert
 | 
			
		||||
            resolved_text_task = handle_resolved_task_sms_alert
 | 
			
		||||
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            alert = cls.objects.get(assigned_task=instance, resolved=False)
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.task_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.task_text_on_resolved
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        alert.resolve()
 | 
			
		||||
 | 
			
		||||
        # check if a resolved email notification should be send
 | 
			
		||||
        if email_on_resolved and not alert.resolved_email_sent:
 | 
			
		||||
            resolved_email_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
        # check if resolved text should be sent
 | 
			
		||||
        if text_on_resolved and not alert.resolved_sms_sent:
 | 
			
		||||
            resolved_text_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
        # check if resolved script should be run
 | 
			
		||||
        if (
 | 
			
		||||
            alert_template
 | 
			
		||||
            and alert_template.resolved_action
 | 
			
		||||
            and not alert.resolved_action_run
 | 
			
		||||
        ):
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                args=alert_template.resolved_action_args,
 | 
			
		||||
                timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                full=True,
 | 
			
		||||
                run_on_any=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # command was successful
 | 
			
		||||
            if type(r) == dict:
 | 
			
		||||
                alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                    r["execution_time"]
 | 
			
		||||
                )
 | 
			
		||||
                alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertTemplate(models.Model):
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -53,3 +89,117 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
            r.json(),
 | 
			
		||||
            {"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
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkin/"
 | 
			
		||||
        agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent_updated,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": agent_updated.winagent_dl,
 | 
			
		||||
                "version": agent_updated.version,
 | 
			
		||||
                "inno": agent_updated.win_inno_exe,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent failed to update and still on same version
 | 
			
		||||
        payload = {
 | 
			
		||||
            "func": "hello",
 | 
			
		||||
            "agent_id": agent_updated.agent_id,
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent successful update
 | 
			
		||||
        payload["version"] = settings.LATEST_AGENT_VER
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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()),
 | 
			
		||||
@@ -17,4 +18,6 @@ urlpatterns = [
 | 
			
		||||
    path("choco/", views.Choco.as_view()),
 | 
			
		||||
    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()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from checks.utils import bytes2human
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
@@ -35,6 +36,8 @@ class CheckIn(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        updated = False
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if pyver.parse(request.data["version"]) > pyver.parse(
 | 
			
		||||
@@ -59,14 +62,8 @@ class CheckIn(APIView):
 | 
			
		||||
            ).update(status="completed")
 | 
			
		||||
 | 
			
		||||
        # handles any alerting actions
 | 
			
		||||
        agent.handle_alert(checkin=True)
 | 
			
		||||
 | 
			
		||||
        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")
 | 
			
		||||
        if Alert.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            Alert.handle_alert_resolve(agent)
 | 
			
		||||
 | 
			
		||||
        # get any pending actions
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():  # type: ignore
 | 
			
		||||
@@ -263,18 +260,13 @@ class SupersededWinUpdate(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckRunner(APIView):
 | 
			
		||||
    """
 | 
			
		||||
    For the windows golang agent
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
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,14 +325,13 @@ 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):
 | 
			
		||||
    """
 | 
			
		||||
    For the windows golang agent
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
@@ -314,6 +341,7 @@ class TaskRunner(APIView):
 | 
			
		||||
        return Response(TaskGOGetSerializer(task).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk, agentid):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
        from logs.models import AuditLog
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
@@ -325,8 +353,17 @@ class TaskRunner(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(last_run=djangotime.now())
 | 
			
		||||
 | 
			
		||||
        new_task = AutomatedTask.objects.get(pk=task.pk)
 | 
			
		||||
        new_task.handle_alert()
 | 
			
		||||
        status = "failing" if task.retcode != 0 else "passing"
 | 
			
		||||
 | 
			
		||||
        new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
 | 
			
		||||
        new_task.status = status
 | 
			
		||||
        new_task.save()
 | 
			
		||||
 | 
			
		||||
        if status == "passing":
 | 
			
		||||
            if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(new_task)
 | 
			
		||||
        else:
 | 
			
		||||
            Alert.handle_alert_failure(new_task)
 | 
			
		||||
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=agent.hostname,
 | 
			
		||||
@@ -475,3 +512,59 @@ class Installer(APIView):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChocoResult(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        action = get_object_or_404(PendingAction, pk=pk)
 | 
			
		||||
        results: str = request.data["results"]
 | 
			
		||||
 | 
			
		||||
        software_name = action.details["name"].lower()
 | 
			
		||||
        success = [
 | 
			
		||||
            "install",
 | 
			
		||||
            "of",
 | 
			
		||||
            software_name,
 | 
			
		||||
            "was",
 | 
			
		||||
            "successful",
 | 
			
		||||
            "installed",
 | 
			
		||||
        ]
 | 
			
		||||
        duplicate = [software_name, "already", "installed", "--force", "reinstall"]
 | 
			
		||||
        installed = False
 | 
			
		||||
 | 
			
		||||
        if all(x in results.lower() for x in success):
 | 
			
		||||
            installed = True
 | 
			
		||||
        elif all(x in results.lower() for x in duplicate):
 | 
			
		||||
            installed = True
 | 
			
		||||
 | 
			
		||||
        action.details["output"] = results
 | 
			
		||||
        action.details["installed"] = installed
 | 
			
		||||
        action.status = "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)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-02 04:15
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0030_agent_offline_time'),
 | 
			
		||||
        ('clients', '0009_auto_20210212_1408'),
 | 
			
		||||
        ('automation', '0007_policy_alert_template'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='policy',
 | 
			
		||||
            name='excluded_agents',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='agents.Agent'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='policy',
 | 
			
		||||
            name='excluded_clients',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Client'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='policy',
 | 
			
		||||
            name='excluded_sites',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Site'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -17,8 +17,18 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    excluded_sites = models.ManyToManyField(
 | 
			
		||||
        "clients.Site", related_name="policy_exclusions", blank=True
 | 
			
		||||
    )
 | 
			
		||||
    excluded_clients = models.ManyToManyField(
 | 
			
		||||
        "clients.Client", related_name="policy_exclusions", blank=True
 | 
			
		||||
    )
 | 
			
		||||
    excluded_agents = models.ManyToManyField(
 | 
			
		||||
        "agents.Agent", related_name="policy_exclusions", blank=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@@ -33,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
 | 
			
		||||
 | 
			
		||||
@@ -52,19 +65,41 @@ class Policy(BaseAuditModel):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def is_agent_excluded(self, agent):
 | 
			
		||||
        return (
 | 
			
		||||
            agent in self.excluded_agents.all()
 | 
			
		||||
            or agent.site in self.excluded_sites.all()
 | 
			
		||||
            or agent.client in self.excluded_clients.all()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def related_agents(self):
 | 
			
		||||
        return self.get_related("server") | self.get_related("workstation")
 | 
			
		||||
 | 
			
		||||
    def get_related(self, mon_type):
 | 
			
		||||
        explicit_agents = self.agents.filter(monitoring_type=mon_type)  # type: ignore
 | 
			
		||||
        explicit_clients = getattr(self, f"{mon_type}_clients").all()
 | 
			
		||||
        explicit_sites = getattr(self, f"{mon_type}_sites").all()
 | 
			
		||||
        explicit_agents = (
 | 
			
		||||
            self.agents.filter(monitoring_type=mon_type)  # type: ignore
 | 
			
		||||
            .exclude(
 | 
			
		||||
                pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
 | 
			
		||||
            )
 | 
			
		||||
            .exclude(site__in=self.excluded_sites.all())
 | 
			
		||||
            .exclude(site__client__in=self.excluded_clients.all())
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
 | 
			
		||||
            pk__in=self.excluded_clients.all()
 | 
			
		||||
        )
 | 
			
		||||
        explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
 | 
			
		||||
            pk__in=self.excluded_sites.all()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks = Policy.objects.none()
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks |= Agent.objects.filter(
 | 
			
		||||
            site__in=[
 | 
			
		||||
                site for site in explicit_sites if site.client not in explicit_clients
 | 
			
		||||
                site
 | 
			
		||||
                for site in explicit_sites
 | 
			
		||||
                if site.client not in explicit_clients
 | 
			
		||||
                and site.client not in self.excluded_clients.all()
 | 
			
		||||
            ],
 | 
			
		||||
            monitoring_type=mon_type,
 | 
			
		||||
        ).values_list("pk", flat=True)
 | 
			
		||||
@@ -119,23 +154,39 @@ class Policy(BaseAuditModel):
 | 
			
		||||
            client_policy = client.workstation_policy
 | 
			
		||||
            site_policy = site.workstation_policy
 | 
			
		||||
 | 
			
		||||
        if agent_policy and agent_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            agent_policy
 | 
			
		||||
            and agent_policy.active
 | 
			
		||||
            and not agent_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in agent_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
                    added_task_pks.append(task.pk)
 | 
			
		||||
        if site_policy and site_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            site_policy
 | 
			
		||||
            and site_policy.active
 | 
			
		||||
            and not site_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in site_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
                    added_task_pks.append(task.pk)
 | 
			
		||||
        if client_policy and client_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            client_policy
 | 
			
		||||
            and client_policy.active
 | 
			
		||||
            and not client_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in client_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
                    added_task_pks.append(task.pk)
 | 
			
		||||
 | 
			
		||||
        if default_policy and default_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            default_policy
 | 
			
		||||
            and default_policy.active
 | 
			
		||||
            and not default_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in default_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
@@ -205,7 +256,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        enforced_checks = list()
 | 
			
		||||
        policy_checks = list()
 | 
			
		||||
 | 
			
		||||
        if agent_policy and agent_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            agent_policy
 | 
			
		||||
            and agent_policy.active
 | 
			
		||||
            and not agent_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if agent_policy.enforced:
 | 
			
		||||
                for check in agent_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
@@ -213,7 +268,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                for check in agent_policy.policychecks.all():
 | 
			
		||||
                    policy_checks.append(check)
 | 
			
		||||
 | 
			
		||||
        if site_policy and site_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            site_policy
 | 
			
		||||
            and site_policy.active
 | 
			
		||||
            and not site_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if site_policy.enforced:
 | 
			
		||||
                for check in site_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
@@ -221,7 +280,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                for check in site_policy.policychecks.all():
 | 
			
		||||
                    policy_checks.append(check)
 | 
			
		||||
 | 
			
		||||
        if client_policy and client_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            client_policy
 | 
			
		||||
            and client_policy.active
 | 
			
		||||
            and not client_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if client_policy.enforced:
 | 
			
		||||
                for check in client_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
@@ -229,7 +292,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                for check in client_policy.policychecks.all():
 | 
			
		||||
                    policy_checks.append(check)
 | 
			
		||||
 | 
			
		||||
        if default_policy and default_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            default_policy
 | 
			
		||||
            and default_policy.active
 | 
			
		||||
            and not default_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if default_policy.enforced:
 | 
			
		||||
                for check in default_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,11 @@ from rest_framework.serializers import (
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
@@ -25,6 +27,9 @@ class PolicyTableSerializer(ModelSerializer):
 | 
			
		||||
    agents_count = SerializerMethodField(read_only=True)
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    alert_template = ReadOnlyField(source="alert_template.id")
 | 
			
		||||
    excluded_clients = ClientSerializer(many=True)
 | 
			
		||||
    excluded_sites = SiteSerializer(many=True)
 | 
			
		||||
    excluded_agents = AgentHostnameSerializer(many=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
@@ -31,7 +32,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        serializer = PolicyTableSerializer(policies, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -41,13 +42,13 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicySerializer(policy)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -79,13 +80,13 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            "desc": "policy desc",
 | 
			
		||||
            "active": True,
 | 
			
		||||
            "enforced": False,
 | 
			
		||||
            "copyId": policy.pk,
 | 
			
		||||
            "copyId": policy.pk,  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(f"/automation/policies/", data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(policy.autotasks.count(), 3)
 | 
			
		||||
        self.assertEqual(policy.policychecks.count(), 7)
 | 
			
		||||
        self.assertEqual(policy.autotasks.count(), 3)  # type: ignore
 | 
			
		||||
        self.assertEqual(policy.policychecks.count(), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -96,7 +97,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True, enforced=False)
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": "Test Policy Update",
 | 
			
		||||
@@ -121,7 +122,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_agent_checks_from_policies_task.assert_called_with(
 | 
			
		||||
            policypk=policy.pk, create_tasks=True
 | 
			
		||||
            policypk=policy.pk, create_tasks=True  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
@@ -138,7 +139,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        agents = baker.make_recipe(
 | 
			
		||||
            "agents.agent", site=site, policy=policy, _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -153,14 +154,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # create policy with tasks
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        url = f"/automation/{policy.pk}/policyautomatedtasks/"
 | 
			
		||||
        url = f"/automation/{policy.pk}/policyautomatedtasks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AutoTasksFieldSerializer(tasks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -170,14 +171,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        checks = self.create_checks(policy=policy)
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/{policy.pk}/policychecks/"
 | 
			
		||||
        url = f"/automation/{policy.pk}/policychecks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyCheckSerializer(checks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data), 7)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -199,7 +200,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        serializer = PolicyCheckStatusSerializer([managed_check], many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_policy_overview(self):
 | 
			
		||||
@@ -212,40 +213,40 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        clients = baker.make(
 | 
			
		||||
            "clients.Client",
 | 
			
		||||
            server_policy=cycle(policies),
 | 
			
		||||
            workstation_policy=cycle(policies),
 | 
			
		||||
            server_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            workstation_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            _quantity=5,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "clients.Site",
 | 
			
		||||
            client=cycle(clients),
 | 
			
		||||
            server_policy=cycle(policies),
 | 
			
		||||
            workstation_policy=cycle(policies),
 | 
			
		||||
            client=cycle(clients),  # type: ignore
 | 
			
		||||
            server_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            workstation_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make("clients.Site", client=cycle(clients), _quantity=3)
 | 
			
		||||
        baker.make("clients.Site", client=cycle(clients), _quantity=3)  # type: ignore
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        serializer = PolicyOverviewSerializer(clients, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_related(self):
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/related/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/related/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertIsInstance(resp.data["server_clients"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_clients"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["server_sites"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_sites"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["agents"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["server_clients"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_clients"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["server_sites"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_sites"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["agents"], list)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -257,16 +258,16 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # create policy managed tasks
 | 
			
		||||
        policy_tasks = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask", parent_task=task.id, _quantity=5
 | 
			
		||||
            "autotasks.AutomatedTask", parent_task=task.id, _quantity=5  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"
 | 
			
		||||
        url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
 | 
			
		||||
        resp = self.client.patch(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data), 5)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
@@ -284,7 +285,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called_once_with([task.pk for task in tasks])
 | 
			
		||||
        mock_task.assert_called_once_with([task.pk for task in tasks])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
@@ -299,7 +300,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "critical": "approve",
 | 
			
		||||
            "important": "approve",
 | 
			
		||||
            "moderate": "ignore",
 | 
			
		||||
@@ -325,11 +326,11 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{patch_policy.pk}/"
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{patch_policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": patch_policy.pk,
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "id": patch_policy.pk,  # type: ignore
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "critical": "approve",
 | 
			
		||||
            "important": "approve",
 | 
			
		||||
            "moderate": "ignore",
 | 
			
		||||
@@ -358,10 +359,10 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=6)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)  # type: ignore
 | 
			
		||||
        agents = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            site=cycle(sites),
 | 
			
		||||
            site=cycle(sites),  # type: ignore
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -371,24 +372,24 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test reset agents in site
 | 
			
		||||
        data = {"site": sites[0].id}
 | 
			
		||||
        data = {"site": sites[0].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site=sites[0])
 | 
			
		||||
        agents = Agent.objects.filter(site=sites[0])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            for k, v in inherit_fields.items():
 | 
			
		||||
                self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
 | 
			
		||||
 | 
			
		||||
        # test reset agents in client
 | 
			
		||||
        data = {"client": clients[1].id}
 | 
			
		||||
        data = {"client": clients[1].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site__client=clients[1])
 | 
			
		||||
        agents = Agent.objects.filter(site__client=clients[1])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            for k, v in inherit_fields.items():
 | 
			
		||||
@@ -425,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):
 | 
			
		||||
@@ -435,46 +455,46 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # Get Site and Client from an agent in list
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=5)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)  # type: ignore
 | 
			
		||||
        server_agents = baker.make_recipe(
 | 
			
		||||
            "agents.server_agent",
 | 
			
		||||
            site=cycle(sites),
 | 
			
		||||
            site=cycle(sites),  # type: ignore
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
        workstation_agents = baker.make_recipe(
 | 
			
		||||
            "agents.workstation_agent",
 | 
			
		||||
            site=cycle(sites),
 | 
			
		||||
            site=cycle(sites),  # type: ignore
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
 | 
			
		||||
        # Add Client to Policy
 | 
			
		||||
        policy.server_clients.add(server_agents[13].client)
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[15].client)
 | 
			
		||||
        policy.server_clients.add(server_agents[13].client)  # type: ignore
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[15].client)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_clients"]), 1)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_clients"]), 1)
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Site to Policy and the agents and sites length shouldn't change
 | 
			
		||||
        policy.server_sites.add(server_agents[13].site)
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[15].site)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)
 | 
			
		||||
        policy.server_sites.add(server_agents[13].site)  # type: ignore
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[15].site)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Agent to Policy and the agents length shouldn't change
 | 
			
		||||
        policy.agents.add(server_agents[13])
 | 
			
		||||
        policy.agents.add(workstation_agents[15])
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)
 | 
			
		||||
        policy.agents.add(server_agents[13])  # type: ignore
 | 
			
		||||
        policy.agents.add(workstation_agents[15])  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_policy_checks(self):
 | 
			
		||||
        from .tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
@@ -485,7 +505,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        # test policy assigned to agent
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id)
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure all checks were created. should be 7
 | 
			
		||||
        agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
 | 
			
		||||
@@ -535,7 +555,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site, policy=policy)
 | 
			
		||||
        self.create_checks(agent=agent, script=script)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id, create_tasks=True)
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id, create_tasks=True)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure each agent check says overriden_by_policy
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
 | 
			
		||||
@@ -843,7 +863,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
 | 
			
		||||
 | 
			
		||||
        # pick a policy check and delete it from the agent
 | 
			
		||||
        policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id
 | 
			
		||||
        policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id  # type: ignore
 | 
			
		||||
 | 
			
		||||
        delete_policy_check_task(policy_check_id)
 | 
			
		||||
 | 
			
		||||
@@ -868,7 +888,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # pick a policy check and update it with new values
 | 
			
		||||
        ping_check = (
 | 
			
		||||
            Policy.objects.get(pk=policy.id)
 | 
			
		||||
            Policy.objects.get(pk=policy.id)  # type: ignore
 | 
			
		||||
            .policychecks.filter(check_type="ping")
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
@@ -895,7 +915,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_tasks_from_policies_task(policy.id)
 | 
			
		||||
        generate_agent_tasks_from_policies_task(policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
 | 
			
		||||
 | 
			
		||||
@@ -905,14 +925,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        for task in agent_tasks:
 | 
			
		||||
            self.assertTrue(task.managed_by_policy)
 | 
			
		||||
            if task.name == "Task1":
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[0].id)
 | 
			
		||||
                self.assertEqual(task.name, tasks[0].name)
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[0].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[0].name)  # type: ignore
 | 
			
		||||
            if task.name == "Task2":
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[1].id)
 | 
			
		||||
                self.assertEqual(task.name, tasks[1].name)
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[1].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[1].name)  # type: ignore
 | 
			
		||||
            if task.name == "Task3":
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[2].id)
 | 
			
		||||
                self.assertEqual(task.name, tasks[2].name)
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[2].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[2].name)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    def test_delete_policy_tasks(self, delete_win_task_schedule):
 | 
			
		||||
@@ -922,10 +942,10 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        delete_policy_autotask_task(tasks[0].id)
 | 
			
		||||
        delete_policy_autotask_task(tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        delete_win_task_schedule.assert_called_with(
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.run_win_task.delay")
 | 
			
		||||
@@ -934,12 +954,12 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotask_task([task.id for task in tasks])
 | 
			
		||||
        run_win_policy_autotask_task([task.id for task in tasks])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        run_win_task.side_effect = [task.id for task in tasks]
 | 
			
		||||
        run_win_task.side_effect = [task.id for task in tasks]  # type: ignore
 | 
			
		||||
        self.assertEqual(run_win_task.call_count, 3)
 | 
			
		||||
        for task in tasks:
 | 
			
		||||
            run_win_task.assert_any_call(task.id)
 | 
			
		||||
        for task in tasks:  # type: ignore
 | 
			
		||||
            run_win_task.assert_any_call(task.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.enable_or_disable_win_task.delay")
 | 
			
		||||
    def test_update_policy_tasks(self, enable_or_disable_win_task):
 | 
			
		||||
@@ -952,17 +972,17 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        tasks[0].enabled = False
 | 
			
		||||
        tasks[0].save()
 | 
			
		||||
        tasks[0].enabled = False  # type: ignore
 | 
			
		||||
        tasks[0].save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id)
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id)  # type: ignore
 | 
			
		||||
        enable_or_disable_win_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
 | 
			
		||||
        self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id, update_agent=True)
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id, update_agent=True)  # type: ignore
 | 
			
		||||
        enable_or_disable_win_task.assert_called_with(
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id, False
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id, False  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.generate_tasks_from_policies")
 | 
			
		||||
@@ -984,3 +1004,110 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    def test_policy_exclusions(self, delete_task):
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", policy=policy)
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe(
 | 
			
		||||
            "agents.agent", policy=policy, monitoring_type="server"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add agent to policy exclusions
 | 
			
		||||
        policy.excluded_agents.set([agent])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
        policy.excluded_agents.clear()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add policy exclusions to site
 | 
			
		||||
        policy.excluded_sites.set([agent.site])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks and reset
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
        policy.excluded_sites.clear()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add policy exclusions to client
 | 
			
		||||
        policy.excluded_clients.set([agent.client])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks and reset
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
        policy.excluded_clients.clear()  # type: ignore
 | 
			
		||||
        agent.policy = None
 | 
			
		||||
        agent.save()
 | 
			
		||||
 | 
			
		||||
        # test on default policy
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
        core.server_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add policy exclusions to client
 | 
			
		||||
        policy.excluded_clients.set([agent.client])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
    def test_removing_duplicate_pending_task_actions(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_creating_checks_with_assigned_tasks(self):
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -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()),
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import DateTimeField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
@@ -224,165 +223,25 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        create_win_task_schedule.delay(task.pk)
 | 
			
		||||
 | 
			
		||||
    def handle_alert(self) -> None:
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
        from autotasks.tasks import (
 | 
			
		||||
            handle_resolved_task_email_alert,
 | 
			
		||||
            handle_resolved_task_sms_alert,
 | 
			
		||||
            handle_task_email_alert,
 | 
			
		||||
            handle_task_sms_alert,
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
            or self.email_alert
 | 
			
		||||
            or self.text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.task_always_alert
 | 
			
		||||
                    or alert_template.task_always_email
 | 
			
		||||
                    or alert_template.task_always_text
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.status = "failing" if self.retcode != 0 else "passing"
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if self.agent.maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # see if agent has an alert template and use that
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        # resolve alert if it exists
 | 
			
		||||
        if self.status == "passing":
 | 
			
		||||
            if Alert.objects.filter(assigned_task=self, resolved=False).exists():
 | 
			
		||||
                alert = Alert.objects.get(assigned_task=self, resolved=False)
 | 
			
		||||
                alert.resolve()
 | 
			
		||||
 | 
			
		||||
                # check if resolved email should be send
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template and alert_template.task_email_on_resolved
 | 
			
		||||
                ) and not alert.resolved_email_sent:
 | 
			
		||||
                    handle_resolved_task_email_alert.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved text should be sent
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template and alert_template.task_text_on_resolved
 | 
			
		||||
                ) and not alert.resolved_sms_sent:
 | 
			
		||||
                    handle_resolved_task_sms_alert.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved script should be run
 | 
			
		||||
                if not alert.resolved_action_run and (
 | 
			
		||||
                    alert_template and alert_template.resolved_action
 | 
			
		||||
                ):
 | 
			
		||||
 | 
			
		||||
                    r = self.agent.run_script(
 | 
			
		||||
                        scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                        args=alert_template.resolved_action_args,
 | 
			
		||||
                        timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                        wait=True,
 | 
			
		||||
                        full=True,
 | 
			
		||||
                        run_on_any=True,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    # command was successful
 | 
			
		||||
                    if type(r) == dict:
 | 
			
		||||
                        alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                        alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                        alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                        alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                            r["execution_time"]
 | 
			
		||||
                        )
 | 
			
		||||
                        alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                        alert.save()
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for task: {self.name}"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        # create alert if task is failing
 | 
			
		||||
        else:
 | 
			
		||||
            if not Alert.objects.filter(assigned_task=self, resolved=False).exists():
 | 
			
		||||
 | 
			
		||||
                # check if alert should be created and if not return
 | 
			
		||||
                if (
 | 
			
		||||
                    self.dashboard_alert
 | 
			
		||||
                    or self.email_alert
 | 
			
		||||
                    or self.text_alert
 | 
			
		||||
                    or (
 | 
			
		||||
                        alert_template
 | 
			
		||||
                        and (
 | 
			
		||||
                            alert_template.task_always_alert
 | 
			
		||||
                            or alert_template.task_always_email
 | 
			
		||||
                            or alert_template.task_always_text
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                ):
 | 
			
		||||
                    alert = Alert.create_task_alert(self)
 | 
			
		||||
                else:
 | 
			
		||||
                    return
 | 
			
		||||
            else:
 | 
			
		||||
                alert = Alert.objects.get(assigned_task=self, resolved=False)
 | 
			
		||||
 | 
			
		||||
                # check if alert severity changed on task and update the alert
 | 
			
		||||
                if self.alert_severity != alert.severity:
 | 
			
		||||
                    alert.severity = self.alert_severity
 | 
			
		||||
                    alert.save(update_fields=["severity"])
 | 
			
		||||
 | 
			
		||||
            # create alert in dashboard if enabled
 | 
			
		||||
            if self.dashboard_alert or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.task_dashboard_alert_severity
 | 
			
		||||
                and alert_template.task_always_alert
 | 
			
		||||
            ):
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
            # send email if enabled
 | 
			
		||||
            if self.email_alert or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.task_email_alert_severity
 | 
			
		||||
                and alert_template.task_always_email
 | 
			
		||||
            ):
 | 
			
		||||
                handle_task_email_alert.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_template=alert_template.task_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # send text if enabled
 | 
			
		||||
            if self.text_alert or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.task_text_alert_severity
 | 
			
		||||
                and alert_template.task_always_text
 | 
			
		||||
            ):
 | 
			
		||||
                handle_task_sms_alert.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_template=alert_template.task_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # check if any scripts should be run
 | 
			
		||||
            if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
                r = self.agent.run_script(
 | 
			
		||||
                    scriptpk=alert_template.action.pk,
 | 
			
		||||
                    args=alert_template.action_args,
 | 
			
		||||
                    timeout=alert_template.action_timeout,
 | 
			
		||||
                    wait=True,
 | 
			
		||||
                    full=True,
 | 
			
		||||
                    run_on_any=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # command was successful
 | 
			
		||||
                if type(r) == dict:
 | 
			
		||||
                    alert.action_retcode = r["retcode"]
 | 
			
		||||
                    alert.action_stdout = r["stdout"]
 | 
			
		||||
                    alert.action_stderr = r["stderr"]
 | 
			
		||||
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                    alert.action_run = djangotime.now()
 | 
			
		||||
                    alert.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(
 | 
			
		||||
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for task: {self.name}"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
    def send_email(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"
 | 
			
		||||
@@ -394,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"
 | 
			
		||||
@@ -413,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 = (
 | 
			
		||||
@@ -427,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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}")
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/checks/migrations/0023_check_run_interval.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/checks/migrations/0023_check_run_interval.py
									
									
									
									
									
										Normal 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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -3,26 +3,19 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
from statistics import mean
 | 
			
		||||
from typing import Any, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from .tasks import (
 | 
			
		||||
    handle_check_email_alert_task,
 | 
			
		||||
    handle_check_sms_alert_task,
 | 
			
		||||
    handle_resolved_check_email_alert_task,
 | 
			
		||||
    handle_resolved_check_sms_alert_task,
 | 
			
		||||
)
 | 
			
		||||
from .utils import bytes2human
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
@@ -100,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
 | 
			
		||||
@@ -188,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:
 | 
			
		||||
@@ -205,7 +202,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"  # type: ignore
 | 
			
		||||
            return f"{self.get_check_type_display()}: Drive {self.disk} - {text}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.name}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "cpuload" or self.check_type == "memory":
 | 
			
		||||
@@ -216,7 +213,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            return f"{self.get_check_type_display()} > {text}"  # type: ignore
 | 
			
		||||
            return f"{self.get_check_type_display()} - {text}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.svc_display_name}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
@@ -266,161 +263,27 @@ class Check(BaseAuditModel):
 | 
			
		||||
            "modified_time",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def handle_alert(self) -> None:
 | 
			
		||||
        from alerts.models import Alert, AlertTemplate
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if self.agent.maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # see if agent has an alert template and use that
 | 
			
		||||
        alert_template: Union[AlertTemplate, None] = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        # resolve alert if it exists
 | 
			
		||||
        if self.status == "passing":
 | 
			
		||||
            if Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                alert = Alert.objects.get(assigned_check=self, resolved=False)
 | 
			
		||||
                alert.resolve()
 | 
			
		||||
 | 
			
		||||
                # check if a resolved email notification should be send
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.check_email_on_resolved
 | 
			
		||||
                    and not alert.resolved_email_sent
 | 
			
		||||
                ):
 | 
			
		||||
                    handle_resolved_check_email_alert_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved text should be sent
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.check_text_on_resolved
 | 
			
		||||
                    and not alert.resolved_sms_sent
 | 
			
		||||
                ):
 | 
			
		||||
                    handle_resolved_check_sms_alert_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved script should be run
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.resolved_action
 | 
			
		||||
                    and not alert.resolved_action_run
 | 
			
		||||
                ):
 | 
			
		||||
                    r = self.agent.run_script(
 | 
			
		||||
                        scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                        args=alert_template.resolved_action_args,
 | 
			
		||||
                        timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                        wait=True,
 | 
			
		||||
                        full=True,
 | 
			
		||||
                        run_on_any=True,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    # command was successful
 | 
			
		||||
                    if type(r) == dict:
 | 
			
		||||
                        alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                        alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                        alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                        alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                            r["execution_time"]
 | 
			
		||||
                        )
 | 
			
		||||
                        alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                        alert.save()
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for {self.check_type} check"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        elif self.fail_count >= self.fails_b4_alert:
 | 
			
		||||
            if not Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
 | 
			
		||||
                # check if alert should be created and if not return
 | 
			
		||||
                if (
 | 
			
		||||
                    self.dashboard_alert
 | 
			
		||||
                    or self.email_alert
 | 
			
		||||
                    or self.text_alert
 | 
			
		||||
                    or (
 | 
			
		||||
                        alert_template
 | 
			
		||||
                        and (
 | 
			
		||||
                            alert_template.check_always_alert
 | 
			
		||||
                            or alert_template.check_always_email
 | 
			
		||||
                            or alert_template.check_always_text
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                ):
 | 
			
		||||
                    alert = Alert.create_check_alert(self)
 | 
			
		||||
                else:
 | 
			
		||||
                    return
 | 
			
		||||
            else:
 | 
			
		||||
                alert = Alert.objects.get(assigned_check=self, resolved=False)
 | 
			
		||||
 | 
			
		||||
                # check if alert severity changed on check and update the alert
 | 
			
		||||
                if self.alert_severity != alert.severity:
 | 
			
		||||
                    alert.severity = self.alert_severity
 | 
			
		||||
                    alert.save(update_fields=["severity"])
 | 
			
		||||
 | 
			
		||||
            # create alert in dashboard if enabled
 | 
			
		||||
            if self.dashboard_alert or (
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
            or self.email_alert
 | 
			
		||||
            or self.text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.check_dashboard_alert_severity
 | 
			
		||||
                and alert_template.check_always_alert
 | 
			
		||||
            ):
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
            # send email if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.email_sent
 | 
			
		||||
                and self.email_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.check_email_alert_severity
 | 
			
		||||
                and alert_template.check_always_email
 | 
			
		||||
            ):
 | 
			
		||||
                handle_check_email_alert_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.check_always_alert
 | 
			
		||||
                    or alert_template.check_always_email
 | 
			
		||||
                    or alert_template.check_always_text
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # send text if enabled
 | 
			
		||||
            if self.text_alert or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.check_text_alert_severity
 | 
			
		||||
                and alert_template.check_always_text
 | 
			
		||||
            ):
 | 
			
		||||
                handle_check_sms_alert_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # check if any scripts should be run
 | 
			
		||||
            if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
                r = self.agent.run_script(
 | 
			
		||||
                    scriptpk=alert_template.action.pk,
 | 
			
		||||
                    args=alert_template.action_args,
 | 
			
		||||
                    timeout=alert_template.action_timeout,
 | 
			
		||||
                    wait=True,
 | 
			
		||||
                    full=True,
 | 
			
		||||
                    run_on_any=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # command was successful
 | 
			
		||||
                if type(r) == dict:
 | 
			
		||||
                    alert.action_retcode = r["retcode"]
 | 
			
		||||
                    alert.action_stdout = r["stdout"]
 | 
			
		||||
                    alert.action_stderr = r["stderr"]
 | 
			
		||||
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                    alert.action_run = djangotime.now()
 | 
			
		||||
                    alert.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(
 | 
			
		||||
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for {self.check_type} check{r}"
 | 
			
		||||
                    )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_check_history(self, value: int, more_info: Any = None) -> None:
 | 
			
		||||
        CheckHistory.objects.create(check_history=self, y=value, results=more_info)
 | 
			
		||||
 | 
			
		||||
    def handle_checkv2(self, data):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        # cpuload or mem checks
 | 
			
		||||
        if self.check_type == "cpuload" or self.check_type == "memory":
 | 
			
		||||
@@ -629,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"
 | 
			
		||||
@@ -653,11 +516,14 @@ class Check(BaseAuditModel):
 | 
			
		||||
            self.fail_count += 1
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
 | 
			
		||||
            if self.fail_count >= self.fails_b4_alert:
 | 
			
		||||
                Alert.handle_alert_failure(self)
 | 
			
		||||
 | 
			
		||||
        elif self.status == "passing":
 | 
			
		||||
            self.fail_count = 0
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
 | 
			
		||||
        self.handle_alert()
 | 
			
		||||
            if Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(self)
 | 
			
		||||
 | 
			
		||||
        return self.status
 | 
			
		||||
 | 
			
		||||
@@ -701,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,
 | 
			
		||||
@@ -724,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):
 | 
			
		||||
@@ -751,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"
 | 
			
		||||
 | 
			
		||||
@@ -833,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:
 | 
			
		||||
@@ -882,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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
from logging import warning
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
@@ -311,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)
 | 
			
		||||
@@ -970,24 +963,205 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
    def test_handle_eventlog_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        eventlog = baker.make_recipe("checks.eventlog_check", agent=self.agent)
 | 
			
		||||
        eventlog = baker.make_recipe(
 | 
			
		||||
            "checks.eventlog_check",
 | 
			
		||||
            event_type="warning",
 | 
			
		||||
            fail_when="contains",
 | 
			
		||||
            event_id=123,
 | 
			
		||||
            alert_severity="warning",
 | 
			
		||||
            agent=self.agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
        data = {}
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": eventlog.id,
 | 
			
		||||
            "log": [
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "warning",
 | 
			
		||||
                    "eventID": 150,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "warning",
 | 
			
		||||
                    "eventID": 123,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "error",
 | 
			
		||||
                    "eventID": 123,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "error",
 | 
			
		||||
                    "eventID": 123,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        # self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        # test failing when contains
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {}
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        # resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        # self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "warning")
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {}
 | 
			
		||||
        # test passing when not contains and message
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        # resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        # self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        # test failing when not contains and message and source
 | 
			
		||||
        eventlog.fail_when = "not_contains"
 | 
			
		||||
        eventlog.alert_severity = "error"
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.event_source = "doesnt exist"
 | 
			
		||||
        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")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing when contains with source and message
 | 
			
		||||
        eventlog.event_message = "test"
 | 
			
		||||
        eventlog.event_source = "source"
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        # test failing with wildcard not contains and source
 | 
			
		||||
        eventlog.event_id_is_wildcard = True
 | 
			
		||||
        eventlog.event_source = "doesn't exist"
 | 
			
		||||
        eventlog.event_message = ""
 | 
			
		||||
        eventlog.event_id = 0
 | 
			
		||||
        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")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing with wildcard contains
 | 
			
		||||
        eventlog.event_source = ""
 | 
			
		||||
        eventlog.event_message = ""
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        # test failing with wildcard contains and message
 | 
			
		||||
        eventlog.fail_when = "contains"
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.alert_severity = "info"
 | 
			
		||||
        eventlog.event_message = "test"
 | 
			
		||||
        eventlog.event_source = ""
 | 
			
		||||
        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")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test passing with wildcard not contains message and source
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.event_source = "doesnt exist"
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        # 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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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",)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
@@ -27,6 +28,18 @@ var (
 | 
			
		||||
	DownloadUrl string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var netTransport = &http.Transport{
 | 
			
		||||
	Dial: (&net.Dialer{
 | 
			
		||||
		Timeout: 5 * time.Second,
 | 
			
		||||
	}).Dial,
 | 
			
		||||
	TLSHandshakeTimeout: 5 * time.Second,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var netClient = &http.Client{
 | 
			
		||||
	Timeout:   time.Second * 900,
 | 
			
		||||
	Transport: netTransport,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func downloadAgent(filepath string) (err error) {
 | 
			
		||||
 | 
			
		||||
	out, err := os.Create(filepath)
 | 
			
		||||
@@ -35,7 +48,7 @@ func downloadAgent(filepath string) (err error) {
 | 
			
		||||
	}
 | 
			
		||||
	defer out.Close()
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Get(DownloadUrl)
 | 
			
		||||
	resp, err := netClient.Get(DownloadUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -59,7 +72,6 @@ func main() {
 | 
			
		||||
	localMesh := flag.String("local-mesh", "", "Use local mesh agent")
 | 
			
		||||
	silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
 | 
			
		||||
	cert := flag.String("cert", "", "Path to ca.pem")
 | 
			
		||||
	timeout := flag.String("timeout", "", "Timeout for subprocess calls")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	var debug bool = false
 | 
			
		||||
@@ -93,10 +105,6 @@ func main() {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-cert", *cert)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(strings.TrimSpace(*timeout)) != 0 {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-timeout", *timeout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Rdp == "1" {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-rdp")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,11 +33,20 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
 | 
			
		||||
 | 
			
		||||
    Try
 | 
			
		||||
    {
 | 
			
		||||
        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:\Windows\Temp\trmm*\*'
 | 
			
		||||
        
 | 
			
		||||
        $DefenderStatus = Get-MpComputerStatus | select  AntivirusEnabled
 | 
			
		||||
        if ($DefenderStatus -match "True") {
 | 
			
		||||
            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:\Windows\Temp\trmm*\*'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    Catch {
 | 
			
		||||
        # pass
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    Try
 | 
			
		||||
    {  
 | 
			
		||||
        Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
 | 
			
		||||
        Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
 | 
			
		||||
        write-host ('Extracting...')
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +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,
 | 
			
		||||
            "client_tree_sort": request.user.client_tree_sort,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/logs/migrations/0012_auto_20210228_0943.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/logs/migrations/0012_auto_20210228_0943.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-02-28 09:43
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('logs', '0011_auto_20201119_0854'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='pendingaction',
 | 
			
		||||
            name='action_type',
 | 
			
		||||
            field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('taskaction', 'Scheduled Task Action'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install')], max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -9,6 +9,7 @@ ACTION_TYPE_CHOICES = [
 | 
			
		||||
    ("schedreboot", "Scheduled Reboot"),
 | 
			
		||||
    ("taskaction", "Scheduled Task Action"),
 | 
			
		||||
    ("agentupdate", "Agent Update"),
 | 
			
		||||
    ("chocoinstall", "Chocolatey Software Install"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AUDIT_ACTION_TYPE_CHOICES = [
 | 
			
		||||
@@ -249,9 +250,12 @@ 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"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self):
 | 
			
		||||
@@ -261,6 +265,9 @@ class PendingAction(models.Model):
 | 
			
		||||
        elif self.action_type == "agentupdate":
 | 
			
		||||
            return f"Agent update to {self.details['version']}"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "chocoinstall":
 | 
			
		||||
            return f"{self.details['name']} software install"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "taskaction":
 | 
			
		||||
            if self.details["action"] == "taskdelete":
 | 
			
		||||
                return "Device pending task deletion"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,9 @@ from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .serializers import PendingActionSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAuditViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -177,63 +176,97 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_agent_pending_actions(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        pending_actions = baker.make(
 | 
			
		||||
    def test_get_pending_actions(self):
 | 
			
		||||
        url = "/logs/pendingactions/"
 | 
			
		||||
        agent1 = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        agent2 = baker.make_recipe("agents.online_agent")
 | 
			
		||||
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
            agent=agent1,
 | 
			
		||||
            action_type="chocoinstall",
 | 
			
		||||
            details={"name": "googlechrome", "output": None, "installed": False},
 | 
			
		||||
            _quantity=12,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent2,
 | 
			
		||||
            action_type="chocoinstall",
 | 
			
		||||
            status="completed",
 | 
			
		||||
            details={"name": "adobereader", "output": None, "installed": False},
 | 
			
		||||
            _quantity=14,
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/logs/{agent.pk}/pendingactions/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PendingActionSerializer(pending_actions, many=True)
 | 
			
		||||
        data = {"showCompleted": False}
 | 
			
		||||
        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"], 14)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["total"], 26)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 6)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        PendingAction.objects.filter(action_type="chocoinstall").update(
 | 
			
		||||
            status="completed"
 | 
			
		||||
        )
 | 
			
		||||
        data = {"showCompleted": True}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data["actions"]), 26)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["completed_count"], 26)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["total"], 26)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
        data = {"showCompleted": True, "agentPK": agent1.pk}
 | 
			
		||||
        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"], 12)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["total"], 12)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_all_pending_actions(self):
 | 
			
		||||
        url = "/logs/allpendingactions/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PendingActionSerializer(pending_actions, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 6)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_cancel_pending_action(self, nats_cmd):
 | 
			
		||||
        url = "/logs/cancelpendingaction/"
 | 
			
		||||
        # TODO fix this TypeError: Object of type coroutine is not JSON serializable
 | 
			
		||||
        """ agent = baker.make("agents.Agent", version="1.1.1")
 | 
			
		||||
        pending_action = baker.make(
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        url = "/logs/pendingactions/"
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        action = baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="schedreboot",
 | 
			
		||||
            details={
 | 
			
		||||
                "time": "2021-01-13 18:20:00",
 | 
			
		||||
                "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"pk": pending_action.id}
 | 
			
		||||
        resp = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        data = {"pk": action.pk}  # type: ignore
 | 
			
		||||
        r = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "delschedtask",
 | 
			
		||||
            "schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"},
 | 
			
		||||
        }
 | 
			
		||||
        nats_cmd.assert_called_with(nats_data, timeout=10)
 | 
			
		||||
 | 
			
		||||
        # try request again and it should fail since pending action doesn't exist
 | 
			
		||||
        resp = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404) """
 | 
			
		||||
        # try request again and it should 404 since pending action doesn't exist
 | 
			
		||||
        r = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        action2 = baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="schedreboot",
 | 
			
		||||
            details={
 | 
			
		||||
                "time": "2021-01-13 18:20:00",
 | 
			
		||||
                "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"pk": action2.pk}  # type: ignore
 | 
			
		||||
        nats_cmd.return_value = "error deleting sched task"
 | 
			
		||||
        r = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "error deleting sched task")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,9 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("pendingactions/", views.PendingActions.as_view()),
 | 
			
		||||
    path("auditlogs/", views.GetAuditLogs.as_view()),
 | 
			
		||||
    path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()),
 | 
			
		||||
    path("<int:pk>/pendingactions/", views.agent_pending_actions),
 | 
			
		||||
    path("allpendingactions/", views.all_pending_actions),
 | 
			
		||||
    path("cancelpendingaction/", views.cancel_pending_action),
 | 
			
		||||
    path("debuglog/<mode>/<hostname>/<order>/", views.debug_log),
 | 
			
		||||
    path("downloadlog/", views.download_log),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -106,34 +106,46 @@ class FilterOptionsAuditLog(APIView):
 | 
			
		||||
        return Response("error", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def agent_pending_actions(request, pk):
 | 
			
		||||
    action = PendingAction.objects.filter(agent__pk=pk)
 | 
			
		||||
    return Response(PendingActionSerializer(action, many=True).data)
 | 
			
		||||
class PendingActions(APIView):
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        status_filter = "completed" if request.data["showCompleted"] else "pending"
 | 
			
		||||
        if "agentPK" in request.data.keys():
 | 
			
		||||
            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()
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def all_pending_actions(request):
 | 
			
		||||
    actions = PendingAction.objects.all().select_related("agent")
 | 
			
		||||
    return Response(PendingActionSerializer(actions, many=True).data)
 | 
			
		||||
        ret = {
 | 
			
		||||
            "actions": PendingActionSerializer(actions, many=True).data,
 | 
			
		||||
            "completed_count": completed,
 | 
			
		||||
            "total": total,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
    def delete(self, request):
 | 
			
		||||
        action = get_object_or_404(PendingAction, pk=request.data["pk"])
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "delschedtask",
 | 
			
		||||
            "schedtaskpayload": {"name": action.details["taskname"]},
 | 
			
		||||
        }
 | 
			
		||||
        r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error(r)
 | 
			
		||||
 | 
			
		||||
@api_view(["DELETE"])
 | 
			
		||||
def cancel_pending_action(request):
 | 
			
		||||
    action = get_object_or_404(PendingAction, pk=request.data["pk"])
 | 
			
		||||
    if not action.agent.has_gotasks:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.1 or greater")
 | 
			
		||||
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "delschedtask",
 | 
			
		||||
        "schedtaskpayload": {"name": action.details["taskname"]},
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        return notify_error(r)
 | 
			
		||||
 | 
			
		||||
    action.delete()
 | 
			
		||||
    return Response(f"{action.agent.hostname}: {action.description} was cancelled")
 | 
			
		||||
        action.delete()
 | 
			
		||||
        return Response(f"{action.agent.hostname}: {action.description} was cancelled")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
 
 | 
			
		||||
@@ -9,52 +9,28 @@ class TestNatsAPIViews(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_nats_wmi(self):
 | 
			
		||||
        url = "/natsapi/wmi/"
 | 
			
		||||
        baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14)
 | 
			
		||||
    def test_nats_agents(self):
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3
 | 
			
		||||
            "agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=14
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.offline_agent", version=settings.LATEST_AGENT_VER, _quantity=6
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7)
 | 
			
		||||
 | 
			
		||||
        url = "/natsapi/online/agents/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.json()["agent_ids"]), 17)
 | 
			
		||||
        self.assertEqual(len(r.json()["agent_ids"]), 14)
 | 
			
		||||
 | 
			
		||||
    def test_natscheckin_patch(self):
 | 
			
		||||
        from logs.models import PendingAction
 | 
			
		||||
 | 
			
		||||
        url = "/natsapi/checkin/"
 | 
			
		||||
        agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent_updated,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": agent_updated.winagent_dl,
 | 
			
		||||
                "version": agent_updated.version,
 | 
			
		||||
                "inno": agent_updated.win_inno_exe,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent failed to update and still on same version
 | 
			
		||||
        payload = {
 | 
			
		||||
            "func": "hello",
 | 
			
		||||
            "agent_id": agent_updated.agent_id,
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        url = "/natsapi/offline/agents/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(len(r.json()["agent_ids"]), 11)
 | 
			
		||||
 | 
			
		||||
        # test agent successful update
 | 
			
		||||
        payload["version"] = settings.LATEST_AGENT_VER
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "completed")
 | 
			
		||||
        action.delete()
 | 
			
		||||
        url = "/natsapi/asdjaksdasd/agents/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,6 @@ from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("natsinfo/", views.nats_info),
 | 
			
		||||
    path("checkin/", views.NatsCheckIn.as_view()),
 | 
			
		||||
    path("syncmesh/", views.SyncMeshNodeID.as_view()),
 | 
			
		||||
    path("winupdates/", views.NatsWinUpdates.as_view()),
 | 
			
		||||
    path("choco/", views.NatsChoco.as_view()),
 | 
			
		||||
    path("wmi/", views.NatsWMI.as_view()),
 | 
			
		||||
    path("offline/", views.OfflineAgents.as_view()),
 | 
			
		||||
    path("<str:stat>/agents/", views.NatsAgents.as_view()),
 | 
			
		||||
    path("logcrash/", views.LogCrash.as_view()),
 | 
			
		||||
    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.decorators import (
 | 
			
		||||
    api_view,
 | 
			
		||||
    authentication_classes,
 | 
			
		||||
@@ -15,12 +11,7 @@ from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.serializers import WinAgentSerializer
 | 
			
		||||
from agents.tasks import handle_agent_recovery_task
 | 
			
		||||
from checks.utils import bytes2human
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error
 | 
			
		||||
from winupdate.models import WinUpdate
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
@@ -32,269 +23,24 @@ def nats_info(request):
 | 
			
		||||
    return Response({"user": "tacticalrmm", "password": settings.SECRET_KEY})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NatsCheckIn(APIView):
 | 
			
		||||
 | 
			
		||||
class NatsAgents(APIView):
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        updated = False
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if pyver.parse(request.data["version"]) > pyver.parse(
 | 
			
		||||
            agent.version
 | 
			
		||||
        ) or pyver.parse(request.data["version"]) == pyver.parse(
 | 
			
		||||
            settings.LATEST_AGENT_VER
 | 
			
		||||
        ):
 | 
			
		||||
            updated = True
 | 
			
		||||
        agent.version = request.data["version"]
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["version", "last_seen"])
 | 
			
		||||
    def get(self, request, stat: str):
 | 
			
		||||
        if stat not in ["online", "offline"]:
 | 
			
		||||
            return notify_error("invalid request")
 | 
			
		||||
 | 
			
		||||
        # change agent update pending status to completed if agent has just updated
 | 
			
		||||
        if (
 | 
			
		||||
            updated
 | 
			
		||||
            and agent.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            agent.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).update(status="completed")
 | 
			
		||||
 | 
			
		||||
        # handles any alerting actions
 | 
			
		||||
        agent.handle_alert(checkin=True)
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
 | 
			
		||||
        # get any pending actions
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():  # type: ignore
 | 
			
		||||
            agent.handle_pending_actions()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
 | 
			
		||||
        if request.data["func"] == "disks":
 | 
			
		||||
            disks = request.data["disks"]
 | 
			
		||||
            new = []
 | 
			
		||||
            for disk in disks:
 | 
			
		||||
                tmp = {}
 | 
			
		||||
                for _, _ in disk.items():
 | 
			
		||||
                    tmp["device"] = disk["device"]
 | 
			
		||||
                    tmp["fstype"] = disk["fstype"]
 | 
			
		||||
                    tmp["total"] = bytes2human(disk["total"])
 | 
			
		||||
                    tmp["used"] = bytes2human(disk["used"])
 | 
			
		||||
                    tmp["free"] = bytes2human(disk["free"])
 | 
			
		||||
                    tmp["percent"] = int(disk["percent"])
 | 
			
		||||
                new.append(tmp)
 | 
			
		||||
 | 
			
		||||
            serializer.is_valid(raise_exception=True)
 | 
			
		||||
            serializer.save(disks=new)
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
 | 
			
		||||
        if request.data["func"] == "loggedonuser":
 | 
			
		||||
            if request.data["logged_in_username"] != "None":
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save(last_logged_in_user=request.data["logged_in_username"])
 | 
			
		||||
                return Response("ok")
 | 
			
		||||
 | 
			
		||||
        if request.data["func"] == "software":
 | 
			
		||||
            raw: SoftwareList = request.data["software"]
 | 
			
		||||
            if not isinstance(raw, list):
 | 
			
		||||
                return notify_error("err")
 | 
			
		||||
 | 
			
		||||
            sw = filter_software(raw)
 | 
			
		||||
            if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
			
		||||
                InstalledSoftware(agent=agent, software=sw).save()
 | 
			
		||||
            else:
 | 
			
		||||
                s = agent.installedsoftware_set.first()  # type: ignore
 | 
			
		||||
                s.software = sw
 | 
			
		||||
                s.save(update_fields=["software"])
 | 
			
		||||
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # called once during tacticalagent windows service startup
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if not agent.choco_installed:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
 | 
			
		||||
 | 
			
		||||
        time.sleep(0.5)
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SyncMeshNodeID(APIView):
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if agent.mesh_node_id != request.data["nodeid"]:
 | 
			
		||||
            agent.mesh_node_id = request.data["nodeid"]
 | 
			
		||||
            agent.save(update_fields=["mesh_node_id"])
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NatsChoco(APIView):
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent.choco_installed = request.data["installed"]
 | 
			
		||||
        agent.save(update_fields=["choco_installed"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NatsWinUpdates(APIView):
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        reboot_policy: str = agent.get_patch_policy().reboot_after_install
 | 
			
		||||
        reboot = False
 | 
			
		||||
 | 
			
		||||
        if reboot_policy == "always":
 | 
			
		||||
            reboot = True
 | 
			
		||||
 | 
			
		||||
        if request.data["needs_reboot"]:
 | 
			
		||||
            if reboot_policy == "required":
 | 
			
		||||
                reboot = True
 | 
			
		||||
            elif reboot_policy == "never":
 | 
			
		||||
                agent.needs_reboot = True
 | 
			
		||||
                agent.save(update_fields=["needs_reboot"])
 | 
			
		||||
 | 
			
		||||
        if reboot:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
			
		||||
            logger.info(f"{agent.hostname} is rebooting after updates were installed.")
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        u = agent.winupdates.filter(guid=request.data["guid"]).last()  # type: ignore
 | 
			
		||||
        success: bool = request.data["success"]
 | 
			
		||||
        if success:
 | 
			
		||||
            u.result = "success"
 | 
			
		||||
            u.downloaded = True
 | 
			
		||||
            u.installed = True
 | 
			
		||||
            u.date_installed = djangotime.now()
 | 
			
		||||
            u.save(
 | 
			
		||||
                update_fields=[
 | 
			
		||||
                    "result",
 | 
			
		||||
                    "downloaded",
 | 
			
		||||
                    "installed",
 | 
			
		||||
                    "date_installed",
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
        ret: list[str] = []
 | 
			
		||||
        agents = Agent.objects.only(
 | 
			
		||||
            "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
        )
 | 
			
		||||
        if stat == "online":
 | 
			
		||||
            ret = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
        else:
 | 
			
		||||
            u.result = "failed"
 | 
			
		||||
            u.save(update_fields=["result"])
 | 
			
		||||
            ret = [i.agent_id for i in agents if i.status != "online"]
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        updates = request.data["wua_updates"]
 | 
			
		||||
        for update in updates:
 | 
			
		||||
            if agent.winupdates.filter(guid=update["guid"]).exists():  # type: ignore
 | 
			
		||||
                u = agent.winupdates.filter(guid=update["guid"]).last()  # type: ignore
 | 
			
		||||
                u.downloaded = update["downloaded"]
 | 
			
		||||
                u.installed = update["installed"]
 | 
			
		||||
                u.save(update_fields=["downloaded", "installed"])
 | 
			
		||||
            else:
 | 
			
		||||
                try:
 | 
			
		||||
                    kb = "KB" + update["kb_article_ids"][0]
 | 
			
		||||
                except:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                WinUpdate(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    guid=update["guid"],
 | 
			
		||||
                    kb=kb,
 | 
			
		||||
                    title=update["title"],
 | 
			
		||||
                    installed=update["installed"],
 | 
			
		||||
                    downloaded=update["downloaded"],
 | 
			
		||||
                    description=update["description"],
 | 
			
		||||
                    severity=update["severity"],
 | 
			
		||||
                    categories=update["categories"],
 | 
			
		||||
                    category_ids=update["category_ids"],
 | 
			
		||||
                    kb_article_ids=update["kb_article_ids"],
 | 
			
		||||
                    more_info_urls=update["more_info_urls"],
 | 
			
		||||
                    support_url=update["support_url"],
 | 
			
		||||
                    revision_number=update["revision_number"],
 | 
			
		||||
                ).save()
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
 | 
			
		||||
        # more superseded updates cleanup
 | 
			
		||||
        if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
 | 
			
		||||
            for u in agent.winupdates.filter(  # type: ignore
 | 
			
		||||
                date_installed__isnull=True, result="failed"
 | 
			
		||||
            ).exclude(installed=True):
 | 
			
		||||
                u.delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupersededWinUpdate(APIView):
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        updates = agent.winupdates.filter(guid=request.data["guid"])  # type: ignore
 | 
			
		||||
        for u in updates:
 | 
			
		||||
            u.delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NatsWMI(APIView):
 | 
			
		||||
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        agents = Agent.objects.only(
 | 
			
		||||
            "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
        )
 | 
			
		||||
        online: list[str] = [
 | 
			
		||||
            i.agent_id
 | 
			
		||||
            for i in agents
 | 
			
		||||
            if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
        return Response({"agent_ids": online})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OfflineAgents(APIView):
 | 
			
		||||
    authentication_classes = []  # type: ignore
 | 
			
		||||
    permission_classes = []  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        agents = Agent.objects.only(
 | 
			
		||||
            "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
        )
 | 
			
		||||
        offline: list[str] = [
 | 
			
		||||
            i.agent_id for i in agents if i.has_nats and i.status != "online"
 | 
			
		||||
        ]
 | 
			
		||||
        return Response({"agent_ids": offline})
 | 
			
		||||
        return Response({"agent_ids": ret})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogCrash(APIView):
 | 
			
		||||
@@ -305,4 +51,10 @@ class LogCrash(APIView):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["last_seen"])
 | 
			
		||||
 | 
			
		||||
        if hasattr(settings, "DEBUGTEST") and settings.DEBUGTEST:
 | 
			
		||||
            logger.info(
 | 
			
		||||
                f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -6,17 +6,17 @@ celery==5.0.5
 | 
			
		||||
certifi==2020.12.5
 | 
			
		||||
cffi==1.14.5
 | 
			
		||||
chardet==4.0.0
 | 
			
		||||
cryptography==3.4.6
 | 
			
		||||
cryptography==3.4.7
 | 
			
		||||
decorator==4.4.2
 | 
			
		||||
Django==3.1.7
 | 
			
		||||
django-cors-headers==3.7.0
 | 
			
		||||
django-rest-knox==4.1.0
 | 
			
		||||
djangorestframework==3.12.2
 | 
			
		||||
djangorestframework==3.12.3
 | 
			
		||||
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
 | 
			
		||||
urllib3==1.26.3
 | 
			
		||||
twilio==6.55.0
 | 
			
		||||
urllib3==1.26.4
 | 
			
		||||
uWSGI==2.0.19.1
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
vine==5.0.0
 | 
			
		||||
websockets==8.1
 | 
			
		||||
zipp==3.4.0
 | 
			
		||||
zipp==3.4.1
 | 
			
		||||
 
 | 
			
		||||
@@ -201,5 +201,47 @@
 | 
			
		||||
        "name": "Display Message To User",
 | 
			
		||||
        "description": "Displays a popup message to the currently logged on user",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "VerifyAntivirus.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/beejayzed",
 | 
			
		||||
        "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"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Rename_Computer.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
        "name": "Rename Computer",
 | 
			
		||||
        "description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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": {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,11 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import ChocoLog, ChocoSoftware, InstalledSoftware
 | 
			
		||||
from .models import ChocoSoftware, InstalledSoftware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChocoAdmin(admin.ModelAdmin):
 | 
			
		||||
    readonly_fields = ("added",)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChocoLogAdmin(admin.ModelAdmin):
 | 
			
		||||
    readonly_fields = ("time",)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
admin.site.register(ChocoSoftware, ChocoAdmin)
 | 
			
		||||
admin.site.register(ChocoLog, ChocoLogAdmin)
 | 
			
		||||
admin.site.register(InstalledSoftware)
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,4 +1,5 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,4 +18,4 @@ class Command(BaseCommand):
 | 
			
		||||
            ChocoSoftware.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
        ChocoSoftware(chocos=chocos).save()
 | 
			
		||||
        self.stdout.write("Chocos saved to db")
 | 
			
		||||
        self.stdout.write(self.style.SUCCESS("Chocos saved to db"))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								api/tacticalrmm/software/migrations/0003_delete_chocolog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/tacticalrmm/software/migrations/0003_delete_chocolog.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-01 21:43
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('software', '0002_auto_20200810_0544'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.DeleteModel(
 | 
			
		||||
            name='ChocoLog',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -8,23 +8,7 @@ class ChocoSoftware(models.Model):
 | 
			
		||||
    added = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        from .serializers import ChocoSoftwareSerializer
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            str(len(ChocoSoftwareSerializer(self).data["chocos"])) + f" - {self.added}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChocoLog(models.Model):
 | 
			
		||||
    agent = models.ForeignKey(Agent, related_name="chocolog", on_delete=models.CASCADE)
 | 
			
		||||
    name = models.CharField(max_length=255)
 | 
			
		||||
    version = models.CharField(max_length=255)
 | 
			
		||||
    message = models.TextField()
 | 
			
		||||
    installed = models.BooleanField(default=False)
 | 
			
		||||
    time = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.agent.hostname} | {self.name} | {self.time}"
 | 
			
		||||
        return f"{len(self.chocos)} - {self.added}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstalledSoftware(models.Model):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,6 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .models import ChocoSoftware, InstalledSoftware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChocoSoftwareSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ChocoSoftware
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
from .models import InstalledSoftware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstalledSoftwareSerializer(serializers.ModelSerializer):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
from .models import ChocoLog
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def install_program(pk, name, version):
 | 
			
		||||
    agent = Agent.objects.get(pk=pk)
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "installwithchoco",
 | 
			
		||||
        "choco_prog_name": name,
 | 
			
		||||
        "choco_prog_ver": version,
 | 
			
		||||
    }
 | 
			
		||||
    r: str = asyncio.run(agent.nats_cmd(nats_data, timeout=915))
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        logger.error(f"Failed to install {name} {version} on {agent.salt_id}: timeout")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        output = r.lower()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Failed to install {name} {version} on {agent.salt_id}: {e}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    success = [
 | 
			
		||||
        "install",
 | 
			
		||||
        "of",
 | 
			
		||||
        name.lower(),
 | 
			
		||||
        "was",
 | 
			
		||||
        "successful",
 | 
			
		||||
        "installed",
 | 
			
		||||
    ]
 | 
			
		||||
    duplicate = [name.lower(), "already", "installed", "--force", "reinstall"]
 | 
			
		||||
 | 
			
		||||
    installed = False
 | 
			
		||||
 | 
			
		||||
    if all(x in output for x in success):
 | 
			
		||||
        installed = True
 | 
			
		||||
        logger.info(f"Successfully installed {name} {version} on {agent.salt_id}")
 | 
			
		||||
    elif all(x in output for x in duplicate):
 | 
			
		||||
        logger.warning(f"Already installed: {name} {version} on {agent.salt_id}")
 | 
			
		||||
    else:
 | 
			
		||||
        logger.error(f"Something went wrong - {name} {version} on {agent.salt_id}")
 | 
			
		||||
 | 
			
		||||
    ChocoLog(
 | 
			
		||||
        agent=agent, name=name, version=version, message=output, installed=installed
 | 
			
		||||
    ).save()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import ChocoLog
 | 
			
		||||
from .models import ChocoSoftware
 | 
			
		||||
from .serializers import InstalledSoftwareSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -15,29 +17,17 @@ class TestSoftwareViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_chocos_get(self):
 | 
			
		||||
        url = "/software/chocos/"
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        with open(os.path.join(settings.BASE_DIR, "software/chocos.json")) as f:
 | 
			
		||||
            chocos = json.load(f)
 | 
			
		||||
 | 
			
		||||
        if ChocoSoftware.objects.exists():
 | 
			
		||||
            ChocoSoftware.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
        ChocoSoftware(chocos=chocos).save()
 | 
			
		||||
        resp = self.client.get(url)
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("software.tasks.install_program.delay")
 | 
			
		||||
    def test_chocos_install(self, install_program):
 | 
			
		||||
        url = "/software/install/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
 | 
			
		||||
        # test a call where agent doesn't exist
 | 
			
		||||
        invalid_data = {"pk": 500, "name": "Test Software", "version": "1.0.0"}
 | 
			
		||||
        resp = self.client.post(url, invalid_data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"pk": agent.pk, "name": "Test Software", "version": "1.0.0"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        install_program.assert_called_with(data["pk"], data["name"], data["version"])
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_chocos_installed(self):
 | 
			
		||||
        # test a call where agent doesn't exist
 | 
			
		||||
        resp = self.client.get("/software/installed/500/", format="json")
 | 
			
		||||
@@ -64,26 +54,3 @@ class TestSoftwareViews(TacticalTestCase):
 | 
			
		||||
        self.assertEquals(resp.data, serializer.data)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSoftwareTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_install_program(self, nats_cmd):
 | 
			
		||||
        from .tasks import install_program
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        nats_cmd.return_value = "install of git was successful"
 | 
			
		||||
        _ = install_program(agent.pk, "git", "2.3.4")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "installwithchoco",
 | 
			
		||||
                "choco_prog_name": "git",
 | 
			
		||||
                "choco_prog_ver": "2.3.4",
 | 
			
		||||
            },
 | 
			
		||||
            timeout=915,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists())
 | 
			
		||||
 
 | 
			
		||||
@@ -2,31 +2,51 @@ import asyncio
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.utils import filter_software, notify_error
 | 
			
		||||
 | 
			
		||||
from .models import ChocoSoftware, InstalledSoftware
 | 
			
		||||
from .serializers import ChocoSoftwareSerializer, InstalledSoftwareSerializer
 | 
			
		||||
from .tasks import install_program
 | 
			
		||||
from .serializers import InstalledSoftwareSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def chocos(request):
 | 
			
		||||
    chocos = ChocoSoftware.objects.last()
 | 
			
		||||
    return Response(ChocoSoftwareSerializer(chocos).data["chocos"])
 | 
			
		||||
    return Response(ChocoSoftware.objects.last().chocos)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def install(request):
 | 
			
		||||
    pk = request.data["pk"]
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    if pyver.parse(agent.version) < pyver.parse("1.4.8"):
 | 
			
		||||
        return notify_error("Requires agent v1.4.8")
 | 
			
		||||
 | 
			
		||||
    name = request.data["name"]
 | 
			
		||||
    version = request.data["version"]
 | 
			
		||||
    install_program.delay(pk, name, version)
 | 
			
		||||
    return Response(f"{name} will be installed shortly on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
    action = PendingAction.objects.create(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        action_type="chocoinstall",
 | 
			
		||||
        details={"name": name, "output": None, "installed": False},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "installwithchoco",
 | 
			
		||||
        "choco_prog_name": name,
 | 
			
		||||
        "pending_action_pk": action.pk,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(nats_data, timeout=2))
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        action.delete()
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        f"{name} will be installed shortly on {agent.hostname}. Check the Pending Actions menu to see the status/output"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@@ -43,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":
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,8 @@ class AuditMiddleware:
 | 
			
		||||
                # Here's our fully formed and authenticated (or not, depending on credentials) request
 | 
			
		||||
                request = view.initialize_request(request)
 | 
			
		||||
            except (AttributeError, TypeError):
 | 
			
		||||
                from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
                # Can't initialize the request from this view. Fallback to using default permission classes
 | 
			
		||||
                request = APIView().initialize_request(request)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import os
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 | 
			
		||||
BASE_DIR = Path(__file__).resolve().parent.parent
 | 
			
		||||
 | 
			
		||||
SCRIPTS_DIR = "/srv/salt/scripts"
 | 
			
		||||
 | 
			
		||||
@@ -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.18"
 | 
			
		||||
TRMM_VERSION = "0.4.30"
 | 
			
		||||
 | 
			
		||||
# bump this version everytime vue code is changed
 | 
			
		||||
# to alert user they need to manually refresh their browser
 | 
			
		||||
APP_VER = "0.0.116"
 | 
			
		||||
APP_VER = "0.0.123"
 | 
			
		||||
 | 
			
		||||
# https://github.com/wh1te909/rmmagent
 | 
			
		||||
LATEST_AGENT_VER = "1.4.7"
 | 
			
		||||
LATEST_AGENT_VER = "1.4.13"
 | 
			
		||||
 | 
			
		||||
MESH_VER = "0.7.73"
 | 
			
		||||
MESH_VER = "0.7.93"
 | 
			
		||||
 | 
			
		||||
# for the update script, bump when need to recreate venv or npm install
 | 
			
		||||
PIP_VER = "10"
 | 
			
		||||
NPM_VER = "8"
 | 
			
		||||
PIP_VER = "12"
 | 
			
		||||
NPM_VER = "11"
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
@@ -39,11 +39,9 @@ except ImportError:
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
INSTALLED_APPS = [
 | 
			
		||||
    "django.contrib.admin",
 | 
			
		||||
    "django.contrib.auth",
 | 
			
		||||
    "django.contrib.contenttypes",
 | 
			
		||||
    "django.contrib.sessions",
 | 
			
		||||
    "django.contrib.messages",
 | 
			
		||||
    "django.contrib.staticfiles",
 | 
			
		||||
    "rest_framework",
 | 
			
		||||
    "rest_framework.authtoken",
 | 
			
		||||
@@ -66,10 +64,20 @@ INSTALLED_APPS = [
 | 
			
		||||
    "natsapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if not "TRAVIS" in os.environ and not "AZPIPELINE" in os.environ:
 | 
			
		||||
    if DEBUG:
 | 
			
		||||
if not "AZPIPELINE" in os.environ:
 | 
			
		||||
    if DEBUG:  # type: ignore
 | 
			
		||||
        INSTALLED_APPS += ("django_extensions",)
 | 
			
		||||
 | 
			
		||||
if "AZPIPELINE" in os.environ:
 | 
			
		||||
    ADMIN_ENABLED = False
 | 
			
		||||
 | 
			
		||||
if ADMIN_ENABLED:  # type: ignore
 | 
			
		||||
    INSTALLED_APPS += (
 | 
			
		||||
        "django.contrib.admin",
 | 
			
		||||
        "django.contrib.messages",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
    "django.middleware.security.SecurityMiddleware",
 | 
			
		||||
    "django.contrib.sessions.middleware.SessionMiddleware",
 | 
			
		||||
@@ -78,10 +86,11 @@ MIDDLEWARE = [
 | 
			
		||||
    "django.middleware.csrf.CsrfViewMiddleware",
 | 
			
		||||
    "django.contrib.auth.middleware.AuthenticationMiddleware",
 | 
			
		||||
    "tacticalrmm.middleware.AuditMiddleware",
 | 
			
		||||
    "django.contrib.messages.middleware.MessageMiddleware",
 | 
			
		||||
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if ADMIN_ENABLED:  # type: ignore
 | 
			
		||||
    MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
 | 
			
		||||
 | 
			
		||||
REST_KNOX = {
 | 
			
		||||
    "TOKEN_TTL": timedelta(hours=5),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,10 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.urls import include, path
 | 
			
		||||
from knox import views as knox_views
 | 
			
		||||
 | 
			
		||||
from accounts.views import CheckCreds, LoginView
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path(settings.ADMIN_URL, admin.site.urls),
 | 
			
		||||
    path("checkcreds/", CheckCreds.as_view()),
 | 
			
		||||
    path("login/", LoginView.as_view()),
 | 
			
		||||
    path("logout/", knox_views.LogoutView.as_view()),
 | 
			
		||||
@@ -27,3 +25,8 @@ urlpatterns = [
 | 
			
		||||
    path("accounts/", include("accounts.urls")),
 | 
			
		||||
    path("natsapi/", include("natsapi.urls")),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
 | 
			
		||||
    from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
    urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ def auto_approve_updates_task():
 | 
			
		||||
    # scheduled task that checks and approves updates daily
 | 
			
		||||
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
        "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
@@ -46,7 +46,7 @@ def auto_approve_updates_task():
 | 
			
		||||
def check_agent_update_schedule_task():
 | 
			
		||||
    # scheduled task that installs updates on agents if enabled
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
        "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    online = [
 | 
			
		||||
        i
 | 
			
		||||
 
 | 
			
		||||
@@ -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 .
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ COPY --from=CREATE_VENV_STAGE ${VIRTUAL_ENV} ${VIRTUAL_ENV}
 | 
			
		||||
# install deps
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get upgrade -y && \
 | 
			
		||||
    apt-get install -y --no-install-recommends git && \
 | 
			
		||||
    apt-get install -y --no-install-recommends git rsync && \
 | 
			
		||||
    rm -rf /var/lib/apt/lists/* && \
 | 
			
		||||
    go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo && \
 | 
			
		||||
    groupadd -g 1000 "${TACTICAL_USER}" && \
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ if [ "$1" = 'tactical-init' ]; then
 | 
			
		||||
  test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
 | 
			
		||||
 | 
			
		||||
  # copy container data to volume
 | 
			
		||||
  cp -af ${TACTICAL_TMP_DIR}/. ${TACTICAL_DIR}/
 | 
			
		||||
  rsync -a --no-perms --no-owner --delete --exclude "tmp/*" --exclude "certs/*" --exclude="api/tacticalrmm/private/*" "${TACTICAL_TMP_DIR}/" "${TACTICAL_DIR}/"
 | 
			
		||||
 | 
			
		||||
  until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
 | 
			
		||||
    echo "waiting for postgresql container to be ready..."
 | 
			
		||||
@@ -106,6 +106,7 @@ MESH_SITE = 'https://${MESH_HOST}'
 | 
			
		||||
MESH_TOKEN_KEY = '${MESH_TOKEN}'
 | 
			
		||||
REDIS_HOST    = '${REDIS_HOST}'
 | 
			
		||||
MESH_WS_URL = 'ws://${MESH_CONTAINER}:443'
 | 
			
		||||
ADMIN_ENABLED = False
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								docs/docs/contributing.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								docs/docs/contributing.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
# Contributing
 | 
			
		||||
 | 
			
		||||
### Contributing to the docs
 | 
			
		||||
 | 
			
		||||
Docs are built with [MKDocs for Material](https://squidfunk.github.io/mkdocs-material/)
 | 
			
		||||
 | 
			
		||||
To setup a local environment to add/edit to this documentation site:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
mkdir ~/rmmdocs && cd ~/rmmdocs
 | 
			
		||||
git clone https://github.com/wh1te909/tacticalrmm.git .
 | 
			
		||||
python3 -m venv env
 | 
			
		||||
source env/bin/activate
 | 
			
		||||
pip install --upgrade pip
 | 
			
		||||
pip install --upgrade setuptools wheel
 | 
			
		||||
pip install -r api/tacticalrmm/requirements-dev.txt
 | 
			
		||||
cd docs
 | 
			
		||||
mkdocs serve
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Open your browser and navigate to `http://yourserverip:8005`
 | 
			
		||||
 | 
			
		||||
Add/edit markdown files in the `docs/docs` folder and you'll see live changes at the url above.
 | 
			
		||||
 | 
			
		||||
Edit `docs/mkdocs.yml` to edit structure and add new files.
 | 
			
		||||
 | 
			
		||||
Full mkdocs documentation [here](https://squidfunk.github.io/mkdocs-material/getting-started/)
 | 
			
		||||
 | 
			
		||||
Once finished, [create a pull request](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github) to the `develop` branch for review.
 | 
			
		||||
@@ -10,29 +10,9 @@ Linux / Mac agents are currently under development.
 | 
			
		||||
Yes, you will just need to setup local DNS for the 3 subdomains, either by editing host files on all your agents or through a local DNS server.
 | 
			
		||||
#### I am locked out of the web UI. How do I reset my password?
 | 
			
		||||
 | 
			
		||||
SSH into your server and run these commands:
 | 
			
		||||
 | 
			
		||||
!!!note
 | 
			
		||||
    The code below will reset the password for the account that was created during install.
 | 
			
		||||
    To reset a password for a different user, you should use the web UI (see the next question below), but can also do so through the command line by replacing<br/>
 | 
			
		||||
    `#!python user = User.objects.first()`<br/>
 | 
			
		||||
    with<br/>
 | 
			
		||||
    `#!python user = User.objects.get(username='someuser')`
 | 
			
		||||
    <br/>
 | 
			
		||||
    in the code snippet below.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
tactical@tacrmm:~$ /rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py shell
 | 
			
		||||
Python 3.9.2 (default, Feb 21 2021, 00:50:28)
 | 
			
		||||
[GCC 9.3.0] on linux
 | 
			
		||||
Type "help", "copyright", "credits" or "license" for more information.
 | 
			
		||||
(InteractiveConsole)
 | 
			
		||||
>>> from accounts.models import User
 | 
			
		||||
>>> user = User.objects.first()
 | 
			
		||||
>>> user.set_password("superSekret123")
 | 
			
		||||
>>> user.save()
 | 
			
		||||
>>> exit()
 | 
			
		||||
SSH into your server and run:
 | 
			
		||||
```bash
 | 
			
		||||
/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py reset_password <username>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
@@ -42,17 +22,22 @@ From the web UI, click **Settings > User Administration** and then right-click o
 | 
			
		||||

 | 
			
		||||
<br/><br/>
 | 
			
		||||
Or from the command line:<br/>
 | 
			
		||||
```python
 | 
			
		||||
tactical@tacrmm:~$ /rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py shell
 | 
			
		||||
Python 3.9.2 (default, Feb 21 2021, 00:50:28)
 | 
			
		||||
[GCC 9.3.0] on linux
 | 
			
		||||
Type "help", "copyright", "credits" or "license" for more information.
 | 
			
		||||
(InteractiveConsole)
 | 
			
		||||
>>> from accounts.models import User
 | 
			
		||||
>>> user = User.objects.get(username='someuser')
 | 
			
		||||
>>> user.totp_key = None
 | 
			
		||||
>>> user.save(update_fields=['totp_key'])
 | 
			
		||||
>>> exit()
 | 
			
		||||
```bash
 | 
			
		||||
/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py reset_2fa <username>
 | 
			
		||||
```
 | 
			
		||||
<br/>
 | 
			
		||||
Then simply log out of the web UI and next time the user logs in they will be redirected to the 2FA setup page which will present a barcode to be scanned with the Authenticator app.
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
#### How do I recover my MeshCentral login credentials?
 | 
			
		||||
 | 
			
		||||
From Tactical's web UI: *Settings > Global Settings > MeshCentral*
 | 
			
		||||
 | 
			
		||||
Copy the username then ssh into the server and run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cd /meshcentral/
 | 
			
		||||
sudo systemctl stop meshcentral
 | 
			
		||||
node node_modules/meshcentral --resetaccount <username> --pass <newpassword>
 | 
			
		||||
sudo systemctl start meshcentral
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										19
									
								
								docs/docs/functions/django_admin.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								docs/docs/functions/django_admin.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# Django Admin
 | 
			
		||||
 | 
			
		||||
!!!warning
 | 
			
		||||
    Do not use the django admin unless you really know what you're doing.<br />You should never need to access it unless you are familiar with django or are instructed to do something here by one of the developers.
 | 
			
		||||
 | 
			
		||||
The django admin is basically a web interface for the postgres database.
 | 
			
		||||
 | 
			
		||||
As of Tactical RMM v0.4.19, the django admin is disabled by default.
 | 
			
		||||
 | 
			
		||||
To enable it, edit `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` and change `ADMIN_ENABLED` from `False` to `True` then `sudo systemctl restart rmm`
 | 
			
		||||
 | 
			
		||||
Login to the django admin using the same credentials as your normal web ui login.
 | 
			
		||||
 | 
			
		||||
If you did not save the django admin url (which was printed out at the end of the install script), check the `local_settings.py` file referenced above for the `ADMIN_URL` variable. Then simply append the value of this variable to your api domain (`https://api.yourdomain.com/`) to get the full url.
 | 
			
		||||
 | 
			
		||||
Example of a full django admin url:
 | 
			
		||||
```
 | 
			
		||||
https://api.example.com/JwboKNYb3v6K93Fvtcz0G3vUM17LMTSZggOUAxa97jQfAh0P5xosEk7u2PPkjEfdOtucUp/
 | 
			
		||||
```
 | 
			
		||||
@@ -8,8 +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/)
 | 
			
		||||
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
 | 
			
		||||
## [LIVE DEMO](https://rmm.tacticalrmm.io/)
 | 
			
		||||
 | 
			
		||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ The following optional arguments can be passed to any of the installation method
 | 
			
		||||
```
 | 
			
		||||
-log debug
 | 
			
		||||
```
 | 
			
		||||
Will print very verbose logging during agent install. Usefull for troubleshooting agent install.
 | 
			
		||||
Will print very verbose logging during agent install. Useful for troubleshooting agent install.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
-silent
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -52,8 +52,8 @@ ufw allow ssh
 | 
			
		||||
 | 
			
		||||
Allow ssh from only allowed IP's (__highly__ recommended)
 | 
			
		||||
```bash
 | 
			
		||||
ufw allow from X.X.X.X to any port 22
 | 
			
		||||
ufw allow from X.X.X.X to any port 22
 | 
			
		||||
ufw allow proto tcp from X.X.X.X to any port 22
 | 
			
		||||
ufw allow proto tcp from X.X.X.X to any port 22
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Enable and activate the firewall
 | 
			
		||||
@@ -69,7 +69,7 @@ We'll be using `example.com` as our domain for this example.
 | 
			
		||||
    The RMM uses 3 different sites. The Vue frontend e.g. `rmm.example.com` which is where you'll be accesing your RMM from the browser, the REST backend e.g. `api.example.com` and Meshcentral e.g. `mesh.example.com`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Get the public IP of your server with `curl icanhazip.com`<br/>
 | 
			
		||||
Get the public IP of your server with `curl https://icanhazip.tacticalrmm.io`<br/>
 | 
			
		||||
Open the DNS manager of wherever the domain you purchased is hosted.<br/>
 | 
			
		||||
Create 3 A records: `rmm`, `api` and `mesh` and point them to the public IP of your server:
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +110,8 @@ Create a login for the RMM web UI:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
A bunch of URLS / usernames / passwords will be printed out at the end of the install script. Save these somewhere safe.
 | 
			
		||||
A bunch of URLS / usernames / passwords will be printed out at the end of the install script. **Save these somewhere safe.** [Recover them if you didn't](faq.md#how-do-i-recover-my-meshcentral-login-credentials)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Copy the url for the meshagent exe (`https://mesh.example.com/agentinvite?c=......`), paste it in your browser and download the mesh agent:
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								docs/docs/management_cmds.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								docs/docs/management_cmds.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
# Management Commands
 | 
			
		||||
 | 
			
		||||
To run any of the management commands you must first activate the python virtual env:
 | 
			
		||||
```bash
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
source ../env/bin/activate
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Reset a user's password
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py reset_password <username>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Reset a user's 2fa token
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py reset_2fa <username>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Find all agents that have X software installed
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py find_software "adobe"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Show outdated online agents
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py show_outdated_agents
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Log out all active web sessions
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py delete_tokens
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Check for orphaned tasks on all agents and remove them
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py remove_orphaned_tasks
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Create a MeshCentral agent invite link
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py get_mesh_exe_url
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Bulk update agent offline/overdue time
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
wget -q https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/api/tacticalrmm/agents/management/commands/bulk_change_checkin.py -O /rmm/api/tacticalrmm/agents/management/commands/bulk_change_checkin.py
 | 
			
		||||
```
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
Change offline time on all agents to 5 minutes
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py bulk_change_checkin --offline --all 5
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Change overdue time on all agents to 10 minutes
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py bulk_change_checkin --overdue --all 10
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Change overdue time on all agents in client named *Example Client* to 12 minutes
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py bulk_change_checkin --overdue --client "Example Client" 12
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Change offline time on all agents in site named *Example Site* to 2 minutes
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py bulk_change_checkin --offline --site "Example Site" 2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
!!!info
 | 
			
		||||
    It is currently not possible to restore to a different domain/subdomain, only to a different physical or virtual server.
 | 
			
		||||
 | 
			
		||||
!!!danger
 | 
			
		||||
    You must update your old RMM to the latest version using the `update.sh` script before attempting to restore.
 | 
			
		||||
#### Prepare the new server
 | 
			
		||||
Create the same exact linux user account as you did when you installed the original server.
 | 
			
		||||
 | 
			
		||||
@@ -11,7 +13,7 @@ Add it to the sudoers group and setup the firewall.
 | 
			
		||||
Refer to the [installation instructions](install_server.md) for steps on how to do all of the above.
 | 
			
		||||
 | 
			
		||||
#### Change DNS A records
 | 
			
		||||
Open the DNS manager of whever your domain is hosted.
 | 
			
		||||
Open the DNS manager of wherever your domain is hosted.
 | 
			
		||||
 | 
			
		||||
Change the 3 A records `rmm`, `api` and `mesh` and point them to the public IP of your new server.
 | 
			
		||||
 | 
			
		||||
@@ -23,11 +25,11 @@ Download the restore script.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
 | 
			
		||||
chmod +x restore.sh
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Call the restore script, passing it the backup file as the first argument:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
chmod +x restore.sh
 | 
			
		||||
./restore.sh rmm-backup-XXXXXXXXX.tar
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								docs/docs/sponsor.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								docs/docs/sponsor.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# Sponsor Tactical RMM
 | 
			
		||||
 | 
			
		||||
Tactical RMM is an MIT-licensed open-source project maintained by [@wh1te909](https://github.com/wh1te909), [@sadnub](https://github.com/sadnub) and a community of open source contributors. We work on behalf of the community to create new features, fix bugs, and maintain Tactical RMM.
 | 
			
		||||
 | 
			
		||||
Tactical RMM is currently the only open-source RMM available on the market. Responses from a recent survey have rated Tactical RMM as having the fastest and most reliable remote command and script execution compared to all other commercial RMM's. Sponsorships help keep this project alive and motivate the developers to continue to put in more time and effort into the advancement of Tactical RMM. Thousands of hours have been put in to developing, maintaining, improving, and supporting Tactical RMM. Your sponsorship would be greatly appreciated and helpful to keep this project going.
 | 
			
		||||
 | 
			
		||||
Funds donated will be used to setup a company to purchase a code signing certificate to sign all executables that are used by the RMM, costs needed to maintain demo and documentation sites, to support development of new features and the Linux and Mac agents.
 | 
			
		||||
 | 
			
		||||
If you have signed up to be a sponsor and have not been added to the Sponsors role on discord after a few hours, or if your github username is different than your discord username, please DM wh1te909 on [Discord](https://discord.gg/upGTkWp) after sponsoring to be added to the Sponsors role.
 | 
			
		||||
 | 
			
		||||
We are always looking for feedback and ways to improve Tactical RMM to better address your needs. Please feel free to open a [github issue](https://github.com/wh1te909/tacticalrmm/issues) or drop us a message on [Discord](https://discord.gg/upGTkWp) with your feedback or requests.
 | 
			
		||||
 | 
			
		||||
[Sponsor with Github](https://github.com/wh1te909)
 | 
			
		||||
 | 
			
		||||
[Sponsor with Ko-fi](https://ko-fi.com/tacticalrmm)
 | 
			
		||||
 | 
			
		||||
@@ -51,9 +51,16 @@ This will print out a ton of info. You should be able to see the error from the
 | 
			
		||||
 | 
			
		||||
Please then copy/paste the logs and post them either in our [Discord support chat](https://discord.gg/upGTkWp), or create a [github issue](https://github.com/wh1te909/tacticalrmm/issues).
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
#### Web UI frozen or not loading / website errors / general errors
 | 
			
		||||
 | 
			
		||||
First, run the [update script](update_server.md#updating-to-the-latest-rmm-version) with the `--force` flag. <br/>This will fix permissions and reinstall python/node packages that might have gotten corrupted.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
./update.sh --force
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Check all the systemd services that the rmm uses to function and check to make sure they're all active/running and enabled:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								docs/docs/update_docker.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/docs/update_docker.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
# Updating the RMM (Docker)
 | 
			
		||||
 | 
			
		||||
#### Updating to the latest RMM version
 | 
			
		||||
 | 
			
		||||
Tactical RMM updates the docker images on every release and should be available within a few minutes
 | 
			
		||||
 | 
			
		||||
SSH into your server as a root user and run the below commands:<br/>
 | 
			
		||||
```bash
 | 
			
		||||
cd [dir/with/compose/file]
 | 
			
		||||
mv docker-compose.yml docker-compose.yml.old
 | 
			
		||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/docker/docker-compose.yml
 | 
			
		||||
sudo docker-compose pull
 | 
			
		||||
sudo docker-compose down
 | 
			
		||||
sudo docker-compose up -d --remove-orphans
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Keeping your Let's Encrypt certificate up to date
 | 
			
		||||
 | 
			
		||||
To renew your Let's Encrypt wildcard cert, run the following command, replacing `example.com` with your domain and `admin@example.com` with your email:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo certbot certonly --manual -d *.example.com --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m admin@example.com --no-eff-email
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Verify the domain with the TXT record. Once issued, run the below commands to base64 encode the certificates and add then to the .env file
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
echo "CERT_PUB_KEY=$(sudo base64 -w 0 /etc/letsencrypt/live/${rootdomain}/fullchain.pem)" >> .env
 | 
			
		||||
echo "CERT_PRIV_KEY=$(sudo base64 -w 0 /etc/letsencrypt/live/${rootdomain}/privkey.pem)" >> .env
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
!!!warning 
 | 
			
		||||
    You must remove the old and any duplicate entries for CERT_PUB_KEY and CERT_PRIV_KEY in the .env file
 | 
			
		||||
 | 
			
		||||
Now run `sudo docker-compose restart` and the new certificate will be in effect
 | 
			
		||||
@@ -20,9 +20,9 @@ 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
 | 
			
		||||
tactical@tacrmm:~$ wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
 | 
			
		||||
tactical@tacrmm:~$ chmod +x update.sh
 | 
			
		||||
tactical@tacrmm:~$ ./update.sh
 | 
			
		||||
wget -N https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
 | 
			
		||||
chmod +x update.sh
 | 
			
		||||
./update.sh
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
@@ -30,7 +30,7 @@ tactical@tacrmm:~$ ./update.sh
 | 
			
		||||
If you are already on the latest version, the update script will notify you of this and return immediately.<br/><br/>
 | 
			
		||||
You can pass the optional `--force` flag to the update script to forcefully run through an update, which will bypass the check for latest version.<br/>
 | 
			
		||||
```bash
 | 
			
		||||
tactical@tacrmm:~$ ./update.sh --force
 | 
			
		||||
./update.sh --force
 | 
			
		||||
```
 | 
			
		||||
This is usefull for a botched update that might have not completed fully.<br/><br/>
 | 
			
		||||
The update script will also fix any permissions that might have gotten messed up during a botched update, or if you accidentally ran the update script as the `root` user.
 | 
			
		||||
@@ -48,7 +48,7 @@ The update script will also fix any permissions that might have gotten messed up
 | 
			
		||||
#### Keeping your Let's Encrypt certificate up to date
 | 
			
		||||
 | 
			
		||||
!!!info
 | 
			
		||||
    Currently, the update script does not automatically renew your Let's Encrypt wildcard certificate, which expires every 3 months, since this non-trivial to automate using the DNS TXT record method.
 | 
			
		||||
    Currently, the update script does not automatically renew your Let's Encrypt wildcard certificate, which expires every 3 months, since this is non-trivial to automate using the DNS TXT record method.
 | 
			
		||||
 | 
			
		||||
To renew your Let's Encrypt wildcard cert, run the following command, replacing `example.com` with your domain and `admin@example.com` with your email:
 | 
			
		||||
 | 
			
		||||
@@ -62,4 +62,4 @@ After this you have renewed the cert, simply run the `update.sh` script, passing
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
./update.sh --force
 | 
			
		||||
```
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,28 @@
 | 
			
		||||
site_name: "Tactical RMM Documentation"
 | 
			
		||||
nav:
 | 
			
		||||
  - Home: index.md
 | 
			
		||||
  - Sponsor: sponsor.md
 | 
			
		||||
  - RMM Installation:
 | 
			
		||||
      - "Traditional Install": install_server.md
 | 
			
		||||
      - "Docker Install": install_docker.md
 | 
			
		||||
  - Agent Installation: install_agent.md
 | 
			
		||||
  - Updating:
 | 
			
		||||
      - "Updating the RMM": update_server.md
 | 
			
		||||
      - "Updating the RMM (Docker)": update_docker.md
 | 
			
		||||
      - "Updating Agents": update_agents.md
 | 
			
		||||
  - Functionality:
 | 
			
		||||
      - "Remote Background": functions/remote_bg.md
 | 
			
		||||
      - "Maintenance Mode": functions/maintenance_mode.md
 | 
			
		||||
      - "Alerting": alerting.md
 | 
			
		||||
      - "User Interface Preferences": functions/user_ui.md
 | 
			
		||||
      - "Django Admin": functions/django_admin.md
 | 
			
		||||
  - Backup: backup.md
 | 
			
		||||
  - Restore: restore.md
 | 
			
		||||
  - Troubleshooting: troubleshooting.md
 | 
			
		||||
  - FAQ: faq.md
 | 
			
		||||
  - Management Commands: management_cmds.md
 | 
			
		||||
  - MeshCentral Integration: mesh_integration.md
 | 
			
		||||
  - Contributing: contributing.md
 | 
			
		||||
  - License: license.md
 | 
			
		||||
site_description: "A remote monitoring and management tool"
 | 
			
		||||
site_author: "wh1te909"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							@@ -4,9 +4,8 @@ go 1.16
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/go-resty/resty/v2 v2.5.0
 | 
			
		||||
	github.com/josephspurrier/goversioninfo v1.2.0
 | 
			
		||||
	github.com/nats-io/nats.go v1.10.1-0.20210107160453-a133396829fc
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.4
 | 
			
		||||
	github.com/wh1te909/rmmagent v1.4.6
 | 
			
		||||
	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,18 +1,5 @@
 | 
			
		||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 | 
			
		||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
 | 
			
		||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 | 
			
		||||
github.com/capnspacehook/taskmaster v0.0.0-20201022195506-c2d8b114cec0/go.mod h1:257CYs3Wd/CTlLQ3c72jKv+fFE2MV3WPNnV5jiroYUU=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/elastic/go-sysinfo v1.5.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
 | 
			
		||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 | 
			
		||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 | 
			
		||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.5.0 h1:WFb5bD49/85PO7WgAjZ+/TJQ+Ty1XOcWEfD1zIFCM1c=
 | 
			
		||||
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 | 
			
		||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 | 
			
		||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 | 
			
		||||
@@ -20,19 +7,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
 | 
			
		||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 | 
			
		||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
 | 
			
		||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 | 
			
		||||
github.com/gonutz/w32 v1.0.1-0.20201105145118-e88c649a9470/go.mod h1:Rc/YP5K9gv0FW4p6X9qL3E7Y56lfMflEol1fLElfMW4=
 | 
			
		||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
			
		||||
github.com/iamacarpet/go-win64api v0.0.0-20200715182619-8cbc936e1a5a/go.mod h1:oGJx9dz0Ny7HC7U55RZ0Smd6N9p3hXP/+hOFtuYrAxM=
 | 
			
		||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
			
		||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
 | 
			
		||||
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
 | 
			
		||||
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
 | 
			
		||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 | 
			
		||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA=
 | 
			
		||||
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
 | 
			
		||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
 | 
			
		||||
@@ -58,77 +35,33 @@ github.com/nats-io/nkeys v0.2.0 h1:WXKF7diOaPU9cJdLD7nuzwasQy9vT1tBqzXZZf3AMJM=
 | 
			
		||||
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
 | 
			
		||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 | 
			
		||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 | 
			
		||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 | 
			
		||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 | 
			
		||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 | 
			
		||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 | 
			
		||||
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
 | 
			
		||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 | 
			
		||||
github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs=
 | 
			
		||||
github.com/rickb777/date v1.15.3/go.mod h1:+spwdRnUrpqbYLOmRM6y8FbQMXwpNwHrNcWuOUipge4=
 | 
			
		||||
github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
 | 
			
		||||
github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
 | 
			
		||||
github.com/shirou/gopsutil/v3 v3.21.1/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
 | 
			
		||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc=
 | 
			
		||||
github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA=
 | 
			
		||||
github.com/wh1te909/go-win64api v0.0.0-20201021040544-8fba2a0fc3d0/go.mod h1:cfD5/vNQFm5PD5Q32YYYBJ6VIs9etzp8CJ9dinUcpUA=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.4.6 h1:6cHJQRGe0YCcPJwggPU7X9tlF6Cxn41OX4Vt4YADt0Y=
 | 
			
		||||
github.com/wh1te909/rmmagent v1.4.6/go.mod h1:qh346DIU177vsveCjMLjdrsMVvt0hFYQHC4uRYL7RLU=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
			
		||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
 | 
			
		||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200622182413-4b0db7f3f76b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b h1:HSSdksA3iHk8fuZz7C7+A6tDgtIRF+7FSXu5TgK09I8=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
 | 
			
		||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 | 
			
		||||
@@ -136,14 +69,3 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
 | 
			
		||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 | 
			
		||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
 | 
			
		||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 | 
			
		||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								install.sh
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="40"
 | 
			
		||||
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,6 +371,7 @@ MESH_USERNAME = "${meshusername}"
 | 
			
		||||
MESH_SITE = "https://${meshdomain}"
 | 
			
		||||
REDIS_HOST    = "localhost"
 | 
			
		||||
KEEP_SALT = False
 | 
			
		||||
ADMIN_ENABLED = True
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${localvars}" > /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
 | 
			
		||||
@@ -406,6 +412,12 @@ python manage.py generate_barcode ${RANDBASE} ${djangousername} ${frontenddomain
 | 
			
		||||
deactivate
 | 
			
		||||
read -n 1 -s -r -p "Press any key to continue..."
 | 
			
		||||
 | 
			
		||||
uwsgiprocs=4
 | 
			
		||||
if [[ "$numprocs" == "1" ]]; then
 | 
			
		||||
  uwsgiprocs=2
 | 
			
		||||
else
 | 
			
		||||
  uwsgiprocs=$numprocs
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
uwsgini="$(cat << EOF
 | 
			
		||||
[uwsgi]
 | 
			
		||||
@@ -413,8 +425,8 @@ chdir = /rmm/api/tacticalrmm
 | 
			
		||||
module = tacticalrmm.wsgi
 | 
			
		||||
home = /rmm/api/env
 | 
			
		||||
master = true
 | 
			
		||||
processes = 6
 | 
			
		||||
threads = 6
 | 
			
		||||
processes = ${uwsgiprocs}
 | 
			
		||||
threads = ${uwsgiprocs}
 | 
			
		||||
enable-threads = true
 | 
			
		||||
socket = /rmm/api/tacticalrmm/tacticalrmm.sock
 | 
			
		||||
harakiri = 300
 | 
			
		||||
@@ -423,7 +435,6 @@ buffer-size = 65535
 | 
			
		||||
vacuum = true
 | 
			
		||||
die-on-term = true
 | 
			
		||||
max-requests = 500
 | 
			
		||||
max-requests-delta = 1000
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini
 | 
			
		||||
@@ -805,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							@@ -9,7 +9,7 @@ import (
 | 
			
		||||
	"github.com/wh1te909/tacticalrmm/natsapi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var version = "1.0.8"
 | 
			
		||||
var version = "1.1.0"
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	ver := flag.Bool("version", false, "Prints version")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										126
									
								
								natsapi/api.go
									
									
									
									
									
								
							
							
						
						
									
										126
									
								
								natsapi/api.go
									
									
									
									
									
								
							@@ -6,14 +6,13 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-resty/resty/v2"
 | 
			
		||||
	nats "github.com/nats-io/nats.go"
 | 
			
		||||
	"github.com/ugorji/go/codec"
 | 
			
		||||
	rmm "github.com/wh1te909/rmmagent/shared"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var rClient = resty.New()
 | 
			
		||||
@@ -51,7 +50,7 @@ func Listen(apihost, natshost, version string, debug bool) {
 | 
			
		||||
	log.Println("Nats connection url: ", natsurl)
 | 
			
		||||
 | 
			
		||||
	rClient.SetHostURL(api)
 | 
			
		||||
	rClient.SetTimeout(30 * time.Second)
 | 
			
		||||
	rClient.SetTimeout(10 * time.Second)
 | 
			
		||||
	natsinfo, err := rClient.R().SetResult(&NatsInfo{}).Get("/natsinfo/")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
@@ -75,122 +74,9 @@ func Listen(apihost, natshost, version string, debug bool) {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	wg.Add(1)
 | 
			
		||||
	go getWMI(rClient, nc)
 | 
			
		||||
	go monitorAgents(rClient, nc)
 | 
			
		||||
 | 
			
		||||
	nc.Subscribe("*", func(msg *nats.Msg) {
 | 
			
		||||
		var mh codec.MsgpackHandle
 | 
			
		||||
		mh.RawToString = true
 | 
			
		||||
		dec := codec.NewDecoderBytes(msg.Data, &mh)
 | 
			
		||||
 | 
			
		||||
		switch msg.Reply {
 | 
			
		||||
		case "hello":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckIn
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Patch("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "startup":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckIn
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Post("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "osinfo":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckInOS
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "winservices":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckInWinServices
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "publicip":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckInPublicIP
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "disks":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckInDisk
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "loggedonuser":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckInLoggedUser
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "software":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.CheckInSW
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/checkin/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "syncmesh":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.MeshNodeID
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Post("/syncmesh/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "getwinupdates":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.WinUpdateResult
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Post("/winupdates/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "winupdateresult":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.WinUpdateInstallResult
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Patch("/winupdates/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "superseded":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.SupersededUpdate
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Post("/superseded/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "needsreboot":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.AgentNeedsReboot
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Put("/winupdates/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		case "chocoinstall":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p *rmm.ChocoInstalled
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					rClient.R().SetBody(p).Post("/choco/")
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	nc.Flush()
 | 
			
		||||
 | 
			
		||||
	if err := nc.LastError(); err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runtime.Goexit()
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user