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