Compare commits
	
		
			72 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2eefedadb3 | ||
| 
						 | 
					e63d7a0b8a | ||
| 
						 | 
					2a1b1849fa | ||
| 
						 | 
					0461cb7f19 | ||
| 
						 | 
					0932e0be03 | ||
| 
						 | 
					4638ac9474 | ||
| 
						 | 
					d8d7255029 | ||
| 
						 | 
					fa05276c3f | ||
| 
						 | 
					e50a5d51d8 | ||
| 
						 | 
					c03ba78587 | ||
| 
						 | 
					ff07c69e7d | ||
| 
						 | 
					735b84b26d | ||
| 
						 | 
					8dd069ad67 | ||
| 
						 | 
					1857e68003 | ||
| 
						 | 
					ff2508382a | ||
| 
						 | 
					9cb952b116 | ||
| 
						 | 
					105e8089bb | ||
| 
						 | 
					730f37f247 | ||
| 
						 | 
					284716751f | ||
| 
						 | 
					8d0db699bf | ||
| 
						 | 
					53cf1cae58 | ||
| 
						 | 
					307e4719e0 | ||
| 
						 | 
					5effae787a | ||
| 
						 | 
					6532be0b52 | ||
| 
						 | 
					fb225a5347 | ||
| 
						 | 
					b83830a45e | ||
| 
						 | 
					ca28288c33 | ||
| 
						 | 
					b6f8d9cb25 | ||
| 
						 | 
					9cad0f11e5 | ||
| 
						 | 
					807be08566 | ||
| 
						 | 
					67f6a985f8 | ||
| 
						 | 
					f87d54ae8d | ||
| 
						 | 
					d894bf7271 | ||
| 
						 | 
					56e0e5cace | ||
| 
						 | 
					685084e784 | ||
| 
						 | 
					cbeec5a973 | ||
| 
						 | 
					3fff56bcd7 | ||
| 
						 | 
					c504c23eec | ||
| 
						 | 
					16dae5a655 | ||
| 
						 | 
					e512c5ae7d | ||
| 
						 | 
					094078b928 | ||
| 
						 | 
					34fc3ff919 | ||
| 
						 | 
					4391f48e78 | ||
| 
						 | 
					775608a3c0 | ||
| 
						 | 
					b326228901 | ||
| 
						 | 
					b2e98173a8 | ||
| 
						 | 
					65c9b7952c | ||
| 
						 | 
					b9dc9e7d62 | ||
| 
						 | 
					ce178d0354 | ||
| 
						 | 
					a3ff6efebc | ||
| 
						 | 
					6a9bc56723 | ||
| 
						 | 
					c9ac158d25 | ||
| 
						 | 
					4b937a0fe8 | ||
| 
						 | 
					405bf26ac5 | ||
| 
						 | 
					5dcda0e0a0 | ||
| 
						 | 
					83e9b60308 | ||
| 
						 | 
					10b40b4730 | ||
| 
						 | 
					79d6d804ef | ||
| 
						 | 
					e9c7b6d8f8 | ||
| 
						 | 
					4fcfbfb3f4 | ||
| 
						 | 
					30cde14ed3 | ||
| 
						 | 
					cf76e6f538 | ||
| 
						 | 
					d0f600ec8d | ||
| 
						 | 
					675f9e956f | ||
| 
						 | 
					381605a6bb | ||
| 
						 | 
					0fce66062b | ||
| 
						 | 
					747cc9e5da | ||
| 
						 | 
					25a1b464da | ||
| 
						 | 
					3b6738b547 | ||
| 
						 | 
					fc93e3e97f | ||
| 
						 | 
					0edbb13d48 | ||
| 
						 | 
					673687341c | 
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM python:3.9.6-slim
 | 
			
		||||
FROM python:3.9.9-slim
 | 
			
		||||
 | 
			
		||||
ENV TACTICAL_DIR /opt/tactical
 | 
			
		||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
 | 
			
		||||
@@ -13,10 +13,6 @@ EXPOSE 8000 8383 8005
 | 
			
		||||
RUN groupadd -g 1000 tactical && \
 | 
			
		||||
    useradd -u 1000 -g 1000 tactical
 | 
			
		||||
 | 
			
		||||
# Copy nats-api file
 | 
			
		||||
COPY natsapi/bin/nats-api /usr/local/bin/
 | 
			
		||||
RUN chmod +x /usr/local/bin/nats-api
 | 
			
		||||
 | 
			
		||||
# Copy dev python reqs
 | 
			
		||||
COPY .devcontainer/requirements.txt  /
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,7 @@ EOF
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py load_chocos
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py reload_nats
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
 | 
			
		||||
 | 
			
		||||
  # create super user 
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -49,3 +49,5 @@ nats-rmm.conf
 | 
			
		||||
docs/site/
 | 
			
		||||
reset_db.sh
 | 
			
		||||
run_go_cmd.py
 | 
			
		||||
nats-api.conf
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import send_agent_update_task
 | 
			
		||||
from tacticalrmm.utils import AGENT_DEFER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Triggers an agent update task to run"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER)
 | 
			
		||||
        agent_ids: list[str] = [
 | 
			
		||||
            i.agent_id
 | 
			
		||||
            for i in q
 | 
			
		||||
            if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
        ]
 | 
			
		||||
        send_agent_update_task.delay(agent_ids=agent_ids)
 | 
			
		||||
@@ -748,8 +748,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                try:
 | 
			
		||||
                    ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    DebugLog.error(agent=self, log_type="agent_issues", message=e)
 | 
			
		||||
                    ret = str(e)
 | 
			
		||||
                    DebugLog.error(agent=self, log_type="agent_issues", message=ret)
 | 
			
		||||
 | 
			
		||||
            await nc.close()
 | 
			
		||||
            return ret
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,15 @@ class AgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    client = serializers.ReadOnlyField(source="client.name")
 | 
			
		||||
    site_name = serializers.ReadOnlyField(source="site.name")
 | 
			
		||||
    custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
    patches_last_installed = serializers.ReadOnlyField()
 | 
			
		||||
    last_seen = serializers.ReadOnlyField()
 | 
			
		||||
 | 
			
		||||
    def get_all_timezones(self, obj):
 | 
			
		||||
        return pytz.all_timezones
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        exclude = ["last_seen", "id", "patches_last_installed"]
 | 
			
		||||
        exclude = ["id"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ from logs.models import DebugLog, PendingAction
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.utils import run_nats_api_cmd
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.utils import get_winagent_url
 | 
			
		||||
@@ -80,7 +79,7 @@ def force_code_sign(agent_ids: list[str]) -> None:
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def send_agent_update_task(agent_ids: list[str]) -> None:
 | 
			
		||||
    chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30))
 | 
			
		||||
    chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for agent_id in chunk:
 | 
			
		||||
            agent_update(agent_id)
 | 
			
		||||
@@ -268,7 +267,7 @@ def run_script_email_results_task(
 | 
			
		||||
                server.send_message(msg)
 | 
			
		||||
                server.quit()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        DebugLog.error(message=e)
 | 
			
		||||
        DebugLog.error(message=str(e))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -299,25 +298,6 @@ def clear_faults_task(older_than_days: int) -> None:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def get_wmi_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("wmi", ids, timeout=45)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def agent_checkin_task() -> None:
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,12 @@ from core.models import CoreSettings
 | 
			
		||||
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 tacticalrmm.utils import (
 | 
			
		||||
    get_default_timezone,
 | 
			
		||||
    notify_error,
 | 
			
		||||
    reload_nats,
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
)
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
@@ -74,34 +79,13 @@ class GetAgents(APIView):
 | 
			
		||||
            or "detail" in request.query_params.keys()
 | 
			
		||||
            and request.query_params["detail"] == "true"
 | 
			
		||||
        ):
 | 
			
		||||
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
                .select_related("site", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
                .filter(filter)
 | 
			
		||||
                .only(
 | 
			
		||||
                    "pk",
 | 
			
		||||
                    "hostname",
 | 
			
		||||
                    "agent_id",
 | 
			
		||||
                    "site",
 | 
			
		||||
                    "policy",
 | 
			
		||||
                    "alert_template",
 | 
			
		||||
                    "monitoring_type",
 | 
			
		||||
                    "description",
 | 
			
		||||
                    "needs_reboot",
 | 
			
		||||
                    "overdue_text_alert",
 | 
			
		||||
                    "overdue_email_alert",
 | 
			
		||||
                    "overdue_time",
 | 
			
		||||
                    "offline_time",
 | 
			
		||||
                    "last_seen",
 | 
			
		||||
                    "boot_time",
 | 
			
		||||
                    "logged_in_username",
 | 
			
		||||
                    "last_logged_in_user",
 | 
			
		||||
                    "time_zone",
 | 
			
		||||
                    "maintenance_mode",
 | 
			
		||||
                    "pending_actions_count",
 | 
			
		||||
                    "has_patches_pending",
 | 
			
		||||
                )
 | 
			
		||||
                .defer(*AGENT_DEFER)
 | 
			
		||||
            )
 | 
			
		||||
            ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
            serializer = AgentTableSerializer(agents, many=True, context=ctx)
 | 
			
		||||
@@ -109,7 +93,7 @@ class GetAgents(APIView):
 | 
			
		||||
        # if detail=false
 | 
			
		||||
        else:
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
                .select_related("site")
 | 
			
		||||
                .filter(filter)
 | 
			
		||||
                .only("agent_id", "hostname", "site")
 | 
			
		||||
