v0.12.0
This commit is contained in:
parent
c95d11da47
commit
202edc0588
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
21
LICENSE
21
LICENSE
|
@ -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.
|
|
@ -0,0 +1,74 @@
|
|||
### Tactical RMM License Version 1.0
|
||||
|
||||
Text of license:   Copyright © 2022 AmidaWare LLC. All rights reserved.<br>
|
||||
          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”).<br>
|
||||
   You may only use the Licensed Software in accordance with the License.<br>
|
||||
   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.
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"])
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -116,6 +116,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
|||
"italic",
|
||||
"policy",
|
||||
"block_policy_inheritance",
|
||||
"plat",
|
||||
"goarch",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -40,5 +40,4 @@ urlpatterns = [
|
|||
path("versions/", views.get_agent_versions),
|
||||
path("update/", views.update_agents),
|
||||
path("installer/", views.install_agent),
|
||||
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
)
|
|
@ -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():
|
||||
|
|
|
@ -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}
|
|
@ -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))
|
||||
|
|
|
@ -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")
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -13,6 +13,7 @@ SCRIPT_SHELLS = [
|
|||
("powershell", "Powershell"),
|
||||
("cmd", "Batch (CMD)"),
|
||||
("python", "Python"),
|
||||
("shell", "Shell")
|
||||
]
|
||||
|
||||
SCRIPT_TYPES = [
|
||||
|
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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.<br>
|
||||
          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.
|
||||
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”).<br>
|
||||
   You may only use the Licensed Software in accordance with the License.<br>
|
||||
   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.
