diff --git a/.devcontainer/api.dockerfile b/.devcontainer/api.dockerfile index 01406271..a14238c6 100644 --- a/.devcontainer/api.dockerfile +++ b/.devcontainer/api.dockerfile @@ -1,11 +1,11 @@ # pulls community scripts from git repo -FROM python:3.9.9-slim AS GET_SCRIPTS_STAGE +FROM python:3.10-slim AS GET_SCRIPTS_STAGE RUN apt-get update && \ apt-get install -y --no-install-recommends git && \ git clone https://github.com/amidaware/community-scripts.git /community-scripts -FROM python:3.9.9-slim +FROM python:3.10-slim ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready @@ -17,6 +17,9 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 8000 8383 8005 +RUN apt-get update && \ + apt-get install -y build-essential + RUN groupadd -g 1000 tactical && \ useradd -u 1000 -g 1000 tactical diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 7bbd3061..f0f22014 100644 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -133,6 +133,8 @@ if [ "$1" = 'tactical-init-dev' ]; then # setup Python virtual env and install dependencies ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV} + "${VIRTUAL_ENV}"/bin/python -m pip install --upgrade pip + "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir setuptools wheel "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt django_setup diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt index ebf7805b..87430b24 100644 --- a/.devcontainer/requirements.txt +++ b/.devcontainer/requirements.txt @@ -1,37 +1,36 @@ # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file -asyncio-nats-client -celery -channels -channels_redis -django-ipware +asgiref==3.5.0 +celery==5.2.3 +channels==3.0.4 +channels_redis==3.3.1 +daphne==3.0.2 Django==3.2.12 -django-cors-headers -django-rest-knox -djangorestframework -msgpack -psycopg2-binary -pycparser -pycryptodome -pyotp -pyparsing -pytz -qrcode -redis -twilio -packaging -validators -websockets -black -Werkzeug -django-extensions -coverage -coveralls -model_bakery -mkdocs -mkdocs-material -pymdown-extensions -Pygments -pysnooper -isort -drf_spectacular -pandas +django-cors-headers==3.11.0 +django-ipware==4.0.2 +django-rest-knox==4.2.0 +djangorestframework==3.13.1 +future==0.18.2 +msgpack==1.0.3 +nats-py==2.0.0 +packaging==21.3 +psycopg2-binary==2.9.3 +pycryptodome==3.14.1 +pyotp==2.6.0 +pytz==2021.3 +qrcode==7.3.1 +redis==4.1.3 +requests==2.27.1 +twilio==7.6.0 +urllib3==1.26.8 +validators==0.18.2 +websockets==10.1 +drf_spectacular==0.21.2 + +# dev +black==22.1.0 +Werkzeug==2.0.2 +django-extensions==3.1.5 +Pygments==2.11.2 +isort==5.10.1 +mypy==0.931 +types-pytz==2021.3.4 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b2bc428f..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-present wh1te909 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..530804b4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,74 @@ +### Tactical RMM License Version 1.0 + +Text of license:   Copyright © 2022 AmidaWare LLC. All rights reserved.
+          Amending the text of this license is not permitted. + +Trade Mark:    "Tactical RMM" is a trade mark of AmidaWare LLC. + +Licensor:       AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA. + +Licensed Software:  The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions). + +### 1. Preamble +The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices. The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes. + +The Tactical RMM License is not an open-source software license. This license contains certain restrictions on the use of the Licensed Software. For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor. + +### 2. License Grant +Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions. No additional rights will be implied under this license. + +* The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted. + +This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following: +* a service allowing third parties to interact remotely through a computer network; +* as part of a SaaS service or product; +* as part of the provision of a managed hosting service or product; +* the offering of installation and/or configuration services; +* the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM). + +The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work. + +The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works. + +All use of the Licensed Software must immediately cease if use breaches the terms of this license. + +### 3. Derivative Works +If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online. + +### 4. Copyright Notice +The following copyright notice shall be included in all copies of the Licensed Software: + +   Copyright © 2022 AmidaWare LLC. + +   Licensed under the Tactical RMM License Version 1.0 (the “License”).
+   You may only use the Licensed Software in accordance with the License.
+   A copy of the License is available at: https://license.tacticalrmm.com + +### 5. Disclaimer of Warranty +THE LICENSED SOFTWARE IS PROVIDED "AS IS". TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT. + +### 6. Limits of Liability +THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW. NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA. + +### 7. Termination +This license shall terminate with immediate effect if there is a material breach of any of its terms. + +### 8. No partnership, agency or joint venture +Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person. + +### 9. No endorsement +The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent. + +### 10. Trademarks +No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice). + +### 11. Entire agreement +This license contains the whole agreement relating to its subject matter. + + + +### 12. Severance +If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license. + +### 13. Acceptance of these terms +The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software. \ No newline at end of file diff --git a/api/tacticalrmm/agents/baker_recipes.py b/api/tacticalrmm/agents/baker_recipes.py index cacb2657..ba475500 100644 --- a/api/tacticalrmm/agents/baker_recipes.py +++ b/api/tacticalrmm/agents/baker_recipes.py @@ -32,6 +32,7 @@ agent = Recipe( monitoring_type=cycle(["workstation", "server"]), agent_id=seq(generate_agent_id("DESKTOP-TEST123")), last_seen=djangotime.now() - djangotime.timedelta(days=5), + plat="windows" ) server_agent = agent.extend( diff --git a/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py b/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py index 9c1cb15c..daf9e6a4 100644 --- a/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py +++ b/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py @@ -5,7 +5,8 @@ from django.utils import timezone as djangotime from packaging import version as pyver from agents.models import Agent -from tacticalrmm.utils import AGENT_DEFER, reload_nats +from tacticalrmm.utils import reload_nats +from tacticalrmm.constants import AGENT_DEFER class Command(BaseCommand): diff --git a/api/tacticalrmm/agents/management/commands/fake_agents.py b/api/tacticalrmm/agents/management/commands/fake_agents.py index 47e5c91a..f4b579df 100644 --- a/api/tacticalrmm/agents/management/commands/fake_agents.py +++ b/api/tacticalrmm/agents/management/commands/fake_agents.py @@ -168,7 +168,6 @@ class Command(BaseCommand): public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"] total_rams = [4, 8, 16, 32, 64, 128] - used_rams = [10, 13, 60, 25, 76, 34, 56, 34, 39] now = dt.datetime.now() @@ -285,7 +284,6 @@ class Command(BaseCommand): agent.hostname = random.choice(hostnames) agent.version = settings.LATEST_AGENT_VER - agent.salt_ver = "1.1.0" agent.site = Site.objects.get(name=site) agent.agent_id = self.rand_string(25) agent.description = random.choice(descriptions) @@ -295,10 +293,8 @@ class Command(BaseCommand): agent.plat = "windows" agent.plat_release = "windows-2019Server" agent.total_ram = random.choice(total_rams) - agent.used_ram = random.choice(used_rams) agent.boot_time = random.choice(boot_times) agent.logged_in_username = random.choice(user_names) - agent.antivirus = "windowsdefender" agent.mesh_node_id = ( "3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb" ) @@ -308,7 +304,6 @@ class Command(BaseCommand): agent.wmi_detail = random.choice(wmi_details) agent.services = services agent.disks = random.choice(disks) - agent.salt_id = "not-used" agent.save() @@ -329,9 +324,7 @@ class Command(BaseCommand): agent=agent, guid=i, kb=windows_updates[i]["KBs"][0], - mandatory=windows_updates[i]["Mandatory"], title=windows_updates[i]["Title"], - needs_reboot=windows_updates[i]["NeedsReboot"], installed=windows_updates[i]["Installed"], downloaded=windows_updates[i]["Downloaded"], description=windows_updates[i]["Description"], diff --git a/api/tacticalrmm/agents/management/commands/fix_salt_key.py b/api/tacticalrmm/agents/management/commands/fix_salt_key.py deleted file mode 100644 index ad6ee84a..00000000 --- a/api/tacticalrmm/agents/management/commands/fix_salt_key.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.core.management.base import BaseCommand - -from agents.models import Agent - - -class Command(BaseCommand): - help = "Changes existing agents salt_id from a property to a model field" - - def handle(self, *args, **kwargs): - agents = Agent.objects.filter(salt_id=None) - for agent in agents: - self.stdout.write( - self.style.SUCCESS(f"Setting salt_id on {agent.hostname}") - ) - agent.salt_id = f"{agent.hostname}-{agent.pk}" - agent.save(update_fields=["salt_id"]) diff --git a/api/tacticalrmm/agents/management/commands/update_agents.py b/api/tacticalrmm/agents/management/commands/update_agents.py index d5f52c45..f1d5031f 100644 --- a/api/tacticalrmm/agents/management/commands/update_agents.py +++ b/api/tacticalrmm/agents/management/commands/update_agents.py @@ -5,7 +5,7 @@ from packaging import version as pyver from agents.models import Agent from core.models import CoreSettings from agents.tasks import send_agent_update_task -from tacticalrmm.utils import AGENT_DEFER +from tacticalrmm.constants import AGENT_DEFER class Command(BaseCommand): diff --git a/api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py b/api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py new file mode 100644 index 00000000..907cc7e0 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2022-02-27 05:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0042_alter_agent_time_zone'), + ] + + operations = [ + migrations.RemoveField( + model_name='agent', + name='antivirus', + ), + migrations.RemoveField( + model_name='agent', + name='local_ip', + ), + migrations.RemoveField( + model_name='agent', + name='used_ram', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py b/api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py new file mode 100644 index 00000000..22ccf9aa --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.12 on 2022-02-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0043_auto_20220227_0554'), + ] + + operations = [ + migrations.RenameField( + model_name='agent', + old_name='salt_id', + new_name='goarch', + ), + migrations.RemoveField( + model_name='agent', + name='salt_ver', + ), + ] diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index 742ddfb8..431d7813 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -30,24 +30,20 @@ class Agent(BaseAuditModel): objects = PermissionQuerySet.as_manager() version = models.CharField(default="0.1.0", max_length=255) - salt_ver = models.CharField(default="1.0.3", max_length=255) operating_system = models.CharField(null=True, blank=True, max_length=255) plat = models.CharField(max_length=255, null=True, blank=True) + goarch = models.CharField(max_length=255, null=True, blank=True) plat_release = models.CharField(max_length=255, null=True, blank=True) hostname = models.CharField(max_length=255) - salt_id = models.CharField(null=True, blank=True, max_length=255) - local_ip = models.TextField(null=True, blank=True) # deprecated agent_id = models.CharField(max_length=200, unique=True) last_seen = models.DateTimeField(null=True, blank=True) services = models.JSONField(null=True, blank=True) public_ip = models.CharField(null=True, max_length=255) total_ram = models.IntegerField(null=True, blank=True) - used_ram = models.IntegerField(null=True, blank=True) # deprecated disks = models.JSONField(null=True, blank=True) boot_time = models.FloatField(null=True, blank=True) logged_in_username = models.CharField(null=True, blank=True, max_length=255) last_logged_in_user = models.CharField(null=True, blank=True, max_length=255) - antivirus = models.CharField(default="n/a", max_length=255) # deprecated monitoring_type = models.CharField(max_length=30) description = models.CharField(null=True, blank=True, max_length=255) mesh_node_id = models.CharField(null=True, blank=True, max_length=255) @@ -91,8 +87,6 @@ class Agent(BaseAuditModel): ) def save(self, *args, **kwargs): - from automation.tasks import generate_agent_checks_task - # get old agent if exists old_agent = Agent.objects.get(pk=self.pk) if self.pk else None super(Agent, self).save(old_model=old_agent, *args, **kwargs) @@ -108,6 +102,8 @@ class Agent(BaseAuditModel): or (old_agent.monitoring_type != self.monitoring_type) or (old_agent.block_policy_inheritance != self.block_policy_inheritance) ): + from automation.tasks import generate_agent_checks_task + generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True) def __str__(self): @@ -129,6 +125,9 @@ class Agent(BaseAuditModel): @property def arch(self): + if self.plat != "windows": + return self.goarch + if self.operating_system is not None: if "64 bit" in self.operating_system or "64bit" in self.operating_system: return "64" @@ -196,6 +195,12 @@ class Agent(BaseAuditModel): @property def cpu_model(self): + if self.plat == "linux": + try: + return self.wmi_detail["cpus"] + except: + return ["unknown cpu model"] + ret = [] try: cpus = self.wmi_detail["cpu"] @@ -207,6 +212,14 @@ class Agent(BaseAuditModel): @property def graphics(self): + if self.plat == "linux": + try: + if not self.wmi_detail["gpus"]: + return "No graphics cards" + return self.wmi_detail["gpus"] + except: + return "Error getting graphics cards" + ret, mrda = [], [] try: graphics = self.wmi_detail["graphics"] @@ -228,6 +241,12 @@ class Agent(BaseAuditModel): @property def local_ips(self): + if self.plat == "linux": + try: + return ", ".join(self.wmi_detail["local_ips"]) + except: + return "error getting local ips" + ret = [] try: ips = self.wmi_detail["network_config"] @@ -254,6 +273,12 @@ class Agent(BaseAuditModel): @property def make_model(self): + if self.plat == "linux": + try: + return self.wmi_detail["make_model"] + except: + return "error getting make/model" + try: comp_sys = self.wmi_detail["comp_sys"][0] comp_sys_prod = self.wmi_detail["comp_sys_prod"][0] @@ -284,6 +309,12 @@ class Agent(BaseAuditModel): @property def physical_disks(self): + if self.plat == "linux": + try: + return self.wmi_detail["disks"] + except: + return ["unknown disk"] + try: disks = self.wmi_detail["disk"] ret = [] @@ -305,6 +336,42 @@ class Agent(BaseAuditModel): except: return ["unknown disk"] + def is_supported_script(self, shell: str) -> bool: + if self.plat.lower() == "windows" and shell in ["cmd", "powershell", "python"]: + return True + elif self.plat.lower() == "linux" and shell in ["shell", "python"]: + return True + else: + return False + + def get_agent_policies(self): + site_policy = getattr(self.site, f"{self.monitoring_type}_policy", None) + client_policy = getattr(self.client, f"{self.monitoring_type}_policy", None) + default_policy = getattr( + CoreSettings.objects.first(), f"{self.monitoring_type}_policy", None + ) + + return { + "agent_policy": self.policy + if self.policy and not self.policy.is_agent_excluded(self) + else None, + "site_policy": site_policy + if (site_policy and not site_policy.is_agent_excluded(self)) + and not self.block_policy_inheritance + else None, + "client_policy": client_policy + if (client_policy and not client_policy.is_agent_excluded(self)) + and not self.block_policy_inheritance + and not self.site.block_policy_inheritance + else None, + "default_policy": default_policy + if (default_policy and not default_policy.is_agent_excluded(self)) + and not self.block_policy_inheritance + and not self.site.block_policy_inheritance + and not self.client.block_policy_inheritance + else None, + } + def check_run_interval(self) -> int: interval = self.check_interval # determine if any agent checks have a custom interval and set the lowest interval diff --git a/api/tacticalrmm/agents/serializers.py b/api/tacticalrmm/agents/serializers.py index 4d19b17f..7248ebc1 100644 --- a/api/tacticalrmm/agents/serializers.py +++ b/api/tacticalrmm/agents/serializers.py @@ -116,6 +116,8 @@ class AgentTableSerializer(serializers.ModelSerializer): "italic", "policy", "block_policy_inheritance", + "plat", + "goarch", ] depth = 2 diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index 26f933ec..f4a0a8b5 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -13,7 +13,7 @@ from scripts.models import Script from tacticalrmm.celery import app from agents.models import Agent -from agents.utils import get_winagent_url +from agents.utils import get_agent_url def agent_update(agent_id: str, force: bool = False) -> str: @@ -34,7 +34,7 @@ def agent_update(agent_id: str, force: bool = False) -> str: version = settings.LATEST_AGENT_VER inno = agent.win_inno_exe - url = get_winagent_url(agent.arch) + url = get_agent_url(agent.arch, agent.plat) if not force: if agent.pendingactions.filter( diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index 76f85bdc..af6bab18 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -1428,7 +1428,7 @@ class TestAgentTasks(TacticalTestCase): self.authenticate() self.setup_coresettings() - @patch("agents.utils.get_winagent_url") + @patch("agents.utils.get_agent_url") @patch("agents.models.Agent.nats_cmd") def test_agent_update(self, nats_cmd, get_url): get_url.return_value = "https://exe.tacticalrmm.io" diff --git a/api/tacticalrmm/agents/urls.py b/api/tacticalrmm/agents/urls.py index 321cf253..4d4350c9 100644 --- a/api/tacticalrmm/agents/urls.py +++ b/api/tacticalrmm/agents/urls.py @@ -40,5 +40,4 @@ urlpatterns = [ path("versions/", views.get_agent_versions), path("update/", views.update_agents), path("installer/", views.install_agent), - path("/getmeshexe/", views.get_mesh_exe), ] diff --git a/api/tacticalrmm/agents/utils.py b/api/tacticalrmm/agents/utils.py index 9d745e94..db61b614 100644 --- a/api/tacticalrmm/agents/utils.py +++ b/api/tacticalrmm/agents/utils.py @@ -1,33 +1,28 @@ -import random +import asyncio import urllib.parse -import requests +import tempfile from django.conf import settings -from core.models import CodeSignToken +from django.http import FileResponse + +from core.models import CoreSettings, CodeSignToken +from tacticalrmm.constants import MeshAgentIdent +from core.utils import get_mesh_ws_url, get_mesh_device_id -def get_exegen_url() -> str: - urls: list[str] = settings.EXE_GEN_URLS - for url in urls: - try: - r = requests.get(url, timeout=10) - except: - continue +def get_agent_url(arch: str, plat: str) -> str: - if r.status_code == 200: - return url - - return random.choice(urls) - - -def get_winagent_url(arch: str) -> str: - - dl_url = settings.DL_32 if arch == "32" else settings.DL_64 + if plat == "windows": + endpoint = "winagents" + dl_url = settings.DL_32 if arch == "32" else settings.DL_64 + else: + endpoint = "linuxagents" + dl_url = "" try: t: CodeSignToken = CodeSignToken.objects.first() # type: ignore if t.is_valid: - base_url = get_exegen_url() + "/api/v1/winagents/?" + base_url = settings.EXE_GEN_URL + f"/api/v1/{endpoint}/?" params = { "version": settings.LATEST_AGENT_VER, "arch": arch, @@ -38,3 +33,56 @@ def get_winagent_url(arch: str) -> str: pass return dl_url + + +def generate_linux_install( + client: str, + site: str, + agent_type: str, + arch: str, + token: str, + api: str, + download_url: str, +) -> FileResponse: + + match arch: + case "amd64": + arch_id = MeshAgentIdent.LINUX64 + case "386": + arch_id = MeshAgentIdent.LINUX32 + case "arm64": + arch_id = MeshAgentIdent.LINUX_ARM_64 + case "arm": + arch_id = MeshAgentIdent.LINUX_ARM_HF + + core: CoreSettings = CoreSettings.objects.first() # type: ignore + + uri = get_mesh_ws_url() + mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) + mesh_dl = f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}" # type: ignore + + sh = settings.LINUX_AGENT_SCRIPT + with open(sh, "r") as f: + text = f.read() + + replace = { + "agentDLChange": download_url, + "meshDLChange": mesh_dl, + "clientIDChange": client, + "siteIDChange": site, + "agentTypeChange": agent_type, + "tokenChange": token, + "apiURLChange": api, + } + + for i, j in replace.items(): + text = text.replace(i, j) + + with tempfile.NamedTemporaryFile() as fp: + with open(fp.name, "w") as f: + f.write(text) + f.write("\n") + + return FileResponse( + open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh" + ) diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 1b90acb0..6da9f14d 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -17,7 +17,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.exceptions import PermissionDenied -from core.models import CoreSettings +from core.models import CoreSettings, CodeSignToken from logs.models import AuditLog, DebugLog, PendingAction from scripts.models import Script from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task @@ -25,8 +25,9 @@ from tacticalrmm.utils import ( get_default_timezone, notify_error, reload_nats, - AGENT_DEFER, ) +from tacticalrmm.constants import AGENT_DEFER +from core.utils import get_mesh_ws_url, send_command_with_mesh from winupdate.serializers import WinUpdatePolicySerializer from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task from tacticalrmm.permissions import ( @@ -156,7 +157,13 @@ class GetUpdateDeleteAgent(APIView): # uninstall agent def delete(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) + + code = "foo" + if agent.plat == "linux": + with open(settings.LINUX_AGENT_SCRIPT, "r") as f: + code = f.read() + + asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False)) name = agent.hostname agent.delete() reload_nats() @@ -327,12 +334,17 @@ def get_event_log(request, agent_id, logtype, days): def send_raw_cmd(request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) timeout = int(request.data["timeout"]) + if request.data["shell"] == "custom" and request.data["custom_shell"]: + shell = request.data["custom_shell"] + else: + shell = request.data["shell"] + data = { "func": "rawcmd", "timeout": timeout, "payload": { "command": request.data["cmd"], - "shell": request.data["shell"], + "shell": shell, }, } @@ -353,7 +365,7 @@ def send_raw_cmd(request, agent_id): username=request.user.username, agent=agent, cmd=request.data["cmd"], - shell=request.data["shell"], + shell=shell, debug_info={"ip": request._client_ip}, ) @@ -429,7 +441,7 @@ def install_agent(request): from knox.models import AuthToken from accounts.models import User - from agents.utils import get_winagent_url + from agents.utils import get_agent_url client_id = request.data["client"] site_id = request.data["site"] @@ -439,26 +451,15 @@ def install_agent(request): if not _has_perm_on_site(request.user, site_id): raise PermissionDenied() - # response type is blob so we have to use - # status codes and render error message on the frontend - if arch == "64" and not os.path.exists( - os.path.join(settings.EXE_DIR, "meshagent.exe") - ): - return notify_error( - "Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" - ) - - if arch == "32" and not os.path.exists( - os.path.join(settings.EXE_DIR, "meshagent-x86.exe") - ): - return notify_error( - "Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" - ) - inno = ( f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe" ) - download_url = get_winagent_url(arch) + if request.data["installMethod"] == "linux": + plat = "linux" + else: + plat = "windows" + + download_url = get_agent_url(arch, plat) installer_user = User.objects.filter(is_installer_user=True).first() @@ -482,6 +483,33 @@ def install_agent(request): file_name=request.data["fileName"], ) + elif request.data["installMethod"] == "linux": + # TODO + # linux agents are in beta for now, only available for sponsors for testing + # remove this after it's out of beta + + try: + t: CodeSignToken = CodeSignToken.objects.first() # type: ignore + except: + return notify_error("Something went wrong") + + if t is None: + return notify_error("Missing code signing token") + if not t.is_valid: + return notify_error("Code signing token is not valid") + + from agents.utils import generate_linux_install + + return generate_linux_install( + client=str(client_id), + site=str(site_id), + agent_type=request.data["agenttype"], + arch=arch, + token=token, + api=request.data["api"], + download_url=download_url, + ) + elif request.data["installMethod"] == "manual": cmd = [ inno, @@ -575,10 +603,15 @@ def recover(request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) mode = request.data["mode"] - # attempt a realtime recovery, otherwise fall back to old recovery method - if mode == "tacagent" or mode == "mesh": + if mode == "tacagent": + cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm" + uri = get_mesh_ws_url() + asyncio.run(send_command_with_mesh(cmd, uri, agent.mesh_node_id, 1, 0)) + return Response("Recovery will be attempted shortly") + + elif mode == "mesh": data = {"func": "recover", "payload": {"mode": mode}} - r = asyncio.run(agent.nats_cmd(data, timeout=10)) + r = asyncio.run(agent.nats_cmd(data, timeout=20)) if r == "ok": return Response("Successfully completed recovery") @@ -590,13 +623,6 @@ def recover(request, agent_id): if mode == "command" and not request.data["cmd"]: return notify_error("Command is required") - # if we've made it this far and realtime recovery didn't work, - # tacagent service is the fallback recovery so we obv can't use that to recover itself if it's down - if mode == "tacagent": - return notify_error( - "Requires RPC service to be functional. Please recover that first" - ) - # we should only get here if all other methods fail RecoveryAction( agent=agent, @@ -701,27 +727,6 @@ def run_script(request, agent_id): return Response(f"{script.name} will now be run on {agent.hostname}") -@api_view(["POST"]) -def get_mesh_exe(request, arch): - filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe" - mesh_exe = os.path.join(settings.EXE_DIR, filename) - if not os.path.exists(mesh_exe): - return notify_error(f"File {filename} has not been uploaded.") - - if settings.DEBUG: - with open(mesh_exe, "rb") as f: - response = HttpResponse( - f.read(), content_type="application/vnd.microsoft.portable-executable" - ) - response["Content-Disposition"] = f"inline; filename={filename}" - return response - else: - response = HttpResponse() - response["Content-Disposition"] = f"attachment; filename={filename}" - response["X-Accel-Redirect"] = f"/private/exe/{filename}" - return response - - class GetAddNotes(APIView): permission_classes = [IsAuthenticated, AgentNotesPerms] @@ -819,6 +824,11 @@ def bulk(request): elif request.data["monType"] == "workstations": q = q.filter(monitoring_type="workstation") + if request.data["osType"] == "windows": + q = q.filter(plat="windows") + else: + q = q.filter(plat="linux") + agents: list[int] = [agent.pk for agent in q] if not agents: @@ -832,10 +842,15 @@ def bulk(request): ) if request.data["mode"] == "command": + if request.data["shell"] == "custom" and request.data["custom_shell"]: + shell = request.data["custom_shell"] + else: + shell = request.data["shell"] + handle_bulk_command_task.delay( agents, request.data["cmd"], - request.data["shell"], + shell, request.data["timeout"], request.user.username[:50], run_on_offline=request.data["offlineAgents"], diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 9d308d87..d9631196 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -1,9 +1,7 @@ import asyncio -import os import time from django.conf import settings -from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime from packaging import version as pyver @@ -24,6 +22,9 @@ from logs.models import PendingAction, DebugLog from software.models import InstalledSoftware from tacticalrmm.utils import notify_error, reload_nats from winupdate.models import WinUpdate, WinUpdatePolicy +from core.models import CoreSettings +from core.utils import get_mesh_ws_url, get_mesh_device_id, download_mesh_agent +from tacticalrmm.constants import MeshAgentIdent class CheckIn(APIView): @@ -315,25 +316,18 @@ class MeshExe(APIView): """Sends the mesh exe to the installer""" def post(self, request): - exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe" - mesh_exe = os.path.join(settings.EXE_DIR, exe) + match request.data: + case {"arch": "64", "plat": "windows"}: + arch = MeshAgentIdent.WIN64 + case {"arch": "32", "plat": "windows"}: + arch = MeshAgentIdent.WIN32 - if not os.path.exists(mesh_exe): - return notify_error("Mesh Agent executable not found") + core: CoreSettings = CoreSettings.objects.first() # type: ignore - if settings.DEBUG: - with open(mesh_exe, "rb") as f: - response = HttpResponse( - f.read(), - content_type="application/vnd.microsoft.portable-executable", - ) - response["Content-Disposition"] = f"inline; filename={exe}" - return response - else: - response = HttpResponse() - response["Content-Disposition"] = f"attachment; filename={exe}" - response["X-Accel-Redirect"] = f"/private/exe/{exe}" - return response + uri = get_mesh_ws_url() + mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) + dl_url = f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0" # type: ignore + return download_mesh_agent(dl_url) class NewAgent(APIView): @@ -354,11 +348,11 @@ class NewAgent(APIView): monitoring_type=request.data["monitoring_type"], description=request.data["description"], mesh_node_id=request.data["mesh_node_id"], + goarch=request.data["goarch"], + plat=request.data["plat"], last_seen=djangotime.now(), ) agent.save() - agent.salt_id = f"{agent.hostname}-{agent.pk}" - agent.save(update_fields=["salt_id"]) user = User.objects.create_user( # type: ignore username=request.data["agent_id"], @@ -386,13 +380,8 @@ class NewAgent(APIView): debug_info={"ip": request._client_ip}, ) - return Response( - { - "pk": agent.pk, - "saltid": f"{agent.hostname}-{agent.pk}", - "token": token.key, - } - ) + ret = {"pk": agent.pk, "token": token.key} + return Response(ret) class Software(APIView): diff --git a/api/tacticalrmm/automation/models.py b/api/tacticalrmm/automation/models.py index 96701c22..63c1e4d2 100644 --- a/api/tacticalrmm/automation/models.py +++ b/api/tacticalrmm/automation/models.py @@ -135,86 +135,41 @@ class Policy(BaseAuditModel): # List of all tasks to be applied tasks = list() - added_task_pks = list() agent_tasks_parent_pks = [ task.parent_task for task in agent.autotasks.filter(managed_by_policy=True) ] # Get policies applied to agent and agent site and client - client = agent.client - site = agent.site + policies = agent.get_agent_policies() - default_policy = None - client_policy = None - site_policy = None - agent_policy = agent.policy - - # Get the Client/Site policy based on if the agent is server or workstation - if agent.monitoring_type == "server": - default_policy = CoreSettings.objects.first().server_policy - client_policy = client.server_policy - site_policy = site.server_policy - elif agent.monitoring_type == "workstation": - default_policy = CoreSettings.objects.first().workstation_policy - client_policy = client.workstation_policy - site_policy = site.workstation_policy - - # check if client/site/agent is blocking inheritance and blank out policies - if agent.block_policy_inheritance: - site_policy = None - client_policy = None - default_policy = None - elif site.block_policy_inheritance: - client_policy = None - default_policy = None - elif client.block_policy_inheritance: - default_policy = None - - if ( - agent_policy - and agent_policy.active - and not agent_policy.is_agent_excluded(agent) - ): - for task in agent_policy.autotasks.all(): - if task.pk not in added_task_pks: + if policies["agent_policy"] and policies["agent_policy"].active: + for task in policies["agent_policy"].autotasks.all(): + if task.pk not in [task.pk for task in tasks]: tasks.append(task) - added_task_pks.append(task.pk) - 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: + if policies["site_policy"] and policies["site_policy"].active: + for task in policies["site_policy"].autotasks.all(): + if task.pk not in [task.pk for task in tasks]: tasks.append(task) - added_task_pks.append(task.pk) - 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: + if policies["client_policy"] and policies["client_policy"].active: + for task in policies["client_policy"].autotasks.all(): + if task.pk not in [task.pk for task in tasks]: tasks.append(task) - added_task_pks.append(task.pk) - 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: + if policies["default_policy"] and policies["default_policy"].active: + for task in policies["default_policy"].autotasks.all(): + if task.pk not in [task.pk for task in tasks]: tasks.append(task) - added_task_pks.append(task.pk) + + # remove policy tasks that use scripts that aren't compatible with the agent platform + tasks = [task for task in tasks if agent.is_supported_script(task.script.shell)] # remove policy tasks from agent not included in policy for task in agent.autotasks.filter( parent_task__in=[ taskpk for taskpk in agent_tasks_parent_pks - if taskpk not in added_task_pks + if taskpk not in [task.pk for task in tasks] ] ): if task.sync_status == "initial": @@ -225,7 +180,7 @@ class Policy(BaseAuditModel): # change tasks from pendingdeletion to notsynced if policy was added or changed agent.autotasks.filter(sync_status="pendingdeletion").filter( - parent_task__in=[taskpk for taskpk in added_task_pks] + parent_task__in=[taskpk for taskpk in [task.pk for task in tasks]] ).update(sync_status="notsynced") return [task for task in tasks if task.pk not in agent_tasks_parent_pks] @@ -241,85 +196,43 @@ class Policy(BaseAuditModel): ] # Get policies applied to agent and agent site and client - client = agent.client - site = agent.site - - default_policy = None - client_policy = None - site_policy = None - agent_policy = agent.policy - - if agent.monitoring_type == "server": - default_policy = CoreSettings.objects.first().server_policy - client_policy = client.server_policy - site_policy = site.server_policy - elif agent.monitoring_type == "workstation": - default_policy = CoreSettings.objects.first().workstation_policy - client_policy = client.workstation_policy - site_policy = site.workstation_policy - - # check if client/site/agent is blocking inheritance and blank out policies - if agent.block_policy_inheritance: - site_policy = None - client_policy = None - default_policy = None - elif site.block_policy_inheritance: - client_policy = None - default_policy = None - elif client.block_policy_inheritance: - default_policy = None + policies = agent.get_agent_policies() # Used to hold the policies that will be applied and the order in which they are applied # Enforced policies are applied first enforced_checks = list() policy_checks = list() - 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(): + if policies["agent_policy"] and policies["agent_policy"].active: + if policies["agent_policy"].enforced: + for check in policies["agent_policy"].policychecks.all(): enforced_checks.append(check) else: - for check in agent_policy.policychecks.all(): + for check in policies["agent_policy"].policychecks.all(): policy_checks.append(check) - 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(): + if policies["site_policy"] and policies["site_policy"].active: + if policies["site_policy"].enforced: + for check in policies["site_policy"].policychecks.all(): enforced_checks.append(check) else: - for check in site_policy.policychecks.all(): + for check in policies["site_policy"].policychecks.all(): policy_checks.append(check) - 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(): + if policies["client_policy"] and policies["client_policy"].active: + if policies["client_policy"].enforced: + for check in policies["client_policy"].policychecks.all(): enforced_checks.append(check) else: - for check in client_policy.policychecks.all(): + for check in policies["client_policy"].policychecks.all(): policy_checks.append(check) - 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(): + if policies["default_policy"] and policies["default_policy"].active: + if policies["default_policy"].enforced: + for check in policies["default_policy"].policychecks.all(): enforced_checks.append(check) else: - for check in default_policy.policychecks.all(): + for check in policies["default_policy"].policychecks.all(): policy_checks.append(check) # Sorted Checks already added @@ -342,7 +255,7 @@ class Policy(BaseAuditModel): # Loop over checks in with enforced policies first, then non-enforced policies for check in enforced_checks + agent_checks + policy_checks: - if check.check_type == "diskspace": + if check.check_type == "diskspace" and agent.plat == "windows": # Check if drive letter was already added if check.disk not in added_diskspace_checks: added_diskspace_checks.append(check.disk) @@ -364,7 +277,7 @@ class Policy(BaseAuditModel): check.overriden_by_policy = True check.save() - if check.check_type == "cpuload": + if check.check_type == "cpuload" and agent.plat == "windows": # Check if cpuload list is empty if not added_cpuload_checks: added_cpuload_checks.append(check) @@ -375,7 +288,7 @@ class Policy(BaseAuditModel): check.overriden_by_policy = True check.save() - if check.check_type == "memory": + if check.check_type == "memory" and agent.plat == "windows": # Check if memory check list is empty if not added_memory_checks: added_memory_checks.append(check) @@ -386,7 +299,7 @@ class Policy(BaseAuditModel): check.overriden_by_policy = True check.save() - if check.check_type == "winsvc": + if check.check_type == "winsvc" and agent.plat == "windows": # Check if service name was already added if check.svc_name not in added_winsvc_checks: added_winsvc_checks.append(check.svc_name) @@ -397,7 +310,9 @@ class Policy(BaseAuditModel): check.overriden_by_policy = True check.save() - if check.check_type == "script": + if check.check_type == "script" and agent.is_supported_script( + check.script.shell + ): # Check if script id was already added if check.script.id not in added_script_checks: added_script_checks.append(check.script.id) @@ -408,7 +323,7 @@ class Policy(BaseAuditModel): check.overriden_by_policy = True check.save() - if check.check_type == "eventlog": + if check.check_type == "eventlog" and agent.plat == "windows": # Check if events were already added if [check.log_name, check.event_id] not in added_eventlog_checks: added_eventlog_checks.append([check.log_name, check.event_id]) diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index 2257afe4..808faa92 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -69,7 +69,7 @@ class TestPolicyViews(TacticalTestCase): # create policy with tasks and checks policy = baker.make("automation.Policy") checks = self.create_checks(policy=policy) - tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) + tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3) # assign a task to a check tasks[0].assigned_check = checks[0] # type: ignore @@ -248,11 +248,11 @@ class TestPolicyViews(TacticalTestCase): # policy with a task policy = baker.make("automation.Policy") - task = baker.make("autotasks.AutomatedTask", policy=policy) + task = baker.make_recipe("autotasks.task", policy=policy) # create policy managed tasks - policy_tasks = baker.make( - "autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore + policy_tasks = baker.make_recipe( + "autotasks.task", parent_task=task.id, _quantity=5 # type: ignore ) url = f"/automation/tasks/{task.id}/status/" # type: ignore @@ -269,8 +269,8 @@ class TestPolicyViews(TacticalTestCase): def test_run_win_task(self, mock_task): # create managed policy tasks - tasks = baker.make( - "autotasks.AutomatedTask", + tasks = baker.make_recipe( + "autotasks.task", managed_by_policy=True, parent_task=1, _quantity=6, @@ -577,8 +577,8 @@ class TestPolicyTasks(TacticalTestCase): policy = baker.make("automation.Policy", active=True) self.create_checks(policy=policy) - baker.make( - "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 + baker.make_recipe( + "autotasks.task", policy=policy, name=seq("Task"), _quantity=3 ) server_agent = baker.make_recipe("agents.server_agent") @@ -859,8 +859,8 @@ class TestPolicyTasks(TacticalTestCase): # create test data policy = baker.make("automation.Policy", active=True) - tasks = baker.make( - "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 + tasks = baker.make_recipe( + "autotasks.task", policy=policy, name=seq("Task"), _quantity=3 ) agent = baker.make_recipe("agents.server_agent", policy=policy) @@ -889,7 +889,7 @@ class TestPolicyTasks(TacticalTestCase): from .tasks import delete_policy_autotasks_task, generate_agent_checks_task policy = baker.make("automation.Policy", active=True) - tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) + tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3) agent = baker.make_recipe("agents.server_agent", policy=policy) generate_agent_checks_task(agents=[agent.pk], create_tasks=True) @@ -904,7 +904,7 @@ class TestPolicyTasks(TacticalTestCase): from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task policy = baker.make("automation.Policy", active=True) - tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) + tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3) agent = baker.make_recipe("agents.server_agent", policy=policy) generate_agent_checks_task(agents=[agent.pk], create_tasks=True) @@ -923,8 +923,8 @@ class TestPolicyTasks(TacticalTestCase): # setup data policy = baker.make("automation.Policy", active=True) - tasks = baker.make( - "autotasks.AutomatedTask", + tasks = baker.make_recipe( + "autotasks.task", enabled=True, policy=policy, _quantity=3, @@ -977,7 +977,7 @@ class TestPolicyTasks(TacticalTestCase): # setup data policy = baker.make("automation.Policy", active=True) baker.make_recipe("checks.memory_check", policy=policy) - task = baker.make("autotasks.AutomatedTask", policy=policy) + task = baker.make_recipe("autotasks.task", policy=policy) agent = baker.make_recipe( "agents.agent", policy=policy, monitoring_type="server" ) @@ -1072,7 +1072,7 @@ class TestPolicyTasks(TacticalTestCase): # setup data policy = baker.make("automation.Policy", active=True) baker.make_recipe("checks.memory_check", policy=policy) - baker.make("autotasks.AutomatedTask", policy=policy) + baker.make_recipe("autotasks.task", policy=policy) agent = baker.make_recipe("agents.agent", monitoring_type="server") core = CoreSettings.objects.first() diff --git a/api/tacticalrmm/autotasks/baker_recipes.py b/api/tacticalrmm/autotasks/baker_recipes.py new file mode 100644 index 00000000..50212080 --- /dev/null +++ b/api/tacticalrmm/autotasks/baker_recipes.py @@ -0,0 +1,8 @@ +from itertools import cycle +from model_bakery.recipe import Recipe, seq, foreign_key +script = Recipe("scripts.script") + +task = Recipe( + "autotasks.AutomatedTask", + script=foreign_key(script), +) diff --git a/api/tacticalrmm/clients/models.py b/api/tacticalrmm/clients/models.py index 3f8ed6a5..8c36b437 100644 --- a/api/tacticalrmm/clients/models.py +++ b/api/tacticalrmm/clients/models.py @@ -6,7 +6,7 @@ from django.db import models from agents.models import Agent from logs.models import BaseAuditModel from tacticalrmm.models import PermissionQuerySet -from tacticalrmm.utils import AGENT_DEFER +from tacticalrmm.constants import AGENT_DEFER def _default_failing_checks_data(): diff --git a/api/tacticalrmm/core/agent_linux.sh b/api/tacticalrmm/core/agent_linux.sh new file mode 100755 index 00000000..1b591393 --- /dev/null +++ b/api/tacticalrmm/core/agent_linux.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +if [ $EUID -ne 0 ]; then + echo "ERROR: Must be run as root" + exit 1 +fi + +#### +agentDL='agentDLChange' +meshDL='meshDLChange' + +apiURL='apiURLChange' +token='tokenChange' +clientID='clientIDChange' +siteID='siteIDChange' +agentType='agentTypeChange' +proxy='' + +agentBinPath='/usr/local/bin' +binName='tacticalagent' +agentBin="${agentBinPath}/${binName}" +agentConf='/etc/tacticalagent' +agentSvcName='tacticalagent.service' +agentSysD="/etc/systemd/system/${agentSvcName}" +meshDir='/opt/tacmesh' +meshSystemBin="${meshDir}/meshagent" + +RemoveOldAgent() { + if [ -f "${agentSysD}" ]; then + systemctl disable --now tacticalagent.service + rm -f ${agentSysD} + systemctl daemon-reload + fi + + if [ -f "${agentConf}" ]; then + rm -f ${agentConf} + fi + + if [ -f "${agentBin}" ]; then + rm -f ${agentBin} + fi +} + +InstallMesh() { + meshTmpDir=$(mktemp -d -t "mesh-XXXXXXXXX") + meshTmpBin="${meshTmpDir}/meshagent" + wget -q -O ${meshTmpBin} ${meshDL} + chmod +x ${meshTmpBin} + mkdir -p ${meshDir} + ${meshTmpBin} -install --installPath=${meshDir} + sleep 1 + rm -rf ${meshTmpDir} +} + +RemoveMesh() { + ${meshSystemBin} -uninstall + sleep 1 + rm -rf ${meshDir} + systemctl daemon-reload +} + +Uninstall() { + RemoveMesh + RemoveOldAgent +} + +if [ $# -ne 0 ] && [ $1 == 'uninstall' ]; then + Uninstall + exit 0 +fi + +RemoveOldAgent + +echo "Downloading tactical agent..." +wget -q -O ${agentBin} "${agentDL}" +chmod +x ${agentBin} + +MESH_NODE_ID="" + +if [ $# -ne 0 ] && [ $1 == '--nomesh' ]; then + echo "Skipping mesh install" +else + if [ -f "${meshSystemBin}" ]; then + RemoveMesh + fi + echo "Downloading and installing mesh agent..." + InstallMesh + sleep 2 + echo "Getting mesh node id..." + MESH_NODE_ID=$(${agentBin} -m nixmeshnodeid) +fi + +if [ ! -d "${agentBinPath}" ]; then + echo "Creating ${agentBinPath}" + mkdir -p ${agentBinPath} +fi + +INSTALL_CMD="${agentBin} -m install -api ${apiURL} -client-id ${clientID} -site-id ${siteID} -agent-type ${agentType} -auth ${token}" + +if [ "${MESH_NODE_ID}" != '' ]; then + INSTALL_CMD+=" -meshnodeid ${MESH_NODE_ID}" +fi + +if [ "${proxy}" != '' ]; then + INSTALL_CMD+=" -proxy ${proxy}" +fi + +eval ${INSTALL_CMD} + +tacticalsvc="$(cat << EOF +[Unit] +Description=Tactical RMM Linux Agent + +[Service] +Type=simple +ExecStart=${agentBin} -m svc +User=root +Group=root +Restart=always +RestartSec=5s +LimitNOFILE=1000000 +KillMode=process + +[Install] +WantedBy=multi-user.target +EOF +)" +echo "${tacticalsvc}" | tee ${agentSysD} > /dev/null + +systemctl daemon-reload +systemctl enable --now ${agentSvcName} \ No newline at end of file diff --git a/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py b/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py index 2b261c03..4b0a65b0 100644 --- a/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py +++ b/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py @@ -1,26 +1,16 @@ import asyncio import json - import websockets -from django.conf import settings + from django.core.management.base import BaseCommand -from core.models import CoreSettings - -from .helpers import get_auth_token +from core.utils import get_mesh_ws_url class Command(BaseCommand): help = "Sets up initial mesh central configuration" - async def websocket_call(self, mesh_settings): - token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) - - if settings.DOCKER_BUILD: - uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" - else: - site = mesh_settings.mesh_site.replace("https", "wss") - uri = f"{site}/control.ashx?auth={token}" + async def websocket_call(self, uri): async with websockets.connect(uri) as websocket: @@ -41,9 +31,9 @@ class Command(BaseCommand): response = json.loads(message) if response["action"] == "createInviteLink": - print(response["url"].replace(":4443", ":443")) + self.stdout.write(response["url"].replace(":4443", ":443")) break def handle(self, *args, **kwargs): - mesh_settings = CoreSettings.objects.first() - asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings)) + uri = get_mesh_ws_url() + asyncio.run(self.websocket_call(uri)) diff --git a/api/tacticalrmm/core/management/commands/helpers.py b/api/tacticalrmm/core/management/commands/helpers.py deleted file mode 100644 index da902601..00000000 --- a/api/tacticalrmm/core/management/commands/helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -import time -from base64 import b64encode - -from Crypto.Cipher import AES -from Crypto.Random import get_random_bytes - - -def get_auth_token(user, key): - key = bytes.fromhex(key) - key1 = key[0:32] - msg = '{{"userid":"{}", "domainid":"{}", "time":{}}}'.format( - f"user//{user}", "", int(time.time()) - ) - iv = get_random_bytes(12) - - a = AES.new(key1, AES.MODE_GCM, iv) - msg, tag = a.encrypt_and_digest(bytes(msg, "utf-8")) - - return b64encode(iv + tag + msg, altchars=b"@$").decode("utf-8") diff --git a/api/tacticalrmm/core/management/commands/initial_mesh_setup.py b/api/tacticalrmm/core/management/commands/initial_mesh_setup.py index efe2d6a2..b4d9e97d 100644 --- a/api/tacticalrmm/core/management/commands/initial_mesh_setup.py +++ b/api/tacticalrmm/core/management/commands/initial_mesh_setup.py @@ -6,22 +6,13 @@ from django.conf import settings from django.core.management.base import BaseCommand from core.models import CoreSettings - -from .helpers import get_auth_token +from core.utils import get_mesh_ws_url class Command(BaseCommand): help = "Sets up initial mesh central configuration" - async def websocket_call(self, mesh_settings): - - token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) - - if settings.DOCKER_BUILD: - uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" - else: - site = mesh_settings.mesh_site.replace("https", "wss") - uri = f"{site}/control.ashx?auth={token}" + async def websocket_call(self, uri): async with websockets.connect(uri) as websocket: @@ -82,9 +73,8 @@ class Command(BaseCommand): return try: - asyncio.get_event_loop().run_until_complete( - self.websocket_call(mesh_settings) - ) + uri = get_mesh_ws_url() + asyncio.run(self.websocket_call(uri)) self.stdout.write("Initial Mesh Central setup complete") except websockets.exceptions.ConnectionClosedError: self.stdout.write( diff --git a/api/tacticalrmm/core/management/commands/post_update_tasks.py b/api/tacticalrmm/core/management/commands/post_update_tasks.py index 0695f521..4cf8f403 100644 --- a/api/tacticalrmm/core/management/commands/post_update_tasks.py +++ b/api/tacticalrmm/core/management/commands/post_update_tasks.py @@ -3,10 +3,11 @@ from django.core.management.base import BaseCommand from django.utils.timezone import make_aware import datetime as dt -from logs.models import PendingAction from scripts.models import Script from autotasks.models import AutomatedTask from accounts.models import User +from agents.models import Agent +from tacticalrmm.constants import AGENT_DEFER class Command(BaseCommand): @@ -15,9 +16,6 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): self.stdout.write("Running post update tasks") - # remove task pending actions. deprecated 4/20/2021 - PendingAction.objects.filter(action_type="taskaction").delete() - # load community scripts into the db Script.load_community_scripts() @@ -71,4 +69,16 @@ class Command(BaseCommand): except: continue + # set goarch for older windows agents + for agent in Agent.objects.defer(*AGENT_DEFER): + if not agent.goarch: + if agent.arch == "64": + agent.goarch = "amd64" + elif agent.arch == "32": + agent.goarch = "386" + else: + agent.goarch = "amd64" + + agent.save(update_fields=["goarch"]) + self.stdout.write("Post update tasks finished") diff --git a/api/tacticalrmm/core/migrations/0030_coresettings_mesh_device_group.py b/api/tacticalrmm/core/migrations/0030_coresettings_mesh_device_group.py new file mode 100644 index 00000000..83f82692 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0030_coresettings_mesh_device_group.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-02-16 21:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0029_alter_coresettings_default_time_zone'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='mesh_device_group', + field=models.CharField(blank=True, default='TacticalRMM', max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/core/models.py b/api/tacticalrmm/core/models.py index fa0a2204..53a66822 100644 --- a/api/tacticalrmm/core/models.py +++ b/api/tacticalrmm/core/models.py @@ -61,6 +61,9 @@ class CoreSettings(BaseAuditModel): mesh_token = models.CharField(max_length=255, null=True, blank=True, default="") mesh_username = models.CharField(max_length=255, null=True, blank=True, default="") mesh_site = models.CharField(max_length=255, null=True, blank=True, default="") + mesh_device_group = models.CharField( + max_length=255, null=True, blank=True, default="TacticalRMM" + ) agent_auto_update = models.BooleanField(default=True) workstation_policy = models.ForeignKey( "automation.Policy", @@ -319,22 +322,14 @@ class CodeSignToken(models.Model): if not self.token: return False - errors = [] - for url in settings.EXE_GEN_URLS: - try: - r = requests.post( - f"{url}/api/v1/checktoken", - json={"token": self.token}, - headers={"Content-type": "application/json"}, - timeout=15, - ) - except Exception as e: - errors.append(str(e)) - else: - errors = [] - break - - if errors: + try: + r = requests.post( + f"{settings.EXE_GEN_URL}/api/v1/checktoken", + json={"token": self.token}, + headers={"Content-type": "application/json"}, + timeout=15, + ) + except: return False return r.status_code == 200 diff --git a/api/tacticalrmm/core/tasks.py b/api/tacticalrmm/core/tasks.py index dba7b778..9ba447e6 100644 --- a/api/tacticalrmm/core/tasks.py +++ b/api/tacticalrmm/core/tasks.py @@ -11,7 +11,7 @@ from alerts.tasks import prune_resolved_alerts from core.models import CoreSettings from logs.tasks import prune_debug_log, prune_audit_log from tacticalrmm.celery import app -from tacticalrmm.utils import AGENT_DEFER +from tacticalrmm.constants import AGENT_DEFER from agents.models import Agent from clients.models import Client, Site from alerts.models import Alert diff --git a/api/tacticalrmm/core/urls.py b/api/tacticalrmm/core/urls.py index 6ca270f6..ccf45b39 100644 --- a/api/tacticalrmm/core/urls.py +++ b/api/tacticalrmm/core/urls.py @@ -3,7 +3,6 @@ from django.urls import path from . import views urlpatterns = [ - path("uploadmesh/", views.UploadMeshAgent.as_view()), path("settings/", views.GetEditCoreSettings.as_view()), path("version/", views.version), path("emailtest/", views.email_test), diff --git a/api/tacticalrmm/core/utils.py b/api/tacticalrmm/core/utils.py new file mode 100644 index 00000000..bd1fcad7 --- /dev/null +++ b/api/tacticalrmm/core/utils.py @@ -0,0 +1,88 @@ +import time +import json +import requests +import tempfile +import websockets +from base64 import b64encode +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes + +from django.http import FileResponse +from django.conf import settings + + +def get_auth_token(user, key): + key = bytes.fromhex(key) + key1 = key[0:32] + msg = '{{"userid":"{}", "domainid":"{}", "time":{}}}'.format( + f"user//{user}", "", int(time.time()) + ) + iv = get_random_bytes(12) + + a = AES.new(key1, AES.MODE_GCM, iv) + msg, tag = a.encrypt_and_digest(bytes(msg, "utf-8")) # type: ignore + + return b64encode(iv + tag + msg, altchars=b"@$").decode("utf-8") + + +def get_mesh_ws_url() -> str: + from core.models import CoreSettings + + core = CoreSettings.objects.first() + token = get_auth_token(core.mesh_username, core.mesh_token) # type: ignore + + if settings.DOCKER_BUILD: + uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" + else: + site = core.mesh_site.replace("https", "wss") # type: ignore + uri = f"{site}/control.ashx?auth={token}" + + return uri + + +async def get_mesh_device_id(uri: str, device_group: str): + async with websockets.connect(uri) as ws: # type: ignore + payload = {"action": "meshes", "responseid": "meshctrl"} + await ws.send(json.dumps(payload)) + + async for message in ws: + r = json.loads(message) + if r["action"] == "meshes": + return list(filter(lambda x: x["name"] == device_group, r["meshes"]))[ + 0 + ]["_id"].split("mesh//")[1] + + +def download_mesh_agent(dl_url: str) -> FileResponse: + with tempfile.NamedTemporaryFile(prefix="mesh-", dir=settings.EXE_DIR) as fp: + r = requests.get(dl_url, stream=True, timeout=15) + with open(fp.name, "wb") as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + del r + + return FileResponse(open(fp.name, "rb"), as_attachment=True, filename=fp.name) + + +def _b64_to_hex(h): + return b64encode(bytes.fromhex(h)).decode().replace(r"/", "$").replace(r"+", "@") + + +async def send_command_with_mesh( + cmd: str, uri: str, mesh_node_id: str, shell: int, run_as_user: int +): + node_id = _b64_to_hex(mesh_node_id) + async with websockets.connect(uri) as ws: # type: ignore + await ws.send( + json.dumps( + { + "action": "runcommands", + "cmds": cmd, + "nodeids": [f"node//{node_id}"], + "runAsUser": run_as_user, + "type": shell, + "responseid": "trmm", + } + ) + ) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index 2108a0dc..defad466 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -36,28 +36,6 @@ from .serializers import ( ) -class UploadMeshAgent(APIView): - permission_classes = [IsAuthenticated, CoreSettingsPerms] - parser_class = (FileUploadParser,) - - def put(self, request, format=None): - if "meshagent" not in request.data and "arch" not in request.data: - raise ParseError("Empty content") - - arch = request.data["arch"] - f = request.data["meshagent"] - mesh_exe = os.path.join( - settings.EXE_DIR, "meshagent.exe" if arch == "64" else "meshagent-x86.exe" - ) - with open(mesh_exe, "wb+") as j: - for chunk in f.chunks(): - j.write(chunk) - - return Response( - "Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED - ) - - class GetEditCoreSettings(APIView): permission_classes = [IsAuthenticated, CoreSettingsPerms] @@ -232,23 +210,15 @@ class CodeSign(APIView): def patch(self, request): import requests - errors = [] - for url in settings.EXE_GEN_URLS: - try: - r = requests.post( - f"{url}/api/v1/checktoken", - json={"token": request.data["token"]}, - headers={"Content-type": "application/json"}, - timeout=15, - ) - except Exception as e: - errors.append(str(e)) - else: - errors = [] - break - - if errors: - return notify_error(", ".join(errors)) + try: + r = requests.post( + f"{settings.EXE_GEN_URL}/api/v1/checktoken", + json={"token": request.data["token"]}, + headers={"Content-type": "application/json"}, + timeout=15, + ) + except Exception as e: + return notify_error(str(e)) if r.status_code == 400 or r.status_code == 401: # type: ignore return notify_error(r.json()["ret"]) # type: ignore diff --git a/api/tacticalrmm/logs/migrations/0023_alter_pendingaction_action_type.py b/api/tacticalrmm/logs/migrations/0023_alter_pendingaction_action_type.py new file mode 100644 index 00000000..3d39fa51 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0023_alter_pendingaction_action_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-02-27 05:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('logs', '0022_auto_20211105_0158'), + ] + + operations = [ + migrations.AlterField( + model_name='pendingaction', + name='action_type', + field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install'), ('runcmd', 'Run Command'), ('runscript', 'Run Script'), ('runpatchscan', 'Run Patch Scan'), ('runpatchinstall', 'Run Patch Install')], max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/logs/models.py b/api/tacticalrmm/logs/models.py index 9bb35b03..007104ed 100644 --- a/api/tacticalrmm/logs/models.py +++ b/api/tacticalrmm/logs/models.py @@ -14,7 +14,6 @@ def get_debug_level(): ACTION_TYPE_CHOICES = [ ("schedreboot", "Scheduled Reboot"), - ("taskaction", "Scheduled Task Action"), # deprecated ("agentupdate", "Agent Update"), ("chocoinstall", "Chocolatey Software Install"), ("runcmd", "Run Command"), diff --git a/api/tacticalrmm/logs/serializers.py b/api/tacticalrmm/logs/serializers.py index 89faabcd..41863a45 100644 --- a/api/tacticalrmm/logs/serializers.py +++ b/api/tacticalrmm/logs/serializers.py @@ -19,7 +19,6 @@ class AuditLogSerializer(serializers.ModelSerializer): class PendingActionSerializer(serializers.ModelSerializer): hostname = serializers.ReadOnlyField(source="agent.hostname") - salt_id = serializers.ReadOnlyField(source="agent.salt_id") client = serializers.ReadOnlyField(source="agent.client.name") site = serializers.ReadOnlyField(source="agent.site.name") due = serializers.ReadOnlyField() diff --git a/api/tacticalrmm/logs/views.py b/api/tacticalrmm/logs/views.py index 8c6bc321..00b7f577 100644 --- a/api/tacticalrmm/logs/views.py +++ b/api/tacticalrmm/logs/views.py @@ -9,7 +9,8 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.exceptions import PermissionDenied -from tacticalrmm.utils import notify_error, get_default_timezone, AGENT_DEFER +from tacticalrmm.utils import notify_error, get_default_timezone +from tacticalrmm.constants import AGENT_DEFER from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent from .models import AuditLog, PendingAction, DebugLog diff --git a/api/tacticalrmm/requirements.txt b/api/tacticalrmm/requirements.txt index a482ad53..8b63cb97 100644 --- a/api/tacticalrmm/requirements.txt +++ b/api/tacticalrmm/requirements.txt @@ -23,15 +23,15 @@ pyotp==2.6.0 pyparsing==3.0.7 pytz==2021.3 qrcode==7.3.1 -redis==4.1.3 +redis==4.1.4 requests==2.27.1 six==1.16.0 sqlparse==0.4.2 -twilio==7.6.0 +twilio==7.7.0 urllib3==1.26.8 uWSGI==2.0.20 validators==0.18.2 vine==5.0.0 -websockets==10.1 +websockets==10.2 zipp==3.7.0 drf_spectacular==0.21.2 \ No newline at end of file diff --git a/api/tacticalrmm/scripts/migrations/0016_auto_20220217_1446.py b/api/tacticalrmm/scripts/migrations/0016_auto_20220217_1446.py new file mode 100644 index 00000000..aafa82f1 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0016_auto_20220217_1446.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2022-02-17 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0015_auto_20211128_1637'), + ] + + operations = [ + migrations.AlterField( + model_name='script', + name='shell', + field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python'), ('shell', 'Shell')], default='powershell', max_length=100), + ), + migrations.AlterField( + model_name='scriptsnippet', + name='shell', + field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python'), ('shell', 'Shell')], default='powershell', max_length=15), + ), + ] diff --git a/api/tacticalrmm/scripts/models.py b/api/tacticalrmm/scripts/models.py index e8065a57..e9f4265e 100644 --- a/api/tacticalrmm/scripts/models.py +++ b/api/tacticalrmm/scripts/models.py @@ -13,6 +13,7 @@ SCRIPT_SHELLS = [ ("powershell", "Powershell"), ("cmd", "Batch (CMD)"), ("python", "Python"), + ("shell", "Shell") ] SCRIPT_TYPES = [ diff --git a/api/tacticalrmm/tacticalrmm/constants.py b/api/tacticalrmm/tacticalrmm/constants.py new file mode 100644 index 00000000..44611d7f --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/constants.py @@ -0,0 +1,53 @@ +from enum import Enum + + +class MeshAgentIdent(Enum): + WIN32 = 3 + WIN64 = 4 + LINUX32 = 5 + LINUX64 = 6 + LINUX_ARM_64 = 26 + LINUX_ARM_HF = 25 + + def __str__(self): + return str(self.value) + + +AGENT_DEFER = ("wmi_detail", "services") + + +WEEK_DAYS = { + "Sunday": 0x1, + "Monday": 0x2, + "Tuesday": 0x4, + "Wednesday": 0x8, + "Thursday": 0x10, + "Friday": 0x20, + "Saturday": 0x40, +} + +MONTHS = { + "January": 0x1, + "February": 0x2, + "March": 0x4, + "April": 0x8, + "May": 0x10, + "June": 0x20, + "July": 0x40, + "August": 0x80, + "September": 0x100, + "October": 0x200, + "November": 0x400, + "December": 0x800, +} + +WEEKS = { + "First Week": 0x1, + "Second Week": 0x2, + "Third Week": 0x4, + "Fourth Week": 0x8, + "Last Week": 0x10, +} + +MONTH_DAYS = {f"{b}": 0x1 << a for a, b in enumerate(range(1, 32))} +MONTH_DAYS["Last Day"] = 0x80000000 diff --git a/api/tacticalrmm/tacticalrmm/middleware.py b/api/tacticalrmm/tacticalrmm/middleware.py index 07f93d12..48439ed2 100644 --- a/api/tacticalrmm/tacticalrmm/middleware.py +++ b/api/tacticalrmm/tacticalrmm/middleware.py @@ -105,9 +105,7 @@ class DemoMiddleware: {"name": "update_agents", "methods": ["POST"]}, {"name": "send_raw_cmd", "methods": ["POST"]}, {"name": "install_agent", "methods": ["POST"]}, - {"name": "get_mesh_exe", "methods": ["POST"]}, {"name": "GenerateAgent", "methods": ["GET"]}, - {"name": "UploadMeshAgent", "methods": ["PUT"]}, {"name": "email_test", "methods": ["POST"]}, {"name": "server_maintenance", "methods": ["POST"]}, {"name": "CodeSign", "methods": ["PATCH", "POST"]}, @@ -151,3 +149,42 @@ class DemoMiddleware: for i in self.not_allowed: if view_func.__name__ == i["name"] and request.method in i["methods"]: return self.drf_mock_response(request, notify_error(err)) + + +class LinuxMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + self.not_implemented = [ + {"name": "ScanWindowsUpdates", "methods": ["POST"]}, + {"name": "GetSoftware", "methods": ["POST", "PUT"]}, + ] + + def __call__(self, request): + return self.get_response(request) + + def drf_mock_response(self, request, resp): + from rest_framework.views import APIView + + view = APIView() + view.headers = view.default_response_headers + return view.finalize_response(request, resp).render() # type: ignore + + def process_view(self, request, view_func, view_args, view_kwargs): + if not request.path.startswith(EXCLUDE_PATHS): + if "agent_id" in view_kwargs.keys(): + from agents.models import Agent + + err = "Not currently implemented for linux" + agent = Agent.objects.only("id", "agent_id", "plat").get( + agent_id=view_kwargs["agent_id"] + ) + if agent.plat == "linux": + from .utils import notify_error + + for i in self.not_implemented: + if ( + view_func.__name__ == i["name"] + and request.method in i["methods"] + ): + return self.drf_mock_response(request, notify_error(err)) diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index 4b712e53..0c6095b5 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -12,21 +12,23 @@ LOG_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/log") EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe") +LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh" + AUTH_USER_MODEL = "accounts.User" # latest release -TRMM_VERSION = "0.11.3" +TRMM_VERSION = "0.12.0" # bump this version everytime vue code is changed # to alert user they need to manually refresh their browser APP_VER = "0.0.157" # https://github.com/wh1te909/rmmagent -LATEST_AGENT_VER = "1.8.0" +LATEST_AGENT_VER = "2.0.1" -MESH_VER = "0.9.79" +MESH_VER = "0.9.95" -NATS_SERVER_VER = "2.7.2" +NATS_SERVER_VER = "2.7.3" # for the update script, bump when need to recreate venv or npm install PIP_VER = "26" @@ -38,10 +40,7 @@ WHEEL_VER = "0.37.1" 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" -EXE_GEN_URLS = [ - "https://exe2.tacticalrmm.io", - "https://exe.tacticalrmm.io", -] +EXE_GEN_URL = "https://agents.tacticalrmm.com" DEFAULT_AUTO_FIELD = "django.db.models.AutoField" @@ -139,6 +138,7 @@ MIDDLEWARE = [ "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "tacticalrmm.middleware.AuditMiddleware", + "tacticalrmm.middleware.LinuxMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] @@ -207,11 +207,18 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, "tacticalrmm/static/")] LOGGING = { "version": 1, "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + "datefmt": "%d/%b/%Y %H:%M:%S", + }, + }, "handlers": { "file": { "level": "ERROR", "class": "logging.FileHandler", "filename": os.path.join(LOG_DIR, "django_debug.log"), + "formatter": "verbose", } }, "loggers": { diff --git a/api/tacticalrmm/tacticalrmm/tests.py b/api/tacticalrmm/tacticalrmm/tests.py index 91ec8cf3..764955ea 100644 --- a/api/tacticalrmm/tacticalrmm/tests.py +++ b/api/tacticalrmm/tacticalrmm/tests.py @@ -9,8 +9,8 @@ from .utils import ( generate_winagent_exe, get_bit_days, reload_nats, - AGENT_DEFER, ) +from tacticalrmm.constants import AGENT_DEFER class TestUtils(TacticalTestCase): diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index 1397665a..23bd4346 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -19,47 +19,10 @@ from rest_framework.response import Response from core.models import CodeSignToken from logs.models import DebugLog from agents.models import Agent +from tacticalrmm.constants import WEEK_DAYS, MONTHS, WEEKS, MONTH_DAYS notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) -AGENT_DEFER = ["wmi_detail", "services"] - -WEEK_DAYS = { - "Sunday": 0x1, - "Monday": 0x2, - "Tuesday": 0x4, - "Wednesday": 0x8, - "Thursday": 0x10, - "Friday": 0x20, - "Saturday": 0x40, -} - -MONTHS = { - "January": 0x1, - "February": 0x2, - "March": 0x4, - "April": 0x8, - "May": 0x10, - "June": 0x20, - "July": 0x40, - "August": 0x80, - "September": 0x100, - "October": 0x200, - "November": 0x400, - "December": 0x800, -} - -WEEKS = { - "First Week": 0x1, - "Second Week": 0x2, - "Third Week": 0x4, - "Fourth Week": 0x8, - "Last Week": 0x10, -} - -MONTH_DAYS = {f"{b}": 0x1 << a for a, b in enumerate(range(1, 32))} -MONTH_DAYS["Last Day"] = 0x80000000 - def generate_winagent_exe( client: int, @@ -74,7 +37,7 @@ def generate_winagent_exe( file_name: str, ) -> Union[Response, FileResponse]: - from agents.utils import get_winagent_url + from agents.utils import get_agent_url inno = ( f"winagent-v{settings.LATEST_AGENT_VER}.exe" @@ -82,7 +45,7 @@ def generate_winagent_exe( else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe" ) - dl_url = get_winagent_url(arch) + dl_url = get_agent_url(arch, "windows") try: codetoken = CodeSignToken.objects.first().token # type:ignore @@ -105,25 +68,18 @@ def generate_winagent_exe( } headers = {"Content-type": "application/json"} - errors = [] with tempfile.NamedTemporaryFile() as fp: - for url in settings.EXE_GEN_URLS: - try: - r = requests.post( - f"{url}/api/v1/exe", - json=data, - headers=headers, - stream=True, - timeout=900, - ) - except Exception as e: - errors.append(str(e)) - else: - errors = [] - break - if errors: - DebugLog.error(message=errors) + try: + r = requests.post( + f"{settings.EXE_GEN_URL}/api/v1/exe", + json=data, + headers=headers, + stream=True, + timeout=900, + ) + except Exception as e: + DebugLog.error(message=str(e)) return notify_error( "Something went wrong. Check debug error log for exact error message" ) diff --git a/api/tacticalrmm/winupdate/migrations/0012_auto_20220227_0554.py b/api/tacticalrmm/winupdate/migrations/0012_auto_20220227_0554.py new file mode 100644 index 00000000..689ff0d1 --- /dev/null +++ b/api/tacticalrmm/winupdate/migrations/0012_auto_20220227_0554.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.12 on 2022-02-27 05:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('winupdate', '0011_auto_20210917_1954'), + ] + + operations = [ + migrations.RemoveField( + model_name='winupdate', + name='mandatory', + ), + migrations.RemoveField( + model_name='winupdate', + name='needs_reboot', + ), + ] diff --git a/api/tacticalrmm/winupdate/models.py b/api/tacticalrmm/winupdate/models.py index 3ae2858a..cd2e4653 100644 --- a/api/tacticalrmm/winupdate/models.py +++ b/api/tacticalrmm/winupdate/models.py @@ -44,9 +44,7 @@ class WinUpdate(models.Model): ) guid = models.CharField(max_length=255, null=True, blank=True) kb = models.CharField(max_length=100, null=True, blank=True) - mandatory = models.BooleanField(default=False) # deprecated title = models.TextField(null=True, blank=True) - needs_reboot = models.BooleanField(default=False) # deprecated installed = models.BooleanField(default=False) downloaded = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) diff --git a/docs/docs/license.md b/docs/docs/license.md index b2bc428f..530804b4 100644 --- a/docs/docs/license.md +++ b/docs/docs/license.md @@ -1,21 +1,74 @@ -MIT License +### Tactical RMM License Version 1.0 -Copyright (c) 2019-present wh1te909 +Text of license:   Copyright © 2022 AmidaWare LLC. All rights reserved.
+          Amending the text of this license is not permitted. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Trade Mark:    "Tactical RMM" is a trade mark of AmidaWare LLC. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Licensor:       AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +Licensed Software:  The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions). + +### 1. Preamble +The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices. The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes. + +The Tactical RMM License is not an open-source software license. This license contains certain restrictions on the use of the Licensed Software. For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor. + +### 2. License Grant +Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions. No additional rights will be implied under this license. + +* The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted. + +This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following: +* a service allowing third parties to interact remotely through a computer network; +* as part of a SaaS service or product; +* as part of the provision of a managed hosting service or product; +* the offering of installation and/or configuration services; +* the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM). + +The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work. + +The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works. + +All use of the Licensed Software must immediately cease if use breaches the terms of this license. + +### 3. Derivative Works +If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online. + +### 4. Copyright Notice +The following copyright notice shall be included in all copies of the Licensed Software: + +   Copyright © 2022 AmidaWare LLC. + +   Licensed under the Tactical RMM License Version 1.0 (the “License”).
+   You may only use the Licensed Software in accordance with the License.
+   A copy of the License is available at: https://license.tacticalrmm.com + +### 5. Disclaimer of Warranty +THE LICENSED SOFTWARE IS PROVIDED "AS IS". TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT. + +### 6. Limits of Liability +THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW. NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA. + +### 7. Termination +This license shall terminate with immediate effect if there is a material breach of any of its terms. + +### 8. No partnership, agency or joint venture +Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person. + +### 9. No endorsement +The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent. + +### 10. Trademarks +No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice). + +### 11. Entire agreement +This license contains the whole agreement relating to its subject matter. + + + +### 12. Severance +If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license. + +### 13. Acceptance of these terms +The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software. \ No newline at end of file diff --git a/go.mod b/go.mod index 7c4d205e..01c9e6ec 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/nats-io/nats-server/v2 v2.4.0 // indirect github.com/nats-io/nats.go v1.13.0 github.com/ugorji/go/codec v1.2.6 - github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83 + github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index b06047b7..db5bfe17 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/wh1te909/trmm-shared v0.0.0-20211111193154-6d7f8e4d0dcd h1:18S4tn72OO github.com/wh1te909/trmm-shared v0.0.0-20211111193154-6d7f8e4d0dcd/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8= github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83 h1:faCwMxF0DwMppqThweKdmoxfruB/C/NjTYDG5d9O5V4= github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8= +github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 h1:PfOl03o+Y+svWrfXAAu1QWUDePu1yqTq0pf4rpnN8eA= +github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= diff --git a/install.sh b/install.sh index 8327ddf2..e90c8d59 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="58" +SCRIPT_VERSION="59" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh' sudo apt install -y curl wget dirmngr gnupg lsb-release @@ -12,6 +12,7 @@ RED='\033[0;31m' NC='\033[0m' SCRIPTS_DIR="/opt/trmm-community-scripts" +PYTHON_VER="3.10.2" TMP_FILE=$(mktemp -p "" "rmminstall_XXXXXXXXXX") curl -s -L "${SCRIPT_URL}" > ${TMP_FILE} @@ -177,7 +178,7 @@ sudo sed -i 's/# server_names_hash_bucket_size.*/server_names_hash_bucket_size 6 print_green 'Installing NodeJS' -curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - +curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt update sudo apt install -y gcc g++ make sudo apt install -y nodejs @@ -192,19 +193,19 @@ sudo apt install -y mongodb-org sudo systemctl enable mongod sudo systemctl restart mongod -print_green 'Installing Python 3.9' +print_green 'Installing Python 3.10.2' sudo apt install -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev libbz2-dev numprocs=$(nproc) cd ~ -wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz -tar -xf Python-3.9.9.tgz -cd Python-3.9.9 +wget https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tgz +tar -xf Python-${PYTHON_VER}.tgz +cd Python-${PYTHON_VER} ./configure --enable-optimizations make -j $numprocs sudo make altinstall cd ~ -sudo rm -rf Python-3.9.9 Python-3.9.9.tgz +sudo rm -rf Python-${PYTHON_VER} Python-${PYTHON_VER}.tgz print_green 'Installing redis and git' @@ -220,7 +221,7 @@ 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 +sudo apt install -y postgresql-14 sleep 2 sudo systemctl enable postgresql sudo systemctl restart postgresql @@ -355,7 +356,7 @@ SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" /rmm/api/tacticalrmm/tacticalrmm/setting WHEEL_VER=$(grep "^WHEEL_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') cd /rmm/api -python3.9 -m venv env +python3.10 -m venv env source /rmm/api/env/bin/activate cd /rmm/api/tacticalrmm pip install --no-cache-dir --upgrade pip @@ -796,11 +797,11 @@ echo "${meshtoken}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settin print_green 'Creating meshcentral account and group' sudo systemctl stop meshcentral -sleep 3 +sleep 1 cd /meshcentral node node_modules/meshcentral --createaccount ${meshusername} --pass ${MESHPASSWD} --email ${letsemail} -sleep 2 +sleep 1 node node_modules/meshcentral --adminaccount ${meshusername} sudo systemctl start meshcentral @@ -813,8 +814,7 @@ while ! [[ $CHECK_MESH_READY2 ]]; do done node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} AddDeviceGroup --name TacticalRMM -sleep 5 -MESHEXE=$(node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} GenerateInviteLink --group TacticalRMM --hours 8) +sleep 1 sudo systemctl enable nats.service cd /rmm/api/tacticalrmm @@ -841,9 +841,6 @@ done printf >&2 "${YELLOW}%0.s*${NC}" {1..80} printf >&2 "\n\n" printf >&2 "${YELLOW}Installation complete!${NC}\n\n" -printf >&2 "${YELLOW}Download the meshagent 64 bit EXE from:\n\n${GREEN}" -echo ${MESHEXE} | sed 's/{.*}//' -printf >&2 "${NC}\n\n" printf >&2 "${YELLOW}Access your rmm at: ${GREEN}https://${frontenddomain}${NC}\n\n" printf >&2 "${YELLOW}Django admin url: ${GREEN}https://${rmmdomain}/${ADMINURL}${NC}\n\n" printf >&2 "${YELLOW}MeshCentral username: ${GREEN}${meshusername}${NC}\n" diff --git a/main.go b/main.go index 063b8318..60827d5d 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( ) var ( - version = "3.0.0" + version = "3.0.1" log = logrus.New() ) diff --git a/natsapi/bin/nats-api b/natsapi/bin/nats-api index fe0e5a18..a3ddac40 100755 Binary files a/natsapi/bin/nats-api and b/natsapi/bin/nats-api differ diff --git a/natsapi/svc.go b/natsapi/svc.go index 304d23f6..b6bb422e 100644 --- a/natsapi/svc.go +++ b/natsapi/svc.go @@ -83,6 +83,15 @@ func Svc(logger *logrus.Logger, cfg string) { logger.Errorln(err) } + // TODO add this to main stmt once agent 2.0.0 has been out for a while + if r.GoArch != "" { + stmt = `UPDATE agents_agent SET goarch=$1 WHERE agents_agent.agent_id=$2;` + _, err = db.Exec(stmt, r.GoArch, r.Agentid) + if err != nil { + logger.Errorln(err) + } + } + if r.Username != "None" { stmt = `UPDATE agents_agent SET last_logged_in_user=$1 WHERE agents_agent.agent_id=$2;` logger.Debugln("Updating last logged in user:", r.Username) diff --git a/restore.sh b/restore.sh index 09861d8f..0ab4fe9d 100755 --- a/restore.sh +++ b/restore.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="33" +SCRIPT_VERSION="34" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh' sudo apt update @@ -13,6 +13,7 @@ RED='\033[0;31m' NC='\033[0m' SCRIPTS_DIR="/opt/trmm-community-scripts" +PYTHON_VER="3.10.2" TMP_FILE=$(mktemp -p "" "rmmrestore_XXXXXXXXXX") curl -s -L "${SCRIPT_URL}" > ${TMP_FILE} @@ -111,7 +112,7 @@ sudo apt update print_green 'Installing NodeJS' -curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - +curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt update sudo apt install -y gcc g++ make sudo apt install -y nodejs @@ -163,19 +164,19 @@ print_green 'Restoring systemd services' sudo cp $tmp_dir/systemd/* /etc/systemd/system/ sudo systemctl daemon-reload -print_green 'Installing Python 3.9' +print_green 'Installing Python 3.10.2' sudo apt install -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev libbz2-dev numprocs=$(nproc) cd ~ -wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz -tar -xf Python-3.9.9.tgz -cd Python-3.9.9 +wget https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tgz +tar -xf Python-${PYTHON_VER}.tgz +cd Python-${PYTHON_VER} ./configure --enable-optimizations make -j $numprocs sudo make altinstall cd ~ -sudo rm -rf Python-3.9.9 Python-3.9.9.tgz +sudo rm -rf Python-${PYTHON_VER} Python-${PYTHON_VER}.tgz print_green 'Installing redis and git' @@ -193,7 +194,7 @@ print_green 'Installing postgresql' 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 +sudo apt install -y postgresql-14 sleep 2 sudo systemctl enable postgresql sudo systemctl restart postgresql @@ -308,7 +309,7 @@ SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" /rmm/api/tacticalrmm/tacticalrmm/setting WHEEL_VER=$(grep "^WHEEL_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') cd /rmm/api -python3.9 -m venv env +python3.10 -m venv env source /rmm/api/env/bin/activate cd /rmm/api/tacticalrmm pip install --no-cache-dir --upgrade pip diff --git a/web/package-lock.json b/web/package-lock.json index 96cdfb27..524f9f6c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "dependencies": { "@quasar/extras": "^1.12.5", - "apexcharts": "^3.33.1", + "apexcharts": "^3.33.2", "axios": "^0.26.0", "dotenv": "^16.0.0", "qrcode.vue": "^3.3.3", @@ -3077,9 +3077,9 @@ } }, "node_modules/apexcharts": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.1.tgz", - "integrity": "sha512-5aVzrgJefd8EH4w7oRmuOhA3+cxJxQg27cYg3ANVGvPCOB4AY3mVVNtFHRFaIq7bv8ws4GRaA9MWfzoWQw3MPQ==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.2.tgz", + "integrity": "sha512-GkHZ3o36ZT/jSBh5y1pxxRzwM3tvtladtkcUTfXwP0wYAHK8Qj0X4ZPsupP7emRIjhOVpGsCxW9xeO3F5w+AOQ==", "dependencies": { "svg.draggable.js": "^2.2.2", "svg.easing.js": "^2.0.0", @@ -15286,9 +15286,9 @@ } }, "apexcharts": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.1.tgz", - "integrity": "sha512-5aVzrgJefd8EH4w7oRmuOhA3+cxJxQg27cYg3ANVGvPCOB4AY3mVVNtFHRFaIq7bv8ws4GRaA9MWfzoWQw3MPQ==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.33.2.tgz", + "integrity": "sha512-GkHZ3o36ZT/jSBh5y1pxxRzwM3tvtladtkcUTfXwP0wYAHK8Qj0X4ZPsupP7emRIjhOVpGsCxW9xeO3F5w+AOQ==", "requires": { "svg.draggable.js": "^2.2.2", "svg.easing.js": "^2.0.0", diff --git a/web/package.json b/web/package.json index 83df414d..79d1b6a5 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@quasar/extras": "^1.12.5", - "apexcharts": "^3.33.1", + "apexcharts": "^3.33.2", "axios": "^0.26.0", "dotenv": "^16.0.0", "qrcode.vue": "^3.3.3", diff --git a/web/src/api/agents.js b/web/src/api/agents.js index 390dbe5c..bf0ab2ce 100644 --- a/web/src/api/agents.js +++ b/web/src/api/agents.js @@ -14,8 +14,8 @@ export function openAgentWindow(agent_id) { openURL(url, null, { popup: true, scrollbars: false, location: false, status: false, toolbar: false, menubar: false, width: 1600, height: 900 }); } -export function runRemoteBackground(agent_id) { - const url = router.resolve(`/remotebackground/${agent_id}`).href; +export function runRemoteBackground(agent_id, agentPlatform) { + const url = router.resolve(`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`).href; openURL(url, null, { popup: true, scrollbars: false, location: false, status: false, toolbar: false, menubar: false, width: 1280, height: 900 }); } diff --git a/web/src/api/core.js b/web/src/api/core.js index f6293cc4..a78b41c7 100644 --- a/web/src/api/core.js +++ b/web/src/api/core.js @@ -10,11 +10,6 @@ export async function fetchCustomFields(params = {}) { } catch (e) { console.error(e) } } -export async function uploadMeshAgent(payload) { - const { data } = await axios.put(`${baseUrl}/uploadmesh/`, payload) - return data -} - export async function fetchDashboardInfo(params = {}) { const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params }) return data diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index fa1e6c52..f27d8ff6 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -40,6 +40,9 @@ +