Compare commits

...

72 Commits

Author SHA1 Message Date
wh1te909
2eefedadb3 Release 0.10.2 2021-11-21 02:24:29 +00:00
wh1te909
e63d7a0b8a bump version 2021-11-21 02:24:07 +00:00
wh1te909
2a1b1849fa fix nats-api not working in docker 2021-11-21 02:02:29 +00:00
wh1te909
0461cb7f19 update docs 2021-11-20 22:21:02 +00:00
Dan
0932e0be03 Merge pull request #811 from silversword411/develop
docs updates
2021-11-20 14:17:11 -08:00
silversword411
4638ac9474 docs - reiterating no root and backup 2021-11-20 12:59:42 -05:00
silversword411
d8d7255029 docs - filter tips 2021-11-20 12:50:10 -05:00
wh1te909
fa05276c3f black 2021-11-19 20:00:22 +00:00
silversword411
e50a5d51d8 docs - troubleshooting enhancements 2021-11-19 14:14:12 -05:00
sadnub
c03ba78587 make swagger views optional 2021-11-19 13:58:38 -05:00
wh1te909
ff07c69e7d Release 0.10.1 2021-11-19 17:41:12 +00:00
wh1te909
735b84b26d bump version 2021-11-19 17:39:14 +00:00
sadnub
8dd069ad67 push models.py file update for scripts 2021-11-19 12:13:20 -05:00
sadnub
1857e68003 change filename db field to not be required 2021-11-19 10:46:40 -05:00
wh1te909
ff2508382a Release 0.10.0 2021-11-19 08:37:39 +00:00
wh1te909
9cb952b116 bump version 2021-11-19 08:04:25 +00:00
wh1te909
105e8089bb trigger an agent update task after rmm update 2021-11-19 07:25:32 +00:00
wh1te909
730f37f247 add debian 11 support and update reqs 2021-11-19 06:58:18 +00:00
wh1te909
284716751f update docs for new service 2021-11-19 06:32:15 +00:00
sadnub
8d0db699bf remove dynamic agent options function 2021-11-18 21:11:53 -05:00
Dan
53cf1cae58 Merge pull request #807 from silversword411/develop
docs and script adds
2021-11-18 12:32:22 -08:00
silversword411
307e4719e0 wip script - user enable/disabling 2021-11-18 12:23:52 -05:00
silversword411
5effae787a Community scripts - Fixing Drive Volume check 2021-11-18 10:45:25 -05:00
silversword411
6532be0b52 docs - reverting content tabs 2021-11-18 10:05:01 -05:00
silversword411
fb225a5347 community scripts add - Win11 check 2021-11-18 05:24:22 -05:00
silversword411
b83830a45e docs moving position 2021-11-18 05:20:12 -05:00
wh1te909
ca28288c33 add missing onMounted 2021-11-18 08:42:26 +00:00
wh1te909
b6f8d9cb25 change drive color based on percent closes #802 2021-11-18 07:46:17 +00:00
Dan
9cad0f11e5 Merge pull request #803 from silversword411/develop
Scripts and docs
2021-11-17 11:24:29 -08:00
silversword411
807be08566 docs - adding how to invalidate all auth tokens 2021-11-17 10:43:52 -05:00
sadnub
67f6a985f8 increase font size on script editors and fix import error 2021-11-16 21:16:03 -05:00
sadnub
f87d54ae8d move imports for styles and select light or dark theme for editor depending on if dark mode is enabled 2021-11-16 20:45:42 -05:00
sadnub
d894bf7271 move to ace text editor. Fixes script line wrap issue and more features. Fixes #712 2021-11-16 20:19:46 -05:00
sadnub
56e0e5cace formatting 2021-11-15 21:17:28 -05:00
sadnub
685084e784 add agent counts to client/site tooltip. Closes #426 2021-11-15 21:16:18 -05:00
sadnub
cbeec5a973 swagger api documentation start 2021-11-15 17:50:59 -05:00
sadnub
3fff56bcd7 cleanup script manager and snippet modals and move agent select dropdown for test script to script form 2021-11-15 17:50:26 -05:00
silversword411
c504c23eec docs add mesh token recovery 2021-11-15 16:47:18 -05:00
silversword411
16dae5a655 docs Updating index and adding permissions and considerations for choosing install type 2021-11-15 15:42:02 -05:00
silversword411
e512c5ae7d Merge branch 'wh1te909:develop' into develop 2021-11-15 15:39:56 -05:00
silversword411
094078b928 scripts wip adding disk status 2021-11-15 15:26:07 -05:00
wh1te909
34fc3ff919 fix issue where emails/sms were not being sent if recipients in global settings were empty, even if they were present in an alert template recipients 2021-11-15 00:05:42 +00:00
wh1te909
4391f48e78 add some tests 2021-11-14 19:52:21 +00:00
wh1te909
775608a3c0 update reqs 2021-11-14 19:51:28 +00:00
Dan
b326228901 Merge pull request #800 from silversword411/develop
script library - fixing choco
2021-11-14 11:27:40 -08:00
silversword411
b2e98173a8 script library - fixing choco 2021-11-14 13:04:37 -05:00
wh1te909
65c9b7952c have task runs appear in history tab closes #716 2021-11-14 09:18:32 +00:00
wh1te909
b9dc9e7d62 speed up some views 2021-11-14 09:15:43 +00:00
Dan
ce178d0354 Merge pull request #799 from silversword411/develop
Community scripts: Adding syntax for tooltip
2021-11-14 00:54:15 -08:00
sadnub
a3ff6efebc remove nats-api from api dev image 2021-11-13 16:56:50 -05:00
wh1te909
6a9bc56723 update for new service 2021-11-13 21:30:01 +00:00
wh1te909
c9ac158d25 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-11-13 20:18:18 +00:00
silversword411
4b937a0fe8 Community scripts: Adding syntax for tooltip 2021-11-13 14:10:05 -05:00
sadnub
405bf26ac5 formatting 2021-11-13 13:40:26 -05:00
sadnub
5dcda0e0a0 allow q-select slots in tactical-dropdown. Fix info icon on run script dialog 2021-11-13 13:39:38 -05:00
sadnub
83e9b60308 when filtering agents add category to the side of options 2021-11-13 12:55:09 -05:00
sadnub
10b40b4730 script syntax highlighting. Resolves #702 2021-11-13 12:55:09 -05:00
wh1te909
79d6d804ef stringify errors before saving to db 2021-11-13 08:31:45 +00:00
wh1te909
e9c7b6d8f8 fix tests 2021-11-13 01:25:25 +00:00
wh1te909
4fcfbfb3f4 more go rework 2021-11-13 00:45:28 +00:00
wh1te909
30cde14ed3 update go mod 2021-11-13 00:36:57 +00:00
wh1te909
cf76e6f538 remove deprecated endpoint, add another deprecation 2021-11-13 00:33:52 +00:00
wh1te909
d0f600ec8d filter_software now handled by agent 2021-11-13 00:32:44 +00:00
wh1te909
675f9e956f remove some celery tasks now handled by agent/go 2021-11-13 00:32:03 +00:00
wh1te909
381605a6bb remove tests 2021-11-13 00:31:06 +00:00
wh1te909
0fce66062b remove some utils now handled by agent 2021-11-13 00:30:45 +00:00
wh1te909
747cc9e5da remove tasks 2021-11-13 00:27:34 +00:00
sadnub
25a1b464da Fix block inheritance on client/site 2021-11-10 22:45:25 -05:00
Dan
3b6738b547 Merge pull request #798 from silversword411/develop
Wip script additions
2021-11-10 11:12:28 -08:00
silversword411
fc93e3e97f Merge branch 'wh1te909:develop' into develop 2021-11-10 11:01:34 -05:00
silversword411
0edbb13d48 scripts wip revert windows update to default settings 2021-11-10 11:00:44 -05:00
silversword411
673687341c scripts wip adding 2021-11-10 09:03:17 -05:00
85 changed files with 2460 additions and 1896 deletions