|
2
go.mod
2
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
|
||||
)
|
||||
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
29
install.sh
29
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"
|
||||
|
|
2
main.go
2
main.go
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
log = logrus.New()
|
||||
)
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
|
|
19
restore.sh
19
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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
</q-icon>
|
||||
</q-th>
|
||||
</template>
|
||||
<template v-slot:header-cell-plat="props">
|
||||
<q-th auto-width :props="props"></q-th>
|
||||
</template>
|
||||
<template v-slot:header-cell-checks-status="props">
|
||||
<q-th :props="props">
|
||||
<q-icon name="fas fa-check-double" size="1.2em">
|
||||
|
@ -78,10 +81,10 @@
|
|||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
@contextmenu="agentRowSelected(props.row.agent_id)"
|
||||
@contextmenu="agentRowSelected(props.row.agent_id, props.row.plat)"
|
||||
:props="props"
|
||||
:class="rowSelectedClass(props.row.agent_id)"
|
||||
@click="agentRowSelected(props.row.agent_id)"
|
||||
@click="agentRowSelected(props.row.agent_id, props.row.plat)"
|
||||
@dblclick="rowDoubleClicked(props.row.agent_id)"
|
||||
>
|
||||
<q-menu context-menu>
|
||||
|
@ -138,6 +141,16 @@
|
|||
v-model="props.row.overdue_dashboard_alert"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td key="plat" :props="props">
|
||||
<q-icon v-if="props.row.plat === 'windows'" name="mdi-microsoft-windows" size="sm" color="primary">
|
||||
<q-tooltip>Microsoft Windows</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else-if="props.row.plat === 'linux'" name="mdi-linux" size="sm" color="primary">
|
||||
<q-tooltip>Linux</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
|
||||
<q-td key="checks-status" :props="props">
|
||||
<q-icon v-if="props.row.maintenance_mode" name="construction" size="1.2em" color="green">
|
||||
<q-tooltip>Maintenance Mode Enabled</q-tooltip>
|
||||
|
@ -155,9 +168,9 @@
|
|||
<q-tooltip>Checks passing</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
|
||||
<q-td key="client_name" :props="props">{{ props.row.client_name }}</q-td>
|
||||
<q-td key="site_name" :props="props">{{ props.row.site_name }}</q-td>
|
||||
|
||||
<q-td key="hostname" :props="props">{{ props.row.hostname }}</q-td>
|
||||
<q-td key="description" :props="props">{{ props.row.description }}</q-td>
|
||||
<q-td key="user" :props="props">
|
||||
|
@ -286,7 +299,7 @@ export default {
|
|||
});
|
||||
});
|
||||
},
|
||||
rowDoubleClicked(agent_id) {
|
||||
rowDoubleClicked(agent_id, agentPlatform) {
|
||||
this.$store.commit("setActiveRow", agent_id);
|
||||
this.$q.loading.show();
|
||||
// give time for store to change active row
|
||||
|
@ -300,7 +313,7 @@ export default {
|
|||
runTakeControl(agent_id);
|
||||
break;
|
||||
case "remotebg":
|
||||
runRemoteBackground(agent_id);
|
||||
runRemoteBackground(agent_id, agentPlatform);
|
||||
break;
|
||||
case "urlaction":
|
||||
runURLAction({ agent_id: agent_id, action: this.agentUrlAction });
|
||||
|
@ -316,8 +329,9 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
agentRowSelected(agent_id) {
|
||||
agentRowSelected(agent_id, agentPlatform) {
|
||||
this.$store.commit("setActiveRow", agent_id);
|
||||
this.$store.commit("setAgentPlatform", agentPlatform);
|
||||
},
|
||||
overdueAlert(category, agent, alert_action) {
|
||||
let db_field = "";
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
</q-menu>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="runRemoteBackground(agent.agent_id)">
|
||||
<q-item clickable v-close-popup @click="runRemoteBackground(agent.agent_id, agent.plat)">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-cogs" />
|
||||
</q-item-section>
|
||||
|
@ -185,7 +185,6 @@
|
|||
<script>
|
||||
// composition imports
|
||||
import { ref, inject } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { fetchURLActions, runURLAction } from "@/api/core";
|
||||
import {
|
||||
|
@ -217,7 +216,6 @@ export default {
|
|||
},
|
||||
setup(props) {
|
||||
const $q = useQuasar();
|
||||
const store = useStore();
|
||||
|
||||
const refreshDashboard = inject("refreshDashboard");
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
|
||||
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||
Only supported for Windows agents at this time
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-tabs
|
||||
v-model="tab"
|
||||
|
@ -88,6 +91,7 @@ export default {
|
|||
// setup vuex
|
||||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
const loading = ref(false);
|
||||
|
||||
// assets tab logic
|
||||
|
@ -116,6 +120,7 @@ export default {
|
|||
assets,
|
||||
tab,
|
||||
selectedAgent,
|
||||
agentPlatform,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
|
||||
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||
Only supported for Windows agents at this time
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-table
|
||||
dense
|
||||
|
@ -300,6 +303,7 @@ export default {
|
|||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
@ -428,6 +432,7 @@ export default {
|
|||
pagination,
|
||||
selectedAgent,
|
||||
tabHeight,
|
||||
agentPlatform,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<q-btn class="q-mr-sm" dense flat push @click="getChecks" icon="refresh" />
|
||||
<q-btn-dropdown icon="add" label="New" no-caps dense flat>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="showCheckModal('diskspace')">
|
||||
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('diskspace')">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="far fa-hdd" />
|
||||
</q-item-section>
|
||||
|
@ -37,19 +37,19 @@
|
|||
</q-item-section>
|
||||
<q-item-section>Ping Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showCheckModal('cpuload')">
|
||||
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('cpuload')">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-microchip" />
|
||||
</q-item-section>
|
||||
<q-item-section>CPU Load Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showCheckModal('memory')">
|
||||
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('memory')">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-memory" />
|
||||
</q-item-section>
|
||||
<q-item-section>Memory Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showCheckModal('winsvc')">
|
||||
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('winsvc')">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-cogs" />
|
||||
</q-item-section>
|
||||
|
@ -61,7 +61,7 @@
|
|||
</q-item-section>
|
||||
<q-item-section>Script Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showCheckModal('eventlog')">
|
||||
<q-item v-if="agentPlatform === 'windows'" clickable v-close-popup @click="showCheckModal('eventlog')">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-clipboard-list" />
|
||||
</q-item-section>
|
||||
|
@ -336,6 +336,7 @@ export default {
|
|||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
@ -482,6 +483,7 @@ export default {
|
|||
pagination,
|
||||
tabHeight,
|
||||
selectedAgent,
|
||||
agentPlatform,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
|
||||
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||
Only supported for Windows agents at this time
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-table
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
|
@ -103,6 +106,7 @@ export default {
|
|||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
|
||||
// software tab logic
|
||||
const software = ref([]);
|
||||
|
@ -154,6 +158,7 @@ export default {
|
|||
pagination,
|
||||
selectedAgent,
|
||||
tabHeight,
|
||||
agentPlatform,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
|
||||
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||
Only supported for Windows agents at this time
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-table
|
||||
dense
|
||||
|
@ -179,6 +182,7 @@ export default {
|
|||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
@ -274,6 +278,7 @@ export default {
|
|||
loading,
|
||||
selectedAgent,
|
||||
tabHeight,
|
||||
agentPlatform,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||
Only supported for Windows agents at this time
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="row q-pt-sm q-pl-sm">
|
||||
<div class="col-2">
|
||||
<q-select dense options-dense outlined v-model="days" :options="lastDaysOptions" :label="showDays" />
|
||||
|
@ -90,6 +93,7 @@ export default {
|
|||
},
|
||||
props: {
|
||||
agent_id: !String,
|
||||
agentPlatform: !String,
|
||||
},
|
||||
setup(props) {
|
||||
// quasar setup
|
||||
|
@ -121,7 +125,9 @@ export default {
|
|||
}
|
||||
|
||||
// vue lifecycle hooks
|
||||
onMounted(getEventLog);
|
||||
onMounted(() => {
|
||||
if (props.agentPlatform === "windows") getEventLog();
|
||||
});
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<div v-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||
Only supported for Windows agents at this time
|
||||
</div>
|
||||
<q-table
|
||||
v-else
|
||||
dense
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
class="remote-bg-tbl-sticky"
|
||||
|
@ -150,6 +154,7 @@ export default {
|
|||
},
|
||||
props: {
|
||||
agent_id: !String,
|
||||
agentPlatform: !String,
|
||||
},
|
||||
setup(props) {
|
||||
// quasar setup
|
||||
|
@ -190,8 +195,9 @@ export default {
|
|||
}
|
||||
|
||||
// vue lifecycle hooks
|
||||
onMounted(getServices);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.agentPlatform === "windows") getServices();
|
||||
});
|
||||
return {
|
||||
// reactive data
|
||||
services,
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 40vw">
|
||||
<q-bar>
|
||||
Upload Mesh Exe
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit.prevent="submitForm">
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio v-model="form.arch" val="64" label="64 bit" />
|
||||
<q-radio v-model="form.arch" val="32" label="32 bit" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-file
|
||||
v-model="form.meshagent"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
label="Upload MeshAgent"
|
||||
stack-label
|
||||
filled
|
||||
counter
|
||||
class="full-width"
|
||||
accept=".exe"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
</q-file>
|
||||
</q-card-section>
|
||||
<q-card-actions>
|
||||
<q-space />
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
<q-btn :loading="loading" dense flat label="Upload" color="primary" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { uploadMeshAgent } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
export default {
|
||||
name: "UploadMesh",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
setup(props) {
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// upload mesh logic
|
||||
const form = ref({
|
||||
meshagent: null,
|
||||
arch: "64",
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function submitForm() {
|
||||
loading.value = true;
|
||||
let result = "";
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("arch", form.value.arch);
|
||||
formData.append("meshagent", form.value.meshagent);
|
||||
try {
|
||||
result = await uploadMeshAgent(formData);
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
form,
|
||||
loading,
|
||||
|
||||
//methods
|
||||
submitForm,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -56,9 +56,6 @@
|
|||
<q-td v-if="props.row.action_type === 'schedreboot'">
|
||||
<q-icon name="power_settings_new" size="sm" />
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.action_type === 'taskaction'">
|
||||
<q-icon name="fas fa-tasks" size="sm" />
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.action_type === 'agentupdate'">
|
||||
<q-icon name="update" size="sm" />
|
||||
</q-td>
|
||||
|
|
|
@ -40,12 +40,7 @@
|
|||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>-local-mesh "C:\\<some folder or path>\\meshagent.exe"</code>
|
||||
</q-badge>
|
||||
<span>
|
||||
To skip downloading the Mesh Agent during the install. Download it
|
||||
<span style="cursor: pointer; text-decoration: underline" class="text-primary" @click="downloadMesh"
|
||||
>here</span
|
||||
>
|
||||
</span>
|
||||
<span> To skip downloading the Mesh Agent during the install.</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
|
@ -92,20 +87,5 @@ export default {
|
|||
name: "AgentDownload",
|
||||
mixins: [mixins],
|
||||
props: ["info"],
|
||||
methods: {
|
||||
downloadMesh() {
|
||||
const fileName = this.info.arch === "64" ? "meshagent.exe" : "meshagent-x86.exe";
|
||||
this.$axios
|
||||
.post(`/agents/${this.info.arch}/getmeshexe/`, {}, { responseType: "blob" })
|
||||
.then(({ data }) => {
|
||||
const blob = new Blob([data], { type: "application/vnd.microsoft.portable-executable" });
|
||||
let link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = fileName;
|
||||
link.click();
|
||||
})
|
||||
.catch(e => {});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -12,7 +12,6 @@
|
|||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio dense v-model="state.mode" val="mesh" label="Mesh Agent" />
|
||||
<q-radio dense v-model="state.mode" val="rpc" label="Tactical RPC" />
|
||||
<q-radio dense v-model="state.mode" val="tacagent" label="Tactical Agent" />
|
||||
<q-radio dense v-model="state.mode" val="command" label="Shell Command" />
|
||||
</div>
|
||||
|
@ -21,16 +20,12 @@
|
|||
Fix issues with the Mesh Agent which handles take control, live terminal and file browser.
|
||||
</q-card-section>
|
||||
<q-card-section v-else-if="state.mode === 'tacagent'">
|
||||
Fix issues with the TacticalAgent windows service which handles agent check-in.
|
||||
</q-card-section>
|
||||
<q-card-section v-else-if="state.mode === 'rpc'">
|
||||
Fix issues with the Tactical RPC service which handles most of the agent's realtime functions and scheduled
|
||||
tasks.
|
||||
Fix issues with the Tactical RMM Agent windows service.
|
||||
</q-card-section>
|
||||
<q-card-section v-else-if="state.mode === 'command'">
|
||||
<p>Run a shell command on the agent.</p>
|
||||
<p>You should use the 'Send Command' feature from the agent's context menu for sending shell commands.</p>
|
||||
<p>Only use this as a last resort if unable to recover the Tactical RPC service.</p>
|
||||
<p>Only use this as a last resort if unable to recover the Tactical RMM Agent service.</p>
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="state.cmd"
|
||||
|
|
|
@ -55,6 +55,18 @@
|
|||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<p>Agent OS</p>
|
||||
<q-option-group
|
||||
v-model="state.osType"
|
||||
:options="osTypeOptions"
|
||||
color="primary"
|
||||
dense
|
||||
inline
|
||||
class="q-pl-sm"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-show="state.target !== 'agents'">
|
||||
<p>Agent Type</p>
|
||||
<q-option-group
|
||||
|
@ -71,7 +83,7 @@
|
|||
<tactical-dropdown
|
||||
:rules="[val => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="scriptOptions"
|
||||
:options="filteredScriptOptions"
|
||||
label="Select Script"
|
||||
outlined
|
||||
mapOptions
|
||||
|
@ -93,7 +105,25 @@
|
|||
|
||||
<q-card-section v-if="mode === 'command'">
|
||||
<p>Shell</p>
|
||||
<q-option-group v-model="state.shell" :options="shellOptions" color="primary" dense inline class="q-pl-sm" />
|
||||
<q-option-group
|
||||
v-model="state.shell"
|
||||
:options="shellOptions"
|
||||
color="primary"
|
||||
dense
|
||||
inline
|
||||
class="q-pl-sm"
|
||||
@update:model-value="state.custom_shell = null"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="state.shell === 'custom'">
|
||||
<q-input
|
||||
v-model="state.custom_shell"
|
||||
outlined
|
||||
label="Custom shell"
|
||||
stack-label
|
||||
placeholder="/usr/bin/python3"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="mode === 'command'">
|
||||
<q-input
|
||||
|
@ -101,11 +131,7 @@
|
|||
outlined
|
||||
label="Command"
|
||||
stack-label
|
||||
:placeholder="
|
||||
state.shell === 'cmd'
|
||||
? 'rmdir /S /Q C:\\Windows\\System32'
|
||||
: 'Remove-Item -Recurse -Force C:\\Windows\\System32'
|
||||
"
|
||||
:placeholder="cmdPlaceholder(state.shell)"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
@ -160,6 +186,8 @@ import { useAgentDropdown } from "@/composables/agents";
|
|||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||
import { runBulkAction } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
import { removeExtraOptionCategories } from "@/utils/format";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
|
||||
|
@ -171,6 +199,11 @@ const monTypeOptions = [
|
|||
{ label: "Workstations", value: "workstations" },
|
||||
];
|
||||
|
||||
const osTypeOptions = [
|
||||
{ label: "Windows", value: "windows" },
|
||||
{ label: "Linux", value: "linux" },
|
||||
];
|
||||
|
||||
const targetOptions = [
|
||||
{ label: "Client", value: "client" },
|
||||
{ label: "Site", value: "site" },
|
||||
|
@ -178,11 +211,6 @@ const targetOptions = [
|
|||
{ label: "All", value: "all" },
|
||||
];
|
||||
|
||||
const shellOptions = [
|
||||
{ label: "CMD", value: "cmd" },
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
];
|
||||
|
||||
const patchModeOptions = [
|
||||
{ label: "Scan", value: "scan" },
|
||||
{ label: "Install", value: "install" },
|
||||
|
@ -200,6 +228,20 @@ export default {
|
|||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
const shellOptions = computed(() => {
|
||||
if (state.value.osType === "windows") {
|
||||
return [
|
||||
{ label: "CMD", value: "cmd" },
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ label: "Bash", value: "/bin/bash" },
|
||||
{ label: "Custom", value: "custom" },
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// quasar dialog setup
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
|
@ -214,8 +256,10 @@ export default {
|
|||
mode: props.mode,
|
||||
target: "client",
|
||||
monType: "all",
|
||||
osType: "windows",
|
||||
cmd: "",
|
||||
shell: "cmd",
|
||||
custom_shell: null,
|
||||
patchMode: "scan",
|
||||
offlineAgents: false,
|
||||
client,
|
||||
|
@ -236,6 +280,19 @@ export default {
|
|||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => state.value.osType,
|
||||
(newValue, oldValue) => {
|
||||
state.value.custom_shell = null;
|
||||
|
||||
if (newValue === "windows") {
|
||||
state.value.shell = "cmd";
|
||||
} else {
|
||||
state.value.shell = "/bin/bash";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
|
||||
|
@ -259,6 +316,19 @@ export default {
|
|||
: "";
|
||||
});
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
if (props.mode !== "script") return [];
|
||||
|
||||
if (state.value.osType === "linux")
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(script => script.category || script.shell === "shell" || script.shell === "python")
|
||||
);
|
||||
else
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(script => script.category || script.shell !== "shell")
|
||||
);
|
||||
});
|
||||
|
||||
// component lifecycle hooks
|
||||
onMounted(() => {
|
||||
getAgentOptions();
|
||||
|
@ -273,13 +343,14 @@ export default {
|
|||
agentOptions,
|
||||
clientOptions,
|
||||
siteOptions,
|
||||
scriptOptions,
|
||||
filteredScriptOptions,
|
||||
loading,
|
||||
shellOptions,
|
||||
|
||||
// non-reactive data
|
||||
monTypeOptions,
|
||||
osTypeOptions,
|
||||
targetOptions,
|
||||
shellOptions,
|
||||
patchModeOptions,
|
||||
|
||||
//computed
|
||||
|
@ -287,6 +358,7 @@ export default {
|
|||
|
||||
//methods
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
|
|
|
@ -25,6 +25,28 @@
|
|||
<q-card-section class="q-gutter-sm">
|
||||
<q-select dense options-dense outlined label="Site" v-model="site" :options="sites" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio
|
||||
v-model="agentOS"
|
||||
val="windows"
|
||||
label="Windows"
|
||||
@update:model-value="
|
||||
installMethod = 'exe';
|
||||
arch = '64';
|
||||
"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="agentOS"
|
||||
val="linux"
|
||||
label="Linux"
|
||||
@update:model-value="
|
||||
installMethod = 'linux';
|
||||
arch = 'amd64';
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio v-model="agenttype" val="server" label="Server" @update:model-value="power = false" />
|
||||
|
@ -44,7 +66,7 @@
|
|||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-card-section v-show="agentOS === 'windows'">
|
||||
<div class="q-gutter-sm">
|
||||
<q-checkbox v-model="rdp" dense label="Enable RDP" />
|
||||
<q-checkbox v-model="ping" dense label="Enable Ping">
|
||||
|
@ -54,18 +76,27 @@
|
|||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
OS
|
||||
Arch
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio v-model="arch" val="64" label="64 bit" />
|
||||
<q-radio v-model="arch" val="32" label="32 bit" />
|
||||
<q-radio v-model="arch" val="64" label="64 bit" v-show="agentOS === 'windows'" />
|
||||
<q-radio v-model="arch" val="32" label="32 bit" v-show="agentOS === 'windows'" />
|
||||
<q-radio v-model="arch" val="amd64" label="64 bit" v-show="agentOS !== 'windows'" />
|
||||
<q-radio v-model="arch" val="386" label="32 bit" v-show="agentOS !== 'windows'" />
|
||||
<q-radio v-model="arch" val="arm64" label="ARM 64 bit" v-show="agentOS !== 'windows'" />
|
||||
<q-radio v-model="arch" val="arm" label="ARM 32 bit (Rasp Pi)" v-show="agentOS !== 'windows'" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
Installation Method
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio v-model="installMethod" val="exe" label="Dynamically generated exe" />
|
||||
<q-radio v-model="installMethod" val="powershell" label="Powershell" />
|
||||
<q-radio v-model="installMethod" val="manual" label="Manual" />
|
||||
<q-radio
|
||||
v-model="installMethod"
|
||||
val="exe"
|
||||
v-show="agentOS === 'windows'"
|
||||
label="Dynamically generated exe"
|
||||
/>
|
||||
<q-radio v-model="installMethod" val="powershell" v-show="agentOS === 'windows'" label="Powershell" />
|
||||
<q-radio v-model="installMethod" val="manual" v-show="agentOS === 'windows'" label="Manual" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="left">
|
||||
|
@ -105,6 +136,7 @@ export default {
|
|||
info: {},
|
||||
installMethod: "exe",
|
||||
arch: "64",
|
||||
agentOS: "windows",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -161,6 +193,7 @@ export default {
|
|||
arch: this.arch,
|
||||
api,
|
||||
fileName,
|
||||
os: this.agentOS,
|
||||
};
|
||||
|
||||
if (this.installMethod === "manual") {
|
||||
|
@ -192,19 +225,24 @@ export default {
|
|||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
} else if (this.installMethod === "powershell") {
|
||||
const psName = `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.ps1`;
|
||||
} else if (this.installMethod === "powershell" || this.installMethod === "linux") {
|
||||
this.$q.loading.show();
|
||||
let ext = this.installMethod === "powershell" ? "ps1" : "sh";
|
||||
const scriptName = `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.${ext}`;
|
||||
this.$axios
|
||||
.post("/agents/installer/", data, { responseType: "blob" })
|
||||
.then(({ data }) => {
|
||||
this.$q.loading.hide();
|
||||
const blob = new Blob([data], { type: "text/plain" });
|
||||
let link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = psName;
|
||||
link.download = scriptName;
|
||||
link.click();
|
||||
this.showDLMessage();
|
||||
if (this.installMethod === "powershell") this.showDLMessage();
|
||||
})
|
||||
.catch(e => {});
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
showDLMessage() {
|
||||
|
@ -230,6 +268,9 @@ export default {
|
|||
case "manual":
|
||||
text = "Show manual installation instructions";
|
||||
break;
|
||||
case "linux":
|
||||
text = "Download linux install script";
|
||||
break;
|
||||
}
|
||||
|
||||
return text;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<tactical-dropdown
|
||||
:rules="[val => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="scriptOptions"
|
||||
:options="filteredScriptOptions"
|
||||
label="Select script"
|
||||
outlined
|
||||
mapOptions
|
||||
|
@ -102,13 +102,14 @@
|
|||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
import { runScript } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { formatScriptSyntax } from "@/utils/format";
|
||||
import { formatScriptSyntax, removeExtraOptionCategories } from "@/utils/format";
|
||||
|
||||
//ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
|
||||
|
||||
|
@ -136,13 +137,14 @@ export default {
|
|||
// setup dropdowns
|
||||
const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } = useScriptDropdown(props.script, {
|
||||
onMount: true,
|
||||
filterByPlatform: props.agent.plat,
|
||||
});
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
// main run script functionaity
|
||||
const state = ref({
|
||||
output: "wait",
|
||||
email: [],
|
||||
emails: [],
|
||||
emailMode: "default",
|
||||
custom_field: null,
|
||||
save_all_output: false,
|
||||
|
@ -171,6 +173,17 @@ export default {
|
|||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
if (props.agent.plat === "linux")
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(script => script.category || script.shell === "shell" || script.shell === "python")
|
||||
);
|
||||
else
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(script => script.category || script.shell !== "shell")
|
||||
);
|
||||
});
|
||||
|
||||
// watchers
|
||||
watch([() => state.value.output, () => state.value.emailMode], () => (state.value.emails = []));
|
||||
|
||||
|
@ -178,7 +191,7 @@ export default {
|
|||
// reactive data
|
||||
state,
|
||||
loading,
|
||||
scriptOptions,
|
||||
filteredScriptOptions,
|
||||
link,
|
||||
syntax,
|
||||
ret,
|
||||
|
|
|
@ -12,10 +12,29 @@
|
|||
<q-card-section>
|
||||
<p>Shell</p>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio dense v-model="state.shell" val="cmd" label="CMD" />
|
||||
<q-radio dense v-model="state.shell" val="powershell" label="Powershell" />
|
||||
<q-radio
|
||||
v-if="agent.plat !== 'windows'"
|
||||
dense
|
||||
v-model="state.shell"
|
||||
val="/bin/bash"
|
||||
label="Bash"
|
||||
@update:model-value="state.custom_shell = null"
|
||||
/>
|
||||
<q-radio v-if="agent.plat !== 'windows'" dense v-model="state.shell" val="custom" label="Custom" />
|
||||
<q-radio v-if="agent.plat === 'windows'" dense v-model="state.shell" val="cmd" label="CMD" />
|
||||
<q-radio v-if="agent.plat === 'windows'" dense v-model="state.shell" val="powershell" label="Powershell" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="state.shell === 'custom'">
|
||||
<q-input
|
||||
v-model="state.custom_shell"
|
||||
outlined
|
||||
label="Custom shell"
|
||||
stack-label
|
||||
placeholder="/usr/bin/python3"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model.number="state.timeout"
|
||||
|
@ -38,11 +57,7 @@
|
|||
outlined
|
||||
label="Command"
|
||||
stack-label
|
||||
:placeholder="
|
||||
state.shell === 'cmd'
|
||||
? 'rmdir /S /Q C:\\Windows\\System32'
|
||||
: 'Remove-Item -Recurse -Force C:\\Windows\\System32'
|
||||
"
|
||||
:placeholder="cmdPlaceholder(state.shell)"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
@ -63,6 +78,7 @@
|
|||
import { ref } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { sendAgentCommand } from "@/api/agents";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
|
||||
export default {
|
||||
name: "SendCommand",
|
||||
|
@ -76,9 +92,10 @@ export default {
|
|||
|
||||
// run command logic
|
||||
const state = ref({
|
||||
shell: "cmd",
|
||||
shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash",
|
||||
cmd: null,
|
||||
timeout: 30,
|
||||
custom_shell: null,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
@ -103,6 +120,7 @@ export default {
|
|||
|
||||
// methods
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
|
|
|
@ -317,9 +317,9 @@
|
|||
<q-input dense outlined v-model="settings.mesh_token" class="col-6" />
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4"></div>
|
||||
<div class="col-4">Mesh Device Group Name:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-btn label="Upload Mesh Agents" color="primary" @click="uploadMeshAgentModal" />
|
||||
<q-input dense outlined v-model="settings.mesh_device_group" class="col-6" />
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="customfields">
|
||||
|
@ -433,7 +433,6 @@ import CustomFields from "@/components/modals/coresettings/CustomFields";
|
|||
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable";
|
||||
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable";
|
||||
import APIKeysTable from "@/components/core/APIKeysTable";
|
||||
import UploadMesh from "@/components/core/UploadMesh";
|
||||
|
||||
export default {
|
||||
name: "EditCoreSettings",
|
||||
|
@ -596,11 +595,6 @@ export default {
|
|||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
uploadMeshAgentModal() {
|
||||
this.$q.dialog({
|
||||
component: UploadMesh,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getCoreSettings();
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
<v-ace-editor
|
||||
v-model:value="formScript.script_body"
|
||||
class="col-8"
|
||||
:lang="formScript.shell === 'cmd' ? 'batchfile' : formScript.shell"
|
||||
:lang="lang"
|
||||
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
|
||||
:style="{ height: `${maximized ? '87vh' : '64vh'}` }"
|
||||
wrap
|
||||
|
@ -127,7 +127,7 @@
|
|||
|
||||
<script>
|
||||
// composable imports
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useQuasar, useDialogPluginComponent } from "quasar";
|
||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
||||
import { useAgentDropdown } from "@/composables/agents";
|
||||
|
@ -142,6 +142,7 @@ import { VAceEditor } from "vue3-ace-editor";
|
|||
import "ace-builds/src-noconflict/mode-powershell";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/mode-batchfile";
|
||||
import "ace-builds/src-noconflict/mode-sh";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow";
|
||||
|
||||
|
@ -185,6 +186,16 @@ export default {
|
|||
const loading = ref(false);
|
||||
const agentLoading = ref(false);
|
||||
|
||||
// watch(script.value, (newValue, oldValue) => {
|
||||
// if (!props.script && script.value.script_body === "") {
|
||||
// if (newValue.shell === "shell") {
|
||||
// script.value.script_body = "#!/bin/bash\n\n# don't forget to include the shebang above!\n\n";
|
||||
// } else if (newValue.shell === "python") {
|
||||
// script.value.script_body = "#!/usr/bin/python3\n\n# don't forget to include the shebang above!\n\n";
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.script) {
|
||||
return props.readonly
|
||||
|
@ -197,6 +208,15 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
// convert highlighter language to match what ace expects
|
||||
const lang = computed(() => {
|
||||
if (script.value.shell === "cmd") return "batchfile";
|
||||
else if (script.value.shell === "powershell") return "powershell";
|
||||
else if (script.value.shell === "python") return "python";
|
||||
else if (script.value.shell === "shell") return "sh";
|
||||
else return "";
|
||||
});
|
||||
|
||||
// get code if editing or cloning script
|
||||
if (props.script)
|
||||
downloadScript(script.value.id, { with_snippets: props.readonly }).then(r => {
|
||||
|
@ -218,7 +238,9 @@ export default {
|
|||
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
@ -248,6 +270,7 @@ export default {
|
|||
agentOptions,
|
||||
agent,
|
||||
agentLoading,
|
||||
lang,
|
||||
|
||||
// non-reactive data
|
||||
shellOptions,
|
||||
|
|
|
@ -106,6 +106,9 @@
|
|||
<q-icon v-else-if="props.node.shell === 'cmd'" name="mdi-microsoft-windows" color="primary">
|
||||
<q-tooltip> Batch </q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else-if="props.node.shell === 'shell'" name="mdi-bash" color="primary">
|
||||
<q-tooltip> Shell </q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<span class="q-pl-xs text-weight-bold">{{ props.node.name }}</span>
|
||||
<span class="q-pl-xs">{{ props.node.description }}</span>
|
||||
|
@ -289,6 +292,9 @@
|
|||
<q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
|
||||
<q-tooltip> Batch </q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else-if="props.row.shell === 'shell'" size="sm" name="mdi-bash" color="primary">
|
||||
<q-tooltip> Shell </q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<!-- name -->
|
||||
<q-td>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<v-ace-editor
|
||||
v-model:value="formSnippet.code"
|
||||
:lang="formSnippet.shell === 'cmd' ? 'batchfile' : formSnippet.shell"
|
||||
:lang="lang"
|
||||
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
|
||||
:style="{ height: `${maximized ? '80vh' : '70vh'}` }"
|
||||
wrap
|
||||
|
@ -70,6 +70,7 @@ import { VAceEditor } from "vue3-ace-editor";
|
|||
import "ace-builds/src-noconflict/mode-powershell";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/mode-batchfile";
|
||||
import "ace-builds/src-noconflict/mode-sh";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow";
|
||||
|
||||
|
@ -104,6 +105,15 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
// convert highlighter language to match what ace expects
|
||||
const lang = computed(() => {
|
||||
if (snippet.value.shell === "cmd") return "batchfile";
|
||||
else if (snippet.value.shell === "powershell") return "powershell";
|
||||
else if (snippet.value.shell === "python") return "python";
|
||||
else if (snippet.value.shell === "shell") return "sh";
|
||||
else return "";
|
||||
});
|
||||
|
||||
async function submitForm() {
|
||||
loading.value = true;
|
||||
try {
|
||||
|
@ -121,6 +131,7 @@ export default {
|
|||
// reactive data
|
||||
formSnippet: snippet.value,
|
||||
maximized,
|
||||
lang,
|
||||
loading,
|
||||
|
||||
// non-reactive data
|
||||
|
|
|
@ -72,6 +72,9 @@
|
|||
<q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
|
||||
<q-tooltip> Batch </q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else-if="props.row.shell === 'shell'" name="mdi-bash" color="primary">
|
||||
<q-tooltip> Shell </q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<!-- name -->
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
|
|
|
@ -34,11 +34,11 @@
|
|||
<q-file
|
||||
label="Script Upload"
|
||||
v-model="file"
|
||||
hint="Supported file types: .ps1, .bat, .py"
|
||||
hint="Supported file types: .ps1, .bat, .py, .sh"
|
||||
filled
|
||||
dense
|
||||
counter
|
||||
accept=".ps1, .bat, .py"
|
||||
accept=".ps1, .bat, .py, .sh"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
|
|
|
@ -24,3 +24,9 @@ export function useAgentDropdown() {
|
|||
getAgentOptions
|
||||
}
|
||||
}
|
||||
|
||||
export function cmdPlaceholder(shell) {
|
||||
if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
|
||||
else if (shell === "powershell") return "Remove-Item -Recurse -Force C:\\Windows\\System32";
|
||||
else return "rm -rf --no-preserve-root /";
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
|
|||
const link = ref("")
|
||||
const baseUrl = "https://github.com/amidaware/community-scripts/blob/main/scripts/"
|
||||
|
||||
// specifing flat returns an array of script names versus {value:id, label: hostname}
|
||||
async function getScriptOptions(showCommunityScripts = false, flat = false) {
|
||||
scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat))
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions(showCommunityScripts = false) {
|
||||
scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts })))
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
|
@ -25,7 +25,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
|
|||
defaultTimeout.value = tmpScript.timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
syntax.value = tmpScript.syntax
|
||||
link.value = `${baseUrl}${tmpScript.filename}`
|
||||
link.value = tmpScript.script_type === "builtin" ? `${baseUrl}${tmpScript.filename}` : null
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -53,4 +53,5 @@ export const shellOptions = [
|
|||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" }
|
||||
];
|
|
@ -13,6 +13,7 @@ export default function () {
|
|||
treeReady: false,
|
||||
selectedTree: "",
|
||||
selectedRow: null,
|
||||
agentPlatform: "windows",
|
||||
agentTableLoading: false,
|
||||
needrefresh: false,
|
||||
refreshSummaryTab: false,
|
||||
|
@ -55,6 +56,9 @@ export default function () {
|
|||
setActiveRow(state, agent_id) {
|
||||
state.selectedRow = agent_id;
|
||||
},
|
||||
setAgentPlatform(state, agentPlatform) {
|
||||
state.agentPlatform = agentPlatform;
|
||||
},
|
||||
retrieveToken(state, { token, username }) {
|
||||
state.token = token;
|
||||
state.username = username;
|
||||
|
|
|
@ -3,6 +3,17 @@ import { validateTimePeriod } from "@/utils/validation"
|
|||
|
||||
// dropdown options formatting
|
||||
|
||||
export function removeExtraOptionCategories(array) {
|
||||
let tmp = []
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (!(array[i].category && array[i + 1].category)) {
|
||||
tmp.push(array[i])
|
||||
}
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
function _formatOptions(data, { label, value = "id", flat = false, allowDuplicates = true }) {
|
||||
if (!flat)
|
||||
// returns array of options in object format [{label: label, value: 1}]
|
||||
|
@ -21,41 +32,35 @@ function _formatOptions(data, { label, value = "id", flat = false, allowDuplicat
|
|||
}
|
||||
}
|
||||
|
||||
export function formatScriptOptions(data, flat = false) {
|
||||
if (flat) {
|
||||
// returns just script names in array
|
||||
return _formatOptions(data, { label: "name", value: "pk", flat: true, allowDuplicates: false })
|
||||
} else {
|
||||
export function formatScriptOptions(data) {
|
||||
let options = [];
|
||||
let categories = [];
|
||||
let create_unassigned = false
|
||||
data.forEach(script => {
|
||||
if (!!script.category && !categories.includes(script.category)) {
|
||||
categories.push(script.category);
|
||||
} else if (!script.category) {
|
||||
create_unassigned = true
|
||||
}
|
||||
});
|
||||
|
||||
let options = [];
|
||||
let categories = [];
|
||||
let create_unassigned = false
|
||||
if (create_unassigned) categories.push("Unassigned")
|
||||
|
||||
categories.sort().forEach(cat => {
|
||||
options.push({ category: cat });
|
||||
let tmp = [];
|
||||
data.forEach(script => {
|
||||
if (!!script.category && !categories.includes(script.category)) {
|
||||
categories.push(script.category);
|
||||
} else if (!script.category) {
|
||||
create_unassigned = true
|
||||
if (script.category === cat) {
|
||||
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax, script_type: script.script_type, shell: script.shell });
|
||||
} else if (cat === "Unassigned" && !script.category) {
|
||||
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax, script_type: script.script_type, shell: script.shell });
|
||||
}
|
||||
});
|
||||
})
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
if (create_unassigned) categories.push("Unassigned")
|
||||
|
||||
categories.sort().forEach(cat => {
|
||||
options.push({ category: cat });
|
||||
let tmp = [];
|
||||
data.forEach(script => {
|
||||
if (script.category === cat) {
|
||||
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
|
||||
} else if (cat === "Unassigned" && !script.category) {
|
||||
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
|
||||
}
|
||||
})
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatAgentOptions(data, flat = false, value_field = "agent_id") {
|
||||
|
|
|
@ -367,6 +367,13 @@ export default {
|
|||
name: "dashboardalert",
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "plat",
|
||||
label: "",
|
||||
field: "plat",
|
||||
sortable: true,
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "checks-status",
|
||||
align: "left",
|
||||
|
@ -458,6 +465,7 @@ export default {
|
|||
],
|
||||
visibleColumns: [
|
||||
"smsalert",
|
||||
"plat",
|
||||
"emailalert",
|
||||
"dashboardalert",
|
||||
"checks-status",
|
||||
|
|
|
@ -28,24 +28,6 @@
|
|||
<div>Default timezone for agents:</div>
|
||||
<q-select dense options-dense outlined v-model="timezone" :options="allTimezones" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<q-file
|
||||
v-model="meshagent"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
label="Upload MeshAgent"
|
||||
stack-label
|
||||
filled
|
||||
counter
|
||||
class="full-width"
|
||||
accept=".exe"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="center">
|
||||
<q-btn label="Finish" color="primary" class="full-width" type="submit" />
|
||||
</q-card-actions>
|
||||
|
@ -71,7 +53,6 @@ export default {
|
|||
site: {
|
||||
name: "",
|
||||
},
|
||||
meshagent: null,
|
||||
allTimezones: [],
|
||||
timezone: null,
|
||||
arch: "64",
|
||||
|
@ -88,19 +69,11 @@ export default {
|
|||
};
|
||||
this.$axios
|
||||
.post("/clients/", data)
|
||||
.then(r => {
|
||||
let formData = new FormData();
|
||||
formData.append("arch", this.arch);
|
||||
formData.append("meshagent", this.meshagent);
|
||||
this.$axios
|
||||
.put("/core/uploadmesh/", formData)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.$router.push({ name: "Dashboard" });
|
||||
})
|
||||
.catch(e => this.$q.loading.hide());
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.$router.push({ name: "Dashboard" });
|
||||
})
|
||||
.catch(e => this.$q.loading.hide());
|
||||
.catch(() => this.$q.loading.hide());
|
||||
},
|
||||
getSettings() {
|
||||
this.$axios
|
||||
|
@ -109,7 +82,7 @@ export default {
|
|||
this.allTimezones = Object.freeze(r.data.all_timezones);
|
||||
this.timezone = r.data.default_time_zone;
|
||||
})
|
||||
.catch(e => {});
|
||||
.catch(() => {});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -12,9 +12,14 @@
|
|||
>
|
||||
<q-tab name="terminal" icon="fas fa-terminal" label="Terminal" />
|
||||
<q-tab name="filebrowser" icon="far fa-folder-open" label="File Browser" />
|
||||
<q-tab name="services" icon="fas fa-cogs" label="Services" />
|
||||
<q-tab v-if="$route.query.agentPlatform === 'windows'" name="services" icon="fas fa-cogs" label="Services" />
|
||||
<q-tab name="processes" icon="fas fa-chart-area" label="Processes" />
|
||||
<q-tab name="eventlog" icon="fas fa-clipboard-list" label="Event Log" />
|
||||
<q-tab
|
||||
v-if="$route.query.agentPlatform === 'windows'"
|
||||
name="eventlog"
|
||||
icon="fas fa-clipboard-list"
|
||||
label="Event Log"
|
||||
/>
|
||||
</q-tabs>
|
||||
<q-separator />
|
||||
<q-tab-panels v-model="tab">
|
||||
|
@ -27,11 +32,11 @@
|
|||
<q-tab-panel name="processes" class="q-pa-none">
|
||||
<ProcessManager :agent_id="agent_id" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="services" class="q-pa-none">
|
||||
<ServicesManager :agent_id="agent_id" />
|
||||
<q-tab-panel v-if="$route.query.agentPlatform === 'windows'" name="services" class="q-pa-none">
|
||||
<ServicesManager :agent_id="agent_id" :agentPlatform="$route.query.agentPlatform" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="eventlog" class="q-pa-none">
|
||||
<EventLogManager :agent_id="agent_id" />
|
||||
<q-tab-panel v-if="$route.query.agentPlatform === 'windows'" name="eventlog" class="q-pa-none">
|
||||
<EventLogManager :agent_id="agent_id" :agentPlatform="$route.query.agentPlatform" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="filebrowser" class="q-pa-none">
|
||||
<iframe :src="file" :style="{ height: `${$q.screen.height - 30}px`, width: `${$q.screen.width}px` }"></iframe>
|
||||
|
|
Loading…
Reference in New Issue