Release 0.19.4

This commit is contained in:
wh1te909 2024-10-23 17:25:12 +00:00
commit 59c880dc36
23 changed files with 296 additions and 62 deletions

View File

@ -39,7 +39,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
## Mac agent versions supported
- 64 bit Intel and Apple Silicon (M1, M2)
- 64 bit Intel and Apple Silicon (M-Series)
## Installation / Backup / Restore / Usage

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-06 05:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0037_role_can_run_server_scripts_role_can_use_webterm"),
]
operations = [
migrations.AddField(
model_name="role",
name="can_edit_global_keystore",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="role",
name="can_view_global_keystore",
field=models.BooleanField(default=False),
),
]

View File

@ -131,6 +131,8 @@ class Role(BaseAuditModel):
can_manage_customfields = models.BooleanField(default=False)
can_run_server_scripts = models.BooleanField(default=False)
can_use_webterm = models.BooleanField(default=False)
can_view_global_keystore = models.BooleanField(default=False)
can_edit_global_keystore = models.BooleanField(default=False)
# checks
can_list_checks = models.BooleanField(default=False)

View File

@ -0,0 +1,36 @@
# Generated by Django 4.2.16 on 2024-10-05 20:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("core", "0047_alter_coresettings_notify_on_warning_alerts"),
("agents", "0059_alter_agenthistory_id"),
]
operations = [
migrations.AddField(
model_name="agenthistory",
name="collector_all_output",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="agenthistory",
name="custom_field",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="history",
to="core.customfield",
),
),
migrations.AddField(
model_name="agenthistory",
name="save_to_agent_note",
field=models.BooleanField(default=False),
),
]

View File

@ -1122,6 +1122,15 @@ class AgentHistory(models.Model):
on_delete=models.SET_NULL,
)
script_results = models.JSONField(null=True, blank=True)
custom_field = models.ForeignKey(
"core.CustomField",
null=True,
blank=True,
related_name="history",
on_delete=models.SET_NULL,
)
collector_all_output = models.BooleanField(default=False)
save_to_agent_note = models.BooleanField(default=False)
def __str__(self) -> str:
return f"{self.agent.hostname} - {self.type}"

View File