View File

@@ -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 /

View File

@@ -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
View File

@@ -49,3 +49,5 @@ nats-rmm.conf
docs/site/
reset_db.sh
run_go_cmd.py
nats-api.conf

View 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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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 = {

View File

@@ -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"

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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"
},

View 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),
),
]

View File

@@ -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),
),
]

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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")

View File

@@ -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())

View File

@@ -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:

View File

@@ -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)

View File

@@ -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
]

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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
)"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -8,6 +8,11 @@ At the top right of your web administration interface, click your Username > pre
*****
## Use the filters in the agent list
![User Preferences](images/tipsntricks_filters.png)
*****
## 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.

View File

@@ -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
```

View File

@@ -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}

View File

@@ -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:

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
View File

@@ -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
View 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()
}

View File

@@ -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
View 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
View 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
}

View File

@@ -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}

View File

@@ -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

View File

@@ -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
View 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"
}

View 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

View File

@@ -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

View 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

View 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

View 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
}

View 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"

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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": [

View File

@@ -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

View File

@@ -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
}

View File

@@ -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,
};
},
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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);

View File

@@ -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

View File

@@ -21,7 +21,7 @@ export default function () {
agentUrlAction: null,
defaultAgentTblTab: "server",
clientTreeSort: "alphafail",
clientTreeSplitter: 11,
clientTreeSplitter: 20,
noCodeSign: false,
hosted: false
}

View File

@@ -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("<", "&lt;").replaceAll(">", "&gt;")
temp = temp.replaceAll("&lt;", `<span style="color:#d4d4d4">&lt;</span>`).replaceAll("&gt;", `<span style="color:#d4d4d4">&gt;</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) {

View File

@@ -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: {