@@ -125,9 +109,7 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
    # get agent details
 | 
			
		||||
    def get(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        return Response(
 | 
			
		||||
            AgentSerializer(agent, context={"default_tz": get_default_timezone()}).data
 | 
			
		||||
        )
 | 
			
		||||
        return Response(AgentSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
    # edit agent
 | 
			
		||||
    def put(self, request, agent_id):
 | 
			
		||||
 
 | 
			
		||||
@@ -464,7 +464,7 @@ class Alert(models.Model):
 | 
			
		||||
                try:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    DebugLog.error(log_type="scripting", message=e)
 | 
			
		||||
                    DebugLog.error(log_type="scripting", message=str(e))
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ from model_bakery import baker, seq
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from agents.tasks import handle_agents_task
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .serializers import (
 | 
			
		||||
@@ -676,25 +677,14 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        url = "/api/v3/checkin/"
 | 
			
		||||
 | 
			
		||||
        agent_template_text.version = settings.LATEST_AGENT_VER
 | 
			
		||||
        agent_template_text.last_seen = djangotime.now()
 | 
			
		||||
        agent_template_text.save()
 | 
			
		||||
 | 
			
		||||
        agent_template_email.version = settings.LATEST_AGENT_VER
 | 
			
		||||
        agent_template_email.last_seen = djangotime.now()
 | 
			
		||||
        agent_template_email.save()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "agent_id": agent_template_text.agent_id,
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "agent_id": agent_template_email.agent_id,
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        handle_agents_task()
 | 
			
		||||
 | 
			
		||||
        recovery_sms.assert_called_with(
 | 
			
		||||
            pk=Alert.objects.get(agent=agent_template_text).pk
 | 
			
		||||
@@ -1365,15 +1355,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save()
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkin/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "agent_id": agent.agent_id,
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        handle_agents_task()
 | 
			
		||||
 | 
			
		||||
        # this is what data should be
 | 
			
		||||
        data = {
 | 
			
		||||
 
 | 
			
		||||
@@ -130,42 +130,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        self.assertIsInstance(r.json()["check_interval"], int)
 | 
			
		||||
        self.assertEqual(len(r.json()["checks"]), 15)
 | 
			
		||||
 | 
			
		||||
    def test_checkin_patch(self):
 | 
			
		||||
        from logs.models import PendingAction
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkin/"
 | 
			
		||||
        agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent_updated,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": agent_updated.winagent_dl,
 | 
			
		||||
                "version": agent_updated.version,
 | 
			
		||||
                "inno": agent_updated.win_inno_exe,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent failed to update and still on same version
 | 
			
		||||
        payload = {
 | 
			
		||||
            "func": "hello",
 | 
			
		||||
            "agent_id": agent_updated.agent_id,
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent successful update
 | 
			
		||||
        payload["version"] = settings.LATEST_AGENT_VER
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "completed")
 | 
			
		||||
        action.delete()
 | 
			
		||||
 | 
			
		||||
    @patch("apiv3.views.reload_nats")
 | 
			
		||||
    def test_agent_recovery(self, reload_nats):
 | 
			
		||||
        reload_nats.return_value = "ok"
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from checks.utils import bytes2human
 | 
			
		||||
from logs.models import PendingAction, DebugLog
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
 | 
			
		||||
from tacticalrmm.utils import notify_error, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -32,55 +32,11 @@ class CheckIn(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        """
 | 
			
		||||
        !!! DEPRECATED AS OF AGENT 1.6.0 !!!
 | 
			
		||||
        !!! DEPRECATED AS OF AGENT 1.7.0 !!!
 | 
			
		||||
        Endpoint be removed in a future release
 | 
			
		||||
        """
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        updated = False
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if pyver.parse(request.data["version"]) > pyver.parse(
 | 
			
		||||
            agent.version
 | 
			
		||||
        ) or pyver.parse(request.data["version"]) == pyver.parse(
 | 
			
		||||
            settings.LATEST_AGENT_VER
 | 
			
		||||
        ):
 | 
			
		||||
            updated = True
 | 
			
		||||
        agent.version = request.data["version"]
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["version", "last_seen"])
 | 
			
		||||
 | 
			
		||||
        # change agent update pending status to completed if agent has just updated
 | 
			
		||||
        if (
 | 
			
		||||
            updated
 | 
			
		||||
            and agent.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            agent.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).update(status="completed")
 | 
			
		||||
 | 
			
		||||
        # handles any alerting actions
 | 
			
		||||
        if Alert.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            Alert.handle_alert_resolve(agent)
 | 
			
		||||
 | 
			
		||||
        # 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()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
 | 
			
		||||
@@ -109,11 +65,8 @@ class CheckIn(APIView):
 | 
			
		||||
                return Response("ok")
 | 
			
		||||
 | 
			
		||||
        if request.data["func"] == "software":
 | 
			
		||||
            raw: SoftwareList = request.data["software"]
 | 
			
		||||
            if not isinstance(raw, list):
 | 
			
		||||
                return notify_error("err")
 | 
			
		||||
            sw = request.data["software"]
 | 
			
		||||
 | 
			
		||||
            sw = filter_software(raw)
 | 
			
		||||
            if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
			
		||||
                InstalledSoftware(agent=agent, software=sw).save()
 | 
			
		||||
            else:
 | 
			
		||||
@@ -371,6 +324,13 @@ class TaskRunner(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        new_task = serializer.save(last_run=djangotime.now())
 | 
			
		||||
 | 
			
		||||
        AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type="task_run",
 | 
			
		||||
            script=task.script,
 | 
			
		||||
            script_results=request.data,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check if task is a collector and update the custom field
 | 
			
		||||
        if task.custom_field:
 | 
			
		||||
            if not task.stderr:
 | 
			
		||||
@@ -500,11 +460,7 @@ class Software(APIView):
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        raw: SoftwareList = request.data["software"]
 | 
			
		||||
        if not isinstance(raw, list):
 | 
			
		||||
            return notify_error("err")
 | 
			
		||||
 | 
			
		||||
        sw = filter_software(raw)
 | 
			
		||||
        sw = request.data["software"]
 | 
			
		||||
        if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
			
		||||
            InstalledSoftware(agent=agent, software=sw).save()
 | 
			
		||||
        else:
 | 
			
		||||
@@ -570,7 +526,18 @@ class AgentRecovery(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.prefetch_related("recoveryactions").only(
 | 
			
		||||
                "pk", "agent_id", "last_seen"
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # TODO remove these 2 lines after agent v1.7.0 has been out for a while
 | 
			
		||||
        # this is handled now by nats-api service
 | 
			
		||||
        agent.last_seen = djangotime.now()
 | 
			
		||||
        agent.save(update_fields=["last_seen"])
 | 
			
		||||
 | 
			
		||||
        recovery = agent.recoveryactions.filter(last_run=None).last()  # type: ignore
 | 
			
		||||
        ret = {"mode": "pass", "shellcmd": ""}
 | 
			
		||||
        if recovery is None:
 | 
			
		||||
 
 | 
			
		||||
@@ -654,3 +654,9 @@ class TestTaskPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("post", url)
 | 
			
		||||
        self.check_not_authorized("post", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
    def test_policy_fields_to_copy_exists(self):
 | 
			
		||||
        fields = [i.name for i in AutomatedTask._meta.get_fields()]
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask")
 | 
			
		||||
        for i in task.policy_fields_to_copy:  # type: ignore
 | 
			
		||||
            self.assertIn(i, fields)
 | 
			
		||||
 
 | 
			
		||||
@@ -1096,3 +1096,12 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("patch", url)
 | 
			
		||||
        self.check_not_authorized("patch", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
    def test_policy_fields_to_copy_exists(self):
 | 
			
		||||
        from .models import Check
 | 
			
		||||
 | 
			
		||||
        fields = [i.name for i in Check._meta.get_fields()]
 | 
			
		||||
        check = baker.make("checks.Check")
 | 
			
		||||
 | 
			
		||||
        for i in check.policy_fields_to_copy:  # type: ignore
 | 
			
		||||
            self.assertIn(i, fields)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ from django.db import models
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
from tacticalrmm.utils import AGENT_DEFER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Client(BaseAuditModel):
 | 
			
		||||
@@ -73,29 +74,20 @@ class Client(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def agent_count(self) -> int:
 | 
			
		||||
        return Agent.objects.filter(site__client=self).count()
 | 
			
		||||
        return Agent.objects.defer(*AGENT_DEFER).filter(site__client=self).count()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_maintenanace_mode_agents(self):
 | 
			
		||||
        return (
 | 
			
		||||
            Agent.objects.filter(site__client=self, maintenance_mode=True).count() > 0
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
            .filter(site__client=self, maintenance_mode=True)
 | 
			
		||||
            .count()
 | 
			
		||||
            > 0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_failing_checks(self):
 | 
			
		||||
        agents = (
 | 
			
		||||
            Agent.objects.only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "overdue_email_alert",
 | 
			
		||||
                "overdue_text_alert",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            .filter(site__client=self)
 | 
			
		||||
            .prefetch_related("agentchecks", "autotasks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.defer(*AGENT_DEFER).filter(site__client=self)
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
@@ -194,23 +186,21 @@ class Site(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def agent_count(self) -> int:
 | 
			
		||||
        return Agent.objects.filter(site=self).count()
 | 
			
		||||
        return Agent.objects.defer(*AGENT_DEFER).filter(site=self).count()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_maintenanace_mode_agents(self):
 | 
			
		||||
        return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
 | 
			
		||||
        return (
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
            .filter(site=self, maintenance_mode=True)
 | 
			
		||||
            .count()
 | 
			
		||||
            > 0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_failing_checks(self):
 | 
			
		||||
        agents = (
 | 
			
		||||
            Agent.objects.only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "overdue_email_alert",
 | 
			
		||||
                "overdue_text_alert",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
            .filter(site=self)
 | 
			
		||||
            .prefetch_related("agentchecks", "autotasks")
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
import os
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Generate conf for nats-api"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        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"],
 | 
			
		||||
        }
 | 
			
		||||
        conf = os.path.join(settings.BASE_DIR, "nats-api.conf")
 | 
			
		||||
        with open(conf, "w") as f:
 | 
			
		||||
            json.dump(config, f)
 | 
			
		||||
@@ -119,7 +119,6 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
    def sms_is_configured(self):
 | 
			
		||||
        return all(
 | 
			
		||||
            [
 | 
			
		||||
                self.sms_alert_recipients,
 | 
			
		||||
                self.twilio_auth_token,
 | 
			
		||||
                self.twilio_account_sid,
 | 
			
		||||
                self.twilio_number,
 | 
			
		||||
@@ -131,7 +130,6 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
        # smtp with username/password authentication
 | 
			
		||||
        if (
 | 
			
		||||
            self.smtp_requires_auth
 | 
			
		||||
            and self.email_alert_recipients
 | 
			
		||||
            and self.smtp_from_email
 | 
			
		||||
            and self.smtp_host
 | 
			
		||||
            and self.smtp_host_user
 | 
			
		||||
@@ -142,7 +140,6 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
        # smtp relay
 | 
			
		||||
        elif (
 | 
			
		||||
            not self.smtp_requires_auth
 | 
			
		||||
            and self.email_alert_recipients
 | 
			
		||||
            and self.smtp_from_email
 | 
			
		||||
            and self.smtp_host
 | 
			
		||||
            and self.smtp_port
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
asgiref==3.4.1
 | 
			
		||||
asyncio-nats-client==0.11.4
 | 
			
		||||
celery==5.1.2
 | 
			
		||||
celery==5.2.1
 | 
			
		||||
certifi==2021.10.8
 | 
			
		||||
cffi==1.15.0
 | 
			
		||||
channels==3.0.4
 | 
			
		||||
channels_redis==3.3.1
 | 
			
		||||
chardet==4.0.0
 | 
			
		||||
cryptography==3.4.8
 | 
			
		||||
cryptography==35.0.0
 | 
			
		||||
daphne==3.0.2
 | 
			
		||||
Django==3.2.9
 | 
			
		||||
django-cors-headers==3.10.0
 | 
			
		||||
@@ -16,8 +16,8 @@ djangorestframework==3.12.4
 | 
			
		||||
future==0.18.2
 | 
			
		||||
loguru==0.5.3
 | 
			
		||||
msgpack==1.0.2
 | 
			
		||||
packaging==21.2
 | 
			
		||||
psycopg2-binary==2.9.1
 | 
			
		||||
packaging==21.3
 | 
			
		||||
psycopg2-binary==2.9.2
 | 
			
		||||
pycparser==2.21
 | 
			
		||||
pycryptodome==3.11.0
 | 
			
		||||
pyotp==2.6.0
 | 
			
		||||
@@ -28,10 +28,11 @@ redis==3.5.3
 | 
			
		||||
requests==2.26.0
 | 
			
		||||
six==1.16.0
 | 
			
		||||
sqlparse==0.4.2
 | 
			
		||||
twilio==7.3.0
 | 
			
		||||
twilio==7.3.1
 | 
			
		||||
urllib3==1.26.7
 | 
			
		||||
uWSGI==2.0.20
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
vine==5.0.0
 | 
			
		||||
websockets==9.1
 | 
			
		||||
zipp==3.6.0
 | 
			
		||||
drf_spectacular==0.21.0
 | 
			
		||||
@@ -102,9 +102,7 @@
 | 
			
		||||
    "submittedBy": "https://github.com/bradhawkins85",
 | 
			
		||||
    "name": "TacticalRMM - Agent Rename",
 | 
			
		||||
    "description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "<string>"
 | 
			
		||||
    ],
 | 
			
		||||
    "syntax": "<string>",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):TacticalRMM Related"
 | 
			
		||||
  },
 | 
			
		||||
@@ -114,9 +112,7 @@
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Bitlocker - Check Drive for Status",
 | 
			
		||||
    "description": "Runs a check on drive for Bitlocker status. Returns 0 if Bitlocker is not enabled, 1 if Bitlocker is enabled",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "[Drive <string>]"
 | 
			
		||||
    ],
 | 
			
		||||
    "syntax": "[Drive <string>]",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
@@ -241,12 +237,22 @@
 | 
			
		||||
    "category": "TRMM (Win):Updates",
 | 
			
		||||
    "default_timeout": "25000"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "4d0ba685-2259-44be-9010-8ed2fa48bf74",
 | 
			
		||||
    "filename": "Win_Win11_Ready.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/adamjrberry/",
 | 
			
		||||
    "name": "Windows 11 Upgrade capable check",
 | 
			
		||||
    "description": "Checks to see if machine is Win11 capable",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Updates",
 | 
			
		||||
    "default_timeout": "3600"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
 | 
			
		||||
    "filename": "Win_Disk_Status.ps1",
 | 
			
		||||
    "filename": "Win_Disk_Volume_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Disk Hardware Health Check (using Event Viewer errors)",
 | 
			
		||||
    "description": "Checks local disks for errors reported in event viewer within the last 24 hours",
 | 
			
		||||
    "name": "Disk Drive Volume Health Check (using Event Viewer errors)",
 | 
			
		||||
    "description": "Checks Drive Volumes for errors reported in event viewer within the last 24 hours",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Hardware"
 | 
			
		||||
  },
 | 
			
		||||
@@ -431,11 +437,7 @@
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "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",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "-$PackageName <string>",
 | 
			
		||||
      "[-Hosts <string>]",
 | 
			
		||||
      "[-mode {(install) | update | uninstall}]"
 | 
			
		||||
    ],
 | 
			
		||||
    "syntax": "-$PackageName <string>\n[-Hosts <string>]\n[-mode {(install) | update | uninstall}]",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):3rd Party Software>Chocolatey",
 | 
			
		||||
    "default_timeout": "600"
 | 
			
		||||
@@ -500,12 +502,7 @@
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Rename Computer",
 | 
			
		||||
    "description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "-NewName <string>",
 | 
			
		||||
      "[-Username <string>]",
 | 
			
		||||
      "[-Password <string>]",
 | 
			
		||||
      "[-Restart]"
 | 
			
		||||
    ],
 | 
			
		||||
    "syntax": "-NewName <string>\n[-Username <string>]\n[-Password <string>]\n[-Restart]",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other",
 | 
			
		||||
    "default_timeout": 30
 | 
			
		||||
@@ -516,9 +513,7 @@
 | 
			
		||||
    "submittedBy": "https://github.com/tremor021",
 | 
			
		||||
    "name": "Power - Restart or Shutdown PC",
 | 
			
		||||
    "description": "Restart PC. Add parameter: shutdown if you want to shutdown computer",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "[shutdown]"
 | 
			
		||||
    ],
 | 
			
		||||
    "syntax": "[shutdown]",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Updates"
 | 
			
		||||
  },
 | 
			
		||||
@@ -757,13 +752,7 @@
 | 
			
		||||
    "submittedBy": "https://github.com/brodur",
 | 
			
		||||
    "name": "User - Create Local",
 | 
			
		||||
    "description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "-username <string>",
 | 
			
		||||
      "-password <string>",
 | 
			
		||||
      "[-description <string>]",
 | 
			
		||||
      "[-fullname <string>]",
 | 
			
		||||
      "[-group <string>]"
 | 
			
		||||
    ],
 | 
			
		||||
    "syntax": "-username <string>\n-password <string>\n[-description <string>]\n[-fullname <string>]\n[-group <string>]",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):User Management"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/scripts/migrations/0013_script_syntax.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/scripts/migrations/0013_script_syntax.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-11-13 16:25
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('scripts', '0012_auto_20210917_1954'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='script',
 | 
			
		||||
            name='syntax',
 | 
			
		||||
            field=models.TextField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-11-19 15:44
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('scripts', '0013_script_syntax'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='script',
 | 
			
		||||
            name='filename',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -24,7 +24,7 @@ class Script(BaseAuditModel):
 | 
			
		||||
    guid = models.CharField(max_length=64, null=True, blank=True)
 | 
			
		||||
    name = models.CharField(max_length=255)
 | 
			
		||||
    description = models.TextField(null=True, blank=True, default="")
 | 
			
		||||
    filename = models.CharField(max_length=255)  # deprecated
 | 
			
		||||
    filename = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    shell = models.CharField(
 | 
			
		||||
        max_length=100, choices=SCRIPT_SHELLS, default="powershell"
 | 
			
		||||
    )
 | 
			
		||||
@@ -37,6 +37,7 @@ class Script(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    syntax = TextField(null=True, blank=True)
 | 
			
		||||
    favorite = models.BooleanField(default=False)
 | 
			
		||||
    category = models.CharField(max_length=100, null=True, blank=True)
 | 
			
		||||
    code_base64 = models.TextField(null=True, blank=True, default="")
 | 
			
		||||
@@ -115,6 +116,8 @@ class Script(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
                args = script["args"] if "args" in script.keys() else []
 | 
			
		||||
 | 
			
		||||
                syntax = script["syntax"] if "syntax" in script.keys() else ""
 | 
			
		||||
 | 
			
		||||
                if s.exists():
 | 
			
		||||
                    i = s.first()
 | 
			
		||||
                    i.name = script["name"]  # type: ignore
 | 
			
		||||
@@ -123,6 +126,8 @@ class Script(BaseAuditModel):
 | 
			
		||||
                    i.shell = script["shell"]  # type: ignore
 | 
			
		||||
                    i.default_timeout = default_timeout  # type: ignore
 | 
			
		||||
                    i.args = args  # type: ignore
 | 
			
		||||
                    i.syntax = syntax  # type: ignore
 | 
			
		||||
                    i.filename = script["filename"]  # type: ignore
 | 
			
		||||
 | 
			
		||||
                    with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
 | 
			
		||||
                        script_bytes = (
 | 
			
		||||
@@ -139,6 +144,8 @@ class Script(BaseAuditModel):
 | 
			
		||||
                            "code_base64",
 | 
			
		||||
                            "shell",
 | 
			
		||||
                            "args",
 | 
			
		||||
                            "filename",
 | 
			
		||||
                            "syntax",
 | 
			
		||||
                        ]
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
@@ -157,6 +164,8 @@ class Script(BaseAuditModel):
 | 
			
		||||
                        s.shell = script["shell"]
 | 
			
		||||
                        s.default_timeout = default_timeout
 | 
			
		||||
                        s.args = args
 | 
			
		||||
                        s.filename = script["filename"]
 | 
			
		||||
                        s.syntax = syntax
 | 
			
		||||
 | 
			
		||||
                        with open(
 | 
			
		||||
                            os.path.join(scripts_dir, script["filename"]), "rb"
 | 
			
		||||
@@ -178,6 +187,8 @@ class Script(BaseAuditModel):
 | 
			
		||||
                                "code_base64",
 | 
			
		||||
                                "shell",
 | 
			
		||||
                                "args",
 | 
			
		||||
                                "filename",
 | 
			
		||||
                                "syntax",
 | 
			
		||||
                            ]
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
@@ -200,6 +211,8 @@ class Script(BaseAuditModel):
 | 
			
		||||
                            category=category,
 | 
			
		||||
                            default_timeout=default_timeout,
 | 
			
		||||
                            args=args,
 | 
			
		||||
                            filename=script["filename"],
 | 
			
		||||
                            syntax=syntax,
 | 
			
		||||
                        ).save()
 | 
			
		||||
 | 
			
		||||
        # delete community scripts that had their name changed
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,8 @@ class ScriptTableSerializer(ModelSerializer):
 | 
			
		||||
            "category",
 | 
			
		||||
            "favorite",
 | 
			
		||||
            "default_timeout",
 | 
			
		||||
            "syntax",
 | 
			
		||||
            "filename",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -32,6 +34,8 @@ class ScriptSerializer(ModelSerializer):
 | 
			
		||||
            "favorite",
 | 
			
		||||
            "code_base64",
 | 
			
		||||
            "default_timeout",
 | 
			
		||||
            "syntax",
 | 
			
		||||
            "filename",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.utils import filter_software, notify_error
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import ChocoSoftware, InstalledSoftware
 | 
			
		||||
from .permissions import SoftwarePerms
 | 
			
		||||
@@ -76,13 +76,11 @@ class GetSoftware(APIView):
 | 
			
		||||
        if r == "timeout" or r == "natsdown":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
        sw = filter_software(r)
 | 
			
		||||
 | 
			
		||||
        if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
			
		||||
            InstalledSoftware(agent=agent, software=sw).save()
 | 
			
		||||
            InstalledSoftware(agent=agent, software=r).save()
 | 
			
		||||
        else:
 | 
			
		||||
            s = agent.installedsoftware_set.first()  # type: ignore
 | 
			
		||||
            s.software = sw
 | 
			
		||||
            s.software = r
 | 
			
		||||
            s.save(update_fields=["software"])
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -38,15 +38,7 @@ app.conf.beat_schedule = {
 | 
			
		||||
    },
 | 
			
		||||
    "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",
 | 
			
		||||
        "schedule": crontab(minute=18, hour="*/5"),
 | 
			
		||||
        "schedule": crontab(minute="*/3"),
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -59,11 +51,10 @@ def debug_task(self):
 | 
			
		||||
@app.on_after_finalize.connect
 | 
			
		||||
def setup_periodic_tasks(sender, **kwargs):
 | 
			
		||||
 | 
			
		||||
    from agents.tasks import agent_outages_task, agent_checkin_task
 | 
			
		||||
    from agents.tasks import agent_outages_task
 | 
			
		||||
    from alerts.tasks import unsnooze_alerts
 | 
			
		||||
    from core.tasks import core_maintenance_tasks, cache_db_fields_task
 | 
			
		||||
 | 
			
		||||
    sender.add_periodic_task(45.0, agent_checkin_task.s())
 | 
			
		||||
    sender.add_periodic_task(60.0, agent_outages_task.s())
 | 
			
		||||
    sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s())
 | 
			
		||||
    sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s())
 | 
			
		||||
 
 | 
			
		||||
@@ -15,22 +15,22 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
 | 
			
		||||
AUTH_USER_MODEL = "accounts.User"
 | 
			
		||||
 | 
			
		||||
# latest release
 | 
			
		||||
TRMM_VERSION = "0.9.2"
 | 
			
		||||
TRMM_VERSION = "0.10.2"
 | 
			
		||||
 | 
			
		||||
# bump this version everytime vue code is changed
 | 
			
		||||
# to alert user they need to manually refresh their browser
 | 
			
		||||
APP_VER = "0.0.150"
 | 
			
		||||
APP_VER = "0.0.152"
 | 
			
		||||
 | 
			
		||||
# https://github.com/wh1te909/rmmagent
 | 
			
		||||
LATEST_AGENT_VER = "1.6.2"
 | 
			
		||||
LATEST_AGENT_VER = "1.7.0"
 | 
			
		||||
 | 
			
		||||
MESH_VER = "0.9.45"
 | 
			
		||||
MESH_VER = "0.9.51"
 | 
			
		||||
 | 
			
		||||
NATS_SERVER_VER = "2.3.3"
 | 
			
		||||
 | 
			
		||||
# for the update script, bump when need to recreate venv or npm install
 | 
			
		||||
PIP_VER = "23"
 | 
			
		||||
NPM_VER = "24"
 | 
			
		||||
PIP_VER = "24"
 | 
			
		||||
NPM_VER = "25"
 | 
			
		||||
 | 
			
		||||
SETUPTOOLS_VER = "58.5.3"
 | 
			
		||||
WHEEL_VER = "0.37.0"
 | 
			
		||||
@@ -65,6 +65,13 @@ REST_FRAMEWORK = {
 | 
			
		||||
        "knox.auth.TokenAuthentication",
 | 
			
		||||
        "tacticalrmm.auth.APIAuthentication",
 | 
			
		||||
    ),
 | 
			
		||||
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SPECTACULAR_SETTINGS = {
 | 
			
		||||
    "TITLE": "Tactical RMM API",
 | 
			
		||||
    "DESCRIPTION": "Simple and Fast remote monitoring and management tool",
 | 
			
		||||
    "VERSION": TRMM_VERSION,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if not "AZPIPELINE" in os.environ:
 | 
			
		||||
@@ -97,6 +104,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    "logs",
 | 
			
		||||
    "scripts",
 | 
			
		||||
    "alerts",
 | 
			
		||||
    "drf_spectacular",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
if not "AZPIPELINE" in os.environ:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,15 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from unittest.mock import mock_open, patch
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.test import override_settings
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .utils import (
 | 
			
		||||
    bitdays_to_string,
 | 
			
		||||
    filter_software,
 | 
			
		||||
    generate_winagent_exe,
 | 
			
		||||
    get_bit_days,
 | 
			
		||||
    reload_nats,
 | 
			
		||||
    run_nats_api_cmd,
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -78,12 +74,6 @@ class TestUtils(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        mock_subprocess.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @patch("subprocess.run")
 | 
			
		||||
    def test_run_nats_api_cmd(self, mock_subprocess):
 | 
			
		||||
        ids = ["a", "b", "c"]
 | 
			
		||||
        _ = run_nats_api_cmd("wmi", ids)
 | 
			
		||||
        mock_subprocess.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    def test_bitdays_to_string(self):
 | 
			
		||||
        a = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
 | 
			
		||||
        all_days = [
 | 
			
		||||
@@ -104,11 +94,10 @@ class TestUtils(TacticalTestCase):
 | 
			
		||||
        r = bitdays_to_string(bit_weekdays)
 | 
			
		||||
        self.assertEqual(r, "Every day")
 | 
			
		||||
 | 
			
		||||
    def test_filter_software(self):
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/software1.json")
 | 
			
		||||
        ) as f:
 | 
			
		||||
            sw = json.load(f)
 | 
			
		||||
    def test_defer_fields_exist(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        r = filter_software(sw)
 | 
			
		||||
        self.assertIsInstance(r, list)
 | 
			
		||||
        fields = [i.name for i in Agent._meta.get_fields()]
 | 
			
		||||
 | 
			
		||||
        for i in AGENT_DEFER:
 | 
			
		||||
            self.assertIn(i, fields)
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,18 @@ if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
 | 
			
		||||
 | 
			
		||||
    urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
 | 
			
		||||
 | 
			
		||||
if hasattr(settings, "SWAGGER_ENABLED") and settings.SWAGGER_ENABLED:
 | 
			
		||||
    from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
 | 
			
		||||
 | 
			
		||||
    urlpatterns += (
 | 
			
		||||
        path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
 | 
			
		||||
        path(
 | 
			
		||||
            "api/schema/swagger-ui/",
 | 
			
		||||
            SpectacularSwaggerView.as_view(url_name="schema"),
 | 
			
		||||
            name="swagger-ui",
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
ws_urlpatterns = [
 | 
			
		||||
    path("ws/dashinfo/", DashInfo.as_asgi()),  # type: ignore
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
import subprocess
 | 
			
		||||
import tempfile
 | 
			
		||||
import time
 | 
			
		||||
@@ -23,7 +22,7 @@ from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
SoftwareList = list[dict[str, str]]
 | 
			
		||||
AGENT_DEFER = ["wmi_detail", "services"]
 | 
			
		||||
 | 
			
		||||
WEEK_DAYS = {
 | 
			
		||||
    "Sunday": 0x1,
 | 
			
		||||
@@ -147,26 +146,6 @@ def bitdays_to_string(day: int) -> str:
 | 
			
		||||
    return ", ".join(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_software(sw: SoftwareList) -> SoftwareList:
 | 
			
		||||
    ret: SoftwareList = []
 | 
			
		||||
    printable = set(string.printable)
 | 
			
		||||
    for s in sw:
 | 
			
		||||
        ret.append(
 | 
			
		||||
            {
 | 
			
		||||
                "name": "".join(filter(lambda x: x in printable, s["name"])),
 | 
			
		||||
                "version": "".join(filter(lambda x: x in printable, s["version"])),
 | 
			
		||||
                "publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
 | 
			
		||||
                "install_date": s["install_date"],
 | 
			
		||||
                "size": s["size"],
 | 
			
		||||
                "source": s["source"],
 | 
			
		||||
                "location": s["location"],
 | 
			
		||||
                "uninstall": s["uninstall"],
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def reload_nats():
 | 
			
		||||
    users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}]
 | 
			
		||||
    agents = Agent.objects.prefetch_related("user").only(
 | 
			
		||||
@@ -239,38 +218,6 @@ KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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(
 | 
			
		||||
        dir="/opt/tactical/tmp" if settings.DOCKER_BUILD else None
 | 
			
		||||
    ) 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, timeout=timeout)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            DebugLog.error(message=e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_latest_trmm_ver() -> str:
 | 
			
		||||
    url = "https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py"
 | 
			
		||||
    try:
 | 
			
		||||
@@ -283,7 +230,7 @@ def get_latest_trmm_ver() -> str:
 | 
			
		||||
            if "TRMM_VERSION" in line:
 | 
			
		||||
                return line.split(" ")[2].strip('"')
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        DebugLog.error(message=e)
 | 
			
		||||
        DebugLog.error(message=str(e))
 | 
			
		||||
 | 
			
		||||
    return "error"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="15"
 | 
			
		||||
SCRIPT_VERSION="16"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
 | 
			
		||||
 | 
			
		||||
GREEN='\033[0;32m'
 | 
			
		||||
@@ -75,9 +75,9 @@ sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
 | 
			
		||||
 | 
			
		||||
sudo gzip -9 -c /var/lib/redis/appendonly.aof > ${tmp_dir}/redis/appendonly.aof.gz
 | 
			
		||||
 | 
			
		||||
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/
 | 
			
		||||
if [ -f "${sysd}/daphne.service" ]; then
 | 
			
		||||
    sudo cp ${sysd}/daphne.service ${tmp_dir}/systemd/
 | 
			
		||||
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/daphne.service ${tmp_dir}/systemd/
 | 
			
		||||
if [ -f "${sysd}/nats-api.service" ]; then
 | 
			
		||||
    sudo cp ${sysd}/nats-api.service ${tmp_dir}/systemd/
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
cat /rmm/api/tacticalrmm/tacticalrmm/private/log/django_debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,9 @@ RUN apk add --no-cache inotify-tools supervisor bash
 | 
			
		||||
 | 
			
		||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
 | 
			
		||||
 | 
			
		||||
COPY natsapi/bin/nats-api /usr/local/bin/
 | 
			
		||||
RUN chmod +x /usr/local/bin/nats-api
 | 
			
		||||
 | 
			
		||||
COPY docker/containers/tactical-nats/entrypoint.sh /
 | 
			
		||||
RUN chmod +x /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,10 @@ set -e
 | 
			
		||||
 | 
			
		||||
if [ "${DEV}" = 1 ]; then
 | 
			
		||||
  NATS_CONFIG=/workspace/api/tacticalrmm/nats-rmm.conf
 | 
			
		||||
  NATS_API_CONFIG=/workspace/api/tacticalrmm/nats-api.conf
 | 
			
		||||
else
 | 
			
		||||
  NATS_CONFIG="${TACTICAL_DIR}/api/nats-rmm.conf"
 | 
			
		||||
  NATS_API_CONFIG="${TACTICAL_DIR}/api/nats-api.conf"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
sleep 15
 | 
			
		||||
@@ -37,6 +39,12 @@ stdout_logfile=/dev/fd/1
 | 
			
		||||
stdout_logfile_maxbytes=0
 | 
			
		||||
redirect_stderr=true
 | 
			
		||||
 | 
			
		||||
[program:nats-api]
 | 
			
		||||
command=/bin/bash -c "/usr/local/bin/nats-api -config ${NATS_API_CONFIG}"
 | 
			
		||||
stdout_logfile=/dev/fd/1
 | 
			
		||||
stdout_logfile_maxbytes=0
 | 
			
		||||
redirect_stderr=true
 | 
			
		||||
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# creates python virtual env
 | 
			
		||||
FROM python:3.9.6-slim AS CREATE_VENV_STAGE
 | 
			
		||||
FROM python:3.9.9-slim AS CREATE_VENV_STAGE
 | 
			
		||||
 | 
			
		||||
ARG DEBIAN_FRONTEND=noninteractive
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ RUN apt-get update && \
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# runtime image
 | 
			
		||||
FROM python:3.9.6-slim
 | 
			
		||||
FROM python:3.9.9-slim
 | 
			
		||||
 | 
			
		||||
# set env variables
 | 
			
		||||
ENV VIRTUAL_ENV /opt/venv
 | 
			
		||||
@@ -50,10 +50,6 @@ RUN apt-get update && \
 | 
			
		||||
 | 
			
		||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
 | 
			
		||||
 | 
			
		||||
# copy nats-api file
 | 
			
		||||
COPY natsapi/bin/nats-api /usr/local/bin/
 | 
			
		||||
RUN chmod +x /usr/local/bin/nats-api
 | 
			
		||||
 | 
			
		||||
# docker init
 | 
			
		||||
COPY docker/containers/tactical/entrypoint.sh /
 | 
			
		||||
RUN chmod +x /entrypoint.sh
 | 
			
		||||
 
 | 
			
		||||
@@ -129,6 +129,7 @@ EOF
 | 
			
		||||
  python manage.py load_chocos
 | 
			
		||||
  python manage.py load_community_scripts
 | 
			
		||||
  python manage.py reload_nats
 | 
			
		||||
  python manage.py create_natsapi_conf
 | 
			
		||||
  python manage.py create_installer_user
 | 
			
		||||
 | 
			
		||||
  # create super user 
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,16 @@ networks:
 | 
			
		||||
      driver: default
 | 
			
		||||
      config:
 | 
			
		||||
        - subnet: 172.20.0.0/24
 | 
			
		||||
  api-db:
 | 
			
		||||
  redis:
 | 
			
		||||
  mesh-db:
 | 
			
		||||
  api-db: null
 | 
			
		||||
  redis: null
 | 
			
		||||
  mesh-db: null # docker managed persistent volumes
 | 
			
		||||
 | 
			
		||||
# docker managed persistent volumes
 | 
			
		||||
volumes:
 | 
			
		||||
  tactical_data:
 | 
			
		||||
  postgres_data:
 | 
			
		||||
  mongo_data:
 | 
			
		||||
  mesh_data:
 | 
			
		||||
  redis_data:
 | 
			
		||||
  tactical_data: null
 | 
			
		||||
  postgres_data: null
 | 
			
		||||
  mongo_data: null
 | 
			
		||||
  mesh_data: null
 | 
			
		||||
  redis_data: null
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  # postgres database for api service
 | 
			
		||||
@@ -41,7 +40,7 @@ services:
 | 
			
		||||
    image: redis:6.0-alpine
 | 
			
		||||
    command: redis-server --appendonly yes
 | 
			
		||||
    restart: always
 | 
			
		||||
    volumes: 
 | 
			
		||||
    volumes:
 | 
			
		||||
      - redis_data:/data
 | 
			
		||||
    networks:
 | 
			
		||||
      - redis
 | 
			
		||||
@@ -51,7 +50,7 @@ services:
 | 
			
		||||
    container_name: trmm-init
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    restart: on-failure
 | 
			
		||||
    command: ["tactical-init"]
 | 
			
		||||
    command: [ "tactical-init" ]
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTGRES_USER: ${POSTGRES_USER}
 | 
			
		||||
      POSTGRES_PASS: ${POSTGRES_PASS}
 | 
			
		||||
@@ -63,13 +62,13 @@ services:
 | 
			
		||||
      TRMM_PASS: ${TRMM_PASS}
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - tactical-postgres
 | 
			
		||||
      - tactical-meshcentral    
 | 
			
		||||
      - tactical-meshcentral
 | 
			
		||||
    networks:
 | 
			
		||||
      - api-db
 | 
			
		||||
      - proxy
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical_data:/opt/tactical
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  # nats
 | 
			
		||||
  tactical-nats:
 | 
			
		||||
    container_name: trmm-nats
 | 
			
		||||
@@ -82,6 +81,7 @@ services:
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical_data:/opt/tactical
 | 
			
		||||
    networks:
 | 
			
		||||
      api-db: null
 | 
			
		||||
      proxy:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - ${API_HOST}
 | 
			
		||||
@@ -91,7 +91,7 @@ services:
 | 
			
		||||
    container_name: trmm-meshcentral
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment: 
 | 
			
		||||
    environment:
 | 
			
		||||
      MESH_HOST: ${MESH_HOST}
 | 
			
		||||
      MESH_USER: ${MESH_USER}
 | 
			
		||||
      MESH_PASS: ${MESH_PASS}
 | 
			
		||||
@@ -102,7 +102,7 @@ services:
 | 
			
		||||
      proxy:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - ${MESH_HOST}
 | 
			
		||||
      mesh-db:
 | 
			
		||||
      mesh-db: null
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical_data:/opt/tactical
 | 
			
		||||
      - mesh_data:/home/node/app/meshcentral-data
 | 
			
		||||
@@ -137,7 +137,7 @@ services:
 | 
			
		||||
  tactical-backend:
 | 
			
		||||
    container_name: trmm-backend
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-backend"]
 | 
			
		||||
    command: [ "tactical-backend" ]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      - proxy
 | 
			
		||||
@@ -152,7 +152,7 @@ services:
 | 
			
		||||
  tactical-websockets:
 | 
			
		||||
    container_name: trmm-websockets
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-websockets"]
 | 
			
		||||
    command: [ "tactical-websockets" ]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      - proxy
 | 
			
		||||
@@ -188,7 +188,7 @@ services:
 | 
			
		||||
  tactical-celery:
 | 
			
		||||
    container_name: trmm-celery
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-celery"]
 | 
			
		||||
    command: [ "tactical-celery" ]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      - redis
 | 
			
		||||
@@ -204,7 +204,7 @@ services:
 | 
			
		||||
  tactical-celerybeat:
 | 
			
		||||
    container_name: trmm-celerybeat
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-celerybeat"]
 | 
			
		||||
    command: [ "tactical-celerybeat" ]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      - proxy
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								docs/docs/functions/permissions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/docs/functions/permissions.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# User Roles and Permissions
 | 
			
		||||
 | 
			
		||||
## Permission Manager
 | 
			
		||||
 | 
			
		||||
Make sure you've setup at least 1 valid (Super User aka Administrator) role under _Settings > Permission Manager_
 | 
			
		||||
 | 
			
		||||
1. Login as usual Tactical user
 | 
			
		||||
2. Go to Settings - Permissions Manager
 | 
			
		||||
3. Click New Role
 | 
			
		||||
4. You can all the role anything, I called it Admins
 | 
			
		||||
5. Tick the Super User Box/or relevant permissions required
 | 
			
		||||
6. Click Save then exit Permissions Manager
 | 
			
		||||
7. Go to Settings - Users
 | 
			
		||||
8. Open current logged in user/or any other user and assign role (created above step 6) in the Role drop down box.
 | 
			
		||||
9. Click Save 
 | 
			
		||||
 | 
			
		||||
Once you've set that up a Super User role and assigned your primary user, you can create other Roles with more limited access.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/tipsntricks_filters.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/tipsntricks_filters.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										17
									
								
								docs/docs/install_considerations.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/docs/install_considerations.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
# Install Considerations
 | 
			
		||||
 | 
			
		||||
There's pluses and minuses to each install type. Be aware that:
 | 
			
		||||
 | 
			
		||||
- There is no migration script, once you've installed with one type there is no "conversion". You'll be installing a new server and migrating agents manually if you decide to go another way.
 | 
			
		||||
 | 
			
		||||
## Traditional Install
 | 
			
		||||
 | 
			
		||||
- It's a VM/machine. One storage device to backup if you want to do VM based backups
 | 
			
		||||
- You have a [backup](backup.md) and [restore](restore.md) script
 | 
			
		||||
 | 
			
		||||
## Docker Install
 | 
			
		||||
 | 
			
		||||
- Docker is more complicated in concept: has volumes and images
 | 
			
		||||
- If you're running multiple apps it uses less resources in the long run because you only have one OS base files underlying many Containers/Apps
 | 
			
		||||
- Backup/restore is by via Docker methods only
 | 
			
		||||
- Docker has container replication/mirroring options for redundancy/multiple servers
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
 | 
			
		||||
#### Hardware / OS
 | 
			
		||||
 | 
			
		||||
A fresh linux VM running either Ubuntu 20.04 LTS or Debian 10 with 3GB RAM
 | 
			
		||||
A fresh linux VM running either Ubuntu 20.04 LTS or Debian 10/11 with 3GB RAM
 | 
			
		||||
 | 
			
		||||
!!!warning
 | 
			
		||||
    The provided install script assumes a fresh server with no software installed on it. Attempting to run it on an existing server with other services **will** break things and the install will fail.
 | 
			
		||||
@@ -65,6 +65,9 @@ usermod -a -G sudo tactical
 | 
			
		||||
!!!tip
 | 
			
		||||
    [Enable passwordless sudo to make your life easier](https://linuxconfig.org/configure-sudo-without-password-on-ubuntu-20-04-focal-fossa-linux)
 | 
			
		||||
 | 
			
		||||
!!!note
 | 
			
		||||
    You will never login to the server again as `root` again unless something has gone horribly wrong, and you're working with the developers.
 | 
			
		||||
    
 | 
			
		||||
### Setup the firewall (optional but highly recommended)
 | 
			
		||||
 | 
			
		||||
!!!info
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,14 @@ python manage.py show_outdated_agents
 | 
			
		||||
python manage.py delete_tokens
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Reset all Auth Tokens for Install agents and web sessions
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py shell
 | 
			
		||||
from knox.models import AuthToken
 | 
			
		||||
AuthToken.objects.all().delete()
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Check for orphaned tasks on all agents and remove them
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,11 @@ At the top right of your web administration interface, click your Username > pre
 | 
			
		||||
 | 
			
		||||
*****
 | 
			
		||||
 | 
			
		||||
## Use the filters in the agent list
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
*****
 | 
			
		||||
## MeshCentral
 | 
			
		||||
 | 
			
		||||
Tactical RMM is actually 2 products: An RMM service with agent, and a secondary [MeshCentral](https://github.com/Ylianst/MeshCentral) install that handles the `Take Control` and `Remote Background` stuff.
 | 
			
		||||
 
 | 
			
		||||
@@ -63,9 +63,44 @@ If you have agents that are relatively old, you will need to uninstall them manu
 | 
			
		||||
 | 
			
		||||
## Agents not checking in or showing up / General agent issues
 | 
			
		||||
 | 
			
		||||
These are nats problems. Try quickfix first:
 | 
			
		||||
 | 
			
		||||
### from Admin Web Interface
 | 
			
		||||
 | 
			
		||||
First, reload NATS from tactical's web UI:<br>
 | 
			
		||||
*Tools > Server Maintenance > Reload Nats Configuration*
 | 
			
		||||
 | 
			
		||||
If that doesn't work, check each part starting with the server:
 | 
			
		||||
 | 
			
		||||
### Server SSH login
 | 
			
		||||
 | 
			
		||||
Reload NATS:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py reload_nats
 | 
			
		||||
sudo systemctl restart nats
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Look at nats service errors (make sure it's running)
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl status nats
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If nats isn't running see detailed reason why it isn't:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl stop nats
 | 
			
		||||
nats-server -DVV -c /rmm/api/tacticalrmm/nats-rmm.conf
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Fix the problem, then restart nats.
 | 
			
		||||
```
 | 
			
		||||
sudo systemctl restart nats
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### From Agent Install
 | 
			
		||||
 | 
			
		||||
Open CMD as admin on the problem computer and stop the agent services:
 | 
			
		||||
 | 
			
		||||
```cmd
 | 
			
		||||
@@ -114,6 +149,7 @@ sudo systemctl status celery
 | 
			
		||||
sudo systemctl status celerybeat
 | 
			
		||||
sudo systemctl status nginx
 | 
			
		||||
sudo systemctl status nats
 | 
			
		||||
sudo systemctl status nats-api
 | 
			
		||||
sudo systemctl status meshcentral
 | 
			
		||||
sudo systemctl status mongod
 | 
			
		||||
sudo systemctl status postgresql
 | 
			
		||||
@@ -161,3 +197,11 @@ Are you trying to use a proxy to share your single public IP with multiple servi
 | 
			
		||||
4. Click the add link
 | 
			
		||||
5. Download both agents
 | 
			
		||||
6. In Tactical RMM, go **Settings > Global Settings > MeshCentral > Upload Mesh Agents** upload them both into the appropriate places.
 | 
			
		||||
 | 
			
		||||
## Need to recover your mesh token?
 | 
			
		||||
 | 
			
		||||
Login to server with SSH and run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
node /meshcentral/node_modules/meshcentral --logintokenkey
 | 
			
		||||
```
 | 
			
		||||
@@ -430,7 +430,7 @@ You need to add the certificate private key and public keys to the following fil
 | 
			
		||||
 | 
			
		||||
7. Restart services
 | 
			
		||||
   
 | 
			
		||||
        sudo systemctl restart rmm celery celerybeat nginx nats natsapi
 | 
			
		||||
        sudo systemctl restart rmm celery celerybeat nginx nats nats-api
 | 
			
		||||
 | 
			
		||||
## Use certbot to do acme challenge over http
 | 
			
		||||
 | 
			
		||||
@@ -720,7 +720,7 @@ python manage.py reload_nats
 | 
			
		||||
 | 
			
		||||
### Restart services
 | 
			
		||||
 | 
			
		||||
for i in rmm celery celerybeat nginx nats natsapi
 | 
			
		||||
for i in rmm celery celerybeat nginx nats nats-api
 | 
			
		||||
do
 | 
			
		||||
printf >&2 "${GREEN}Restarting ${i} service...${NC}\n"
 | 
			
		||||
sudo systemctl restart ${i}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,9 @@
 | 
			
		||||
 | 
			
		||||
## Updating to the latest RMM version
 | 
			
		||||
 | 
			
		||||
!!!question
 | 
			
		||||
    You have a [backup](https://docs.docker.com/desktop/backup-and-restore/) right?
 | 
			
		||||
 | 
			
		||||
Tactical RMM updates the docker images on every release and should be available within a few minutes
 | 
			
		||||
 | 
			
		||||
SSH into your server as a root user and run the below commands:
 | 
			
		||||
 
 | 
			
		||||
@@ -19,13 +19,16 @@ Other than this, you should avoid making any changes to your server and let the
 | 
			
		||||
    
 | 
			
		||||
    Sometimes, manual intervention will be required during an update in the form of yes/no prompts, so attempting to automate this will ignore these prompts and cause your installation to break.
 | 
			
		||||
 | 
			
		||||
SSH into your server as the linux user you created during install.
 | 
			
		||||
SSH into your server as the linux user you created during install (eg `tactical`).
 | 
			
		||||
 | 
			
		||||
!!!danger
 | 
			
		||||
    __Never__ run any update scripts or commands as the `root` user.
 | 
			
		||||
    
 | 
			
		||||
    This will mess up permissions and break your installation.
 | 
			
		||||
 | 
			
		||||
!!!question
 | 
			
		||||
    You have a [backup](backup.md) right?
 | 
			
		||||
 | 
			
		||||
Download the update script and run it:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,15 @@ nav:
 | 
			
		||||
  - Home: index.md
 | 
			
		||||
  - Sponsor: sponsor.md
 | 
			
		||||
  - Code Signing: code_signing.md
 | 
			
		||||
  - RMM Installation:
 | 
			
		||||
  - RMM Server Installation:
 | 
			
		||||
      - "Install Considerations": install_considerations.md
 | 
			
		||||
      - "Traditional Install": install_server.md
 | 
			
		||||
      - "Docker Install": install_docker.md
 | 
			
		||||
  - Agent Installation: install_agent.md
 | 
			
		||||
  - Updating:
 | 
			
		||||
  - RMM Server Updating:
 | 
			
		||||
      - "Updating the RMM": update_server.md
 | 
			
		||||
      - "Updating the RMM (Docker)": update_docker.md
 | 
			
		||||
  - Agents:
 | 
			
		||||
      - "Agent Installation": install_agent.md
 | 
			
		||||
      - "Updating Agents": update_agents.md
 | 
			
		||||
  - Functionality:
 | 
			
		||||
      - "Alerting": functions/alerting.md
 | 
			
		||||
@@ -20,6 +22,7 @@ nav:
 | 
			
		||||
      - "Django Admin": functions/django_admin.md
 | 
			
		||||
      - "Global Keystore": functions/keystore.md
 | 
			
		||||
      - "Maintenance Mode": functions/maintenance_mode.md
 | 
			
		||||
      - "Permissions": functions/permissions.md
 | 
			
		||||
      - "Remote Background": functions/remote_bg.md
 | 
			
		||||
      - "Settings Override": functions/settings_override.md
 | 
			
		||||
      - "Scripting": functions/scripting.md
 | 
			
		||||
@@ -83,4 +86,4 @@ markdown_extensions:
 | 
			
		||||
  - codehilite:
 | 
			
		||||
      guess_lang: false
 | 
			
		||||
  - toc:
 | 
			
		||||
      permalink: true
 | 
			
		||||
      permalink: true
 | 
			
		||||
							
								
								
									
										13
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,13 +1,22 @@
 | 
			
		||||
module github.com/wh1te909/tacticalrmm
 | 
			
		||||
 | 
			
		||||
go 1.16
 | 
			
		||||
go 1.17
 | 
			
		||||
 | 
			
		||||
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.4.0 // indirect
 | 
			
		||||
	github.com/nats-io/nats.go v1.12.0
 | 
			
		||||
	github.com/nats-io/nats.go v1.12.3
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.6
 | 
			
		||||
	github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83
 | 
			
		||||
	google.golang.org/protobuf v1.27.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/nats-io/nkeys v0.3.0 // indirect
 | 
			
		||||
	github.com/nats-io/nuid v1.0.1 // indirect
 | 
			
		||||
	github.com/sirupsen/logrus v1.8.1 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,3 +1,4 @@
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 | 
			
		||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 | 
			
		||||
@@ -31,17 +32,34 @@ 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/nats.go v1.12.3 h1:te0GLbRsjtejEkZKKiuk46tbfIn6FfCSv3WWSo1+51E=
 | 
			
		||||
github.com/nats-io/nats.go v1.12.3/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=
 | 
			
		||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 | 
			
		||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 | 
			
		||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
			
		||||
github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
 | 
			
		||||
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211001174053-e5699d36a79b h1:WLA6eHSBVuuUSrwDO9K4srMAGY/NEyBwxe0beFQyXEg=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211001174053-e5699d36a79b/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111174321-133e360c1dc9 h1:2yQWajVLFbhoQT2HBq3HpVA1WwfkwXGxf805qR6MEx4=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111174321-133e360c1dc9/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111183133-95fd87bc23ff h1:rmMbsIlEuAmPeBssEjcZCh5hRYtc6ajKuhvlCrSQj64=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111183133-95fd87bc23ff/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111190958-39c3e2dfec67 h1:sez6UO2rKiCKYa4VTPKfmEyO0Qn6Bps2T//2Y3YkKbM=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111190958-39c3e2dfec67/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111193154-6d7f8e4d0dcd h1:18S4tn72OOCWGbfkaMI7mo6luFWM7gi9vg5uofLfdTE=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211111193154-6d7f8e4d0dcd/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83 h1:faCwMxF0DwMppqThweKdmoxfruB/C/NjTYDG5d9O5V4=
 | 
			
		||||
github.com/wh1te909/trmm-shared v0.0.0-20211112185254-e9c45faf2b83/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
			
		||||
@@ -52,6 +70,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 | 
			
		||||
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								install.sh
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="55"
 | 
			
		||||
SCRIPT_VERSION="56"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt install -y curl wget dirmngr gnupg lsb-release
 | 
			
		||||
@@ -40,11 +40,11 @@ fi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# determine system
 | 
			
		||||
if ([ "$osname" = "ubuntu" ] && [ "$fullrelno" = "20.04" ]) || ([ "$osname" = "debian" ] && [ $relno -eq 10 ]); then
 | 
			
		||||
if ([ "$osname" = "ubuntu" ] && [ "$fullrelno" = "20.04" ]) || ([ "$osname" = "debian" ] && [ $relno -ge 10 ]); then
 | 
			
		||||
  echo $fullrel
 | 
			
		||||
else
 | 
			
		||||
 echo $fullrel
 | 
			
		||||
 echo -ne "${RED}Only Ubuntu release 20.04 and Debian 10 are supported\n"
 | 
			
		||||
 echo -ne "${RED}Supported versions: Ubuntu 20.04, Debian 10 and 11\n"
 | 
			
		||||
 echo -ne "Your system does not appear to be supported${NC}\n"
 | 
			
		||||
 exit 1
 | 
			
		||||
fi
 | 
			
		||||
@@ -64,9 +64,11 @@ fi
 | 
			
		||||
 | 
			
		||||
if ([ "$osname" = "ubuntu" ]); then
 | 
			
		||||
  mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname $codename/mongodb-org/4.4 multiverse"
 | 
			
		||||
# there is no bullseye repo yet for mongo so just use buster on debian 11
 | 
			
		||||
elif ([ "$osname" = "debian" ] && [ $relno -eq 11 ]); then
 | 
			
		||||
  mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname buster/mongodb-org/4.4 main"
 | 
			
		||||
else
 | 
			
		||||
  mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname $codename/mongodb-org/4.4 main"
 | 
			
		||||
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
postgresql_repo="deb [arch=amd64] https://apt.postgresql.org/pub/repos/apt/ $codename-pgdg main"
 | 
			
		||||
@@ -193,14 +195,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.6/Python-3.9.6.tgz
 | 
			
		||||
tar -xf Python-3.9.6.tgz
 | 
			
		||||
cd Python-3.9.6
 | 
			
		||||
wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz
 | 
			
		||||
tar -xf Python-3.9.9.tgz
 | 
			
		||||
cd Python-3.9.9
 | 
			
		||||
./configure --enable-optimizations
 | 
			
		||||
make -j $numprocs
 | 
			
		||||
sudo make altinstall
 | 
			
		||||
cd ~
 | 
			
		||||
sudo rm -rf Python-3.9.6 Python-3.9.6.tgz
 | 
			
		||||
sudo rm -rf Python-3.9.9 Python-3.9.9.tgz
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
print_green 'Installing redis and git'
 | 
			
		||||
@@ -351,6 +353,7 @@ pip install --no-cache-dir setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
 | 
			
		||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
 | 
			
		||||
python manage.py migrate
 | 
			
		||||
python manage.py collectstatic --no-input
 | 
			
		||||
python manage.py create_natsapi_conf
 | 
			
		||||
python manage.py load_chocos
 | 
			
		||||
python manage.py load_community_scripts
 | 
			
		||||
printf >&2 "${YELLOW}%0.s*${NC}" {1..80}
 | 
			
		||||
@@ -439,7 +442,7 @@ echo "${daphneservice}" | sudo tee /etc/systemd/system/daphne.service > /dev/nul
 | 
			
		||||
natsservice="$(cat << EOF
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=NATS Server
 | 
			
		||||
After=network.target ntp.service
 | 
			
		||||
After=network.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
PrivateTmp=true
 | 
			
		||||
@@ -458,6 +461,25 @@ EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${natsservice}" | sudo tee /etc/systemd/system/nats.service > /dev/null
 | 
			
		||||
 | 
			
		||||
natsapi="$(cat << EOF
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=TacticalRMM Nats Api v1
 | 
			
		||||
After=nats.service
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
ExecStart=/usr/local/bin/nats-api
 | 
			
		||||
User=${USER}
 | 
			
		||||
Group=${USER}
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=5s
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${natsapi}" | sudo tee /etc/systemd/system/nats-api.service > /dev/null
 | 
			
		||||
 | 
			
		||||
nginxrmm="$(cat << EOF
 | 
			
		||||
server_tokens off;
 | 
			
		||||
 | 
			
		||||
@@ -791,6 +813,10 @@ python manage.py reload_nats
 | 
			
		||||
deactivate
 | 
			
		||||
sudo systemctl start nats.service
 | 
			
		||||
 | 
			
		||||
sleep 1
 | 
			
		||||
sudo systemctl enable nats-api.service
 | 
			
		||||
sudo systemctl start nats-api.service
 | 
			
		||||
 | 
			
		||||
## disable django admin
 | 
			
		||||
sed -i 's/ADMIN_ENABLED = True/ADMIN_ENABLED = False/g' /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								main.go
									
									
									
									
									
								
							@@ -6,15 +6,19 @@ import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/wh1te909/tacticalrmm/natsapi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var version = "2.3.0"
 | 
			
		||||
var (
 | 
			
		||||
	version = "3.0.0"
 | 
			
		||||
	log     = logrus.New()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	ver := flag.Bool("version", false, "Prints version")
 | 
			
		||||
	mode := flag.String("m", "", "Mode")
 | 
			
		||||
	config := flag.String("c", "", "config file")
 | 
			
		||||
	cfg := flag.String("config", "", "Path to config file")
 | 
			
		||||
	logLevel := flag.String("log", "INFO", "The log level")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if *ver {
 | 
			
		||||
@@ -22,14 +26,15 @@ func main() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch *mode {
 | 
			
		||||
	case "wmi":
 | 
			
		||||
		api.GetWMI(*config)
 | 
			
		||||
	case "checkin":
 | 
			
		||||
		api.CheckIn(*config)
 | 
			
		||||
	case "agentinfo":
 | 
			
		||||
		api.AgentInfo(*config)
 | 
			
		||||
	default:
 | 
			
		||||
		fmt.Println(version)
 | 
			
		||||
	}
 | 
			
		||||
	setupLogging(logLevel)
 | 
			
		||||
 | 
			
		||||
	api.Svc(log, *cfg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setupLogging(level *string) {
 | 
			
		||||
	ll, err := logrus.ParseLevel(*level)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ll = logrus.InfoLevel
 | 
			
		||||
	}
 | 
			
		||||
	log.SetLevel(ll)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										166
									
								
								natsapi/svc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								natsapi/svc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	_ "github.com/lib/pq"
 | 
			
		||||
	nats "github.com/nats-io/nats.go"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/ugorji/go/codec"
 | 
			
		||||
	trmm "github.com/wh1te909/trmm-shared"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Svc(logger *logrus.Logger, cfg string) {
 | 
			
		||||
	logger.Debugln("Starting Svc()")
 | 
			
		||||
	db, r, err := GetConfig(cfg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := setupNatsOptions(r.Key)
 | 
			
		||||
	nc, err := nats.Connect(r.NatsURL, opts...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nc.Subscribe("*", func(msg *nats.Msg) {
 | 
			
		||||
		var mh codec.MsgpackHandle
 | 
			
		||||
		mh.MapType = reflect.TypeOf(map[string]interface{}(nil))
 | 
			
		||||
		mh.RawToString = true
 | 
			
		||||
		dec := codec.NewDecoderBytes(msg.Data, &mh)
 | 
			
		||||
 | 
			
		||||
		switch msg.Reply {
 | 
			
		||||
		case "agent-hello":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p trmm.CheckInNats
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					loc, _ := time.LoadLocation("UTC")
 | 
			
		||||
					now := time.Now().In(loc)
 | 
			
		||||
					logger.Debugln("Hello", p, now)
 | 
			
		||||
					stmt := `
 | 
			
		||||
					UPDATE agents_agent
 | 
			
		||||
					SET last_seen=$1, version=$2
 | 
			
		||||
					WHERE agents_agent.agent_id=$3;
 | 
			
		||||
					`
 | 
			
		||||
 | 
			
		||||
					_, err = db.Exec(stmt, now, p.Version, p.Agentid)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
		case "agent-publicip":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var p trmm.PublicIPNats
 | 
			
		||||
				if err := dec.Decode(&p); err == nil {
 | 
			
		||||
					logger.Debugln("Public IP", p)
 | 
			
		||||
					stmt := `
 | 
			
		||||
					UPDATE agents_agent SET public_ip=$1 WHERE agents_agent.agent_id=$2;`
 | 
			
		||||
					_, err = db.Exec(stmt, p.PublicIP, p.Agentid)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
		case "agent-agentinfo":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var r trmm.AgentInfoNats
 | 
			
		||||
				if err := dec.Decode(&r); err == nil {
 | 
			
		||||
					stmt := `
 | 
			
		||||
						UPDATE agents_agent
 | 
			
		||||
						SET hostname=$1, operating_system=$2,
 | 
			
		||||
						plat=$3, total_ram=$4, boot_time=$5, needs_reboot=$6, logged_in_username=$7
 | 
			
		||||
						WHERE agents_agent.agent_id=$8;`
 | 
			
		||||
 | 
			
		||||
					logger.Debugln("Info", r)
 | 
			
		||||
					_, err = db.Exec(stmt, r.Hostname, r.OS, r.Platform, r.TotalRAM, r.BootTime, r.RebootNeeded, r.Username, r.Agentid)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if r.Username != "None" {
 | 
			
		||||
						stmt = `UPDATE agents_agent SET last_logged_in_user=$1 WHERE agents_agent.agent_id=$2;`
 | 
			
		||||
						logger.Debugln("Updating last logged in user:", r.Username)
 | 
			
		||||
						_, err = db.Exec(stmt, r.Username, r.Agentid)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							logger.Errorln(err)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
		case "agent-disks":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var r trmm.WinDisksNats
 | 
			
		||||
				if err := dec.Decode(&r); err == nil {
 | 
			
		||||
					logger.Debugln("Disks", r)
 | 
			
		||||
					b, err := json.Marshal(r.Disks)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					stmt := `
 | 
			
		||||
					UPDATE agents_agent SET disks=$1 WHERE agents_agent.agent_id=$2;`
 | 
			
		||||
 | 
			
		||||
					_, err = db.Exec(stmt, b, r.Agentid)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
		case "agent-winsvc":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var r trmm.WinSvcNats
 | 
			
		||||
				if err := dec.Decode(&r); err == nil {
 | 
			
		||||
					logger.Debugln("WinSvc", r)
 | 
			
		||||
					b, err := json.Marshal(r.WinSvcs)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					stmt := `
 | 
			
		||||
					UPDATE agents_agent SET services=$1 WHERE agents_agent.agent_id=$2;`
 | 
			
		||||
 | 
			
		||||
					_, err = db.Exec(stmt, b, r.Agentid)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
		case "agent-wmi":
 | 
			
		||||
			go func() {
 | 
			
		||||
				var r trmm.WinWMINats
 | 
			
		||||
				if err := dec.Decode(&r); err == nil {
 | 
			
		||||
					logger.Debugln("WMI", r)
 | 
			
		||||
					b, err := json.Marshal(r.WMI)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					stmt := `
 | 
			
		||||
					UPDATE agents_agent SET wmi_detail=$1 WHERE agents_agent.agent_id=$2;`
 | 
			
		||||
 | 
			
		||||
					_, err = db.Exec(stmt, b, r.Agentid)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						logger.Errorln(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	nc.Flush()
 | 
			
		||||
 | 
			
		||||
	if err := nc.LastError(); err != nil {
 | 
			
		||||
		logger.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
	runtime.Goexit()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										257
									
								
								natsapi/tasks.go
									
									
									
									
									
								
							
							
						
						
									
										257
									
								
								natsapi/tasks.go
									
									
									
									
									
								
							@@ -1,257 +0,0 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	_ "github.com/lib/pq"
 | 
			
		||||
	nats "github.com/nats-io/nats.go"
 | 
			
		||||
	"github.com/ugorji/go/codec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type JsonFile struct {
 | 
			
		||||
	Agents  []string `json:"agents"`
 | 
			
		||||
	Key     string   `json:"key"`
 | 
			
		||||
	NatsURL string   `json:"natsurl"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DjangoConfig struct {
 | 
			
		||||
	Key     string `json:"key"`
 | 
			
		||||
	NatsURL string `json:"natsurl"`
 | 
			
		||||
	User    string `json:"user"`
 | 
			
		||||
	Pass    string `json:"pass"`
 | 
			
		||||
	Host    string `json:"host"`
 | 
			
		||||
	Port    int    `json:"port"`
 | 
			
		||||
	DBName  string `json:"dbname"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Agent struct {
 | 
			
		||||
	ID      int    `db:"id"`
 | 
			
		||||
	AgentID string `db:"agent_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Recovery struct {
 | 
			
		||||
	Func string            `json:"func"`
 | 
			
		||||
	Data map[string]string `json:"payload"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setupNatsOptions(key string) []nats.Option {
 | 
			
		||||
	opts := []nats.Option{
 | 
			
		||||
		nats.Name("TacticalRMM"),
 | 
			
		||||
		nats.UserInfo("tacticalrmm", key),
 | 
			
		||||
		nats.ReconnectWait(time.Second * 2),
 | 
			
		||||
		nats.RetryOnFailedConnect(true),
 | 
			
		||||
		nats.MaxReconnects(3),
 | 
			
		||||
		nats.ReconnectBufSize(-1),
 | 
			
		||||
	}
 | 
			
		||||
	return opts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func CheckIn(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"})
 | 
			
		||||
 | 
			
		||||
	opts := setupNatsOptions(r.Key)
 | 
			
		||||
 | 
			
		||||
	nc, err := nats.Connect(r.NatsURL, opts...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer nc.Close()
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
			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 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 != nil {
 | 
			
		||||
						fmt.Println(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}(a.AgentID, a.ID, nc, &wg, db, now)
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
	db.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAgents(file string) (agents []Agent, db *sqlx.DB, r DjangoConfig, err error) {
 | 
			
		||||
	jret, _ := ioutil.ReadFile(file)
 | 
			
		||||
	err = json.Unmarshal(jret, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	db.SetMaxOpenConns(15)
 | 
			
		||||
 | 
			
		||||
	agent := Agent{}
 | 
			
		||||
	rows, err := db.Queryx("SELECT agents_agent.id, agents_agent.agent_id FROM agents_agent")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for rows.Next() {
 | 
			
		||||
		err := rows.StructScan(&agent)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		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": "agentinfo"})
 | 
			
		||||
 | 
			
		||||
	opts := setupNatsOptions(r.Key)
 | 
			
		||||
 | 
			
		||||
	nc, err := nats.Connect(r.NatsURL, opts...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer nc.Close()
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	wg.Add(len(agents))
 | 
			
		||||
 | 
			
		||||
	for _, a := range agents {
 | 
			
		||||
		go func(id string, pk int, nc *nats.Conn, wg *sync.WaitGroup, db *sqlx.DB) {
 | 
			
		||||
			defer wg.Done()
 | 
			
		||||
 | 
			
		||||
			var r AgentInfoRet
 | 
			
		||||
			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(&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)
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
	db.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetWMI(file string) {
 | 
			
		||||
	var result JsonFile
 | 
			
		||||
	var payload []byte
 | 
			
		||||
	var mh codec.MsgpackHandle
 | 
			
		||||
	mh.RawToString = true
 | 
			
		||||
	ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle))
 | 
			
		||||
	ret.Encode(map[string]string{"func": "wmi"})
 | 
			
		||||
 | 
			
		||||
	jret, _ := ioutil.ReadFile(file)
 | 
			
		||||
	err := json.Unmarshal(jret, &result)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := setupNatsOptions(result.Key)
 | 
			
		||||
 | 
			
		||||
	nc, err := nats.Connect(result.NatsURL, opts...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalln(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer nc.Close()
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	wg.Add(len(result.Agents))
 | 
			
		||||
 | 
			
		||||
	for _, id := range result.Agents {
 | 
			
		||||
		go func(id string, nc *nats.Conn, wg *sync.WaitGroup) {
 | 
			
		||||
			defer wg.Done()
 | 
			
		||||
			time.Sleep(time.Duration(randRange(0, 28)) * time.Second)
 | 
			
		||||
			nc.Publish(id, payload)
 | 
			
		||||
		}(id, nc, &wg)
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								natsapi/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								natsapi/types.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
type Agent struct {
 | 
			
		||||
	ID      int    `db:"id"`
 | 
			
		||||
	AgentID string `db:"agent_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DjangoConfig struct {
 | 
			
		||||
	Key     string `json:"key"`
 | 
			
		||||
	NatsURL string `json:"natsurl"`
 | 
			
		||||
	User    string `json:"user"`
 | 
			
		||||
	Pass    string `json:"pass"`
 | 
			
		||||
	Host    string `json:"host"`
 | 
			
		||||
	Port    int    `json:"port"`
 | 
			
		||||
	DBName  string `json:"dbname"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								natsapi/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								natsapi/utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	_ "github.com/lib/pq"
 | 
			
		||||
	nats "github.com/nats-io/nats.go"
 | 
			
		||||
	trmm "github.com/wh1te909/trmm-shared"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setupNatsOptions(key string) []nats.Option {
 | 
			
		||||
	opts := []nats.Option{
 | 
			
		||||
		nats.Name("TacticalRMM"),
 | 
			
		||||
		nats.UserInfo("tacticalrmm", key),
 | 
			
		||||
		nats.ReconnectWait(time.Second * 2),
 | 
			
		||||
		nats.RetryOnFailedConnect(true),
 | 
			
		||||
		nats.MaxReconnects(-1),
 | 
			
		||||
		nats.ReconnectBufSize(-1),
 | 
			
		||||
	}
 | 
			
		||||
	return opts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetConfig(cfg string) (db *sqlx.DB, r DjangoConfig, err error) {
 | 
			
		||||
	if cfg == "" {
 | 
			
		||||
		cfg = "/rmm/api/tacticalrmm/nats-api.conf"
 | 
			
		||||
		if !trmm.FileExists(cfg) {
 | 
			
		||||
			err = errors.New("unable to find config file")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	jret, _ := ioutil.ReadFile(cfg)
 | 
			
		||||
	err = json.Unmarshal(jret, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	db.SetMaxOpenConns(20)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								restore.sh
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								restore.sh
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="31"
 | 
			
		||||
SCRIPT_VERSION="32"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt update
 | 
			
		||||
@@ -39,20 +39,22 @@ if [ ! "$osname" = "ubuntu" ] && [ ! "$osname" = "debian" ]; then
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# determine system
 | 
			
		||||
if ([ "$osname" = "ubuntu" ] && [ "$fullrelno" = "20.04" ]) || ([ "$osname" = "debian" ] && [ $relno -eq 10 ]); then
 | 
			
		||||
if ([ "$osname" = "ubuntu" ] && [ "$fullrelno" = "20.04" ]) || ([ "$osname" = "debian" ] && [ $relno -ge 10 ]); then
 | 
			
		||||
  echo $fullrel
 | 
			
		||||
else
 | 
			
		||||
 echo $fullrel
 | 
			
		||||
 echo -ne "${RED}Only Ubuntu release 20.04 and Debian 10 are supported\n"
 | 
			
		||||
 echo -ne "${RED}Supported versions: Ubuntu 20.04, Debian 10 and 11\n"
 | 
			
		||||
 echo -ne "Your system does not appear to be supported${NC}\n"
 | 
			
		||||
 exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if ([ "$osname" = "ubuntu" ]); then
 | 
			
		||||
  mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname $codename/mongodb-org/4.4 multiverse"
 | 
			
		||||
# there is no bullseye repo yet for mongo so just use buster on debian 11
 | 
			
		||||
elif ([ "$osname" = "debian" ] && [ $relno -eq 11 ]); then
 | 
			
		||||
  mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname buster/mongodb-org/4.4 main"
 | 
			
		||||
else
 | 
			
		||||
  mongodb_repo="deb [arch=amd64] https://repo.mongodb.org/apt/$osname $codename/mongodb-org/4.4 main"
 | 
			
		||||
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
postgresql_repo="deb [arch=amd64] https://apt.postgresql.org/pub/repos/apt/ $codename-pgdg main"
 | 
			
		||||
@@ -164,14 +166,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.6/Python-3.9.6.tgz
 | 
			
		||||
tar -xf Python-3.9.6.tgz
 | 
			
		||||
cd Python-3.9.6
 | 
			
		||||
wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz
 | 
			
		||||
tar -xf Python-3.9.9.tgz
 | 
			
		||||
cd Python-3.9.9
 | 
			
		||||
./configure --enable-optimizations
 | 
			
		||||
make -j $numprocs
 | 
			
		||||
sudo make altinstall
 | 
			
		||||
cd ~
 | 
			
		||||
sudo rm -rf Python-3.9.6 Python-3.9.6.tgz
 | 
			
		||||
sudo rm -rf Python-3.9.9 Python-3.9.9.tgz
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
print_green 'Installing redis and git'
 | 
			
		||||
@@ -304,6 +306,7 @@ pip install --no-cache-dir setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
 | 
			
		||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
 | 
			
		||||
python manage.py migrate
 | 
			
		||||
python manage.py collectstatic --no-input
 | 
			
		||||
python manage.py create_natsapi_conf
 | 
			
		||||
python manage.py reload_nats
 | 
			
		||||
deactivate
 | 
			
		||||
 | 
			
		||||
@@ -333,7 +336,7 @@ sudo chown -R $USER:$GROUP /home/${USER}/.cache
 | 
			
		||||
print_green 'Enabling Services'
 | 
			
		||||
sudo systemctl daemon-reload
 | 
			
		||||
 | 
			
		||||
for i in celery.service celerybeat.service rmm.service daphne.service nginx
 | 
			
		||||
for i in celery.service celerybeat.service rmm.service daphne.service nats-api.service nginx
 | 
			
		||||
do
 | 
			
		||||
  sudo systemctl enable ${i}
 | 
			
		||||
  sudo systemctl stop ${i}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,48 +10,54 @@
 | 
			
		||||
      .PARAMETER PackageName
 | 
			
		||||
      Use this to specify which software to install eg: PackageName googlechrome
 | 
			
		||||
      .EXAMPLE
 | 
			
		||||
      Hosts 20 PackageName googlechrome
 | 
			
		||||
      -Hosts 20 -PackageName googlechrome
 | 
			
		||||
      .EXAMPLE
 | 
			
		||||
      Mode upgrade Hosts 50
 | 
			
		||||
      -Mode upgrade -Hosts 50
 | 
			
		||||
      .EXAMPLE
 | 
			
		||||
      Mode uninstall PackageName googlechrome
 | 
			
		||||
      -Mode uninstall -PackageName googlechrome
 | 
			
		||||
      .NOTES
 | 
			
		||||
      9/2021 v1 Initial release by @silversword411 and @bradhawkins 
 | 
			
		||||
      11/14/2021 v1.1 Fixing typos and logic flow
 | 
			
		||||
  #>
 | 
			
		||||
 | 
			
		||||
param (
 | 
			
		||||
    [string] $Hosts = "0",
 | 
			
		||||
    [Int] $Hosts = "0",
 | 
			
		||||
    [string] $PackageName,
 | 
			
		||||
    [string] $Mode = "install",
 | 
			
		||||
    [string] $Mode = "install"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
$ErrorCount = 0
 | 
			
		||||
 | 
			
		||||
if (!$PackageName) {
 | 
			
		||||
    write-output "No choco package name provided, please include Example: `"PackageName googlechrome`" `n"
 | 
			
		||||
    $ErrorCount += 1
 | 
			
		||||
if ($Mode -ne "upgrade" -and !$PackageName) {
 | 
			
		||||
    write-output "No choco package name provided, please include Example: `"-PackageName googlechrome`" `n"
 | 
			
		||||
    Exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!$Mode -eq "upgrade") {
 | 
			
		||||
    $randrange = ($Hosts + 1) * 10
 | 
			
		||||
if ($Hosts -ne "0") {
 | 
			
		||||
    $randrange = ($Hosts + 1) * 6
 | 
			
		||||
    # Write-Output "Calculating rnd"
 | 
			
		||||
    # Write-Output "randrange $randrange"
 | 
			
		||||
    $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
 | 
			
		||||
    # Write-Output "rnd=$rnd"
 | 
			
		||||
}
 | 
			
		||||
else {
 | 
			
		||||
    $randrange = ($Hosts + 1) * 6
 | 
			
		||||
    $rnd = Get-Random -Minimum 1 -Maximum $randrange; 
 | 
			
		||||
    $rnd = "1"
 | 
			
		||||
    # Write-Output "rnd set to 1 manually"
 | 
			
		||||
    # Write-Output "rnd=$rnd"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if ($Mode -eq "upgrade") {
 | 
			
		||||
    # Write-Output "Starting Upgrade"
 | 
			
		||||
    Start-Sleep -Seconds $rnd; 
 | 
			
		||||
    choco $Mode $PackageName -y
 | 
			
		||||
    choco upgrade -y all
 | 
			
		||||
    # Write-Output "Running upgrade"
 | 
			
		||||
    Exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# write-output "Running install/uninstall mode"
 | 
			
		||||
Start-Sleep -Seconds $rnd; 
 | 
			
		||||
choco $Mode $PackageName -y
 | 
			
		||||
Exit 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
# Checks local disks for errors reported in event viewer within the last 24 hours
 | 
			
		||||
 | 
			
		||||
$ErrorActionPreference = 'silentlycontinue'
 | 
			
		||||
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
 | 
			
		||||
if (Get-WinEvent -FilterHashtable @{LogName = 'system'; ID = '11', '9', '15', '52', '129', '7', '98'; Level = 2, 3; ProviderName = '*disk*', '*storsvc*', '*ntfs*'; StartTime = $TimeSpan } -MaxEvents 10 | Where-Object -Property Message -Match Volume*)
 | 
			
		||||
{
 | 
			
		||||
    Write-Output "Disk errors detected please investigate"
 | 
			
		||||
    Get-WinEvent -FilterHashtable @{LogName = 'system'; ID = '11', '9', '15', '52', '129', '7', '98'; Level = 2, 3; ProviderName = '*disk*', '*storsvc*', '*ntfs*'; StartTime = $TimeSpan }
 | 
			
		||||
    exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
else {
 | 
			
		||||
    Write-Output "Disks are Healthy"
 | 
			
		||||
    exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
# Checks local disks for errors reported in event viewer within the last 24 hours
 | 
			
		||||
 | 
			
		||||
$ErrorActionPreference = 'silentlycontinue'
 | 
			
		||||
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
 | 
			
		||||
if (Get-WinEvent -FilterHashtable @{LogName = 'system'; ID = '11', '9', '15', '52', '129', '7', '98'; Level = 2, 3; ProviderName = '*disk*', '*storsvc*', '*ntfs*'; StartTime = $TimeSpan } -MaxEvents 10 | Where-Object -Property Message -Match Volume*)
 | 
			
		||||
{
 | 
			
		||||
    Write-Output "Disk errors detected please investigate"
 | 
			
		||||
    Get-WinEvent -FilterHashtable @{LogName = 'system'; ID = '11', '9', '15', '52', '129', '7', '98'; Level = 2, 3; ProviderName = '*disk*', '*storsvc*', '*ntfs*'; StartTime = $TimeSpan }
 | 
			
		||||
    exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
else {
 | 
			
		||||
    Write-Output "Disks are Healthy"
 | 
			
		||||
    exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
							
								
								
									
										484
									
								
								scripts/Win_Win11_Ready.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										484
									
								
								scripts/Win_Win11_Ready.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,484 @@
 | 
			
		||||
#=============================================================================================================================
 | 
			
		||||
#
 | 
			
		||||
#Script to check if a machine is ready for Windows 11
 | 
			
		||||
#Returns 'Not Windows 11 Ready' if any of the checks fail, and returns 'Windows 11 Ready' if they all pass.
 | 
			
		||||
#Useful if running in an automation policy and want to populate a custom field of all agents with their readiness.
 | 
			
		||||
#This is a modified version of the official Microsoft script here: https://aka.ms/HWReadinessScript
 | 
			
		||||
#
 | 
			
		||||
#=============================================================================================================================
 | 
			
		||||
 | 
			
		||||
$exitCode = 0
 | 
			
		||||
 | 
			
		||||
[int]$MinOSDiskSizeGB = 64
 | 
			
		||||
[int]$MinMemoryGB = 4
 | 
			
		||||
[Uint32]$MinClockSpeedMHz = 1000
 | 
			
		||||
[Uint32]$MinLogicalCores = 2
 | 
			
		||||
[Uint16]$RequiredAddressWidth = 64
 | 
			
		||||
 | 
			
		||||
$PASS_STRING = "PASS"
 | 
			
		||||
$FAIL_STRING = "FAIL"
 | 
			
		||||
$FAILED_TO_RUN_STRING = "FAILED TO RUN"
 | 
			
		||||
$UNDETERMINED_CAPS_STRING = "UNDETERMINED"
 | 
			
		||||
$UNDETERMINED_STRING = "Undetermined"
 | 
			
		||||
$CAPABLE_STRING = "Capable"
 | 
			
		||||
$NOT_CAPABLE_STRING = "Not capable"
 | 
			
		||||
$CAPABLE_CAPS_STRING = "CAPABLE"
 | 
			
		||||
$NOT_CAPABLE_CAPS_STRING = "NOT CAPABLE"
 | 
			
		||||
$STORAGE_STRING = "Storage"
 | 
			
		||||
$OS_DISK_SIZE_STRING = "OSDiskSize"
 | 
			
		||||
$MEMORY_STRING = "Memory"
 | 
			
		||||
$SYSTEM_MEMORY_STRING = "System_Memory"
 | 
			
		||||
$GB_UNIT_STRING = "GB"
 | 
			
		||||
$TPM_STRING = "TPM"
 | 
			
		||||
$TPM_VERSION_STRING = "TPMVersion"
 | 
			
		||||
$PROCESSOR_STRING = "Processor"
 | 
			
		||||
$SECUREBOOT_STRING = "SecureBoot"
 | 
			
		||||
$I7_7820HQ_CPU_STRING = "i7-7820hq CPU"
 | 
			
		||||
 | 
			
		||||
# 0=name of check, 1=attribute checked, 2=value, 3=PASS/FAIL/UNDETERMINED
 | 
			
		||||
$logFormat = '{0}: {1}={2}. {3}; '
 | 
			
		||||
 | 
			
		||||
# 0=name of check, 1=attribute checked, 2=value, 3=unit of the value, 4=PASS/FAIL/UNDETERMINED
 | 
			
		||||
$logFormatWithUnit = '{0}: {1}={2}{3}. {4}; '
 | 
			
		||||
 | 
			
		||||
# 0=name of check.
 | 
			
		||||
$logFormatReturnReason = '{0}, '
 | 
			
		||||
 | 
			
		||||
# 0=exception.
 | 
			
		||||
$logFormatException = '{0}; '
 | 
			
		||||
 | 
			
		||||
# 0=name of check, 1= attribute checked and its value, 2=PASS/FAIL/UNDETERMINED
 | 
			
		||||
$logFormatWithBlob = '{0}: {1}. {2}; '
 | 
			
		||||
 | 
			
		||||
# return returnCode is -1 when an exception is thrown. 1 if the value does not meet requirements. 0 if successful. -2 default, script didn't run.
 | 
			
		||||
$outObject = @{ returnCode = -2; returnResult = $FAILED_TO_RUN_STRING; returnReason = ""; logging = "" }
 | 
			
		||||
 | 
			
		||||
# NOT CAPABLE(1) state takes precedence over UNDETERMINED(-1) state
 | 
			
		||||
function Private:UpdateReturnCode {
 | 
			
		||||
    param(
 | 
			
		||||
        [Parameter(Mandatory = $true)]
 | 
			
		||||
        [ValidateRange(-2, 1)]
 | 
			
		||||
        [int] $ReturnCode
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    Switch ($ReturnCode) {
 | 
			
		||||
 | 
			
		||||
        0 {
 | 
			
		||||
            if ($outObject.returnCode -eq -2) {
 | 
			
		||||
                $outObject.returnCode = $ReturnCode
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        1 {
 | 
			
		||||
            $outObject.returnCode = $ReturnCode
 | 
			
		||||
        }
 | 
			
		||||
        -1 {
 | 
			
		||||
            if ($outObject.returnCode -ne 1) {
 | 
			
		||||
                $outObject.returnCode = $ReturnCode
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$Source = @"
 | 
			
		||||
using Microsoft.Win32;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
    public class CpuFamilyResult
 | 
			
		||||
    {
 | 
			
		||||
        public bool IsValid { get; set; }
 | 
			
		||||
        public string Message { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class CpuFamily
 | 
			
		||||
    {
 | 
			
		||||
        [StructLayout(LayoutKind.Sequential)]
 | 
			
		||||
        public struct SYSTEM_INFO
 | 
			
		||||
        {
 | 
			
		||||
            public ushort ProcessorArchitecture;
 | 
			
		||||
            ushort Reserved;
 | 
			
		||||
            public uint PageSize;
 | 
			
		||||
            public IntPtr MinimumApplicationAddress;
 | 
			
		||||
            public IntPtr MaximumApplicationAddress;
 | 
			
		||||
            public IntPtr ActiveProcessorMask;
 | 
			
		||||
            public uint NumberOfProcessors;
 | 
			
		||||
            public uint ProcessorType;
 | 
			
		||||
            public uint AllocationGranularity;
 | 
			
		||||
            public ushort ProcessorLevel;
 | 
			
		||||
            public ushort ProcessorRevision;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [DllImport("kernel32.dll")]
 | 
			
		||||
        internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo);
 | 
			
		||||
 | 
			
		||||
        public enum ProcessorFeature : uint
 | 
			
		||||
        {
 | 
			
		||||
            ARM_SUPPORTED_INSTRUCTIONS = 34
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [DllImport("kernel32.dll")]
 | 
			
		||||
        [return: MarshalAs(UnmanagedType.Bool)]
 | 
			
		||||
        static extern bool IsProcessorFeaturePresent(ProcessorFeature processorFeature);
 | 
			
		||||
 | 
			
		||||
        private const ushort PROCESSOR_ARCHITECTURE_X86 = 0;
 | 
			
		||||
        private const ushort PROCESSOR_ARCHITECTURE_ARM64 = 12;
 | 
			
		||||
        private const ushort PROCESSOR_ARCHITECTURE_X64 = 9;
 | 
			
		||||
 | 
			
		||||
        private const string INTEL_MANUFACTURER = "GenuineIntel";
 | 
			
		||||
        private const string AMD_MANUFACTURER = "AuthenticAMD";
 | 
			
		||||
        private const string QUALCOMM_MANUFACTURER = "Qualcomm Technologies Inc";
 | 
			
		||||
 | 
			
		||||
        public static CpuFamilyResult Validate(string manufacturer, ushort processorArchitecture)
 | 
			
		||||
        {
 | 
			
		||||
            CpuFamilyResult cpuFamilyResult = new CpuFamilyResult();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(manufacturer))
 | 
			
		||||
            {
 | 
			
		||||
                cpuFamilyResult.IsValid = false;
 | 
			
		||||
                cpuFamilyResult.Message = "Manufacturer is null or empty";
 | 
			
		||||
                return cpuFamilyResult;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            string registryPath = "HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0";
 | 
			
		||||
            SYSTEM_INFO sysInfo = new SYSTEM_INFO();
 | 
			
		||||
            GetNativeSystemInfo(ref sysInfo);
 | 
			
		||||
 | 
			
		||||
            switch (processorArchitecture)
 | 
			
		||||
            {
 | 
			
		||||
                case PROCESSOR_ARCHITECTURE_ARM64:
 | 
			
		||||
 | 
			
		||||
                    if (manufacturer.Equals(QUALCOMM_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        bool isArmv81Supported = IsProcessorFeaturePresent(ProcessorFeature.ARM_SUPPORTED_INSTRUCTIONS);
 | 
			
		||||
 | 
			
		||||
                        if (!isArmv81Supported)
 | 
			
		||||
                        {
 | 
			
		||||
                            string registryName = "CP 4030";
 | 
			
		||||
                            long registryValue = (long)Registry.GetValue(registryPath, registryName, -1);
 | 
			
		||||
                            long atomicResult = (registryValue >> 20) & 0xF;
 | 
			
		||||
 | 
			
		||||
                            if (atomicResult >= 2)
 | 
			
		||||
                            {
 | 
			
		||||
                                isArmv81Supported = true;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        cpuFamilyResult.IsValid = isArmv81Supported;
 | 
			
		||||
                        cpuFamilyResult.Message = isArmv81Supported ? "" : "Processor does not implement ARM v8.1 atomic instruction";
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        cpuFamilyResult.IsValid = false;
 | 
			
		||||
                        cpuFamilyResult.Message = "The processor isn't currently supported for Windows 11";
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case PROCESSOR_ARCHITECTURE_X64:
 | 
			
		||||
                case PROCESSOR_ARCHITECTURE_X86:
 | 
			
		||||
 | 
			
		||||
                    int cpuFamily = sysInfo.ProcessorLevel;
 | 
			
		||||
                    int cpuModel = (sysInfo.ProcessorRevision >> 8) & 0xFF;
 | 
			
		||||
                    int cpuStepping = sysInfo.ProcessorRevision & 0xFF;
 | 
			
		||||
 | 
			
		||||
                    if (manufacturer.Equals(INTEL_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        try
 | 
			
		||||
                        {
 | 
			
		||||
                            cpuFamilyResult.IsValid = true;
 | 
			
		||||
                            cpuFamilyResult.Message = "";
 | 
			
		||||
 | 
			
		||||
                            if (cpuFamily == 6)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (cpuModel <= 95 && cpuModel != 85)
 | 
			
		||||
                                {
 | 
			
		||||
                                    cpuFamilyResult.IsValid = false;
 | 
			
		||||
                                    cpuFamilyResult.Message = "";
 | 
			
		||||
                                }
 | 
			
		||||
                                else if ((cpuModel == 142 || cpuModel == 158) && cpuStepping == 9)
 | 
			
		||||
                                {
 | 
			
		||||
                                    string registryName = "Platform Specific Field 1";
 | 
			
		||||
                                    int registryValue = (int)Registry.GetValue(registryPath, registryName, -1);
 | 
			
		||||
 | 
			
		||||
                                    if ((cpuModel == 142 && registryValue != 16) || (cpuModel == 158 && registryValue != 8))
 | 
			
		||||
                                    {
 | 
			
		||||
                                        cpuFamilyResult.IsValid = false;
 | 
			
		||||
                                    }
 | 
			
		||||
                                    cpuFamilyResult.Message = "PlatformId " + registryValue;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        catch (Exception ex)
 | 
			
		||||
                        {
 | 
			
		||||
                            cpuFamilyResult.IsValid = false;
 | 
			
		||||
                            cpuFamilyResult.Message = "Exception:" + ex.GetType().Name;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (manufacturer.Equals(AMD_MANUFACTURER, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        cpuFamilyResult.IsValid = true;
 | 
			
		||||
                        cpuFamilyResult.Message = "";
 | 
			
		||||
 | 
			
		||||
                        if (cpuFamily < 23 || (cpuFamily == 23 && (cpuModel == 1 || cpuModel == 17)))
 | 
			
		||||
                        {
 | 
			
		||||
                            cpuFamilyResult.IsValid = false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        cpuFamilyResult.IsValid = false;
 | 
			
		||||
                        cpuFamilyResult.Message = "Unsupported Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                default:
 | 
			
		||||
                    cpuFamilyResult.IsValid = false;
 | 
			
		||||
                    cpuFamilyResult.Message = "Unsupported CPU category. Manufacturer: " + manufacturer + ", Architecture: " + processorArchitecture + ", CPUFamily: " + sysInfo.ProcessorLevel + ", ProcessorRevision: " + sysInfo.ProcessorRevision;
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            return cpuFamilyResult;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
"@
 | 
			
		||||
 | 
			
		||||
# Storage
 | 
			
		||||
try {
 | 
			
		||||
    $osDrive = Get-WmiObject -Class Win32_OperatingSystem | Select-Object -Property SystemDrive
 | 
			
		||||
    $osDriveSize = Get-WmiObject -Class Win32_LogicalDisk -filter "DeviceID='$($osDrive.SystemDrive)'" | Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } }  
 | 
			
		||||
 | 
			
		||||
    if ($null -eq $osDriveSize) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode 1
 | 
			
		||||
        $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING
 | 
			
		||||
        $outObject.logging += $logFormatWithBlob -f $STORAGE_STRING, "Storage is null", $FAIL_STRING
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
    elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode 1
 | 
			
		||||
        $outObject.returnReason += $logFormatReturnReason -f $STORAGE_STRING
 | 
			
		||||
        $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $FAIL_STRING
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $outObject.logging += $logFormatWithUnit -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, ($osDriveSize.SizeGB), $GB_UNIT_STRING, $PASS_STRING
 | 
			
		||||
        UpdateReturnCode -ReturnCode 0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
catch {
 | 
			
		||||
    UpdateReturnCode -ReturnCode -1
 | 
			
		||||
    $outObject.logging += $logFormat -f $STORAGE_STRING, $OS_DISK_SIZE_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Memory (bytes)
 | 
			
		||||
try {
 | 
			
		||||
    $memory = Get-WmiObject Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } }
 | 
			
		||||
 | 
			
		||||
    if ($null -eq $memory) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode 1
 | 
			
		||||
        $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING
 | 
			
		||||
        $outObject.logging += $logFormatWithBlob -f $MEMORY_STRING, "Memory is null", $FAIL_STRING
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
    elseif ($memory.SizeGB -lt $MinMemoryGB) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode 1
 | 
			
		||||
        $outObject.returnReason += $logFormatReturnReason -f $MEMORY_STRING
 | 
			
		||||
        $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $FAIL_STRING
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $outObject.logging += $logFormatWithUnit -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, ($memory.SizeGB), $GB_UNIT_STRING, $PASS_STRING
 | 
			
		||||
        UpdateReturnCode -ReturnCode 0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
catch {
 | 
			
		||||
    UpdateReturnCode -ReturnCode -1
 | 
			
		||||
    $outObject.logging += $logFormat -f $MEMORY_STRING, $SYSTEM_MEMORY_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# TPM
 | 
			
		||||
try {
 | 
			
		||||
    $tpm = Get-Tpm
 | 
			
		||||
 | 
			
		||||
    if ($null -eq $tpm) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode 1
 | 
			
		||||
        $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
 | 
			
		||||
        $outObject.logging += $logFormatWithBlob -f $TPM_STRING, "TPM is null", $FAIL_STRING
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
    elseif ($tpm.TpmPresent) {
 | 
			
		||||
        $tpmVersion = Get-WmiObject -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm | Select-Object -Property SpecVersion
 | 
			
		||||
 | 
			
		||||
        if ($null -eq $tpmVersion.SpecVersion) {
 | 
			
		||||
            UpdateReturnCode -ReturnCode 1
 | 
			
		||||
            $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
 | 
			
		||||
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, "null", $FAIL_STRING
 | 
			
		||||
            $exitCode = 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $majorVersion = $tpmVersion.SpecVersion.Split(",")[0] -as [int]
 | 
			
		||||
        if ($majorVersion -lt 2) {
 | 
			
		||||
            UpdateReturnCode -ReturnCode 1
 | 
			
		||||
            $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
 | 
			
		||||
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $FAIL_STRING
 | 
			
		||||
            $exitCode = 1
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpmVersion.SpecVersion), $PASS_STRING
 | 
			
		||||
            UpdateReturnCode -ReturnCode 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        if ($tpm.GetType().Name -eq "String") {
 | 
			
		||||
            UpdateReturnCode -ReturnCode -1
 | 
			
		||||
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
            $outObject.logging += $logFormatException -f $tpm
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            UpdateReturnCode -ReturnCode  1
 | 
			
		||||
            $outObject.returnReason += $logFormatReturnReason -f $TPM_STRING
 | 
			
		||||
            $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, ($tpm.TpmPresent), $FAIL_STRING
 | 
			
		||||
        }
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
catch {
 | 
			
		||||
    UpdateReturnCode -ReturnCode -1
 | 
			
		||||
    $outObject.logging += $logFormat -f $TPM_STRING, $TPM_VERSION_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# CPU Details
 | 
			
		||||
$cpuDetails;
 | 
			
		||||
try {
 | 
			
		||||
    $cpuDetails = @(Get-WmiObject -Class Win32_Processor)[0]
 | 
			
		||||
 | 
			
		||||
    if ($null -eq $cpuDetails) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode 1
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
        $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING
 | 
			
		||||
        $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, "CpuDetails is null", $FAIL_STRING
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $processorCheckFailed = $false
 | 
			
		||||
 | 
			
		||||
        # AddressWidth
 | 
			
		||||
        if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) {
 | 
			
		||||
            UpdateReturnCode -ReturnCode 1
 | 
			
		||||
            $processorCheckFailed = $true
 | 
			
		||||
            $exitCode = 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # ClockSpeed is in MHz
 | 
			
		||||
        if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -le $MinClockSpeedMHz) {
 | 
			
		||||
            UpdateReturnCode -ReturnCode 1;
 | 
			
		||||
            $processorCheckFailed = $true
 | 
			
		||||
            $exitCode = 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # Number of Logical Cores
 | 
			
		||||
        if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) {
 | 
			
		||||
            UpdateReturnCode -ReturnCode 1
 | 
			
		||||
            $processorCheckFailed = $true
 | 
			
		||||
            $exitCode = 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # CPU Family
 | 
			
		||||
        Add-Type -TypeDefinition $Source
 | 
			
		||||
        $cpuFamilyResult = [CpuFamily]::Validate([String]$cpuDetails.Manufacturer, [uint16]$cpuDetails.Architecture)
 | 
			
		||||
 | 
			
		||||
        $cpuDetailsLog = "{AddressWidth=$($cpuDetails.AddressWidth); MaxClockSpeed=$($cpuDetails.MaxClockSpeed); NumberOfLogicalCores=$($cpuDetails.NumberOfLogicalProcessors); Manufacturer=$($cpuDetails.Manufacturer); Caption=$($cpuDetails.Caption); $($cpuFamilyResult.Message)}"
 | 
			
		||||
 | 
			
		||||
        if (!$cpuFamilyResult.IsValid) {
 | 
			
		||||
            UpdateReturnCode -ReturnCode 1
 | 
			
		||||
            $processorCheckFailed = $true
 | 
			
		||||
            $exitCode = 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($processorCheckFailed) {
 | 
			
		||||
            $outObject.returnReason += $logFormatReturnReason -f $PROCESSOR_STRING
 | 
			
		||||
            $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $FAIL_STRING
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $outObject.logging += $logFormatWithBlob -f $PROCESSOR_STRING, ($cpuDetailsLog), $PASS_STRING
 | 
			
		||||
            UpdateReturnCode -ReturnCode 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
catch {
 | 
			
		||||
    UpdateReturnCode -ReturnCode -1
 | 
			
		||||
    $outObject.logging += $logFormat -f $PROCESSOR_STRING, $PROCESSOR_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# SecureBooot
 | 
			
		||||
try {
 | 
			
		||||
    $isSecureBootEnabled = Confirm-SecureBootUEFI
 | 
			
		||||
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $CAPABLE_STRING, $PASS_STRING
 | 
			
		||||
    UpdateReturnCode -ReturnCode 0
 | 
			
		||||
}
 | 
			
		||||
catch [System.PlatformNotSupportedException] {
 | 
			
		||||
    # PlatformNotSupportedException "Cmdlet not supported on this platform." - SecureBoot is not supported or is non-UEFI computer.
 | 
			
		||||
    UpdateReturnCode -ReturnCode 1
 | 
			
		||||
    $outObject.returnReason += $logFormatReturnReason -f $SECUREBOOT_STRING
 | 
			
		||||
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $NOT_CAPABLE_STRING, $FAIL_STRING
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
catch [System.UnauthorizedAccessException] {
 | 
			
		||||
    UpdateReturnCode -ReturnCode -1
 | 
			
		||||
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
catch {
 | 
			
		||||
    UpdateReturnCode -ReturnCode -1
 | 
			
		||||
    $outObject.logging += $logFormatWithBlob -f $SECUREBOOT_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
    $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
    $exitCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# i7-7820hq CPU
 | 
			
		||||
try {
 | 
			
		||||
    $supportedDevices = @('surface studio 2', 'precision 5520')
 | 
			
		||||
    $systemInfo = @(Get-WmiObject -Class Win32_ComputerSystem)[0]
 | 
			
		||||
 | 
			
		||||
    if ($null -ne $cpuDetails) {
 | 
			
		||||
        if ($cpuDetails.Name -match 'i7-7820hq cpu @ 2.90ghz') {
 | 
			
		||||
            $modelOrSKUCheckLog = $systemInfo.Model.Trim()
 | 
			
		||||
            if ($supportedDevices -contains $modelOrSKUCheckLog) {
 | 
			
		||||
                $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $modelOrSKUCheckLog, $PASS_STRING
 | 
			
		||||
                $outObject.returnCode = 0
 | 
			
		||||
                $exitCode = 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
catch {
 | 
			
		||||
    if ($outObject.returnCode -ne 0) {
 | 
			
		||||
        UpdateReturnCode -ReturnCode -1
 | 
			
		||||
        $outObject.logging += $logFormatWithBlob -f $I7_7820HQ_CPU_STRING, $UNDETERMINED_STRING, $UNDETERMINED_CAPS_STRING
 | 
			
		||||
        $outObject.logging += $logFormatException -f "$($_.Exception.GetType().Name) $($_.Exception.Message)"
 | 
			
		||||
        $exitCode = 1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Switch ($outObject.returnCode) {
 | 
			
		||||
 | 
			
		||||
    0 { $outObject.returnResult = $CAPABLE_CAPS_STRING }
 | 
			
		||||
    1 { $outObject.returnResult = $NOT_CAPABLE_CAPS_STRING }
 | 
			
		||||
    -1 { $outObject.returnResult = $UNDETERMINED_CAPS_STRING }
 | 
			
		||||
    -2 { $outObject.returnResult = $FAILED_TO_RUN_STRING }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if (0 -eq $outObject.returncode) {
 | 
			
		||||
    "Windows 11 Ready"
 | 
			
		||||
}
 | 
			
		||||
else {
 | 
			
		||||
    "Not Windows 11 Ready"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								scripts_wip/Win_Disk_Status_New.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								scripts_wip/Win_Disk_Status_New.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
<#
 | 
			
		||||
      .SYNOPSIS
 | 
			
		||||
      Used to monitor Disk Health, returns error when having issues
 | 
			
		||||
      .DESCRIPTION
 | 
			
		||||
      Monitors the Event Viewer | System Log | For known Disk related errors. If no parameters are specified, it'll only search the last 1 day of event logs (good for a daily run/alert using Tasks and Automation)
 | 
			
		||||
      .PARAMETER Time
 | 
			
		||||
      Optional: If specified, it will search that number of days in the Event Viewer | System Logs
 | 
			
		||||
      .EXAMPLE
 | 
			
		||||
      -Time 365
 | 
			
		||||
      .NOTES
 | 
			
		||||
      4/2021 v1 Initial release by dinger1986
 | 
			
		||||
      11/2021 v1.1 Fixing missed bad sectors etc by silversword
 | 
			
		||||
  #>
 | 
			
		||||
 | 
			
		||||
param (
 | 
			
		||||
    [string] $Time = "1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
$ErrorActionPreference = 'silentlycontinue'
 | 
			
		||||
$TimeSpan = (Get-Date) - (New-TimeSpan -Day $Time)
 | 
			
		||||
# ID: 7     
 | 
			
		||||
# ID: 9     
 | 
			
		||||
# ID: 11    
 | 
			
		||||
# ID: 15    
 | 
			
		||||
# ID: 52    
 | 
			
		||||
# ID: 98    
 | 
			
		||||
# ID: 129   "Reset to device, \Device\RaidPort0, was issued."     Provider=storahci
 | 
			
		||||
# ID: 153   Bad Sectors aka "The IO operation at logical block address..."    ProviderName=disk
 | 
			
		||||
if (Get-WinEvent -FilterHashtable @{LogName = 'system'; ID = '7', '9', '11', '15', '52', '98', '129', '153'; Level = 1, 2, 3; ProviderName = '*disk*', '*storsvc*', '*ntfs*'; StartTime = $TimeSpan } -MaxEvents 10 ) {
 | 
			
		||||
    Write-Output "Disk errors detected please investigate"
 | 
			
		||||
    Get-WinEvent -FilterHashtable @{LogName = 'system'; ID = '7', '9', '11', '15', '52', '98', '129', '153'; Level = 1, 2, 3; ProviderName = '*disk*', '*storsvc*', '*ntfs*'; StartTime = $TimeSpan } | Format-List TimeCreated, Id, LevelDisplayName, Message
 | 
			
		||||
    exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
else {
 | 
			
		||||
    Write-Output "Disks are Healthy"
 | 
			
		||||
    exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
@@ -1,129 +0,0 @@
 | 
			
		||||
# If this is a virtual machine, we don't need to continue
 | 
			
		||||
$Computer = Get-CimInstance -ClassName 'Win32_ComputerSystem'
 | 
			
		||||
if ($Computer.Model -like 'Virtual*') {
 | 
			
		||||
    exit
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
$disks = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' |
 | 
			
		||||
    Select-Object 'InstanceName')
 | 
			
		||||
 
 | 
			
		||||
$Warnings = @()
 | 
			
		||||
 
 | 
			
		||||
foreach ($disk in $disks.InstanceName) {
 | 
			
		||||
    # Retrieve SMART data
 | 
			
		||||
    $SmartData = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_ATAPISMartData' |
 | 
			
		||||
    Where-Object 'InstanceName' -eq $disk)
 | 
			
		||||
 
 | 
			
		||||
    [Byte[]]$RawSmartData = $SmartData | Select-Object -ExpandProperty 'VendorSpecific'
 | 
			
		||||
    
 | 
			
		||||
    # Starting at the third number (first two are irrelevant)
 | 
			
		||||
    # get the relevant data by iterating over every 12th number
 | 
			
		||||
    # and saving the values from an offset of the SMART attribute ID
 | 
			
		||||
    [PSCustomObject[]]$Output = for ($i = 2; $i -lt $RawSmartData.Count; $i++) {
 | 
			
		||||
        if (0 -eq ($i - 2) % 12 -and $RawSmartData[$i] -ne 0) {
 | 
			
		||||
            # Construct the raw attribute value by combining the two bytes that make it up
 | 
			
		||||
            [Decimal]$RawValue = ($RawSmartData[$i + 6] * [Math]::Pow(2, 8) + $RawSmartData[$i + 5])
 | 
			
		||||
 
 | 
			
		||||
            $InnerOutput = [PSCustomObject]@{
 | 
			
		||||
                DiskID   = $disk
 | 
			
		||||
                ID       = $RawSmartData[$i]
 | 
			
		||||
                #Flags    = $RawSmartData[$i + 1]
 | 
			
		||||
                #Value    = $RawSmartData[$i + 3]
 | 
			
		||||
                Worst    = $RawSmartData[$i + 4]
 | 
			
		||||
                RawValue = $RawValue
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
            $InnerOutput
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    # Reallocated Sectors Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 5 | Where-Object RawValue -gt 1 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Spin Retry Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 10 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Recalibration Retries 
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 11 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Used Reserved Block Count Total
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 179 | Where-Object RawValue -gt 1 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Erase Failure Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 182 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # SATA Downshift Error Count or Runtime Bad Block
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 183 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # End-to-End error / IOEDC
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 184 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Reported Uncorrectable Errors
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 187 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Command Timeout
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 188 | Where-Object RawValue -gt 2 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # High Fly Writes
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 189 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Temperature Celcius
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 194 | Where-Object RawValue -gt 50 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Reallocation Event Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 196 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Current Pending Sector Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 197 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Uncorrectable Sector Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 198 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # UltraDMA CRC Error Count
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 199 | Where-Object RawValue -ne 0 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # Soft Read Error Rate
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 201 | Where-Object Worst -lt 95 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # SSD Life Left
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 231 | Where-Object Worst -lt 50 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
    # SSD Media Wear Out Indicator
 | 
			
		||||
    $Warnings += $Output | Where-Object ID -eq 233 | Where-Object Worst -lt 50 | Format-Table
 | 
			
		||||
 
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
$Warnings += Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' |
 | 
			
		||||
    Select-Object InstanceName, PredictFailure, Reason |
 | 
			
		||||
    Where-Object {$_.PredictFailure -ne $False} | Format-Table
 | 
			
		||||
 
 | 
			
		||||
$Warnings += Get-CimInstance -ClassName 'Win32_DiskDrive' |
 | 
			
		||||
    Select-Object Model, SerialNumber, Name, Size, Status |
 | 
			
		||||
    Where-Object {$_.status -ne 'OK'} | Format-Table
 | 
			
		||||
 
 | 
			
		||||
$Warnings += Get-PhysicalDisk |
 | 
			
		||||
    Select-Object FriendlyName, Size, MediaType, OperationalStatus, HealthStatus |
 | 
			
		||||
    Where-Object {$_.OperationalStatus -ne 'OK' -or $_.HealthStatus -ne 'Healthy'} | Format-Table
 | 
			
		||||
 
 | 
			
		||||
if ($Warnings) {
 | 
			
		||||
    $Warnings = $warnings | Out-String
 | 
			
		||||
    $Warnings
 | 
			
		||||
    Write-Output "There are SMART impending Failures"
 | 
			
		||||
    Write-Output "$Warnings"
 | 
			
		||||
    Exit 2
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
elseif ($Error) {
 | 
			
		||||
    Write-Output "There were errors detecting smart on this system"
 | 
			
		||||
    Write-Output "$Error"
 | 
			
		||||
    exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
else
 | 
			
		||||
{
 | 
			
		||||
Write-Output "There are no SMART Failures detected"
 | 
			
		||||
exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
							
								
								
									
										62
									
								
								scripts_wip/Win_Network_Connection_Monitor2.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								scripts_wip/Win_Network_Connection_Monitor2.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
<#
 | 
			
		||||
.SYNOPSIS
 | 
			
		||||
Script that will do the ICMPv4 ping and write to file with timestamps for logging.
 | 
			
		||||
.DESCRIPTION
 | 
			
		||||
This script will ping the specified host/IP with time stamps. The result will also be written to the log <Destination_PingOutput.txt> file.
 | 
			
		||||
 | 
			
		||||
Example usage:
 | 
			
		||||
.\Smart_Ping.ps1 -ComputerName myServer1 -min 10
 | 
			
		||||
This will ping the myServer1 for 10 minutes.
 | 
			
		||||
 | 
			
		||||
Author: phyoepaing3.142@gmail.com
 | 
			
		||||
Country: Myanmar(Burma)
 | 
			
		||||
Released: 12/13/2016
 | 
			
		||||
 | 
			
		||||
.EXAMPLE
 | 
			
		||||
.\Smart_Ping.ps1 -ComputerName myServer1 -hr 1 -min 20
 | 
			
		||||
This will ping  myServer1 for 1 hour and 20 minutes. Buffer size will be 32 bytes by default.
 | 
			
		||||
 | 
			
		||||
.\Smart_Ping.ps1 192.168.43.1 -size 5000
 | 
			
		||||
This will ping 192.168.43.1  10 minutes or 600 seconds by defaut. Buffer size will be 5000 bytes.
 | 
			
		||||
 | 
			
		||||
.PARAMETER hr
 | 
			
		||||
Specify the number of hours to ping a specified host. Decimal number is supported. Eg. -hr 0.5 for 30 minutes.
 | 
			
		||||
 | 
			
		||||
.PARAMETER min
 | 
			
		||||
Specify the number of minutes to ping a specified host. Decimal number is supported. Eg. -hr 0.5 for 30 seconds.
 | 
			
		||||
 | 
			
		||||
.PARAMETER sec
 | 
			
		||||
Specify the number of minutes to ping a specified host.
 | 
			
		||||
 | 
			
		||||
.LINK
 | 
			
		||||
You can find this script and more at: https://www.sysadminplus.blogspot.com/
 | 
			
		||||
#>
 | 
			
		||||
 | 
			
		||||
param([Parameter(Position = 0, Mandatory = $true)][string]$ComputerName, [single]$hr = 0, [single]$min = 0, $sec = 0, [int]$size = 32)
 | 
			
		||||
 | 
			
		||||
If ($size -gt 65500) { Write-Host -Fore red "Invalid buffer size specified. Valid range is from 0 to 65500."; Exit; }	## If the buffer size is larger than 65500, then exits the script.
 | 
			
		||||
If ($hr -eq 0 -AND $min -eq 0 -AND $sec -eq 0) { $min = 10 }
 | 
			
		||||
[int]$second = ($hr) * 3600 + $min * 60	+ $sec	## Convert Hour/Minute/Second value to seconds
 | 
			
		||||
$ts = [timespan]::fromseconds($second)					## Covert second values to h:m:ss
 | 
			
		||||
$var1 = "Duration of Ping time is $($ts.ToString("hh\:mm\:ss"))"
 | 
			
		||||
$var2 = "Ping from $($env:ComputerName) to $ComputerName at $([datetime]::now)";
 | 
			
		||||
Write-Host -fore yellow $var1;
 | 
			
		||||
Write-Host -fore yellow $var2;
 | 
			
		||||
$var1 | Out-File "$($ComputerName)_PingOutput.txt"; $var2  | Out-File -Append "$($ComputerName)_PingOutput.txt";
 | 
			
		||||
$Time = @(); ## Create the array to put the time values at each ping
 | 
			
		||||
############## Ping the specific host and manipulate output #################
 | 
			
		||||
Ping -t $ComputerName -n $second -l $size  | where { !($_ -match "ping" -OR $_ -Match "packets" -OR $_ -Match "Approximate" -OR $_ -Match "Minimum" -OR $_ -eq "") } | foreach {
 | 
			
		||||
	If ($_ -match "reply") { "$(([datetime]::now) ) $_" } else { "$(([datetime]::now) ) $_" } 
 | 
			
		||||
	$TimePiece = $_.Split(' ') -match "time"
 | 
			
		||||
	############## Fetch the ping packet round trip time and place into the variable   #########
 | 
			
		||||
	If ($TimePiece -match "<") {
 | 
			
		||||
		$Time += $TimePiece.split('<')[1].trimEnd('ms')
 | 
			
		||||
	} 
 | 
			
		||||
	elseif ($TimePiece -match '=') {
 | 
			
		||||
		$Time += $TimePiece.split('=')[1].trimEnd('ms')
 | 
			
		||||
	}
 | 
			
		||||
} | Tee-Object -Append "$($ComputerName)_PingOutput.txt"  
 | 
			
		||||
############# Calculate the the manimum, maximum & avarage ######################
 | 
			
		||||
$LastLine = "Maximum = $(($Time | measure -maximum).maximum)ms, Minimum = $(($Time | measure -minimum).minimum)ms, Average = $([int]($Time | measure -average).average)ms"
 | 
			
		||||
$LastLine | Out-File -Append "$($ComputerName)_PingOutput.txt"  
 | 
			
		||||
Write-Host -fore yellow $LastLine
 | 
			
		||||
							
								
								
									
										53
									
								
								scripts_wip/Win_Network_Connection_Monitoring.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								scripts_wip/Win_Network_Connection_Monitoring.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
function Start-ConnectionMonitoring {
 | 
			
		||||
    param($isp, $gateway, $Logfile, [int]$Delay = 10, [Ipaddress] $adapter, [switch]$ispPopup, [switch]$gateWayPopup)
 | 
			
		||||
    $spacer = '--------------------------'
 | 
			
		||||
    while ($true) {
 | 
			
		||||
        if (!(Test-Connection $gateway -source $adapter -count 1 -ea Ignore)) {
 | 
			
		||||
            get-date | Add-Content -path $Logfile
 | 
			
		||||
            "$gateWay Connection Failure" | add-content -Path $Logfile
 | 
			
		||||
            $outagetime = Start-ContinousPing -address $gateway -adapter $adapter -Delay $Delay
 | 
			
		||||
            "Total Outage time in Seconds: $outageTime" | Add-Content -path $Logfile
 | 
			
		||||
            if ($gateWayPopup) {
 | 
			
		||||
                New-PopupMessage -location $gateway -outagetime $outagetime
 | 
			
		||||
            }
 | 
			
		||||
            $spacer | add-content -Path $Logfile
 | 
			
		||||
        }
 | 
			
		||||
        if ((!(Test-Connection  $isp -Source $adapter -count 1 -ea Ignore)) -and (Test-Connection $gateway -count 1 -ea Ignore)) {
 | 
			
		||||
            get-date | Add-Content -path $Logfile
 | 
			
		||||
            "$isp Connection Failure" | Add-Content -Path $Logfile
 | 
			
		||||
            $outagetime = Start-ContinousPing -address $isp -adapter $adapter -Delay $Delay
 | 
			
		||||
            "Total Outage time in Seconds: $outageTime" | Add-Content -path $Logfile
 | 
			
		||||
            if ($ispPopup) {
 | 
			
		||||
                New-PopupMessage -location $isp -outagetime $outagetime
 | 
			
		||||
            }           
 | 
			
		||||
            $spacer | add-content -Path $Logfile
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
        }
 | 
			
		||||
        Start-Sleep -Seconds $Delay
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Start-ContinousPing {
 | 
			
		||||
    param($address, [ipaddress] $adapter, [int]$Delay = 10)
 | 
			
		||||
    $currentTime = get-date
 | 
			
		||||
    While (!(Test-Connection $address -Source $adapter -count 1 -ea Ignore)) {
 | 
			
		||||
        Sleep -Seconds $Delay
 | 
			
		||||
    }
 | 
			
		||||
    $outageTime = ((get-date) - $currentTime).TotalSeconds
 | 
			
		||||
    $outageTime
 | 
			
		||||
}
 | 
			
		||||
function New-PopupMessage {
 | 
			
		||||
    param($location, $outagetime)
 | 
			
		||||
    $Popup = New-Object -ComObject Wscript.Shell
 | 
			
		||||
    $popup.popup("$location Failure - seconds: $outagetime ", 0, "$location", 0x1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$Logfile = "c:\temp\connection.log"
 | 
			
		||||
$isp = 'google.com'
 | 
			
		||||
if (!(test-path $Logfile)) {
 | 
			
		||||
    new-item -Path $Logfile
 | 
			
		||||
}
 | 
			
		||||
$IP = (Get-NetIPConfiguration -InterfaceAlias 'Ethernet').ipv4address.ipaddress
 | 
			
		||||
$gateway = (Get-NetIPConfiguration).ipv4defaultGateway.nexthop
 | 
			
		||||
Start-ConnectionMonitoring -isp $isp -gateway $gateway -Logfile $Logfile -adapter $IP -ispPopup -gateWayPopup
 | 
			
		||||
							
								
								
									
										29
									
								
								scripts_wip/Win_User_EnableDisable.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								scripts_wip/Win_User_EnableDisable.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
<#
 | 
			
		||||
      .SYNOPSIS
 | 
			
		||||
      Used to enable or disable users
 | 
			
		||||
      .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 Name
 | 
			
		||||
      Required: Username
 | 
			
		||||
      .PARAMETER Enabled
 | 
			
		||||
      Required: yes/no
 | 
			
		||||
      .EXAMPLE
 | 
			
		||||
      -Name user -Enabled no
 | 
			
		||||
      .NOTES
 | 
			
		||||
      11/15/2021 v1 Initial release by @silversword411
 | 
			
		||||
  #>
 | 
			
		||||
 | 
			
		||||
param (
 | 
			
		||||
    [string] $Name,
 | 
			
		||||
    [string] $Enabled
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if (!$Enabled -or !$Name) {
 | 
			
		||||
    write-output "Missing required parameters. Please include Example: `"-Name username - -Enabled yes/no`" `n"
 | 
			
		||||
    Exit 1
 | 
			
		||||
}
 | 
			
		||||
else {
 | 
			
		||||
    net user $Name /active:$Enabled
 | 
			
		||||
    Write-Output "$Name set as active:$Enabled"
 | 
			
		||||
    Exit 0
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								scripts_wip/Win_Windows_Update_RevertToDefault.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								scripts_wip/Win_Windows_Update_RevertToDefault.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
# Tactical RMM Patch management disables Windows Automatic Update settings by setting the registry key below to 1. 
 | 
			
		||||
# Run this to revert back to default
 | 
			
		||||
 | 
			
		||||
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUOptions" -Value "0"
 | 
			
		||||
@@ -133,6 +133,7 @@ celerystatus=$(systemctl is-active celery)
 | 
			
		||||
celerybeatstatus=$(systemctl is-active celerybeat)
 | 
			
		||||
nginxstatus=$(systemctl is-active nginx)
 | 
			
		||||
natsstatus=$(systemctl is-active nats)
 | 
			
		||||
natsapistatus=$(systemctl is-active nats-api)
 | 
			
		||||
 | 
			
		||||
# RMM Service
 | 
			
		||||
if [ $rmmstatus = active ]; then
 | 
			
		||||
@@ -198,6 +199,17 @@ else
 | 
			
		||||
    echo -ne ${RED}  'nats Service isnt running (Tactical wont work without this)' | tee -a checklog.log
 | 
			
		||||
	printf >&2 "\n\n"
 | 
			
		||||
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# nats-api Service
 | 
			
		||||
if [ $natsapistatus = active ]; then
 | 
			
		||||
    echo -ne ${GREEN} Success nats-api Service is running | tee -a checklog.log
 | 
			
		||||
	printf >&2 "\n\n"
 | 
			
		||||
else
 | 
			
		||||
	printf >&2 "\n\n" | tee -a checklog.log
 | 
			
		||||
    echo -ne ${RED}  'nats-api Service isnt running (Tactical wont work without this)' | tee -a checklog.log
 | 
			
		||||
	printf >&2 "\n\n"
 | 
			
		||||
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
	echo -ne ${YELLOW} Checking Open Ports | tee -a checklog.log 
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								update.sh
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								update.sh
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="125"
 | 
			
		||||
SCRIPT_VERSION="126"
 | 
			
		||||
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'
 | 
			
		||||
@@ -123,7 +123,30 @@ sudo systemctl daemon-reload
 | 
			
		||||
sudo systemctl enable daphne.service
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
for i in nginx nats rmm daphne celery celerybeat
 | 
			
		||||
if [ ! -f /etc/systemd/system/nats-api.service ]; then
 | 
			
		||||
natsapi="$(cat << EOF
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=TacticalRMM Nats Api v1
 | 
			
		||||
After=nats.service
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
ExecStart=/usr/local/bin/nats-api
 | 
			
		||||
User=${USER}
 | 
			
		||||
Group=${USER}
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=5s
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${natsapi}" | sudo tee /etc/systemd/system/nats-api.service > /dev/null
 | 
			
		||||
sudo systemctl daemon-reload
 | 
			
		||||
sudo systemctl enable nats-api.service
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
for i in nginx nats-api nats rmm daphne celery celerybeat
 | 
			
		||||
do
 | 
			
		||||
printf >&2 "${GREEN}Stopping ${i} service...${NC}\n"
 | 
			
		||||
sudo systemctl stop ${i}
 | 
			
		||||
@@ -175,14 +198,14 @@ 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.6/Python-3.9.6.tgz
 | 
			
		||||
  tar -xf Python-3.9.6.tgz
 | 
			
		||||
  cd Python-3.9.6
 | 
			
		||||
  wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz
 | 
			
		||||
  tar -xf Python-3.9.9.tgz
 | 
			
		||||
  cd Python-3.9.9
 | 
			
		||||
  ./configure --enable-optimizations
 | 
			
		||||
  make -j $numprocs
 | 
			
		||||
  sudo make altinstall
 | 
			
		||||
  cd ~
 | 
			
		||||
  sudo rm -rf Python-3.9.6 Python-3.9.6.tgz
 | 
			
		||||
  sudo rm -rf Python-3.9.9 Python-3.9.9.tgz
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
HAS_LATEST_NATS=$(/usr/local/bin/nats-server -version | grep "${NATS_SERVER_VER}")
 | 
			
		||||
@@ -276,6 +299,7 @@ python manage.py collectstatic --no-input
 | 
			
		||||
python manage.py reload_nats
 | 
			
		||||
python manage.py load_chocos
 | 
			
		||||
python manage.py create_installer_user
 | 
			
		||||
python manage.py create_natsapi_conf
 | 
			
		||||
python manage.py post_update_tasks
 | 
			
		||||
deactivate
 | 
			
		||||
 | 
			
		||||
@@ -292,12 +316,15 @@ sudo rm -rf /var/www/rmm/dist
 | 
			
		||||
sudo cp -pr /rmm/web/dist /var/www/rmm/
 | 
			
		||||
sudo chown www-data:www-data -R /var/www/rmm/dist
 | 
			
		||||
 | 
			
		||||
for i in rmm daphne celery celerybeat nginx nats
 | 
			
		||||
for i in nats nats-api rmm daphne celery celerybeat nginx
 | 
			
		||||
do
 | 
			
		||||
printf >&2 "${GREEN}Starting ${i} service${NC}\n"
 | 
			
		||||
sudo systemctl start ${i}
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
sleep 1
 | 
			
		||||
/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py update_agents
 | 
			
		||||
 | 
			
		||||
CURRENT_MESH_VER=$(cd /meshcentral/node_modules/meshcentral && node -p -e "require('./package.json').version")
 | 
			
		||||
if [[ "${CURRENT_MESH_VER}" != "${LATEST_MESH_VER}" ]] || [[ "$force" = true ]]; then
 | 
			
		||||
  printf >&2 "${GREEN}Updating meshcentral from ${CURRENT_MESH_VER} to ${LATEST_MESH_VER}${NC}\n"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										617
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										617
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -10,19 +10,18 @@
 | 
			
		||||
    "test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@quasar/extras": "^1.11.4",
 | 
			
		||||
    "@quasar/extras": "^1.12.0",
 | 
			
		||||
    "apexcharts": "^3.27.1",
 | 
			
		||||
    "axios": "^0.22.0",
 | 
			
		||||
    "axios": "^0.24.0",
 | 
			
		||||
    "dotenv": "^8.6.0",
 | 
			
		||||
    "prismjs": "^1.23.0",
 | 
			
		||||
    "qrcode.vue": "^3.2.2",
 | 
			
		||||
    "quasar": "^2.3.0",
 | 
			
		||||
    "vue-prism-editor": "^2.0.0-alpha.2",
 | 
			
		||||
    "quasar": "^2.3.2",
 | 
			
		||||
    "vue3-ace-editor": "^2.2.1",
 | 
			
		||||
    "vue3-apexcharts": "^1.4.0",
 | 
			
		||||
    "vuex": "^4.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@quasar/app": "^3.1.10",
 | 
			
		||||
    "@quasar/app": "^3.2.3",
 | 
			
		||||
    "@quasar/cli": "^1.2.2"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,13 @@ export default {
 | 
			
		||||
body
 | 
			
		||||
  overflow-y: hidden
 | 
			
		||||
 | 
			
		||||
.tbl-sticky
 | 
			
		||||
  thead tr th
 | 
			
		||||
    position: sticky
 | 
			
		||||
    z-index: 1
 | 
			
		||||
  thead tr:first-child th
 | 
			
		||||
    top: 0
 | 
			
		||||
 | 
			
		||||
.tabs-tbl-sticky
 | 
			
		||||
 | 
			
		||||
  thead tr th
 | 
			
		||||
 
 | 
			
		||||
@@ -4,17 +4,13 @@ const baseUrl = "/scripts"
 | 
			
		||||
 | 
			
		||||
// script operations
 | 
			
		||||
export async function fetchScripts(params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/`, { params: params })
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/`, { params: params })
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function testScript(agent_id, payload) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.post(`${baseUrl}/${agent_id}/test/`, payload)
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/${agent_id}/test/`, payload)
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function saveScript(payload) {
 | 
			
		||||
@@ -33,45 +29,34 @@ export async function removeScript(id) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function downloadScript(id, params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/${id}/download/`, { params: params })
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/${id}/download/`, { params: params })
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// script snippet operations
 | 
			
		||||
export async function fetchScriptSnippets(params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/snippets/`, { params: params })
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/snippets/`, { params: params })
 | 
			
		||||
  return data
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function saveScriptSnippet(payload) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.post(`${baseUrl}/snippets/`, payload)
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.post(`${baseUrl}/snippets/`, payload)
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function fetchScriptSnippet(id, params = {}) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get(`${baseUrl}/snippets/${id}/`, { params: params })
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.get(`${baseUrl}/snippets/${id}/`, { params: params })
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function editScriptSnippet(payload) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.put(`${baseUrl}/snippets/${payload.id}/`, payload)
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.put(`${baseUrl}/snippets/${payload.id}/`, payload)
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function removeScriptSnippet(id) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.delete(`${baseUrl}/snippets/${id}/`)
 | 
			
		||||
    return data
 | 
			
		||||
  } catch (e) { }
 | 
			
		||||
  const { data } = await axios.delete(`${baseUrl}/snippets/${id}/`)
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
@@ -102,7 +102,13 @@
 | 
			
		||||
        <span class="text-subtitle2 text-bold">Disks</span>
 | 
			
		||||
        <div v-for="disk in disks" :key="disk.device">
 | 
			
		||||
          <span>{{ disk.device }} ({{ disk.fstype }})</span>
 | 
			
		||||
          <q-linear-progress rounded size="15px" :value="disk.percent / 100" color="green" class="q-mt-sm" />
 | 
			
		||||
          <q-linear-progress
 | 
			
		||||
            rounded
 | 
			
		||||
            size="15px"
 | 
			
		||||
            :value="disk.percent / 100"
 | 
			
		||||
            :color="diskBarColor(disk.percent)"
 | 
			
		||||
            class="q-mt-sm"
 | 
			
		||||
          />
 | 
			
		||||
          <span>{{ disk.free }} free of {{ disk.total }}</span>
 | 
			
		||||
          <q-separator />
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -130,6 +136,16 @@ export default {
 | 
			
		||||
    const summary = ref(null);
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
    function diskBarColor(percent) {
 | 
			
		||||
      if (percent < 80) {
 | 
			
		||||
        return "positive";
 | 
			
		||||
      } else if (percent > 80 && percent < 95) {
 | 
			
		||||
        return "warning";
 | 
			
		||||
      } else {
 | 
			
		||||
        return "negative";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const disks = computed(() => {
 | 
			
		||||
      if (!summary.value.disks) {
 | 
			
		||||
        return [];
 | 
			
		||||
@@ -181,6 +197,7 @@ export default {
 | 
			
		||||
      // methods
 | 
			
		||||
      getSummary,
 | 
			
		||||
      refreshSummary,
 | 
			
		||||
      diskBarColor,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -115,7 +115,6 @@ export default {
 | 
			
		||||
        url = `/clients/${this.object.id}/`;
 | 
			
		||||
        data = {
 | 
			
		||||
          client: {
 | 
			
		||||
            pk: this.object.id,
 | 
			
		||||
            server_policy: this.selectedServerPolicy,
 | 
			
		||||
            workstation_policy: this.selectedWorkstationPolicy,
 | 
			
		||||
            block_policy_inheritance: this.blockInheritance,
 | 
			
		||||
@@ -125,7 +124,6 @@ export default {
 | 
			
		||||
        url = `/clients/sites/${this.object.id}/`;
 | 
			
		||||
        data = {
 | 
			
		||||
          site: {
 | 
			
		||||
            pk: this.object.id,
 | 
			
		||||
            server_policy: this.selectedServerPolicy,
 | 
			
		||||
            workstation_policy: this.selectedWorkstationPolicy,
 | 
			
		||||
            block_policy_inheritance: this.blockInheritance,
 | 
			
		||||
@@ -186,7 +184,7 @@ export default {
 | 
			
		||||
    if (this.type !== "agent") {
 | 
			
		||||
      this.selectedServerPolicy = this.object.server_policy;
 | 
			
		||||
      this.selectedWorkstationPolicy = this.object.workstation_policy;
 | 
			
		||||
      this.blockInheritance = this.object.blockInheritance;
 | 
			
		||||
      this.blockInheritance = this.object.block_policy_inheritance;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.selectedAgentPolicy = this.object.policy;
 | 
			
		||||
      this.blockInheritance = this.object.block_policy_inheritance;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide" persistent @keydown.esc="onDialogHide" :maximized="maximized">
 | 
			
		||||
    <q-card class="dialog-plugin" style="min-width: 50vw">
 | 
			
		||||
    <q-card class="dialog-plugin" style="min-width: 60vw">
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        Run a script on {{ agent.hostname }}
 | 
			
		||||
        <q-space />
 | 
			
		||||
@@ -24,7 +24,13 @@
 | 
			
		||||
            outlined
 | 
			
		||||
            mapOptions
 | 
			
		||||
            filterable
 | 
			
		||||
          />
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:after>
 | 
			
		||||
              <q-btn size="sm" round dense flat icon="info" @click="openScriptURL">
 | 
			
		||||
                <q-tooltip v-if="syntax" class="bg-white text-primary text-body1" v-html="formatScriptSyntax(syntax)" />
 | 
			
		||||
              </q-btn>
 | 
			
		||||
            </template>
 | 
			
		||||
          </tactical-dropdown>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
@@ -97,12 +103,12 @@
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
import { useDialogPluginComponent, openURL } from "quasar";
 | 
			
		||||
import { useScriptDropdown } from "@/composables/scripts";
 | 
			
		||||
import { useCustomFieldDropdown } from "@/composables/core";
 | 
			
		||||
import { runScript } from "@/api/agents";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
 | 
			
		||||
import { formatScriptSyntax } from "@/utils/format";
 | 
			
		||||
//ui imports
 | 
			
		||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
 | 
			
		||||
 | 
			
		||||
@@ -128,7 +134,9 @@ export default {
 | 
			
		||||
    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
			
		||||
 | 
			
		||||
    // setup dropdowns
 | 
			
		||||
    const { script, scriptOptions, defaultTimeout, defaultArgs } = useScriptDropdown(props.script, { onMount: true });
 | 
			
		||||
    const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } = useScriptDropdown(props.script, {
 | 
			
		||||
      onMount: true,
 | 
			
		||||
    });
 | 
			
		||||
    const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
 | 
			
		||||
 | 
			
		||||
    // main run script functionaity
 | 
			
		||||
@@ -159,6 +167,10 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function openScriptURL() {
 | 
			
		||||
      link.value ? openURL(link.value) : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // watchers
 | 
			
		||||
    watch([() => state.value.output, () => state.value.emailMode], () => (state.value.emails = []));
 | 
			
		||||
 | 
			
		||||
@@ -167,6 +179,8 @@ export default {
 | 
			
		||||
      state,
 | 
			
		||||
      loading,
 | 
			
		||||
      scriptOptions,
 | 
			
		||||
      link,
 | 
			
		||||
      syntax,
 | 
			
		||||
      ret,
 | 
			
		||||
      maximized,
 | 
			
		||||
      customFieldOptions,
 | 
			
		||||
@@ -175,7 +189,9 @@ export default {
 | 
			
		||||
      outputOptions,
 | 
			
		||||
 | 
			
		||||
      //methods
 | 
			
		||||
      formatScriptSyntax,
 | 
			
		||||
      sendScript,
 | 
			
		||||
      openScriptURL,
 | 
			
		||||
 | 
			
		||||
      // quasar dialog plugin
 | 
			
		||||
      dialogRef,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide" persistent @keydown.esc="onDialogHide" :maximized="maximized">
 | 
			
		||||
    <q-card class="q-dialog-plugin" :style="maximized ? '' : 'width: 70vw; max-width: 90vw'">
 | 
			
		||||
    <q-card class="q-dialog-plugin" :style="maximized ? '' : 'width: 80vw; max-width: 90vw'">
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        {{ title }}
 | 
			
		||||
        <q-space />
 | 
			
		||||
@@ -15,73 +15,63 @@
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <q-form @submit="submitForm">
 | 
			
		||||
        <q-card-section class="row">
 | 
			
		||||
          <div class="q-pa-sm col-1" style="width: auto">
 | 
			
		||||
            <q-icon
 | 
			
		||||
              class="cursor-pointer"
 | 
			
		||||
              :name="formScript.favorite ? 'star' : 'star_outline'"
 | 
			
		||||
              size="md"
 | 
			
		||||
              color="yellow-8"
 | 
			
		||||
              @[clickEvent]="formScript.favorite = !formScript.favorite"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="q-pa-sm col-2">
 | 
			
		||||
            <q-input
 | 
			
		||||
              filled
 | 
			
		||||
              dense
 | 
			
		||||
              :readonly="readonly"
 | 
			
		||||
              v-model="formScript.name"
 | 
			
		||||
              label="Name"
 | 
			
		||||
              :rules="[val => !!val || '*Required']"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="q-pa-sm col-2">
 | 
			
		||||
            <q-select
 | 
			
		||||
              :readonly="readonly"
 | 
			
		||||
              options-dense
 | 
			
		||||
              filled
 | 
			
		||||
              dense
 | 
			
		||||
              v-model="formScript.shell"
 | 
			
		||||
              :options="shellOptions"
 | 
			
		||||
              emit-value
 | 
			
		||||
              map-options
 | 
			
		||||
              label="Shell Type"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="q-pa-sm col-2">
 | 
			
		||||
            <q-input
 | 
			
		||||
              type="number"
 | 
			
		||||
              filled
 | 
			
		||||
              dense
 | 
			
		||||
              :readonly="readonly"
 | 
			
		||||
              v-model.number="formScript.default_timeout"
 | 
			
		||||
              label="Timeout (seconds)"
 | 
			
		||||
              :rules="[val => val >= 5 || 'Minimum is 5']"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="q-pa-sm col-3">
 | 
			
		||||
            <tactical-dropdown
 | 
			
		||||
              hint="Press Enter or Tab when adding a new value"
 | 
			
		||||
              filled
 | 
			
		||||
              v-model="formScript.category"
 | 
			
		||||
              :options="categories"
 | 
			
		||||
              use-input
 | 
			
		||||
              clearable
 | 
			
		||||
              new-value-mode="add-unique"
 | 
			
		||||
              filterable
 | 
			
		||||
              label="Category"
 | 
			
		||||
              :readonly="readonly"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="q-pa-sm col-2">
 | 
			
		||||
            <q-input filled dense :readonly="readonly" v-model="formScript.description" label="Description" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <div class="q-px-sm q-pt-none q-pb-sm q-mt-none row">
 | 
			
		||||
        <div class="q-pt-sm q-px-sm row">
 | 
			
		||||
          <q-input
 | 
			
		||||
            filled
 | 
			
		||||
            dense
 | 
			
		||||
            class="col-2"
 | 
			
		||||
            :readonly="readonly"
 | 
			
		||||
            v-model="formScript.name"
 | 
			
		||||
            label="Name"
 | 
			
		||||
            :rules="[val => !!val || '*Required']"
 | 
			
		||||
          />
 | 
			
		||||
          <q-select
 | 
			
		||||
            class="q-pl-sm col-2"
 | 
			
		||||
            :readonly="readonly"
 | 
			
		||||
            options-dense
 | 
			
		||||
            filled
 | 
			
		||||
            dense
 | 
			
		||||
            v-model="formScript.shell"
 | 
			
		||||
            :options="shellOptions"
 | 
			
		||||
            emit-value
 | 
			
		||||
            map-options
 | 
			
		||||
            label="Shell Type"
 | 
			
		||||
          />
 | 
			
		||||
          <q-input
 | 
			
		||||
            type="number"
 | 
			
		||||
            class="q-pl-sm col-2"
 | 
			
		||||
            filled
 | 
			
		||||
            dense
 | 
			
		||||
            :readonly="readonly"
 | 
			
		||||
            v-model.number="formScript.default_timeout"
 | 
			
		||||
            label="Timeout (seconds)"
 | 
			
		||||
            :rules="[val => val >= 5 || 'Minimum is 5']"
 | 
			
		||||
          />
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            class="q-pl-sm col-3"
 | 
			
		||||
            filled
 | 
			
		||||
            v-model="formScript.category"
 | 
			
		||||
            :options="categories"
 | 
			
		||||
            use-input
 | 
			
		||||
            clearable
 | 
			
		||||
            new-value-mode="add-unique"
 | 
			
		||||
            filterable
 | 
			
		||||
            label="Category"
 | 
			
		||||
            :readonly="readonly"
 | 
			
		||||
            hide-bottom-space
 | 
			
		||||
          />
 | 
			
		||||
          <q-input
 | 
			
		||||
            class="q-pl-sm col-3"
 | 
			
		||||
            filled
 | 
			
		||||
            dense
 | 
			
		||||
            :readonly="readonly"
 | 
			
		||||
            v-model="formScript.description"
 | 
			
		||||
            label="Description"
 | 
			
		||||
          />
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            v-model="formScript.args"
 | 
			
		||||
            label="Script Arguments (press Enter after typing each argument)"
 | 
			
		||||
            class="col-12"
 | 
			
		||||
            class="q-pb-sm col-12 row"
 | 
			
		||||
            filled
 | 
			
		||||
            use-input
 | 
			
		||||
            multiple
 | 
			
		||||
@@ -91,16 +81,41 @@
 | 
			
		||||
            :readonly="readonly"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <CodeEditor
 | 
			
		||||
          v-model="code"
 | 
			
		||||
          :style="maximized ? '--prism-height: 76vh' : '--prism-height: 70vh'"
 | 
			
		||||
          :readonly="readonly"
 | 
			
		||||
          :shell="formScript.shell"
 | 
			
		||||
        <v-ace-editor
 | 
			
		||||
          v-model:value="code"
 | 
			
		||||
          :lang="formScript.shell === 'cmd' ? 'batchfile' : formScript.shell"
 | 
			
		||||
          :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
 | 
			
		||||
          :style="{ height: `${maximized ? '72vh' : '64vh'}` }"
 | 
			
		||||
          wrap
 | 
			
		||||
          :printMargin="false"
 | 
			
		||||
          :options="{ fontSize: '14px' }"
 | 
			
		||||
        />
 | 
			
		||||
        <q-card-actions align="right">
 | 
			
		||||
        <q-card-actions>
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            style="width: 350px"
 | 
			
		||||
            dense
 | 
			
		||||
            :loading="agentLoading"
 | 
			
		||||
            filled
 | 
			
		||||
            v-model="agent"
 | 
			
		||||
            :options="agentOptions"
 | 
			
		||||
            label="Agent to run test script on"
 | 
			
		||||
            mapOptions
 | 
			
		||||
            filterable
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:after>
 | 
			
		||||
              <q-btn
 | 
			
		||||
                size="md"
 | 
			
		||||
                color="primary"
 | 
			
		||||
                dense
 | 
			
		||||
                flat
 | 
			
		||||
                label="Test Script"
 | 
			
		||||
                :disable="!agent || !code || !formScript.default_timeout"
 | 
			
		||||
                @click="openTestScriptModal"
 | 
			
		||||
              />
 | 
			
		||||
            </template>
 | 
			
		||||
          </tactical-dropdown>
 | 
			
		||||
          <q-space />
 | 
			
		||||
          <q-btn dense flat label="Cancel" v-close-popup />
 | 
			
		||||
          <q-btn dense flat color="primary" label="Test Script" @click="openTestScriptModal" />
 | 
			
		||||
          <q-btn v-if="!readonly" :loading="loading" dense flat label="Save" color="primary" type="submit" />
 | 
			
		||||
        </q-card-actions>
 | 
			
		||||
      </q-form>
 | 
			
		||||
@@ -110,15 +125,23 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// composable imports
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
import { ref, computed, onMounted } from "vue";
 | 
			
		||||
import { useQuasar, useDialogPluginComponent } from "quasar";
 | 
			
		||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
 | 
			
		||||
import { useAgentDropdown } from "@/composables/agents";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
import CodeEditor from "@/components/ui/CodeEditor";
 | 
			
		||||
import TestScriptModal from "@/components/scripts/TestScriptModal";
 | 
			
		||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
 | 
			
		||||
import { VAceEditor } from "vue3-ace-editor";
 | 
			
		||||
 | 
			
		||||
// imports for ace editor
 | 
			
		||||
import "ace-builds/src-noconflict/mode-powershell";
 | 
			
		||||
import "ace-builds/src-noconflict/mode-python";
 | 
			
		||||
import "ace-builds/src-noconflict/mode-batchfile";
 | 
			
		||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
 | 
			
		||||
import "ace-builds/src-noconflict/theme-tomorrow";
 | 
			
		||||
 | 
			
		||||
// static data
 | 
			
		||||
import { shellOptions } from "@/composables/scripts";
 | 
			
		||||
@@ -127,8 +150,8 @@ export default {
 | 
			
		||||
  name: "ScriptFormModal",
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
  components: {
 | 
			
		||||
    CodeEditor,
 | 
			
		||||
    TacticalDropdown,
 | 
			
		||||
    VAceEditor,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    script: Object,
 | 
			
		||||
@@ -147,6 +170,9 @@ export default {
 | 
			
		||||
    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
			
		||||
    const $q = useQuasar();
 | 
			
		||||
 | 
			
		||||
    // setup agent dropdown
 | 
			
		||||
    const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
			
		||||
 | 
			
		||||
    // script form logic
 | 
			
		||||
    const script = props.script
 | 
			
		||||
      ? ref(Object.assign({}, props.script))
 | 
			
		||||
@@ -156,8 +182,8 @@ export default {
 | 
			
		||||
    const code = ref("");
 | 
			
		||||
    const maximized = ref(false);
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const agentLoading = ref(false);
 | 
			
		||||
 | 
			
		||||
    const clickEvent = computed(() => (!props.readonly ? "click" : null));
 | 
			
		||||
    const title = computed(() => {
 | 
			
		||||
      if (props.script) {
 | 
			
		||||
        return props.readonly
 | 
			
		||||
@@ -204,22 +230,32 @@ export default {
 | 
			
		||||
        component: TestScriptModal,
 | 
			
		||||
        componentProps: {
 | 
			
		||||
          script: { ...script.value, code: code.value },
 | 
			
		||||
          agent: agent.value,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // component life cycle hooks
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
      agentLoading.value = true;
 | 
			
		||||
      await getAgentOptions();
 | 
			
		||||
      agentLoading.value = false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      // reactive data
 | 
			
		||||
      formScript: script.value,
 | 
			
		||||
      code,
 | 
			
		||||
      maximized,
 | 
			
		||||
      loading,
 | 
			
		||||
      agentOptions,
 | 
			
		||||
      agent,
 | 
			
		||||
      agentLoading,
 | 
			
		||||
 | 
			
		||||
      // non-reactive data
 | 
			
		||||
      shellOptions,
 | 
			
		||||
 | 
			
		||||
      //computed
 | 
			
		||||
      clickEvent,
 | 
			
		||||
      title,
 | 
			
		||||
 | 
			
		||||
      //methods
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
			
		||||
    <q-card class="q-dialog-plugin" style="width: 90vw; max-width: 90vw">
 | 
			
		||||
    <q-card
 | 
			
		||||
      class="q-dialog-plugin"
 | 
			
		||||
      id="script-manager-card"
 | 
			
		||||
      :style="{
 | 
			
		||||
        width: `${$q.screen.width - 100}px`,
 | 
			
		||||
        'max-width': `${$q.screen.width - 100}px`,
 | 
			
		||||
        height: `${$q.screen.height - 100}px`,
 | 
			
		||||
        'max-height': `${$q.screen.height - 100}px`,
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        <q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager
 | 
			
		||||
        <q-space />
 | 
			
		||||
@@ -8,320 +17,307 @@
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <div class="q-pa-md">
 | 
			
		||||
        <div class="q-gutter-sm row">
 | 
			
		||||
          <q-btn-dropdown icon="add" label="New" no-caps dense flat>
 | 
			
		||||
            <q-list dense>
 | 
			
		||||
              <q-item clickable v-close-popup @click="newScriptModal">
 | 
			
		||||
                <q-item-section side>
 | 
			
		||||
                  <q-icon size="xs" name="add" />
 | 
			
		||||
                </q-item-section>
 | 
			
		||||
                <q-item-section>
 | 
			
		||||
                  <q-item-label>New Script</q-item-label>
 | 
			
		||||
                </q-item-section>
 | 
			
		||||
              </q-item>
 | 
			
		||||
              <q-item clickable v-close-popup @click="uploadScriptModal">
 | 
			
		||||
                <q-item-section side>
 | 
			
		||||
                  <q-icon size="xs" name="cloud_upload" />
 | 
			
		||||
                </q-item-section>
 | 
			
		||||
                <q-item-section>
 | 
			
		||||
                  <q-item-label>Upload Script</q-item-label>
 | 
			
		||||
                </q-item-section>
 | 
			
		||||
              </q-item>
 | 
			
		||||
            </q-list>
 | 
			
		||||
          </q-btn-dropdown>
 | 
			
		||||
          <q-btn
 | 
			
		||||
            no-caps
 | 
			
		||||
            dense
 | 
			
		||||
            flat
 | 
			
		||||
            class="q-ml-sm"
 | 
			
		||||
            label="Script Snippets"
 | 
			
		||||
            icon="mdi-script"
 | 
			
		||||
            @click="ScriptSnippetModal"
 | 
			
		||||
          />
 | 
			
		||||
          <q-btn
 | 
			
		||||
            dense
 | 
			
		||||
            flat
 | 
			
		||||
            no-caps
 | 
			
		||||
            class="q-ml-sm"
 | 
			
		||||
            :label="tableView ? 'Folder View' : 'Table View'"
 | 
			
		||||
            :icon="tableView ? 'folder' : 'list'"
 | 
			
		||||
            @click="tableView = !tableView"
 | 
			
		||||
          />
 | 
			
		||||
          <q-btn
 | 
			
		||||
            dense
 | 
			
		||||
            flat
 | 
			
		||||
            no-caps
 | 
			
		||||
            class="q-ml-sm"
 | 
			
		||||
            :label="showCommunityScripts ? 'Hide Community Scripts' : 'Show Community Scripts'"
 | 
			
		||||
            :icon="showCommunityScripts ? 'visibility_off' : 'visibility'"
 | 
			
		||||
            @click="setShowCommunityScripts(!showCommunityScripts)"
 | 
			
		||||
          />
 | 
			
		||||
      <div class="row q-pt-xs q-pl-xs">
 | 
			
		||||
        <q-btn-dropdown icon="add" label="New" no-caps dense flat>
 | 
			
		||||
          <q-list dense>
 | 
			
		||||
            <q-item clickable v-close-popup @click="newScriptModal">
 | 
			
		||||
              <q-item-section side>
 | 
			
		||||
                <q-icon size="xs" name="add" />
 | 
			
		||||
              </q-item-section>
 | 
			
		||||
              <q-item-section>
 | 
			
		||||
                <q-item-label>New Script</q-item-label>
 | 
			
		||||
              </q-item-section>
 | 
			
		||||
            </q-item>
 | 
			
		||||
            <q-item clickable v-close-popup @click="uploadScriptModal">
 | 
			
		||||
              <q-item-section side>
 | 
			
		||||
                <q-icon size="xs" name="cloud_upload" />
 | 
			
		||||
              </q-item-section>
 | 
			
		||||
              <q-item-section>
 | 
			
		||||
                <q-item-label>Upload Script</q-item-label>
 | 
			
		||||
              </q-item-section>
 | 
			
		||||
            </q-item>
 | 
			
		||||
          </q-list>
 | 
			
		||||
        </q-btn-dropdown>
 | 
			
		||||
        <q-btn
 | 
			
		||||
          no-caps
 | 
			
		||||
          dense
 | 
			
		||||
          flat
 | 
			
		||||
          class="q-ml-sm"
 | 
			
		||||
          label="Script Snippets"
 | 
			
		||||
          icon="mdi-script"
 | 
			
		||||
          @click="ScriptSnippetModal"
 | 
			
		||||
        />
 | 
			
		||||
        <q-btn
 | 
			
		||||
          dense
 | 
			
		||||
          flat
 | 
			
		||||
          no-caps
 | 
			
		||||
          class="q-ml-sm"
 | 
			
		||||
          :label="tableView ? 'Folder View' : 'Table View'"
 | 
			
		||||
          :icon="tableView ? 'folder' : 'list'"
 | 
			
		||||
          @click="tableView = !tableView"
 | 
			
		||||
        />
 | 
			
		||||
        <q-btn
 | 
			
		||||
          dense
 | 
			
		||||
          flat
 | 
			
		||||
          no-caps
 | 
			
		||||
          class="q-ml-sm"
 | 
			
		||||
          :label="showCommunityScripts ? 'Hide Community Scripts' : 'Show Community Scripts'"
 | 
			
		||||
          :icon="showCommunityScripts ? 'visibility_off' : 'visibility'"
 | 
			
		||||
          @click="setShowCommunityScripts(!showCommunityScripts)"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
          <q-space />
 | 
			
		||||
          <q-input
 | 
			
		||||
            v-model="search"
 | 
			
		||||
            style="width: 300px"
 | 
			
		||||
            label="Search"
 | 
			
		||||
            dense
 | 
			
		||||
            outlined
 | 
			
		||||
            clearable
 | 
			
		||||
            class="q-pr-md q-pb-xs"
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:prepend>
 | 
			
		||||
              <q-icon name="search" color="primary" />
 | 
			
		||||
            </template>
 | 
			
		||||
          </q-input>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="scroll" style="min-height: 65vh; max-height: 65vh">
 | 
			
		||||
          <!-- List View -->
 | 
			
		||||
          <q-tree
 | 
			
		||||
            ref="folderTree"
 | 
			
		||||
            v-if="!tableView"
 | 
			
		||||
            :nodes="tree"
 | 
			
		||||
            :filter="search"
 | 
			
		||||
            no-connectors
 | 
			
		||||
            node-key="id"
 | 
			
		||||
            v-model:expanded="expanded"
 | 
			
		||||
            no-results-label="No Scripts Found"
 | 
			
		||||
            no-nodes-label="No Scripts Found"
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:header-script="props">
 | 
			
		||||
              <div
 | 
			
		||||
                class="cursor-pointer"
 | 
			
		||||
                @dblclick="
 | 
			
		||||
                  props.node.script_type === 'builtin' ? viewCodeModal(props.node) : editScriptModal(props.node)
 | 
			
		||||
                "
 | 
			
		||||
              >
 | 
			
		||||
                <q-icon v-if="props.node.favorite" color="yellow-8" name="star" size="sm" class="q-px-sm" />
 | 
			
		||||
                <q-icon v-else color="yellow-8" name="star_outline" size="sm" class="q-px-sm" />
 | 
			
		||||
 | 
			
		||||
                <q-icon v-if="props.node.shell === 'powershell'" name="mdi-powershell" color="primary">
 | 
			
		||||
                  <q-tooltip> Powershell </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
                <q-icon v-else-if="props.node.shell === 'python'" name="mdi-language-python" color="primary">
 | 
			
		||||
                  <q-tooltip> Python </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
                <q-icon v-else-if="props.node.shell === 'cmd'" name="mdi-microsoft-windows" color="primary">
 | 
			
		||||
                  <q-tooltip> Batch </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
 | 
			
		||||
                <span class="q-pl-xs text-weight-bold">{{ props.node.name }}</span>
 | 
			
		||||
                <span class="q-pl-xs">{{ props.node.description }}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <!-- context menu -->
 | 
			
		||||
              <q-menu context-menu>
 | 
			
		||||
                <q-list dense style="min-width: 200px">
 | 
			
		||||
                  <q-item clickable v-close-popup @click="viewCodeModal(props.node)">
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="remove_red_eye" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>View Code</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-item clickable v-close-popup @click="cloneScriptModal(props.node)">
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="content_copy" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>Clone</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-item
 | 
			
		||||
                    clickable
 | 
			
		||||
                    v-close-popup
 | 
			
		||||
                    @click="editScriptModal(props.node)"
 | 
			
		||||
                    :disable="props.node.script_type === 'builtin'"
 | 
			
		||||
                  >
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="edit" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>Edit</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-item
 | 
			
		||||
                    clickable
 | 
			
		||||
                    v-close-popup
 | 
			
		||||
                    @click="deleteScript(props.node)"
 | 
			
		||||
                    :disable="props.node.script_type === 'builtin'"
 | 
			
		||||
                  >
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="delete" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>Delete</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                  <q-item clickable v-close-popup @click="favoriteScript(props.node)">
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="star" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>{{
 | 
			
		||||
                      props.node.favorite ? "Remove as Favorite" : "Add as Favorite"
 | 
			
		||||
                    }}</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-item clickable v-close-popup @click="exportScript(props.node)">
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="cloud_download" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>Download Script</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                  <q-item clickable v-close-popup>
 | 
			
		||||
                    <q-item-section>Close</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
                </q-list>
 | 
			
		||||
              </q-menu>
 | 
			
		||||
            </template>
 | 
			
		||||
          </q-tree>
 | 
			
		||||
          <q-table
 | 
			
		||||
            v-if="tableView"
 | 
			
		||||
            dense
 | 
			
		||||
            :table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
 | 
			
		||||
            class="settings-tbl-sticky"
 | 
			
		||||
            :rows="visibleScripts"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
            :loading="loading"
 | 
			
		||||
            v-model:pagination="pagination"
 | 
			
		||||
            :filter="search"
 | 
			
		||||
            row-key="id"
 | 
			
		||||
            binary-state-sort
 | 
			
		||||
            hide-pagination
 | 
			
		||||
            virtual-scroll
 | 
			
		||||
            :rows-per-page-options="[0]"
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:header-cell-favorite="props">
 | 
			
		||||
              <q-th :props="props" auto-width>
 | 
			
		||||
                <q-icon name="star" color="yellow-8" size="sm" />
 | 
			
		||||
              </q-th>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template v-slot:header-cell-shell="props">
 | 
			
		||||
              <q-th :props="props" auto-width> Shell </q-th>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template v-slot:no-data> No Scripts Found </template>
 | 
			
		||||
            <template v-slot:body="props">
 | 
			
		||||
              <!-- Table View -->
 | 
			
		||||
              <q-tr
 | 
			
		||||
                :props="props"
 | 
			
		||||
                @dblclick="props.row.script_type === 'builtin' ? viewCodeModal(props.row) : editScriptModal(props.row)"
 | 
			
		||||
                class="cursor-pointer"
 | 
			
		||||
              >
 | 
			
		||||
                <!-- Context Menu -->
 | 
			
		||||
                <q-menu context-menu>
 | 
			
		||||
                  <q-list dense style="min-width: 200px">
 | 
			
		||||
                    <q-item clickable v-close-popup @click="viewCodeModal(props.row)">
 | 
			
		||||
                      <q-item-section side>
 | 
			
		||||
                        <q-icon name="remove_red_eye" />
 | 
			
		||||
                      </q-item-section>
 | 
			
		||||
                      <q-item-section>View Code</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
 | 
			
		||||
                    <q-item clickable v-close-popup @click="cloneScriptModal(props.row)">
 | 
			
		||||
                      <q-item-section side>
 | 
			
		||||
                        <q-icon name="content_copy" />
 | 
			
		||||
                      </q-item-section>
 | 
			
		||||
                      <q-item-section>Clone</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
 | 
			
		||||
                    <q-item
 | 
			
		||||
                      clickable
 | 
			
		||||
                      v-close-popup
 | 
			
		||||
                      @click="editScriptModal(props.row)"
 | 
			
		||||
                      :disable="props.row.script_type === 'builtin'"
 | 
			
		||||
                    >
 | 
			
		||||
                      <q-item-section side>
 | 
			
		||||
                        <q-icon name="edit" />
 | 
			
		||||
                      </q-item-section>
 | 
			
		||||
                      <q-item-section>Edit</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
 | 
			
		||||
                    <q-item
 | 
			
		||||
                      clickable
 | 
			
		||||
                      v-close-popup
 | 
			
		||||
                      @click="deleteScript(props.row)"
 | 
			
		||||
                      :disable="props.row.script_type === 'builtin'"
 | 
			
		||||
                    >
 | 
			
		||||
                      <q-item-section side>
 | 
			
		||||
                        <q-icon name="delete" />
 | 
			
		||||
                      </q-item-section>
 | 
			
		||||
                      <q-item-section>Delete</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
 | 
			
		||||
                    <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                    <q-item clickable v-close-popup @click="favoriteScript(props.row)">
 | 
			
		||||
                      <q-item-section side>
 | 
			
		||||
                        <q-icon name="star" />
 | 
			
		||||
                      </q-item-section>
 | 
			
		||||
                      <q-item-section>{{
 | 
			
		||||
                        props.row.favorite ? "Remove as Favorite" : "Add as Favorite"
 | 
			
		||||
                      }}</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
 | 
			
		||||
                    <q-item clickable v-close-popup @click="exportScript(props.row)">
 | 
			
		||||
                      <q-item-section side>
 | 
			
		||||
                        <q-icon name="cloud_download" />
 | 
			
		||||
                      </q-item-section>
 | 
			
		||||
                      <q-item-section>Download Script</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
 | 
			
		||||
                    <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                    <q-item clickable v-close-popup>
 | 
			
		||||
                      <q-item-section>Close</q-item-section>
 | 
			
		||||
                    </q-item>
 | 
			
		||||
                  </q-list>
 | 
			
		||||
                </q-menu>
 | 
			
		||||
                <q-td>
 | 
			
		||||
                  <q-icon v-if="props.row.favorite" color="yellow-8" name="star" size="sm" />
 | 
			
		||||
                </q-td>
 | 
			
		||||
                <q-td>
 | 
			
		||||
                  <q-icon v-if="props.row.shell === 'powershell'" name="mdi-powershell" color="primary" size="sm">
 | 
			
		||||
                    <q-tooltip> Powershell </q-tooltip>
 | 
			
		||||
                  </q-icon>
 | 
			
		||||
                  <q-icon v-else-if="props.row.shell === 'python'" name="mdi-language-python" color="primary" size="sm">
 | 
			
		||||
                    <q-tooltip> Python </q-tooltip>
 | 
			
		||||
                  </q-icon>
 | 
			
		||||
                  <q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
 | 
			
		||||
                    <q-tooltip> Batch </q-tooltip>
 | 
			
		||||
                  </q-icon>
 | 
			
		||||
                </q-td>
 | 
			
		||||
                <!-- name -->
 | 
			
		||||
                <q-td>
 | 
			
		||||
                  {{ truncateText(props.row.name, 50) }}
 | 
			
		||||
                  <q-tooltip v-if="props.row.name.length >= 50" style="font-size: 12px">
 | 
			
		||||
                    {{ props.row.name }}
 | 
			
		||||
                  </q-tooltip>
 | 
			
		||||
                </q-td>
 | 
			
		||||
                <!-- args -->
 | 
			
		||||
                <q-td>
 | 
			
		||||
                  <span v-if="props.row.args.length > 0">
 | 
			
		||||
                    {{ truncateText(props.row.args.toString(), 30) }}
 | 
			
		||||
                    <q-tooltip v-if="props.row.args.toString().length >= 30" style="font-size: 12px">
 | 
			
		||||
                      {{ props.row.args }}
 | 
			
		||||
                    </q-tooltip>
 | 
			
		||||
                  </span>
 | 
			
		||||
                </q-td>
 | 
			
		||||
 | 
			
		||||
                <q-td>{{ props.row.category }}</q-td>
 | 
			
		||||
                <q-td>
 | 
			
		||||
                  {{ truncateText(props.row.description, 30) }}
 | 
			
		||||
                  <q-tooltip v-if="props.row.description.length >= 30" style="font-size: 12px">{{
 | 
			
		||||
                    props.row.description
 | 
			
		||||
                  }}</q-tooltip>
 | 
			
		||||
                </q-td>
 | 
			
		||||
                <q-td>{{ props.row.default_timeout }}</q-td>
 | 
			
		||||
              </q-tr>
 | 
			
		||||
            </template>
 | 
			
		||||
          </q-table>
 | 
			
		||||
        </div>
 | 
			
		||||
        <q-space />
 | 
			
		||||
        <q-input v-model="search" style="width: 300px" label="Search" dense outlined clearable class="q-pr-md q-pb-xs">
 | 
			
		||||
          <template v-slot:prepend>
 | 
			
		||||
            <q-icon name="search" color="primary" />
 | 
			
		||||
          </template>
 | 
			
		||||
        </q-input>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- List View -->
 | 
			
		||||
      <div
 | 
			
		||||
        v-if="!tableView"
 | 
			
		||||
        class="scroll q-pl-xs"
 | 
			
		||||
        :style="{ 'max-height': `${$q.screen.height - 182}px`, 'min-height': `${$q.screen.height - 382}px` }"
 | 
			
		||||
      >
 | 
			
		||||
        <q-tree
 | 
			
		||||
          ref="folderTree"
 | 
			
		||||
          :nodes="tree"
 | 
			
		||||
          :filter="search"
 | 
			
		||||
          no-connectors
 | 
			
		||||
          node-key="id"
 | 
			
		||||
          v-model:expanded="expanded"
 | 
			
		||||
          no-results-label="No Scripts Found"
 | 
			
		||||
          no-nodes-label="No Scripts Found"
 | 
			
		||||
        >
 | 
			
		||||
          <template v-slot:header-script="props">
 | 
			
		||||
            <div
 | 
			
		||||
              class="cursor-pointer"
 | 
			
		||||
              @dblclick="props.node.script_type === 'builtin' ? viewCodeModal(props.node) : editScriptModal(props.node)"
 | 
			
		||||
            >
 | 
			
		||||
              <q-icon v-if="props.node.favorite" color="yellow-8" name="star" size="sm" class="q-px-sm" />
 | 
			
		||||
              <q-icon v-else color="yellow-8" name="star_outline" size="sm" class="q-px-sm" />
 | 
			
		||||
 | 
			
		||||
              <q-icon v-if="props.node.shell === 'powershell'" name="mdi-powershell" color="primary">
 | 
			
		||||
                <q-tooltip> Powershell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon v-else-if="props.node.shell === 'python'" name="mdi-language-python" color="primary">
 | 
			
		||||
                <q-tooltip> Python </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon v-else-if="props.node.shell === 'cmd'" name="mdi-microsoft-windows" color="primary">
 | 
			
		||||
                <q-tooltip> Batch </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
 | 
			
		||||
              <span class="q-pl-xs text-weight-bold">{{ props.node.name }}</span>
 | 
			
		||||
              <span class="q-pl-xs">{{ props.node.description }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- context menu -->
 | 
			
		||||
            <q-menu context-menu>
 | 
			
		||||
              <q-list dense style="min-width: 200px">
 | 
			
		||||
                <q-item clickable v-close-popup @click="viewCodeModal(props.node)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="remove_red_eye" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>View Code</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup @click="cloneScriptModal(props.node)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="content_copy" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Clone</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item
 | 
			
		||||
                  clickable
 | 
			
		||||
                  v-close-popup
 | 
			
		||||
                  @click="editScriptModal(props.node)"
 | 
			
		||||
                  :disable="props.node.script_type === 'builtin'"
 | 
			
		||||
                >
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="edit" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Edit</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item
 | 
			
		||||
                  clickable
 | 
			
		||||
                  v-close-popup
 | 
			
		||||
                  @click="deleteScript(props.node)"
 | 
			
		||||
                  :disable="props.node.script_type === 'builtin'"
 | 
			
		||||
                >
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="delete" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Delete</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup @click="favoriteScript(props.node)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="star" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>{{ props.node.favorite ? "Remove as Favorite" : "Add as Favorite" }}</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup @click="exportScript(props.node)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="cloud_download" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Download Script</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup>
 | 
			
		||||
                  <q-item-section>Close</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
              </q-list>
 | 
			
		||||
            </q-menu>
 | 
			
		||||
          </template>
 | 
			
		||||
        </q-tree>
 | 
			
		||||
      </div>
 | 
			
		||||
      <q-table
 | 
			
		||||
        v-if="tableView"
 | 
			
		||||
        dense
 | 
			
		||||
        :table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
 | 
			
		||||
        :style="{ 'max-height': `${$q.screen.height - 182}px` }"
 | 
			
		||||
        class="tbl-sticky"
 | 
			
		||||
        :rows="visibleScripts"
 | 
			
		||||
        :columns="columns"
 | 
			
		||||
        :loading="loading"
 | 
			
		||||
        :pagination="{ rowsPerPage: 0, sortBy: 'favorite', descending: true }"
 | 
			
		||||
        :filter="search"
 | 
			
		||||
        row-key="id"
 | 
			
		||||
        binary-state-sort
 | 
			
		||||
        virtual-scroll
 | 
			
		||||
        :rows-per-page-options="[0]"
 | 
			
		||||
      >
 | 
			
		||||
        <template v-slot:header-cell-favorite="props">
 | 
			
		||||
          <q-th :props="props" auto-width>
 | 
			
		||||
            <q-icon name="star" color="yellow-8" size="sm" />
 | 
			
		||||
          </q-th>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-slot:header-cell-shell="props">
 | 
			
		||||
          <q-th :props="props" auto-width> Shell </q-th>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-slot:no-data> No Scripts Found </template>
 | 
			
		||||
        <template v-slot:body="props">
 | 
			
		||||
          <!-- Table View -->
 | 
			
		||||
          <q-tr
 | 
			
		||||
            :props="props"
 | 
			
		||||
            @dblclick="props.row.script_type === 'builtin' ? viewCodeModal(props.row) : editScriptModal(props.row)"
 | 
			
		||||
            class="cursor-pointer"
 | 
			
		||||
          >
 | 
			
		||||
            <!-- Context Menu -->
 | 
			
		||||
            <q-menu context-menu>
 | 
			
		||||
              <q-list dense style="min-width: 200px">
 | 
			
		||||
                <q-item clickable v-close-popup @click="viewCodeModal(props.row)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="remove_red_eye" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>View Code</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup @click="cloneScriptModal(props.row)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="content_copy" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Clone</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item
 | 
			
		||||
                  clickable
 | 
			
		||||
                  v-close-popup
 | 
			
		||||
                  @click="editScriptModal(props.row)"
 | 
			
		||||
                  :disable="props.row.script_type === 'builtin'"
 | 
			
		||||
                >
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="edit" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Edit</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item
 | 
			
		||||
                  clickable
 | 
			
		||||
                  v-close-popup
 | 
			
		||||
                  @click="deleteScript(props.row)"
 | 
			
		||||
                  :disable="props.row.script_type === 'builtin'"
 | 
			
		||||
                >
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="delete" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Delete</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup @click="favoriteScript(props.row)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="star" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>{{ props.row.favorite ? "Remove as Favorite" : "Add as Favorite" }}</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup @click="exportScript(props.row)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="cloud_download" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Download Script</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                <q-separator></q-separator>
 | 
			
		||||
 | 
			
		||||
                <q-item clickable v-close-popup>
 | 
			
		||||
                  <q-item-section>Close</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
              </q-list>
 | 
			
		||||
            </q-menu>
 | 
			
		||||
            <q-td>
 | 
			
		||||
              <q-icon v-if="props.row.favorite" color="yellow-8" name="star" size="sm" />
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <q-td>
 | 
			
		||||
              <q-icon v-if="props.row.shell === 'powershell'" name="mdi-powershell" color="primary" size="sm">
 | 
			
		||||
                <q-tooltip> Powershell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon v-else-if="props.row.shell === 'python'" name="mdi-language-python" color="primary" size="sm">
 | 
			
		||||
                <q-tooltip> Python </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
 | 
			
		||||
                <q-tooltip> Batch </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <!-- name -->
 | 
			
		||||
            <q-td>
 | 
			
		||||
              {{ truncateText(props.row.name, 50) }}
 | 
			
		||||
              <q-tooltip v-if="props.row.name.length >= 50" style="font-size: 12px">
 | 
			
		||||
                {{ props.row.name }}
 | 
			
		||||
              </q-tooltip>
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <!-- args -->
 | 
			
		||||
            <q-td>
 | 
			
		||||
              <span v-if="props.row.args.length > 0">
 | 
			
		||||
                {{ truncateText(props.row.args.toString(), 30) }}
 | 
			
		||||
                <q-tooltip v-if="props.row.args.toString().length >= 30" style="font-size: 12px">
 | 
			
		||||
                  {{ props.row.args }}
 | 
			
		||||
                </q-tooltip>
 | 
			
		||||
              </span>
 | 
			
		||||
            </q-td>
 | 
			
		||||
 | 
			
		||||
            <q-td>{{ props.row.category }}</q-td>
 | 
			
		||||
            <q-td>
 | 
			
		||||
              {{ truncateText(props.row.description, 30) }}
 | 
			
		||||
              <q-tooltip v-if="props.row.description.length >= 30" style="font-size: 12px">{{
 | 
			
		||||
                props.row.description
 | 
			
		||||
              }}</q-tooltip>
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <q-td>{{ props.row.default_timeout }}</q-td>
 | 
			
		||||
          </q-tr>
 | 
			
		||||
        </template>
 | 
			
		||||
      </q-table>
 | 
			
		||||
    </q-card>
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -410,16 +406,20 @@ export default {
 | 
			
		||||
 | 
			
		||||
    async function getScripts() {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
      scripts.value = await fetchScripts();
 | 
			
		||||
      try {
 | 
			
		||||
        scripts.value = await fetchScripts();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function favoriteScript(script) {
 | 
			
		||||
    async function favoriteScript(script) {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
      const notifyText = !script.favorite ? "Script was favorited!" : "Script was removed as a favorite!";
 | 
			
		||||
      try {
 | 
			
		||||
        editScript({ id: script.id, favorite: !script.favorite });
 | 
			
		||||
        getScripts();
 | 
			
		||||
        const result = await editScript({ id: script.id, favorite: !script.favorite });
 | 
			
		||||
        await getScripts();
 | 
			
		||||
        notifySuccess(notifyText);
 | 
			
		||||
      } catch (e) {}
 | 
			
		||||
 | 
			
		||||
@@ -446,8 +446,12 @@ export default {
 | 
			
		||||
    async function exportScript(script) {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
 | 
			
		||||
      const { code, filename } = await downloadScript(script.id);
 | 
			
		||||
      exportFile(filename, new Blob([code]), { mimeType: "text/plain;charset=utf-8" });
 | 
			
		||||
      try {
 | 
			
		||||
        const { code, filename } = await downloadScript(script.id);
 | 
			
		||||
        exportFile(filename, new Blob([code]), { mimeType: "text/plain;charset=utf-8" });
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -456,11 +460,6 @@ export default {
 | 
			
		||||
    const tableView = ref(true);
 | 
			
		||||
    const expanded = ref([]);
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const pagination = ref({
 | 
			
		||||
      rowsPerPage: 0,
 | 
			
		||||
      sortBy: "favorite",
 | 
			
		||||
      descending: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const visibleScripts = computed(() =>
 | 
			
		||||
      showCommunityScripts.value ? scripts.value : scripts.value.filter(i => i.script_type !== "builtin")
 | 
			
		||||
@@ -494,7 +493,7 @@ export default {
 | 
			
		||||
        // sort by name property
 | 
			
		||||
        const sortedScripts = scriptsTemp.sort(function (a, b) {
 | 
			
		||||
          const nameA = a.name.toUpperCase();
 | 
			
		||||
          const nameB = b.name.toUpperCase(); 
 | 
			
		||||
          const nameB = b.name.toUpperCase();
 | 
			
		||||
 | 
			
		||||
          if (nameA < nameB) {
 | 
			
		||||
            return -1;
 | 
			
		||||
@@ -515,7 +514,7 @@ export default {
 | 
			
		||||
            id: category,
 | 
			
		||||
            children: [],
 | 
			
		||||
          };
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
          for (let x = 0; x < sortedScripts.length; x++) {
 | 
			
		||||
            if (sortedScripts[x].category === category) {
 | 
			
		||||
              temp.children.push({ label: sortedScripts[x].name, header: "script", ...sortedScripts[x] });
 | 
			
		||||
@@ -610,7 +609,6 @@ export default {
 | 
			
		||||
      search,
 | 
			
		||||
      tableView,
 | 
			
		||||
      expanded,
 | 
			
		||||
      pagination,
 | 
			
		||||
      loading,
 | 
			
		||||
      showCommunityScripts,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,37 +15,41 @@
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <q-form @submit="submitForm">
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <div class="q-gutter-sm row">
 | 
			
		||||
            <div class="col-5">
 | 
			
		||||
              <q-input :rules="[val => !!val || '*Required']" v-model="formSnippet.name" label="Name" filled dense />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-2">
 | 
			
		||||
              <q-select
 | 
			
		||||
                v-model="formSnippet.shell"
 | 
			
		||||
                :options="shellOptions"
 | 
			
		||||
                label="Shell Type"
 | 
			
		||||
                options-dense
 | 
			
		||||
                filled
 | 
			
		||||
                dense
 | 
			
		||||
                emit-value
 | 
			
		||||
                map-options
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-4">
 | 
			
		||||
              <q-input filled dense v-model="formSnippet.desc" label="Description" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <div class="row">
 | 
			
		||||
          <q-input
 | 
			
		||||
            :rules="[val => !!val || '*Required']"
 | 
			
		||||
            class="q-pa-sm col-4"
 | 
			
		||||
            v-model="formSnippet.name"
 | 
			
		||||
            label="Name"
 | 
			
		||||
            filled
 | 
			
		||||
            dense
 | 
			
		||||
          />
 | 
			
		||||
          <q-select
 | 
			
		||||
            v-model="formSnippet.shell"
 | 
			
		||||
            :options="shellOptions"
 | 
			
		||||
            class="q-pa-sm col-2"
 | 
			
		||||
            label="Shell Type"
 | 
			
		||||
            options-dense
 | 
			
		||||
            filled
 | 
			
		||||
            dense
 | 
			
		||||
            emit-value
 | 
			
		||||
            map-options
 | 
			
		||||
          />
 | 
			
		||||
          <q-input class="q-pa-sm col-6" filled dense v-model="formSnippet.desc" label="Description" />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <CodeEditor
 | 
			
		||||
          v-model="formSnippet.code"
 | 
			
		||||
          :style="maximized ? '--prism-height: 80vh' : '--prism-height: 70vh'"
 | 
			
		||||
          :shell="formSnippet.shell"
 | 
			
		||||
        <v-ace-editor
 | 
			
		||||
          v-model:value="formSnippet.code"
 | 
			
		||||
          :lang="formSnippet.shell === 'cmd' ? 'batchfile' : formSnippet.shell"
 | 
			
		||||
          :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
 | 
			
		||||
          :style="{ height: `${maximized ? '80vh' : '70vh'}` }"
 | 
			
		||||
          wrap
 | 
			
		||||
          :printMargin="false"
 | 
			
		||||
          :options="{ fontSize: '14px' }"
 | 
			
		||||
        />
 | 
			
		||||
        <q-card-actions align="right">
 | 
			
		||||
          <q-btn flat label="Cancel" v-close-popup />
 | 
			
		||||
          <q-btn :loading="loading" flat label="Save" color="primary" type="submit" />
 | 
			
		||||
          <q-btn dense flat label="Cancel" v-close-popup />
 | 
			
		||||
          <q-btn :loading="loading" dense flat label="Save" color="primary" type="submit" />
 | 
			
		||||
        </q-card-actions>
 | 
			
		||||
      </q-form>
 | 
			
		||||
    </q-card>
 | 
			
		||||
@@ -60,7 +64,14 @@ import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
 | 
			
		||||
import { notifySuccess } from "@/utils/notify";
 | 
			
		||||
 | 
			
		||||
// ui imports
 | 
			
		||||
import CodeEditor from "@/components/ui/CodeEditor";
 | 
			
		||||
import { VAceEditor } from "vue3-ace-editor";
 | 
			
		||||
 | 
			
		||||
// imports for ace editor
 | 
			
		||||
import "ace-builds/src-noconflict/mode-powershell";
 | 
			
		||||
import "ace-builds/src-noconflict/mode-python";
 | 
			
		||||
import "ace-builds/src-noconflict/mode-batchfile";
 | 
			
		||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
 | 
			
		||||
import "ace-builds/src-noconflict/theme-tomorrow";
 | 
			
		||||
 | 
			
		||||
// static data
 | 
			
		||||
import { shellOptions } from "@/composables/scripts";
 | 
			
		||||
@@ -69,7 +80,7 @@ export default {
 | 
			
		||||
  name: "ScriptFormModal",
 | 
			
		||||
  emits: [...useDialogPluginComponent.emits],
 | 
			
		||||
  components: {
 | 
			
		||||
    CodeEditor,
 | 
			
		||||
    VAceEditor,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    snippet: Object,
 | 
			
		||||
@@ -95,20 +106,13 @@ export default {
 | 
			
		||||
 | 
			
		||||
    async function submitForm() {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
      let result = "";
 | 
			
		||||
      try {
 | 
			
		||||
        // edit existing script snippet
 | 
			
		||||
        if (props.snippet) {
 | 
			
		||||
          result = await editScriptSnippet(snippet.value);
 | 
			
		||||
 | 
			
		||||
          // add script snippet
 | 
			
		||||
        } else {
 | 
			
		||||
          result = await saveScriptSnippet(snippet.value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const result = props.snippet ? await editScriptSnippet(snippet.value) : await saveScriptSnippet(snippet.value);
 | 
			
		||||
        onDialogOK();
 | 
			
		||||
        notifySuccess(result);
 | 
			
		||||
      } catch (e) {}
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
			
		||||
    <q-card class="q-dialog-plugin" style="width: 70vw; max-width: 70vw">
 | 
			
		||||
    <q-card
 | 
			
		||||
      class="q-dialog-plugin"
 | 
			
		||||
      :style="{
 | 
			
		||||
        width: `${$q.screen.width - 300}px`,
 | 
			
		||||
        'max-width': `${$q.screen.width - 300}px`,
 | 
			
		||||
        height: `${$q.screen.height - 300}px`,
 | 
			
		||||
        'max-height': `${$q.screen.height - 300}px`,
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        <q-btn @click="getSnippets" class="q-mr-sm" dense flat push icon="refresh" />Script Snippets
 | 
			
		||||
        <q-space />
 | 
			
		||||
@@ -8,70 +16,69 @@
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <q-table
 | 
			
		||||
        dense
 | 
			
		||||
        :table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
 | 
			
		||||
        :style="{ 'max-height': `${$q.screen.height - 300 - 32}px` }"
 | 
			
		||||
        class="tbl-sticky"
 | 
			
		||||
        :rows="snippets"
 | 
			
		||||
        :columns="columns"
 | 
			
		||||
        :loading="loading"
 | 
			
		||||
        :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
 | 
			
		||||
        row-key="id"
 | 
			
		||||
        binary-state-sort
 | 
			
		||||
        virtual-scroll
 | 
			
		||||
        :rows-per-page-options="[0]"
 | 
			
		||||
      >
 | 
			
		||||
        <template v-slot:top>
 | 
			
		||||
          <q-btn dense flat no-caps icon="add" label="New" @click="newSnippetModal" />
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-slot:header-cell-shell="props">
 | 
			
		||||
          <q-th :props="props" auto-width> Shell </q-th>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
      <div class="q-pa-md">
 | 
			
		||||
        <q-btn dense flat no-caps icon="add" label="New" @click="newSnippetModal" />
 | 
			
		||||
        <q-table
 | 
			
		||||
          dense
 | 
			
		||||
          :table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
 | 
			
		||||
          class="settings-tbl-sticky"
 | 
			
		||||
          :rows="snippets"
 | 
			
		||||
          :columns="columns"
 | 
			
		||||
          :loading="loading"
 | 
			
		||||
          v-model:pagination="pagination"
 | 
			
		||||
          row-key="id"
 | 
			
		||||
          binary-state-sort
 | 
			
		||||
          hide-pagination
 | 
			
		||||
          virtual-scroll
 | 
			
		||||
          :rows-per-page-options="[0]"
 | 
			
		||||
        >
 | 
			
		||||
          <template v-slot:header-cell-shell="props">
 | 
			
		||||
            <q-th :props="props" auto-width> Shell </q-th>
 | 
			
		||||
          </template>
 | 
			
		||||
        <template v-slot:body="props">
 | 
			
		||||
          <!-- Table View -->
 | 
			
		||||
          <q-tr :props="props" @dblclick="editSnippetModal(props.row)" class="cursor-pointer">
 | 
			
		||||
            <!-- Context Menu -->
 | 
			
		||||
            <q-menu context-menu>
 | 
			
		||||
              <q-list dense style="min-width: 200px">
 | 
			
		||||
                <q-item clickable v-close-popup @click="editSnippetModal(props.row)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="edit" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Edit</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
          <template v-slot:body="props">
 | 
			
		||||
            <!-- Table View -->
 | 
			
		||||
            <q-tr :props="props" @dblclick="editSnippetModal(props.row)" class="cursor-pointer">
 | 
			
		||||
              <!-- Context Menu -->
 | 
			
		||||
              <q-menu context-menu>
 | 
			
		||||
                <q-list dense style="min-width: 200px">
 | 
			
		||||
                  <q-item clickable v-close-popup @click="editSnippetModal(props.row)">
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="edit" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>Edit</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
                <q-item clickable v-close-popup @click="deleteSnippet(props.row)">
 | 
			
		||||
                  <q-item-section side>
 | 
			
		||||
                    <q-icon name="delete" />
 | 
			
		||||
                  </q-item-section>
 | 
			
		||||
                  <q-item-section>Delete</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-item clickable v-close-popup @click="deleteSnippet(props.row)">
 | 
			
		||||
                    <q-item-section side>
 | 
			
		||||
                      <q-icon name="delete" />
 | 
			
		||||
                    </q-item-section>
 | 
			
		||||
                    <q-item-section>Delete</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
 | 
			
		||||
                  <q-item clickable v-close-popup>
 | 
			
		||||
                    <q-item-section>Close</q-item-section>
 | 
			
		||||
                  </q-item>
 | 
			
		||||
                </q-list>
 | 
			
		||||
              </q-menu>
 | 
			
		||||
              <q-td>
 | 
			
		||||
                <q-icon v-if="props.row.shell === 'powershell'" name="mdi-powershell" color="primary" size="sm">
 | 
			
		||||
                  <q-tooltip> Powershell </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
                <q-icon v-else-if="props.row.shell === 'python'" name="mdi-language-python" color="primary" size="sm">
 | 
			
		||||
                  <q-tooltip> Python </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
                <q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
 | 
			
		||||
                  <q-tooltip> Batch </q-tooltip>
 | 
			
		||||
                </q-icon>
 | 
			
		||||
              </q-td>
 | 
			
		||||
              <!-- name -->
 | 
			
		||||
              <q-td>{{ props.row.name }}</q-td>
 | 
			
		||||
              <q-td>{{ props.row.desc }}</q-td>
 | 
			
		||||
            </q-tr>
 | 
			
		||||
          </template>
 | 
			
		||||
        </q-table>
 | 
			
		||||
      </div>
 | 
			
		||||
                <q-item clickable v-close-popup>
 | 
			
		||||
                  <q-item-section>Close</q-item-section>
 | 
			
		||||
                </q-item>
 | 
			
		||||
              </q-list>
 | 
			
		||||
            </q-menu>
 | 
			
		||||
            <q-td>
 | 
			
		||||
              <q-icon v-if="props.row.shell === 'powershell'" name="mdi-powershell" color="primary" size="sm">
 | 
			
		||||
                <q-tooltip> Powershell </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon v-else-if="props.row.shell === 'python'" name="mdi-language-python" color="primary" size="sm">
 | 
			
		||||
                <q-tooltip> Python </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
              <q-icon v-else-if="props.row.shell === 'cmd'" name="mdi-microsoft-windows" color="primary" size="sm">
 | 
			
		||||
                <q-tooltip> Batch </q-tooltip>
 | 
			
		||||
              </q-icon>
 | 
			
		||||
            </q-td>
 | 
			
		||||
            <!-- name -->
 | 
			
		||||
            <q-td>{{ props.row.name }}</q-td>
 | 
			
		||||
            <q-td>{{ props.row.desc }}</q-td>
 | 
			
		||||
          </q-tr>
 | 
			
		||||
        </template>
 | 
			
		||||
      </q-table>
 | 
			
		||||
    </q-card>
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -124,7 +131,11 @@ export default {
 | 
			
		||||
 | 
			
		||||
    async function getSnippets() {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
      snippets.value = await fetchScriptSnippets();
 | 
			
		||||
      try {
 | 
			
		||||
        snippets.value = await fetchScriptSnippets();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -147,11 +158,6 @@ export default {
 | 
			
		||||
 | 
			
		||||
    // table setup
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
    const pagination = ref({
 | 
			
		||||
      rowsPerPage: 0,
 | 
			
		||||
      sortBy: "name",
 | 
			
		||||
      descending: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function newSnippetModal() {
 | 
			
		||||
      $q.dialog({
 | 
			
		||||
@@ -167,18 +173,15 @@ export default {
 | 
			
		||||
        componentProps: {
 | 
			
		||||
          snippet,
 | 
			
		||||
        },
 | 
			
		||||
      }).onOk(() => {
 | 
			
		||||
        getSnippets();
 | 
			
		||||
      });
 | 
			
		||||
      }).onOk(getSnippets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // component life cycle hooks
 | 
			
		||||
    onMounted(getSnippets());
 | 
			
		||||
    onMounted(getSnippets);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      // reactive data
 | 
			
		||||
      snippets,
 | 
			
		||||
      pagination,
 | 
			
		||||
      loading,
 | 
			
		||||
 | 
			
		||||
      // non-reactive data
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
			
		||||
    <q-card class="q-dialog-plugin" style="min-width: 50vw">
 | 
			
		||||
    <q-card class="q-dialog-plugin" style="min-width: 65vw">
 | 
			
		||||
      <q-bar>
 | 
			
		||||
        Script Test
 | 
			
		||||
        <q-space />
 | 
			
		||||
@@ -8,50 +8,10 @@
 | 
			
		||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
			
		||||
        </q-btn>
 | 
			
		||||
      </q-bar>
 | 
			
		||||
      <q-form @submit.prevent="runTestScript">
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            :rules="[val => !!val || '*Required']"
 | 
			
		||||
            label="Select Agent to run script on"
 | 
			
		||||
            v-model="agent"
 | 
			
		||||
            :options="agentOptions"
 | 
			
		||||
            filterable
 | 
			
		||||
            mapOptions
 | 
			
		||||
            outlined
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <tactical-dropdown
 | 
			
		||||
            v-model="args"
 | 
			
		||||
            label="Script Arguments (press Enter after typing each argument)"
 | 
			
		||||
            filled
 | 
			
		||||
            use-input
 | 
			
		||||
            multiple
 | 
			
		||||
            hide-dropdown-icon
 | 
			
		||||
            input-debounce="0"
 | 
			
		||||
            new-value-mode="add"
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <q-card-section>
 | 
			
		||||
          <q-input
 | 
			
		||||
            v-model.number="timeout"
 | 
			
		||||
            dense
 | 
			
		||||
            outlined
 | 
			
		||||
            type="number"
 | 
			
		||||
            style="max-width: 150px"
 | 
			
		||||
            label="Timeout (seconds)"
 | 
			
		||||
            stack-label
 | 
			
		||||
            :rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
 | 
			
		||||
          />
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <q-card-actions align="right">
 | 
			
		||||
          <q-btn label="Cancel" v-close-popup />
 | 
			
		||||
          <q-btn :loading="loading" label="Run" color="primary" type="submit" />
 | 
			
		||||
        </q-card-actions>
 | 
			
		||||
        <q-card-section v-if="ret" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" style="max-height: 50vh">
 | 
			
		||||
          <pre>{{ ret }}</pre>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
      </q-form>
 | 
			
		||||
      <q-card-section class="scroll" style="max-height: 70vh; height: 70vh">
 | 
			
		||||
        <pre v-if="ret">{{ ret }}</pre>
 | 
			
		||||
        <q-inner-loading :showing="loading" />
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
    </q-card>
 | 
			
		||||
  </q-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -59,7 +19,6 @@
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, onMounted } from "vue";
 | 
			
		||||
import { useAgentDropdown } from "@/composables/agents";
 | 
			
		||||
import { testScript } from "@/api/scripts";
 | 
			
		||||
import { useDialogPluginComponent } from "quasar";
 | 
			
		||||
 | 
			
		||||
@@ -74,18 +33,13 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    script: !Object,
 | 
			
		||||
    agent: !String,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    // setup dropdowns
 | 
			
		||||
    const { agentOptions, getAgentOptions } = useAgentDropdown();
 | 
			
		||||
 | 
			
		||||
    // setup quasar dialog plugin
 | 
			
		||||
    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
			
		||||
 | 
			
		||||
    // main run script functionality
 | 
			
		||||
    const agent = ref(null);
 | 
			
		||||
    const timeout = ref(props.script.default_timeout);
 | 
			
		||||
    const args = ref(props.script.args);
 | 
			
		||||
    const ret = ref(null);
 | 
			
		||||
    const loading = ref(false);
 | 
			
		||||
 | 
			
		||||
@@ -93,28 +47,25 @@ export default {
 | 
			
		||||
      loading.value = true;
 | 
			
		||||
      const data = {
 | 
			
		||||
        code: props.script.code,
 | 
			
		||||
        timeout: timeout.value,
 | 
			
		||||
        args: args.value,
 | 
			
		||||
        timeout: props.script.default_timeout,
 | 
			
		||||
        args: props.script.args,
 | 
			
		||||
        shell: props.script.shell,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      ret.value = await testScript(agent.value, data);
 | 
			
		||||
      try {
 | 
			
		||||
        ret.value = await testScript(props.agent, data);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
      }
 | 
			
		||||
      loading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMounted(getAgentOptions());
 | 
			
		||||
    onMounted(runTestScript);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      // reactive data
 | 
			
		||||
      agent,
 | 
			
		||||
      timeout,
 | 
			
		||||
      args,
 | 
			
		||||
      ret,
 | 
			
		||||
      loading,
 | 
			
		||||
 | 
			
		||||
      // non-reactive data
 | 
			
		||||
      agentOptions,
 | 
			
		||||
 | 
			
		||||
      // methods
 | 
			
		||||
      runTestScript,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <prism-editor class="editor" v-model="code" :highlight="highlighter" line-numbers @click="focusTextArea" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// prism package imports
 | 
			
		||||
import { PrismEditor } from "vue-prism-editor";
 | 
			
		||||
import "vue-prism-editor/dist/prismeditor.min.css";
 | 
			
		||||
 | 
			
		||||
import { highlight, languages } from "prismjs/components/prism-core";
 | 
			
		||||
import "prismjs/components/prism-batch";
 | 
			
		||||
import "prismjs/components/prism-python";
 | 
			
		||||
import "prismjs/components/prism-powershell";
 | 
			
		||||
import "prismjs/themes/prism-tomorrow.css";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "CodeEditor",
 | 
			
		||||
  components: {
 | 
			
		||||
    PrismEditor,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    code: !String,
 | 
			
		||||
    shell: !String,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    function highlighter(code) {
 | 
			
		||||
      if (!props.shell) {
 | 
			
		||||
        return code;
 | 
			
		||||
      }
 | 
			
		||||
      let lang = props.shell === "cmd" ? "batch" : props.shell;
 | 
			
		||||
      return highlight(code, languages[lang]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function focusTextArea() {
 | 
			
		||||
      document.getElementsByClassName("prism-editor__textarea")[0].focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      //methods
 | 
			
		||||
      highlighter,
 | 
			
		||||
      focusTextArea,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
/* required class */
 | 
			
		||||
.editor {
 | 
			
		||||
  /* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
 | 
			
		||||
  background: #2d2d2d;
 | 
			
		||||
  color: #ccc;
 | 
			
		||||
 | 
			
		||||
  /* you must provide font-family font-size line-height. Example: */
 | 
			
		||||
  font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  line-height: 1.5;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  height: var(--prism-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* optional class for removing the outline */
 | 
			
		||||
.prism-editor__textarea:focus {
 | 
			
		||||
  outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prism-editor__textarea,
 | 
			
		||||
.prism-editor__container {
 | 
			
		||||
  width: 500em !important;
 | 
			
		||||
  -ms-overflow-style: none;
 | 
			
		||||
  scrollbar-width: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prism-editor__container::-webkit-scrollbar,
 | 
			
		||||
.prism-editor__textarea::-webkit-scrollbar {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prism-editor__editor {
 | 
			
		||||
  white-space: pre !important;
 | 
			
		||||
}
 | 
			
		||||
.prism-editor__container {
 | 
			
		||||
  overflow-x: auto !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -11,7 +11,12 @@
 | 
			
		||||
    :use-chips="multiple"
 | 
			
		||||
    :use-input="filterable"
 | 
			
		||||
    @[filterEvent]="filterFn"
 | 
			
		||||
    v-bind="$attrs"
 | 
			
		||||
  >
 | 
			
		||||
    <template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
 | 
			
		||||
      <slot :name="slot" v-bind="scope || {}" />
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <template v-slot:option="scope">
 | 
			
		||||
      <q-item
 | 
			
		||||
        v-if="!scope.opt.category"
 | 
			
		||||
@@ -22,6 +27,7 @@
 | 
			
		||||
        <q-item-section>
 | 
			
		||||
          <q-item-label v-html="mapOptions ? scope.opt.label : scope.opt"></q-item-label>
 | 
			
		||||
        </q-item-section>
 | 
			
		||||
        <q-item-section v-if="filtered && mapOptions && scope.opt.cat" side>{{ scope.opt.cat }}</q-item-section>
 | 
			
		||||
      </q-item>
 | 
			
		||||
      <q-item-label v-if="scope.opt.category" header class="q-pa-sm" :key="scope.opt.category">{{
 | 
			
		||||
        scope.opt.category
 | 
			
		||||
@@ -31,10 +37,11 @@
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
// composition imports
 | 
			
		||||
import { ref, toRefs, computed } from "vue";
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "tactical-dropdown",
 | 
			
		||||
  inheritAttrs: false,
 | 
			
		||||
  props: {
 | 
			
		||||
    modelValue: !String,
 | 
			
		||||
    mapOptions: {
 | 
			
		||||
@@ -51,7 +58,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    options: !Array,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
  setup(props, context) {
 | 
			
		||||
    const filtered = ref(false);
 | 
			
		||||
    const filteredOptions = ref(props.options);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,9 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
			
		||||
  const defaultTimeout = ref(30)
 | 
			
		||||
  const defaultArgs = ref([])
 | 
			
		||||
  const script = ref(setScript)
 | 
			
		||||
  const syntax = ref("")
 | 
			
		||||
  const link = ref("")
 | 
			
		||||
  const baseUrl = "https://github.com/wh1te909/tacticalrmm/blob/master/scripts/"
 | 
			
		||||
 | 
			
		||||
  // specifing flat returns an array of script names versus {value:id, label: hostname}
 | 
			
		||||
  async function getScriptOptions(showCommunityScripts = false, flat = false) {
 | 
			
		||||
@@ -21,6 +24,8 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
			
		||||
      const tmpScript = scriptOptions.value.find(i => i.value === script.value);
 | 
			
		||||
      defaultTimeout.value = tmpScript.timeout;
 | 
			
		||||
      defaultArgs.value = tmpScript.args;
 | 
			
		||||
      syntax.value = tmpScript.syntax
 | 
			
		||||
      link.value = `${baseUrl}${tmpScript.filename}`
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -36,6 +41,8 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
			
		||||
    scriptOptions,
 | 
			
		||||
    defaultTimeout,
 | 
			
		||||
    defaultArgs,
 | 
			
		||||
    syntax,
 | 
			
		||||
    link,
 | 
			
		||||
 | 
			
		||||
    //methods
 | 
			
		||||
    getScriptOptions
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ export default function () {
 | 
			
		||||
        agentUrlAction: null,
 | 
			
		||||
        defaultAgentTblTab: "server",
 | 
			
		||||
        clientTreeSort: "alphafail",
 | 
			
		||||
        clientTreeSplitter: 11,
 | 
			
		||||
        clientTreeSplitter: 20,
 | 
			
		||||
        noCodeSign: false,
 | 
			
		||||
        hosted: false
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -44,9 +44,9 @@ export function formatScriptOptions(data, flat = false) {
 | 
			
		||||
      let tmp = [];
 | 
			
		||||
      data.forEach(script => {
 | 
			
		||||
        if (script.category === cat) {
 | 
			
		||||
          tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args });
 | 
			
		||||
          tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
 | 
			
		||||
        } else if (cat === "Unassigned" && !script.category) {
 | 
			
		||||
          tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args });
 | 
			
		||||
          tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax });
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
 | 
			
		||||
@@ -159,6 +159,17 @@ export function formatCustomFields(fields, values) {
 | 
			
		||||
  return tempArray
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatScriptSyntax(syntax) {
 | 
			
		||||
  let temp = syntax
 | 
			
		||||
  temp = temp.replaceAll("<", "<").replaceAll(">", ">")
 | 
			
		||||
  temp = temp.replaceAll("<", `<span style="color:#d4d4d4"><</span>`).replaceAll(">", `<span style="color:#d4d4d4">></span>`)
 | 
			
		||||
  temp = temp.replaceAll("[", `<span style="color:#ffd70a">[</span>`).replaceAll("]", `<span style="color:#ffd70a">]</span>`)
 | 
			
		||||
  temp = temp.replaceAll("(", `<span style="color:#87cefa">(</span>`).replaceAll(")", `<span style="color:#87cefa">)</span>`)
 | 
			
		||||
  temp = temp.replaceAll("{", `<span style="color:#c586b6">{</span>`).replaceAll("}", `<span style="color:#c586b6">}</span>`)
 | 
			
		||||
  temp = temp.replaceAll("\n", `<br />`)
 | 
			
		||||
  return temp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// date formatting
 | 
			
		||||
 | 
			
		||||
export function formatDate(dateString) {
 | 
			
		||||
 
 | 
			
		||||
@@ -114,11 +114,16 @@
 | 
			
		||||
                @update:selected="loadFrame(selectedTree)"
 | 
			
		||||
              >
 | 
			
		||||
                <template v-slot:default-header="props">
 | 
			
		||||
                  <div class="row">
 | 
			
		||||
                  <div class="row items-center">
 | 
			
		||||
                    <q-icon :name="props.node.icon" :color="props.node.color" class="q-mr-sm" />
 | 
			
		||||
                    <span
 | 
			
		||||
                      >{{ props.node.label }} <q-tooltip :delay="600">ID: {{ props.node.id }}</q-tooltip></span
 | 
			
		||||
                    >
 | 
			
		||||
                    <div>
 | 
			
		||||
                      {{ props.node.label }}
 | 
			
		||||
                      <q-tooltip :delay="600">
 | 
			
		||||
                        ID: {{ props.node.id }}<br />
 | 
			
		||||
                        Agent Count:
 | 
			
		||||
                        {{ props.node.children ? props.node.client.agent_count : props.node.site.agent_count }}
 | 
			
		||||
                      </q-tooltip>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <q-menu context-menu>
 | 
			
		||||
                      <q-list dense style="min-width: 200px">
 | 
			
		||||
@@ -942,7 +947,7 @@ export default {
 | 
			
		||||
        return this.$store.state.clientTreeSplitter;
 | 
			
		||||
      },
 | 
			
		||||
      set(newVal) {
 | 
			
		||||
        this.$store.commit("SET_CLIENT_SPLITTER", newVal);
 | 
			
		||||
        this.$store.dispatch("setClientTreeSplitter", newVal);
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    tab: {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user