@ -2,7 +2,7 @@ import json
import os
from itertools import cycle
from typing import TYPE_CHECKING
from unittest.mock import patch
from unittest.mock import PropertyMock, patch
from zoneinfo import ZoneInfo
from django.conf import settings
@ -768,6 +768,67 @@ class TestAgentViews(TacticalTestCase):
self.assertEqual(Note.objects.get(agent=self.agent).note, "ok")
# test run on server
with patch("core.utils.run_server_script") as mock_run_server_script:
mock_run_server_script.return_value = ("output", "error", 1.23456789, 0)
data = {
"script": script.pk,
"output": "wait",
"args": ["arg1", "arg2"],
"timeout": 15,
"run_as_user": False,
"env_vars": ["key1=val1", "key2=val2"],
"run_on_server": True,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
if not hist:
raise AgentHistory.DoesNotExist
mock_run_server_script.assert_called_with(
body=script.script_body,
args=script.parse_script_args(self.agent, script.shell, data["args"]),
env_vars=script.parse_script_env_vars(
self.agent, script.shell, data["env_vars"]
),
shell=script.shell,
timeout=18,
)
expected_ret = {
"stdout": "output",
"stderr": "error",
"execution_time": "1.2346",
"retcode": 0,
}
self.assertEqual(r.data, expected_ret)
hist.refresh_from_db()
expected_script_results = {**expected_ret, "id": hist.pk}
self.assertEqual(hist.script_results, expected_script_results)
# test run on server with server scripts disabled
with patch(
"core.models.CoreSettings.server_scripts_enabled",
new_callable=PropertyMock,
) as server_scripts_enabled:
server_scripts_enabled.return_value = False
data = {
"script": script.pk,
"output": "wait",
"args": ["arg1", "arg2"],
"timeout": 15,
"run_as_user": False,
"env_vars": ["key1=val1", "key2=val2"],
"run_on_server": True,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
def test_get_notes(self):
url = f"{base_url}/notes/"

View File

@ -768,6 +768,10 @@ def run_script(request, agent_id):
run_as_user: bool = request.data["run_as_user"]
env_vars: list[str] = request.data["env_vars"]
req_timeout = int(request.data["timeout"]) + 3
run_on_server: bool | None = request.data.get("run_on_server")
if run_on_server and not get_core_settings().server_scripts_enabled:
return notify_error("This feature is disabled.")
AuditLog.audit_script_run(
username=request.user.username,
@ -784,6 +788,29 @@ def run_script(request, agent_id):
)
history_pk = hist.pk
if run_on_server:
from core.utils import run_server_script
r = run_server_script(
body=script.script_body,
args=script.parse_script_args(agent, script.shell, args),
env_vars=script.parse_script_env_vars(agent, script.shell, env_vars),
shell=script.shell,
timeout=req_timeout,
)
ret = {
"stdout": r[0],
"stderr": r[1],
"execution_time": "{:.4f}".format(r[2]),
"retcode": r[3],
}
hist.script_results = {**ret, "id": history_pk}
hist.save(update_fields=["script_results"])
return Response(ret)
if output == "wait":
r = agent.run_script(
scriptpk=script.pk,
@ -1008,6 +1035,16 @@ def bulk(request):
elif request.data["mode"] == "script":
script = get_object_or_404(Script, pk=request.data["script"])
# prevent API from breaking for those who haven't updated payload
try:
custom_field_pk = request.data["custom_field"]
collector_all_output = request.data["collector_all_output"]
save_to_agent_note = request.data["save_to_agent_note"]
except KeyError:
custom_field_pk = None
collector_all_output = False
save_to_agent_note = False
bulk_script_task.delay(
script_pk=script.pk,
agent_pks=agents,
@ -1016,6 +1053,9 @@ def bulk(request):
username=request.user.username[:50],
run_as_user=request.data["run_as_user"],
env_vars=request.data["env_vars"],
custom_field_pk=custom_field_pk,
collector_all_output=collector_all_output,
save_to_agent_note=save_to_agent_note,
)
return Response(f"{script.name} will now be run on {len(agents)} agents. {ht}")

View File

@ -12,7 +12,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from accounts.models import User
from agents.models import Agent, AgentHistory
from agents.models import Agent, AgentHistory, Note
from agents.serializers import AgentHistorySerializer
from alerts.tasks import cache_agents_alert_template
from apiv3.utils import get_agent_config
@ -40,6 +40,7 @@ from tacticalrmm.constants import (
AuditActionType,
AuditObjType,
CheckStatus,
CustomFieldModel,
DebugLogType,
GoArch,
MeshAgentIdent,
@ -581,11 +582,39 @@ class AgentHistoryResult(APIView):
request.data["script_results"]["retcode"] = 1
hist = get_object_or_404(
AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
AgentHistory.objects.select_related("custom_field").filter(
agent__agent_id=agentid
),
pk=pk,
)
s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
s.is_valid(raise_exception=True)
s.save()
if hist.custom_field:
if hist.custom_field.model == CustomFieldModel.AGENT:
field = hist.custom_field.get_or_create_field_value(hist.agent)
elif hist.custom_field.model == CustomFieldModel.CLIENT:
field = hist.custom_field.get_or_create_field_value(hist.agent.client)
elif hist.custom_field.model == CustomFieldModel.SITE:
field = hist.custom_field.get_or_create_field_value(hist.agent.site)
r = request.data["script_results"]["stdout"]
value = (
r.strip()
if hist.collector_all_output
else r.strip().split("\n")[-1].strip()
)
field.save_to_field(value)
if hist.save_to_agent_note:
Note.objects.create(
agent=hist.agent,
user=request.user,
note=request.data["script_results"]["stdout"],
)
return Response("ok")

View File

@ -365,9 +365,11 @@ class CheckResult(models.Model):
if len(self.history) > 15:
self.history = self.history[-15:]
update_fields.extend(["history"])
update_fields.extend(["history", "more_info"])
avg = int(mean(self.history))
txt = "Memory Usage" if check.check_type == CheckType.MEMORY else "CPU Load"
self.more_info = f"Average {txt}: {avg}%"
if check.error_threshold and avg > check.error_threshold:
self.status = CheckStatus.FAILING

View File

@ -133,6 +133,7 @@ class Site(BaseAuditModel):
old_site.alert_template != self.alert_template
or old_site.workstation_policy != self.workstation_policy
or old_site.server_policy != self.server_policy
or old_site.client != self.client
):
cache_agents_alert_template.delay()

View File

@ -11,6 +11,14 @@ class CoreSettingsPerms(permissions.BasePermission):
return _has_perm(r, "can_edit_core_settings")
class GlobalKeyStorePerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_view_global_keystore")
return _has_perm(r, "can_edit_global_keystore")
class URLActionPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method in {"GET", "PATCH"}:

View File

@ -43,6 +43,7 @@ from .permissions import (
CodeSignPerms,
CoreSettingsPerms,
CustomFieldPerms,
GlobalKeyStorePerms,
RunServerScriptPerms,
ServerMaintPerms,
URLActionPerms,
@ -310,7 +311,7 @@ class CodeSign(APIView):
class GetAddKeyStore(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
permission_classes = [IsAuthenticated, GlobalKeyStorePerms]
def get(self, request):
keys = GlobalKVStore.objects.all()
@ -325,7 +326,7 @@ class GetAddKeyStore(APIView):
class UpdateDeleteKeyStore(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
permission_classes = [IsAuthenticated, GlobalKeyStorePerms]
def put(self, request, pk):
key = get_object_or_404(GlobalKVStore, pk=pk)

View File

@ -1,47 +1,46 @@
adrf==0.1.6
asgiref==3.8.1
celery==5.4.0
certifi==2024.7.4
cffi==1.16.0
certifi==2024.8.30
cffi==1.17.1
channels==4.1.0
channels_redis==4.2.0
cryptography==42.0.8
Django==4.2.14
django-cors-headers==4.4.0
django-filter==24.2
cryptography==43.0.3
Django==4.2.16
django-cors-headers==4.5.0
django-filter==24.3
django-rest-knox==4.2.0
djangorestframework==3.15.2
drf-spectacular==0.27.2
hiredis==2.3.2
kombu==5.3.7
meshctrl==0.1.15
msgpack==1.0.8
nats-py==2.8.0
msgpack==1.1.0
nats-py==2.9.0
packaging==24.1
psutil==5.9.8
psycopg[binary]==3.1.19
psutil==6.0.0
psycopg[binary]==3.2.3
pycparser==2.22
pycryptodome==3.20.0
pycryptodome==3.21.0
pyotp==2.9.0
pyparsing==3.1.2
pyparsing==3.1.4
python-ipware==2.0.2
qrcode==7.4.2
redis==5.0.7
qrcode==8.0
redis==5.0.8
requests==2.32.3
six==1.16.0
sqlparse==0.5.0
sqlparse==0.5.1
twilio==8.13.0
urllib3==2.2.2
uvicorn[standard]==0.30.1
uWSGI==2.0.26
urllib3==2.2.3
uvicorn[standard]==0.31.1
uWSGI==2.0.27
validators==0.24.0
vine==5.1.0
websockets==12.0
zipp==3.19.2
pandas==2.2.2
websockets==13.1
zipp==3.20.2
pandas==2.2.3
kaleido==0.2.1
jinja2==3.1.4
markdown==3.6
plotly==5.22.0
markdown==3.7
plotly==5.24.1
weasyprint==62.3
ocxsect==0.1.5

View File

@ -118,8 +118,14 @@ class Script(BaseAuditModel):
args = script["args"] if "args" in script.keys() else []
env = script["env"] if "env" in script.keys() else []
syntax = script["syntax"] if "syntax" in script.keys() else ""
run_as_user = (
script["run_as_user"] if "run_as_user" in script.keys() else False
)
supported_platforms = (
script["supported_platforms"]
if "supported_platforms" in script.keys()
@ -135,7 +141,9 @@ class Script(BaseAuditModel):
i.shell = script["shell"]
i.default_timeout = default_timeout
i.args = args
i.env_vars = env
i.syntax = syntax
i.run_as_user = run_as_user
i.filename = script["filename"]
i.supported_platforms = supported_platforms
@ -163,8 +171,10 @@ class Script(BaseAuditModel):
category=category,
default_timeout=default_timeout,
args=args,
env_vars=env,
filename=script["filename"],
syntax=syntax,
run_as_user=run_as_user,
supported_platforms=supported_platforms,
)
# new_script.hash_script_body() # also saves script

View File

@ -48,6 +48,7 @@ class ScriptSerializer(ModelSerializer):
"run_as_user",
"env_vars",
]
extra_kwargs = {"script_body": {"trim_whitespace": False}}
class ScriptCheckSerializer(ModelSerializer):
@ -63,3 +64,4 @@ class ScriptSnippetSerializer(ModelSerializer):
class Meta:
model = ScriptSnippet
fields = "__all__"
extra_kwargs = {"code": {"trim_whitespace": False}}

View File

@ -54,12 +54,21 @@ def bulk_script_task(
username: str,
run_as_user: bool = False,
env_vars: list[str] = [],
custom_field_pk: int | None,
collector_all_output: bool = False,
save_to_agent_note: bool = False,
) -> None:
script = Script.objects.get(pk=script_pk)
# always override if set on script model
if script.run_as_user:
run_as_user = True
custom_field = None
if custom_field_pk:
from core.models import CustomField
custom_field = CustomField.objects.get(pk=custom_field_pk)
items = []
agent: "Agent"
for agent in Agent.objects.filter(pk__in=agent_pks):
@ -68,6 +77,9 @@ def bulk_script_task(
type=AgentHistoryType.SCRIPT_RUN,
script=script,
username=username,
custom_field=custom_field,
collector_all_output=collector_all_output,
save_to_agent_note=save_to_agent_note,
)
data = {
"func": "runscriptfull",

View File

@ -21,21 +21,21 @@ MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh"
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.19.3"
TRMM_VERSION = "0.19.4"
# https://github.com/amidaware/tacticalrmm-web
WEB_VERSION = "0.101.48"
WEB_VERSION = "0.101.49"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.194"
APP_VER = "0.0.195"
# https://github.com/amidaware/rmmagent
LATEST_AGENT_VER = "2.8.0"
MESH_VER = "1.1.21"
MESH_VER = "1.1.32"
NATS_SERVER_VER = "2.10.17"
NATS_SERVER_VER = "2.10.22"
# Install Nushell on the agent
# https://github.com/nushell/nushell
@ -81,10 +81,10 @@ INSTALL_DENO_URL = ""
DENO_DEFAULT_PERMISSIONS = "--allow-all"
# for the update script, bump when need to recreate venv
PIP_VER = "44"
PIP_VER = "45"
SETUPTOOLS_VER = "70.2.0"
WHEEL_VER = "0.43.0"
SETUPTOOLS_VER = "75.1.0"
WHEEL_VER = "0.44.0"
AGENT_BASE_URL = "https://agents.tacticalrmm.com"

View File

@ -14,7 +14,7 @@ RUN MESH_VER=$(grep -o 'MESH_VER.*' /tmp/settings.py | cut -d'"' -f 2) && \
cat > package.json <<EOF
{
"dependencies": {
"archiver": "5.3.1",
"archiver": "7.0.1",
"meshcentral": "$MESH_VER",
"mongodb": "4.13.0",
"otplib": "10.2.3",

View File

@ -25,7 +25,8 @@ if [ ! -f "/home/node/app/meshcentral-data/config.json" ] || [[ "${MESH_PERSISTE
encoded_uri=$(node -p "encodeURI('mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}')")
mesh_config="$(cat << EOF
mesh_config="$(
cat <<EOF
{
"settings": {
"mongodb": "${encoded_uri}",
@ -39,8 +40,7 @@ if [ ! -f "/home/node/app/meshcentral-data/config.json" ] || [[ "${MESH_PERSISTE
"aliasPort": 443,
"allowLoginToken": true,
"allowFraming": true,
"_agentPing": 60,
"agentPong": 300,
"agentPing": 35,
"allowHighQualityDesktop": true,
"agentCoreDump": false,
"compression": ${MESH_COMPRESSION_ENABLED},
@ -74,9 +74,9 @@ if [ ! -f "/home/node/app/meshcentral-data/config.json" ] || [[ "${MESH_PERSISTE
}
}
EOF
)"
)"
echo "${mesh_config}" > /home/node/app/meshcentral-data/config.json
echo "${mesh_config}" >/home/node/app/meshcentral-data/config.json
fi
node node_modules/meshcentral --createaccount ${MESH_USER} --pass ${MESH_PASS} --email example@example.com
@ -86,14 +86,14 @@ if [ ! -f "${TACTICAL_DIR}/tmp/mesh_token" ]; then
mesh_token=$(node node_modules/meshcentral --logintokenkey)
if [[ ${#mesh_token} -eq 160 ]]; then
echo ${mesh_token} > /opt/tactical/tmp/mesh_token
echo ${mesh_token} >/opt/tactical/tmp/mesh_token
else
echo "Failed to generate mesh token. Fix the error and restart the mesh container"
fi
fi
# wait for nginx container
until (echo > /dev/tcp/"${NGINX_HOST_IP}"/${NGINX_HOST_PORT}) &> /dev/null; do
until (echo >/dev/tcp/"${NGINX_HOST_IP}"/${NGINX_HOST_PORT}) &>/dev/null; do
echo "waiting for nginx to start..."
sleep 5
done

View File

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

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
SCRIPT_VERSION="85"
SCRIPT_VERSION="86"
SCRIPT_URL="https://raw.githubusercontent.com/amidaware/tacticalrmm/master/install.sh"
sudo apt install -y curl wget dirmngr gnupg lsb-release ca-certificates
@ -420,7 +420,7 @@ mesh_pkg="$(
cat <<EOF
{
"dependencies": {
"archiver": "5.3.1",
"archiver": "7.0.1",
"meshcentral": "${MESH_VER}",
"otplib": "10.2.3",
"pg": "8.7.1",

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
SCRIPT_VERSION="59"
SCRIPT_VERSION="60"
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/restore.sh'
sudo apt update
@ -413,7 +413,7 @@ mesh_pkg="$(
cat <<EOF
{
"dependencies": {
"archiver": "5.3.1",
"archiver": "7.0.1",
"meshcentral": "${MESH_VER}",
"otplib": "10.2.3",
"pg": "8.7.1",
@ -494,7 +494,6 @@ echo "Running management commands...please wait..."
API=$(python manage.py get_config api)
WEB_VERSION=$(python manage.py get_config webversion)
FRONTEND=$(python manage.py get_config webdomain)
webdomain=$(python manage.py get_config webdomain)
meshdomain=$(python manage.py get_config meshdomain)
WEBTAR_URL=$(python manage.py get_webtar_url)
CERT_PUB_KEY=$(python manage.py get_config certfile)
@ -620,9 +619,9 @@ sudo ln -s /etc/nginx/sites-available/rmm.conf /etc/nginx/sites-enabled/rmm.conf
HAS_11=$(grep 127.0.1.1 /etc/hosts)
if [[ $HAS_11 ]]; then
sudo sed -i "/127.0.1.1/s/$/ ${API} ${webdomain} ${meshdomain}/" /etc/hosts
sudo sed -i "/127.0.1.1/s/$/ ${API} ${FRONTEND} ${meshdomain}/" /etc/hosts
else
echo "127.0.1.1 ${API} ${webdomain} ${meshdomain}" | sudo tee --append /etc/hosts >/dev/null
echo "127.0.1.1 ${API} ${FRONTEND} ${meshdomain}" | sudo tee --append /etc/hosts >/dev/null
fi
sudo systemctl enable nats.service

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
SCRIPT_VERSION="153"
SCRIPT_VERSION="154"
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/update.sh'
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
YELLOW='\033[1;33m'
@ -319,7 +319,7 @@ if [[ "${CURRENT_MESH_VER}" != "${LATEST_MESH_VER}" ]] || [[ "$force" = true ]];
cat <<EOF
{
"dependencies": {
"archiver": "5.3.1",
"archiver": "7.0.1",
"meshcentral": "${LATEST_MESH_VER}",
"otplib": "10.2.3",
"pg": "8.7.1",