diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example index 6ded98ee2f..b92a9e98fc 100644 --- a/.devcontainer/.env.example +++ b/.devcontainer/.env.example @@ -26,6 +26,6 @@ POSTGRES_PASS=postgrespass APP_PORT=80 API_PORT=80 HTTP_PROTOCOL=https -DOCKER_NETWORK="172.21.0.0/24" -DOCKER_NGINX_IP="172.21.0.20" -NATS_PORTS="4222:4222" +DOCKER_NETWORK=172.21.0.0/24 +DOCKER_NGINX_IP=172.21.0.20 +NATS_PORTS=4222:4222 diff --git a/.devcontainer/api.dockerfile b/.devcontainer/api.dockerfile index 8f77e09add..04f2b5b054 100644 --- a/.devcontainer/api.dockerfile +++ b/.devcontainer/api.dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.2-slim +FROM python:3.9.6-slim ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt index a70917933b..131d71795d 100644 --- a/.devcontainer/requirements.txt +++ b/.devcontainer/requirements.txt @@ -3,6 +3,7 @@ asyncio-nats-client celery channels channels_redis +django-ipware Django django-cors-headers django-rest-knox diff --git a/README.md b/README.md index 0613a99ebb..2e1cd4e669 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Tactical RMM is a remote monitoring & management tool for Windows computers, bui It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) # [LIVE DEMO](https://rmm.tacticalrmm.io/) -Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app. +Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app. ### [Discord Chat](https://discord.gg/upGTkWp) @@ -35,4 +35,4 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso ## Installation / Backup / Restore / Usage -### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/) \ No newline at end of file +### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/) diff --git a/api/tacticalrmm/accounts/migrations/0024_user_last_login_ip.py b/api/tacticalrmm/accounts/migrations/0024_user_last_login_ip.py new file mode 100644 index 0000000000..554527e695 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0024_user_last_login_ip.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.1 on 2021-07-20 20:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0023_user_is_installer_user'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='last_login_ip', + field=models.GenericIPAddressField(blank=True, default=None, null=True), + ), + ] diff --git a/api/tacticalrmm/accounts/migrations/0025_auto_20210721_0424.py b/api/tacticalrmm/accounts/migrations/0025_auto_20210721_0424.py new file mode 100644 index 0000000000..9c3cf88e75 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0025_auto_20210721_0424.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.1 on 2021-07-21 04:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0024_user_last_login_ip'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='created_by', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='role', + name='created_time', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='role', + name='modified_by', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='role', + name='modified_time', + field=models.DateTimeField(auto_now=True, null=True), + ), + ] diff --git a/api/tacticalrmm/accounts/models.py b/api/tacticalrmm/accounts/models.py index 9f5aaedb4b..74287951af 100644 --- a/api/tacticalrmm/accounts/models.py +++ b/api/tacticalrmm/accounts/models.py @@ -48,6 +48,7 @@ class User(AbstractUser, BaseAuditModel): loading_bar_color = models.CharField(max_length=255, default="red") clear_search_when_switching = models.BooleanField(default=True) is_installer_user = models.BooleanField(default=False) + last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True) agent = models.OneToOneField( "agents.Agent", @@ -73,7 +74,7 @@ def serialize(user): return UserSerializer(user).data -class Role(models.Model): +class Role(BaseAuditModel): name = models.CharField(max_length=255, unique=True) is_superuser = models.BooleanField(default=False) @@ -140,6 +141,13 @@ class Role(models.Model): def __str__(self): return self.name + @staticmethod + def serialize(role): + # serializes the agent and returns json + from .serializers import RoleAuditSerializer + + return RoleAuditSerializer(role).data + @staticmethod def perms(): return [ diff --git a/api/tacticalrmm/accounts/serializers.py b/api/tacticalrmm/accounts/serializers.py index d6783de445..6847720856 100644 --- a/api/tacticalrmm/accounts/serializers.py +++ b/api/tacticalrmm/accounts/serializers.py @@ -31,6 +31,7 @@ class Meta: "email", "is_active", "last_login", + "last_login_ip", "role", ] @@ -57,3 +58,9 @@ class RoleSerializer(ModelSerializer): class Meta: model = Role fields = "__all__" + + +class RoleAuditSerializer(ModelSerializer): + class Meta: + model = Role + fields = "__all__" diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index b01f0929bf..a79017b896 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -3,23 +3,23 @@ from django.contrib.auth import login from django.db import IntegrityError from django.shortcuts import get_object_or_404 +from ipware import get_client_ip from knox.views import LoginView as KnoxLoginView +from logs.models import AuditLog from rest_framework import status from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView - -from logs.models import AuditLog from tacticalrmm.utils import notify_error -from .models import User, Role +from .models import Role, User from .permissions import AccountsPerms, RolesPerms from .serializers import ( + RoleSerializer, TOTPSetupSerializer, UserSerializer, UserUISerializer, - RoleSerializer, ) @@ -40,7 +40,9 @@ def post(self, request, format=None): # check credentials serializer = AuthTokenSerializer(data=request.data) if not serializer.is_valid(): - AuditLog.audit_user_failed_login(request.data["username"]) + AuditLog.audit_user_failed_login( + request.data["username"], debug_info={"ip": request._client_ip} + ) return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) user = serializer.validated_data["user"] @@ -76,10 +78,20 @@ def post(self, request, format=None): if valid: login(request, user) - AuditLog.audit_user_login_successful(request.data["username"]) + + # save ip information + client_ip, is_routable = get_client_ip(request) + user.last_login_ip = client_ip + user.save() + + AuditLog.audit_user_login_successful( + request.data["username"], debug_info={"ip": request._client_ip} + ) return super(LoginView, self).post(request, format=None) else: - AuditLog.audit_user_failed_twofactor(request.data["username"]) + AuditLog.audit_user_failed_twofactor( + request.data["username"], debug_info={"ip": request._client_ip} + ) return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) @@ -87,7 +99,14 @@ class GetAddUsers(APIView): permission_classes = [IsAuthenticated, AccountsPerms] def get(self, request): - users = User.objects.filter(agent=None, is_installer_user=False) + search = request.GET.get("search", None) + + if search: + users = User.objects.filter(agent=None, is_installer_user=False).filter( + username__icontains=search + ) + else: + users = User.objects.filter(agent=None, is_installer_user=False) return Response(UserSerializer(users, many=True).data) diff --git a/api/tacticalrmm/agents/admin.py b/api/tacticalrmm/agents/admin.py index b177b47a98..50a920c49a 100644 --- a/api/tacticalrmm/agents/admin.py +++ b/api/tacticalrmm/agents/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin -from .models import Agent, AgentCustomField, Note, RecoveryAction +from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory admin.site.register(Agent) admin.site.register(RecoveryAction) admin.site.register(Note) admin.site.register(AgentCustomField) +admin.site.register(AgentHistory) diff --git a/api/tacticalrmm/agents/migrations/0038_agenthistory.py b/api/tacticalrmm/agents/migrations/0038_agenthistory.py new file mode 100644 index 0000000000..d9c8fba6ee --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0038_agenthistory.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.1 on 2021-07-06 02:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0037_auto_20210627_0014'), + ] + + operations = [ + migrations.CreateModel( + name='AgentHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now_add=True)), + ('type', models.CharField(choices=[('task_run', 'Task Run'), ('script_run', 'Script Run'), ('cmd_run', 'CMD Run')], default='cmd_run', max_length=50)), + ('command', models.TextField(blank=True, null=True)), + ('status', models.CharField(choices=[('success', 'Success'), ('failure', 'Failure')], default='success', max_length=50)), + ('username', models.CharField(default='system', max_length=50)), + ('results', models.TextField(blank=True, null=True)), + ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='agents.agent')), + ], + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py b/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py new file mode 100644 index 0000000000..f5ad3b5d89 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2021-07-14 07:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0008_script_guid'), + ('agents', '0038_agenthistory'), + ] + + operations = [ + migrations.AddField( + model_name='agenthistory', + name='script', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='history', to='scripts.script'), + ), + migrations.AddField( + model_name='agenthistory', + name='script_results', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index dcb2819f17..450372dfdc 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -16,14 +16,12 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils import timezone as djangotime -from loguru import logger from nats.aio.client import Client as NATS from nats.aio.errors import ErrTimeout +from packaging import version as pyver from core.models import TZ_CHOICES, CoreSettings -from logs.models import BaseAuditModel - -logger.configure(**settings.LOG_CONFIG) +from logs.models import BaseAuditModel, DebugLog class Agent(BaseAuditModel): @@ -91,8 +89,8 @@ class Agent(BaseAuditModel): def save(self, *args, **kwargs): # get old agent if exists - old_agent = type(self).objects.get(pk=self.pk) if self.pk else None - super(BaseAuditModel, self).save(*args, **kwargs) + old_agent = Agent.objects.get(pk=self.pk) if self.pk else None + super(Agent, self).save(old_model=old_agent, *args, **kwargs) # check if new agent has been created # or check if policy have changed on agent @@ -123,7 +121,7 @@ def timezone(self): else: from core.models import CoreSettings - return CoreSettings.objects.first().default_time_zone + return CoreSettings.objects.first().default_time_zone # type: ignore @property def arch(self): @@ -325,6 +323,7 @@ def run_script( full: bool = False, wait: bool = False, run_on_any: bool = False, + history_pk: int = 0, ) -> Any: from scripts.models import Script @@ -343,6 +342,9 @@ def run_script( }, } + if history_pk != 0 and pyver.parse(self.version) >= pyver.parse("1.6.0"): + data["id"] = history_pk + running_agent = self if run_on_any: nats_ping = {"func": "ping"} @@ -411,6 +413,12 @@ def approve_updates(self): update.action = "approve" update.save(update_fields=["action"]) + DebugLog.info( + agent=self, + log_type="windows_updates", + message=f"Approving windows updates on {self.hostname}", + ) + # returns agent policy merged with a client or site specific policy def get_patch_policy(self): @@ -445,8 +453,8 @@ def get_patch_policy(self): # if patch policy still doesn't exist check default policy elif ( - core_settings.server_policy - and core_settings.server_policy.winupdatepolicy.exists() + core_settings.server_policy # type: ignore + and core_settings.server_policy.winupdatepolicy.exists() # type: ignore ): # make sure agent site and client are not blocking inheritance if ( @@ -454,7 +462,7 @@ def get_patch_policy(self): and not site.block_policy_inheritance and not site.client.block_policy_inheritance ): - patch_policy = core_settings.server_policy.winupdatepolicy.get() + patch_policy = core_settings.server_policy.winupdatepolicy.get() # type: ignore elif self.monitoring_type == "workstation": # check agent policy first which should override client or site policy @@ -483,8 +491,8 @@ def get_patch_policy(self): # if patch policy still doesn't exist check default policy elif ( - core_settings.workstation_policy - and core_settings.workstation_policy.winupdatepolicy.exists() + core_settings.workstation_policy # type: ignore + and core_settings.workstation_policy.winupdatepolicy.exists() # type: ignore ): # make sure agent site and client are not blocking inheritance if ( @@ -493,7 +501,7 @@ def get_patch_policy(self): and not site.client.block_policy_inheritance ): patch_policy = ( - core_settings.workstation_policy.winupdatepolicy.get() + core_settings.workstation_policy.winupdatepolicy.get() # type: ignore ) # if policy still doesn't exist return the agent patch policy @@ -608,35 +616,35 @@ def set_alert_template(self): # check if alert template is applied globally and return if ( - core.alert_template - and core.alert_template.is_active + core.alert_template # type: ignore + and core.alert_template.is_active # type: ignore and not self.block_policy_inheritance and not site.block_policy_inheritance and not client.block_policy_inheritance ): - templates.append(core.alert_template) + templates.append(core.alert_template) # type: ignore # if agent is a workstation, check if policy with alert template is assigned to the site, client, or core if ( self.monitoring_type == "server" - and core.server_policy - and core.server_policy.alert_template - and core.server_policy.alert_template.is_active + and core.server_policy # type: ignore + and core.server_policy.alert_template # type: ignore + and core.server_policy.alert_template.is_active # type: ignore and not self.block_policy_inheritance and not site.block_policy_inheritance and not client.block_policy_inheritance ): - templates.append(core.server_policy.alert_template) + templates.append(core.server_policy.alert_template) # type: ignore if ( self.monitoring_type == "workstation" - and core.workstation_policy - and core.workstation_policy.alert_template - and core.workstation_policy.alert_template.is_active + and core.workstation_policy # type: ignore + and core.workstation_policy.alert_template # type: ignore + and core.workstation_policy.alert_template.is_active # type: ignore and not self.block_policy_inheritance and not site.block_policy_inheritance and not client.block_policy_inheritance ): - templates.append(core.workstation_policy.alert_template) + templates.append(core.workstation_policy.alert_template) # type: ignore # go through the templates and return the first one that isn't excluded for template in templates: @@ -739,7 +747,7 @@ async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True): try: ret = msgpack.loads(msg.data) # type: ignore except Exception as e: - logger.error(e) + DebugLog.error(agent=self, log_type="agent_issues", message=e) ret = str(e) await nc.close() @@ -752,12 +760,9 @@ async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True): @staticmethod def serialize(agent): # serializes the agent and returns json - from .serializers import AgentEditSerializer + from .serializers import AgentAuditSerializer - ret = AgentEditSerializer(agent).data - del ret["all_timezones"] - del ret["client"] - return ret + return AgentAuditSerializer(agent).data def delete_superseded_updates(self): try: @@ -772,7 +777,7 @@ def delete_superseded_updates(self): # skip if no version info is available therefore nothing to parse try: vers = [ - re.search(r"\(Version(.*?)\)", i).group(1).strip() + re.search(r"\(Version(.*?)\)", i).group(1).strip() # type: ignore for i in titles ] sorted_vers = sorted(vers, key=LooseVersion) @@ -807,7 +812,7 @@ def send_outage_email(self): from core.models import CoreSettings CORE = CoreSettings.objects.first() - CORE.send_mail( + CORE.send_mail( # type: ignore f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue", ( f"Data has not been received from client {self.client.name}, " @@ -822,7 +827,7 @@ def send_recovery_email(self): from core.models import CoreSettings CORE = CoreSettings.objects.first() - CORE.send_mail( + CORE.send_mail( # type: ignore f"{self.client.name}, {self.site.name}, {self.hostname} - data received", ( f"Data has been received from client {self.client.name}, " @@ -837,7 +842,7 @@ def send_outage_sms(self): from core.models import CoreSettings CORE = CoreSettings.objects.first() - CORE.send_sms( + CORE.send_sms( # type: ignore f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue", alert_template=self.alert_template, ) @@ -846,7 +851,7 @@ def send_recovery_sms(self): from core.models import CoreSettings CORE = CoreSettings.objects.first() - CORE.send_sms( + CORE.send_sms( # type: ignore f"{self.client.name}, {self.site.name}, {self.hostname} - data received", alert_template=self.alert_template, ) @@ -928,3 +933,57 @@ def value(self): return self.bool_value else: return self.string_value + + def save_to_field(self, value): + if self.field.type in [ + "text", + "number", + "single", + "datetime", + ]: + self.string_value = value + self.save() + elif self.field.type == "multiple": + self.multiple_value = value.split(",") + self.save() + elif self.field.type == "checkbox": + self.bool_value = bool(value) + self.save() + + +AGENT_HISTORY_TYPES = ( + ("task_run", "Task Run"), + ("script_run", "Script Run"), + ("cmd_run", "CMD Run"), +) + +AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure")) + + +class AgentHistory(models.Model): + agent = models.ForeignKey( + Agent, + related_name="history", + on_delete=models.CASCADE, + ) + time = models.DateTimeField(auto_now_add=True) + type = models.CharField( + max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run" + ) + command = models.TextField(null=True, blank=True) + status = models.CharField( + max_length=50, choices=AGENT_HISTORY_STATUS, default="success" + ) + username = models.CharField(max_length=50, default="system") + results = models.TextField(null=True, blank=True) + script = models.ForeignKey( + "scripts.Script", + null=True, + blank=True, + related_name="history", + on_delete=models.SET_NULL, + ) + script_results = models.JSONField(null=True, blank=True) + + def __str__(self): + return f"{self.agent.hostname} - {self.type}" diff --git a/api/tacticalrmm/agents/serializers.py b/api/tacticalrmm/agents/serializers.py index d108112b56..fece2274ba 100644 --- a/api/tacticalrmm/agents/serializers.py +++ b/api/tacticalrmm/agents/serializers.py @@ -1,10 +1,10 @@ import pytz -from rest_framework import serializers - from clients.serializers import ClientSerializer +from rest_framework import serializers +from tacticalrmm.utils import get_default_timezone from winupdate.serializers import WinUpdatePolicySerializer -from .models import Agent, AgentCustomField, Note +from .models import Agent, AgentCustomField, Note, AgentHistory class AgentSerializer(serializers.ModelSerializer): @@ -159,6 +159,7 @@ class Meta: "offline_time", "overdue_text_alert", "overdue_email_alert", + "overdue_dashboard_alert", "all_timezones", "winupdatepolicy", "policy", @@ -200,3 +201,22 @@ class NotesSerializer(serializers.ModelSerializer): class Meta: model = Agent fields = ["hostname", "pk", "notes"] + + +class AgentHistorySerializer(serializers.ModelSerializer): + time = serializers.SerializerMethodField(read_only=True) + script_name = serializers.ReadOnlyField(source="script.name") + + class Meta: + model = AgentHistory + fields = "__all__" + + def get_time(self, history): + timezone = get_default_timezone() + return history.time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S") + + +class AgentAuditSerializer(serializers.ModelSerializer): + class Meta: + model = Agent + exclude = ["disks", "services", "wmi_detail"] diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index 0969f7c267..3d7aa7d5d8 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -1,26 +1,21 @@ import asyncio import datetime as dt import random -import tempfile -import json -import subprocess import urllib.parse from time import sleep from typing import Union +from alerts.models import Alert +from core.models import CodeSignToken, CoreSettings from django.conf import settings from django.utils import timezone as djangotime -from loguru import logger +from logs.models import DebugLog, PendingAction from packaging import version as pyver - -from agents.models import Agent -from core.models import CodeSignToken, CoreSettings -from logs.models import PendingAction from scripts.models import Script from tacticalrmm.celery import app from tacticalrmm.utils import run_nats_api_cmd -logger.configure(**settings.LOG_CONFIG) +from agents.models import Agent def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str: @@ -33,8 +28,10 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str # skip if we can't determine the arch if agent.arch is None: - logger.warning( - f"Unable to determine arch on {agent.hostname}. Skipping agent update." + DebugLog.warning( + agent=agent, + log_type="agent_issues", + message=f"Unable to determine arch on {agent.hostname}({agent.pk}). Skipping agent update.", ) return "noarch" @@ -81,7 +78,7 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str @app.task def force_code_sign(pks: list[int]) -> None: try: - token = CodeSignToken.objects.first().token + token = CodeSignToken.objects.first().tokenv # type:ignore except: return @@ -96,7 +93,7 @@ def force_code_sign(pks: list[int]) -> None: @app.task def send_agent_update_task(pks: list[int]) -> None: try: - codesigntoken = CodeSignToken.objects.first().token + codesigntoken = CodeSignToken.objects.first().token # type:ignore except: codesigntoken = None @@ -111,11 +108,11 @@ def send_agent_update_task(pks: list[int]) -> None: @app.task def auto_self_agent_update_task() -> None: core = CoreSettings.objects.first() - if not core.agent_auto_update: + if not core.agent_auto_update: # type:ignore return try: - codesigntoken = CodeSignToken.objects.first().token + codesigntoken = CodeSignToken.objects.first().token # type:ignore except: codesigntoken = None @@ -235,14 +232,24 @@ def run_script_email_results_task( nats_timeout: int, emails: list[str], args: list[str] = [], + history_pk: int = 0, ): agent = Agent.objects.get(pk=agentpk) script = Script.objects.get(pk=scriptpk) r = agent.run_script( - scriptpk=script.pk, args=args, full=True, timeout=nats_timeout, wait=True + scriptpk=script.pk, + args=args, + full=True, + timeout=nats_timeout, + wait=True, + history_pk=history_pk, ) if r == "timeout": - logger.error(f"{agent.hostname} timed out running script.") + DebugLog.error( + agent=agent, + log_type="scripting", + message=f"{agent.hostname}({agent.pk}) timed out running script.", + ) return CORE = CoreSettings.objects.first() @@ -258,28 +265,32 @@ def run_script_email_results_task( msg = EmailMessage() msg["Subject"] = subject - msg["From"] = CORE.smtp_from_email + msg["From"] = CORE.smtp_from_email # type:ignore if emails: msg["To"] = ", ".join(emails) else: - msg["To"] = ", ".join(CORE.email_alert_recipients) + msg["To"] = ", ".join(CORE.email_alert_recipients) # type:ignore msg.set_content(body) try: - with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server: - if CORE.smtp_requires_auth: + with smtplib.SMTP( + CORE.smtp_host, CORE.smtp_port, timeout=20 # type:ignore + ) as server: # type:ignore + if CORE.smtp_requires_auth: # type:ignore server.ehlo() server.starttls() - server.login(CORE.smtp_host_user, CORE.smtp_host_password) + server.login( + CORE.smtp_host_user, CORE.smtp_host_password # type:ignore + ) # type:ignore server.send_message(msg) server.quit() else: server.send_message(msg) server.quit() except Exception as e: - logger.error(e) + DebugLog.error(message=e) @app.task @@ -310,15 +321,6 @@ def clear_faults_task(older_than_days: int) -> None: ) -@app.task -def monitor_agents_task() -> None: - agents = Agent.objects.only( - "pk", "agent_id", "last_seen", "overdue_time", "offline_time" - ) - ids = [i.agent_id for i in agents if i.status != "online"] - run_nats_api_cmd("monitor", ids) - - @app.task def get_wmi_task() -> None: agents = Agent.objects.only( @@ -330,18 +332,62 @@ def get_wmi_task() -> None: @app.task def agent_checkin_task() -> None: - db = settings.DATABASES["default"] - config = { - "key": settings.SECRET_KEY, - "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", - "user": db["USER"], - "pass": db["PASSWORD"], - "host": db["HOST"], - "port": int(db["PORT"]), - "dbname": db["NAME"], - } - with tempfile.NamedTemporaryFile() as fp: - with open(fp.name, "w") as f: - json.dump(config, f) - cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "checkin"] - subprocess.run(cmd, timeout=30) + run_nats_api_cmd("checkin", timeout=30) + + +@app.task +def agent_getinfo_task() -> None: + run_nats_api_cmd("agentinfo", timeout=30) + + +@app.task +def prune_agent_history(older_than_days: int) -> str: + from .models import AgentHistory + + AgentHistory.objects.filter( + time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) + ).delete() + + return "ok" + + +@app.task +def handle_agents_task() -> None: + q = Agent.objects.prefetch_related("pendingactions", "autotasks").only( + "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time" + ) + agents = [ + i + for i in q + if pyver.parse(i.version) >= pyver.parse("1.6.0") and i.status == "online" + ] + for agent in agents: + # change agent update pending status to completed if agent has just updated + if ( + pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER) + and agent.pendingactions.filter( + action_type="agentupdate", status="pending" + ).exists() + ): + agent.pendingactions.filter( + action_type="agentupdate", status="pending" + ).update(status="completed") + + # sync scheduled tasks + if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore + tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore + + for task in tasks: + if task.sync_status == "pendingdeletion": + task.delete_task_on_agent() + elif task.sync_status == "initial": + task.modify_task_on_agent() + elif task.sync_status == "notsynced": + task.create_task_on_agent() + + # handles any alerting actions + if Alert.objects.filter(agent=agent, resolved=False).exists(): + try: + Alert.handle_alert_resolve(agent) + except: + continue diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index a6b5e741d4..c76c08b7a3 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -1,19 +1,18 @@ import json import os -from itertools import cycle +from django.utils import timezone as djangotime from unittest.mock import patch from django.conf import settings +from logs.models import PendingAction from model_bakery import baker from packaging import version as pyver - -from logs.models import PendingAction from tacticalrmm.test import TacticalTestCase from winupdate.models import WinUpdatePolicy from winupdate.serializers import WinUpdatePolicySerializer -from .models import Agent, AgentCustomField -from .serializers import AgentSerializer +from .models import Agent, AgentCustomField, AgentHistory +from .serializers import AgentHistorySerializer, AgentSerializer from .tasks import auto_self_agent_update_task @@ -306,7 +305,7 @@ def test_send_raw_cmd(self, mock_ret): "shell": "cmd", "timeout": 30, } - mock_ret.return_value = "nt authority\system" + mock_ret.return_value = "nt authority\\system" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertIsInstance(r.data, str) # type: ignore @@ -437,7 +436,7 @@ def test_recover(self, nats_cmd): self.assertEqual(r.status_code, 200) self.assertEqual(RecoveryAction.objects.count(), 1) mesh_recovery = RecoveryAction.objects.first() - self.assertEqual(mesh_recovery.mode, "mesh") + self.assertEqual(mesh_recovery.mode, "mesh") # type: ignore nats_cmd.reset_mock() RecoveryAction.objects.all().delete() @@ -472,8 +471,8 @@ def test_recover(self, nats_cmd): self.assertEqual(r.status_code, 200) self.assertEqual(RecoveryAction.objects.count(), 1) cmd_recovery = RecoveryAction.objects.first() - self.assertEqual(cmd_recovery.mode, "command") - self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f") + self.assertEqual(cmd_recovery.mode, "command") # type: ignore + self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f") # type: ignore def test_agents_agent_detail(self): url = f"/agents/{self.agent.pk}/agentdetail/" @@ -770,6 +769,9 @@ def test_recover_mesh(self, nats_cmd): @patch("agents.tasks.run_script_email_results_task.delay") @patch("agents.models.Agent.run_script") def test_run_script(self, run_script, email_task): + from .models import AgentCustomField, Note + from clients.models import ClientCustomField, SiteCustomField + run_script.return_value = "ok" url = "/agents/runscript/" script = baker.make_recipe("scripts.script") @@ -777,7 +779,7 @@ def test_run_script(self, run_script, email_task): # test wait data = { "pk": self.agent.pk, - "scriptPK": script.pk, + "script": script.pk, "output": "wait", "args": [], "timeout": 15, @@ -786,18 +788,18 @@ def test_run_script(self, run_script, email_task): r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) run_script.assert_called_with( - scriptpk=script.pk, args=[], timeout=18, wait=True + scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=0 ) run_script.reset_mock() # test email default data = { "pk": self.agent.pk, - "scriptPK": script.pk, + "script": script.pk, "output": "email", "args": ["abc", "123"], "timeout": 15, - "emailmode": "default", + "emailMode": "default", "emails": ["admin@example.com", "bob@example.com"], } r = self.client.post(url, data, format="json") @@ -812,7 +814,7 @@ def test_run_script(self, run_script, email_task): email_task.reset_mock() # test email overrides - data["emailmode"] = "custom" + data["emailMode"] = "custom" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) email_task.assert_called_with( @@ -826,7 +828,7 @@ def test_run_script(self, run_script, email_task): # test fire and forget data = { "pk": self.agent.pk, - "scriptPK": script.pk, + "script": script.pk, "output": "forget", "args": ["hello", "world"], "timeout": 22, @@ -835,8 +837,138 @@ def test_run_script(self, run_script, email_task): r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) run_script.assert_called_with( - scriptpk=script.pk, args=["hello", "world"], timeout=25 + scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=0 + ) + run_script.reset_mock() + + # test collector + + # save to agent custom field + custom_field = baker.make("core.CustomField", model="agent") + data = { + "pk": self.agent.pk, + "script": script.pk, + "output": "collector", + "args": ["hello", "world"], + "timeout": 22, + "custom_field": custom_field.id, # type: ignore + "save_all_output": True, + } + + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + run_script.assert_called_with( + scriptpk=script.pk, + args=["hello", "world"], + timeout=25, + wait=True, + history_pk=0, + ) + run_script.reset_mock() + + self.assertEqual( + AgentCustomField.objects.get(agent=self.agent.pk, field=custom_field).value, + "ok", + ) + + # save to site custom field + custom_field = baker.make("core.CustomField", model="site") + data = { + "pk": self.agent.pk, + "script": script.pk, + "output": "collector", + "args": ["hello", "world"], + "timeout": 22, + "custom_field": custom_field.id, # type: ignore + "save_all_output": False, + } + + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + run_script.assert_called_with( + scriptpk=script.pk, + args=["hello", "world"], + timeout=25, + wait=True, + history_pk=0, + ) + run_script.reset_mock() + + self.assertEqual( + SiteCustomField.objects.get( + site=self.agent.site.pk, field=custom_field + ).value, + "ok", + ) + + # save to client custom field + custom_field = baker.make("core.CustomField", model="client") + data = { + "pk": self.agent.pk, + "script": script.pk, + "output": "collector", + "args": ["hello", "world"], + "timeout": 22, + "custom_field": custom_field.id, # type: ignore + "save_all_output": False, + } + + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + run_script.assert_called_with( + scriptpk=script.pk, + args=["hello", "world"], + timeout=25, + wait=True, + history_pk=0, ) + run_script.reset_mock() + + self.assertEqual( + ClientCustomField.objects.get( + client=self.agent.client.pk, field=custom_field + ).value, + "ok", + ) + + # test save to note + data = { + "pk": self.agent.pk, + "script": script.pk, + "output": "note", + "args": ["hello", "world"], + "timeout": 22, + } + + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + run_script.assert_called_with( + scriptpk=script.pk, + args=["hello", "world"], + timeout=25, + wait=True, + history_pk=0, + ) + run_script.reset_mock() + + self.assertEqual(Note.objects.get(agent=self.agent).note, "ok") + + def test_get_agent_history(self): + + # setup data + agent = baker.make_recipe("agents.agent") + history = baker.make("agents.AgentHistory", agent=agent, _quantity=30) + url = f"/agents/history/{agent.id}/" + + # test agent not found + r = self.client.get("/agents/history/500/", format="json") + self.assertEqual(r.status_code, 404) + + # test pulling data + r = self.client.get(url, format="json") + data = AgentHistorySerializer(history, many=True).data + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data, data) # type:ignore class TestAgentViewsNew(TacticalTestCase): @@ -1048,3 +1180,25 @@ def test_auto_self_agent_update_task(self, mock_sleep, agent_update): r = auto_self_agent_update_task.s().apply() self.assertEqual(agent_update.call_count, 33) + + def test_agent_history_prune_task(self): + from .tasks import prune_agent_history + + # setup data + agent = baker.make_recipe("agents.agent") + history = baker.make( + "agents.AgentHistory", + agent=agent, + _quantity=50, + ) + + days = 0 + for item in history: # type: ignore + item.time = djangotime.now() - djangotime.timedelta(days=days) + item.save() + days = days + 5 + + # delete AgentHistory older than 30 days + prune_agent_history(30) + + self.assertEqual(AgentHistory.objects.filter(agent=agent).count(), 6) diff --git a/api/tacticalrmm/agents/urls.py b/api/tacticalrmm/agents/urls.py index c1d0fa44c1..fb497519aa 100644 --- a/api/tacticalrmm/agents/urls.py +++ b/api/tacticalrmm/agents/urls.py @@ -29,4 +29,5 @@ path("bulk/", views.bulk), path("maintenance/", views.agent_maintenance), path("/wmi/", views.WMI.as_view()), + path("history//", views.AgentHistoryView.as_view()), ] diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 6e48e5ab3f..5bba8c5dee 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -8,7 +8,6 @@ from django.conf import settings from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from loguru import logger from packaging import version as pyver from rest_framework import status from rest_framework.decorators import api_view, permission_classes @@ -17,14 +16,14 @@ from rest_framework.views import APIView from core.models import CoreSettings -from logs.models import AuditLog, PendingAction +from logs.models import AuditLog, DebugLog, PendingAction from scripts.models import Script from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats from winupdate.serializers import WinUpdatePolicySerializer from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task -from .models import Agent, AgentCustomField, Note, RecoveryAction +from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory from .permissions import ( EditAgentPerms, EvtLogPerms, @@ -42,6 +41,7 @@ from .serializers import ( AgentCustomFieldSerializer, AgentEditSerializer, + AgentHistorySerializer, AgentHostnameSerializer, AgentOverdueActionSerializer, AgentSerializer, @@ -51,8 +51,6 @@ ) from .tasks import run_script_email_results_task, send_agent_update_task -logger.configure(**settings.LOG_CONFIG) - @api_view() def get_agent_versions(request): @@ -115,7 +113,7 @@ def uninstall(request): def edit_agent(request): agent = get_object_or_404(Agent, pk=request.data["id"]) - a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) + a_serializer = AgentEditSerializer(instance=agent, data=request.data, partial=True) a_serializer.is_valid(raise_exception=True) a_serializer.save() @@ -160,17 +158,21 @@ def meshcentral(request, pk): core = CoreSettings.objects.first() token = agent.get_login_token( - key=core.mesh_token, user=f"user//{core.mesh_username}" + key=core.mesh_token, user=f"user//{core.mesh_username}" # type:ignore ) if token == "err": return notify_error("Invalid mesh token") - control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" - terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" - file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" + control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore + terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore + file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore - AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname) + AuditLog.audit_mesh_session( + username=request.user.username, + agent=agent, + debug_info={"ip": request._client_ip}, + ) ret = { "hostname": agent.hostname, @@ -248,6 +250,16 @@ def send_raw_cmd(request): "shell": request.data["shell"], }, } + + if pyver.parse(agent.version) >= pyver.parse("1.6.0"): + hist = AgentHistory.objects.create( + agent=agent, + type="cmd_run", + command=request.data["cmd"], + username=request.user.username[:50], + ) + data["id"] = hist.pk + r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2)) if r == "timeout": @@ -255,9 +267,10 @@ def send_raw_cmd(request): AuditLog.audit_raw_command( username=request.user.username, - hostname=agent.hostname, + agent=agent, cmd=request.data["cmd"], shell=request.data["shell"], + debug_info={"ip": request._client_ip}, ) return Response(r) @@ -508,7 +521,7 @@ def install_agent(request): try: os.remove(ps1) except Exception as e: - logger.error(str(e)) + DebugLog.error(message=str(e)) with open(ps1, "w") as f: f.write(text) @@ -566,26 +579,41 @@ def recover(request): @permission_classes([IsAuthenticated, RunScriptPerms]) def run_script(request): agent = get_object_or_404(Agent, pk=request.data["pk"]) - script = get_object_or_404(Script, pk=request.data["scriptPK"]) + script = get_object_or_404(Script, pk=request.data["script"]) output = request.data["output"] args = request.data["args"] req_timeout = int(request.data["timeout"]) + 3 AuditLog.audit_script_run( username=request.user.username, - hostname=agent.hostname, + agent=agent, script=script.name, + debug_info={"ip": request._client_ip}, ) + history_pk = 0 + if pyver.parse(agent.version) >= pyver.parse("1.6.0"): + hist = AgentHistory.objects.create( + agent=agent, + type="script_run", + script=script, + username=request.user.username[:50], + ) + history_pk = hist.pk + if output == "wait": r = agent.run_script( - scriptpk=script.pk, args=args, timeout=req_timeout, wait=True + scriptpk=script.pk, + args=args, + timeout=req_timeout, + wait=True, + history_pk=history_pk, ) return Response(r) elif output == "email": emails = ( - [] if request.data["emailmode"] == "default" else request.data["emails"] + [] if request.data["emailMode"] == "default" else request.data["emails"] ) run_script_email_results_task.delay( agentpk=agent.pk, @@ -594,8 +622,47 @@ def run_script(request): emails=emails, args=args, ) + elif output == "collector": + from core.models import CustomField + + r = agent.run_script( + scriptpk=script.pk, + args=args, + timeout=req_timeout, + wait=True, + history_pk=history_pk, + ) + + custom_field = CustomField.objects.get(pk=request.data["custom_field"]) + + if custom_field.model == "agent": + field = custom_field.get_or_create_field_value(agent) + elif custom_field.model == "client": + field = custom_field.get_or_create_field_value(agent.client) + elif custom_field.model == "site": + field = custom_field.get_or_create_field_value(agent.site) + else: + return notify_error("Custom Field was invalid") + + value = r if request.data["save_all_output"] else r.split("\n")[-1].strip() + + field.save_to_field(value) + return Response(r) + elif output == "note": + r = agent.run_script( + scriptpk=script.pk, + args=args, + timeout=req_timeout, + wait=True, + history_pk=history_pk, + ) + + Note.objects.create(agent=agent, user=request.user, note=r) + return Response(r) else: - agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout) + agent.run_script( + scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk + ) return Response(f"{script.name} will now be run on {agent.hostname}") @@ -668,7 +735,7 @@ def delete(self, request, pk): @api_view(["POST"]) @permission_classes([IsAuthenticated, RunBulkPerms]) def bulk(request): - if request.data["target"] == "agents" and not request.data["agentPKs"]: + if request.data["target"] == "agents" and not request.data["agents"]: return notify_error("Must select at least 1 agent") if request.data["target"] == "client": @@ -676,7 +743,7 @@ def bulk(request): elif request.data["target"] == "site": q = Agent.objects.filter(site_id=request.data["site"]) elif request.data["target"] == "agents": - q = Agent.objects.filter(pk__in=request.data["agentPKs"]) + q = Agent.objects.filter(pk__in=request.data["agents"]) elif request.data["target"] == "all": q = Agent.objects.only("pk", "monitoring_type") else: @@ -689,29 +756,48 @@ def bulk(request): agents: list[int] = [agent.pk for agent in q] - AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data) + if not agents: + return notify_error("No agents where found meeting the selected criteria") + + AuditLog.audit_bulk_action( + request.user, + request.data["mode"], + request.data, + debug_info={"ip": request._client_ip}, + ) if request.data["mode"] == "command": handle_bulk_command_task.delay( - agents, request.data["cmd"], request.data["shell"], request.data["timeout"] + agents, + request.data["cmd"], + request.data["shell"], + request.data["timeout"], + request.user.username[:50], + run_on_offline=request.data["offlineAgents"], ) return Response(f"Command will now be run on {len(agents)} agents") elif request.data["mode"] == "script": - script = get_object_or_404(Script, pk=request.data["scriptPK"]) + script = get_object_or_404(Script, pk=request.data["script"]) handle_bulk_script_task.delay( - script.pk, agents, request.data["args"], request.data["timeout"] + script.pk, + agents, + request.data["args"], + request.data["timeout"], + request.user.username[:50], ) return Response(f"{script.name} will now be run on {len(agents)} agents") - elif request.data["mode"] == "install": - bulk_install_updates_task.delay(agents) - return Response( - f"Pending updates will now be installed on {len(agents)} agents" - ) - elif request.data["mode"] == "scan": - bulk_check_for_updates_task.delay(agents) - return Response(f"Patch status scan will now run on {len(agents)} agents") + elif request.data["mode"] == "patch": + + if request.data["patchMode"] == "install": + bulk_install_updates_task.delay(agents) + return Response( + f"Pending updates will now be installed on {len(agents)} agents" + ) + elif request.data["patchMode"] == "scan": + bulk_check_for_updates_task.delay(agents) + return Response(f"Patch status scan will now run on {len(agents)} agents") return notify_error("Something went wrong") @@ -746,3 +832,11 @@ def get(self, request, pk): if r != "ok": return notify_error("Unable to contact the agent") return Response("ok") + + +class AgentHistoryView(APIView): + def get(self, request, pk): + agent = get_object_or_404(Agent, pk=pk) + history = AgentHistory.objects.filter(agent=agent) + + return Response(AgentHistorySerializer(history, many=True).data) diff --git a/api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py b/api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py new file mode 100644 index 0000000000..3c571caae1 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.1 on 2021-07-21 04:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0006_auto_20210217_1736'), + ] + + operations = [ + migrations.AddField( + model_name='alerttemplate', + name='created_by', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='alerttemplate', + name='created_time', + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='alerttemplate', + name='modified_by', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='alerttemplate', + name='modified_time', + field=models.DateTimeField(auto_now=True, null=True), + ), + ] diff --git a/api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py b/api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py new file mode 100644 index 0000000000..fe4d1374a4 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.1 on 2021-07-21 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0007_auto_20210721_0423'), + ] + + operations = [ + migrations.AddField( + model_name='alerttemplate', + name='agent_script_actions', + field=models.BooleanField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='alerttemplate', + name='check_script_actions', + field=models.BooleanField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='alerttemplate', + name='task_script_actions', + field=models.BooleanField(blank=True, default=None, null=True), + ), + ] diff --git a/api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py b/api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py new file mode 100644 index 0000000000..6569d2cbd8 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.1 on 2021-07-21 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0008_auto_20210721_1757'), + ] + + operations = [ + migrations.AlterField( + model_name='alerttemplate', + name='agent_script_actions', + field=models.BooleanField(blank=True, default=True, null=True), + ), + migrations.AlterField( + model_name='alerttemplate', + name='check_script_actions', + field=models.BooleanField(blank=True, default=True, null=True), + ), + migrations.AlterField( + model_name='alerttemplate', + name='task_script_actions', + field=models.BooleanField(blank=True, default=True, null=True), + ), + ] diff --git a/api/tacticalrmm/alerts/models.py b/api/tacticalrmm/alerts/models.py index bec1d3b681..c1049bec19 100644 --- a/api/tacticalrmm/alerts/models.py +++ b/api/tacticalrmm/alerts/models.py @@ -3,19 +3,18 @@ import re from typing import TYPE_CHECKING, Union -from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.db import models from django.db.models.fields import BooleanField, PositiveIntegerField from django.utils import timezone as djangotime -from loguru import logger + +from logs.models import BaseAuditModel, DebugLog if TYPE_CHECKING: from agents.models import Agent from autotasks.models import AutomatedTask from checks.models import Check -logger.configure(**settings.LOG_CONFIG) SEVERITY_CHOICES = [ ("info", "Informational"), @@ -173,6 +172,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N always_email = alert_template.agent_always_email always_text = alert_template.agent_always_text alert_interval = alert_template.agent_periodic_alert_days + run_script_action = alert_template.agent_script_actions if instance.should_create_alert(alert_template): alert = cls.create_or_return_availability_alert(instance) @@ -209,6 +209,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N always_email = alert_template.check_always_email always_text = alert_template.check_always_text alert_interval = alert_template.check_periodic_alert_days + run_script_action = alert_template.check_script_actions if instance.should_create_alert(alert_template): alert = cls.create_or_return_check_alert(instance) @@ -242,6 +243,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N always_email = alert_template.task_always_email always_text = alert_template.task_always_text alert_interval = alert_template.task_periodic_alert_days + run_script_action = alert_template.task_script_actions if instance.should_create_alert(alert_template): alert = cls.create_or_return_task_alert(instance) @@ -295,7 +297,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N text_task.delay(pk=alert.pk, alert_interval=alert_interval) # check if any scripts should be run - if alert_template and alert_template.action and not alert.action_run: + if alert_template and alert_template.action and run_script_action and not alert.action_run: # type: ignore r = agent.run_script( scriptpk=alert_template.action.pk, args=alert.parse_script_args(alert_template.action_args), @@ -314,8 +316,10 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N alert.action_run = djangotime.now() alert.save() else: - logger.error( - f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert" + DebugLog.error( + agent=agent, + log_type="scripting", + message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert", ) @classmethod @@ -345,6 +349,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N if alert_template: email_on_resolved = alert_template.agent_email_on_resolved text_on_resolved = alert_template.agent_text_on_resolved + run_script_action = alert_template.agent_script_actions elif isinstance(instance, Check): from checks.tasks import ( @@ -363,6 +368,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N if alert_template: email_on_resolved = alert_template.check_email_on_resolved text_on_resolved = alert_template.check_text_on_resolved + run_script_action = alert_template.check_script_actions elif isinstance(instance, AutomatedTask): from autotasks.tasks import ( @@ -381,6 +387,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N if alert_template: email_on_resolved = alert_template.task_email_on_resolved text_on_resolved = alert_template.task_text_on_resolved + run_script_action = alert_template.task_script_actions else: return @@ -403,6 +410,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N if ( alert_template and alert_template.resolved_action + and run_script_action # type: ignore and not alert.resolved_action_run ): r = agent.run_script( @@ -425,8 +433,10 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N alert.resolved_action_run = djangotime.now() alert.save() else: - logger.error( - f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert" + DebugLog.error( + agent=agent, + log_type="scripting", + message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert", ) def parse_script_args(self, args: list[str]): @@ -451,7 +461,7 @@ def parse_script_args(self, args: list[str]): try: temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore except Exception as e: - logger.error(e) + DebugLog.error(log_type="scripting", message=e) continue else: @@ -460,7 +470,7 @@ def parse_script_args(self, args: list[str]): return temp_args -class AlertTemplate(models.Model): +class AlertTemplate(BaseAuditModel): name = models.CharField(max_length=100) is_active = models.BooleanField(default=True) @@ -517,6 +527,7 @@ class AlertTemplate(models.Model): agent_always_text = BooleanField(null=True, blank=True, default=None) agent_always_alert = BooleanField(null=True, blank=True, default=None) agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0) + agent_script_actions = BooleanField(null=True, blank=True, default=True) # check alert settings check_email_alert_severity = ArrayField( @@ -540,6 +551,7 @@ class AlertTemplate(models.Model): check_always_text = BooleanField(null=True, blank=True, default=None) check_always_alert = BooleanField(null=True, blank=True, default=None) check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0) + check_script_actions = BooleanField(null=True, blank=True, default=True) # task alert settings task_email_alert_severity = ArrayField( @@ -563,6 +575,7 @@ class AlertTemplate(models.Model): task_always_text = BooleanField(null=True, blank=True, default=None) task_always_alert = BooleanField(null=True, blank=True, default=None) task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0) + task_script_actions = BooleanField(null=True, blank=True, default=True) # exclusion settings exclude_workstations = BooleanField(null=True, blank=True, default=False) @@ -581,6 +594,13 @@ class AlertTemplate(models.Model): def __str__(self): return self.name + @staticmethod + def serialize(alert_template): + # serializes the agent and returns json + from .serializers import AlertTemplateAuditSerializer + + return AlertTemplateAuditSerializer(alert_template).data + @property def has_agent_settings(self) -> bool: return ( diff --git a/api/tacticalrmm/alerts/serializers.py b/api/tacticalrmm/alerts/serializers.py index 79d6f17832..24d9b12b0c 100644 --- a/api/tacticalrmm/alerts/serializers.py +++ b/api/tacticalrmm/alerts/serializers.py @@ -119,3 +119,9 @@ class AlertTemplateRelationSerializer(ModelSerializer): class Meta: model = AlertTemplate fields = "__all__" + + +class AlertTemplateAuditSerializer(ModelSerializer): + class Meta: + model = AlertTemplate + fields = "__all__" diff --git a/api/tacticalrmm/alerts/tasks.py b/api/tacticalrmm/alerts/tasks.py index 24bbad1415..42835102e9 100644 --- a/api/tacticalrmm/alerts/tasks.py +++ b/api/tacticalrmm/alerts/tasks.py @@ -1,11 +1,10 @@ from django.utils import timezone as djangotime - -from alerts.models import Alert from tacticalrmm.celery import app @app.task def unsnooze_alerts() -> str: + from .models import Alert Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update( snoozed=False, snooze_until=None @@ -22,3 +21,14 @@ def cache_agents_alert_template(): agent.set_alert_template() return "ok" + + +@app.task +def prune_resolved_alerts(older_than_days: int) -> str: + from .models import Alert + + Alert.objects.filter(resolved=True).filter( + alert_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) + ).delete() + + return "ok" diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py index b7be1ae5c4..3802fc3857 100644 --- a/api/tacticalrmm/alerts/tests.py +++ b/api/tacticalrmm/alerts/tests.py @@ -1,14 +1,13 @@ from datetime import datetime, timedelta from unittest.mock import patch +from core.models import CoreSettings from django.conf import settings from django.utils import timezone as djangotime from model_bakery import baker, seq +from tacticalrmm.test import TacticalTestCase from alerts.tasks import cache_agents_alert_template -from autotasks.models import AutomatedTask -from core.models import CoreSettings -from tacticalrmm.test import TacticalTestCase from .models import Alert, AlertTemplate from .serializers import ( @@ -330,8 +329,8 @@ def test_alert_template_related(self): baker.make("clients.Site", alert_template=alert_template, _quantity=3) baker.make("automation.Policy", alert_template=alert_template) core = CoreSettings.objects.first() - core.alert_template = alert_template - core.save() + core.alert_template = alert_template # type: ignore + core.save() # type: ignore url = f"/alerts/alerttemplates/{alert_template.pk}/related/" # type: ignore @@ -403,16 +402,16 @@ def test_agent_gets_correct_alert_template(self): # assign first Alert Template as to a policy and apply it as default policy.alert_template = alert_templates[0] # type: ignore policy.save() # type: ignore - core.workstation_policy = policy - core.server_policy = policy - core.save() + core.workstation_policy = policy # type: ignore + core.server_policy = policy # type: ignore + core.save() # type: ignore self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore # assign second Alert Template to as default alert template core.alert_template = alert_templates[1] # type: ignore - core.save() + core.save() # type: ignore self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk) # type: ignore @@ -514,6 +513,7 @@ def test_handle_agent_alerts( agent_recovery_email_task, agent_recovery_sms_task, ) + from alerts.models import Alert agent_dashboard_alert = baker.make_recipe("agents.overdue_agent") @@ -727,7 +727,6 @@ def test_handle_check_alerts( send_email, sleep, ): - from alerts.tasks import cache_agents_alert_template from checks.models import Check from checks.tasks import ( handle_check_email_alert_task, @@ -736,6 +735,8 @@ def test_handle_check_alerts( handle_resolved_check_sms_alert_task, ) + from alerts.tasks import cache_agents_alert_template + # create test data agent = baker.make_recipe("agents.agent") agent_no_settings = baker.make_recipe("agents.agent") @@ -1011,7 +1012,6 @@ def test_handle_task_alerts( send_email, sleep, ): - from alerts.tasks import cache_agents_alert_template from autotasks.models import AutomatedTask from autotasks.tasks import ( handle_resolved_task_email_alert, @@ -1020,6 +1020,8 @@ def test_handle_task_alerts( handle_task_sms_alert, ) + from alerts.tasks import cache_agents_alert_template + # create test data agent = baker.make_recipe("agents.agent") agent_no_settings = baker.make_recipe("agents.agent") @@ -1272,17 +1274,17 @@ def test_override_core_settings(self, smtp, sms): ) core = CoreSettings.objects.first() - core.smtp_host = "test.test.com" - core.smtp_port = 587 - core.smtp_recipients = ["recipient@test.com"] - core.twilio_account_sid = "test" - core.twilio_auth_token = "1234123412341234" - core.sms_alert_recipients = ["+1234567890"] + core.smtp_host = "test.test.com" # type: ignore + core.smtp_port = 587 # type: ignore + core.smtp_recipients = ["recipient@test.com"] # type: ignore + core.twilio_account_sid = "test" # type: ignore + core.twilio_auth_token = "1234123412341234" # type: ignore + core.sms_alert_recipients = ["+1234567890"] # type: ignore # test sending email with alert template settings - core.send_mail("Test", "Test", alert_template=alert_template) + core.send_mail("Test", "Test", alert_template=alert_template) # type: ignore - core.send_sms("Test", alert_template=alert_template) + core.send_sms("Test", alert_template=alert_template) # type: ignore @patch("agents.models.Agent.nats_cmd") @patch("agents.tasks.agent_outage_sms_task.delay") @@ -1315,6 +1317,7 @@ def test_alert_actions( "alerts.AlertTemplate", is_active=True, agent_always_alert=True, + agent_script_actions=False, action=failure_action, action_timeout=30, resolved_action=resolved_action, @@ -1328,6 +1331,14 @@ def test_alert_actions( agent_outages_task() + # should not have been called since agent_script_actions is set to False + nats_cmd.assert_not_called() + + alert_template.agent_script_actions = True # type: ignore + alert_template.save() # type: ignore + + agent_outages_task() + # this is what data should be data = { "func": "runscriptfull", @@ -1340,14 +1351,6 @@ def test_alert_actions( nats_cmd.reset_mock() - # Setup cmd mock - success = { - "retcode": 0, - "stdout": "success!", - "stderr": "", - "execution_time": 5.0000, - } - nats_cmd.side_effect = ["pong", success] # make sure script run results were stored @@ -1398,3 +1401,36 @@ def test_parse_script_args(self): ["-Parameter", f"-Another '{alert.id}'"], # type: ignore alert.parse_script_args(args=args), # type: ignore ) + + def test_prune_resolved_alerts(self): + from .tasks import prune_resolved_alerts + + # setup data + resolved_alerts = baker.make( + "alerts.Alert", + resolved=True, + _quantity=25, + ) + + alerts = baker.make( + "alerts.Alert", + resolved=False, + _quantity=25, + ) + + days = 0 + for alert in resolved_alerts: # type: ignore + alert.alert_time = djangotime.now() - djangotime.timedelta(days=days) + alert.save() + days = days + 5 + + days = 0 + for alert in alerts: # type: ignore + alert.alert_time = djangotime.now() - djangotime.timedelta(days=days) + alert.save() + days = days + 5 + + # delete AgentHistory older than 30 days + prune_resolved_alerts(30) + + self.assertEqual(Alert.objects.count(), 31) diff --git a/api/tacticalrmm/apiv3/urls.py b/api/tacticalrmm/apiv3/urls.py index 53d0c0d516..4b732818c9 100644 --- a/api/tacticalrmm/apiv3/urls.py +++ b/api/tacticalrmm/apiv3/urls.py @@ -20,4 +20,5 @@ path("superseded/", views.SupersededWinUpdate.as_view()), path("/chocoresult/", views.ChocoResult.as_view()), path("/recovery/", views.AgentRecovery.as_view()), + path("//histresult/", views.AgentHistoryResult.as_view()), ] diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index c452b191a2..75d9fce77d 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -6,7 +6,6 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime -from loguru import logger from packaging import version as pyver from rest_framework.authentication import TokenAuthentication from rest_framework.authtoken.models import Token @@ -15,20 +14,18 @@ from rest_framework.views import APIView from accounts.models import User -from agents.models import Agent, AgentCustomField -from agents.serializers import WinAgentSerializer +from agents.models import Agent, AgentHistory +from agents.serializers import WinAgentSerializer, AgentHistorySerializer from autotasks.models import AutomatedTask from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer from checks.models import Check from checks.serializers import CheckRunnerGetSerializer from checks.utils import bytes2human -from logs.models import PendingAction +from logs.models import PendingAction, DebugLog from software.models import InstalledSoftware from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats from winupdate.models import WinUpdate, WinUpdatePolicy -logger.configure(**settings.LOG_CONFIG) - class CheckIn(APIView): @@ -36,6 +33,10 @@ class CheckIn(APIView): permission_classes = [IsAuthenticated] def patch(self, request): + """ + !!! DEPRECATED AS OF AGENT 1.6.0 !!! + Endpoint be removed in a future release + """ from alerts.models import Alert updated = False @@ -182,7 +183,11 @@ def put(self, request): if reboot: asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) - logger.info(f"{agent.hostname} is rebooting after updates were installed.") + DebugLog.info( + agent=agent, + log_type="windows_updates", + message=f"{agent.hostname} is rebooting after updates were installed.", + ) agent.delete_superseded_updates() return Response("ok") @@ -350,7 +355,7 @@ class TaskRunner(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk, agentid): - agent = get_object_or_404(Agent, agent_id=agentid) + _ = get_object_or_404(Agent, agent_id=agentid) task = get_object_or_404(AutomatedTask, pk=pk) return Response(TaskGOGetSerializer(task).data) @@ -371,38 +376,7 @@ def patch(self, request, pk, agentid): if task.custom_field: if not task.stderr: - if AgentCustomField.objects.filter( - field=task.custom_field, agent=task.agent - ).exists(): - agent_field = AgentCustomField.objects.get( - field=task.custom_field, agent=task.agent - ) - else: - agent_field = AgentCustomField.objects.create( - field=task.custom_field, agent=task.agent - ) - - # get last line of stdout - value = ( - new_task.stdout - if task.collector_all_output - else new_task.stdout.split("\n")[-1].strip() - ) - - if task.custom_field.type in [ - "text", - "number", - "single", - "datetime", - ]: - agent_field.string_value = value - agent_field.save() - elif task.custom_field.type == "multiple": - agent_field.multiple_value = value.split(",") - agent_field.save() - elif task.custom_field.type == "checkbox": - agent_field.bool_value = bool(value) - agent_field.save() + task.save_collector_results() status = "passing" else: @@ -419,15 +393,6 @@ def patch(self, request, pk, agentid): else: Alert.handle_alert_failure(new_task) - AuditLog.objects.create( - username=agent.hostname, - agent=agent.hostname, - object_type="agent", - action="task_run", - message=f"Scheduled Task {task.name} was run on {agent.hostname}", - after_value=AutomatedTask.serialize(new_task), - ) - return Response("ok") @@ -518,6 +483,7 @@ def post(self, request): action="agent_install", message=f"{request.user} installed new agent {agent.hostname}", after_value=Agent.serialize(agent), + debug_info={"ip": request._client_ip}, ) return Response( @@ -622,3 +588,16 @@ def get(self, request, agentid): reload_nats() return Response(ret) + + +class AgentHistoryResult(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def patch(self, request, agentid, pk): + _ = get_object_or_404(Agent, agent_id=agentid) + hist = get_object_or_404(AgentHistory, pk=pk) + s = AgentHistorySerializer(instance=hist, data=request.data, partial=True) + s.is_valid(raise_exception=True) + s.save() + return Response("ok") diff --git a/api/tacticalrmm/automation/models.py b/api/tacticalrmm/automation/models.py index 3fe8eb1855..96701c2222 100644 --- a/api/tacticalrmm/automation/models.py +++ b/api/tacticalrmm/automation/models.py @@ -33,7 +33,7 @@ def save(self, *args, **kwargs): # get old policy if exists old_policy = type(self).objects.get(pk=self.pk) if self.pk else None - super(BaseAuditModel, self).save(*args, **kwargs) + super(Policy, self).save(old_model=old_policy, *args, **kwargs) # generate agent checks only if active and enforced were changed if old_policy: @@ -50,7 +50,7 @@ def delete(self, *args, **kwargs): from automation.tasks import generate_agent_checks_task agents = list(self.related_agents().only("pk").values_list("pk", flat=True)) - super(BaseAuditModel, self).delete(*args, **kwargs) + super(Policy, self).delete(*args, **kwargs) generate_agent_checks_task.delay(agents=agents, create_tasks=True) @@ -126,9 +126,9 @@ def get_related(self, mon_type): @staticmethod def serialize(policy): # serializes the policy and returns json - from .serializers import PolicySerializer + from .serializers import PolicyAuditSerializer - return PolicySerializer(policy).data + return PolicyAuditSerializer(policy).data @staticmethod def cascade_policy_tasks(agent): diff --git a/api/tacticalrmm/automation/serializers.py b/api/tacticalrmm/automation/serializers.py index 3cf17f4d1b..85b9273a0e 100644 --- a/api/tacticalrmm/automation/serializers.py +++ b/api/tacticalrmm/automation/serializers.py @@ -89,3 +89,9 @@ class Meta: model = AutomatedTask fields = "__all__" depth = 1 + + +class PolicyAuditSerializer(ModelSerializer): + class Meta: + model = Policy + fields = "__all__" diff --git a/api/tacticalrmm/autotasks/models.py b/api/tacticalrmm/autotasks/models.py index c96c50b41c..ad301b3e1f 100644 --- a/api/tacticalrmm/autotasks/models.py +++ b/api/tacticalrmm/autotasks/models.py @@ -6,19 +6,15 @@ import pytz from alerts.models import SEVERITY_CHOICES -from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.db import models from django.db.models.fields import DateTimeField from django.db.utils import DatabaseError from django.utils import timezone as djangotime -from logs.models import BaseAuditModel -from loguru import logger +from logs.models import BaseAuditModel, DebugLog from packaging import version as pyver from tacticalrmm.utils import bitdays_to_string -logger.configure(**settings.LOG_CONFIG) - RUN_TIME_DAY_CHOICES = [ (0, "Monday"), (1, "Tuesday"), @@ -195,9 +191,9 @@ def generate_task_name(): @staticmethod def serialize(task): # serializes the task and returns json - from .serializers import TaskSerializer + from .serializers import TaskAuditSerializer - return TaskSerializer(task).data + return TaskAuditSerializer(task).data def create_policy_task(self, agent=None, policy=None, assigned_check=None): @@ -254,7 +250,7 @@ def create_task_on_agent(self): elif self.task_type == "runonce": # check if scheduled time is in the past - agent_tz = pytz.timezone(agent.timezone) + agent_tz = pytz.timezone(agent.timezone) # type: ignore task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone( pytz.utc ) @@ -280,7 +276,7 @@ def create_task_on_agent(self): }, } - if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse( + if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse( # type: ignore "1.4.7" ): nats_data["schedtaskpayload"]["run_asap_after_missed"] = True @@ -301,19 +297,25 @@ def create_task_on_agent(self): else: return "error" - r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) + r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) # type: ignore if r != "ok": self.sync_status = "initial" self.save(update_fields=["sync_status"]) - logger.warning( - f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in." + DebugLog.warning( + agent=agent, + log_type="agent_issues", + message=f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in.", # type: ignore ) return "timeout" else: self.sync_status = "synced" self.save(update_fields=["sync_status"]) - logger.info(f"{agent.hostname} task {self.name} was successfully created") + DebugLog.info( + agent=agent, + log_type="agent_issues", + message=f"{agent.hostname} task {self.name} was successfully created", # type: ignore + ) return "ok" @@ -333,19 +335,25 @@ def modify_task_on_agent(self): "enabled": self.enabled, }, } - r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) + r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) # type: ignore if r != "ok": self.sync_status = "notsynced" self.save(update_fields=["sync_status"]) - logger.warning( - f"Unable to modify scheduled task {self.name} on {agent.hostname}. It will try again on next agent checkin" + DebugLog.warning( + agent=agent, + log_type="agent_issues", + message=f"Unable to modify scheduled task {self.name} on {agent.hostname}({agent.pk}). It will try again on next agent checkin", # type: ignore ) return "timeout" else: self.sync_status = "synced" self.save(update_fields=["sync_status"]) - logger.info(f"{agent.hostname} task {self.name} was successfully modified") + DebugLog.info( + agent=agent, + log_type="agent_issues", + message=f"{agent.hostname} task {self.name} was successfully modified", # type: ignore + ) return "ok" @@ -362,7 +370,7 @@ def delete_task_on_agent(self): "func": "delschedtask", "schedtaskpayload": {"name": self.win_task_name}, } - r = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) + r = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) # type: ignore if r != "ok" and "The system cannot find the file specified" not in r: self.sync_status = "pendingdeletion" @@ -372,13 +380,19 @@ def delete_task_on_agent(self): except DatabaseError: pass - logger.warning( - f"{agent.hostname} task {self.name} will be deleted on next checkin" + DebugLog.warning( + agent=agent, + log_type="agent_issues", + message=f"{agent.hostname} task {self.name} will be deleted on next checkin", # type: ignore ) return "timeout" else: self.delete() - logger.info(f"{agent.hostname} task {self.name} was deleted") + DebugLog.info( + agent=agent, + log_type="agent_issues", + message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted", # type: ignore + ) return "ok" @@ -391,9 +405,20 @@ def run_win_task(self): .first() ) - asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False)) + asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False)) # type: ignore return "ok" + def save_collector_results(self): + + agent_field = self.custom_field.get_or_create_field_value(self.agent) + + value = ( + self.stdout + if self.collector_all_output + else self.stdout.split("\n")[-1].strip() + ) + agent_field.save_to_field(value) + def should_create_alert(self, alert_template=None): return ( self.dashboard_alert @@ -424,7 +449,7 @@ def send_email(self): + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_mail(subject, body, self.agent.alert_template) + CORE.send_mail(subject, body, self.agent.alert_template) # type: ignore def send_sms(self): from core.models import CoreSettings @@ -441,7 +466,7 @@ def send_sms(self): + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_sms(body, alert_template=self.agent.alert_template) + CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore def send_resolved_email(self): from core.models import CoreSettings @@ -453,7 +478,7 @@ def send_resolved_email(self): + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_mail(subject, body, alert_template=self.agent.alert_template) + CORE.send_mail(subject, body, alert_template=self.agent.alert_template) # type: ignore def send_resolved_sms(self): from core.models import CoreSettings @@ -464,4 +489,4 @@ def send_resolved_sms(self): subject + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_sms(body, alert_template=self.agent.alert_template) + CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore diff --git a/api/tacticalrmm/autotasks/serializers.py b/api/tacticalrmm/autotasks/serializers.py index 138fb38d8f..b383f7987c 100644 --- a/api/tacticalrmm/autotasks/serializers.py +++ b/api/tacticalrmm/autotasks/serializers.py @@ -84,3 +84,9 @@ class TaskRunnerPatchSerializer(serializers.ModelSerializer): class Meta: model = AutomatedTask fields = "__all__" + + +class TaskAuditSerializer(serializers.ModelSerializer): + class Meta: + model = AutomatedTask + fields = "__all__" diff --git a/api/tacticalrmm/autotasks/tasks.py b/api/tacticalrmm/autotasks/tasks.py index 40be71b683..6c6efd6c5b 100644 --- a/api/tacticalrmm/autotasks/tasks.py +++ b/api/tacticalrmm/autotasks/tasks.py @@ -1,18 +1,16 @@ import asyncio import datetime as dt +from logging import log import random from time import sleep from typing import Union -from django.conf import settings from django.utils import timezone as djangotime -from loguru import logger from autotasks.models import AutomatedTask +from logs.models import DebugLog from tacticalrmm.celery import app -logger.configure(**settings.LOG_CONFIG) - @app.task def create_win_task_schedule(pk): @@ -53,12 +51,20 @@ def remove_orphaned_win_tasks(agentpk): agent = Agent.objects.get(pk=agentpk) - logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.") + DebugLog.info( + agent=agent, + log_type="agent_issues", + message=f"Orphaned task cleanup initiated on {agent.hostname}.", + ) r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10)) if not isinstance(r, list) and not r: # empty list - logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}") + DebugLog.error( + agent=agent, + log_type="agent_issues", + message=f"Unable to clean up scheduled tasks on {agent.hostname}: {r}", + ) return "notlist" agent_task_names = list(agent.autotasks.values_list("win_task_name", flat=True)) @@ -83,13 +89,23 @@ def remove_orphaned_win_tasks(agentpk): } ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) if ret != "ok": - logger.error( - f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}" + DebugLog.error( + agent=agent, + log_type="agent_issues", + message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}", ) else: - logger.info(f"Removed orphaned task {task} from {agent.hostname}") + DebugLog.info( + agent=agent, + log_type="agent_issues", + message=f"Removed orphaned task {task} from {agent.hostname}", + ) - logger.info(f"Orphaned task cleanup finished on {agent.hostname}") + DebugLog.info( + agent=agent, + log_type="agent_issues", + message=f"Orphaned task cleanup finished on {agent.hostname}", + ) @app.task diff --git a/api/tacticalrmm/checks/models.py b/api/tacticalrmm/checks/models.py index 1d85ffc295..b347d8bb22 100644 --- a/api/tacticalrmm/checks/models.py +++ b/api/tacticalrmm/checks/models.py @@ -12,10 +12,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from logs.models import BaseAuditModel -from loguru import logger - - -logger.configure(**settings.LOG_CONFIG) CHECK_TYPE_CHOICES = [ ("diskspace", "Disk Space Check"), @@ -475,9 +471,9 @@ def handle_assigned_task(self) -> None: @staticmethod def serialize(check): # serializes the check and returns json - from .serializers import CheckSerializer + from .serializers import CheckAuditSerializer - return CheckSerializer(check).data + return CheckAuditSerializer(check).data # for policy diskchecks @staticmethod diff --git a/api/tacticalrmm/checks/serializers.py b/api/tacticalrmm/checks/serializers.py index 690292436c..2537712161 100644 --- a/api/tacticalrmm/checks/serializers.py +++ b/api/tacticalrmm/checks/serializers.py @@ -220,3 +220,9 @@ def get_x(self, obj): class Meta: model = CheckHistory fields = ("x", "y", "results") + + +class CheckAuditSerializer(serializers.ModelSerializer): + class Meta: + model = Check + fields = "__all__" diff --git a/api/tacticalrmm/clients/models.py b/api/tacticalrmm/clients/models.py index a9c2c6e016..231ba68130 100644 --- a/api/tacticalrmm/clients/models.py +++ b/api/tacticalrmm/clients/models.py @@ -33,13 +33,17 @@ class Client(BaseAuditModel): blank=True, ) - def save(self, *args, **kw): + def save(self, *args, **kwargs): from alerts.tasks import cache_agents_alert_template from automation.tasks import generate_agent_checks_task # get old client if exists - old_client = type(self).objects.get(pk=self.pk) if self.pk else None - super(BaseAuditModel, self).save(*args, **kw) + old_client = Client.objects.get(pk=self.pk) if self.pk else None + super(Client, self).save( + old_model=old_client, + *args, + **kwargs, + ) # check if polcies have changed and initiate task to reapply policies if so if old_client: @@ -50,7 +54,6 @@ def save(self, *args, **kw): old_client.block_policy_inheritance != self.block_policy_inheritance ) ): - generate_agent_checks_task.delay( client=self.pk, create_tasks=True, @@ -120,10 +123,10 @@ def has_failing_checks(self): @staticmethod def serialize(client): - # serializes the client and returns json - from .serializers import ClientSerializer + from .serializers import ClientAuditSerializer - return ClientSerializer(client).data + # serializes the client and returns json + return ClientAuditSerializer(client).data class Site(BaseAuditModel): @@ -153,13 +156,17 @@ class Site(BaseAuditModel): blank=True, ) - def save(self, *args, **kw): + def save(self, *args, **kwargs): from alerts.tasks import cache_agents_alert_template from automation.tasks import generate_agent_checks_task # get old client if exists - old_site = type(self).objects.get(pk=self.pk) if self.pk else None - super(Site, self).save(*args, **kw) + old_site = Site.objects.get(pk=self.pk) if self.pk else None + super(Site, self).save( + old_model=old_site, + *args, + **kwargs, + ) # check if polcies have changed and initiate task to reapply policies if so if old_site: @@ -168,11 +175,10 @@ def save(self, *args, **kw): or (old_site.workstation_policy != self.workstation_policy) or (old_site.block_policy_inheritance != self.block_policy_inheritance) ): - generate_agent_checks_task.delay(site=self.pk, create_tasks=True) - if old_site.alert_template != self.alert_template: - cache_agents_alert_template.delay() + if old_site.alert_template != self.alert_template: + cache_agents_alert_template.delay() class Meta: ordering = ("name",) @@ -233,10 +239,10 @@ def has_failing_checks(self): @staticmethod def serialize(site): - # serializes the site and returns json - from .serializers import SiteSerializer + from .serializers import SiteAuditSerializer - return SiteSerializer(site).data + # serializes the site and returns json + return SiteAuditSerializer(site).data MON_TYPE_CHOICES = [ @@ -308,6 +314,22 @@ def value(self): else: return self.string_value + def save_to_field(self, value): + if self.field.type in [ + "text", + "number", + "single", + "datetime", + ]: + self.string_value = value + self.save() + elif type == "multiple": + self.multiple_value = value.split(",") + self.save() + elif type == "checkbox": + self.bool_value = bool(value) + self.save() + class SiteCustomField(models.Model): site = models.ForeignKey( @@ -342,3 +364,19 @@ def value(self): return self.bool_value else: return self.string_value + + def save_to_field(self, value): + if self.field.type in [ + "text", + "number", + "single", + "datetime", + ]: + self.string_value = value + self.save() + elif type == "multiple": + self.multiple_value = value.split(",") + self.save() + elif type == "checkbox": + self.bool_value = bool(value) + self.save() diff --git a/api/tacticalrmm/clients/serializers.py b/api/tacticalrmm/clients/serializers.py index 1dc2f9e22b..46d63c3974 100644 --- a/api/tacticalrmm/clients/serializers.py +++ b/api/tacticalrmm/clients/serializers.py @@ -1,4 +1,10 @@ -from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError +from django.db.models.base import Model +from rest_framework.serializers import ( + ModelSerializer, + ReadOnlyField, + Serializer, + ValidationError, +) from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField @@ -134,3 +140,15 @@ class Meta: "install_flags", "created", ] + + +class SiteAuditSerializer(ModelSerializer): + class Meta: + model = Site + fields = "__all__" + + +class ClientAuditSerializer(ModelSerializer): + class Meta: + model = Client + fields = "__all__" diff --git a/api/tacticalrmm/clients/views.py b/api/tacticalrmm/clients/views.py index 409d3474b9..d88373abf4 100644 --- a/api/tacticalrmm/clients/views.py +++ b/api/tacticalrmm/clients/views.py @@ -3,10 +3,8 @@ import uuid import pytz -from django.conf import settings from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime -from loguru import logger from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -26,8 +24,6 @@ SiteSerializer, ) -logger.configure(**settings.LOG_CONFIG) - class GetAddClients(APIView): permission_classes = [IsAuthenticated, ManageClientsPerms] diff --git a/api/tacticalrmm/core/migrations/0024_auto_20210707_1828.py b/api/tacticalrmm/core/migrations/0024_auto_20210707_1828.py new file mode 100644 index 0000000000..264f0e7f6f --- /dev/null +++ b/api/tacticalrmm/core/migrations/0024_auto_20210707_1828.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.1 on 2021-07-07 18:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0023_coresettings_clear_faults_days'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='agent_history_prune_days', + field=models.PositiveIntegerField(default=30), + ), + migrations.AddField( + model_name='coresettings', + name='resolved_alerts_prune_days', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0025_auto_20210707_1835.py b/api/tacticalrmm/core/migrations/0025_auto_20210707_1835.py new file mode 100644 index 0000000000..c75c5814e2 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0025_auto_20210707_1835.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.1 on 2021-07-07 18:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0024_auto_20210707_1828'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='agent_debug_level', + field=models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('critical', 'Critical')], default='info', max_length=20), + ), + migrations.AddField( + model_name='coresettings', + name='debug_log_prune_days', + field=models.PositiveIntegerField(default=30), + ), + migrations.AlterField( + model_name='coresettings', + name='agent_history_prune_days', + field=models.PositiveIntegerField(default=60), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0026_coresettings_audit_log_prune_days.py b/api/tacticalrmm/core/migrations/0026_coresettings_audit_log_prune_days.py new file mode 100644 index 0000000000..56ce994e63 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0026_coresettings_audit_log_prune_days.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.1 on 2021-07-21 17:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0025_auto_20210707_1835'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='audit_log_prune_days', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/api/tacticalrmm/core/models.py b/api/tacticalrmm/core/models.py index 2453170d2e..8b66c695fe 100644 --- a/api/tacticalrmm/core/models.py +++ b/api/tacticalrmm/core/models.py @@ -1,17 +1,15 @@ import smtplib from email.message import EmailMessage +from django.db.models.enums import Choices import pytz from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models -from loguru import logger from twilio.rest import Client as TwClient -from logs.models import BaseAuditModel - -logger.configure(**settings.LOG_CONFIG) +from logs.models import BaseAuditModel, DebugLog, LOG_LEVEL_CHOICES TZ_CHOICES = [(_, _) for _ in pytz.all_timezones] @@ -51,6 +49,13 @@ class CoreSettings(BaseAuditModel): ) # removes check history older than days check_history_prune_days = models.PositiveIntegerField(default=30) + resolved_alerts_prune_days = models.PositiveIntegerField(default=0) + agent_history_prune_days = models.PositiveIntegerField(default=60) + debug_log_prune_days = models.PositiveIntegerField(default=30) + audit_log_prune_days = models.PositiveIntegerField(default=0) + agent_debug_level = models.CharField( + max_length=20, choices=LOG_LEVEL_CHOICES, default="info" + ) clear_faults_days = models.IntegerField(default=0) mesh_token = models.CharField(max_length=255, null=True, blank=True, default="") mesh_username = models.CharField(max_length=255, null=True, blank=True, default="") @@ -184,14 +189,14 @@ def send_mail(self, subject, body, alert_template=None, test=False): server.quit() except Exception as e: - logger.error(f"Sending email failed with error: {e}") + DebugLog.error(message=f"Sending email failed with error: {e}") if test: return str(e) else: return True def send_sms(self, body, alert_template=None): - if not alert_template and not self.sms_is_configured: + if not alert_template or not self.sms_is_configured: return # override email recipients if alert_template is passed and is set @@ -205,7 +210,7 @@ def send_sms(self, body, alert_template=None): try: tw_client.messages.create(body=body, to=num, from_=self.twilio_number) except Exception as e: - logger.error(f"SMS failed to send: {e}") + DebugLog.error(message=f"SMS failed to send: {e}") @staticmethod def serialize(core): @@ -265,6 +270,26 @@ def default_value(self): else: return self.default_value_string + def get_or_create_field_value(self, instance): + from agents.models import Agent, AgentCustomField + from clients.models import Client, ClientCustomField, Site, SiteCustomField + + if isinstance(instance, Agent): + if AgentCustomField.objects.filter(field=self, agent=instance).exists(): + return AgentCustomField.objects.get(field=self, agent=instance) + else: + return AgentCustomField.objects.create(field=self, agent=instance) + elif isinstance(instance, Client): + if ClientCustomField.objects.filter(field=self, client=instance).exists(): + return ClientCustomField.objects.get(field=self, client=instance) + else: + return ClientCustomField.objects.create(field=self, client=instance) + elif isinstance(instance, Site): + if SiteCustomField.objects.filter(field=self, site=instance).exists(): + return SiteCustomField.objects.get(field=self, site=instance) + else: + return SiteCustomField.objects.create(field=self, site=instance) + class CodeSignToken(models.Model): token = models.CharField(max_length=255, null=True, blank=True) @@ -287,6 +312,9 @@ def __str__(self): return self.name +OPEN_ACTIONS = (("window", "New Window"), ("tab", "New Tab")) + + class URLAction(models.Model): name = models.CharField(max_length=25) desc = models.CharField(max_length=100, null=True, blank=True) diff --git a/api/tacticalrmm/core/tasks.py b/api/tacticalrmm/core/tasks.py index 5372aac71c..06e3f65c37 100644 --- a/api/tacticalrmm/core/tasks.py +++ b/api/tacticalrmm/core/tasks.py @@ -1,17 +1,15 @@ import pytz -from django.conf import settings from django.utils import timezone as djangotime -from loguru import logger from autotasks.models import AutomatedTask from autotasks.tasks import delete_win_task_schedule from checks.tasks import prune_check_history -from agents.tasks import clear_faults_task +from agents.tasks import clear_faults_task, prune_agent_history +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 -logger.configure(**settings.LOG_CONFIG) - @app.task def core_maintenance_tasks(): @@ -32,11 +30,28 @@ def core_maintenance_tasks(): core = CoreSettings.objects.first() # remove old CheckHistory data - if core.check_history_prune_days > 0: - prune_check_history.delay(core.check_history_prune_days) + if core.check_history_prune_days > 0: # type: ignore + prune_check_history.delay(core.check_history_prune_days) # type: ignore + + # remove old resolved alerts + if core.resolved_alerts_prune_days > 0: # type: ignore + prune_resolved_alerts.delay(core.resolved_alerts_prune_days) # type: ignore + + # remove old agent history + if core.agent_history_prune_days > 0: # type: ignore + prune_agent_history.delay(core.agent_history_prune_days) # type: ignore + + # remove old debug logs + if core.debug_log_prune_days > 0: # type: ignore + prune_debug_log.delay(core.debug_log_prune_days) # type: ignore + + # remove old audit logs + if core.audit_log_prune_days > 0: # type: ignore + prune_audit_log.delay(core.audit_log_prune_days) # type: ignore + # clear faults - if core.clear_faults_days > 0: - clear_faults_task.delay(core.clear_faults_days) + if core.clear_faults_days > 0: # type: ignore + clear_faults_task.delay(core.clear_faults_days) # type: ignore @app.task diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index 0394bcf862..5a01f83fb4 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -346,9 +346,18 @@ def patch(self, request): from requests.utils import requote_uri from agents.models import Agent + from clients.models import Client, Site from tacticalrmm.utils import replace_db_values - agent = get_object_or_404(Agent, pk=request.data["agent"]) + if "agent" in request.data.keys(): + instance = get_object_or_404(Agent, pk=request.data["agent"]) + elif "site" in request.data.keys(): + instance = get_object_or_404(Site, pk=request.data["site"]) + elif "client" in request.data.keys(): + instance = get_object_or_404(Client, pk=request.data["client"]) + else: + return notify_error("received an incorrect request") + action = get_object_or_404(URLAction, pk=request.data["action"]) pattern = re.compile("\\{\\{([\\w\\s]+\\.[\\w\\s]+)\\}\\}") @@ -356,7 +365,7 @@ def patch(self, request): url_pattern = action.pattern for string in re.findall(pattern, action.pattern): - value = replace_db_values(string=string, agent=agent, quotes=False) + value = replace_db_values(string=string, instance=instance, quotes=False) url_pattern = re.sub("\\{\\{" + string + "\\}\\}", str(value), url_pattern) diff --git a/api/tacticalrmm/logs/admin.py b/api/tacticalrmm/logs/admin.py index 67f83298ea..00320fa20f 100644 --- a/api/tacticalrmm/logs/admin.py +++ b/api/tacticalrmm/logs/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from .models import AuditLog, PendingAction +from .models import AuditLog, PendingAction, DebugLog admin.site.register(PendingAction) admin.site.register(AuditLog) +admin.site.register(DebugLog) diff --git a/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py b/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py new file mode 100644 index 0000000000..62f2f7f2ee --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.1 on 2021-06-14 18:35 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("logs", "0012_auto_20210228_0943"), + ] + + operations = [ + migrations.AddField( + model_name="debuglog", + name="agent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="debuglogs", + to="agents.agent", + ), + ), + migrations.AddField( + model_name="debuglog", + name="entry_time", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="debuglog", + name="log_level", + field=models.CharField( + choices=[ + ("info", "Info"), + ("warning", "Warning"), + ("error", "Error"), + ("critical", "Critical"), + ], + default="info", + max_length=50, + ), + ), + migrations.AddField( + model_name="debuglog", + name="log_type", + field=models.CharField( + choices=[ + ("agent_update", "Agent Update"), + ("agent_issues", "Agent Issues"), + ("win_updates", "Windows Updates"), + ("system_issues", "System Issues"), + ("scripting", "Scripting"), + ], + default="system_issues", + max_length=50, + ), + ), + migrations.AddField( + model_name="debuglog", + name="message", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/logs/migrations/0014_auditlog_agent_id.py b/api/tacticalrmm/logs/migrations/0014_auditlog_agent_id.py new file mode 100644 index 0000000000..8dfd480548 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0014_auditlog_agent_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.1 on 2021-06-28 02:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('logs', '0013_auto_20210614_1835'), + ] + + operations = [ + migrations.AddField( + model_name='auditlog', + name='agent_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/logs/migrations/0015_alter_auditlog_object_type.py b/api/tacticalrmm/logs/migrations/0015_alter_auditlog_object_type.py new file mode 100644 index 0000000000..b6decaf9e0 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0015_alter_auditlog_object_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.1 on 2021-07-21 04:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('logs', '0014_auditlog_agent_id'), + ] + + operations = [ + migrations.AlterField( + model_name='auditlog', + name='object_type', + field=models.CharField(choices=[('user', 'User'), ('script', 'Script'), ('agent', 'Agent'), ('policy', 'Policy'), ('winupdatepolicy', 'Patch Policy'), ('client', 'Client'), ('site', 'Site'), ('check', 'Check'), ('automatedtask', 'Automated Task'), ('coresettings', 'Core Settings'), ('bulk', 'Bulk'), ('alert_template', 'Alert Template'), ('role', 'Role')], max_length=100), + ), + ] diff --git a/api/tacticalrmm/logs/migrations/0016_alter_auditlog_object_type.py b/api/tacticalrmm/logs/migrations/0016_alter_auditlog_object_type.py new file mode 100644 index 0000000000..ea8d2a2e77 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0016_alter_auditlog_object_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.1 on 2021-07-21 17:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('logs', '0015_alter_auditlog_object_type'), + ] + + operations = [ + migrations.AlterField( + model_name='auditlog', + name='object_type', + field=models.CharField(choices=[('user', 'User'), ('script', 'Script'), ('agent', 'Agent'), ('policy', 'Policy'), ('winupdatepolicy', 'Patch Policy'), ('client', 'Client'), ('site', 'Site'), ('check', 'Check'), ('automatedtask', 'Automated Task'), ('coresettings', 'Core Settings'), ('bulk', 'Bulk'), ('alerttemplate', 'Alert Template'), ('role', 'Role')], max_length=100), + ), + ] diff --git a/api/tacticalrmm/logs/migrations/0017_auto_20210731_1707.py b/api/tacticalrmm/logs/migrations/0017_auto_20210731_1707.py new file mode 100644 index 0000000000..36dabe089f --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0017_auto_20210731_1707.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.1 on 2021-07-31 17:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('logs', '0016_alter_auditlog_object_type'), + ] + + operations = [ + migrations.AddField( + model_name='pendingaction', + name='cancelable', + field=models.BooleanField(blank=True, default=False), + ), + migrations.AlterField( + model_name='pendingaction', + name='action_type', + field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('taskaction', 'Scheduled Task Action'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install'), ('runcmd', 'Run Command'), ('runscript', 'Run Script'), ('runpatchscan', 'Run Patch Scan'), ('runpatchinstall', 'Run Patch Install')], max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/logs/models.py b/api/tacticalrmm/logs/models.py index 9cad83568c..f5e744fe65 100644 --- a/api/tacticalrmm/logs/models.py +++ b/api/tacticalrmm/logs/models.py @@ -2,14 +2,24 @@ from abc import abstractmethod from django.db import models - from tacticalrmm.middleware import get_debug_info, get_username + +def get_debug_level(): + from core.models import CoreSettings + + return CoreSettings.objects.first().agent_debug_level # type: ignore + + ACTION_TYPE_CHOICES = [ ("schedreboot", "Scheduled Reboot"), ("taskaction", "Scheduled Task Action"), # deprecated ("agentupdate", "Agent Update"), ("chocoinstall", "Chocolatey Software Install"), + ("runcmd", "Run Command"), + ("runscript", "Run Script"), + ("runpatchscan", "Run Patch Scan"), + ("runpatchinstall", "Run Patch Install"), ] AUDIT_ACTION_TYPE_CHOICES = [ @@ -40,6 +50,8 @@ ("automatedtask", "Automated Task"), ("coresettings", "Core Settings"), ("bulk", "Bulk"), + ("alerttemplate", "Alert Template"), + ("role", "Role"), ] STATUS_CHOICES = [ @@ -51,6 +63,7 @@ class AuditLog(models.Model): username = models.CharField(max_length=100) agent = models.CharField(max_length=255, null=True, blank=True) + agent_id = models.PositiveIntegerField(blank=True, null=True) entry_time = models.DateTimeField(auto_now_add=True) action = models.CharField(max_length=100, choices=AUDIT_ACTION_TYPE_CHOICES) object_type = models.CharField(max_length=100, choices=AUDIT_OBJECT_TYPE_CHOICES) @@ -73,24 +86,25 @@ def save(self, *args, **kwargs): return super(AuditLog, self).save(*args, **kwargs) @staticmethod - def audit_mesh_session(username, hostname, debug_info={}): + def audit_mesh_session(username, agent, debug_info={}): AuditLog.objects.create( username=username, - agent=hostname, + agent=agent.hostname, + agent_id=agent.id, object_type="agent", action="remote_session", - message=f"{username} used Mesh Central to initiate a remote session to {hostname}.", + message=f"{username} used Mesh Central to initiate a remote session to {agent.hostname}.", debug_info=debug_info, ) @staticmethod - def audit_raw_command(username, hostname, cmd, shell, debug_info={}): + def audit_raw_command(username, agent, cmd, shell, debug_info={}): AuditLog.objects.create( username=username, - agent=hostname, + agent=agent.hostname, object_type="agent", action="execute_command", - message=f"{username} issued {shell} command on {hostname}.", + message=f"{username} issued {shell} command on {agent.hostname}.", after_value=cmd, debug_info=debug_info, ) @@ -102,6 +116,7 @@ def audit_object_changed( AuditLog.objects.create( username=username, object_type=object_type, + agent_id=before["id"] if object_type == "agent" else None, action="modify", message=f"{username} modified {object_type} {name}", before_value=before, @@ -114,6 +129,7 @@ def audit_object_add(username, object_type, after, name="", debug_info={}): AuditLog.objects.create( username=username, object_type=object_type, + agent=after["id"] if object_type == "agent" else None, action="add", message=f"{username} added {object_type} {name}", after_value=after, @@ -125,6 +141,7 @@ def audit_object_delete(username, object_type, before, name="", debug_info={}): AuditLog.objects.create( username=username, object_type=object_type, + agent=before["id"] if object_type == "agent" else None, action="delete", message=f"{username} deleted {object_type} {name}", before_value=before, @@ -132,13 +149,14 @@ def audit_object_delete(username, object_type, before, name="", debug_info={}): ) @staticmethod - def audit_script_run(username, hostname, script, debug_info={}): + def audit_script_run(username, agent, script, debug_info={}): AuditLog.objects.create( - agent=hostname, + agent=agent.hostname, + agent_id=agent.id, username=username, object_type="agent", action="execute_script", - message=f'{username} ran script: "{script}" on {hostname}', + message=f'{username} ran script: "{script}" on {agent.hostname}', debug_info=debug_info, ) @@ -190,13 +208,13 @@ def audit_bulk_action(username, action, affected, debug_info={}): site = Site.objects.get(pk=affected["site"]) target = f"on all agents within site: {site.client.name}\\{site.name}" elif affected["target"] == "agents": - agents = Agent.objects.filter(pk__in=affected["agentPKs"]).values_list( + agents = Agent.objects.filter(pk__in=affected["agents"]).values_list( "hostname", flat=True ) target = "on multiple agents" if action == "script": - script = Script.objects.get(pk=affected["scriptPK"]) + script = Script.objects.get(pk=affected["script"]) action = f"script: {script.name}" if agents: @@ -212,8 +230,63 @@ def audit_bulk_action(username, action, affected, debug_info={}): ) +LOG_LEVEL_CHOICES = [ + ("info", "Info"), + ("warning", "Warning"), + ("error", "Error"), + ("critical", "Critical"), +] + +LOG_TYPE_CHOICES = [ + ("agent_update", "Agent Update"), + ("agent_issues", "Agent Issues"), + ("win_updates", "Windows Updates"), + ("system_issues", "System Issues"), + ("scripting", "Scripting"), +] + + class DebugLog(models.Model): - pass + entry_time = models.DateTimeField(auto_now_add=True) + agent = models.ForeignKey( + "agents.Agent", + related_name="debuglogs", + on_delete=models.CASCADE, + null=True, + blank=True, + ) + log_level = models.CharField( + max_length=50, choices=LOG_LEVEL_CHOICES, default="info" + ) + log_type = models.CharField( + max_length=50, choices=LOG_TYPE_CHOICES, default="system_issues" + ) + message = models.TextField(null=True, blank=True) + + @classmethod + def info( + cls, + message, + agent=None, + log_type="system_issues", + ): + if get_debug_level() in ["info"]: + cls(log_level="info", agent=agent, log_type=log_type, message=message) + + @classmethod + def warning(cls, message, agent=None, log_type="system_issues"): + if get_debug_level() in ["info", "warning"]: + cls(log_level="warning", agent=agent, log_type=log_type, message=message) + + @classmethod + def error(cls, message, agent=None, log_type="system_issues"): + if get_debug_level() in ["info", "warning", "error"]: + cls(log_level="error", agent=agent, log_type=log_type, message=message) + + @classmethod + def critical(cls, message, agent=None, log_type="system_issues"): + if get_debug_level() in ["info", "warning", "error", "critical"]: + cls(log_level="critical", agent=agent, log_type=log_type, message=message) class PendingAction(models.Model): @@ -232,6 +305,7 @@ class PendingAction(models.Model): choices=STATUS_CHOICES, default="pending", ) + cancelable = models.BooleanField(blank=True, default=False) celery_id = models.CharField(null=True, blank=True, max_length=255) details = models.JSONField(null=True, blank=True) @@ -247,6 +321,8 @@ def due(self): return "Next update cycle" elif self.action_type == "chocoinstall": return "ASAP" + else: + return "On next checkin" @property def description(self): @@ -259,6 +335,14 @@ def description(self): elif self.action_type == "chocoinstall": return f"{self.details['name']} software install" + elif self.action_type in [ + "runcmd", + "runscript", + "runpatchscan", + "runpatchinstall", + ]: + return f"{self.action_type}" + class BaseAuditModel(models.Model): # abstract base class for auditing models @@ -275,13 +359,14 @@ class Meta: def serialize(): pass - def save(self, *args, **kwargs): + def save(self, old_model=None, *args, **kwargs): + if get_username(): - before_value = {} object_class = type(self) object_name = object_class.__name__.lower() username = get_username() + after_value = object_class.serialize(self) # type: ignore # populate created_by and modified_by fields on instance if not getattr(self, "created_by", None): @@ -289,32 +374,37 @@ def save(self, *args, **kwargs): if hasattr(self, "modified_by"): self.modified_by = username - # capture object properties before edit - if self.pk: - before_value = object_class.objects.get(pk=self.id) - # dont create entry for agent add since that is done in view if not self.pk: AuditLog.audit_object_add( username, object_name, - object_class.serialize(self), + after_value, # type: ignore self.__str__(), debug_info=get_debug_info(), ) else: - AuditLog.audit_object_changed( - username, - object_class.__name__.lower(), - object_class.serialize(before_value), - object_class.serialize(self), - self.__str__(), - debug_info=get_debug_info(), - ) - return super(BaseAuditModel, self).save(*args, **kwargs) + if old_model: + before_value = object_class.serialize(old_model) # type: ignore + else: + before_value = object_class.serialize(object_class.objects.get(pk=self.pk)) # type: ignore + # only create an audit entry if the values have changed + if before_value != after_value: # type: ignore + + AuditLog.audit_object_changed( + username, + object_class.__name__.lower(), + before_value, + after_value, # type: ignore + self.__str__(), + debug_info=get_debug_info(), + ) + + super(BaseAuditModel, self).save(*args, **kwargs) def delete(self, *args, **kwargs): + super(BaseAuditModel, self).delete(*args, **kwargs) if get_username(): @@ -322,9 +412,7 @@ def delete(self, *args, **kwargs): AuditLog.audit_object_delete( get_username(), object_class.__name__.lower(), - object_class.serialize(self), + object_class.serialize(self), # type: ignore self.__str__(), debug_info=get_debug_info(), ) - - return super(BaseAuditModel, self).delete(*args, **kwargs) diff --git a/api/tacticalrmm/logs/serializers.py b/api/tacticalrmm/logs/serializers.py index 2234e5be6f..61916a3494 100644 --- a/api/tacticalrmm/logs/serializers.py +++ b/api/tacticalrmm/logs/serializers.py @@ -2,12 +2,12 @@ from tacticalrmm.utils import get_default_timezone -from .models import AuditLog, PendingAction +from .models import AuditLog, DebugLog, PendingAction class AuditLogSerializer(serializers.ModelSerializer): - entry_time = serializers.SerializerMethodField(read_only=True) + ip_address = serializers.ReadOnlyField(source="debug_info.ip") class Meta: model = AuditLog @@ -19,7 +19,6 @@ def get_entry_time(self, log): 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") @@ -30,3 +29,16 @@ class PendingActionSerializer(serializers.ModelSerializer): class Meta: model = PendingAction fields = "__all__" + + +class DebugLogSerializer(serializers.ModelSerializer): + agent = serializers.ReadOnlyField(source="agent.hostname") + entry_time = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = DebugLog + fields = "__all__" + + def get_entry_time(self, log): + timezone = get_default_timezone() + return log.entry_time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S") diff --git a/api/tacticalrmm/logs/tasks.py b/api/tacticalrmm/logs/tasks.py new file mode 100644 index 0000000000..6e0061e079 --- /dev/null +++ b/api/tacticalrmm/logs/tasks.py @@ -0,0 +1,25 @@ +from django.utils import timezone as djangotime + +from tacticalrmm.celery import app + + +@app.task +def prune_debug_log(older_than_days: int) -> str: + from .models import DebugLog + + DebugLog.objects.filter( + entry_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) + ).delete() + + return "ok" + + +@app.task +def prune_audit_log(older_than_days: int) -> str: + from .models import AuditLog + + AuditLog.objects.filter( + entry_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) + ).delete() + + return "ok" diff --git a/api/tacticalrmm/logs/tests.py b/api/tacticalrmm/logs/tests.py index b9a2e03b4b..7d084f8294 100644 --- a/api/tacticalrmm/logs/tests.py +++ b/api/tacticalrmm/logs/tests.py @@ -1,10 +1,11 @@ -from datetime import datetime, timedelta +from itertools import cycle from unittest.mock import patch +from django.utils import timezone as djangotime from model_bakery import baker, seq +from tacticalrmm.test import TacticalTestCase from logs.models import PendingAction -from tacticalrmm.test import TacticalTestCase class TestAuditViews(TacticalTestCase): @@ -16,20 +17,23 @@ def create_audit_records(self): # create clients for client filter site = baker.make("clients.Site") - baker.make_recipe("agents.agent", site=site, hostname="AgentHostname1") + agent1 = baker.make_recipe("agents.agent", site=site, hostname="AgentHostname1") + agent2 = baker.make_recipe("agents.agent", hostname="AgentHostname2") + agent0 = baker.make_recipe("agents.agent", hostname="AgentHostname") + # user jim agent logs baker.make_recipe( "logs.agent_logs", username="jim", agent="AgentHostname1", - entry_time=seq(datetime.now(), timedelta(days=3)), + agent_id=agent1.id, _quantity=15, ) baker.make_recipe( "logs.agent_logs", username="jim", agent="AgentHostname2", - entry_time=seq(datetime.now(), timedelta(days=100)), + agent_id=agent2.id, _quantity=8, ) @@ -38,14 +42,14 @@ def create_audit_records(self): "logs.agent_logs", username="james", agent="AgentHostname1", - entry_time=seq(datetime.now(), timedelta(days=55)), + agent_id=agent1.id, _quantity=7, ) baker.make_recipe( "logs.agent_logs", username="james", agent="AgentHostname2", - entry_time=seq(datetime.now(), timedelta(days=20)), + agent_id=agent2.id, _quantity=10, ) @@ -53,7 +57,7 @@ def create_audit_records(self): baker.make_recipe( "logs.agent_logs", agent=seq("AgentHostname"), - entry_time=seq(datetime.now(), timedelta(days=29)), + agent_id=seq(agent1.id), _quantity=5, ) @@ -61,7 +65,6 @@ def create_audit_records(self): baker.make_recipe( "logs.object_logs", username="james", - entry_time=seq(datetime.now(), timedelta(days=5)), _quantity=17, ) @@ -69,7 +72,6 @@ def create_audit_records(self): baker.make_recipe( "logs.login_logs", username="james", - entry_time=seq(datetime.now(), timedelta(days=7)), _quantity=11, ) @@ -77,51 +79,62 @@ def create_audit_records(self): baker.make_recipe( "logs.login_logs", username="jim", - entry_time=seq(datetime.now(), timedelta(days=11)), _quantity=13, ) - return site + return {"site": site, "agents": [agent0, agent1, agent2]} def test_get_audit_logs(self): url = "/logs/auditlogs/" # create data - site = self.create_audit_records() + data = self.create_audit_records() # test data and result counts data = [ {"filter": {"timeFilter": 30}, "count": 86}, { - "filter": {"timeFilter": 45, "agentFilter": ["AgentHostname2"]}, + "filter": { + "timeFilter": 45, + "agentFilter": [data["agents"][2].id], + }, "count": 19, }, { - "filter": {"userFilter": ["jim"], "agentFilter": ["AgentHostname1"]}, + "filter": { + "userFilter": ["jim"], + "agentFilter": [data["agents"][1].id], + }, "count": 15, }, { "filter": { "timeFilter": 180, "userFilter": ["james"], - "agentFilter": ["AgentHostname1"], + "agentFilter": [data["agents"][1].id], }, "count": 7, }, {"filter": {}, "count": 86}, - {"filter": {"agentFilter": ["DoesntExist"]}, "count": 0}, + {"filter": {"agentFilter": [500]}, "count": 0}, { "filter": { "timeFilter": 35, "userFilter": ["james", "jim"], - "agentFilter": ["AgentHostname1", "AgentHostname2"], + "agentFilter": [ + data["agents"][1].id, + data["agents"][2].id, + ], }, "count": 40, }, {"filter": {"timeFilter": 35, "userFilter": ["james", "jim"]}, "count": 81}, {"filter": {"objectFilter": ["user"]}, "count": 26}, {"filter": {"actionFilter": ["login"]}, "count": 12}, - {"filter": {"clientFilter": [site.client.id]}, "count": 23}, + { + "filter": {"clientFilter": [data["site"].client.id]}, + "count": 23, + }, ] pagination = { @@ -137,45 +150,15 @@ def test_get_audit_logs(self): ) self.assertEqual(resp.status_code, 200) self.assertEqual( - len(resp.data["audit_logs"]), + len(resp.data["audit_logs"]), # type:ignore pagination["rowsPerPage"] if req["count"] > pagination["rowsPerPage"] else req["count"], ) - self.assertEqual(resp.data["total"], req["count"]) + self.assertEqual(resp.data["total"], req["count"]) # type:ignore self.check_not_authenticated("patch", url) - def test_options_filter(self): - url = "/logs/auditlogs/optionsfilter/" - - baker.make_recipe("agents.agent", hostname=seq("AgentHostname"), _quantity=5) - baker.make_recipe("agents.agent", hostname=seq("Server"), _quantity=3) - baker.make("accounts.User", username=seq("Username"), _quantity=7) - baker.make("accounts.User", username=seq("soemthing"), _quantity=3) - - data = [ - {"req": {"type": "agent", "pattern": "AgeNt"}, "count": 5}, - {"req": {"type": "agent", "pattern": "AgentHostname1"}, "count": 1}, - {"req": {"type": "agent", "pattern": "hasjhd"}, "count": 0}, - {"req": {"type": "user", "pattern": "UsEr"}, "count": 7}, - {"req": {"type": "user", "pattern": "UserName1"}, "count": 1}, - {"req": {"type": "user", "pattern": "dfdsadf"}, "count": 0}, - ] - - for req in data: - resp = self.client.post(url, req["req"], format="json") - self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), req["count"]) - - # test for invalid payload. needs to have either type: user or agent - invalid_data = {"type": "object", "pattern": "SomeString"} - - resp = self.client.post(url, invalid_data, format="json") - self.assertEqual(resp.status_code, 400) - - self.check_not_authenticated("post", url) - def test_get_pending_actions(self): url = "/logs/pendingactions/" agent1 = baker.make_recipe("agents.online_agent") @@ -270,3 +253,87 @@ def test_cancel_pending_action(self, nats_cmd): self.assertEqual(r.data, "error deleting sched task") # type: ignore self.check_not_authenticated("delete", url) + + def test_get_debug_log(self): + url = "/logs/debuglog/" + + # create data + agent = baker.make_recipe("agents.agent") + baker.make( + "logs.DebugLog", + log_level=cycle(["error", "info", "warning", "critical"]), + log_type="agent_issues", + agent=agent, + _quantity=4, + ) + + logs = baker.make( + "logs.DebugLog", + log_type="system_issues", + log_level=cycle(["error", "info", "warning", "critical"]), + _quantity=15, + ) + + # test agent filter + data = {"agentFilter": agent.id} + resp = self.client.patch(url, data, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.data), 4) # type: ignore + + # test log type filter and agent + data = {"agentFilter": agent.id, "logLevelFilter": "warning"} + resp = self.client.patch(url, data, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.data), 1) # type: ignore + + # test time filter with other + data = {"logTypeFilter": "system_issues", "logLevelFilter": "error"} + resp = self.client.patch(url, data, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.data), 4) # type: ignore + + self.check_not_authenticated("patch", url) + + +class TestLogTasks(TacticalTestCase): + def test_prune_debug_log(self): + from .models import DebugLog + from .tasks import prune_debug_log + + # setup data + debug_log = baker.make( + "logs.DebugLog", + _quantity=50, + ) + + days = 0 + for item in debug_log: # type:ignore + item.entry_time = djangotime.now() - djangotime.timedelta(days=days) + item.save() + days = days + 5 + + # delete AgentHistory older than 30 days + prune_debug_log(30) + + self.assertEqual(DebugLog.objects.count(), 6) + + def test_prune_audit_log(self): + from .models import AuditLog + from .tasks import prune_audit_log + + # setup data + audit_log = baker.make( + "logs.AuditLog", + _quantity=50, + ) + + days = 0 + for item in audit_log: # type:ignore + item.entry_time = djangotime.now() - djangotime.timedelta(days=days) + item.save() + days = days + 5 + + # delete AgentHistory older than 30 days + prune_audit_log(30) + + self.assertEqual(AuditLog.objects.count(), 6) diff --git a/api/tacticalrmm/logs/urls.py b/api/tacticalrmm/logs/urls.py index 3b34da5cc7..015d47c5c5 100644 --- a/api/tacticalrmm/logs/urls.py +++ b/api/tacticalrmm/logs/urls.py @@ -5,7 +5,5 @@ urlpatterns = [ path("pendingactions/", views.PendingActions.as_view()), path("auditlogs/", views.GetAuditLogs.as_view()), - path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()), - path("debuglog////", views.debug_log), - path("downloadlog/", views.download_log), + path("debuglog/", views.GetDebugLog.as_view()), ] diff --git a/api/tacticalrmm/logs/views.py b/api/tacticalrmm/logs/views.py index a85bb6017e..d474e7174f 100644 --- a/api/tacticalrmm/logs/views.py +++ b/api/tacticalrmm/logs/views.py @@ -1,28 +1,23 @@ import asyncio -import subprocess from datetime import datetime as dt -from django.conf import settings +from accounts.models import User +from accounts.serializers import UserSerializer +from agents.models import Agent +from agents.serializers import AgentHostnameSerializer from django.core.paginator import Paginator from django.db.models import Q -from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime from rest_framework import status -from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView - -from accounts.models import User -from accounts.serializers import UserSerializer -from agents.models import Agent -from agents.serializers import AgentHostnameSerializer from tacticalrmm.utils import notify_error -from .models import AuditLog, PendingAction +from .models import AuditLog, PendingAction, DebugLog from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms -from .serializers import AuditLogSerializer, PendingActionSerializer +from .serializers import AuditLogSerializer, DebugLogSerializer, PendingActionSerializer class GetAuditLogs(APIView): @@ -48,7 +43,7 @@ def patch(self, request): timeFilter = Q() if "agentFilter" in request.data: - agentFilter = Q(agent__in=request.data["agentFilter"]) + agentFilter = Q(agent_id__in=request.data["agentFilter"]) elif "clientFilter" in request.data: clients = Client.objects.filter( @@ -95,25 +90,6 @@ def patch(self, request): ) -class FilterOptionsAuditLog(APIView): - permission_classes = [IsAuthenticated, AuditLogPerms] - - def post(self, request): - if request.data["type"] == "agent": - agents = Agent.objects.filter(hostname__icontains=request.data["pattern"]) - return Response(AgentHostnameSerializer(agents, many=True).data) - - if request.data["type"] == "user": - users = User.objects.filter( - username__icontains=request.data["pattern"], - agent=None, - is_installer_user=False, - ) - return Response(UserSerializer(users, many=True).data) - - return Response("error", status=status.HTTP_400_BAD_REQUEST) - - class PendingActions(APIView): permission_classes = [IsAuthenticated, ManagePendingActionPerms] @@ -158,60 +134,28 @@ def delete(self, request): return Response(f"{action.agent.hostname}: {action.description} was cancelled") -@api_view() -@permission_classes([IsAuthenticated, DebugLogPerms]) -def debug_log(request, mode, hostname, order): - log_file = settings.LOG_CONFIG["handlers"][0]["sink"] - - agents = Agent.objects.prefetch_related("site").only("pk", "hostname") - agent_hostnames = AgentHostnameSerializer(agents, many=True) - - switch_mode = { - "info": "INFO", - "critical": "CRITICAL", - "error": "ERROR", - "warning": "WARNING", - } - level = switch_mode.get(mode, "INFO") - - if hostname == "all" and order == "latest": - cmd = f"grep -h {level} {log_file} | tac" - elif hostname == "all" and order == "oldest": - cmd = f"grep -h {level} {log_file}" - elif hostname != "all" and order == "latest": - cmd = f"grep {hostname} {log_file} | grep -h {level} | tac" - elif hostname != "all" and order == "oldest": - cmd = f"grep {hostname} {log_file} | grep -h {level}" - else: - return Response("error", status=status.HTTP_400_BAD_REQUEST) - - contents = subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - shell=True, - ) - - if not contents.stdout: - resp = f"No {mode} logs" - else: - resp = contents.stdout - - return Response({"log": resp, "agents": agent_hostnames.data}) - - -@api_view() -@permission_classes([IsAuthenticated, DebugLogPerms]) -def download_log(request): - log_file = settings.LOG_CONFIG["handlers"][0]["sink"] - if settings.DEBUG: - with open(log_file, "rb") as f: - response = HttpResponse(f.read(), content_type="text/plain") - response["Content-Disposition"] = "attachment; filename=debug.log" - return response - else: - response = HttpResponse() - response["Content-Disposition"] = "attachment; filename=debug.log" - response["X-Accel-Redirect"] = "/private/log/debug.log" - return response +class GetDebugLog(APIView): + permission_classes = [IsAuthenticated, DebugLogPerms] + + def patch(self, request): + + agentFilter = Q() + logTypeFilter = Q() + logLevelFilter = Q() + + if "logTypeFilter" in request.data: + logTypeFilter = Q(log_type=request.data["logTypeFilter"]) + + if "logLevelFilter" in request.data: + logLevelFilter = Q(log_level=request.data["logLevelFilter"]) + + if "agentFilter" in request.data: + agentFilter = Q(agent=request.data["agentFilter"]) + + debug_logs = ( + DebugLog.objects.filter(logLevelFilter) + .filter(agentFilter) + .filter(logTypeFilter) + ) + + return Response(DebugLogSerializer(debug_logs, many=True).data) diff --git a/api/tacticalrmm/requirements.txt b/api/tacticalrmm/requirements.txt index 5bbb65a04e..6549206207 100644 --- a/api/tacticalrmm/requirements.txt +++ b/api/tacticalrmm/requirements.txt @@ -1,21 +1,22 @@ -asgiref==3.3.4 +asgiref==3.4.1 asyncio-nats-client==0.11.4 -celery==5.1.1 +celery==5.1.2 certifi==2021.5.30 -cffi==1.14.5 -channels==3.0.3 -channels_redis==3.2.0 +cffi==1.14.6 +channels==3.0.4 +channels_redis==3.3.0 chardet==4.0.0 -cryptography==3.4.7 +cryptography==3.4.8 daphne==3.0.2 -Django==3.2.4 -django-cors-headers==3.7.0 +Django==3.2.6 +django-cors-headers==3.8.0 +django-ipware==3.0.2 django-rest-knox==4.1.0 djangorestframework==3.12.4 future==0.18.2 loguru==0.5.3 msgpack==1.0.2 -packaging==20.9 +packaging==21.0 psycopg2-binary==2.9.1 pycparser==2.20 pycryptodome==3.10.1 @@ -24,13 +25,13 @@ pyparsing==2.4.7 pytz==2021.1 qrcode==6.1 redis==3.5.3 -requests==2.25.1 +requests==2.26.0 six==1.16.0 sqlparse==0.4.1 -twilio==6.60.0 -urllib3==1.26.5 +twilio==6.63.1 +urllib3==1.26.6 uWSGI==2.0.19.1 validators==0.18.2 vine==5.0.0 websockets==9.1 -zipp==3.4.1 +zipp==3.5.0 diff --git a/api/tacticalrmm/scripts/community_scripts.json b/api/tacticalrmm/scripts/community_scripts.json index af3d796cb5..dafecde9f0 100644 --- a/api/tacticalrmm/scripts/community_scripts.json +++ b/api/tacticalrmm/scripts/community_scripts.json @@ -175,11 +175,29 @@ "name": "Screenconnect - Get GUID for client", "description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use. ", "args": [ - "-serviceName {{client.ScreenConnectService}}" + "{{client.ScreenConnectService}}" ], "shell": "powershell", "category": "TRMM (Win):Collectors" }, + { + "guid": "9cfdfe8f-82bf-4081-a59f-576d694f4649", + "filename": "Win_Teamviewer_Get_ID.ps1", + "submittedBy": "https://github.com/silversword411", + "name": "TeamViewer - Get ClientID for client", + "description": "Returns Teamviwer ClientID for client - Use with Custom Fields for later use. ", + "shell": "powershell", + "category": "TRMM (Win):Collectors" + }, + { + "guid": "e43081d4-6f71-4ce3-881a-22da749f7a57", + "filename": "Win_AnyDesk_Get_Anynet_ID.ps1", + "submittedBy": "https://github.com/meuchels", + "name": "AnyDesk - Get AnyNetID for client", + "description": "Returns AnyNetID for client - Use with Custom Fields for later use. ", + "shell": "powershell", + "category": "TRMM (Win):Collectors" + }, { "guid": "95a2ee6f-b89b-4551-856e-3081b041caa7", "filename": "Win_Power_Profile_Reset_High_Performance_to_Defaults.ps1", @@ -226,6 +244,30 @@ "shell": "powershell", "category": "TRMM (Win):3rd Party Software" }, + { + "guid": "907652a5-9ec1-4759-9871-a7743f805ff2", + "filename": "Win_Software_Uninstall.ps1", + "submittedBy": "https://github.com/subzdev", + "name": "Software Uninstaller - list, find, and uninstall most software", + "description": "Allows listing, finding and uninstalling most software on Windows. There will be a best effort to uninstall silently if the silent uninstall string is not provided.", + "shell": "powershell", + "category": "TRMM (Win):3rd Party Software", + "default_timeout": "600" + }, + { + "guid": "64c3b1a8-c85f-4800-85a3-485f78a2d9ad", + "filename": "Win_Bitdefender_GravityZone_Install.ps1", + "submittedBy": "https://github.com/jhtechIL/", + "name": "BitDefender Gravity Zone Install", + "description": "Installs BitDefender Gravity Zone, requires client custom field setup. See script comments for details", + "args": [ + "-url {{client.bdurl}}", + "-exe {{client.bdexe}}" + ], + "default_timeout": "2500", + "shell": "powershell", + "category": "TRMM (Win):3rd Party Software" + }, { "guid": "da51111c-aff6-4d87-9d76-0608e1f67fe5", "filename": "Win_Defender_Enable.ps1", @@ -254,6 +296,16 @@ "shell": "cmd", "category": "TRMM (Win):Windows Features" }, + { + "guid": "0afd8d00-b95b-4318-8d07-0b9bc4424287", + "filename": "Win_Feature_NET35_Enable.ps1", + "submittedBy": "https://github.com/silversword411", + "name": "Windows Feature - Enable .NET 3.5", + "description": "Enables the Windows .NET 3.5 Framework in Turn Features on and off", + "shell": "powershell", + "default_timeout": "300", + "category": "TRMM (Win):Windows Features" + }, { "guid": "24f19ead-fdfe-46b4-9dcb-4cd0e12a3940", "filename": "Win_Speedtest.ps1", @@ -368,14 +420,14 @@ "category": "TRMM (Win):Other" }, { - "guid": "5615aa90-0272-427b-8acf-0ca019612501", - "filename": "Win_Chocolatey_Update_Installed.bat", + "guid": "6c78eb04-57ae-43b0-98ed-cbd3ef9e2f80", + "filename": "Win_Chocolatey_Manage_Apps_Bulk.ps1", "submittedBy": "https://github.com/silversword411", - "name": "Update Installed Apps", - "description": "Update all apps that were installed using Chocolatey.", - "shell": "cmd", + "name": "Chocolatey - Install, Uninstall and Upgrade Software", + "description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x", + "shell": "powershell", "category": "TRMM (Win):3rd Party Software>Chocolatey", - "default_timeout": "3600" + "default_timeout": "600" }, { "guid": "fff8024d-d72e-4457-84fa-6c780f69a16f", @@ -450,6 +502,16 @@ "shell": "powershell", "category": "TRMM (Win):Updates" }, + { + "guid": "93038ae0-58ce-433e-a3b9-bc99ad1ea79a", + "filename": "Win_Services_AutomaticStartup_Running.ps1", + "submittedBy": "https://github.com/silversword411", + "name": "Ensure all services with startup type Automatic are running", + "description": "Gets a list of all service with startup type of Automatic but aren't running and tries to start them", + "shell": "powershell", + "default_timeout": "300", + "category": "TRMM (Win):Updates" + }, { "guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8", "filename": "Win_ScreenConnectAIO.ps1", @@ -507,6 +569,16 @@ "category": "TRMM (Win):Network", "default_timeout": "90" }, + { + "guid": "7c0c7e37-60ff-462f-9c34-b5cd4c4796a7", + "filename": "Win_Wifi_SSID_and_Password_Retrieval.ps1", + "submittedBy": "https://github.com/silversword411", + "name": "Network Wireless - Retrieve Saved passwords", + "description": "Returns all saved wifi passwords stored on the computer", + "shell": "powershell", + "category": "TRMM (Win):Network", + "default_timeout": "90" + }, { "guid": "abe78170-7cf9-435b-9666-c5ef6c11a106", "filename": "Win_Network_IPv6_Disable.ps1", @@ -527,6 +599,16 @@ "category": "TRMM (Win):Network", "default_timeout": "90" }, + { + "guid": "5676acca-44e5-46c8-af61-ae795ecb3ef1", + "filename": "Win_Network_IP_DHCP_Renew.bat", + "submittedBy": "https://github.com/silversword411", + "name": "Network - Release and Renew IP", + "description": "Trigger and release and renew of IP address on all network adapters", + "shell": "cmd", + "category": "TRMM (Win):Network", + "default_timeout": "90" + }, { "guid": "83aa4d51-63ce-41e7-829f-3c16e6115bbf", "filename": "Win_Network_DNS_Set_to_1.1.1.2.ps1", @@ -557,6 +639,16 @@ "category": "TRMM (Win):Other", "default_timeout": "90" }, + { + "guid": "43e65e5f-717a-4b6d-a724-1a86229fcd42", + "filename": "Win_Activation_Check.ps1", + "submittedBy": "https://github.com/dinger1986", + "name": "Windows Activation check", + "description": "Checks to see if windows is activated and returns status", + "shell": "powershell", + "category": "TRMM (Win):Other", + "default_timeout": "120" + }, { "guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf", "filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1", diff --git a/api/tacticalrmm/scripts/migrations/0009_scriptsnippet.py b/api/tacticalrmm/scripts/migrations/0009_scriptsnippet.py new file mode 100644 index 0000000000..d4ca4f768b --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0009_scriptsnippet.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.1 on 2021-07-21 19:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0008_script_guid'), + ] + + operations = [ + migrations.CreateModel( + name='ScriptSnippet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('code', models.TextField()), + ('shell', models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python')], max_length=15)), + ], + ), + ] diff --git a/api/tacticalrmm/scripts/migrations/0010_auto_20210726_1634.py b/api/tacticalrmm/scripts/migrations/0010_auto_20210726_1634.py new file mode 100644 index 0000000000..91e5581db1 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0010_auto_20210726_1634.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.1 on 2021-07-26 16:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0009_scriptsnippet'), + ] + + operations = [ + migrations.AddField( + model_name='scriptsnippet', + name='desc', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name='script', + name='code_base64', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AlterField( + model_name='script', + name='description', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AlterField( + model_name='scriptsnippet', + name='name', + field=models.CharField(max_length=40, unique=True), + ), + ] diff --git a/api/tacticalrmm/scripts/migrations/0011_auto_20210731_1707.py b/api/tacticalrmm/scripts/migrations/0011_auto_20210731_1707.py new file mode 100644 index 0000000000..67f9d787ba --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0011_auto_20210731_1707.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.1 on 2021-07-31 17:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0010_auto_20210726_1634'), + ] + + operations = [ + migrations.AlterField( + model_name='scriptsnippet', + name='code', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='scriptsnippet', + name='desc', + field=models.CharField(blank=True, default='', max_length=50), + ), + migrations.AlterField( + model_name='scriptsnippet', + name='shell', + field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python')], default='powershell', max_length=15), + ), + ] diff --git a/api/tacticalrmm/scripts/models.py b/api/tacticalrmm/scripts/models.py index d06918a52c..fc68c4013c 100644 --- a/api/tacticalrmm/scripts/models.py +++ b/api/tacticalrmm/scripts/models.py @@ -1,12 +1,10 @@ import base64 import re -from typing import List, Optional +from typing import List -from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.db import models -from loguru import logger - +from django.db.models.fields import CharField, TextField from logs.models import BaseAuditModel from tacticalrmm.utils import replace_db_values @@ -21,13 +19,11 @@ ("builtin", "Built In"), ] -logger.configure(**settings.LOG_CONFIG) - class Script(BaseAuditModel): - guid = name = models.CharField(max_length=64, null=True, blank=True) + guid = models.CharField(max_length=64, null=True, blank=True) name = models.CharField(max_length=255) - description = models.TextField(null=True, blank=True) + description = models.TextField(null=True, blank=True, default="") filename = models.CharField(max_length=255) # deprecated shell = models.CharField( max_length=100, choices=SCRIPT_SHELLS, default="powershell" @@ -43,20 +39,44 @@ class Script(BaseAuditModel): ) favorite = models.BooleanField(default=False) category = models.CharField(max_length=100, null=True, blank=True) - code_base64 = models.TextField(null=True, blank=True) + code_base64 = models.TextField(null=True, blank=True, default="") default_timeout = models.PositiveIntegerField(default=90) def __str__(self): return self.name @property - def code(self): + def code_no_snippets(self): if self.code_base64: - base64_bytes = self.code_base64.encode("ascii", "ignore") - return base64.b64decode(base64_bytes).decode("ascii", "ignore") + return base64.b64decode(self.code_base64.encode("ascii", "ignore")).decode( + "ascii", "ignore" + ) else: return "" + @property + def code(self): + return self.replace_with_snippets(self.code_no_snippets) + + @classmethod + def replace_with_snippets(cls, code): + # check if snippet has been added to script body + matches = re.finditer(r"{{(.*)}}", code) + if matches: + replaced_code = code + for snippet in matches: + snippet_name = snippet.group(1).strip() + if ScriptSnippet.objects.filter(name=snippet_name).exists(): + value = ScriptSnippet.objects.get(name=snippet_name).code + else: + value = "" + + replaced_code = re.sub(snippet.group(), value, replaced_code) + + return replaced_code + else: + return code + @classmethod def load_community_scripts(cls): import json @@ -97,20 +117,20 @@ def load_community_scripts(cls): if s.exists(): i = s.first() - i.name = script["name"] - i.description = script["description"] - i.category = category - i.shell = script["shell"] - i.default_timeout = default_timeout - i.args = args + i.name = script["name"] # type: ignore + i.description = script["description"] # type: ignore + i.category = category # type: ignore + i.shell = script["shell"] # type: ignore + i.default_timeout = default_timeout # type: ignore + i.args = args # type: ignore with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: script_bytes = ( f.read().decode("utf-8").encode("ascii", "ignore") ) - i.code_base64 = base64.b64encode(script_bytes).decode("ascii") + i.code_base64 = base64.b64encode(script_bytes).decode("ascii") # type: ignore - i.save( + i.save( # type: ignore update_fields=[ "name", "description", @@ -175,7 +195,6 @@ def load_community_scripts(cls): guid=script["guid"], name=script["name"], description=script["description"], - filename=script["filename"], shell=script["shell"], script_type="builtin", category=category, @@ -209,7 +228,7 @@ def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list: if match: # only get the match between the () in regex string = match.group(1) - value = replace_db_values(string=string, agent=agent, shell=shell) + value = replace_db_values(string=string, instance=agent, shell=shell) if value: temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) @@ -221,3 +240,13 @@ def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list: temp_args.append(arg) return temp_args + + +class ScriptSnippet(models.Model): + name = CharField(max_length=40, unique=True) + desc = CharField(max_length=50, blank=True, default="") + code = TextField(default="") + shell = CharField(max_length=15, choices=SCRIPT_SHELLS, default="powershell") + + def __str__(self): + return self.name diff --git a/api/tacticalrmm/scripts/serializers.py b/api/tacticalrmm/scripts/serializers.py index 94f1137010..034a7665c7 100644 --- a/api/tacticalrmm/scripts/serializers.py +++ b/api/tacticalrmm/scripts/serializers.py @@ -1,6 +1,6 @@ from rest_framework.serializers import ModelSerializer, ReadOnlyField -from .models import Script +from .models import Script, ScriptSnippet class ScriptTableSerializer(ModelSerializer): @@ -41,3 +41,9 @@ class ScriptCheckSerializer(ModelSerializer): class Meta: model = Script fields = ["code", "shell"] + + +class ScriptSnippetSerializer(ModelSerializer): + class Meta: + model = ScriptSnippet + fields = "__all__" diff --git a/api/tacticalrmm/scripts/tasks.py b/api/tacticalrmm/scripts/tasks.py index ae952d516d..74b83bdb83 100644 --- a/api/tacticalrmm/scripts/tasks.py +++ b/api/tacticalrmm/scripts/tasks.py @@ -1,12 +1,16 @@ import asyncio -from agents.models import Agent +from packaging import version as pyver + +from agents.models import Agent, AgentHistory from scripts.models import Script from tacticalrmm.celery import app @app.task -def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None: +def handle_bulk_command_task( + agentpks, cmd, shell, timeout, username, run_on_offline=False +) -> None: nats_data = { "func": "rawcmd", "timeout": timeout, @@ -16,11 +20,31 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None: }, } for agent in Agent.objects.filter(pk__in=agentpks): + if pyver.parse(agent.version) >= pyver.parse("1.6.0"): + hist = AgentHistory.objects.create( + agent=agent, + type="cmd_run", + command=cmd, + username=username, + ) + nats_data["id"] = hist.pk + asyncio.run(agent.nats_cmd(nats_data, wait=False)) @app.task -def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None: +def handle_bulk_script_task(scriptpk, agentpks, args, timeout, username) -> None: script = Script.objects.get(pk=scriptpk) for agent in Agent.objects.filter(pk__in=agentpks): - agent.run_script(scriptpk=script.pk, args=args, timeout=timeout) + history_pk = 0 + if pyver.parse(agent.version) >= pyver.parse("1.6.0"): + hist = AgentHistory.objects.create( + agent=agent, + type="script_run", + script=script, + username=username, + ) + history_pk = hist.pk + agent.run_script( + scriptpk=script.pk, args=args, timeout=timeout, history_pk=history_pk + ) diff --git a/api/tacticalrmm/scripts/tests.py b/api/tacticalrmm/scripts/tests.py index 8105c11c2c..39281e979a 100644 --- a/api/tacticalrmm/scripts/tests.py +++ b/api/tacticalrmm/scripts/tests.py @@ -1,15 +1,18 @@ import json import os from pathlib import Path +from unittest.mock import patch from django.conf import settings -from django.core.files.uploadedfile import SimpleUploadedFile from model_bakery import baker - from tacticalrmm.test import TacticalTestCase -from .models import Script -from .serializers import ScriptSerializer, ScriptTableSerializer +from .models import Script, ScriptSnippet +from .serializers import ( + ScriptSerializer, + ScriptTableSerializer, + ScriptSnippetSerializer, +) class TestScriptViews(TacticalTestCase): @@ -18,7 +21,7 @@ def setUp(self): self.authenticate() def test_get_scripts(self): - url = "/scripts/scripts/" + url = "/scripts/" scripts = baker.make("scripts.Script", _quantity=3) serializer = ScriptTableSerializer(scripts, many=True) @@ -29,14 +32,14 @@ def test_get_scripts(self): self.check_not_authenticated("get", url) def test_add_script(self): - url = f"/scripts/scripts/" + url = f"/scripts/" data = { "name": "Name", "description": "Description", "shell": "powershell", "category": "New", - "code": "Some Test Code\nnew Line", + "code_base64": "VGVzdA==", # Test "default_timeout": 99, "args": ["hello", "world", r"{{agent.public_ip}}"], "favorite": False, @@ -46,47 +49,24 @@ def test_add_script(self): resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertTrue(Script.objects.filter(name="Name").exists()) - self.assertEqual(Script.objects.get(name="Name").code, data["code"]) - - # test with file upload - # file with 'Test' as content - file = SimpleUploadedFile( - "test_script.bat", b"\x54\x65\x73\x74", content_type="text/plain" - ) - data = { - "name": "New Name", - "description": "Description", - "shell": "cmd", - "category": "New", - "filename": file, - "default_timeout": 4455, - "args": json.dumps( - ["hello", "world", r"{{agent.public_ip}}"] - ), # simulate javascript's JSON.stringify() for formData - } - - # test with file upload - resp = self.client.post(url, data, format="multipart") - self.assertEqual(resp.status_code, 200) - script = Script.objects.filter(name="New Name").first() - self.assertEquals(script.code, "Test") + self.assertEqual(Script.objects.get(name="Name").code, "Test") self.check_not_authenticated("post", url) def test_modify_script(self): # test a call where script doesn't exist - resp = self.client.put("/scripts/500/script/", format="json") + resp = self.client.put("/scripts/500/", format="json") self.assertEqual(resp.status_code, 404) # make a userdefined script script = baker.make_recipe("scripts.script") - url = f"/scripts/{script.pk}/script/" + url = f"/scripts/{script.pk}/" data = { "name": script.name, "description": "Description Change", "shell": script.shell, - "code": "Test Code\nAnother Line", + "code_base64": "VGVzdA==", # Test "default_timeout": 13344556, } @@ -95,16 +75,18 @@ def test_modify_script(self): self.assertEqual(resp.status_code, 200) script = Script.objects.get(pk=script.pk) self.assertEquals(script.description, "Description Change") - self.assertEquals(script.code, "Test Code\nAnother Line") + self.assertEquals(script.code, "Test") # test edit a builtin script - data = {"name": "New Name", "description": "New Desc", "code": "Some New Code"} + data = { + "name": "New Name", + "description": "New Desc", + "code_base64": "VGVzdA==", + } # Test builtin_script = baker.make_recipe("scripts.script", script_type="builtin") - resp = self.client.put( - f"/scripts/{builtin_script.pk}/script/", data, format="json" - ) + resp = self.client.put(f"/scripts/{builtin_script.pk}/", data, format="json") self.assertEqual(resp.status_code, 400) data = { @@ -112,13 +94,11 @@ def test_modify_script(self): "description": "Description Change", "shell": script.shell, "favorite": True, - "code": "Test Code\nAnother Line", + "code_base64": "VGVzdA==", # Test "default_timeout": 54345, } # test marking a builtin script as favorite - resp = self.client.put( - f"/scripts/{builtin_script.pk}/script/", data, format="json" - ) + resp = self.client.put(f"/scripts/{builtin_script.pk}/", data, format="json") self.assertEqual(resp.status_code, 200) self.assertTrue(Script.objects.get(pk=builtin_script.pk).favorite) @@ -126,11 +106,11 @@ def test_modify_script(self): def test_get_script(self): # test a call where script doesn't exist - resp = self.client.get("/scripts/500/script/", format="json") + resp = self.client.get("/scripts/500/", format="json") self.assertEqual(resp.status_code, 404) script = baker.make("scripts.Script") - url = f"/scripts/{script.pk}/script/" # type: ignore + url = f"/scripts/{script.pk}/" # type: ignore serializer = ScriptSerializer(script) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) @@ -138,14 +118,34 @@ def test_get_script(self): self.check_not_authenticated("get", url) + @patch("agents.models.Agent.nats_cmd") + def test_test_script(self, run_script): + url = "/scripts/testscript/" + + run_script.return_value = "return value" + agent = baker.make_recipe("agents.agent") + data = { + "agent": agent.pk, + "code": "some_code", + "timeout": 90, + "args": [], + "shell": "powershell", + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data, "return value") # type: ignore + + self.check_not_authenticated("post", url) + def test_delete_script(self): # test a call where script doesn't exist - resp = self.client.delete("/scripts/500/script/", format="json") + resp = self.client.delete("/scripts/500/", format="json") self.assertEqual(resp.status_code, 404) # test delete script script = baker.make_recipe("scripts.script") - url = f"/scripts/{script.pk}/script/" + url = f"/scripts/{script.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) @@ -153,7 +153,7 @@ def test_delete_script(self): # test delete community script script = baker.make_recipe("scripts.script", script_type="builtin") - url = f"/scripts/{script.pk}/script/" + url = f"/scripts/{script.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 400) @@ -161,7 +161,7 @@ def test_delete_script(self): def test_download_script(self): # test a call where script doesn't exist - resp = self.client.get("/scripts/500/download/", format="json") + resp = self.client.get("/scripts/download/500/", format="json") self.assertEqual(resp.status_code, 404) # return script code property should be "Test" @@ -170,7 +170,7 @@ def test_download_script(self): script = baker.make( "scripts.Script", code_base64="VGVzdA==", shell="powershell" ) - url = f"/scripts/{script.pk}/download/" # type: ignore + url = f"/scripts/download/{script.pk}/" # type: ignore resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) @@ -178,7 +178,7 @@ def test_download_script(self): # test batch file script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd") - url = f"/scripts/{script.pk}/download/" # type: ignore + url = f"/scripts/download/{script.pk}/" # type: ignore resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) @@ -186,7 +186,7 @@ def test_download_script(self): # test python file script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python") - url = f"/scripts/{script.pk}/download/" # type: ignore + url = f"/scripts/download/{script.pk}/" # type: ignore resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) @@ -497,3 +497,106 @@ def test_script_arg_replacement_boolean_fields(self): ["-Parameter", "-Another $True"], Script.parse_script_args(agent=agent, shell="powershell", args=args), ) + + +class TestScriptSnippetViews(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + self.authenticate() + + def test_get_script_snippets(self): + url = "/scripts/snippets/" + snippets = baker.make("scripts.ScriptSnippet", _quantity=3) + + serializer = ScriptSnippetSerializer(snippets, many=True) + resp = self.client.get(url, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(serializer.data, resp.data) # type: ignore + + self.check_not_authenticated("get", url) + + def test_add_script_snippet(self): + url = f"/scripts/snippets/" + + data = { + "name": "Name", + "description": "Description", + "shell": "powershell", + "code": "Test", + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + self.assertTrue(ScriptSnippet.objects.filter(name="Name").exists()) + + self.check_not_authenticated("post", url) + + def test_modify_script_snippet(self): + # test a call where script doesn't exist + resp = self.client.put("/scripts/snippets/500/", format="json") + self.assertEqual(resp.status_code, 404) + + # make a userdefined script + snippet = baker.make("scripts.ScriptSnippet", name="Test") + url = f"/scripts/snippets/{snippet.pk}/" # type: ignore + + data = {"name": "New Name"} # type: ignore + + resp = self.client.put(url, data, format="json") + self.assertEqual(resp.status_code, 200) + snippet = ScriptSnippet.objects.get(pk=snippet.pk) # type: ignore + self.assertEquals(snippet.name, "New Name") + + self.check_not_authenticated("put", url) + + def test_get_script_snippet(self): + # test a call where script doesn't exist + resp = self.client.get("/scripts/snippets/500/", format="json") + self.assertEqual(resp.status_code, 404) + + snippet = baker.make("scripts.ScriptSnippet") + url = f"/scripts/snippets/{snippet.pk}/" # type: ignore + serializer = ScriptSnippetSerializer(snippet) + resp = self.client.get(url, format="json") + self.assertEqual(resp.status_code, 200) + self.assertEqual(serializer.data, resp.data) # type: ignore + + self.check_not_authenticated("get", url) + + def test_delete_script_snippet(self): + # test a call where script doesn't exist + resp = self.client.delete("/scripts/snippets/500/", format="json") + self.assertEqual(resp.status_code, 404) + + # test delete script snippet + snippet = baker.make("scripts.ScriptSnippet") + url = f"/scripts/snippets/{snippet.pk}/" # type: ignore + resp = self.client.delete(url, format="json") + self.assertEqual(resp.status_code, 200) + + self.assertFalse(ScriptSnippet.objects.filter(pk=snippet.pk).exists()) # type: ignore + + self.check_not_authenticated("delete", url) + + def test_snippet_replacement(self): + + snippet1 = baker.make( + "scripts.ScriptSnippet", name="snippet1", code="Snippet 1 Code" + ) + snippet2 = baker.make( + "scripts.ScriptSnippet", name="snippet2", code="Snippet 2 Code" + ) + + test_no_snippet = "No Snippets Here" + test_with_snippet = "Snippet 1: {{snippet1}}\nSnippet 2: {{snippet2}}" + + # test putting snippet in text + result = Script.replace_with_snippets(test_with_snippet) + self.assertEqual( + result, + f"Snippet 1: {snippet1.code}\nSnippet 2: {snippet2.code}", # type:ignore + ) + + # test text with no snippets + result = Script.replace_with_snippets(test_no_snippet) + self.assertEqual(result, test_no_snippet) diff --git a/api/tacticalrmm/scripts/urls.py b/api/tacticalrmm/scripts/urls.py index 08900ca36d..d418e3f980 100644 --- a/api/tacticalrmm/scripts/urls.py +++ b/api/tacticalrmm/scripts/urls.py @@ -3,7 +3,10 @@ from . import views urlpatterns = [ - path("scripts/", views.GetAddScripts.as_view()), - path("/script/", views.GetUpdateDeleteScript.as_view()), - path("/download/", views.download), + path("", views.GetAddScripts.as_view()), + path("/", views.GetUpdateDeleteScript.as_view()), + path("snippets/", views.GetAddScriptSnippets.as_view()), + path("snippets//", views.GetUpdateDeleteScriptSnippet.as_view()), + path("testscript/", views.TestScript.as_view()), + path("download//", views.download), ] diff --git a/api/tacticalrmm/scripts/views.py b/api/tacticalrmm/scripts/views.py index 1771f58786..48af30ad28 100644 --- a/api/tacticalrmm/scripts/views.py +++ b/api/tacticalrmm/scripts/views.py @@ -1,64 +1,39 @@ import base64 -import json +import asyncio -from django.conf import settings from django.shortcuts import get_object_or_404 -from loguru import logger from rest_framework.decorators import api_view, permission_classes -from rest_framework.parsers import FileUploadParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView - from tacticalrmm.utils import notify_error -from .models import Script +from .models import Script, ScriptSnippet from .permissions import ManageScriptsPerms -from .serializers import ScriptSerializer, ScriptTableSerializer - -logger.configure(**settings.LOG_CONFIG) +from agents.permissions import RunScriptPerms +from .serializers import ( + ScriptSerializer, + ScriptTableSerializer, + ScriptSnippetSerializer, +) class GetAddScripts(APIView): permission_classes = [IsAuthenticated, ManageScriptsPerms] - parser_class = (FileUploadParser,) def get(self, request): - scripts = Script.objects.all() - return Response(ScriptTableSerializer(scripts, many=True).data) - - def post(self, request, format=None): - data = { - "name": request.data["name"], - "category": request.data["category"], - "description": request.data["description"], - "shell": request.data["shell"], - "default_timeout": request.data["default_timeout"], - "script_type": "userdefined", # force all uploads to be userdefined. built in scripts cannot be edited by user - } - # code editor upload - if "args" in request.data.keys() and isinstance(request.data["args"], list): - data["args"] = request.data["args"] + showCommunityScripts = request.GET.get("showCommunityScripts", True) + if not showCommunityScripts or showCommunityScripts == "false": + scripts = Script.objects.filter(script_type="userdefined") + else: + scripts = Script.objects.all() - # file upload, have to json load it cuz it's formData - if "args" in request.data.keys() and "file_upload" in request.data.keys(): - data["args"] = json.loads(request.data["args"]) - - if "favorite" in request.data.keys(): - data["favorite"] = request.data["favorite"] - - if "filename" in request.data.keys(): - message_bytes = request.data["filename"].read() - data["code_base64"] = base64.b64encode(message_bytes).decode( - "ascii", "ignore" - ) + return Response(ScriptTableSerializer(scripts, many=True).data) - elif "code" in request.data.keys(): - message_bytes = request.data["code"].encode("ascii", "ignore") - data["code_base64"] = base64.b64encode(message_bytes).decode("ascii") + def post(self, request): - serializer = ScriptSerializer(data=data, partial=True) + serializer = ScriptSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) obj = serializer.save() @@ -85,11 +60,6 @@ def put(self, request, pk): else: return notify_error("Community scripts cannot be edited.") - elif "code" in data: - message_bytes = data["code"].encode("ascii") - data["code_base64"] = base64.b64encode(message_bytes).decode("ascii") - data.pop("code") - serializer = ScriptSerializer(data=data, instance=script, partial=True) serializer.is_valid(raise_exception=True) obj = serializer.save() @@ -107,11 +77,87 @@ def delete(self, request, pk): return Response(f"{script.name} was deleted!") +class GetAddScriptSnippets(APIView): + permission_classes = [IsAuthenticated, ManageScriptsPerms] + + def get(self, request): + snippets = ScriptSnippet.objects.all() + return Response(ScriptSnippetSerializer(snippets, many=True).data) + + def post(self, request): + + serializer = ScriptSnippetSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response("Script snippet was saved successfully") + + +class GetUpdateDeleteScriptSnippet(APIView): + permission_classes = [IsAuthenticated, ManageScriptsPerms] + + def get(self, request, pk): + snippet = get_object_or_404(ScriptSnippet, pk=pk) + return Response(ScriptSnippetSerializer(snippet).data) + + def put(self, request, pk): + snippet = get_object_or_404(ScriptSnippet, pk=pk) + + serializer = ScriptSnippetSerializer( + instance=snippet, data=request.data, partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response("Script snippet was saved successfully") + + def delete(self, request, pk): + snippet = get_object_or_404(ScriptSnippet, pk=pk) + snippet.delete() + + return Response("Script snippet was deleted successfully") + + +class TestScript(APIView): + permission_classes = [IsAuthenticated, RunScriptPerms] + + def post(self, request): + from .models import Script + from agents.models import Agent + + agent = get_object_or_404(Agent, pk=request.data["agent"]) + + parsed_args = Script.parse_script_args( + self, request.data["shell"], request.data["args"] + ) + + data = { + "func": "runscript", + "timeout": request.data["timeout"], + "script_args": parsed_args, + "payload": { + "code": Script.replace_with_snippets(request.data["code"]), + "shell": request.data["shell"], + }, + } + + r = asyncio.run( + agent.nats_cmd(data, timeout=request.data["timeout"], wait=True) + ) + + return Response(r) + + @api_view() @permission_classes([IsAuthenticated, ManageScriptsPerms]) def download(request, pk): script = get_object_or_404(Script, pk=pk) + with_snippets = request.GET.get("with_snippets", True) + + if with_snippets == "false": + with_snippets = False + if script.shell == "powershell": filename = f"{script.name}.ps1" elif script.shell == "cmd": @@ -119,4 +165,9 @@ def download(request, pk): else: filename = f"{script.name}.py" - return Response({"filename": filename, "code": script.code}) + return Response( + { + "filename": filename, + "code": script.code if with_snippets else script.code_no_snippets, + } + ) diff --git a/api/tacticalrmm/services/views.py b/api/tacticalrmm/services/views.py index 030391f30c..b1a5a6d647 100644 --- a/api/tacticalrmm/services/views.py +++ b/api/tacticalrmm/services/views.py @@ -1,21 +1,16 @@ import asyncio -from django.conf import settings +from agents.models import Agent +from checks.models import Check from django.shortcuts import get_object_or_404 -from loguru import logger from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response - -from agents.models import Agent -from checks.models import Check from tacticalrmm.utils import notify_error from .permissions import ManageWinSvcsPerms from .serializers import ServicesSerializer -logger.configure(**settings.LOG_CONFIG) - @api_view() def get_services(request, pk): diff --git a/api/tacticalrmm/tacticalrmm/celery.py b/api/tacticalrmm/tacticalrmm/celery.py index 75cee9058d..e9fe2cd062 100644 --- a/api/tacticalrmm/tacticalrmm/celery.py +++ b/api/tacticalrmm/tacticalrmm/celery.py @@ -35,9 +35,13 @@ "task": "agents.tasks.auto_self_agent_update_task", "schedule": crontab(minute=35, hour="*"), }, - "monitor-agents": { - "task": "agents.tasks.monitor_agents_task", - "schedule": crontab(minute="*/7"), + "handle-agents": { + "task": "agents.tasks.handle_agents_task", + "schedule": crontab(minute="*"), + }, + "get-agentinfo": { + "task": "agents.tasks.agent_getinfo_task", + "schedule": crontab(minute="*"), }, "get-wmi": { "task": "agents.tasks.get_wmi_task", diff --git a/api/tacticalrmm/tacticalrmm/middleware.py b/api/tacticalrmm/tacticalrmm/middleware.py index f4ac208d43..5f7c0091fb 100644 --- a/api/tacticalrmm/tacticalrmm/middleware.py +++ b/api/tacticalrmm/tacticalrmm/middleware.py @@ -2,6 +2,7 @@ from django.conf import settings from rest_framework.exceptions import AuthenticationFailed +from ipware import get_client_ip request_local = threading.local() @@ -67,6 +68,7 @@ def process_view(self, request, view_func, view_args, view_kwargs): debug_info["view_func"] = view_func.__name__ debug_info["view_args"] = view_args debug_info["view_kwargs"] = view_kwargs + debug_info["ip"] = request._client_ip request_local.debug_info = debug_info @@ -83,3 +85,15 @@ def process_template_response(self, request, response): request_local.debug_info = None request_local.username = None return response + + +class LogIPMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + client_ip, is_routable = get_client_ip(request) + + request._client_ip = client_ip + response = self.get_response(request) + return response diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index 9b9f15e6a1..244693cff7 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -15,23 +15,25 @@ AUTH_USER_MODEL = "accounts.User" # latest release -TRMM_VERSION = "0.7.2" +TRMM_VERSION = "0.8.0" # bump this version everytime vue code is changed # to alert user they need to manually refresh their browser -APP_VER = "0.0.141" +APP_VER = "0.0.142" # https://github.com/wh1te909/rmmagent -LATEST_AGENT_VER = "1.5.9" +LATEST_AGENT_VER = "1.6.0" -MESH_VER = "0.8.60" +MESH_VER = "0.9.15" + +NATS_SERVER_VER = "2.3.3" # for the update script, bump when need to recreate venv or npm install -PIP_VER = "19" -NPM_VER = "19" +PIP_VER = "21" +NPM_VER = "21" -SETUPTOOLS_VER = "57.0.0" -WHEEL_VER = "0.36.2" +SETUPTOOLS_VER = "57.4.0" +WHEEL_VER = "0.37.0" 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" @@ -109,6 +111,7 @@ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "corsheaders.middleware.CorsMiddleware", ## + "tacticalrmm.middleware.LogIPMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -173,12 +176,23 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static") STATICFILES_DIRS = [os.path.join(BASE_DIR, "tacticalrmm/static/")] - -LOG_CONFIG = { - "handlers": [{"sink": os.path.join(LOG_DIR, "debug.log"), "serialize": False}] +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "file": { + "level": "ERROR", + "class": "logging.FileHandler", + "filename": os.path.join(LOG_DIR, "django_debug.log"), + } + }, + "loggers": { + "django.request": {"handlers": ["file"], "level": "ERROR", "propagate": True} + }, } if "AZPIPELINE" in os.environ: + print("PIPELINE") DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", diff --git a/api/tacticalrmm/tacticalrmm/tests.py b/api/tacticalrmm/tacticalrmm/tests.py index cb7b0c20cd..8a24481d91 100644 --- a/api/tacticalrmm/tacticalrmm/tests.py +++ b/api/tacticalrmm/tacticalrmm/tests.py @@ -4,7 +4,8 @@ import requests from django.conf import settings -from django.test import TestCase, override_settings +from django.test import override_settings +from tacticalrmm.test import TacticalTestCase from .utils import ( bitdays_to_string, @@ -16,7 +17,10 @@ ) -class TestUtils(TestCase): +class TestUtils(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + @patch("requests.post") @patch("__main__.__builtins__.open", new_callable=mock_open) def test_generate_winagent_exe_success(self, m_open, mock_post): @@ -77,7 +81,7 @@ def test_reload_nats(self, mock_subprocess): @patch("subprocess.run") def test_run_nats_api_cmd(self, mock_subprocess): ids = ["a", "b", "c"] - _ = run_nats_api_cmd("monitor", ids) + _ = run_nats_api_cmd("wmi", ids) mock_subprocess.assert_called_once() def test_bitdays_to_string(self): diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index bd898701e0..6969e0791d 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -15,14 +15,12 @@ from django.contrib.auth.models import AnonymousUser from django.http import FileResponse from knox.auth import TokenAuthentication -from loguru import logger from rest_framework import status from rest_framework.response import Response -from agents.models import Agent from core.models import CodeSignToken - -logger.configure(**settings.LOG_CONFIG) +from logs.models import DebugLog +from agents.models import Agent notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) @@ -61,7 +59,7 @@ def generate_winagent_exe( ) try: - codetoken = CodeSignToken.objects.first().token + codetoken = CodeSignToken.objects.first().token # type:ignore base_url = get_exegen_url() + "/api/v1/winagents/?" params = { "version": settings.LATEST_AGENT_VER, @@ -107,7 +105,7 @@ def generate_winagent_exe( break if errors: - logger.error(errors) + DebugLog.error(message=errors) return notify_error( "Something went wrong. Check debug error log for exact error message" ) @@ -123,7 +121,7 @@ def generate_winagent_exe( def get_default_timezone(): from core.models import CoreSettings - return pytz.timezone(CoreSettings.objects.first().default_time_zone) + return pytz.timezone(CoreSettings.objects.first().default_time_zone) # type:ignore def get_bit_days(days: list[str]) -> int: @@ -178,28 +176,28 @@ def filter_software(sw: SoftwareList) -> SoftwareList: def reload_nats(): users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}] - agents = Agent.objects.prefetch_related("user").only("pk", "agent_id") + agents = Agent.objects.prefetch_related("user").only( + "pk", "agent_id" + ) # type:ignore for agent in agents: try: users.append( {"user": agent.agent_id, "password": agent.user.auth_token.key} ) except: - logger.critical( - f"{agent.hostname} does not have a user account, NATS will not work" + DebugLog.critical( + agent=agent, + log_type="agent_issues", + message=f"{agent.hostname} does not have a user account, NATS will not work", ) domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] + cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" if hasattr(settings, "CERT_FILE") and hasattr(settings, "KEY_FILE"): if os.path.exists(settings.CERT_FILE) and os.path.exists(settings.KEY_FILE): cert_file = settings.CERT_FILE key_file = settings.KEY_FILE - else: - cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" - key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" - else: - cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" - key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" config = { "tls": { @@ -207,7 +205,7 @@ def reload_nats(): "key_file": key_file, }, "authorization": {"users": users}, - "max_payload": 2048576005, + "max_payload": 67108864, } conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf") @@ -248,21 +246,34 @@ async def __call__(self, scope, receive, send): ) -def run_nats_api_cmd(mode: str, ids: list[str], timeout: int = 30) -> None: - config = { - "key": settings.SECRET_KEY, - "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", - "agents": ids, - } +def run_nats_api_cmd(mode: str, ids: list[str] = [], timeout: int = 30) -> None: + if mode == "wmi": + config = { + "key": settings.SECRET_KEY, + "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", + "agents": ids, + } + else: + db = settings.DATABASES["default"] + config = { + "key": settings.SECRET_KEY, + "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", + "user": db["USER"], + "pass": db["PASSWORD"], + "host": db["HOST"], + "port": int(db["PORT"]), + "dbname": db["NAME"], + } + with tempfile.NamedTemporaryFile() as fp: with open(fp.name, "w") as f: json.dump(config, f) cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", mode] try: - subprocess.run(cmd, capture_output=True, timeout=timeout) + subprocess.run(cmd, timeout=timeout) except Exception as e: - logger.error(e) + DebugLog.error(message=e) def get_latest_trmm_ver() -> str: @@ -277,15 +288,16 @@ def get_latest_trmm_ver() -> str: if "TRMM_VERSION" in line: return line.split(" ")[2].strip('"') except Exception as e: - logger.error(e) + DebugLog.error(message=e) return "error" def replace_db_values( - string: str, agent: Agent = None, shell: str = None, quotes=True + string: str, instance=None, shell: str = None, quotes=True # type:ignore ) -> Union[str, None]: from core.models import CustomField, GlobalKVStore + from clients.models import Client, Site # split by period if exists. First should be model and second should be property i.e {{client.name}} temp = string.split(".") @@ -293,7 +305,7 @@ def replace_db_values( # check for model and property if len(temp) < 2: # ignore arg since it is invalid - return None + return "" # value is in the global keystore and replace value if temp[0] == "global": @@ -302,30 +314,48 @@ def replace_db_values( return f"'{value}'" if quotes else value else: - logger.error( - f"Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store" + DebugLog.error( + log_type="scripting", + message=f"{agent.hostname} Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store", # type:ignore ) - return None + return "" - if not agent: - # agent must be set if not global property - return f"There was an error finding the agent: {agent}" + if not instance: + # instance must be set if not global property + return "" if temp[0] == "client": model = "client" - obj = agent.client + if isinstance(instance, Client): + obj = instance + elif hasattr(instance, "client"): + obj = instance.client + else: + obj = None elif temp[0] == "site": model = "site" - obj = agent.site + if isinstance(instance, Site): + obj = instance + elif hasattr(instance, "site"): + obj = instance.site + else: + obj = None elif temp[0] == "agent": model = "agent" - obj = agent + if isinstance(instance, Agent): + obj = instance + else: + obj = None else: # ignore arg since it is invalid - logger.error( - f"Not enough information to find value for: {string}. Only agent, site, client, and global are supported." + DebugLog.error( + log_type="scripting", + message=f"{instance} Not enough information to find value for: {string}. Only agent, site, client, and global are supported.", ) - return None + return "" + + if not obj: + return "" if hasattr(obj, temp[1]): value = f"'{getattr(obj, temp[1])}'" if quotes else getattr(obj, temp[1]) @@ -359,19 +389,21 @@ def replace_db_values( else: # ignore arg since property is invalid - logger.error( - f"Couldn't find property on supplied variable: {string}. Make sure it exists as a custom field or a valid agent property" + DebugLog.error( + log_type="scripting", + message=f"{instance} Couldn't find property on supplied variable: {string}. Make sure it exists as a custom field or a valid agent property", ) - return None + return "" # log any unhashable type errors if value != None: return value # type: ignore else: - logger.error( - f"Couldn't lookup value for: {string}. Make sure it exists as a custom field or a valid agent property" + DebugLog.error( + log_type="scripting", + message=f" {instance}({instance.pk}) Couldn't lookup value for: {string}. Make sure it exists as a custom field or a valid agent property", ) - return None + return "" def format_shell_array(value: list) -> str: diff --git a/api/tacticalrmm/winupdate/tasks.py b/api/tacticalrmm/winupdate/tasks.py index d0e03c7bb0..5e00434118 100644 --- a/api/tacticalrmm/winupdate/tasks.py +++ b/api/tacticalrmm/winupdate/tasks.py @@ -3,15 +3,12 @@ import time import pytz -from django.conf import settings from django.utils import timezone as djangotime -from loguru import logger from packaging import version as pyver from agents.models import Agent from tacticalrmm.celery import app - -logger.configure(**settings.LOG_CONFIG) +from logs.models import DebugLog @app.task @@ -120,7 +117,11 @@ def check_agent_update_schedule_task(): if install: # initiate update on agent asynchronously and don't worry about ret code - logger.info(f"Installing windows updates on {agent.salt_id}") + DebugLog.info( + agent=agent, + log_type="windows_updates", + message=f"Installing windows updates on {agent.hostname}", + ) nats_data = { "func": "installwinupdates", "guids": agent.get_approved_update_guids(), diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1df4d54c58..84293eb8a5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: Debian10: - AGENT_NAME: "azpipelines-deb10" + AGENT_NAME: "az-pipeline-fran" pool: name: linux-vms @@ -20,6 +20,7 @@ jobs: sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline' sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline' sudo -u postgres psql -c 'CREATE DATABASE pipeline' + sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline SETTINGS_FILE="/myagent/_work/1/s/api/tacticalrmm/tacticalrmm/settings.py" rm -rf /myagent/_work/1/s/api/env cd /myagent/_work/1/s/api diff --git a/backup.sh b/backup.sh index c8d47c5554..1335a8c719 100755 --- a/backup.sh +++ b/backup.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="14" +SCRIPT_VERSION="15" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh' GREEN='\033[0;32m' @@ -80,7 +80,7 @@ if [ -f "${sysd}/daphne.service" ]; then sudo cp ${sysd}/daphne.service ${tmp_dir}/systemd/ fi -cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz +cat /rmm/api/tacticalrmm/tacticalrmm/private/log/django_debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/ cp /rmm/web/.env ${tmp_dir}/rmm/env cp /rmm/api/tacticalrmm/tacticalrmm/private/exe/mesh*.exe ${tmp_dir}/rmm/ diff --git a/docker/.env.example b/docker/.env.example index 3907ae92f0..f6be551a30 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -15,6 +15,7 @@ MESH_USER=tactical MESH_PASS=tactical MONGODB_USER=mongouser MONGODB_PASSWORD=mongopass +MESH_PERSISTENT_CONFIG=0 # database settings POSTGRES_USER=postgres diff --git a/docker/containers/tactical-meshcentral/entrypoint.sh b/docker/containers/tactical-meshcentral/entrypoint.sh index da1a06fb3f..3e612fc19c 100644 --- a/docker/containers/tactical-meshcentral/entrypoint.sh +++ b/docker/containers/tactical-meshcentral/entrypoint.sh @@ -9,14 +9,19 @@ set -e : "${MONGODB_HOST:=tactical-mongodb}" : "${MONGODB_PORT:=27017}" : "${NGINX_HOST_IP:=172.20.0.20}" +: "${MESH_PERSISTENT_CONFIG:=0}" mkdir -p /home/node/app/meshcentral-data mkdir -p ${TACTICAL_DIR}/tmp +if [ ! -f "/home/node/app/meshcentral-data/config.json" ] || [[ "${MESH_PERSISTENT_CONFIG}" -eq 0 ]]; then + +encoded_uri=$(node -p "encodeURI('mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}')") + mesh_config="$(cat << EOF { "settings": { - "mongodb": "mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}", + "mongodb": "${encoded_uri}", "Cert": "${MESH_HOST}", "TLSOffload": "${NGINX_HOST_IP}", "RedirPort": 80, @@ -54,11 +59,19 @@ EOF 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 node node_modules/meshcentral --adminaccount ${MESH_USER} if [ ! -f "${TACTICAL_DIR}/tmp/mesh_token" ]; then - node node_modules/meshcentral --logintokenkey > ${TACTICAL_DIR}/tmp/mesh_token + mesh_token=$(node node_modules/meshcentral --logintokenkey) + + if [[ ${#mesh_token} -eq 160 ]]; then + 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 diff --git a/docker/containers/tactical-nats/dockerfile b/docker/containers/tactical-nats/dockerfile index 5486576dec..697b13c1ff 100644 --- a/docker/containers/tactical-nats/dockerfile +++ b/docker/containers/tactical-nats/dockerfile @@ -1,4 +1,4 @@ -FROM nats:2.2.6-alpine +FROM nats:2.3.3-alpine ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready diff --git a/docker/containers/tactical/dockerfile b/docker/containers/tactical/dockerfile index b77973d2dd..fbf669b32f 100644 --- a/docker/containers/tactical/dockerfile +++ b/docker/containers/tactical/dockerfile @@ -1,5 +1,5 @@ # creates python virtual env -FROM python:3.9.2-slim AS CREATE_VENV_STAGE +FROM python:3.9.6-slim AS CREATE_VENV_STAGE ARG DEBIAN_FRONTEND=noninteractive @@ -24,7 +24,7 @@ RUN apt-get update && \ # runtime image -FROM python:3.9.2-slim +FROM python:3.9.6-slim # set env variables ENV VIRTUAL_ENV /opt/venv diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d32a33f352..613e383ba6 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -97,6 +97,7 @@ services: MESH_PASS: ${MESH_PASS} MONGODB_USER: ${MONGODB_USER} MONGODB_PASSWORD: ${MONGODB_PASSWORD} + MESH_PERSISTENT_CONFIG: ${MESH_PERSISTENT_CONFIG} networks: proxy: aliases: diff --git a/docker/image-build.sh b/docker/image-build.sh index ee668e0e26..a11447d977 100755 --- a/docker/image-build.sh +++ b/docker/image-build.sh @@ -3,6 +3,7 @@ set -o errexit set -o pipefail +# tactical tactical-frontend tactical-nats tactical-nginx DOCKER_IMAGES="tactical tactical-frontend tactical-nats tactical-nginx tactical-meshcentral" cd .. diff --git a/docs/docs/3rdparty_anydesk.md b/docs/docs/3rdparty_anydesk.md new file mode 100644 index 0000000000..1d934b15e2 --- /dev/null +++ b/docs/docs/3rdparty_anydesk.md @@ -0,0 +1,46 @@ +# AnyDesk + +## AnyDesk Integration + +!!!info + You can setup a full automation policy to collect the machine GUID but this example will collect from just one agent for testing purposes. + +From the UI go to **Settings > Global Settings > CUSTOM FIELDS > Agents** + +Add Custom Field
+**Target** = `Agent`
+**Name** = `AnyNetID`
+**Field Type** = `Text`
+ +![Service Name](images/3rdparty_anydesk1.png) + +While in Global Settings go to **URL ACTIONS** + +Add a URL Action
+**Name** = `AnyDesk Control`
+**Description** = `Connect to a AnyDesk Session`
+**URL Pattern** = + +```html +anydesk:{{agent.AnyNetID}} +``` + +Navigate to an agent with AnyDesk running (or apply using **Settings > Automation Manager**).
+Go to Tasks.
+Add Task
+**Select Script** = `AnyDesk - Get AnyNetID for client` (this is a builtin script from script library)
+**Descriptive name of task** = `Collects the AnyNetID for AnyDesk.`
+**Collector Task** = `CHECKED`
+**Custom Field to update** = `AnyNetID`
+ +![Service Name](images/3rdparty_anydesk2.png) + +Click **Next**
+Check **Manual**
+Click **Add Task** + +Right click on the newly created task and click **Run Task Now**. + +Give it a second to execute then right click the agent that you are working with and go to **Run URL Action > AnyDesk Control** + +It launch the session in AnyDesk. diff --git a/docs/docs/3rdparty_bitdefender_gravityzone.md b/docs/docs/3rdparty_bitdefender_gravityzone.md new file mode 100644 index 0000000000..518aa6a14b --- /dev/null +++ b/docs/docs/3rdparty_bitdefender_gravityzone.md @@ -0,0 +1,34 @@ +# BitDefender GravityZone Deployment + +## How to Deploy BitDefender GravityZone + +From the UI go to **Settings > Global Settings > CUSTOM FIELDS > Clients** + +Add a Custom Field
+ +First:
+**Target** = `CLIENTS`
+**Name** = `bdurl`
+**Field Type** = `Text`
+ +![Service Name](images/3rdparty_bdg_RmmCustField.png) + +Log into your GravityZone and on the left hand side, select "Packages" under "Network". + +![Service Name](images/3rdparty_bdg_Packages.png) + +Select the client you are working with and click "Send Download Links" at the top.
+ +![Service Name](images/3rdparty_bdg_DownloadLink.png) + +Copy the appropriate download link + +![Service Name](images/3rdparty_bdg_LinkCopy.png) + +Paste download link into the `bdurl` when you right click your target clients name in the RMM. + +![Service Name](images/3rdparty_bdg_CustFieldLink.png) + +Right click the Agent you want to deploy to and **Run Script**. Select **BitDefender GravityZone Install** and set timeout for 1800 seconds. + +**Install time will vary based on internet speed and other AV removal by BitDefender BEST deployment** \ No newline at end of file diff --git a/docs/docs/3rdparty_grafana.md b/docs/docs/3rdparty_grafana.md new file mode 100644 index 0000000000..798e9400b3 --- /dev/null +++ b/docs/docs/3rdparty_grafana.md @@ -0,0 +1,9 @@ +# Adding Grafana to Tactical RMM + +Adding graphical Dashboards to Tactical. + +See + +![Example1](images/3rdparty_grafana_ex1.png) + +![Example1](images/3rdparty_grafana_ex2.png) \ No newline at end of file diff --git a/docs/docs/3rdparty_teamviewer.md b/docs/docs/3rdparty_teamviewer.md new file mode 100644 index 0000000000..dae2a987e4 --- /dev/null +++ b/docs/docs/3rdparty_teamviewer.md @@ -0,0 +1,46 @@ +# TeamViewer + +## TeamViewer Integration + +!!!info + You can setup a full automation policy to collect the machine GUID but this example will collect from just one agent for testing purposes. + +From the UI go to **Settings > Global Settings > CUSTOM FIELDS > Agents** + +Add Custom Field
+**Target** = `Agent`
+**Name** = `TeamViewerClientID`
+**Field Type** = `Text`
+ +![Service Name](images/3rdparty_teamviewer1.png) + +While in Global Settings go to **URL ACTIONS** + +Add a URL Action
+**Name** = `TeamViewer Control`
+**Description** = `Connect to a Team Viewer Session`
+**URL Pattern** = + +```html +https://start.teamviewer.com/device/{{agent.TeamViewerClientID}}/authorization/password/mode/control +``` + +Navigate to an agent with TeamViewer running (or apply using **Settings > Automation Manager**).
+Go to Tasks.
+Add Task
+**Select Script** = `TeamViewer - Get ClientID for client` (this is a builtin script from script library)
+**Descriptive name of task** = `Collects the ClientID for TeamViewer.`
+**Collector Task** = `CHECKED`
+**Custom Field to update** = `TeamViewerClientID`
+ +![Service Name](images/3rdparty_teamviewer2.png) + +Click **Next**
+Check **Manual**
+Click **Add Task** + +Right click on the newly created task and click **Run Task Now**. + +Give it a second to execute then right click the agent that you are working with and go to **Run URL Action > TeamViewer Control** + +It launch the session and possibly promt for password in TeamViewer. diff --git a/docs/docs/contributing_using_docker.md b/docs/docs/contributing_using_docker.md index ef88bb2ce7..3fe8234261 100644 --- a/docs/docs/contributing_using_docker.md +++ b/docs/docs/contributing_using_docker.md @@ -24,6 +24,18 @@ This is better ![img](images/wls2_upgrade_and_set_default.png) +## Install VSCode Extensions + +[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) + +[Docker](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) + +## Connect to WSL and clone your Github fork + +![Connect to WSL](images/vscode_wsl_docker_setup1.png) + +![Clone Repo](images/vscode_wsl_docker_setup2.png) + ## Create .env file Under .devcontainer duplicate @@ -46,7 +58,20 @@ Customize to your tastes (it doesn't need to be internet configured, just add re 127.0.0.1 mesh.example.com ``` -## View mkdocks live edits in browser +## Launch your Dev VM in Docker + +Right-click `docker-compose.yml` and choose `Compose Up` + +Wait, it'll take a while as docker downloads all the modules and gets running. + +## Develop! + +You're operational! + +!!!note + Self-signed certs are in your dev environment. Navigate to https://api.example.com and https://rmm.example.com and accept the self signed certs to get rid of errors. + +### View mkdocks live edits in browser Change stuff in `/docs/docs/` @@ -54,6 +79,7 @@ mkdocs is Exposed on Port: 8005 Open: [http://rmm.example.com:8005/](http://rmm.example.com:8005/) -## View django administration +### View django administration + +Open: [http://rmm.example.com:8000/admin/](http://rmm.example.com:8000/admin/) -Open: [http://rmm.example.com:8000/admin/](http://rmm.example.com:8000/admin/) \ No newline at end of file diff --git a/docs/docs/faq.md b/docs/docs/faq.md index a59b09d401..1a25cdda0e 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -40,4 +40,30 @@ cd /meshcentral/ sudo systemctl stop meshcentral node node_modules/meshcentral --resetaccount --pass sudo systemctl start meshcentral -``` \ No newline at end of file +``` + +#### Help! I've been hacked there are weird agents appearing in my Tactical RMM + +No, you haven't. + +1. Your installer was scanned by an antivirus. + +2. It didn't recognize the exe. + +3. You have the option enabled to submit unknown applications for analysis. + + ![AV Option1](images/faq_av_option1.png) + +4. They ran it against their virtualization testing cluster. + +5. You allow anyone to connect to your rmm server (you should look into techniques to hide your server from the internet). + +6. Here are some examples of what that looks like. + +![AV Sandbox1](images/faq_av_sandbox1.png) + +![AV Sandbox1](images/faq_av_sandbox2.png) + +![AV Sandbox1](images/faq_av_sandbox3.png) + +![AV Sandbox1](images/faq_av_sandbox4.png) \ No newline at end of file diff --git a/docs/docs/functions/alerting.md b/docs/docs/functions/alerting.md index ff7b8bd99c..c2f5438601 100644 --- a/docs/docs/functions/alerting.md +++ b/docs/docs/functions/alerting.md @@ -3,11 +3,13 @@ Alerting and notifications can be managed centrally using Alert Templates. All an alert template does is configure the Email, Text and Dashboard alert check boxes on Agents, Checks, and Automated Tasks. Using Alert Templates also enables additional feature like: -- Periodic notifications if an alert is left unresolved + +- Periodic notifications if an alert is left unresolved - Being able to notify on certain alert severities - Sending notifications when an alert is resolved - Executing scripts when an alert is triggered or resolved +[Setting up Email Alert Examples](email_alert.md) ## Supported Notifications - **Email Alerts** - Sends email to configured set of email addresses @@ -25,7 +27,6 @@ Alert severities are configured directly on the Check or Automated Task. When th - Warning - Error - ## Adding Alert Templates To create an alert template, go to **Settings > Alerts Manager**. Then click **New** @@ -33,12 +34,14 @@ To create an alert template, go to **Settings > Alerts Manager**. Then click **N The available options are: ### General Settings + - **Name** - The name that is used to identify the Alert Template in the dashboard - **Email Recipients** - Sets the list of email recipients. If this isn't set the email recipients will global settings will be used. - **From Email** - Sets the From email address of the notification. If this isn't set the From address from global settings is used. - **SMS Recipients** - Sets the list of text recipients. If this isn't set the sms list from global settings is used. ### Action Settings + - **Failure Action** - Runs the selected script once on any agent. This is useful for running one-time tasks like sending an http request to an external system to create a ticket. - **Failure Action Args** - Optionally pass in arguments to the failure script. - **Failure Action Timeout** - Sets the timeout for the script. @@ -46,17 +49,25 @@ The available options are: - **Resolved Action Args** - Optionally pass in arguments to the resolved script. - **Resolved Action Timeout** - Sets the timeout for the script. +#### Run actions only on: +- **Agents** - If Enabled, will run script failure/resolved actions on agent overdue alerts else no alert actions will be triggered for agent overdue alerts +- **Checks** - If Enabled, will run script failure/resolved actions on check alerts else no alert actions will be triggered check alerts +- **Tasks** - If Enabled, will run script failure/resolved actions on automated task alerts else no alert actions will be triggered automated task alerts + + ### Agent/Check/Task Failure Settings + - **Email** - When **Enabled**, will send an email notification and override the Email Alert checkbox on the Agent/Check/Task. When **Not Configured**, the Email Alert checkbox on the Agent/Check/Task will take effect. If **Disabled**, no email notifications will be sent and will override any Email alert checkbox on the Agent/Check/Task - **Text** - When **Enabled**, will send a text notification and override the SMS Alert checkbox on the Agent/Check/Task. When **Not Configured**, the SMS Alert checkbox on the Agent/Check/Task will take effect. If **Disabled**, no SMS notifications will be sent and will override any SMS Alert checkbox on the Agent/Check/Task - **Dashboard** - When **Enabled**, will send a dashboard notification and override the Dashboard Alert checkbox on the Agent/Check/Task. When **Not Configured**, the Dashboard Alert checkbox on the Agent/Check/Task will take effect. If **Disabled**, no SMS notifications will be sent and will override any Dashboard Alert checkbox on the Agent/Check/Task - **Alert again if not resolved after (days)** - This sends another notification if the alert isn't resolved after the set amount of days. Set to 0 to disable this -- **Alert on severity** - Only applicable to Check and Task alert notifications. This will only send alerts when they are of the configured severity. +- **Alert on severity** - Only applicable to Check and Task alert notifications. This will only send alerts when they are of the configured severity. !!!info Alert on Severity needs to be configured for check and task notifications to be sent! ### Agent/Check/Task Resolved Settings + - **Email** - If enabled, sends an email notification when an alert is resolved - **Text** - If enabled, sends a text messaged when an alert is resolved @@ -70,7 +81,7 @@ Alert templates can be configured Globally, through an Automation Policy, or set ## Alert Template Exclusions -You can exclude Clients, Sites, and Agents from alert templates. To do this you can: +You can exclude Clients, Sites, and Agents from alert templates. To do this you can: - right-click on the **Alert Template** in **Alerts Manager** and select **Exclusions** - select the **Alert Exclusions** link in the Alert Template row. @@ -79,7 +90,7 @@ You can also **Exclude Desktops** from the alert template. This is useful if you ## Alert Template inheritance -Alerts are applied in the following over. The agent picks the closest matching alert template. +Alerts are applied in the following order. The agent picks the closest matching alert template. 1. Policy w/ Alert Template applied to Site 2. Site diff --git a/docs/docs/functions/database_maintenance.md b/docs/docs/functions/database_maintenance.md new file mode 100644 index 0000000000..da539cb81c --- /dev/null +++ b/docs/docs/functions/database_maintenance.md @@ -0,0 +1,17 @@ +# Database Maintenance + +Tactical RMM ships with data retention defaults that will work fine for most environments. There are situations, depending on the number of agents and checks configured, that these defaults need to be tweaked to improve performance. + +## Adjusting Data Retention + +In the dashboard, go to **Settings > Global Settings > Retention** + +The options are: + +- **Check History** - Will delete check history older than the days specified (default is 30 days). +- **Resolved Alerts** - Will delete alerts that have been resolved older than the days specified (default is disabled). +- **Agent History** - Will delete agent command/script history older than the days specified (default is 60 days). +- **Debug Logs** - Will delete agent debug logs older than the days specified (default is 30 days) +- **Audit Logs** Will delete Tactical RMM audit logs older than the days specified (default is disabled) + +To disable database pruning on a table, set the days to 0. diff --git a/docs/docs/functions/email_alert.md b/docs/docs/functions/email_alert.md new file mode 100644 index 0000000000..e312513930 --- /dev/null +++ b/docs/docs/functions/email_alert.md @@ -0,0 +1,46 @@ +# Email Setup + +Under **Settings > Global Settings > Email Alerts** + +## Setting up Tactical RMM Alerts using Open Relay + +MS 365 in this example + +1. Log into Tactical RMM +2. Go to Settings +3. Go to Global Settings +4. Click on Alerts +5. Enter the email address (or addresses) you want to receive alerts to eg info@mydomain.com +6. Enter the from email address (this will need to be part of your domain on 365, however it doesn’t need a license) eg rmm@mydomain.com +7. Go to MXToolbox.com and enter your domain name in, copy the hostname from there and paste into Host +8. Change the port to 25 +9. Click Save +10. Login to admin.microsoft.com +11. Go to Exchange Admin Centre +12. Go to “Connectors” under “Mail Flow” +13. Click to + button +14. In From: select “Your organisations email server” +15. In To: select “Office 365” +16. Click Next +17. In the Name type in RMM +18. Click By Verifying that the IP address…… +19. Click + +20. Enter your IP and Click OK +21. Click Next +22. Click OK + +## Setting up Tactical RMM Alerts using username & password + +Gmail in this example + +1. Log into Tactical RMM +2. Go to Settings +3. Go to Global Settings +4. Click on Alerts +5. Enter the email address (or addresses) you want to receive alerts to eg info@mydomain.com +6. Enter the from email address myrmm@gmail.com +7. Tick the box “My server requires Authentication” +8. Enter your username e.g. myrmm@gmail.com +9. Enter your password +10. Change the port to 587 +11. Click Save diff --git a/docs/docs/functions/scripting.md b/docs/docs/functions/scripting.md index f9720fbcb5..358dcd8238 100644 --- a/docs/docs/functions/scripting.md +++ b/docs/docs/functions/scripting.md @@ -24,7 +24,7 @@ In the dashboard, browse to **Settings > Scripts Manager**. Click the **New** bu To download a Tactical RMM Script, click on the script in the Script Manager to select it. Then click the **Download Script** button on the top. You can also right-click on the script and select download -## Community Script +## Community Scripts These are script that are built into Tactical RMM. They are provided and mantained by the Tactical RMM community. These scripts are updated whenever Tactical RMM is updated and can't be modified or deleted in the dashboard. @@ -36,9 +36,12 @@ You can choose to hide community script throughout the dashboard by opening **Sc ### Manual run on agent In the **Agent Table**, you can right-click on an agent and select **Run Script**. You have the options of: - - **Wait for Output** - Runs the script and waits for the script to finish running and displays the output. - - **Fire and Forget** - Starts the script and does not wait for output. - - **Email Output** - Starts the script and will email the output. Allows for using the default email address in the global settings or adding a new email address. + +- **Wait for Output** - Runs the script and waits for the script to finish running and displays the output. +- **Fire and Forget** - Starts the script and does not wait for output. +- **Email Output** - Starts the script and will email the output. Allows for using the default email address in the global settings or adding a new email address. +- **Save as Note** - Saves the output as a Note that can be views in the agent Notes tab +- **Collector** - Saves to output to the specified custom field. There is also an option on the agent context menu called **Run Favorited Script**. This will essentially Fire and Forget the script with default args and timeout. @@ -108,3 +111,27 @@ Write-Output "Public IP: $PublicIp" Write-Output "Custom Fields: $CustomField" Write-Output "Global: $Global" ``` + +## Script Snippets + +Script Snippets allow you to create common code blocks or comments and apply them to all of your scripts. This could be initialization code, common error checking, or even code comments. + +### Adding Script Snippets + +In the dashboard, browse to **Settings > Scripts Manager**. Click the **Script Snippets** button. + +- **Name** - This identifies the script snippet in the dashboard +- **Description** - Optional description for the script snippet +- **Shell** - This sets the language of the script. Available options are: + - Powershell + - Windows Batch + - Python + +### Using Script Snippets + +When editing a script, you can add template tags to the script body that contains the script snippet name. For example, if a script snippet exists with the name "Check WMF", you would put {{Check WMF}} in the script body and the snippet code will be replaced. + +!!!info + Everything between {{}} is CaSe sEnSiTive + +The template tags will only be visible when Editing the script. When downloading or viewing the script code the template tags will be replaced with the script snippet code. \ No newline at end of file diff --git a/docs/docs/howitallworks.md b/docs/docs/howitallworks.md index 72a2ad8fa1..10acf0a0f7 100644 --- a/docs/docs/howitallworks.md +++ b/docs/docs/howitallworks.md @@ -1,6 +1,14 @@ # How It All Works -INSERT WIREFRAME GRAPHIC HERE USING +INSERT WIREFRAME GRAPHICS HERE USING SOMETHING LIKE + +1) how nats-django-admin web interface work + +2) Agent installer steps + +3) Agent communication process with server (what ports to which services etc) + +4) Agent checks/tasks and how they work on the workstation/interact with server ## Server @@ -130,7 +138,7 @@ Executes the file (INNO setup exe) Files create `c:\Windows\temp\Tacticalxxxx\` folder for install (and log files) -***** +*** ### Windows Update Management @@ -142,4 +150,14 @@ AUOptions (REG_DWORD): 1: Keep my computer up to date is disabled in Automatic Updates. ``` -Uses this Microsoft API to handle updates: [https://docs.microsoft.com/en-us/windows/win32/api/_wua/](https://docs.microsoft.com/en-us/windows/win32/api/_wua/) \ No newline at end of file +Uses this Microsoft API to handle updates: [https://docs.microsoft.com/en-us/windows/win32/api/_wua/](https://docs.microsoft.com/en-us/windows/win32/api/_wua/) + +### Log files + +You can find 3 sets of detailed logs at `/rmm/api/tacticalrmm/tacticalrmm/private/log` + +* `error.log` nginx log for all errors on all TRMM URL's: rmm, api and mesh + +* `access.log` nginx log for access auditing on all URL's: rmm, api and mesh (_this is a large file, and should be cleaned periodically_) + +* `django_debug.log` created by django webapp diff --git "a/docs/docs/images/2021-07-10_123702 - Code_\342\227\217_community_scripts.json_-_tacticalrmm_-_Visual_St.png" "b/docs/docs/images/2021-07-10_123702 - Code_\342\227\217_community_scripts.json_-_tacticalrmm_-_Visual_St.png" new file mode 100644 index 0000000000..10160f3851 Binary files /dev/null and "b/docs/docs/images/2021-07-10_123702 - Code_\342\227\217_community_scripts.json_-_tacticalrmm_-_Visual_St.png" differ diff --git a/docs/docs/images/3rdparty_anydesk1.png b/docs/docs/images/3rdparty_anydesk1.png new file mode 100644 index 0000000000..0d3fc74924 Binary files /dev/null and b/docs/docs/images/3rdparty_anydesk1.png differ diff --git a/docs/docs/images/3rdparty_anydesk2.png b/docs/docs/images/3rdparty_anydesk2.png new file mode 100644 index 0000000000..dc47f8f233 Binary files /dev/null and b/docs/docs/images/3rdparty_anydesk2.png differ diff --git a/docs/docs/images/3rdparty_bdg_CustFieldLink.png b/docs/docs/images/3rdparty_bdg_CustFieldLink.png new file mode 100644 index 0000000000..ebf21e4e68 Binary files /dev/null and b/docs/docs/images/3rdparty_bdg_CustFieldLink.png differ diff --git a/docs/docs/images/3rdparty_bdg_DownloadLink.png b/docs/docs/images/3rdparty_bdg_DownloadLink.png new file mode 100644 index 0000000000..186f0f6419 Binary files /dev/null and b/docs/docs/images/3rdparty_bdg_DownloadLink.png differ diff --git a/docs/docs/images/3rdparty_bdg_LinkCopy.png b/docs/docs/images/3rdparty_bdg_LinkCopy.png new file mode 100644 index 0000000000..12e0fa8acd Binary files /dev/null and b/docs/docs/images/3rdparty_bdg_LinkCopy.png differ diff --git a/docs/docs/images/3rdparty_bdg_Packages.png b/docs/docs/images/3rdparty_bdg_Packages.png new file mode 100644 index 0000000000..1f84ee30e8 Binary files /dev/null and b/docs/docs/images/3rdparty_bdg_Packages.png differ diff --git a/docs/docs/images/3rdparty_bdg_RmmCustField.png b/docs/docs/images/3rdparty_bdg_RmmCustField.png new file mode 100644 index 0000000000..a2c69d4394 Binary files /dev/null and b/docs/docs/images/3rdparty_bdg_RmmCustField.png differ diff --git a/docs/docs/images/3rdparty_grafana_ex1.png b/docs/docs/images/3rdparty_grafana_ex1.png new file mode 100644 index 0000000000..ae22969204 Binary files /dev/null and b/docs/docs/images/3rdparty_grafana_ex1.png differ diff --git a/docs/docs/images/3rdparty_grafana_ex2.png b/docs/docs/images/3rdparty_grafana_ex2.png new file mode 100644 index 0000000000..bd0cc837f4 Binary files /dev/null and b/docs/docs/images/3rdparty_grafana_ex2.png differ diff --git a/docs/docs/images/3rdparty_teamviewer1.png b/docs/docs/images/3rdparty_teamviewer1.png new file mode 100644 index 0000000000..53fcdcf56f Binary files /dev/null and b/docs/docs/images/3rdparty_teamviewer1.png differ diff --git a/docs/docs/images/3rdparty_teamviewer2.png b/docs/docs/images/3rdparty_teamviewer2.png new file mode 100644 index 0000000000..bd885d24d3 Binary files /dev/null and b/docs/docs/images/3rdparty_teamviewer2.png differ diff --git a/docs/docs/images/example1_taskcollectorscript.png b/docs/docs/images/example1_taskcollectorscript.png index d7abbfbca9..fbe5c6d570 100644 Binary files a/docs/docs/images/example1_taskcollectorscript.png and b/docs/docs/images/example1_taskcollectorscript.png differ diff --git a/docs/docs/images/faq_av_option1.png b/docs/docs/images/faq_av_option1.png new file mode 100644 index 0000000000..718bae3755 Binary files /dev/null and b/docs/docs/images/faq_av_option1.png differ diff --git a/docs/docs/images/faq_av_sandbox1.png b/docs/docs/images/faq_av_sandbox1.png new file mode 100644 index 0000000000..3c7f082c5d Binary files /dev/null and b/docs/docs/images/faq_av_sandbox1.png differ diff --git a/docs/docs/images/faq_av_sandbox2.png b/docs/docs/images/faq_av_sandbox2.png new file mode 100644 index 0000000000..074a7a3bb1 Binary files /dev/null and b/docs/docs/images/faq_av_sandbox2.png differ diff --git a/docs/docs/images/faq_av_sandbox3.png b/docs/docs/images/faq_av_sandbox3.png new file mode 100644 index 0000000000..b45c260132 Binary files /dev/null and b/docs/docs/images/faq_av_sandbox3.png differ diff --git a/docs/docs/images/faq_av_sandbox4.png b/docs/docs/images/faq_av_sandbox4.png new file mode 100644 index 0000000000..6d5a30ad80 Binary files /dev/null and b/docs/docs/images/faq_av_sandbox4.png differ diff --git a/docs/docs/images/tipsntricks_meshcontrol.png b/docs/docs/images/tipsntricks_meshcontrol.png new file mode 100644 index 0000000000..0ed1d1596d Binary files /dev/null and b/docs/docs/images/tipsntricks_meshcontrol.png differ diff --git a/docs/docs/images/tipsntricks_meshterminal.png b/docs/docs/images/tipsntricks_meshterminal.png new file mode 100644 index 0000000000..6c23308ffc Binary files /dev/null and b/docs/docs/images/tipsntricks_meshterminal.png differ diff --git a/docs/docs/images/vscode_wsl_docker_setup1.png b/docs/docs/images/vscode_wsl_docker_setup1.png new file mode 100644 index 0000000000..5f89e142a9 Binary files /dev/null and b/docs/docs/images/vscode_wsl_docker_setup1.png differ diff --git a/docs/docs/images/vscode_wsl_docker_setup2.png b/docs/docs/images/vscode_wsl_docker_setup2.png new file mode 100644 index 0000000000..2a316ce2d5 Binary files /dev/null and b/docs/docs/images/vscode_wsl_docker_setup2.png differ diff --git a/docs/docs/install_agent.md b/docs/docs/install_agent.md index a2878ca680..5fc72d2d24 100644 --- a/docs/docs/install_agent.md +++ b/docs/docs/install_agent.md @@ -9,6 +9,7 @@ `C:\Program Files\Mesh Agent\*`
`C:\Windows\Temp\winagent-v*.exe`
`C:\Windows\Temp\trmm\*`
+ `C:\temp\tacticalrmm*.exe`
diff --git a/docs/docs/tipsntricks.md b/docs/docs/tipsntricks.md index 103eefa902..dfc18715e2 100644 --- a/docs/docs/tipsntricks.md +++ b/docs/docs/tipsntricks.md @@ -8,3 +8,12 @@ At the top right of your web administration interface, click your Username > pre ***** +## Mesh + +Right-click the connect button in *Remote Background | Terminal* for shell options + +![Terminal](images/tipsntricks_meshterminal.png) + +Right-click the connect button in *Take Control* for connect options + +![Terminal](images/tipsntricks_meshcontrol.png) diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 8d2e54f6c5..b7c0b29532 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -89,6 +89,28 @@ Read through the log files in the following folders and check for errors: /var/log/celery ``` +#### Using Cloudflare DNS +- rmm.example.com can be proxied. +- api.example.com can NOT be proxied. +- mesh.example.com can be proxied with the caveat that Mesh checks the cert presented to the agent is the same one on the server. I.e. no MITM. You'll need to copy Cloudflare's edge cert to your server if you want to proxy this domain. + +#### Testing Network Connectivity between agent and server + +Use powershell, make sure you can connect to 443 and 4222 from agent to server: + +```powershell +Test-NetConnection -ComputerName api.example.com -Port 4222 +``` + +```powershell +Test-NetConnection -ComputerName api.example.com -Port 443 +``` + +```powershell +Test-NetConnection -ComputerName rmm.example.com -Port 443 +``` + +Are you trying to use a proxy to share your single public IP with multiple services on 443? This is complicated and [unsupported by Tactical RMM](unsupported_scripts.md), test your setup. \ No newline at end of file diff --git a/docs/docs/update_server.md b/docs/docs/update_server.md index 6eca1541ba..fbb9bbe475 100644 --- a/docs/docs/update_server.md +++ b/docs/docs/update_server.md @@ -17,6 +17,7 @@ Other than this, you should avoid making any changes to your server and let the SSH into your server as the linux user you created during install.

__Never__ run any update scripts or commands as the `root` user.
This will mess up permissions and break your installation.

Download the update script and run it:
+ ```bash wget -N https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh chmod +x update.sh @@ -27,18 +28,19 @@ chmod +x update.sh If you are already on the latest version, the update script will notify you of this and return immediately.

You can pass the optional `--force` flag to the update script to forcefully run through an update, which will bypass the check for latest version.
+ ```bash ./update.sh --force ``` + This is usefull for a botched update that might have not completed fully.

The update script will also fix any permissions that might have gotten messed up during a botched update, or if you accidentally ran the update script as the `root` user.
- !!!warning Do __not__ attempt to manually update MeshCentral to a newer version. - + You should let the `update.sh` script handle this for you. The developers will test MeshCentral and make sure integration does not break before bumping the mesh version. @@ -61,3 +63,16 @@ After this you have renewed the cert, simply run the `update.sh` script, passing ```bash ./update.sh --force ``` + +#### Keep an eye on your disk space + +If you're running low, shrink you database + +1. Choose *Tools menu > Server Maintenance > Prune DB Tables* + +2. At server command prompt run + +```bash +sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog" +sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog" +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index db5babbe9f..b484955da9 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -12,18 +12,19 @@ nav: - "Updating the RMM (Docker)": update_docker.md - "Updating Agents": update_agents.md - Functionality: + - "Alerting": functions/alerting.md - "Automated Tasks": functions/automated_tasks.md - - "Scripting": functions/scripting.md - - "Global Keystore": functions/keystore.md - "Custom Fields": functions/custom_fields.md + - "Django Admin": functions/django_admin.md + - "Global Keystore": functions/keystore.md + - "Maintenance Mode": functions/maintenance_mode.md - "Remote Background": functions/remote_bg.md + - "Settings Override": functions/settings_override.md + - "Scripting": functions/scripting.md - "URL Actions": functions/url_actions.md - - "Maintenance Mode": functions/maintenance_mode.md - - "Alerting": functions/alerting.md - "User Interface Preferences": functions/user_ui.md - - "Django Admin": functions/django_admin.md - - "Settings Override": functions/settings_override.md - "Examples": functions/examples.md + - "Database Maintenace": functions/database_maintenance.md - Backup: backup.md - Restore: restore.md - Troubleshooting: troubleshooting.md @@ -31,7 +32,12 @@ nav: - Management Commands: management_cmds.md - MeshCentral Integration: mesh_integration.md - 3rd Party Integrations: + - "AnyDesk": 3rdparty_anydesk.md + - "BitDefender GravityZone": 3rdparty_bitdefender_gravityzone.md - "Connectwise Control / Screenconnect": 3rdparty_screenconnect.md + - "Grafana": 3rdparty_grafana.md + - "TeamViewer": 3rdparty_teamviewer.md + - Tips n' Tricks: tipsntricks.md - Contributing: - "Contributing to Docs": contributing.md - "Contributing using VSCode": contributing_using_vscode.md @@ -39,6 +45,7 @@ nav: - License: license.md site_description: "A remote monitoring and management tool" site_author: "wh1te909" +site_url: "https://wh1te909.github.io/tacticalrmm/" dev_addr: "0.0.0.0:8005" diff --git a/go.mod b/go.mod index 3471546066..05f44ded38 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/wh1te909/tacticalrmm go 1.16 require ( + github.com/golang/protobuf v1.5.2 // indirect github.com/jmoiron/sqlx v1.3.4 github.com/lib/pq v1.10.2 - github.com/nats-io/nats-server/v2 v2.1.8-0.20201129161730-ebe63db3e3ed // indirect - github.com/nats-io/nats.go v1.11.0 + github.com/nats-io/nats-server/v2 v2.4.0 // indirect + github.com/nats-io/nats.go v1.12.0 github.com/ugorji/go/codec v1.2.6 - golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b // indirect + google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index e20ce7a49c..a526bf5e26 100644 --- a/go.sum +++ b/go.sum @@ -5,39 +5,34 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= -github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/jwt v0.3.3-0.20200519195258-f2bf5ce574c7 h1:RnGotxlghqR5D2KDAu4TyuLqyjuylOsJiAFhXvMvQIc= -github.com/nats-io/jwt v0.3.3-0.20200519195258-f2bf5ce574c7/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M= -github.com/nats-io/jwt/v2 v2.0.0-20200916203241-1f8ce17dff02/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ= -github.com/nats-io/jwt/v2 v2.0.0-20201015190852-e11ce317263c h1:Hc1D9ChlsCMVwCxJ6QT5xqfk2zJ4XNea+LtdfaYhd20= -github.com/nats-io/jwt/v2 v2.0.0-20201015190852-e11ce317263c/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ= -github.com/nats-io/nats-server/v2 v2.1.8-0.20200524125952-51ebd92a9093/go.mod h1:rQnBf2Rv4P9adtAs/Ti6LfFmVtFG6HLhl/H7cVshcJU= -github.com/nats-io/nats-server/v2 v2.1.8-0.20200601203034-f8d6dd992b71/go.mod h1:Nan/1L5Sa1JRW+Thm4HNYcIDcVRFc5zK9OpSZeI2kk4= -github.com/nats-io/nats-server/v2 v2.1.8-0.20200929001935-7f44d075f7ad/go.mod h1:TkHpUIDETmTI7mrHN40D1pzxfzHZuGmtMbtb83TGVQw= -github.com/nats-io/nats-server/v2 v2.1.8-0.20201129161730-ebe63db3e3ed h1:/FdiqqED2Wy6pyVh7K61gN5G0WfbvFVQzGgpHTcAlHA= -github.com/nats-io/nats-server/v2 v2.1.8-0.20201129161730-ebe63db3e3ed/go.mod h1:XD0zHR/jTXdZvWaQfS5mQgsXj6x12kMjKLyAk/cOGgY= -github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE= -github.com/nats-io/nats.go v1.10.1-0.20200531124210-96f2130e4d55/go.mod h1:ARiFsjW9DVxk48WJbO3OSZ2DG8fjkMi7ecLmXoY/n9I= -github.com/nats-io/nats.go v1.10.1-0.20200606002146-fc6fed82929a/go.mod h1:8eAIv96Mo9QW6Or40jUHejS7e4VwZ3VRYD6Sf0BTDp4= -github.com/nats-io/nats.go v1.10.1-0.20201021145452-94be476ad6e0/go.mod h1:VU2zERjp8xmF+Lw2NH4u2t5qWZxwc7jB3+7HVMWQXPI= -github.com/nats-io/nats.go v1.11.0 h1:L263PZkrmkRJRJT2YHU8GwWWvEvmr9/LUKuJTXsF32k= -github.com/nats-io/nats.go v1.11.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= +github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= +github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= +github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= +github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= +github.com/nats-io/nats-server/v2 v2.4.0 h1:auni7PHiuyXR4BnDPzLVs3iyO7W7XUmZs8J5cjVb2BE= +github.com/nats-io/nats-server/v2 v2.4.0/go.mod h1:TUAhMFYh1VISyY/D4WKJUMuGHg8yHtoUTuxkbiej1lc= +github.com/nats-io/nats.go v1.12.0 h1:n0oZzK2aIZDMKuEiMKJ9qkCUgVY5vTAAksSXtLlz5Xc= +github.com/nats-io/nats.go v1.12.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= @@ -48,20 +43,18 @@ github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b h1:HSSdksA3iHk8fuZz7C7+A6tDgtIRF+7FSXu5TgK09I8= -golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -74,5 +67,8 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/install.sh b/install.sh index b2601fcf7a..3f5a312844 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="51" +SCRIPT_VERSION="52" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh' sudo apt install -y curl wget dirmngr gnupg lsb-release @@ -164,18 +164,6 @@ CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem sudo chown ${USER}:${USER} -R /etc/letsencrypt sudo chmod 775 -R /etc/letsencrypt -print_green 'Downloading NATS' - -nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX) -wget https://github.com/nats-io/nats-server/releases/download/v2.2.6/nats-server-v2.2.6-linux-amd64.tar.gz -P ${nats_tmp} - -tar -xzf ${nats_tmp}/nats-server-v2.2.6-linux-amd64.tar.gz -C ${nats_tmp} - -sudo mv ${nats_tmp}/nats-server-v2.2.6-linux-amd64/nats-server /usr/local/bin/ -sudo chmod +x /usr/local/bin/nats-server -sudo chown ${USER}:${USER} /usr/local/bin/nats-server -rm -rf ${nats_tmp} - print_green 'Installing Nginx' sudo apt install -y nginx @@ -204,14 +192,14 @@ print_green 'Installing Python 3.9' 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.2/Python-3.9.2.tgz -tar -xf Python-3.9.2.tgz -cd Python-3.9.2 +wget https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz +tar -xf Python-3.9.6.tgz +cd Python-3.9.6 ./configure --enable-optimizations make -j $numprocs sudo make altinstall cd ~ -sudo rm -rf Python-3.9.2 Python-3.9.2.tgz +sudo rm -rf Python-3.9.6 Python-3.9.6.tgz print_green 'Installing redis and git' @@ -252,6 +240,17 @@ git config user.email "admin@example.com" git config user.name "Bob" git checkout master +print_green 'Downloading NATS' + +NATS_SERVER_VER=$(grep "^NATS_SERVER_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') +nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX) +wget https://github.com/nats-io/nats-server/releases/download/v${NATS_SERVER_VER}/nats-server-v${NATS_SERVER_VER}-linux-amd64.tar.gz -P ${nats_tmp} +tar -xzf ${nats_tmp}/nats-server-v${NATS_SERVER_VER}-linux-amd64.tar.gz -C ${nats_tmp} +sudo mv ${nats_tmp}/nats-server-v${NATS_SERVER_VER}-linux-amd64/nats-server /usr/local/bin/ +sudo chmod +x /usr/local/bin/nats-server +sudo chown ${USER}:${USER} /usr/local/bin/nats-server +rm -rf ${nats_tmp} + print_green 'Installing MeshCentral' MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') diff --git a/main.go b/main.go index af3336e262..cb77d90fea 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "github.com/wh1te909/tacticalrmm/natsapi" ) -var version = "2.1.0" +var version = "2.3.0" func main() { ver := flag.Bool("version", false, "Prints version") @@ -23,12 +23,12 @@ func main() { } switch *mode { - case "monitor": - api.MonitorAgents(*config) case "wmi": api.GetWMI(*config) case "checkin": api.CheckIn(*config) + case "agentinfo": + api.AgentInfo(*config) default: fmt.Println(version) } diff --git a/natsapi/bin/nats-api b/natsapi/bin/nats-api index 69097d7741..43f9f8ada1 100755 Binary files a/natsapi/bin/nats-api and b/natsapi/bin/nats-api differ diff --git a/natsapi/tasks.go b/natsapi/tasks.go index 0aa7ca1bbd..f185f7a208 100644 --- a/natsapi/tasks.go +++ b/natsapi/tasks.go @@ -53,82 +53,83 @@ func setupNatsOptions(key string) []nats.Option { return opts } -func MonitorAgents(file string) { - var result JsonFile - var payload, recPayload []byte - var mh codec.MsgpackHandle - mh.RawToString = true - ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle)) - ret.Encode(map[string]string{"func": "ping"}) - - rec := codec.NewEncoderBytes(&recPayload, new(codec.MsgpackHandle)) - rec.Encode(Recovery{ - Func: "recover", - Data: map[string]string{"mode": "tacagent"}, - }) - - jret, _ := ioutil.ReadFile(file) - err := json.Unmarshal(jret, &result) +func CheckIn(file string) { + agents, db, r, err := GetAgents(file) if err != nil { log.Fatalln(err) } - opts := setupNatsOptions(result.Key) + var payload []byte + ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle)) + ret.Encode(map[string]string{"func": "ping"}) - nc, err := nats.Connect(result.NatsURL, opts...) + opts := setupNatsOptions(r.Key) + + nc, err := nats.Connect(r.NatsURL, opts...) if err != nil { log.Fatalln(err) } defer nc.Close() var wg sync.WaitGroup - var resp string - wg.Add(len(result.Agents)) + wg.Add(len(agents)) - for _, id := range result.Agents { - go func(id string, nc *nats.Conn, wg *sync.WaitGroup) { + loc, _ := time.LoadLocation("UTC") + now := time.Now().In(loc) + + for _, a := range agents { + go func(id string, pk int, nc *nats.Conn, wg *sync.WaitGroup, db *sqlx.DB, now time.Time) { defer wg.Done() + + var resp string + var mh codec.MsgpackHandle + mh.RawToString = true + + time.Sleep(time.Duration(randRange(100, 1500)) * time.Millisecond) out, err := nc.Request(id, payload, 1*time.Second) if err != nil { return } + dec := codec.NewDecoderBytes(out.Data, &mh) if err := dec.Decode(&resp); err == nil { - // if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service) - // then tacticalagent service is hung. forcefully restart it if resp == "pong" { - nc.Publish(id, recPayload) + _, err = db.NamedExec( + `UPDATE agents_agent SET last_seen=:lastSeen WHERE agents_agent.id=:pk`, + map[string]interface{}{"lastSeen": now, "pk": pk}, + ) + if err != nil { + fmt.Println(err) + } } } - }(id, nc, &wg) + }(a.AgentID, a.ID, nc, &wg, db, now) } wg.Wait() + db.Close() } -func CheckIn(file string) { - var r DjangoConfig - +func GetAgents(file string) (agents []Agent, db *sqlx.DB, r DjangoConfig, err error) { jret, _ := ioutil.ReadFile(file) - err := json.Unmarshal(jret, &r) + err = json.Unmarshal(jret, &r) if err != nil { - log.Fatalln(err) + return } psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+ "password=%s dbname=%s sslmode=disable", r.Host, r.Port, r.User, r.Pass, r.DBName) - db, err := sqlx.Connect("postgres", psqlInfo) + db, err = sqlx.Connect("postgres", psqlInfo) if err != nil { - log.Fatalln(err) + return } db.SetMaxOpenConns(15) agent := Agent{} - agents := make([]Agent, 0) rows, err := db.Queryx("SELECT agents_agent.id, agents_agent.agent_id FROM agents_agent") if err != nil { - log.Fatalln(err) + return } for rows.Next() { @@ -138,10 +139,18 @@ func CheckIn(file string) { } agents = append(agents, agent) } + return +} + +func AgentInfo(file string) { + agents, db, r, err := GetAgents(file) + if err != nil { + log.Fatalln(err) + } var payload []byte ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle)) - ret.Encode(map[string]string{"func": "ping"}) + ret.Encode(map[string]string{"func": "agentinfo"}) opts := setupNatsOptions(r.Key) @@ -154,14 +163,11 @@ func CheckIn(file string) { var wg sync.WaitGroup wg.Add(len(agents)) - loc, _ := time.LoadLocation("UTC") - now := time.Now().In(loc) - for _, a := range agents { - go func(id string, pk int, nc *nats.Conn, wg *sync.WaitGroup, db *sqlx.DB, now time.Time) { + go func(id string, pk int, nc *nats.Conn, wg *sync.WaitGroup, db *sqlx.DB) { defer wg.Done() - var resp string + var r AgentInfoRet var mh codec.MsgpackHandle mh.RawToString = true @@ -172,18 +178,27 @@ func CheckIn(file string) { } dec := codec.NewDecoderBytes(out.Data, &mh) - if err := dec.Decode(&resp); err == nil { - if resp == "pong" { - _, err = db.NamedExec( - `UPDATE agents_agent SET last_seen=:lastSeen WHERE agents_agent.id=:pk`, - map[string]interface{}{"lastSeen": now, "pk": pk}, - ) + if err := dec.Decode(&r); err == nil { + stmt := ` + UPDATE agents_agent + SET version=$1, hostname=$2, operating_system=$3, + plat=$4, total_ram=$5, boot_time=$6, needs_reboot=$7, logged_in_username=$8 + WHERE agents_agent.id=$9;` + + _, err = db.Exec(stmt, r.Version, r.Hostname, r.OS, r.Platform, r.TotalRAM, r.BootTime, r.RebootNeeded, r.Username, pk) + if err != nil { + fmt.Println(err) + } + + if r.Username != "None" { + stmt = `UPDATE agents_agent SET last_logged_in_user=$1 WHERE agents_agent.id=$2;` + _, err = db.Exec(stmt, r.Username, pk) if err != nil { fmt.Println(err) } } } - }(a.AgentID, a.ID, nc, &wg, db, now) + }(a.AgentID, a.ID, nc, &wg, db) } wg.Wait() db.Close() @@ -228,3 +243,15 @@ func randRange(min, max int) int { rand.Seed(time.Now().UnixNano()) return rand.Intn(max-min) + min } + +type AgentInfoRet struct { + AgentPK int `json:"id"` + Version string `json:"version"` + Username string `json:"logged_in_username"` + Hostname string `json:"hostname"` + OS string `json:"operating_system"` + Platform string `json:"plat"` + TotalRAM float64 `json:"total_ram"` + BootTime int64 `json:"boot_time"` + RebootNeeded bool `json:"needs_reboot"` +} diff --git a/restore.sh b/restore.sh index ac72549030..97aebf4583 100755 --- a/restore.sh +++ b/restore.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="29" +SCRIPT_VERSION="30" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh' sudo apt update @@ -105,18 +105,6 @@ sudo systemctl restart systemd-journald.service sudo apt update -print_green 'Downloading NATS' - -nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX) -wget https://github.com/nats-io/nats-server/releases/download/v2.2.6/nats-server-v2.2.6-linux-amd64.tar.gz -P ${nats_tmp} - -tar -xzf ${nats_tmp}/nats-server-v2.2.6-linux-amd64.tar.gz -C ${nats_tmp} - -sudo mv ${nats_tmp}/nats-server-v2.2.6-linux-amd64/nats-server /usr/local/bin/ -sudo chmod +x /usr/local/bin/nats-server -sudo chown ${USER}:${USER} /usr/local/bin/nats-server -rm -rf ${nats_tmp} - print_green 'Installing NodeJS' curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - @@ -176,14 +164,14 @@ print_green 'Installing Python 3.9' 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.2/Python-3.9.2.tgz -tar -xf Python-3.9.2.tgz -cd Python-3.9.2 +wget https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz +tar -xf Python-3.9.6.tgz +cd Python-3.9.6 ./configure --enable-optimizations make -j $numprocs sudo make altinstall cd ~ -sudo rm -rf Python-3.9.2 Python-3.9.2.tgz +sudo rm -rf Python-3.9.6 Python-3.9.6.tgz print_green 'Installing redis and git' @@ -228,6 +216,17 @@ git config user.email "admin@example.com" git config user.name "Bob" git checkout master +print_green 'Restoring NATS' + +NATS_SERVER_VER=$(grep "^NATS_SERVER_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') +nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX) +wget https://github.com/nats-io/nats-server/releases/download/v${NATS_SERVER_VER}/nats-server-v${NATS_SERVER_VER}-linux-amd64.tar.gz -P ${nats_tmp} +tar -xzf ${nats_tmp}/nats-server-v${NATS_SERVER_VER}-linux-amd64.tar.gz -C ${nats_tmp} +sudo mv ${nats_tmp}/nats-server-v${NATS_SERVER_VER}-linux-amd64/nats-server /usr/local/bin/ +sudo chmod +x /usr/local/bin/nats-server +sudo chown ${USER}:${USER} /usr/local/bin/nats-server +rm -rf ${nats_tmp} + print_green 'Restoring MeshCentral' MESH_VER=$(grep "^MESH_VER" /rmm/api/tacticalrmm/tacticalrmm/settings.py | awk -F'[= "]' '{print $5}') @@ -270,7 +269,7 @@ echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini cp $tmp_dir/rmm/local_settings.py /rmm/api/tacticalrmm/tacticalrmm/ cp $tmp_dir/rmm/env /rmm/web/.env gzip -d $tmp_dir/rmm/debug.log.gz -cp $tmp_dir/rmm/debug.log /rmm/api/tacticalrmm/tacticalrmm/private/log/ +cp $tmp_dir/rmm/django_debug.log /rmm/api/tacticalrmm/tacticalrmm/private/log/ cp $tmp_dir/rmm/mesh*.exe /rmm/api/tacticalrmm/tacticalrmm/private/exe/ sudo cp /rmm/natsapi/bin/nats-api /usr/local/bin diff --git a/scripts/Win_10_Upgrade.ps1 b/scripts/Win_10_Upgrade.ps1 index a1e368b849..472055cfbf 100644 --- a/scripts/Win_10_Upgrade.ps1 +++ b/scripts/Win_10_Upgrade.ps1 @@ -1,3 +1,4 @@ + Function Write-LogMessage { param( [Parameter(Mandatory)] @@ -82,12 +83,7 @@ Function Test-FreeSpace { } return $true } -Function Test-License { - #Not a big fan of Doing it this way, but it's a lot easier/faster than the alternatives - $returnVal = $false - if ((cscript "$($env:windir)\system32\\slmgr.vbs" /dli) -match "Licensed") { $returnVal = $true } - return $returnVal -} + Function New-Windows10Install { $ErrorActionPreference = "SilentlyContinue" $dir = "$($env:SystemDrive)\_Windows_FU\packages" diff --git a/scripts/Win_Activation_Check.ps1 b/scripts/Win_Activation_Check.ps1 new file mode 100644 index 0000000000..9454c09c57 --- /dev/null +++ b/scripts/Win_Activation_Check.ps1 @@ -0,0 +1,13 @@ +$WinVerAct = (cscript /Nologo "C:\Windows\System32\slmgr.vbs" /xpr) -join '' + +if ($WinVerAct -like '*Activated*') { + Write-Output "All looks fine $WinVerAct" + exit 0 +} + +else { + Write-Output "Theres an issue $WinVerAct" + exit 1 +} + +Exit $LASTEXITCODE \ No newline at end of file diff --git a/scripts/Win_AnyDesk_Get_Anynet_ID.ps1 b/scripts/Win_AnyDesk_Get_Anynet_ID.ps1 new file mode 100644 index 0000000000..5b3ba83c2a --- /dev/null +++ b/scripts/Win_AnyDesk_Get_Anynet_ID.ps1 @@ -0,0 +1,15 @@ +$Paths = @($Env:APPDATA, $Env:ProgramData, $Env:ALLUSERSPROFILE) + +foreach ($Path in $Paths) { + If (Test-Path $Path\AnyDesk) { + $GoodPath = $Path + } +} + +$ConfigPath = $GoodPath + "\AnyDesk\system.conf" + +$ResultsIdSearch = Select-String -Path $ConfigPath -Pattern ad.anynet.id + +$Result = @($ResultsIdSearch -split '=') + +$Result[1] diff --git a/scripts/Win_Bitdefender_GravityZone_Install.ps1 b/scripts/Win_Bitdefender_GravityZone_Install.ps1 new file mode 100644 index 0000000000..c8213a0519 --- /dev/null +++ b/scripts/Win_Bitdefender_GravityZone_Install.ps1 @@ -0,0 +1,138 @@ +<# +.Synopsis + Installs BitDefender Gravity Zone +.DESCRIPTION + Find the BitDefender URL on your GravityZone page + Network > Packages > Select Name You Want > Send Download Links > Select Installation Links > Appropriate Link + $exe is deprecated. The filename is extracted from $url + $url is the Installation Link in the GravityZone + $log if provided will output verbose logs with timestamps. This can be used to determine how long the installer took. + + TacticalRMM: Need to add Custom Fields to the Client or Site and invoke them in the Script Arguments; example shown. + Name the url "bdurl" in the client custom field. + -url {{client.bdurl} + + SuperOps.ai: Add url and exe run time variables. + +.NOTES + General notes + v1.0 initial release by https://github.com/jhtechIL/ + v1.0.1 has the following changes + - $exe parameter is determined from the $url. A deprecation notice is output if it's specified. The param will + be kept to prevent errors from those that have it specified. + - Dynamically get the temp folder instead of using a hardcoded folder. + - Add many checks to verify we are on the happy path. Output messages if we stray from the happy path. + - Added -log parameter to output verbose logs with timestamps. + - Prefer Exit() over [Environment]::Exit since the later closes the console window while testing. + - Downgrade TLS for Windows prior to Windows 10 + + The string between the [] is a base64 encoded URL for the installer + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("StringBetweenSquareBraces=")) +#> + +param( + [string] $url, + [string] $exe, + [switch] $log +) + +function Get-TimeStamp() { + return Get-Date -UFormat "%Y-%m-%d %H:%M:%S" +} + +if ($log) { + # Skip the "Stdout:" line + Write-Output "" +} + + +if (($exe -ne $null) -and ($exe.Length -gt 0)) { + Write-Output "$(Get-Timestamp) The -exe parameter is deprecated (not needed)" +} + +if (($url -eq $null) -or ($url.Length -eq 0)) { + Write-Output "$(Get-Timestamp) Url parameter is not specified" + Exit(1) +} + +$exe = [uri]::UnescapeDataString($([uri]$url).segments[-1]) +if ($exe -eq $null) { + Write-Output "$(Get-Timestamp) Exe could not be extracted from the URL" + Write-Output "$(Get-Timestamp) Make sure the URL is not modified from the original URL" + Exit(1) +} + +#Check if software is installed. If folder is present, terminate script +if ($log) { + Write-Output "$(Get-Timestamp) Checking if Bitdefender is installed..." +} +$64bit = if ([System.IntPtr]::Size -eq 8) { $true } else { $false } +$RegKeys = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\') +if ($true -eq $64bit) { $RegKeys += 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\' } +$Apps = @($RegKeys | Get-ChildItem | Get-ItemProperty | Where-Object { $_.DisplayName -like "*BitDefender*" }) +if ($Apps.Count -gt 0) { + Write-Output "$(Get-Timestamp) Bitdefender is already installed" + Exit(0) +} + +$tmpDir = [System.IO.Path]::GetTempPath() +if (!(Test-Path $tmpDir)) { + Write-Output "$(Get-Timestamp) Couldn't get path to temp folder" + Exit(1) +} +$tmpExe = Join-Path -Path $tmpDir -ChildPath $exe + +# Download +if ($log) { + Write-Output "$(Get-Timestamp) Bitdefender is not installed" + Write-Output "$(Get-Timestamp) Downloading installer..." +} +if ([Environment]::OSVersion.Version -le (new-object 'Version' 7,0)) { + # This is required for Windows 7, 8.1 + Write-Output "$(Get-Timestamp) Adjusting TLS version(s) for Windows prior to Win 10" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 +} +$(New-Object System.Net.WebClient).DownloadFile($url, $tmpExe) +if (!$?) { + Write-Output "$(Get-Timestamp) Download failed: $($error[0].ToString())" + Write-Output "$(Get-Timestamp) Stacktrace: $($error[0].Exception.ToString())" + Write-Output "$(Get-Timestamp) Filename: ${tmpExe}" + Exit(1) +} +if ((Get-Item -LiteralPath $tmpExe).length -eq 0) { + Write-Output "$(Get-Timestamp) Downloaded file is 0 bytes" + Write-Output "$(Get-Timestamp) Filename: ${tmpExe}" + Get-Item -LiteralPath $tmpExe + Exit(1) +} + +# Install +if ($log) { + Write-Output "$(Get-Timestamp) Downloaded" + Write-Output "$(Get-Timestamp) Installing..." +} +$tmpExe = Get-Item -LiteralPath $tmpExe +Start-Process -FilePath $tmpExe -ArgumentList '/quietinstall /skipeula ' -Wait +if (!$?) { + Write-Output "$(Get-Timestamp) Installation failed: $($error[0].ToString())" + Write-Output "$(Get-Timestamp) Stacktrace: $($error[0].Exception.ToString())" + if (Test-Path -PathType Leaf -Path $tmpExe) { + Remove-Item $tmpExe + } + Exit(1) +} +if ($log) { + Write-Output "$(Get-Timestamp) Installed" + Write-Output "$(Get-Timestamp) Cleaning up temp file..." +} + + +# Cleanup +if (Test-Path -PathType Leaf -Path $tmpExe) { + Remove-Item $tmpExe +} +if ($log) { + Write-Output "$(Get-Timestamp) Finished!" +} +Exit(0) + diff --git a/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 new file mode 100644 index 0000000000..e69bc83de4 --- /dev/null +++ b/scripts/Win_Chocolatey_Manage_Apps_Bulk.ps1 @@ -0,0 +1,55 @@ +<# + .SYNOPSIS + This will install software using the chocolatey, with rate limiting when run with Hosts parameter + .DESCRIPTION + For installing packages using chocolatey. If you're running against more than 10, include the Hosts parameter to limit the speed. If running on more than 30 agents at a time make sure you also change the script timeout setting. + .PARAMETER Mode + 3 options: install (default), uninstall, or upgrade. + .PARAMETER Hosts + Use this to specify the number of computer(s) you're running the command on. This will dynamically introduce waits to try and minimize the chance of hitting rate limits (20/min) on the chocolatey.org site: Hosts 20 + .PARAMETER PackageName + Use this to specify which software to install eg: PackageName googlechrome + .EXAMPLE + Hosts 20 PackageName googlechrome + .EXAMPLE + Mode upgrade Hosts 50 + .EXAMPLE + Mode uninstall PackageName googlechrome + #> + +param ( + [string] $Hosts = "0", + [string] $PackageName, + [string] $Mode = "install", +) + +$ErrorCount = 0 + +if (!$PackageName) { + write-output "No choco package name provided, please include Example: `"PackageName googlechrome`" `n" + $ErrorCount += 1 +} + +if (!$Mode -eq "upgrade") { + $randrange = ($Hosts + 1) * 10 + $rnd = Get-Random -Minimum 1 -Maximum $randrange; + Start-Sleep -Seconds $rnd; + choco ugrade -y all + Write-Output "Running upgrade" + Exit 0 +} + +if (!$Hosts -eq "0") { + write-output "No Hosts Specified, running concurrently" + choco $Mode $PackageName -y + Exit 0 +} +else { + $randrange = ($Hosts + 1) * 6 + $rnd = Get-Random -Minimum 1 -Maximum $randrange; + Start-Sleep -Seconds $rnd; + choco $Mode $PackageName -y + Exit 0 +} + +Exit $LASTEXITCODE \ No newline at end of file diff --git a/scripts/Win_Chocolatey_Update_Installed.bat b/scripts/Win_Chocolatey_Update_Installed.bat deleted file mode 100644 index e4fcd75334..0000000000 --- a/scripts/Win_Chocolatey_Update_Installed.bat +++ /dev/null @@ -1 +0,0 @@ -cup all -y diff --git a/scripts/Win_Feature_NET35_Enable.ps1 b/scripts/Win_Feature_NET35_Enable.ps1 new file mode 100644 index 0000000000..c8e2fd9dc4 --- /dev/null +++ b/scripts/Win_Feature_NET35_Enable.ps1 @@ -0,0 +1 @@ +Enable-WindowsOptionalFeature -Online -FeatureName "NetFx3" \ No newline at end of file diff --git a/scripts/Win_Network_IP_DHCP_Renew.bat b/scripts/Win_Network_IP_DHCP_Renew.bat new file mode 100644 index 0000000000..89bbda09aa --- /dev/null +++ b/scripts/Win_Network_IP_DHCP_Renew.bat @@ -0,0 +1 @@ +ipconfig /release && ipconfig /renew \ No newline at end of file diff --git a/scripts/Win_Rename_Computer.ps1 b/scripts/Win_Rename_Computer.ps1 index 98b8b7eb6f..8e97d2e9af 100644 --- a/scripts/Win_Rename_Computer.ps1 +++ b/scripts/Win_Rename_Computer.ps1 @@ -1,19 +1,83 @@ -# Chanage the computer name in Windows -# v1.0 -# First Command Parameter will be new computer name -# Second Command Parameter if yes will auto-restart computer - -if ($Args.Count -eq 0) { - Write-Output "Computer name arg is required" - exit 1 -} +<# +.SYNOPSIS + Rename computer. + +.DESCRIPTION + Rename domain and non domain joined computers. + +.PARAMETER NewName + Specifies the new computer name. + +.PARAMETER Username + Specifies the username with permission to rename a domain computer. + Required for domain joined computers. + Do not add the domain part like "Domain01\" to the username as that is already extracted and appended to the Rename-Computer cmdlet. + +.PARAMETER Password + Specifies the password for the username. + Required for domain joined computers. + +.PARAMETER Restart + Switch to force the computer to restart after a successful rename. -$param1=$args[0] -$ToRestartTypeYes=$args[1] +.OUTPUTS + Results are printed to the console. -Rename-Computer -newname "$param1" +.EXAMPLE + PS C:\> .\Win_Rename_Computer.ps1 -Username myuser -Password mypassword -NewName mynewname -Restart -# Restart the computer for rename to take effect -if ($ToRestartTypeYes -eq 'yes') { - Restart-Computer -Force +.NOTES + Change Log + V1.0 Initial release + V2.0 Added domain join +#> + +param( + [string] $Username, + [string] $Password, + [switch] $Restart, + [string] $NewName +) + +if (!$NewName){ + Write-Host "-NewName parameter required." + Exit 1 } + +if ((Get-WmiObject win32_computersystem).partofdomain -eq $false) { + # Rename Non Domain Joined Computer + + if ($Restart) { + Rename-computer -NewName $NewName -Force -Restart + } else { + Rename-computer -NewName $NewName -Force + } + Write-Host "Attempted rename of computer to $NewName." + +} +else { + # Rename Domain Joined Computer + + if (!$Username){ + Write-Host "-Username parameter required on domain joined computers." + Exit 1 + } + + if (!$Password){ + Write-Host "-Password parameter required on domain joined computers." + Exit 1 + } + + $securePassword = ConvertTo-SecureString -string $Password -asPlainText -Force + + $domainUsername = (Get-WmiObject Win32_ComputerSystem).Domain + "\$Username" + + $credential = New-Object System.Management.Automation.PSCredential($domainUsername, $securePassword) + + if ($Restart) { + Rename-computer -NewName $NewName -DomainCredential $credential -Force -Restart + } else { + Rename-computer -NewName $NewName -DomainCredential $credential -Force + } + Write-Host "Attempted rename of domain computer to $NewName." +} \ No newline at end of file diff --git a/scripts/Win_Screenconnect_GetGUID.ps1 b/scripts/Win_Screenconnect_GetGUID.ps1 index 1cd423f28e..518278eb28 100644 --- a/scripts/Win_Screenconnect_GetGUID.ps1 +++ b/scripts/Win_Screenconnect_GetGUID.ps1 @@ -19,8 +19,9 @@ if (!$serviceName) { if (!$ErrorCount -eq 0) { exit 1 } - -$imagePath = Get-Itempropertyvalue "HKLM:\SYSTEM\ControlSet001\Services\$serviceName" -Name "ImagePath" + + +$imagePath = (Get-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName).GetValue('ImagePath') $imagePath2 = ($imagePath -split "&s=")[1] $machineGUID = ($imagePath2 -split "&k=")[0] -Write-Output $machineGUID \ No newline at end of file +Write-Output $machineGUID diff --git a/scripts/Win_Services_AutomaticStartup_Running.ps1 b/scripts/Win_Services_AutomaticStartup_Running.ps1 new file mode 100644 index 0000000000..183ea37f40 --- /dev/null +++ b/scripts/Win_Services_AutomaticStartup_Running.ps1 @@ -0,0 +1,19 @@ +### +# Author: Dave Long +# Date: 2021-05-12 +# +# Gets a list of all services that should be running (startup type is automatic), +# but are currently not running and optionally tries to start them. +### + +# To not automatically try to start all non-running automatic services +# change the following variable value to $false + +$AutoStart = $true + +$Services = Get-Service | ` + Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" } + +$Services | Format-Table + +if ($AutoStart) { $Services | Start-Service } diff --git a/scripts/Win_Software_Uninstall.ps1 b/scripts/Win_Software_Uninstall.ps1 new file mode 100644 index 0000000000..7c0246666a --- /dev/null +++ b/scripts/Win_Software_Uninstall.ps1 @@ -0,0 +1,206 @@ +<# +.Synopsis + Allows listing, finding and uninstalling most software on Windows. There will be a best effort to uninstall silently if the silent + uninstall string is not provided. +.DESCRIPTION + Allows listing, finding and uninstalling most software on Windows. There will be a best effort to uninstall silently if the silent + uninstall string is not provided. +.INPUTS + -list Will list all installed 32-bit and 64-bit software installed on the target machine. + -list "" will find a particular application installed giving you the uninstall string and quiet uninstall string if it exists + -list "" -u "" will allow you to uninstall the software from the Windows machine silently + -list "" -u "" will allow you to uninstall the software from the Windows machine silently +.EXAMPLE + Follow the steps below via script arguments to find and then uninstall VLC Media Player. + Step 1: -list "vlc" + Step 1 result: + 1 results + ********** + Name: VLC media player + Version: 3.0.12 + Uninstall String: "C:\Program Files\VideoLAN\VLC\uninstall.exe" + ********** + Step 2: -list "vlc" -u "C:\Program Files\VideoLAN\VLC\uninstall.exe" + Step 3: Will get result back stating if the application has been uninstalled or not. +.EXAMPLE + For a more complex uninstall of for example the Bentley CONNECTION Client with extra arguments. + Step 1: -list "CONNECTION Client" + Step 1 result: + 2 results + ********** + Name: CONNECTION Client + Version: 11.0.3.14 + Silent Uninstall String: "C:\ProgramData\Package Cache\{54c12e19-d8a1-4c26-80cd-6af08f602d4f}\Setup_CONNECTIONClientx64_11.00.03.14.exe" /uninstall /quiet + ********** + Name: CONNECTION Client + Version: 11.00.03.14 + Uninstall String: MsiExec.exe /X{BF2011BD-2485-4CBA-BBFB-93205438C75B} + ********** + Step 2: -list "CONNECTION Client" -u "C:\ProgramData\Package Cache\{54c12e19-d8a1-4c26-80cd-6af08f602d4f}\Setup_CONNECTIONClientx64_11.00.03.14.exe" -args "/uninstall /quiet" + Step 3: Will get result back stating if the application has been uninstalled or not. + .NOTES + See https://github.com/subzdev/uninstall_software/blob/main/uninstall_software.ps1 . If you have extra additions please feel free to contribute and create PR + v1.0 - 8/18/2021 Initial release +#> + +[CmdletBinding()] +param( + [switch]$list, + [string]$find, + [string]$u, + [string]$args +) + +$Paths = @("HKLM:\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\*", "HKLM:\SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\*", "HKU:\*\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*") + +$null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS + +If ($list -And !($find) -And !($u) -And !($args)) { + + $ResultCount = (Get-ItemProperty $Paths | Where-Object { $_.UninstallString -notlike "" } | Measure-Object).Count + Write-Output "$($ResultCount) results `r" + Write-Output "********** `n" + + foreach ($app in Get-ItemProperty $Paths | Where-Object { $_.UninstallString -notlike "" } | Sort-Object DisplayName) { + + if ($app.UninstallString) { + $UninstallString = if ($app.QuietUninstallString) { "Silent Uninstall String: $($app.QuietUninstallString)" } else { "Uninstall String: $($app.UninstallString)" } + Write-Output "Name: $($app.DisplayName)" + Write-Output "Version: $($app.DisplayVersion)" + Write-Output $UninstallString + Write-Output "`r" + Write-Output "**********" + Write-Output "`r" + + } + else { + + } + } +} + +If ($list -And $find -And !($u)) { + + $FindResults = (Get-ItemProperty $Paths | Where-object { $_.Displayname -match [regex]::Escape($find) } | Measure-Object).Count + Write-Output "`r" + Write-Output "$($FindResults) results `r" + Write-Output "********** `n" + foreach ($app in Get-ItemProperty $Paths | Where-Object { $_.Displayname -match [regex]::Escape($find) } | Sort-Object DisplayName) { + + if ($app.UninstallString) { + $UninstallString = if ($app.QuietUninstallString) { "Silent Uninstall String: $($app.QuietUninstallString)" } else { "Uninstall String: $($app.UninstallString)" } + Write-Output "Name: $($app.DisplayName)" + Write-Output "Version: $($app.DisplayVersion)" + Write-Output $UninstallString + Write-Output "`r" + Write-Output "**********" + Write-Output "`r" + + } + else { + + } + } +} + + +################################## +#uninstall code 32-bit and 64-bit +################################# +Function WithArgs ($u, $exeargs) { + Start-Process -Filepath "$u" -ArgumentList $exeargs -Wait + $UninstallTest = (Get-ItemProperty $Paths | Where-object { $_.UninstallString -match [regex]::Escape($u) }).DisplayName + If ($UninstallTest) { + + Write-Output "$($AppName) has not been uninstalled" + + } + else { + + Write-Output "$($AppName) has been uninstalled" + } +} + +$AppName = (Get-ItemProperty $Paths | Where-object { $_.UninstallString -match [regex]::Escape($u) }).DisplayName + +If ($list -And $find -And $u -Or $u -And !($list)) { + + If ($u -Match [regex]::Escape("MsiExec")) { + + $MsiArguments = $u -Replace "MsiExec.exe /I", "/X" -Replace "MsiExec.exe ", "" + Start-Process -FilePath msiexec.exe -ArgumentList "$MsiArguments /quiet /norestart" -Wait + $UninstallTest = (Get-ItemProperty $Paths | Where-object { $_.UninstallString -match [regex]::Escape($u) }).DisplayName + If ($UninstallTest) { + + Write-Output "$($AppName) has not been uninstalled" + + } + else { + + Write-Output "$($AppName) has been uninstalled" + + } + } + else { + If (Test-Path -Path "$u" -PathType Leaf) { + If ($args) { + + $exeargs = $args + ' ' + "/S /SILENT /VERYSILENT /NORESTART" + WithArgs $u $exeargs + } + else { + + $exeargs = "/S /SILENT /VERYSILENT /NORESTART" + WithArgs $u $exeargs + + } + + } + else { + + Write-Output "The path '$($u)' does not exist." + + } + } +} + +If ($list -And $u) { + + If ($u -Match [regex]::Escape("MsiExec")) { + + $MsiArguments = $u -Replace "MsiExec.exe /I", "/X" -Replace "MsiExec.exe ", "" + Start-Process -FilePath msiexec.exe -ArgumentList "$MsiArguments /quiet /norestart" -Wait + $UninstallTest = (Get-ItemProperty $Paths | Where-object { $_.UninstallString -match [regex]::Escape($u) }).DisplayName + If ($UninstallTest) { + + Write-Output "$($AppName) has not been uninstalled" + + } + else { + + Write-Output "$($AppName) has been uninstalled" + } + + } + else { + If (Test-Path -Path "$u" -PathType Leaf) { + If ($args) { + + $exeargs = $args + ' ' + "/S /SILENT /VERYSILENT /NORESTART" + WithArgs $u $exeargs + } + else { + + $exeargs = "/S /SILENT /VERYSILENT /NORESTART" + WithArgs $u $exeargs + + } + + } + else { + + Write-Output "The path '$($u)' does not exist." + + } + } +} \ No newline at end of file diff --git a/scripts/Win_Teamviewer_Get_ID.ps1 b/scripts/Win_Teamviewer_Get_ID.ps1 new file mode 100644 index 0000000000..bb78167e32 --- /dev/null +++ b/scripts/Win_Teamviewer_Get_ID.ps1 @@ -0,0 +1,30 @@ +# Retrieve Teamviewer ID from TRMM agent. This tests versions 6+ known Registry Paths. + +$TeamViewerVersionsNums = @('6', '7', '8', '9', '') +$RegPaths = @('HKLM:\SOFTWARE\TeamViewer', 'HKLM:\SOFTWARE\Wow6432Node\TeamViewer') +$Paths = @(foreach ($TeamViewerVersionsNum in $TeamViewerVersionsNums) { + foreach ($RegPath in $RegPaths) { + $RegPath + $TeamViewerVersionsNum + } + }) + +foreach ($Path in $Paths) { + If (Test-Path $Path) { + $GoodPath = $Path + } +} + +foreach ($FullPath in $GoodPath) { + If ($null -ne (Get-Item -Path $FullPath).GetValue('ClientID')) { + $TeamViewerID = (Get-Item -Path $FullPath).GetValue('ClientID') + $ErrorActionPreference = 'silentlycontinue' + + } + + + +} +Write-Output $TeamViewerID + + +Exit $LASTEXITCODE \ No newline at end of file diff --git a/scripts_wip/Win_Wifi_SSID_and_Password_Retrieval.ps1 b/scripts/Win_Wifi_SSID_and_Password_Retrieval.ps1 similarity index 100% rename from scripts_wip/Win_Wifi_SSID_and_Password_Retrieval.ps1 rename to scripts/Win_Wifi_SSID_and_Password_Retrieval.ps1 diff --git a/scripts_wip/Mac_Users_List.sh b/scripts_wip/Mac_Users_List.sh new file mode 100644 index 0000000000..d3d7f3473f --- /dev/null +++ b/scripts_wip/Mac_Users_List.sh @@ -0,0 +1 @@ +/usr/bin/dscl . -list /Users \ No newline at end of file diff --git a/scripts_wip/Win_Chocolatey_Update_Bulk.bat b/scripts_wip/Win_Chocolatey_Update_Bulk.bat new file mode 100644 index 0000000000..8d2c54ea00 --- /dev/null +++ b/scripts_wip/Win_Chocolatey_Update_Bulk.bat @@ -0,0 +1,43 @@ + + ECHO Enter number of clients you're running against as a parameter if you are running against multiple clients. + ECHO A random sleep time will be introduced to minimize the chance of being temporarily blacklisted + ECHO See https://docs.chocolatey.org/en-us/community-repository/community-packages-disclaimer#rate-limiting + + +IF %1.==. GOTO No1 +IF %2.==. GOTO No2 + + +GOTO End1 + +:No1 +rem No parameters + ECHO Running No1: No parameters provided + cup -y all +GOTO End1 + +:No2 +rem One parameter provided + ECHO Running No2: One Parameter provided + +@echo off & setlocal EnableDelayedExpansion + +for /L %%a in (1) do ( + call:rand 1 %2 + echo !RAND_NUM! +) +:rand +SET /A RAND_NUM=%RANDOM% * (%2 - %1 + 1) / 32768 + %1 +echo RAND_NUM is !RAND_NUM! +Set /A SleepTime=!RAND_NUM! * 60 +echo SleepTime is %SleepTime% + +timeout /t %SleepTime% /nobreak +ECHO finished waiting +cup -y all + +GOTO End1 + +:End1 + +rem We've reached the end \ No newline at end of file diff --git a/scripts_wip/Win_Driver_Restrict_PrinterInstallToAdmin.bat b/scripts_wip/Win_Driver_Restrict_PrinterInstallToAdmin.bat new file mode 100644 index 0000000000..5c81458fda --- /dev/null +++ b/scripts_wip/Win_Driver_Restrict_PrinterInstallToAdmin.bat @@ -0,0 +1 @@ +reg add "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint" /v RestrictDriverInstallationToAdministrators /t REG_DWORD /d 1 /f \ No newline at end of file diff --git a/scripts_wip/Win_File_TakeOwnership.ps1 b/scripts_wip/Win_File_TakeOwnership.ps1 new file mode 100644 index 0000000000..81a1e35c12 --- /dev/null +++ b/scripts_wip/Win_File_TakeOwnership.ps1 @@ -0,0 +1,14 @@ +########################################################################################### +#Take Ownership / Set Folder Permissions v1.0 +#By Alan O'Brien +#Line 8,11,13 + 14: Change the path to the folder that you want to take full control of +#Line 9: Change to whatever account you want applied to the folder to take ownership of it +#Final line will show if the permission applied correctly +########################################################################################### +$ACL = Get-Acl -Path "C:\Users\XXX\XXX\Desktop" +$User = New-Object System.Security.Principal.Ntaccount("BUILTIN\Administrators") +$ACL.SetOwner($User) +$ACL | Set-Acl -Path "C:\Users\XXX\XXX\Desktop" +$ACL.SetAccessRuleProtection($true, $true) +$ACL = Get-Acl -Path "C:\Users\XXX\XXX\Desktop" +Get-ACL -Path "C:\Users\XXX\XXX\Desktop" \ No newline at end of file diff --git a/scripts_wip/Win_Hello_Disable.bat b/scripts_wip/Win_Hello_Disable.bat new file mode 100644 index 0000000000..1138c271d4 --- /dev/null +++ b/scripts_wip/Win_Hello_Disable.bat @@ -0,0 +1,2 @@ +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PassportForWork" /v Enabled /t REG_DWORD /d 0 /f +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PassportForWork" /v DisablePostLogonProvisioning /t REG_DWORD /d 1 /f \ No newline at end of file diff --git a/scripts_wip/Win_InActivity_Timout_Set.ps1 b/scripts_wip/Win_InActivity_Timout_Set.ps1 new file mode 100644 index 0000000000..b176abeaf3 --- /dev/null +++ b/scripts_wip/Win_InActivity_Timout_Set.ps1 @@ -0,0 +1,17 @@ +Write-host "Trusting PS Gallery" +Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + +Write-Host "Installing PolicyFileEditor" +Install-Module -Name PolicyFileEditor + +$UserDir = "$env:windir\system32\GroupPolicy\User\registry.pol" + +Write-Host "Setting inactivity timeout to 10 mins" +$RegPath = 'Software\Policies\Microsoft\Windows\CurrentVersion\Policies\System' +$RegName = 'InactivityTimeoutSecs ' +$RegData = '600' +$RegType = 'DWord' +Set-PolicyFileEntry -Path $UserDir -Key $RegPath -ValueName $RegName -Data $RegData -Type $RegType + +# apply the new policy immediately +gpupdate.exe /force \ No newline at end of file diff --git a/scripts_wip/Win_Network_Hosts_AddRemove.ps1 b/scripts_wip/Win_Network_Hosts_AddRemove.ps1 new file mode 100644 index 0000000000..f3bf1053a8 --- /dev/null +++ b/scripts_wip/Win_Network_Hosts_AddRemove.ps1 @@ -0,0 +1,37 @@ +# For TRMM need to be able to handle all 3 TRMM url's at once. Add these command parameters. Should probably include with howto doc that will use Key Store default recommended keys eg +# rmmurl would be {{global.rmmurl}} +# meshurl would be {{global.meshurl}} +# apiurl would be {{global.apiurl}} +# rmmip would be {{global.rmmip}} +# -allip (does all) 3 URL's with same IP +# -rmmurl +# -meshurl +# -apiurl +# -rmmip +# -meship +# -apiip + + + +# By Tom Chantler - https://tomssl.com/2019/04/30/a-better-way-to-add-and-remove-windows-hosts-file-entries/ +param([bool]$CheckHostnameOnly = $false) +$DesiredIP = $IP +$Hostname = $URL + +# Adds entry to the hosts file. +#Requires -RunAsAdministrator +$hostsFilePath = "$($Env:WinDir)\system32\Drivers\etc\hosts" +$hostsFile = Get-Content $hostsFilePath + +Write-Host "About to add $desiredIP for $Hostname to hosts file" -ForegroundColor Gray + +$escapedHostname = [Regex]::Escape($Hostname) +$patternToMatch = If ($CheckHostnameOnly) { ".*\s+$escapedHostname.*" } Else { ".*$DesiredIP\s+$escapedHostname.*" } +If (($hostsFile) -match $patternToMatch) { + Write-Host $desiredIP.PadRight(20, " ") "$Hostname - not adding; already in hosts file" -ForegroundColor DarkYellow +} +Else { + Write-Host $desiredIP.PadRight(20, " ") "$Hostname - adding to hosts file... " -ForegroundColor Yellow -NoNewline + Add-Content -Encoding UTF8 $hostsFilePath ("$DesiredIP".PadRight(20, " ") + "$Hostname") + Write-Host " done" +} \ No newline at end of file diff --git a/scripts_wip/Win_Powershell_Upgrade.ps1 b/scripts_wip/Win_Powershell_Upgrade.ps1 new file mode 100644 index 0000000000..ac1519c283 --- /dev/null +++ b/scripts_wip/Win_Powershell_Upgrade.ps1 @@ -0,0 +1,20 @@ +# save the file and self-host: https://www.microsoft.com/en-us/download/confirmation.aspx?id=54616 +# Win 2012 x64 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/W2K12-KB3191565-x64.msu +# Win7 x64 and Svr 2008 R2 x64 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7AndW2K8R2-KB3191566-x64.zip +# Win7 x32 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win7-KB3191566-x86.zip +# Win 8.1 x64 and Svr 2012 R2 x64 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu +# Win 81 x32 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1-KB3191564-x86.msu + +if ($PSVersionTable.PSVersion.Major -lt 5) { + Write-Output "Old Version - Need to Upgrade" + # Download MSU file - EDIT THIS URL + # $url = "http://your site.com/Win7AndW2K8R2-KB3191566-x64.msu" + (new-object System.Net.WebClient).DownloadFile($url, 'C:\temp\filename.msu') + + ## Run upgrade process + start-process -FilePath "c:\windows\system32\wusa.exe" -ArgumentList "c:\temp\filename.msu /quiet /norestart /log:c:\temp\log.evt" + Write-Output "Run upgrade process" +} +else { + Write-Output "Already at 5.0 or Higher" +} \ No newline at end of file diff --git a/scripts_wip/Win_Security_Audit.ps1 b/scripts_wip/Win_Security_Audit.ps1 new file mode 100644 index 0000000000..940200fd49 --- /dev/null +++ b/scripts_wip/Win_Security_Audit.ps1 @@ -0,0 +1,420 @@ +# boilerplate +[int]$varBuildString=50 +[int]$varKernel = ([System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Windows\system32\kernel32.dll")).FileBuildPart +$ErrorActionPreference = "stop" +$varTimeZone=(get-itemproperty 'HKLM:\SYSTEM\CurrentControlSet\Control\TimeZoneInformation' -Name TimeZoneKeyName).TimeZoneKeyName -replace '[^a-zA-Z:()\s]',"-" +$varPSVersion= "PowerShell version: " + $PSVersionTable.PSVersion.Major + '.' + $PSVersionTable.PSVersion.Minor +[int]$varDomainRole=(Get-WmiObject -Class Win32_ComputerSystem).DomainRole +[int]$varWarnings=0 +[int]$varAlerts=0 + +if (!([System.Diagnostics.EventLog]::SourceExists("Security Audit"))) { + New-EventLog -LogName 'Application' -Source 'Security Audit' +} + +# preliminary pabulum +write-host "Security Audit: build $varBuildString" +write-host `r +write-host "Local Time: " (get-date) +write-host "Local Timezone: " $varTimeZone +write-host "Windows Version: Build $varKernel`:" (get-WMiObject -computername $env:computername -Class win32_operatingSystem).caption +write-host $varPSVersion + +# workgroup/domain +if (!(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) { + if ((Get-WmiObject -Class Win32_ComputerSystem).Workgroup -match 'WORKGROUP') { + write-host "Workgroup: Default Workgroup Setting `(`"WORKGROUP`"`)" + } else { + write-host "Workgroup: "(Get-WmiObject -Class Win32_ComputerSystem).Workgroup + } +} else { + write-host "Domain: "(Get-WmiObject -Class Win32_ComputerSystem).Domain +} + +write-host "=============================================================================" +write-host `r + +# kernel +if ($varKernel -lt 7601) { + write-host "- ALERT: This Component only runs on devices running Windows 7 SP1/Server 2008 R2 and higher." + write-host " Please update this device." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Error -EventID 59101 -Message "Security Audit Alert: This OS is not supported by Microsoft and will not be receiving Security Updates.`rIt is crucial that this device is upgraded or decommissioned as soon as possible.`rThe audit cannot proceed." + exit +} + +if ($varKernel -eq 7601) { + if ($varDomainRole -gt 1) { + # windows 7 timeout + write-host "- ALERT: Support for Windows 7 was discontinued on the 14th of January 2020." + write-host " This device will not receive security updates and should be upgraded or decommissioned." + write-host " Microsoft will provide extended support for this Operating System at a cost until 2023." + } else { + write-host "- ALERT: Support for Windows Server 2008 R2 was discontinued on the 14th of January 2020." + write-host " This can be mitigated for three years by moving to Azure; if this device has already been" + write-host " migrated to Azure, this message can be disregarded until 2023." + } + $varAlerts++ + write-host `r + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Error -EventID 59103 -Message "Security Audit Alert: This OS is not supported by Microsoft and will not be receiving Security Updates.`rIt is crucial that this device is upgraded or decommissioned as soon as possible." +} + +if ($varKernel -eq 9200) { + if ($varDomainRole -gt 1) { + write-host "- ALERT: Windows 8.0 has been discontinued by Microsoft." #server 2012 still supported until 2023 + write-host " Please update this device to Windows 8.1 or Windows 10." + $varAlerts++ + write-host `r + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Error -EventID 59103 -Message "Security Audit Alert: This OS is not supported by Microsoft and will not be receiving Security Updates.`rIt is crucial that this device is upgraded or decommissioned as soon as possible." + } +} + +write-host "= Account Security Audit ----------------------------------------------------" + +# is admin account disabled? +$localAccountExists = Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='$true'" +If ( -not $localAccountExists ) { + write-host "+ No Local Accounts (Admin, Guest) exist on this device." +} else { + #is guest acct disabled? + if ((Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='$true' AND SID LIKE '%-501'").disabled) { + write-host "+ The Guest account is disabled." + } else { + write-host "- ALERT: The Guest account is enabled." + write-host " Guest accounts are considered unsafe and should be disabled on this device." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59104 -Message "Security Audit Warning: The Guest account is enabled on this device.`rThis unprotected user account can be used as a vantage point by malware and should be disabled." + } + + #is admin acct disabled? + if ((Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='$true' AND SID LIKE '%-500'").disabled) { + write-host "+ The Administrator account is disabled." + } else { + write-host "- ALERT: The Administrator account is enabled." + write-host " Management should be handled by a domain administrator and not the local user." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59105 -Message "Security Audit Warning: The local Administrator account is enabled on this device.`rThis can be used as a vantage point by malware and should be disabled." + } + + + # are all accounts in the administrators group disabled? v2: ps2 compat + $arrLocalAdmins=@() + (Get-WMIObject -Class Win32_Group -Filter "LocalAccount=TRUE and SID='S-1-5-32-544'").GetRelated("Win32_Account","","","","PartComponent","GroupComponent",$FALSE,$NULL) | where-object {$_.Domain -match $env:COMPUTERNAME} | ForEach-Object { + $varCurrentName=$_.Name + if (!(Get-WmiObject -Class Win32_UserAccount -filter "Name like '$varCurrentName' AND LocalAccount=TRUE" | % {$_.disabled})) { + $arrLocalAdmins += ($varCurrentName -as [string]) + } + } + + $arrLocalAdmins = $arrLocalAdmins | where {$_ -match "\w"} + if ($arrLocalAdmins) { + $varWarnings++ + write-host "- WARNING: The following local users are within the `'Administrators`' user group:" + foreach ($iteration in $arrLocalAdmins) { + write-host ": $iteration" + if ((Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) { + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59106 -Message "Security Audit Warning: The local user `"$iteration`" is listed as an Administrator.`rLocal users should not have device-level administrative privileges; devices should be governed by the network administrator." + } else { + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59106 -Message "Security Audit Warning: The local user `"$iteration`" is listed as an Administrator.`rLocal users should not have device-level administrative privileges; the device should be part of, and governed by, a domain." + } + } + } else { + write-host "+ No accounts within the `'Administrators`' group have local access." + } +} + +if ((Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) { + write-host "- WARNING: This device is part of an AD domain." + write-host " If it has not been done already, consider enabling the user-level Active Directory setting" + write-host " `'Account is sensitive and cannot be delegated`' to mitigate the spread of malware via token impersonation." + write-host ' More info: https://www.theregister.co.uk/2018/12/03/notpetya_ncc_eternalglue_production_network/' + $varWarnings++ +} + +# net accounts, since we're not doing anything with the data besides displaying it + +write-host `r +write-host "= Password Policy Audit -----------------------------------------------------" +if (!(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) { + foreach ($iteration in (net accounts | where {$_ -match "\w"})) { + if ($iteration -match ":") {write-host : $iteration} + } +} else { + write-host ": Skipping local password policy audit as device will use domain-enforced policy settings." +} + +# default password for automatic logon +try { + $varDefaultPassLength=((get-itemproperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name defaultPassword).defaultPassword).length + $varDefaultPass=(get-itemproperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name defaultPassword).defaultPassword + $varDefaultUser="undefined" + $varDefaultUser=(get-itemproperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name defaultUserName).defaultUserName + write-host "`- ALERT: A user password is being stored in the Registry in plaintext `($varDefaultPassLength characters.`)" + write-host " It is stored in HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon under value `"DefaultPassword`"." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59107 -Message "Security Audit Warning: Account password for user `"$varDefaultUser`" stored in plaintext in Registry.`rThe user appears to have configured their device to log into their user account automatically via the Registry.`rTheir password is stored in plaintext at HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon under value `"DefaultPassword`" and should be removed ASAP." + #since we have the password, may as well analyse it + # -- length + if ($varDefaultPassLength -le 7) { + write-host "> As the password for username `"$varDefaultUser`" is readily available, it has been analysed for length." + write-host " The user's password is fewer than 8 characters." + write-host " A longer password - or stronger password policies - should be regimented." + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59108 -Message "Security Audit Warning: Since the password for user `"$varDefaultUser`" is stored in plaintext in the Registry, it's been analysed for length.`rThe password is fewer than 8 characters in length. Implement a stronger password or stronger password policy settings." + } + # -- strength + if ($varDefaultPass -match 'password' -or $varDefaultPass -match 'p4ssw0rd' -or $varDefaultPass -match '12345' -or $varDefaultPass -match 'qwerty' -or $varDefaultPass -match 'letmein') { + write-host "> As the password for username `"$varDefaultUser`" is readily available, it has been analysed for strength." + write-host " The user's password is one of many known common passwords." + write-host " A more unique password - or stronger password policies - should be regimented." + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Error -EventID 59109 -Message "Security Audit Alert: Since the password for user `"$varDefaultUser`" is stored in plaintext in the Registry, it's been analysed for security.`rThe password is one of many very well-known common password strings. Implement a more unique password or stronger password policy settings." + } +} catch [System.Exception] { + write-host "+ No account credentials are stored in the Registry." +} + +write-host `r +write-host "= Network Security Audit ----------------------------------------------------" + +# Restrict Null Session Access Value in Registry (shares that are accessible anonymously) +try { + $varNullSession=(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\LanManServer\Parameters').restrictnullsessaccess + if ($varNullSession -ne 1) { + write-host "- ALERT: Device does not restrict access to anonymous shares. This poses a security risk." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59110 -Message "Security Audit Warning: Access to anonymous shares is permitted and should be disabled.`rThe setting is stored in the Registry at HKLM:\SYSTEM\CurrentControlSet\Services\LanManServer\Parameters under value `"RestrictNullSessAccess`"." + } else { + write-host "+ Device restricts access to anonymous shares." + } +} catch [System.Exception] { + write-host ": Unable to determine whether this device restricts access to anonymous shares." +} + +# is telnet server enabled +get-process tlntsvr -erroraction silentlycontinue | out-null +if ($?) { + write-host "- ALERT: Telnet Server is active." + write-host " Telnet is considered insecure as commands are sent in plaintext. Consider using a more secure alternative." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59111 -Message "Security Audit Warning: Telnet Server is running and should be replaced by a more secure alternative." +} else { + write-host "+ Telnet Server is not installed." +} + +# is SMBv1 permitted? +# - server +try { + $varSMBCheck=(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters').SMB1 + if ($varSMBCheck -eq 1) { + write-host "- ALERT: Device is configured as an SMBv1 server." + write-host " This is a huge security risk. This protocol is actively exploited by malware." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59112 -Message "Security Audit Warning: Device serves using the vulnerable and actively-exploited SMBv1 protocol.`rMicrosoft advisory: https://blogs.technet.microsoft.com/filecab/2016/09/16/stop-using-smb1/" + } else { + write-host "+ Device is not configured as an SMBv1 server." + } +} catch [System.Exception] { + write-host "+ Device is not configured as an SMBv1 server." +} + +# - client (https://support.microsoft.com/en-us/help/2696547/how-to-detect-enable-and-disable-smbv1-smbv2-and-smbv3-in-windows-and) +$varClientSMB1=(get-service lanmanserver).requiredservices | where-object {$_.DisplayName -match '1.xxx'} +if ($varClientSMB1) { + write-host "- ALERT: Device is configured as an SMBv1 client." + write-host " This is a huge security risk. This protocol is actively exploited by malware." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59113 -Message "Security Audit Warning: Device is configured as a client for the vulnerable and actively-exploited SMBv1 protocol.`rMicrosoft advisory: https://blogs.technet.microsoft.com/filecab/2016/09/16/stop-using-smb1/" +} else { + write-host "+ Device is not configured as an SMBv1 client." +} + +# windows firewall + +# do you really think there's anybody out there? +if (((Get-WmiObject win32_service -Filter "name like '%mpssvc%'").state) -match 'Running') { + write-host "+ Windows Firewall is running:" + $varFirewallRunning=$true +} else { + write-host "- ALERT: Windows Firewall is not running." + write-host " Unless a third-party Firewall program is running in its stead, please re-enable it." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59119 -Message "Security Audit Warning: Windows Firewall is not running.`rIf this was unintentional, please re-enable Windows Firewall.`rIf this was intentional, please ensure the replacement solution is operational and configured." +} + +# - firewall enabled for private networks? +try { + $varSMBCheck=(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile').EnableFirewall + if ($varSMBCheck -ne 1) { + write-host "- ALERT: Windows Firewall is disabled for Private networks." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59116 -Message "Security Audit Warning: Windows Firewall is disabled for Private networks.`rIf this was unintentional, please revert the setting.`rIf this was intentional, please ensure the replacement solution is operational and configured." + } else { + write-host "+ Windows Firewall is enabled for Private networks." + } +} catch [System.Exception] { + write-host "- ALERT: Unable to ascertain Windows Firewall state for Private networks." + $varAlerts++ +} + +# - firewall enabled for public networks? +try { + $varSMBCheck=(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\PublicProfile').EnableFirewall + if ($varSMBCheck -ne 1) { + write-host "- ALERT: Windows Firewall is disabled for Public networks." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59117 -Message "Security Audit Warning: Windows Firewall is disabled for Public networks.`rIf this was unintentional, please revert the setting.`rIf this was intentional, please ensure the replacement solution is operational and configured." + } else { + write-host "+ Windows Firewall is enabled for Public networks." + } +} catch [System.Exception] { + write-host "- ALERT: Unable to ascertain Windows Firewall state for Public networks." + $varAlerts++ +} + +# - firewall is enabled when connected to a domain? +if ((Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) { + try { + $varSMBCheck=(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\DomainProfile').EnableFirewall + if ($varSMBCheck -ne 1) { + write-host "- ALERT: Windows Firewall is disabled for Domains." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59118 -Message "Security Audit Warning: Windows Firewall is disabled for Domains.`rIf this was unintentional, please revert the setting.`rIf this was intentional, please ensure the replacement solution is operational and configured." + } else { + write-host "+ Windows Firewall is enabled for Domains." + } + } catch [System.Exception] { + write-host "- ALERT: Unable to ascertain Windows Firewall state for Domains." + $varAlerts++ + } +} else { + write-host ": Device is not part of a domain; checks for domain-level firewall compliance skipped." +} + +# - show active profiles. this will read strangely but it's the only way to do it without butchering the i18n +if ($varFirewallRunning) { + write-host "= Active Windows Firewall Profile Settings (from NETSH):" + foreach ($iteration in (netsh advfirewall show currentprofile | select-string ":" | select-string " ")) { + $varActiveProfile = $iteration -as [string] + write-host ": " $varActiveProfile.substring(0,$varActiveProfile.Length-2) + } +} else { + write-host ": Not showing active Windows Firewall profile/s as Windows Firewall is not running." +} + +# teamviewer +Get-ChildItem "C:\Users" | ?{ $_.PSIsContainer } | % { + if (test-path "C:\Users\$_\AppData\Roaming\TeamViewer\Connections.txt") { + write-host "- WARNING: User `"$_`" has used TeamViewer software." + write-host " While TeamViewer is not inherently unsafe, any remote connection should be scrutinised." + $varTeamViewer=$true + $varWarnings++ + } +} +if (!$varTeamViewer) { + write-host "+ TeamViewer has not been used on this device. (All connections should go via Datto RMM.)" +} + +write-host `r +write-host "= Device Security Audit -----------------------------------------------------" + +if ($varKernel -ge 9200) { + try { + $varSecureBoot=Confirm-SecureBootUEFI + if ($varSecureBoot) { + write-host "+ UEFI Secure Boot is supported and enabled on this device." + } else { + write-host "- ALERT: UEFI Secure Boot is supported but not enabled on this device." + write-host " This setting should be enabled to prevent malware from interfering with the Windows boot process." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59114 -Message "Security Audit Warning: UEFI Secure Boot is supported on this device but has not been enabled.`rThis may have been configured deliberately to facilitate installation of other Operating Systems that do not have a Microsoft Secure Boot shim available; however, the setting still leaves a device vulnerable and should be changed." + } + } catch [PlatformNotSupportedException] { + write-host ": UEFI Secure Boot is not supported on this device." + write-host " The device may use the legacy BIOS platform instead of UEFI or it may be a virtual machine." + } +} else { + write-host ": UEFI Secure Boot is not supported on Windows 7." +} + +write-host "= Windows 10 Exploit Protection settings:" + +if ($varKernel -ge 16299) { + $varExploitProtection=Get-ProcessMitigation -System + if ($varExploitProtection.DEP.Enable -match 'OFF') {$varExploitFlaws+="Enable DEP / "} + if ($varExploitProtection.CFG.Enable -match 'OFF') {$varExploitFlaws+="Enable Control Flow Guard / "} + if ($varExploitProtection.ASLR.BottomUp -match 'OFF') {$varExploitFlaws+="Enable Bottom-up ASLR / "} + if ($varExploitProtection.ASLR.HighEntropy -match 'OFF') {$varExploitFlaws+="Enable High-Entropy ASLR / "} + if ($varExploitProtection.SEHOP.Enable -match 'OFF') {$varExploitFlaws+="Enable Exception Chain Validation (SEHOP) / "} + if ($varExploitProtection.Heap.TerminateOnError -match 'OFF') {$varExploitFlaws+="Enable Heap Integrity Validation"} + + if ($varExploitFlaws) { + write-host "- WARNING: System Exploit Protection configuration differs from Windows 10 Exploit Protection Settings." + write-host " These settings were configured deliberately, most likely in response to a compatibility conflict." + write-host " Mitigation steps are listed below. Compare them closely with your system configuration." + write-host ": $varExploitFlaws" + $varWarnings++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Warning -EventID 59115 -Message "Security Audit Warning: Windows 10 Exploit Protection settings have been altered from the default.`rThis is generally done deliberately by the end-user or administrator in order to mitigate against a specific compatibility or performance issue.`rRegardless, it is bad practice to deviate from Microsoft's standards. Please scrutinise the mitigation steps below and ensure you have a strong justification for dismissing each.`r$varExploitFlaws" + } else { + write-host "+ Main Windows 10 Exploit Protection Settings have not been altered from default recommendations." + } +} else { + write-host ": Windows 10 Exploit Protection is only available from Windows 10 build 1709 onward." + write-host " Older systems may benefit from the Microsoft Enhanced Mitigation Toolkit (EMET)." +} + +#security policy I: get the data +[array]$arrSecurityPolicies=@() +try { + Get-ChildItem -Recurse 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Safer\codeidentifiers\0\Paths' | % { + [array]$arrSecurityPolicies += (Get-ItemProperty registry::$_).ItemData + } +} catch [System.Exception] { + write-host "- ALERT: Device contains no security policies. These can be used to halt execution of hazardous file types." + write-host " Please consider blocking execution of CPL, SCR, VBS and the Right-to-Left Override character in SecPol.msc." + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Error -EventID 59120 -Message "Security Audit Alert: The Windows Security Policy is not configured to block files with dangerous extensions from executing.`rThese file types are: $varFileRisks`.`rIn addition, the right-to-left unicode character should also be blocked to mitigate against extension masquerade attacks.`rMore information: https://www.ipa.go.jp/security/english/virus/press/201110/E_PR201110.html" + $varNoSecPols=$true +} + +#security policy II: parse the data -- i know this seems like bizarre logic but match seems to be a law unto itself +if (!$varNoSecPols) { + if ($arrSecurityPolicies -match '.VBS') { + #do nothing + } else { + $varFileRisks+="VBS, " + } + if ($arrSecurityPolicies -match '.CPL') { + #do nothing + } else { + $varFileRisks+="CPL, " + } + if ($arrSecurityPolicies -match '.SCR') { + #do nothing + } else { + $varFileRisks+="SCR, " + } + if ($arrSecurityPolicies -match "\u202E") { + #do nothing + } else { + $varFileRisks+="Right-to-Left override" + } +} + +#security policy III: deliver the results +if ($varFileRisks) { + write-host "- ALERT: The Security Policy does not prohibit execution of problematic file types (https://goo.gl/P6ec8q)." + write-host " These file types are: $varFileRisks" + $varAlerts++ + Write-EventLog -LogName 'Application' -Source 'Security Audit' -EntryType Error -EventID 59120 -Message "Security Audit Alert: The Windows Security Policy is not configured to block files with dangerous extensions from executing.`rThese file types are: $varFileRisks`.`rIn addition, the right-to-left unicode character should also be blocked to mitigate against extension masquerade attacks.`rMore information: https://www.ipa.go.jp/security/english/virus/press/201110/E_PR201110.html" +} + +write-host `r +write-host "=============================================================================" +if ($varWarnings -ge 1) { + write-host "- Total warnings: $varWarnings" +} +if ($varAlerts -ge 1) { + write-host "- Total alerts: $varAlerts" +} +write-host "=============================================================================" +write-host "Security audit completed at $(get-date)" +write-host "You may consider also running the BitLocker audit Component on this device." \ No newline at end of file diff --git a/scripts_wip/Win_Teamviewer_Get_ID.ps1 b/scripts_wip/Win_Teamviewer_Get_ID.ps1 deleted file mode 100644 index 464483b6da..0000000000 --- a/scripts_wip/Win_Teamviewer_Get_ID.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -# Retrieve Teamviewer ID from TRMM agent - -$clientId = Get-ItemProperty HKLM:\SOFTWARE\Wow6432Node\TeamViewer -Name ClientID -ErrorAction SilentlyContinue - -If (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\TeamViewer' -Name ClientID -ErrorAction SilentlyContinue) { - - Write-Output $clientid.Clientid - exit 0 - -} -Else { - - Write-Output 'Teamviewer is not installed.' - exit 1 -} - -Exit $LASTEXITCODE \ No newline at end of file diff --git a/scripts_wip/Win_User_Password_Reset.ps1 b/scripts_wip/Win_User_Password_Reset.ps1 new file mode 100644 index 0000000000..c8e158c82c --- /dev/null +++ b/scripts_wip/Win_User_Password_Reset.ps1 @@ -0,0 +1,8 @@ +# Set the Password-String -- defaults to THIS.IS.NOT.SECURE +$newpwd = ConvertTo-SecureString -String "THIS.IS.NOT.SECURE" -AsPlainText ?Force + +# Set the correct local user you want to reset +$UserAccount = Get-LocalUser -Name "ADMINUSER" + +# Set it +$UserAccount | Set-LocalUser -Password $newpwd \ No newline at end of file diff --git a/scripts_wip/Win_WeatherNews_Taskbar.bat b/scripts_wip/Win_WeatherNews_Taskbar.bat new file mode 100644 index 0000000000..b66c391804 --- /dev/null +++ b/scripts_wip/Win_WeatherNews_Taskbar.bat @@ -0,0 +1,5 @@ +REM turns off the task icon for news and weather icon for windows 10 build 21H1 +REM key switches 0 - on 1 - hides 2 - off +REM reg delete HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Feeds /f removes it completely + +reg add HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Feeds /v ShellFeedsTaskbarViewMode /t REG_Dword /d 2 /f diff --git a/update.sh b/update.sh index e3a87cfb74..5a5e395c71 100644 --- a/update.sh +++ b/update.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_VERSION="124" +SCRIPT_VERSION="125" SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh' LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py' YELLOW='\033[1;33m' @@ -63,6 +63,7 @@ fi LATEST_MESH_VER=$(grep "^MESH_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') LATEST_PIP_VER=$(grep "^PIP_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') LATEST_NPM_VER=$(grep "^NPM_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') +NATS_SERVER_VER=$(grep "^NATS_SERVER_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}') CURRENT_PIP_VER=$(grep "^PIP_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') CURRENT_NPM_VER=$(grep "^NPM_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') @@ -174,24 +175,24 @@ if ! [[ $HAS_PY39 ]]; then 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.2/Python-3.9.2.tgz - tar -xf Python-3.9.2.tgz - cd Python-3.9.2 + wget https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz + tar -xf Python-3.9.6.tgz + cd Python-3.9.6 ./configure --enable-optimizations make -j $numprocs sudo make altinstall cd ~ - sudo rm -rf Python-3.9.2 Python-3.9.2.tgz + sudo rm -rf Python-3.9.6 Python-3.9.6.tgz fi -HAS_NATS220=$(/usr/local/bin/nats-server -version | grep v2.2.6) -if ! [[ $HAS_NATS220 ]]; then - printf >&2 "${GREEN}Updating nats to v2.2.6${NC}\n" +HAS_LATEST_NATS=$(/usr/local/bin/nats-server -version | grep "${NATS_SERVER_VER}") +if ! [[ $HAS_LATEST_NATS ]]; then + printf >&2 "${GREEN}Updating nats to v${NATS_SERVER_VER}${NC}\n" nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX) - wget https://github.com/nats-io/nats-server/releases/download/v2.2.6/nats-server-v2.2.6-linux-amd64.tar.gz -P ${nats_tmp} - tar -xzf ${nats_tmp}/nats-server-v2.2.6-linux-amd64.tar.gz -C ${nats_tmp} + wget https://github.com/nats-io/nats-server/releases/download/v${NATS_SERVER_VER}/nats-server-v${NATS_SERVER_VER}-linux-amd64.tar.gz -P ${nats_tmp} + tar -xzf ${nats_tmp}/nats-server-v${NATS_SERVER_VER}-linux-amd64.tar.gz -C ${nats_tmp} sudo rm -f /usr/local/bin/nats-server - sudo mv ${nats_tmp}/nats-server-v2.2.6-linux-amd64/nats-server /usr/local/bin/ + sudo mv ${nats_tmp}/nats-server-v${NATS_SERVER_VER}-linux-amd64/nats-server /usr/local/bin/ sudo chmod +x /usr/local/bin/nats-server sudo chown ${USER}:${USER} /usr/local/bin/nats-server rm -rf ${nats_tmp} diff --git a/web/package-lock.json b/web/package-lock.json index a789420e74..505a5a22ac 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -5,21 +5,22 @@ "requires": true, "packages": { "": { + "name": "web", "version": "0.1.8", "dependencies": { - "@quasar/extras": "^1.10.8", + "@quasar/extras": "^1.10.11", "apexcharts": "^3.27.1", "axios": "^0.21.1", "dotenv": "^8.6.0", "prismjs": "^1.23.0", "qrcode.vue": "^3.2.2", - "quasar": "^2.0.1", + "quasar": "^2.0.4", "vue-prism-editor": "^2.0.0-alpha.2", "vue3-apexcharts": "^1.4.0", "vuex": "^4.0.2" }, "devDependencies": { - "@quasar/app": "^3.0.1", + "@quasar/app": "^3.1.0", "@quasar/cli": "^1.2.1" } }, @@ -1891,9 +1892,9 @@ } }, "node_modules/@quasar/app": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@quasar/app/-/app-3.0.1.tgz", - "integrity": "sha512-a1hm4miFkvc9setIqtVAKyILHhJ0ZD+Xw52gtGgwwSNGQfKL6UY04837bkjzVOXQ1ybcd5Kpjv3kWPhdTL3TZA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@quasar/app/-/app-3.1.0.tgz", + "integrity": "sha512-b0yCblS5yVYxNjFIuCf2xoZZsNXlq1RQM/b2PxZqlGxOWG4AM02HxLSUrb1YvhwnYsYxo2qm1dbF52Ut6oE/iw==", "dev": true, "dependencies": { "@quasar/babel-preset-app": "2.0.1", @@ -1905,32 +1906,32 @@ "@types/terser-webpack-plugin": "5.0.3", "@types/webpack-bundle-analyzer": "4.4.0", "@types/webpack-dev-server": "3.11.3", - "@vue/compiler-sfc": "3.1.2", - "@vue/server-renderer": "3.1.2", + "@vue/compiler-sfc": "3.2.4", + "@vue/server-renderer": "3.2.4", "archiver": "5.3.0", - "autoprefixer": "10.2.6", + "autoprefixer": "10.3.1", "browserslist": "^4.12.0", - "chalk": "4.1.1", + "chalk": "4.1.2", "chokidar": "3.5.2", "ci-info": "3.2.0", - "compression-webpack-plugin": "8.0.0", - "copy-webpack-plugin": "9.0.0", + "compression-webpack-plugin": "8.0.1", + "copy-webpack-plugin": "9.0.1", "cross-spawn": "7.0.3", "css-loader": "5.2.6", - "css-minimizer-webpack-plugin": "3.0.1", - "cssnano": "5.0.6", + "css-minimizer-webpack-plugin": "3.0.2", + "cssnano": "5.0.8", "dot-prop": "6.0.1", "elementtree": "0.1.7", "error-stack-parser": "2.0.6", "express": "4.17.1", - "fast-glob": "3.2.5", + "fast-glob": "3.2.7", "file-loader": "6.2.0", "fork-ts-checker-webpack-plugin": "6.1.0", "fs-extra": "10.0.0", "hash-sum": "2.0.0", "html-minifier": "4.0.0", - "html-webpack-plugin": "5.3.1", - "inquirer": "8.1.1", + "html-webpack-plugin": "5.3.2", + "inquirer": "8.1.2", "isbinaryfile": "4.0.8", "launch-editor-middleware": "2.2.1", "lodash.debounce": "4.0.8", @@ -1945,29 +1946,28 @@ "open": "7.1.0", "ouch": "2.0.0", "postcss": "^8.2.10", - "postcss-loader": "5.3.0", + "postcss-loader": "6.1.1", "postcss-rtlcss": "3.3.4", - "pretty-error": "3.0.3", + "pretty-error": "3.0.4", "register-service-worker": "1.7.2", "sass": "1.32.12", - "sass-loader": "12.0.0", + "sass-loader": "12.1.0", "semver": "7.3.5", "table": "6.7.1", - "terser-webpack-plugin": "5.1.3", + "terser-webpack-plugin": "5.1.4", "ts-loader": "8.0.17", "typescript": "4.2.2", "url-loader": "4.1.1", - "vue": "3.1.2", - "vue-loader": "16.2.0", - "vue-router": "4.0.10", + "vue": "3.2.4", + "vue-loader": "16.4.1", + "vue-router": "4.0.11", "vue-style-loader": "4.1.3", "webpack": "^5.35.0", "webpack-bundle-analyzer": "4.4.2", "webpack-chain": "6.5.1", - "webpack-dev-server": "4.0.0-beta.3", + "webpack-dev-server": "4.0.0", "webpack-merge": "5.8.0", - "webpack-node-externals": "3.0.0", - "zlib": "1.0.5" + "webpack-node-externals": "3.0.0" }, "bin": { "quasar": "bin/quasar" @@ -1982,6 +1982,22 @@ "url": "https://donate.quasar.dev" } }, + "node_modules/@quasar/app/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@quasar/babel-preset-app": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@quasar/babel-preset-app/-/babel-preset-app-2.0.1.tgz", @@ -2132,9 +2148,9 @@ } }, "node_modules/@quasar/extras": { - "version": "1.10.8", - "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.10.8.tgz", - "integrity": "sha512-s/uTQ/NVQIExOZjXJ0LnpPlqiiuGV0PFw5UKbXypWMhPpfQInnic/uU3SR8ZGtlNC3KHV5U+NTk9K9muTtoDpA==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.10.11.tgz", + "integrity": "sha512-/zJiT8iExl0j2k1zA21Eho8SPMtG5ehcYayszunrq/z7zDp728oWSteI9AfQFnF8/+M06f5HUzy+Vssf6IKH/g==", "funding": { "type": "github", "url": "https://donate.quasar.dev" @@ -2289,9 +2305,9 @@ } }, "node_modules/@types/html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", "dev": true }, "node_modules/@types/http-proxy": { @@ -2304,9 +2320,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", "dev": true }, "node_modules/@types/mime": { @@ -2346,9 +2362,9 @@ "dev": true }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, "node_modules/@types/serve-static": { @@ -2485,13 +2501,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.2.tgz", - "integrity": "sha512-nHmq7vLjq/XM2IMbZUcKWoH5sPXa2uR/nIKZtjbK5F3TcbnYE/zKsrSUR9WZJ03unlwotNBX1OyxVt9HbWD7/Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.4.tgz", + "integrity": "sha512-c8NuQq7mUXXxA4iqD5VUKpyVeklK53+DMbojYMyZ0VPPrb0BUWrZWFiqSDT+MFDv0f6Hv3QuLiHWb1BWMXBbrw==", "dependencies": { "@babel/parser": "^7.12.0", "@babel/types": "^7.12.0", - "@vue/shared": "3.1.2", + "@vue/shared": "3.2.4", "estree-walker": "^2.0.1", "source-map": "^0.6.1" } @@ -2505,27 +2521,27 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.2.tgz", - "integrity": "sha512-k2+SWcWH0jL6WQAX7Or2ONqu5MbtTgTO0dJrvebQYzgqaKMXNI90RNeWeCxS4BnNFMDONpHBeFgbwbnDWIkmRg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.4.tgz", + "integrity": "sha512-uj1nwO4794fw2YsYas5QT+FU/YGrXbS0Qk+1c7Kp1kV7idhZIghWLTjyvYibpGoseFbYLPd+sW2/noJG5H04EQ==", "dependencies": { - "@vue/compiler-core": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-core": "3.2.4", + "@vue/shared": "3.2.4" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.2.tgz", - "integrity": "sha512-SeG/2+DvwejQ7oAiSx8BrDh5qOdqCYHGClPiTvVIHTfSIHiS2JjMbCANdDCjHkTOh/O7WZzo2JhdKm98bRBxTw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.4.tgz", + "integrity": "sha512-GM+ouDdDzhqgkLmBH4bgq4kiZxJQArSppJiZHWHIx9XRaefHLmc1LBNPmN8ivm4SVfi2i7M2t9k8ZnjsScgzPQ==", "dev": true, "dependencies": { "@babel/parser": "^7.13.9", "@babel/types": "^7.13.0", "@types/estree": "^0.0.48", - "@vue/compiler-core": "3.1.2", - "@vue/compiler-dom": "3.1.2", - "@vue/compiler-ssr": "3.1.2", - "@vue/shared": "3.1.2", + "@vue/compiler-core": "3.2.4", + "@vue/compiler-dom": "3.2.4", + "@vue/compiler-ssr": "3.2.4", + "@vue/shared": "3.2.4", "consolidate": "^0.16.0", "estree-walker": "^2.0.1", "hash-sum": "^2.0.0", @@ -2536,9 +2552,6 @@ "postcss-modules": "^4.0.0", "postcss-selector-parser": "^6.0.4", "source-map": "^0.6.1" - }, - "peerDependencies": { - "vue": "3.1.2" } }, "node_modules/@vue/compiler-sfc/node_modules/@types/estree": { @@ -2557,13 +2570,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.2.tgz", - "integrity": "sha512-BwXo9LFk5OSWdMyZQ4bX1ELHX0Z/9F+ld/OaVnpUPzAZCHslBYLvyKUVDwv2C/lpLjRffpC2DOUEdl1+RP1aGg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.4.tgz", + "integrity": "sha512-bKZuXu9/4XwsFHFWIKQK+5kN7mxIIWmMmT2L4VVek7cvY/vm3p4WTsXYDGZJy0htOTXvM2ifr6sflg012T0hsw==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-dom": "3.2.4", + "@vue/shared": "3.2.4" } }, "node_modules/@vue/devtools-api": { @@ -2572,49 +2585,49 @@ "integrity": "sha512-44fPrrN1cqcs6bFkT0C+yxTM6PZXLbR+ESh1U1j8UD22yO04gXvxH62HApMjLbS3WqJO/iCNC+CYT+evPQh2EQ==" }, "node_modules/@vue/reactivity": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.2.tgz", - "integrity": "sha512-glJzJoN2xE7I2lRvwKM5u1BHRPTd1yc8iaf//Lai/78/uYAvE5DXp5HzWRFOwMlbRvMGJHIQjOqoxj87cDAaag==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.4.tgz", + "integrity": "sha512-ljWTR0hr8Tn09hM2tlmWxZzCBPlgGLnq/k8K8X6EcJhtV+C8OzFySnbWqMWataojbrQOocThwsC8awKthSl2uQ==", "dependencies": { - "@vue/shared": "3.1.2" + "@vue/shared": "3.2.4" } }, "node_modules/@vue/runtime-core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.2.tgz", - "integrity": "sha512-gsPZG4dRIkixuuKmoj4P9IHgfT0yaFLcqWOM5F/bCk0nxQn1XtxH8oUehWuET726KhbukvDoJfe9G2CKviy80w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.4.tgz", + "integrity": "sha512-W6PtEOs8P8jKYPo3JwaMAozZQivxInUleGfNwI2pK1t8ZLZIxn4kAf7p4VF4jJdQB8SZBzpfWdLUc06j7IOmpQ==", "dependencies": { - "@vue/reactivity": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/reactivity": "3.2.4", + "@vue/shared": "3.2.4" } }, "node_modules/@vue/runtime-dom": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.2.tgz", - "integrity": "sha512-QvINxjLucEZFzp5f0NVu7JqWYCv5TKQfkH2FDs/N6QNE4iKcYtKrWdT0HKfABnVXG28Znqv6rIH0dH4ZAOwxpA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.4.tgz", + "integrity": "sha512-HcVtLyn2SGwsf6BFPwkvDPDOhOqkOKcfHDpBp5R1coX+qMsOFrY8lJnGXIY+JnxqFjND00E9+u+lq5cs/W7ooA==", "dependencies": { - "@vue/runtime-core": "3.1.2", - "@vue/shared": "3.1.2", + "@vue/runtime-core": "3.2.4", + "@vue/shared": "3.2.4", "csstype": "^2.6.8" } }, "node_modules/@vue/server-renderer": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.1.2.tgz", - "integrity": "sha512-XDw8KTrz/siiU5p6Zlicvf2KIjSZrqaxATBPM/9FYNnyv4LTS14JC5daTL13rk50d3UPBurRR/3wJupVvtQJ4w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.4.tgz", + "integrity": "sha512-ai9WxJ78nnUDk+26vwZhlA1Quz3tA+90DgJX6iseen2Wwnndd91xicFW+6ROR/ZP0yFNuQ017eZJBw8OqoPL+w==", "dev": true, "dependencies": { - "@vue/compiler-ssr": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-ssr": "3.2.4", + "@vue/shared": "3.2.4" }, "peerDependencies": { - "vue": "3.1.2" + "vue": "3.2.4" } }, "node_modules/@vue/shared": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.2.tgz", - "integrity": "sha512-EmH/poaDWBPJaPILXNI/1fvUbArJQmmTyVCwvvyDYDFnkPoTclAbHRAtyIvqfez7jybTDn077HTNILpxlsoWhg==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.4.tgz", + "integrity": "sha512-j2j1MRmjalVKr3YBTxl/BClSIc8UQ8NnPpLYclxerK65JIowI4O7n8O8lElveEtEoHxy1d7BelPUDI0Q4bumqg==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.0", @@ -3169,13 +3182,13 @@ } }, "node_modules/autoprefixer": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.6.tgz", - "integrity": "sha512-8lChSmdU6dCNMCQopIf4Pe5kipkAGj/fvTMslCsih0uHpOrXOPUEVOmYMMqmw3cekQkSD7EhIeuYl5y0BLdKqg==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.1.tgz", + "integrity": "sha512-L8AmtKzdiRyYg7BUXJTzigmhbQRCXFKz6SA1Lqo0+AR2FBbQ4aTAPFSDlOutnFkjhiz8my4agGXog1xlMjPJ6A==", "dev": true, "dependencies": { "browserslist": "^4.16.6", - "caniuse-lite": "^1.0.30001230", + "caniuse-lite": "^1.0.30001243", "colorette": "^1.2.2", "fraction.js": "^4.1.1", "normalize-range": "^0.1.2", @@ -3717,9 +3730,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001239", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz", - "integrity": "sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ==", + "version": "1.0.30001248", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz", + "integrity": "sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw==", "dev": true, "funding": { "type": "opencollective", @@ -4016,9 +4029,9 @@ "dev": true }, "node_modules/colord": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.0.1.tgz", - "integrity": "sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.7.0.tgz", + "integrity": "sha512-pZJBqsHz+pYyw3zpX6ZRXWoCHM1/cvFikY9TV8G3zcejCaKE0lhankoj8iScyrrePA8C7yJ5FStfA9zbcOnw7Q==", "dev": true }, "node_modules/colorette": { @@ -4094,13 +4107,13 @@ } }, "node_modules/compression-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-/pBUx1gV8nL6YPKKa9QRs4xoemU28bfqAu8z5hLy2eTrWog4fU8lQYtlpbYwCWtIfAHLxsgSUHjk9RqK6UL+Yw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-8.0.1.tgz", + "integrity": "sha512-VWDXcOgEafQDMFXEnoia0VBXJ+RMw81pmqe/EBiOIBnMfY8pG26eqwIS/ytGpzy1rozydltL0zL6KDH9XNWBxQ==", "dev": true, "dependencies": { "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1" + "serialize-javascript": "^6.0.0" }, "engines": { "node": ">= 12.13.0" @@ -4114,12 +4127,12 @@ } }, "node_modules/compression-webpack-plugin/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" }, @@ -4131,6 +4144,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/compression-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4258,9 +4280,9 @@ "dev": true }, "node_modules/copy-webpack-plugin": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.0.tgz", - "integrity": "sha512-k8UB2jLIb1Jip2nZbCz83T/XfhfjX6mB1yLJNYKrpYi7FQimfOoFv/0//iT6HV1K8FwUB5yUbCcnpLebJXJTug==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.1.tgz", + "integrity": "sha512-14gHKKdYIxF84jCEgPgYXCPpldbwpxxLbCmA7LReY7gvbaT555DgeBWBgBZM116tv/fO6RRJrsivBqRyRlukhw==", "dev": true, "dependencies": { "fast-glob": "^3.2.5", @@ -4269,7 +4291,7 @@ "normalize-path": "^3.0.0", "p-limit": "^3.1.0", "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1" + "serialize-javascript": "^6.0.0" }, "engines": { "node": ">= 12.13.0" @@ -4283,9 +4305,9 @@ } }, "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.0.tgz", - "integrity": "sha512-Hdd4287VEJcZXUwv1l8a+vXC1GjOQqXe+VS30w/ypihpcnu9M1n3xeYeJu5CBpeEQj2nAab2xxz28GuA3vp4Ww==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.1.tgz", + "integrity": "sha512-kEVjS71mQazDBHKcsq4E9u/vUzaLcw1A8EtUeydawvIWQCJM0qQ08G1H7/XTjFUulla6XQiDOG6MXSaG0HDKog==", "dev": true, "dependencies": { "is-glob": "^4.0.1" @@ -4295,12 +4317,12 @@ } }, "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" }, @@ -4312,6 +4334,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/core-js": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz", @@ -4366,9 +4397,9 @@ } }, "node_modules/cosmiconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", - "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", @@ -4443,9 +4474,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz", - "integrity": "sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.1.tgz", + "integrity": "sha512-BZ1aOuif2Sb7tQYY1GeCjG7F++8ggnwUkH5Ictw0mrdpqpEd+zWmcPdstnH2TItlb74FqR0DrVEieon221T/1Q==", "dev": true, "dependencies": { "timsort": "^0.3.0" @@ -4518,17 +4549,17 @@ } }, "node_modules/css-minimizer-webpack-plugin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.1.tgz", - "integrity": "sha512-RGFIv6iZWUPO2T1vE5+5pNCSs2H2xtHYRdfZPiiNH8Of6QOn9BeFnZSoHiQMkmsxOO/JkPe4BpKfs7slFIWcTA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz", + "integrity": "sha512-B3I5e17RwvKPJwsxjjWcdgpU/zqylzK1bPVghcmpFHRL48DXiBgrtqz1BJsn68+t/zzaLp9kYAaEDvQ7GyanFQ==", "dev": true, "dependencies": { - "cssnano": "^5.0.0", + "cssnano": "^5.0.6", "jest-worker": "^27.0.2", "p-limit": "^3.0.2", - "postcss": "^8.2.9", + "postcss": "^8.3.5", "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", + "serialize-javascript": "^6.0.0", "source-map": "^0.6.1" }, "engines": { @@ -4551,12 +4582,12 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" }, @@ -4568,6 +4599,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/css-minimizer-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4640,14 +4680,15 @@ } }, "node_modules/cssnano": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.6.tgz", - "integrity": "sha512-NiaLH/7yqGksFGsFNvSRe2IV/qmEBAeDE64dYeD8OBrgp6lE8YoMeQJMtsv5ijo6MPyhuoOvFhI94reahBRDkw==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.8.tgz", + "integrity": "sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==", "dev": true, "dependencies": { - "cosmiconfig": "^7.0.0", - "cssnano-preset-default": "^5.1.3", - "is-resolvable": "^1.1.0" + "cssnano-preset-default": "^5.1.4", + "is-resolvable": "^1.1.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" }, "engines": { "node": "^10 || ^12 || >=14.0" @@ -4661,9 +4702,9 @@ } }, "node_modules/cssnano-preset-default": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.3.tgz", - "integrity": "sha512-qo9tX+t4yAAZ/yagVV3b+QBKeLklQbmgR3wI7mccrDcR+bEk9iHgZN1E7doX68y9ThznLya3RDmR+nc7l6/2WQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz", + "integrity": "sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==", "dev": true, "dependencies": { "css-declaration-sorter": "^6.0.3", @@ -4678,7 +4719,7 @@ "postcss-merge-longhand": "^5.0.2", "postcss-merge-rules": "^5.0.2", "postcss-minify-font-values": "^5.0.1", - "postcss-minify-gradients": "^5.0.1", + "postcss-minify-gradients": "^5.0.2", "postcss-minify-params": "^5.0.1", "postcss-minify-selectors": "^5.1.0", "postcss-normalize-charset": "^5.0.1", @@ -5050,6 +5091,15 @@ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "dev": true }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -5861,12 +5911,15 @@ } }, "node_modules/execa/node_modules/is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/exit-on-epipe": { @@ -5992,17 +6045,16 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", - "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" }, "engines": { "node": ">=8" @@ -6865,6 +6917,21 @@ "node": "*" } }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", @@ -6889,12 +6956,6 @@ "he": "bin/he" } }, - "node_modules/hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -6931,18 +6992,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "node_modules/hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, "node_modules/html-entities": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", @@ -7059,15 +7108,15 @@ "dev": true }, "node_modules/html-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-rZsVvPXUYFyME0cuGkyOHfx9hmkFa4pWfxY/mdY38PsBEaVNsRoA+Id+8z6DBDgyv3zaw6XQszdF8HLwfQvcdQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.3.2.tgz", + "integrity": "sha512-HvB33boVNCz2lTyBsSiMffsJ+m0YLIQ+pskblXgN9fnjS1BgEcuAfdInfXfGrkdXV406k9FiDi86eVCDBgJOyQ==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^5.0.0", "html-minifier-terser": "^5.0.1", - "lodash": "^4.17.20", - "pretty-error": "^2.1.1", + "lodash": "^4.17.21", + "pretty-error": "^3.0.4", "tapable": "^2.0.0" }, "engines": { @@ -7081,16 +7130,6 @@ "webpack": "^5.20.0" } }, - "node_modules/html-webpack-plugin/node_modules/pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^2.0.4" - } - }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -7314,9 +7353,9 @@ "dev": true }, "node_modules/inquirer": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.1.tgz", - "integrity": "sha512-hUDjc3vBkh/uk1gPfMAD/7Z188Q8cvTGl0nxwaCdwSbzFh6ZKkZh+s2ozVxbE5G9ZNRyeY0+lgbAIOUFsFf98w==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.2.tgz", + "integrity": "sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -7329,7 +7368,7 @@ "mute-stream": "0.0.8", "ora": "^5.3.0", "run-async": "^2.4.0", - "rxjs": "^6.6.6", + "rxjs": "^7.2.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" @@ -7338,6 +7377,21 @@ "node": ">=8.0.0" } }, + "node_modules/inquirer/node_modules/rxjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", + "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", + "dev": true, + "dependencies": { + "tslib": "~2.1.0" + } + }, + "node_modules/inquirer/node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + }, "node_modules/internal-ip": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", @@ -7439,12 +7493,13 @@ } }, "node_modules/is-arguments": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -7489,29 +7544,6 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, - "node_modules/is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "dependencies": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "node_modules/is-color-stop/node_modules/css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/is-core-module": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", @@ -7525,10 +7557,13 @@ } }, "node_modules/is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -7715,13 +7750,13 @@ } }, "node_modules/is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -7975,12 +8010,6 @@ "json-buffer": "3.0.0" } }, - "node_modules/killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8137,6 +8166,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -9338,13 +9376,13 @@ } }, "node_modules/p-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.5.0.tgz", - "integrity": "sha512-5Hwh4aVQSu6BEP+w2zKlVXtFAaYQe1qWuVADSgoeVlLjwe/Q/AMSoRR4MDeaAfu8llT+YNbEijWu/YF3m6avkg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", + "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", "dev": true, "dependencies": { "@types/retry": "^0.12.0", - "retry": "^0.12.0" + "retry": "^0.13.1" }, "engines": { "node": ">=8" @@ -9821,17 +9859,17 @@ } }, "node_modules/postcss-loader": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-5.3.0.tgz", - "integrity": "sha512-/+Z1RAmssdiSLgIZwnJHwBMnlABPgF7giYzTN2NOfr9D21IJZ4mQC1R2miwp80zno9M4zMD/umGI8cR+2EL5zw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.1.1.tgz", + "integrity": "sha512-lBmJMvRh1D40dqpWKr9Rpygwxn8M74U9uaCSeYGNKLGInbk9mXBt1ultHf2dH9Ghk6Ue4UXlXWwGMH9QdUJ5ug==", "dev": true, "dependencies": { "cosmiconfig": "^7.0.0", "klona": "^2.0.4", - "semver": "^7.3.4" + "semver": "^7.3.5" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", @@ -9894,13 +9932,13 @@ } }, "node_modules/postcss-minify-gradients": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz", - "integrity": "sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz", + "integrity": "sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==", "dev": true, "dependencies": { + "colord": "^2.6", "cssnano-utils": "^2.0.1", - "is-color-stop": "^1.1.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -9946,9 +9984,9 @@ } }, "node_modules/postcss-modules": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.1.3.tgz", - "integrity": "sha512-dBT39hrXe4OAVYJe/2ZuIZ9BzYhOe7t+IhedYeQ2OxKwDpAGlkEN/fR0fGnrbx4BvgbMReRX4hCubYK9cE/pJQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.2.2.tgz", + "integrity": "sha512-/H08MGEmaalv/OU8j6bUKi/kZr2kqGF6huAW8m9UAgOLWtpFdhA14+gPBoymtqyv+D4MLsmqaF2zvIegdCxJXg==", "dev": true, "dependencies": { "generic-names": "^2.0.1", @@ -10147,9 +10185,9 @@ } }, "node_modules/postcss-normalize-url/node_modules/normalize-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.0.1.tgz", - "integrity": "sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "engines": { "node": ">=10" @@ -10298,13 +10336,13 @@ } }, "node_modules/pretty-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.3.tgz", - "integrity": "sha512-nFB0BMeWNJA4YfmrgqPhOH3UQjMQZASZ2ueBfmlyqpVy9+ExLcmwXL/Iu4Wb9pbt/cubQXK4ir8IZUnE8EwFnw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.4.tgz", + "integrity": "sha512-ytLFLfv1So4AO1UkoBF6GXQgJRaKbiSiGFICaOPNwQ3CMvBvXpLRubeQWyPGnsbV/t9ml9qto6IeCsho0aEvwQ==", "dev": true, "dependencies": { "lodash": "^4.17.20", - "renderkid": "^2.0.5" + "renderkid": "^2.0.6" } }, "node_modules/printj": { @@ -10422,9 +10460,9 @@ } }, "node_modules/quasar": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.0.1.tgz", - "integrity": "sha512-DDxcuEardFvebCTNeGeneS/3NazY9ZqNPII9o2VC/ujZn7/hIWDsB0ajYJG8l/pV8kkiaiaIBaE93EzoXfEXrA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.0.4.tgz", + "integrity": "sha512-W53vn99KKeJI+xHT7ah1qOGCqEDG2+x7G47se8lf93wFTXQAyBw+O0TbuOdZqoKpguwT4T2yo4dTMz7WRmRqGA==", "engines": { "node": ">= 10.18.1", "npm": ">= 6.13.4", @@ -10453,6 +10491,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "dev": true, "engines": { "node": ">=0.4.x" @@ -10832,9 +10871,9 @@ } }, "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "engines": { "node": ">= 4" @@ -10850,18 +10889,6 @@ "node": ">=0.10.0" } }, - "node_modules/rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "node_modules/rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11089,9 +11116,9 @@ } }, "node_modules/sass-loader": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.0.0.tgz", - "integrity": "sha512-LJQMyDdNdhcvoO2gJFw7KpTaioVFDeRJOuatRDUNgCIqyu4s4kgDsNofdGzAZB1zFOgo/p3fy+aR/uGXamcJBg==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.1.0.tgz", + "integrity": "sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg==", "dev": true, "dependencies": { "klona": "^2.0.4", @@ -11894,15 +11921,15 @@ } }, "node_modules/svgo": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.3.0.tgz", - "integrity": "sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.4.0.tgz", + "integrity": "sha512-W25S1UUm9Lm9VnE0TvCzL7aso/NCzDEaXLaElCUO/KaVitw0+IBicSVfM1L1c0YHK5TOFh73yQ2naCpVHEQ/OQ==", "dev": true, "dependencies": { "@trysound/sax": "0.1.1", - "chalk": "^4.1.0", + "colorette": "^1.2.2", "commander": "^7.1.0", - "css-select": "^3.1.2", + "css-select": "^4.1.3", "css-tree": "^1.1.2", "csso": "^4.2.0", "stable": "^0.1.8" @@ -11923,34 +11950,6 @@ "node": ">= 10" } }, - "node_modules/svgo/node_modules/css-select": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", - "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^4.0.0", - "domhandler": "^4.0.0", - "domutils": "^2.4.3", - "nth-check": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", - "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/table": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", @@ -12033,15 +12032,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.3.tgz", - "integrity": "sha512-cxGbMqr6+A2hrIB5ehFIF+F/iST5ZOxvOmy9zih9ySbP1C2oEWQSOUS+2SNBTjzx5xLKO4xnod9eywdfq1Nb9A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz", + "integrity": "sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==", "dev": true, "dependencies": { "jest-worker": "^27.0.2", "p-limit": "^3.1.0", "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", + "serialize-javascript": "^6.0.0", "source-map": "^0.6.1", "terser": "^5.7.0" }, @@ -12074,6 +12073,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/terser-webpack-plugin/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12647,24 +12655,33 @@ } }, "node_modules/vue": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.2.tgz", - "integrity": "sha512-q/rbKpb7aofax4ugqu2k/uj7BYuNPcd6Z5/qJtfkJQsE0NkwVoCyeSh7IZGH61hChwYn3CEkh4bHolvUPxlQ+w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.4.tgz", + "integrity": "sha512-rNCFmoewm8IwmTK0nj3ysKq53iRpNEFKoBJ4inar6tIh7Oj7juubS39RI8UI+VE7x+Cs2z6PBsadtZu7z2qppg==", "dependencies": { - "@vue/compiler-dom": "3.1.2", - "@vue/runtime-dom": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-dom": "3.2.4", + "@vue/runtime-dom": "3.2.4", + "@vue/shared": "3.2.4" } }, "node_modules/vue-loader": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz", - "integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.4.1.tgz", + "integrity": "sha512-nL1bDhfMAZgTVmVkOXQaK/WJa9zFDLM9vKHbh5uGv6HeH1TmZrXMWUEVhUrACT38XPhXM4Awtjj25EvhChEgXw==", "dev": true, "dependencies": { "chalk": "^4.1.0", "hash-sum": "^2.0.0", "loader-utils": "^2.0.0" + }, + "peerDependencies": { + "@vue/compiler-sfc": "^3.0.8", + "webpack": "^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } } }, "node_modules/vue-loader/node_modules/loader-utils": { @@ -12693,9 +12710,9 @@ } }, "node_modules/vue-router": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.10.tgz", - "integrity": "sha512-YbPf6QnZpyyWfnk7CUt2Bme+vo7TLfg1nGZNkvYqKYh4vLaFw6Gn8bPGdmt5m4qrGnKoXLqc4htAsd3dIukICA==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.11.tgz", + "integrity": "sha512-sha6I8fx9HWtvTrFZfxZkiQQBpqSeT+UCwauYjkdOQYRvwsGwimlQQE2ayqUwuuXGzquFpCPoXzYKWlzL4OuXg==", "dev": true, "dependencies": { "@vue/devtools-api": "^6.0.0-beta.14" @@ -12881,20 +12898,20 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", - "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz", + "integrity": "sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw==", "dev": true, "dependencies": { "colorette": "^1.2.2", "mem": "^8.1.1", "memfs": "^3.2.2", - "mime-types": "^2.1.30", + "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^3.0.0" }, "engines": { - "node": ">= v10.23.3" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", @@ -12905,12 +12922,12 @@ } }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" }, @@ -12923,38 +12940,36 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.0.0-beta.3.tgz", - "integrity": "sha512-Ud7ieH15No/KiSdRuzk+2k+S4gSCR/N7m4hJhesDbKQEZy3P+NPXTXfsimNOZvbVX2TRuIEFB+VdLZFn8DwGwg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.0.0.tgz", + "integrity": "sha512-ya5cjoBSf3LqrshZn2HMaRZQx8YRNBE+tx+CQNFGaLLHrvs4Y1aik0sl5SFhLz2cW1O9/NtyaZhthc+8UiuvkQ==", "dev": true, "dependencies": { "ansi-html": "^0.0.7", "bonjour": "^3.5.0", "chokidar": "^3.5.1", + "colorette": "^1.2.2", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", "del": "^6.0.0", "express": "^4.17.1", - "find-cache-dir": "^3.3.1", "graceful-fs": "^4.2.6", "html-entities": "^2.3.2", - "http-proxy-middleware": "^1.3.1", + "http-proxy-middleware": "^2.0.0", "internal-ip": "^6.2.0", - "ipaddr.js": "^2.0.0", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "open": "^7.4.2", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", "p-retry": "^4.5.0", "portfinder": "^1.0.28", - "schema-utils": "^3.0.0", + "schema-utils": "^3.1.0", "selfsigned": "^1.10.11", "serve-index": "^1.9.1", "sockjs": "^0.3.21", "spdy": "^4.0.2", - "strip-ansi": "^6.0.0", + "strip-ansi": "^7.0.0", "url": "^0.11.0", - "webpack-dev-middleware": "^4.1.0", - "ws": "^7.4.5" + "webpack-dev-middleware": "^5.0.0", + "ws": "^8.1.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -12963,7 +12978,7 @@ "node": ">= 12.13.0" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^4.37.0 || ^5.0.0" }, "peerDependenciesMeta": { "webpack-cli": { @@ -12971,20 +12986,16 @@ } } }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", - "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.0.tgz", + "integrity": "sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==", "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, "engines": { - "node": ">=8.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { @@ -12997,28 +13008,29 @@ } }, "node_modules/webpack-dev-server/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz", + "integrity": "sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==", "dev": true, "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" }, @@ -13030,6 +13042,42 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.0.tgz", + "integrity": "sha512-UhDTSnGF1dc0DRbUqr1aXwNoY3RgVkSWG8BrpnuFIxhP57IqbS7IRta2Gfiavds4yCxc5+fEAVVOgBZWnYkvzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-uYhVJ/m9oXwEI04iIVmgLmugh2qrZihkywG9y5FfZV2ATeLIzHf93qs+tUNqlttbQK957/VX3mtwAS+UfIwA4g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-merge": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", @@ -13351,16 +13399,6 @@ "engines": { "node": ">= 10" } - }, - "node_modules/zlib": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", - "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=", - "dev": true, - "hasInstallScript": true, - "engines": { - "node": ">=0.2.0" - } } }, "dependencies": { @@ -14682,9 +14720,9 @@ "dev": true }, "@quasar/app": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@quasar/app/-/app-3.0.1.tgz", - "integrity": "sha512-a1hm4miFkvc9setIqtVAKyILHhJ0ZD+Xw52gtGgwwSNGQfKL6UY04837bkjzVOXQ1ybcd5Kpjv3kWPhdTL3TZA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@quasar/app/-/app-3.1.0.tgz", + "integrity": "sha512-b0yCblS5yVYxNjFIuCf2xoZZsNXlq1RQM/b2PxZqlGxOWG4AM02HxLSUrb1YvhwnYsYxo2qm1dbF52Ut6oE/iw==", "dev": true, "requires": { "@quasar/babel-preset-app": "2.0.1", @@ -14696,32 +14734,32 @@ "@types/terser-webpack-plugin": "5.0.3", "@types/webpack-bundle-analyzer": "4.4.0", "@types/webpack-dev-server": "3.11.3", - "@vue/compiler-sfc": "3.1.2", - "@vue/server-renderer": "3.1.2", + "@vue/compiler-sfc": "3.2.4", + "@vue/server-renderer": "3.2.4", "archiver": "5.3.0", - "autoprefixer": "10.2.6", + "autoprefixer": "10.3.1", "browserslist": "^4.12.0", - "chalk": "4.1.1", + "chalk": "4.1.2", "chokidar": "3.5.2", "ci-info": "3.2.0", - "compression-webpack-plugin": "8.0.0", - "copy-webpack-plugin": "9.0.0", + "compression-webpack-plugin": "8.0.1", + "copy-webpack-plugin": "9.0.1", "cross-spawn": "7.0.3", "css-loader": "5.2.6", - "css-minimizer-webpack-plugin": "3.0.1", - "cssnano": "5.0.6", + "css-minimizer-webpack-plugin": "3.0.2", + "cssnano": "5.0.8", "dot-prop": "6.0.1", "elementtree": "0.1.7", "error-stack-parser": "2.0.6", "express": "4.17.1", - "fast-glob": "3.2.5", + "fast-glob": "3.2.7", "file-loader": "6.2.0", "fork-ts-checker-webpack-plugin": "6.1.0", "fs-extra": "10.0.0", "hash-sum": "2.0.0", "html-minifier": "4.0.0", - "html-webpack-plugin": "5.3.1", - "inquirer": "8.1.1", + "html-webpack-plugin": "5.3.2", + "inquirer": "8.1.2", "isbinaryfile": "4.0.8", "launch-editor-middleware": "2.2.1", "lodash.debounce": "4.0.8", @@ -14736,29 +14774,40 @@ "open": "7.1.0", "ouch": "2.0.0", "postcss": "^8.2.10", - "postcss-loader": "5.3.0", + "postcss-loader": "6.1.1", "postcss-rtlcss": "3.3.4", - "pretty-error": "3.0.3", + "pretty-error": "3.0.4", "register-service-worker": "1.7.2", "sass": "1.32.12", - "sass-loader": "12.0.0", + "sass-loader": "12.1.0", "semver": "7.3.5", "table": "6.7.1", - "terser-webpack-plugin": "5.1.3", + "terser-webpack-plugin": "5.1.4", "ts-loader": "8.0.17", "typescript": "4.2.2", "url-loader": "4.1.1", - "vue": "3.1.2", - "vue-loader": "16.2.0", - "vue-router": "4.0.10", + "vue": "3.2.4", + "vue-loader": "16.4.1", + "vue-router": "4.0.11", "vue-style-loader": "4.1.3", "webpack": "^5.35.0", "webpack-bundle-analyzer": "4.4.2", "webpack-chain": "6.5.1", - "webpack-dev-server": "4.0.0-beta.3", + "webpack-dev-server": "4.0.0", "webpack-merge": "5.8.0", - "webpack-node-externals": "3.0.0", - "zlib": "1.0.5" + "webpack-node-externals": "3.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "@quasar/babel-preset-app": { @@ -14877,9 +14926,9 @@ } }, "@quasar/extras": { - "version": "1.10.8", - "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.10.8.tgz", - "integrity": "sha512-s/uTQ/NVQIExOZjXJ0LnpPlqiiuGV0PFw5UKbXypWMhPpfQInnic/uU3SR8ZGtlNC3KHV5U+NTk9K9muTtoDpA==" + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.10.11.tgz", + "integrity": "sha512-/zJiT8iExl0j2k1zA21Eho8SPMtG5ehcYayszunrq/z7zDp728oWSteI9AfQFnF8/+M06f5HUzy+Vssf6IKH/g==" }, "@quasar/fastclick": { "version": "1.1.4", @@ -15014,9 +15063,9 @@ } }, "@types/html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", "dev": true }, "@types/http-proxy": { @@ -15029,9 +15078,9 @@ } }, "@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", "dev": true }, "@types/mime": { @@ -15071,9 +15120,9 @@ "dev": true }, "@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, "@types/serve-static": { @@ -15206,13 +15255,13 @@ } }, "@vue/compiler-core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.2.tgz", - "integrity": "sha512-nHmq7vLjq/XM2IMbZUcKWoH5sPXa2uR/nIKZtjbK5F3TcbnYE/zKsrSUR9WZJ03unlwotNBX1OyxVt9HbWD7/Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.4.tgz", + "integrity": "sha512-c8NuQq7mUXXxA4iqD5VUKpyVeklK53+DMbojYMyZ0VPPrb0BUWrZWFiqSDT+MFDv0f6Hv3QuLiHWb1BWMXBbrw==", "requires": { "@babel/parser": "^7.12.0", "@babel/types": "^7.12.0", - "@vue/shared": "3.1.2", + "@vue/shared": "3.2.4", "estree-walker": "^2.0.1", "source-map": "^0.6.1" }, @@ -15225,27 +15274,27 @@ } }, "@vue/compiler-dom": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.2.tgz", - "integrity": "sha512-k2+SWcWH0jL6WQAX7Or2ONqu5MbtTgTO0dJrvebQYzgqaKMXNI90RNeWeCxS4BnNFMDONpHBeFgbwbnDWIkmRg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.4.tgz", + "integrity": "sha512-uj1nwO4794fw2YsYas5QT+FU/YGrXbS0Qk+1c7Kp1kV7idhZIghWLTjyvYibpGoseFbYLPd+sW2/noJG5H04EQ==", "requires": { - "@vue/compiler-core": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-core": "3.2.4", + "@vue/shared": "3.2.4" } }, "@vue/compiler-sfc": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.2.tgz", - "integrity": "sha512-SeG/2+DvwejQ7oAiSx8BrDh5qOdqCYHGClPiTvVIHTfSIHiS2JjMbCANdDCjHkTOh/O7WZzo2JhdKm98bRBxTw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.4.tgz", + "integrity": "sha512-GM+ouDdDzhqgkLmBH4bgq4kiZxJQArSppJiZHWHIx9XRaefHLmc1LBNPmN8ivm4SVfi2i7M2t9k8ZnjsScgzPQ==", "dev": true, "requires": { "@babel/parser": "^7.13.9", "@babel/types": "^7.13.0", "@types/estree": "^0.0.48", - "@vue/compiler-core": "3.1.2", - "@vue/compiler-dom": "3.1.2", - "@vue/compiler-ssr": "3.1.2", - "@vue/shared": "3.1.2", + "@vue/compiler-core": "3.2.4", + "@vue/compiler-dom": "3.2.4", + "@vue/compiler-ssr": "3.2.4", + "@vue/shared": "3.2.4", "consolidate": "^0.16.0", "estree-walker": "^2.0.1", "hash-sum": "^2.0.0", @@ -15273,13 +15322,13 @@ } }, "@vue/compiler-ssr": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.2.tgz", - "integrity": "sha512-BwXo9LFk5OSWdMyZQ4bX1ELHX0Z/9F+ld/OaVnpUPzAZCHslBYLvyKUVDwv2C/lpLjRffpC2DOUEdl1+RP1aGg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.4.tgz", + "integrity": "sha512-bKZuXu9/4XwsFHFWIKQK+5kN7mxIIWmMmT2L4VVek7cvY/vm3p4WTsXYDGZJy0htOTXvM2ifr6sflg012T0hsw==", "dev": true, "requires": { - "@vue/compiler-dom": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-dom": "3.2.4", + "@vue/shared": "3.2.4" } }, "@vue/devtools-api": { @@ -15288,46 +15337,46 @@ "integrity": "sha512-44fPrrN1cqcs6bFkT0C+yxTM6PZXLbR+ESh1U1j8UD22yO04gXvxH62HApMjLbS3WqJO/iCNC+CYT+evPQh2EQ==" }, "@vue/reactivity": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.2.tgz", - "integrity": "sha512-glJzJoN2xE7I2lRvwKM5u1BHRPTd1yc8iaf//Lai/78/uYAvE5DXp5HzWRFOwMlbRvMGJHIQjOqoxj87cDAaag==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.4.tgz", + "integrity": "sha512-ljWTR0hr8Tn09hM2tlmWxZzCBPlgGLnq/k8K8X6EcJhtV+C8OzFySnbWqMWataojbrQOocThwsC8awKthSl2uQ==", "requires": { - "@vue/shared": "3.1.2" + "@vue/shared": "3.2.4" } }, "@vue/runtime-core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.2.tgz", - "integrity": "sha512-gsPZG4dRIkixuuKmoj4P9IHgfT0yaFLcqWOM5F/bCk0nxQn1XtxH8oUehWuET726KhbukvDoJfe9G2CKviy80w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.4.tgz", + "integrity": "sha512-W6PtEOs8P8jKYPo3JwaMAozZQivxInUleGfNwI2pK1t8ZLZIxn4kAf7p4VF4jJdQB8SZBzpfWdLUc06j7IOmpQ==", "requires": { - "@vue/reactivity": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/reactivity": "3.2.4", + "@vue/shared": "3.2.4" } }, "@vue/runtime-dom": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.2.tgz", - "integrity": "sha512-QvINxjLucEZFzp5f0NVu7JqWYCv5TKQfkH2FDs/N6QNE4iKcYtKrWdT0HKfABnVXG28Znqv6rIH0dH4ZAOwxpA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.4.tgz", + "integrity": "sha512-HcVtLyn2SGwsf6BFPwkvDPDOhOqkOKcfHDpBp5R1coX+qMsOFrY8lJnGXIY+JnxqFjND00E9+u+lq5cs/W7ooA==", "requires": { - "@vue/runtime-core": "3.1.2", - "@vue/shared": "3.1.2", + "@vue/runtime-core": "3.2.4", + "@vue/shared": "3.2.4", "csstype": "^2.6.8" } }, "@vue/server-renderer": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.1.2.tgz", - "integrity": "sha512-XDw8KTrz/siiU5p6Zlicvf2KIjSZrqaxATBPM/9FYNnyv4LTS14JC5daTL13rk50d3UPBurRR/3wJupVvtQJ4w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.4.tgz", + "integrity": "sha512-ai9WxJ78nnUDk+26vwZhlA1Quz3tA+90DgJX6iseen2Wwnndd91xicFW+6ROR/ZP0yFNuQ017eZJBw8OqoPL+w==", "dev": true, "requires": { - "@vue/compiler-ssr": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-ssr": "3.2.4", + "@vue/shared": "3.2.4" } }, "@vue/shared": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.2.tgz", - "integrity": "sha512-EmH/poaDWBPJaPILXNI/1fvUbArJQmmTyVCwvvyDYDFnkPoTclAbHRAtyIvqfez7jybTDn077HTNILpxlsoWhg==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.4.tgz", + "integrity": "sha512-j2j1MRmjalVKr3YBTxl/BClSIc8UQ8NnPpLYclxerK65JIowI4O7n8O8lElveEtEoHxy1d7BelPUDI0Q4bumqg==" }, "@webassemblyjs/ast": { "version": "1.11.0", @@ -15795,13 +15844,13 @@ "dev": true }, "autoprefixer": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.6.tgz", - "integrity": "sha512-8lChSmdU6dCNMCQopIf4Pe5kipkAGj/fvTMslCsih0uHpOrXOPUEVOmYMMqmw3cekQkSD7EhIeuYl5y0BLdKqg==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.1.tgz", + "integrity": "sha512-L8AmtKzdiRyYg7BUXJTzigmhbQRCXFKz6SA1Lqo0+AR2FBbQ4aTAPFSDlOutnFkjhiz8my4agGXog1xlMjPJ6A==", "dev": true, "requires": { "browserslist": "^4.16.6", - "caniuse-lite": "^1.0.30001230", + "caniuse-lite": "^1.0.30001243", "colorette": "^1.2.2", "fraction.js": "^4.1.1", "normalize-range": "^0.1.2", @@ -16217,9 +16266,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001239", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz", - "integrity": "sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ==", + "version": "1.0.30001248", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz", + "integrity": "sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw==", "dev": true }, "caw": { @@ -16452,9 +16501,9 @@ "dev": true }, "colord": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.0.1.tgz", - "integrity": "sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.7.0.tgz", + "integrity": "sha512-pZJBqsHz+pYyw3zpX6ZRXWoCHM1/cvFikY9TV8G3zcejCaKE0lhankoj8iScyrrePA8C7yJ5FStfA9zbcOnw7Q==", "dev": true }, "colorette": { @@ -16535,25 +16584,34 @@ } }, "compression-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-/pBUx1gV8nL6YPKKa9QRs4xoemU28bfqAu8z5hLy2eTrWog4fU8lQYtlpbYwCWtIfAHLxsgSUHjk9RqK6UL+Yw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-8.0.1.tgz", + "integrity": "sha512-VWDXcOgEafQDMFXEnoia0VBXJ+RMw81pmqe/EBiOIBnMfY8pG26eqwIS/ytGpzy1rozydltL0zL6KDH9XNWBxQ==", "dev": true, "requires": { "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1" + "serialize-javascript": "^6.0.0" }, "dependencies": { "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } } } }, @@ -16650,9 +16708,9 @@ "dev": true }, "copy-webpack-plugin": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.0.tgz", - "integrity": "sha512-k8UB2jLIb1Jip2nZbCz83T/XfhfjX6mB1yLJNYKrpYi7FQimfOoFv/0//iT6HV1K8FwUB5yUbCcnpLebJXJTug==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.1.tgz", + "integrity": "sha512-14gHKKdYIxF84jCEgPgYXCPpldbwpxxLbCmA7LReY7gvbaT555DgeBWBgBZM116tv/fO6RRJrsivBqRyRlukhw==", "dev": true, "requires": { "fast-glob": "^3.2.5", @@ -16661,28 +16719,37 @@ "normalize-path": "^3.0.0", "p-limit": "^3.1.0", "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1" + "serialize-javascript": "^6.0.0" }, "dependencies": { "glob-parent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.0.tgz", - "integrity": "sha512-Hdd4287VEJcZXUwv1l8a+vXC1GjOQqXe+VS30w/ypihpcnu9M1n3xeYeJu5CBpeEQj2nAab2xxz28GuA3vp4Ww==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.1.tgz", + "integrity": "sha512-kEVjS71mQazDBHKcsq4E9u/vUzaLcw1A8EtUeydawvIWQCJM0qQ08G1H7/XTjFUulla6XQiDOG6MXSaG0HDKog==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } } } }, @@ -16727,9 +16794,9 @@ } }, "cosmiconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", - "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", "dev": true, "requires": { "@types/parse-json": "^4.0.0", @@ -16783,9 +16850,9 @@ "dev": true }, "css-declaration-sorter": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz", - "integrity": "sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.1.tgz", + "integrity": "sha512-BZ1aOuif2Sb7tQYY1GeCjG7F++8ggnwUkH5Ictw0mrdpqpEd+zWmcPdstnH2TItlb74FqR0DrVEieon221T/1Q==", "dev": true, "requires": { "timsort": "^0.3.0" @@ -16834,31 +16901,40 @@ } }, "css-minimizer-webpack-plugin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.1.tgz", - "integrity": "sha512-RGFIv6iZWUPO2T1vE5+5pNCSs2H2xtHYRdfZPiiNH8Of6QOn9BeFnZSoHiQMkmsxOO/JkPe4BpKfs7slFIWcTA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz", + "integrity": "sha512-B3I5e17RwvKPJwsxjjWcdgpU/zqylzK1bPVghcmpFHRL48DXiBgrtqz1BJsn68+t/zzaLp9kYAaEDvQ7GyanFQ==", "dev": true, "requires": { - "cssnano": "^5.0.0", + "cssnano": "^5.0.6", "jest-worker": "^27.0.2", "p-limit": "^3.0.2", - "postcss": "^8.2.9", + "postcss": "^8.3.5", "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", + "serialize-javascript": "^6.0.0", "source-map": "^0.6.1" }, "dependencies": { "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16911,20 +16987,21 @@ "dev": true }, "cssnano": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.6.tgz", - "integrity": "sha512-NiaLH/7yqGksFGsFNvSRe2IV/qmEBAeDE64dYeD8OBrgp6lE8YoMeQJMtsv5ijo6MPyhuoOvFhI94reahBRDkw==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.8.tgz", + "integrity": "sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==", "dev": true, "requires": { - "cosmiconfig": "^7.0.0", - "cssnano-preset-default": "^5.1.3", - "is-resolvable": "^1.1.0" + "cssnano-preset-default": "^5.1.4", + "is-resolvable": "^1.1.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" } }, "cssnano-preset-default": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.3.tgz", - "integrity": "sha512-qo9tX+t4yAAZ/yagVV3b+QBKeLklQbmgR3wI7mccrDcR+bEk9iHgZN1E7doX68y9ThznLya3RDmR+nc7l6/2WQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz", + "integrity": "sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==", "dev": true, "requires": { "css-declaration-sorter": "^6.0.3", @@ -16939,7 +17016,7 @@ "postcss-merge-longhand": "^5.0.2", "postcss-merge-rules": "^5.0.2", "postcss-minify-font-values": "^5.0.1", - "postcss-minify-gradients": "^5.0.1", + "postcss-minify-gradients": "^5.0.2", "postcss-minify-params": "^5.0.1", "postcss-minify-selectors": "^5.1.0", "postcss-normalize-charset": "^5.0.1", @@ -17238,6 +17315,12 @@ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "dev": true }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -17896,9 +17979,9 @@ "dev": true }, "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true } } @@ -18010,17 +18093,16 @@ "dev": true }, "fast-glob": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", - "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" } }, "fast-json-stable-stringify": { @@ -18680,6 +18762,15 @@ "has-symbol-support-x": "^1.4.1" } }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, "has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", @@ -18698,12 +18789,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -18742,18 +18827,6 @@ } } }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", - "dev": true - }, "html-entities": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", @@ -18850,28 +18923,16 @@ } }, "html-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-rZsVvPXUYFyME0cuGkyOHfx9hmkFa4pWfxY/mdY38PsBEaVNsRoA+Id+8z6DBDgyv3zaw6XQszdF8HLwfQvcdQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.3.2.tgz", + "integrity": "sha512-HvB33boVNCz2lTyBsSiMffsJ+m0YLIQ+pskblXgN9fnjS1BgEcuAfdInfXfGrkdXV406k9FiDi86eVCDBgJOyQ==", "dev": true, "requires": { "@types/html-minifier-terser": "^5.0.0", "html-minifier-terser": "^5.0.1", - "lodash": "^4.17.20", - "pretty-error": "^2.1.1", + "lodash": "^4.17.21", + "pretty-error": "^3.0.4", "tapable": "^2.0.0" - }, - "dependencies": { - "pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", - "dev": true, - "requires": { - "lodash": "^4.17.20", - "renderkid": "^2.0.4" - } - } } }, "htmlparser2": { @@ -19040,9 +19101,9 @@ "dev": true }, "inquirer": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.1.tgz", - "integrity": "sha512-hUDjc3vBkh/uk1gPfMAD/7Z188Q8cvTGl0nxwaCdwSbzFh6ZKkZh+s2ozVxbE5G9ZNRyeY0+lgbAIOUFsFf98w==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.2.tgz", + "integrity": "sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -19055,10 +19116,27 @@ "mute-stream": "0.0.8", "ora": "^5.3.0", "run-async": "^2.4.0", - "rxjs": "^6.6.6", + "rxjs": "^7.2.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" + }, + "dependencies": { + "rxjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", + "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", + "dev": true, + "requires": { + "tslib": "~2.1.0" + } + }, + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } } }, "internal-ip": { @@ -19134,12 +19212,13 @@ "dev": true }, "is-arguments": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "requires": { - "call-bind": "^1.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, "is-arrayish": { @@ -19174,28 +19253,6 @@ } } }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - }, - "dependencies": { - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - } - } - }, "is-core-module": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", @@ -19206,10 +19263,13 @@ } }, "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "dev": true + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-docker": { "version": "2.2.1", @@ -19327,13 +19387,13 @@ } }, "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" } }, "is-resolvable": { @@ -19530,12 +19590,6 @@ "json-buffer": "3.0.0" } }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -19672,6 +19726,12 @@ } } }, + "lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -20589,13 +20649,13 @@ } }, "p-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.5.0.tgz", - "integrity": "sha512-5Hwh4aVQSu6BEP+w2zKlVXtFAaYQe1qWuVADSgoeVlLjwe/Q/AMSoRR4MDeaAfu8llT+YNbEijWu/YF3m6avkg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", + "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", "dev": true, "requires": { "@types/retry": "^0.12.0", - "retry": "^0.12.0" + "retry": "^0.13.1" } }, "p-timeout": { @@ -20954,14 +21014,14 @@ "requires": {} }, "postcss-loader": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-5.3.0.tgz", - "integrity": "sha512-/+Z1RAmssdiSLgIZwnJHwBMnlABPgF7giYzTN2NOfr9D21IJZ4mQC1R2miwp80zno9M4zMD/umGI8cR+2EL5zw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.1.1.tgz", + "integrity": "sha512-lBmJMvRh1D40dqpWKr9Rpygwxn8M74U9uaCSeYGNKLGInbk9mXBt1ultHf2dH9Ghk6Ue4UXlXWwGMH9QdUJ5ug==", "dev": true, "requires": { "cosmiconfig": "^7.0.0", "klona": "^2.0.4", - "semver": "^7.3.4" + "semver": "^7.3.5" } }, "postcss-merge-longhand": { @@ -20998,13 +21058,13 @@ } }, "postcss-minify-gradients": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz", - "integrity": "sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz", + "integrity": "sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==", "dev": true, "requires": { + "colord": "^2.6", "cssnano-utils": "^2.0.1", - "is-color-stop": "^1.1.0", "postcss-value-parser": "^4.1.0" } }, @@ -21032,9 +21092,9 @@ } }, "postcss-modules": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.1.3.tgz", - "integrity": "sha512-dBT39hrXe4OAVYJe/2ZuIZ9BzYhOe7t+IhedYeQ2OxKwDpAGlkEN/fR0fGnrbx4BvgbMReRX4hCubYK9cE/pJQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.2.2.tgz", + "integrity": "sha512-/H08MGEmaalv/OU8j6bUKi/kZr2kqGF6huAW8m9UAgOLWtpFdhA14+gPBoymtqyv+D4MLsmqaF2zvIegdCxJXg==", "dev": true, "requires": { "generic-names": "^2.0.1", @@ -21160,9 +21220,9 @@ }, "dependencies": { "normalize-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.0.1.tgz", - "integrity": "sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true } } @@ -21259,13 +21319,13 @@ "dev": true }, "pretty-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.3.tgz", - "integrity": "sha512-nFB0BMeWNJA4YfmrgqPhOH3UQjMQZASZ2ueBfmlyqpVy9+ExLcmwXL/Iu4Wb9pbt/cubQXK4ir8IZUnE8EwFnw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.4.tgz", + "integrity": "sha512-ytLFLfv1So4AO1UkoBF6GXQgJRaKbiSiGFICaOPNwQ3CMvBvXpLRubeQWyPGnsbV/t9ml9qto6IeCsho0aEvwQ==", "dev": true, "requires": { "lodash": "^4.17.20", - "renderkid": "^2.0.5" + "renderkid": "^2.0.6" } }, "printj": { @@ -21360,9 +21420,9 @@ "dev": true }, "quasar": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.0.1.tgz", - "integrity": "sha512-DDxcuEardFvebCTNeGeneS/3NazY9ZqNPII9o2VC/ujZn7/hIWDsB0ajYJG8l/pV8kkiaiaIBaE93EzoXfEXrA==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.0.4.tgz", + "integrity": "sha512-W53vn99KKeJI+xHT7ah1qOGCqEDG2+x7G47se8lf93wFTXQAyBw+O0TbuOdZqoKpguwT4T2yo4dTMz7WRmRqGA==" }, "query-string": { "version": "5.1.1", @@ -21680,9 +21740,9 @@ } }, "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true }, "reusify": { @@ -21691,18 +21751,6 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -21874,9 +21922,9 @@ } }, "sass-loader": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.0.0.tgz", - "integrity": "sha512-LJQMyDdNdhcvoO2gJFw7KpTaioVFDeRJOuatRDUNgCIqyu4s4kgDsNofdGzAZB1zFOgo/p3fy+aR/uGXamcJBg==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.1.0.tgz", + "integrity": "sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg==", "dev": true, "requires": { "klona": "^2.0.4", @@ -22508,15 +22556,15 @@ } }, "svgo": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.3.0.tgz", - "integrity": "sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.4.0.tgz", + "integrity": "sha512-W25S1UUm9Lm9VnE0TvCzL7aso/NCzDEaXLaElCUO/KaVitw0+IBicSVfM1L1c0YHK5TOFh73yQ2naCpVHEQ/OQ==", "dev": true, "requires": { "@trysound/sax": "0.1.1", - "chalk": "^4.1.0", + "colorette": "^1.2.2", "commander": "^7.1.0", - "css-select": "^3.1.2", + "css-select": "^4.1.3", "css-tree": "^1.1.2", "csso": "^4.2.0", "stable": "^0.1.8" @@ -22527,25 +22575,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true - }, - "css-select": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", - "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^4.0.0", - "domhandler": "^4.0.0", - "domutils": "^2.4.3", - "nth-check": "^2.0.0" - } - }, - "css-what": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", - "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==", - "dev": true } } }, @@ -22622,15 +22651,15 @@ } }, "terser-webpack-plugin": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.3.tgz", - "integrity": "sha512-cxGbMqr6+A2hrIB5ehFIF+F/iST5ZOxvOmy9zih9ySbP1C2oEWQSOUS+2SNBTjzx5xLKO4xnod9eywdfq1Nb9A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz", + "integrity": "sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==", "dev": true, "requires": { "jest-worker": "^27.0.2", "p-limit": "^3.1.0", "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", + "serialize-javascript": "^6.0.0", "source-map": "^0.6.1", "terser": "^5.7.0" }, @@ -22646,6 +22675,15 @@ "ajv-keywords": "^3.5.2" } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -23077,19 +23115,19 @@ "dev": true }, "vue": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.2.tgz", - "integrity": "sha512-q/rbKpb7aofax4ugqu2k/uj7BYuNPcd6Z5/qJtfkJQsE0NkwVoCyeSh7IZGH61hChwYn3CEkh4bHolvUPxlQ+w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.4.tgz", + "integrity": "sha512-rNCFmoewm8IwmTK0nj3ysKq53iRpNEFKoBJ4inar6tIh7Oj7juubS39RI8UI+VE7x+Cs2z6PBsadtZu7z2qppg==", "requires": { - "@vue/compiler-dom": "3.1.2", - "@vue/runtime-dom": "3.1.2", - "@vue/shared": "3.1.2" + "@vue/compiler-dom": "3.2.4", + "@vue/runtime-dom": "3.2.4", + "@vue/shared": "3.2.4" } }, "vue-loader": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz", - "integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.4.1.tgz", + "integrity": "sha512-nL1bDhfMAZgTVmVkOXQaK/WJa9zFDLM9vKHbh5uGv6HeH1TmZrXMWUEVhUrACT38XPhXM4Awtjj25EvhChEgXw==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -23117,9 +23155,9 @@ "requires": {} }, "vue-router": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.10.tgz", - "integrity": "sha512-YbPf6QnZpyyWfnk7CUt2Bme+vo7TLfg1nGZNkvYqKYh4vLaFw6Gn8bPGdmt5m4qrGnKoXLqc4htAsd3dIukICA==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.11.tgz", + "integrity": "sha512-sha6I8fx9HWtvTrFZfxZkiQQBpqSeT+UCwauYjkdOQYRvwsGwimlQQE2ayqUwuuXGzquFpCPoXzYKWlzL4OuXg==", "dev": true, "requires": { "@vue/devtools-api": "^6.0.0-beta.14" @@ -23308,26 +23346,26 @@ } }, "webpack-dev-middleware": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", - "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz", + "integrity": "sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw==", "dev": true, "requires": { "colorette": "^1.2.2", "mem": "^8.1.1", "memfs": "^3.2.2", - "mime-types": "^2.1.30", + "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^3.0.0" }, "dependencies": { "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } @@ -23335,52 +23373,43 @@ } }, "webpack-dev-server": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.0.0-beta.3.tgz", - "integrity": "sha512-Ud7ieH15No/KiSdRuzk+2k+S4gSCR/N7m4hJhesDbKQEZy3P+NPXTXfsimNOZvbVX2TRuIEFB+VdLZFn8DwGwg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.0.0.tgz", + "integrity": "sha512-ya5cjoBSf3LqrshZn2HMaRZQx8YRNBE+tx+CQNFGaLLHrvs4Y1aik0sl5SFhLz2cW1O9/NtyaZhthc+8UiuvkQ==", "dev": true, "requires": { "ansi-html": "^0.0.7", "bonjour": "^3.5.0", "chokidar": "^3.5.1", + "colorette": "^1.2.2", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", "del": "^6.0.0", "express": "^4.17.1", - "find-cache-dir": "^3.3.1", "graceful-fs": "^4.2.6", "html-entities": "^2.3.2", - "http-proxy-middleware": "^1.3.1", + "http-proxy-middleware": "^2.0.0", "internal-ip": "^6.2.0", - "ipaddr.js": "^2.0.0", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "open": "^7.4.2", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", "p-retry": "^4.5.0", "portfinder": "^1.0.28", - "schema-utils": "^3.0.0", + "schema-utils": "^3.1.0", "selfsigned": "^1.10.11", "serve-index": "^1.9.1", "sockjs": "^0.3.21", "spdy": "^4.0.2", - "strip-ansi": "^6.0.0", + "strip-ansi": "^7.0.0", "url": "^0.11.0", - "webpack-dev-middleware": "^4.1.0", - "ws": "^7.4.5" + "webpack-dev-middleware": "^5.0.0", + "ws": "^8.1.0" }, "dependencies": { - "http-proxy-middleware": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", - "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", - "dev": true, - "requires": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - } + "ansi-regex": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.0.tgz", + "integrity": "sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==", + "dev": true }, "ipaddr.js": { "version": "2.0.1", @@ -23389,25 +23418,42 @@ "dev": true }, "open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz", + "integrity": "sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==", "dev": true, "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" } }, "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "@types/json-schema": "^7.0.6", + "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } + }, + "strip-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.0.tgz", + "integrity": "sha512-UhDTSnGF1dc0DRbUqr1aXwNoY3RgVkSWG8BrpnuFIxhP57IqbS7IRta2Gfiavds4yCxc5+fEAVVOgBZWnYkvzg==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.0" + } + }, + "ws": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-uYhVJ/m9oXwEI04iIVmgLmugh2qrZihkywG9y5FfZV2ATeLIzHf93qs+tUNqlttbQK957/VX3mtwAS+UfIwA4g==", + "dev": true, + "requires": {} } } }, @@ -23611,12 +23657,6 @@ "compress-commons": "^4.1.0", "readable-stream": "^3.6.0" } - }, - "zlib": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", - "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=", - "dev": true } } } diff --git a/web/package.json b/web/package.json index 8c2476b25f..02c8089cee 100644 --- a/web/package.json +++ b/web/package.json @@ -10,19 +10,19 @@ "test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\"" }, "dependencies": { - "@quasar/extras": "^1.10.8", + "@quasar/extras": "^1.10.11", "apexcharts": "^3.27.1", "axios": "^0.21.1", "dotenv": "^8.6.0", "prismjs": "^1.23.0", "qrcode.vue": "^3.2.2", - "quasar": "^2.0.1", + "quasar": "^2.0.4", "vue-prism-editor": "^2.0.0-alpha.2", "vue3-apexcharts": "^1.4.0", "vuex": "^4.0.2" }, "devDependencies": { - "@quasar/app": "^3.0.1", + "@quasar/app": "^3.1.0", "@quasar/cli": "^1.2.1" }, "browserslist": [ diff --git a/web/quasar.conf.js b/web/quasar.conf.js index 9b42594954..232bc6a587 100644 --- a/web/quasar.conf.js +++ b/web/quasar.conf.js @@ -62,7 +62,6 @@ module.exports = function () { https: false, host: process.env.DEV_HOST, port: process.env.DEV_PORT, - public: process.env.APP_URL, open: false }, diff --git a/web/src/api/accounts.js b/web/src/api/accounts.js new file mode 100644 index 0000000000..dcb4b3b3e8 --- /dev/null +++ b/web/src/api/accounts.js @@ -0,0 +1,10 @@ +import axios from "axios" + +const baseUrl = "/accounts" + +export async function fetchUsers(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/users/`, { params: params }) + return data + } catch (e) { } +} \ No newline at end of file diff --git a/web/src/api/agents.js b/web/src/api/agents.js new file mode 100644 index 0000000000..ed1762ff51 --- /dev/null +++ b/web/src/api/agents.js @@ -0,0 +1,31 @@ +import axios from "axios" + +const baseUrl = "/agents" + +export async function fetchAgents() { + try { + const { data } = await axios.get(`${baseUrl}/listagentsnodetail/`) + return data + } catch (e) { } +} + +export async function fetchAgentHistory(pk) { + try { + const { data } = await axios.get(`${baseUrl}/history/${pk}`) + return data + } catch (e) { } +} + +export async function runScript(payload) { + try { + const { data } = await axios.post(`${baseUrl}/runscript/`, payload) + return data + } catch (e) { } +} + +export async function runBulkAction(payload) { + + const { data } = await axios.post("/agents/bulk/", payload) + return data + +} \ No newline at end of file diff --git a/web/src/api/clients.js b/web/src/api/clients.js new file mode 100644 index 0000000000..a6a8ff294b --- /dev/null +++ b/web/src/api/clients.js @@ -0,0 +1,17 @@ +import axios from "axios" + +const baseUrl = "/clients" + +export async function fetchClients() { + try { + const { data } = await axios.get(`${baseUrl}/clients/`) + return data + } catch (e) { } +} + +export async function fetchSites() { + try { + const { data } = await axios.get(`${baseUrl}/sites/`) + return data + } catch (e) { } +} \ No newline at end of file diff --git a/web/src/api/core.js b/web/src/api/core.js new file mode 100644 index 0000000000..d0380107c5 --- /dev/null +++ b/web/src/api/core.js @@ -0,0 +1,10 @@ +import axios from "axios" + +const baseUrl = "/core" + +export async function fetchCustomFields(params) { + try { + const { data } = await axios.get(`${baseUrl}/customfields/`, { params: params }) + return data + } catch (e) { } +} \ No newline at end of file diff --git a/web/src/api/logs.js b/web/src/api/logs.js new file mode 100644 index 0000000000..73ad0f10f7 --- /dev/null +++ b/web/src/api/logs.js @@ -0,0 +1,17 @@ +import axios from "axios" + +const baseUrl = "/logs" + +export async function fetchDebugLog(payload) { + try { + const { data } = await axios.patch(`${baseUrl}/debuglog/`, payload) + return data + } catch (e) { } +} + +export async function fetchAuditLog(payload) { + try { + const { data } = await axios.patch(`${baseUrl}/auditlogs/`, payload) + return data + } catch (e) { } +} diff --git a/web/src/api/scripts.js b/web/src/api/scripts.js new file mode 100644 index 0000000000..192bdb63ec --- /dev/null +++ b/web/src/api/scripts.js @@ -0,0 +1,77 @@ +import axios from "axios" + +const baseUrl = "/scripts" + +// script operations +export async function fetchScripts(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/`, { params: params }) + return data + } catch (e) { } +} + +export async function testScript(payload) { + try { + const { data } = await axios.post(`${baseUrl}/testscript/`, payload) + return data + } catch (e) { } +} + +export async function saveScript(payload) { + const { data } = await axios.post(`${baseUrl}/`, payload) + return data +} + +export async function editScript(payload) { + const { data } = await axios.put(`${baseUrl}/${payload.id}/`, payload) + return data +} + +export async function removeScript(id) { + const { data } = await axios.delete(`${baseUrl}/${id}/`) + return data +} + +export async function downloadScript(id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/download/${id}/`, { params: params }) + return data + } catch (e) { } +} + + +// script snippet operations +export async function fetchScriptSnippets(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/snippets/`, { params: params }) + return data + } catch (e) { } +} + +export async function saveScriptSnippet(payload) { + try { + const { data } = await axios.post(`${baseUrl}/snippets/`, payload) + return data + } catch (e) { } +} + +export async function fetchScriptSnippet(id, params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/snippets/${id}/`, { params: params }) + return data + } catch (e) { } +} + +export async function editScriptSnippet(payload) { + try { + const { data } = await axios.put(`${baseUrl}/snippets/${payload.id}/`, payload) + return data + } catch (e) { } +} + +export async function removeScriptSnippet(id) { + try { + const { data } = await axios.delete(`${baseUrl}/snippets/${id}/`) + return data + } catch (e) { } +} \ No newline at end of file diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue index f072f3d17a..8aa9e68b4b 100644 --- a/web/src/components/AdminManager.vue +++ b/web/src/components/AdminManager.vue @@ -99,6 +99,7 @@ {{ props.row.email }} {{ props.row.last_login }} Never + {{ props.row.last_login_ip }} @@ -143,6 +144,13 @@ export default { align: "left", sortable: true, }, + { + name: "last_login_ip", + label: "Last Logon From", + field: "last_login_ip", + align: "left", + sortable: true, + }, ], pagination: { rowsPerPage: 0, diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index 0e197291c5..d41855ea12 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -112,7 +112,7 @@ - + Run URL Action @@ -141,7 +141,7 @@ Send Command - + @@ -164,7 +164,7 @@ dense clickable v-close-popup - @click="runFavScript(script.value, props.row.id)" + @click="showRunScript(props.row, script.value)" > {{ script.label }} @@ -407,24 +407,20 @@ - + - - - - \ No newline at end of file diff --git a/web/src/components/ChecksTab.vue b/web/src/components/ChecksTab.vue index a218f8ad75..1f669f37e4 100644 --- a/web/src/components/ChecksTab.vue +++ b/web/src/components/ChecksTab.vue @@ -330,6 +330,8 @@ import ScriptOutput from "@/components/modals/checks/ScriptOutput"; import EventLogCheckOutput from "@/components/modals/checks/EventLogCheckOutput"; import CheckGraph from "@/components/graphs/CheckGraph"; +import { truncateText } from "@/utils/format"; + export default { name: "ChecksTab", emits: ["edit"], diff --git a/web/src/components/FileBar.vue b/web/src/components/FileBar.vue index d3b8848676..642380c244 100644 --- a/web/src/components/FileBar.vue +++ b/web/src/components/FileBar.vue @@ -25,10 +25,10 @@ Upload MeshAgent - + Audit Log - + Debug Log @@ -70,7 +70,7 @@ Clients Manager - + Script Manager @@ -105,15 +105,15 @@ - + Bulk Command - + Bulk Script - + Bulk Patch Management @@ -148,24 +148,12 @@ - -
- - - -
- -
- - - -
@@ -178,12 +166,6 @@
- -
- - - -
@@ -194,10 +176,6 @@ - - - - @@ -215,20 +193,21 @@ \ No newline at end of file diff --git a/web/src/components/agents/DebugTab.vue b/web/src/components/agents/DebugTab.vue new file mode 100644 index 0000000000..5e85fab52f --- /dev/null +++ b/web/src/components/agents/DebugTab.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/web/src/components/agents/HistoryTab.vue b/web/src/components/agents/HistoryTab.vue new file mode 100644 index 0000000000..f49f336b9c --- /dev/null +++ b/web/src/components/agents/HistoryTab.vue @@ -0,0 +1,159 @@ + + + \ No newline at end of file diff --git a/web/src/components/automation/modals/PolicyAdd.vue b/web/src/components/automation/modals/PolicyAdd.vue index 1688a90113..1c940b0498 100644 --- a/web/src/components/automation/modals/PolicyAdd.vue +++ b/web/src/components/automation/modals/PolicyAdd.vue @@ -10,46 +10,34 @@ - - - + - - + - + mapOptions + /> This {{ type }} will not inherit from higher policies @@ -69,9 +57,11 @@ \ No newline at end of file diff --git a/web/src/components/logs/AuditManager.vue b/web/src/components/logs/AuditManager.vue new file mode 100644 index 0000000000..1e497f684b --- /dev/null +++ b/web/src/components/logs/AuditManager.vue @@ -0,0 +1,356 @@ + + + \ No newline at end of file diff --git a/web/src/components/logs/DebugLog.vue b/web/src/components/logs/DebugLog.vue new file mode 100644 index 0000000000..38e1209f89 --- /dev/null +++ b/web/src/components/logs/DebugLog.vue @@ -0,0 +1,166 @@ + + + \ No newline at end of file diff --git a/web/src/components/modals/agents/BulkAction.vue b/web/src/components/modals/agents/BulkAction.vue index 55f0e9fdc6..05e9a78c2f 100644 --- a/web/src/components/modals/agents/BulkAction.vue +++ b/web/src/components/modals/agents/BulkAction.vue @@ -1,290 +1,296 @@ \ No newline at end of file diff --git a/web/src/components/modals/agents/EditAgent.vue b/web/src/components/modals/agents/EditAgent.vue index 6a1a2032ce..12a0e1ac73 100644 --- a/web/src/components/modals/agents/EditAgent.vue +++ b/web/src/components/modals/agents/EditAgent.vue @@ -21,27 +21,7 @@
Site:
- - - +
Type:
@@ -122,6 +102,7 @@ +
Custom Fields
@@ -147,12 +128,13 @@ import { mapGetters } from "vuex"; import mixins from "@/mixins/mixins"; import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm"; -import CustomField from "@/components/CustomField"; +import CustomField from "@/components/ui/CustomField"; +import TacticalDropdown from "@/components/ui/TacticalDropdown"; export default { name: "EditAgent", emits: ["edit", "close"], - components: { PatchPolicyForm, CustomField }, + components: { PatchPolicyForm, CustomField, TacticalDropdown }, mixins: [mixins], data() { return { diff --git a/web/src/components/modals/agents/RunScript.vue b/web/src/components/modals/agents/RunScript.vue index aed94dd50f..4adf195d81 100644 --- a/web/src/components/modals/agents/RunScript.vue +++ b/web/src/components/modals/agents/RunScript.vue @@ -1,174 +1,198 @@ \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertTemplateForm.vue b/web/src/components/modals/alerts/AlertTemplateForm.vue index 7441b693be..0dd1cb4ca7 100644 --- a/web/src/components/modals/alerts/AlertTemplateForm.vue +++ b/web/src/components/modals/alerts/AlertTemplateForm.vue @@ -222,6 +222,21 @@ ]" /> + +
+ Run actions only on + The selected script will only run on the following types of alerts + +
+ + + + + + + + @@ -551,6 +566,7 @@ export default { agent_always_text: null, agent_always_alert: null, agent_periodic_alert_days: 0, + agent_script_actions: true, check_email_alert_severity: [], check_text_alert_severity: [], check_dashboard_alert_severity: [], @@ -560,6 +576,7 @@ export default { check_always_text: null, check_always_alert: null, check_periodic_alert_days: 0, + check_script_actions: true, task_email_alert_severity: [], task_text_alert_severity: [], task_dashboard_alert_severity: [], @@ -569,6 +586,7 @@ export default { task_always_text: null, task_always_alert: null, task_periodic_alert_days: 0, + task_script_actions: true, }, scriptOptions: [], severityOptions: [ diff --git a/web/src/components/modals/clients/ClientsForm.vue b/web/src/components/modals/clients/ClientsForm.vue index 1d1a7c9909..75ade035fb 100644 --- a/web/src/components/modals/clients/ClientsForm.vue +++ b/web/src/components/modals/clients/ClientsForm.vue @@ -44,7 +44,7 @@ \ No newline at end of file diff --git a/web/src/components/modals/logs/LogModal.vue b/web/src/components/modals/logs/LogModal.vue deleted file mode 100644 index 986511ea85..0000000000 --- a/web/src/components/modals/logs/LogModal.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/scripts/ScriptFormModal.vue b/web/src/components/modals/scripts/ScriptFormModal.vue deleted file mode 100644 index a64f53c02c..0000000000 --- a/web/src/components/modals/scripts/ScriptFormModal.vue +++ /dev/null @@ -1,311 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/modals/scripts/ScriptUploadModal.vue b/web/src/components/modals/scripts/ScriptUploadModal.vue deleted file mode 100644 index 4c089b5c93..0000000000 --- a/web/src/components/modals/scripts/ScriptUploadModal.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/ScriptFormModal.vue b/web/src/components/scripts/ScriptFormModal.vue new file mode 100644 index 0000000000..f84cc188cc --- /dev/null +++ b/web/src/components/scripts/ScriptFormModal.vue @@ -0,0 +1,235 @@ + + + \ No newline at end of file diff --git a/web/src/components/ScriptManager.vue b/web/src/components/scripts/ScriptManager.vue similarity index 57% rename from web/src/components/ScriptManager.vue rename to web/src/components/scripts/ScriptManager.vue index 65a81d6a55..92ffbf1a75 100644 --- a/web/src/components/ScriptManager.vue +++ b/web/src/components/scripts/ScriptManager.vue @@ -1,6 +1,6 @@