Compare commits
	
		
			186 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d1c3fc8493 | ||
| 
						 | 
					f453b16010 | ||
| 
						 | 
					05151d8978 | ||
| 
						 | 
					8218e1acc3 | ||
| 
						 | 
					30212fc89a | ||
| 
						 | 
					b31c13fcae | ||
| 
						 | 
					6b95fc6f1d | ||
| 
						 | 
					369cf17eb2 | ||
| 
						 | 
					4dd8f512cc | ||
| 
						 | 
					26cfec7d80 | ||
| 
						 | 
					67a87ccf00 | ||
| 
						 | 
					667cebcf94 | ||
| 
						 | 
					bc1747ca1c | ||
| 
						 | 
					945d8647bf | ||
| 
						 | 
					dfe2e94627 | ||
| 
						 | 
					09a5591eec | ||
| 
						 | 
					f2bf06a0ba | ||
| 
						 | 
					eedad4ab1c | ||
| 
						 | 
					336a62ab29 | ||
| 
						 | 
					b5603a5233 | ||
| 
						 | 
					73890f553c | ||
| 
						 | 
					f6243b8968 | ||
| 
						 | 
					3770dc74d4 | ||
| 
						 | 
					45f4e947c5 | ||
| 
						 | 
					9928d7c6e1 | ||
| 
						 | 
					bf776eeb2b | ||
| 
						 | 
					ae7c0e9195 | ||
| 
						 | 
					e90b640602 | ||
| 
						 | 
					ba7529d3f5 | ||
| 
						 | 
					34667f252e | ||
| 
						 | 
					d18bddcb7b | ||
| 
						 | 
					96dff49d33 | ||
| 
						 | 
					b389728338 | ||
| 
						 | 
					cdc7da86f3 | ||
| 
						 | 
					4745cc0378 | ||
| 
						 | 
					434f132479 | ||
| 
						 | 
					fb0f31ffc7 | ||
| 
						 | 
					bb1d73c0ae | ||
| 
						 | 
					0e823d1191 | ||
| 
						 | 
					48f4199ff3 | ||
| 
						 | 
					eaf379587b | ||
| 
						 | 
					672446b7d1 | ||
| 
						 | 
					dfe52c1b07 | ||
| 
						 | 
					d63df03ad8 | ||
| 
						 | 
					aba4f9f2ce | ||
| 
						 | 
					ac5c1e7803 | ||
| 
						 | 
					d521dbf50e | ||
| 
						 | 
					f210ed3e6a | ||
| 
						 | 
					df3cac4ea6 | ||
| 
						 | 
					f778c5175b | ||
| 
						 | 
					6c66ff28dd | ||
| 
						 | 
					d5b6ec702b | ||
| 
						 | 
					c62a5fcef2 | ||
| 
						 | 
					59c47e9200 | ||
| 
						 | 
					4ba44d8932 | ||
| 
						 | 
					27dae05e1b | ||
| 
						 | 
					a251ae9b90 | ||
| 
						 | 
					7e960b2bde | ||
| 
						 | 
					5df4825158 | ||
| 
						 | 
					8984d06d93 | ||
| 
						 | 
					eed7aac047 | ||
| 
						 | 
					54b068de4a | ||
| 
						 | 
					f0f33b00b6 | ||
| 
						 | 
					1043405088 | ||
| 
						 | 
					0131b10805 | ||
| 
						 | 
					a19b441f62 | ||
| 
						 | 
					28edc31d43 | ||
| 
						 | 
					0f9872a818 | ||
| 
						 | 
					76ce4296f3 | ||
| 
						 | 
					3dd2671380 | ||
| 
						 | 
					298ca31332 | ||
| 
						 | 
					8f911aa6b9 | ||
| 
						 | 
					82a5c7d9b1 | ||
| 
						 | 
					7f013dcdba | ||
| 
						 | 
					68e2e16076 | ||
| 
						 | 
					ea23c763c9 | ||
| 
						 | 
					5dcecb3206 | ||
| 
						 | 
					5bd48e2d0e | ||
| 
						 | 
					afd0a02589 | ||
| 
						 | 
					2379192d53 | ||
| 
						 | 
					a6489290c8 | ||
| 
						 | 
					5f74c43415 | ||
| 
						 | 
					aa8b84a302 | ||
| 
						 | 
					b987d041b0 | ||
| 
						 | 
					b62e37307e | ||
| 
						 | 
					61a59aa6ac | ||
| 
						 | 
					f79ec27f1d | ||
| 
						 | 
					b993fe380f | ||
| 
						 | 
					d974b5f55f | ||
| 
						 | 
					f21ae93197 | ||
| 
						 | 
					342ff18be8 | ||
| 
						 | 
					a8236f69bf | ||
| 
						 | 
					ab15a2448d | ||
| 
						 | 
					6ff4d8f558 | ||
| 
						 | 
					bb04ba528c | ||
| 
						 | 
					b94a795189 | ||
| 
						 | 
					9968184733 | ||
| 
						 | 
					1be6f8f87a | ||
| 
						 | 
					426821cceb | ||
| 
						 | 
					4fec0deaf7 | ||
| 
						 | 
					144ac5b6ce | ||
| 
						 | 
					97c73786fa | ||
| 
						 | 
					82e59d7da0 | ||
| 
						 | 
					b2c10de6af | ||
| 
						 | 
					d72029c2c6 | ||
| 
						 | 
					17b9987063 | ||
| 
						 | 
					fde07da2b7 | ||
| 
						 | 
					c23bc29511 | ||
| 
						 | 
					714cad2a52 | ||
| 
						 | 
					357d5d2fde | ||
| 
						 | 
					d477cce901 | ||
| 
						 | 
					eb6af52ad1 | ||
| 
						 | 
					aae75023a7 | ||
| 
						 | 
					41dcd4f458 | ||
| 
						 | 
					4651ae4495 | ||
| 
						 | 
					ed61e0b0fc | ||
| 
						 | 
					1eefc6fbf4 | ||
| 
						 | 
					09ebf2cea2 | ||
| 
						 | 
					b3b0c4cd65 | ||
| 
						 | 
					f4b7924e8f | ||
| 
						 | 
					ea68d38b82 | ||
| 
						 | 
					dfbaa71132 | ||
| 
						 | 
					6c328deb08 | ||
| 
						 | 
					add564d5bf | ||
| 
						 | 
					fa94acb426 | ||
| 
						 | 
					6827468f13 | ||
| 
						 | 
					53fd43868f | ||
| 
						 | 
					9ced7561c5 | ||
| 
						 | 
					31d55d3425 | ||
| 
						 | 
					171d2a5bb9 | ||
| 
						 | 
					c5d05c1205 | ||
| 
						 | 
					2973e0559a | ||
| 
						 | 
					ec27288dcf | ||
| 
						 | 
					f92e5c7093 | ||
| 
						 | 
					7c67155c49 | ||
| 
						 | 
					b102cd4652 | ||
| 
						 | 
					67f9a48c37 | ||
| 
						 | 
					a0c8a1ee65 | ||
| 
						 | 
					7e7d272b06 | ||
| 
						 | 
					3c642240ae | ||
| 
						 | 
					b5157fcaf1 | ||
| 
						 | 
					d1cb42f1bc | ||
| 
						 | 
					84cde1a16a | ||
| 
						 | 
					877f5db1ce | ||
| 
						 | 
					787164e245 | ||
| 
						 | 
					d77fc5e7c5 | ||
| 
						 | 
					cca39a67d6 | ||
| 
						 | 
					a6c9a0431a | ||
| 
						 | 
					729a80a639 | ||
| 
						 | 
					31cb3001f6 | ||
| 
						 | 
					5d0f54a329 | ||
| 
						 | 
					c8c3f5b5b7 | ||
| 
						 | 
					ba473ed75a | ||
| 
						 | 
					7236fd59f8 | ||
| 
						 | 
					9471e8f1fd | ||
| 
						 | 
					a2d39b51bb | ||
| 
						 | 
					2920934b55 | ||
| 
						 | 
					3f709d448e | ||
| 
						 | 
					b79f66183f | ||
| 
						 | 
					8672f57e55 | ||
| 
						 | 
					1e99c82351 | ||
| 
						 | 
					1a2ff851f3 | ||
| 
						 | 
					f1c27c3959 | ||
| 
						 | 
					b30dac0f15 | ||
| 
						 | 
					cc79e5cdaf | ||
| 
						 | 
					d9a3b2f2cb | ||
| 
						 | 
					479b528d09 | ||
| 
						 | 
					461fb84fb9 | ||
| 
						 | 
					bd7685e3fa | ||
| 
						 | 
					cd98cb64b3 | ||
| 
						 | 
					0f32a3ec24 | ||
| 
						 | 
					ca446cac87 | ||
| 
						 | 
					6ea907ffda | ||
| 
						 | 
					5287baa70d | ||
| 
						 | 
					25935fec84 | ||
| 
						 | 
					e855a063ff | ||
| 
						 | 
					c726b8c9f0 | ||
| 
						 | 
					13cb99290e | ||
| 
						 | 
					cea9413fd1 | ||
| 
						 | 
					1432853b39 | ||
| 
						 | 
					6d6c2b86e8 | ||
| 
						 | 
					77b1d964b5 | ||
| 
						 | 
					549936fc09 | ||
| 
						 | 
					c9c32f09c5 | ||
| 
						 | 
					77f7778d4a | ||
| 
						 | 
					84b6be9364 | 
@@ -8,7 +8,7 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
 | 
			
		||||
ENV PYTHONDONTWRITEBYTECODE=1
 | 
			
		||||
ENV PYTHONUNBUFFERED=1
 | 
			
		||||
 | 
			
		||||
EXPOSE 8000
 | 
			
		||||
EXPOSE 8000 8383
 | 
			
		||||
 | 
			
		||||
RUN groupadd -g 1000 tactical && \
 | 
			
		||||
    useradd -u 1000 -g 1000 tactical
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ version: '3.4'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  api-dev:
 | 
			
		||||
    container_name: trmm-api-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    build:
 | 
			
		||||
@@ -21,6 +22,7 @@ services:
 | 
			
		||||
          - tactical-backend
 | 
			
		||||
 | 
			
		||||
  app-dev:
 | 
			
		||||
    container_name: trmm-app-dev
 | 
			
		||||
    image: node:14-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
 | 
			
		||||
@@ -36,6 +38,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # nats
 | 
			
		||||
  nats-dev:
 | 
			
		||||
    container_name: trmm-nats-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nats:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -55,6 +58,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # meshcentral container
 | 
			
		||||
  meshcentral-dev:
 | 
			
		||||
    container_name: trmm-meshcentral-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment: 
 | 
			
		||||
@@ -77,6 +81,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # mongodb container for meshcentral
 | 
			
		||||
  mongodb-dev:
 | 
			
		||||
    container_name: trmm-mongodb-dev
 | 
			
		||||
    image: mongo:4.4
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -92,6 +97,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # postgres database for api service
 | 
			
		||||
  postgres-dev:
 | 
			
		||||
    container_name: trmm-postgres-dev
 | 
			
		||||
    image: postgres:13-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -107,6 +113,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # redis container for celery tasks
 | 
			
		||||
  redis-dev:
 | 
			
		||||
    container_name: trmm-redis-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    image: redis:6.0-alpine
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -115,6 +122,7 @@ services:
 | 
			
		||||
          - tactical-redis
 | 
			
		||||
 | 
			
		||||
  init-dev:
 | 
			
		||||
    container_name: trmm-init-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
@@ -143,6 +151,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery worker service
 | 
			
		||||
  celery-dev:
 | 
			
		||||
    container_name: trmm-celery-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
@@ -160,6 +169,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery beat service
 | 
			
		||||
  celerybeat-dev:
 | 
			
		||||
    container_name: trmm-celerybeat-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
@@ -175,8 +185,29 @@ services:
 | 
			
		||||
      - postgres-dev
 | 
			
		||||
      - redis-dev
 | 
			
		||||
 | 
			
		||||
  nginx-dev:
 | 
			
		||||
  # container for websockets communication
 | 
			
		||||
  websockets-dev:
 | 
			
		||||
    container_name: trmm-websockets-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-websockets-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - tactical-websockets
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - postgres-dev
 | 
			
		||||
      - redis-dev
 | 
			
		||||
 | 
			
		||||
  # container for tactical reverse proxy
 | 
			
		||||
  nginx-dev:
 | 
			
		||||
    container_name: trmm-nginx-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nginx:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
 
 | 
			
		||||
@@ -136,10 +136,11 @@ if [ "$1" = 'tactical-init-dev' ]; then
 | 
			
		||||
  webenv="$(cat << EOF
 | 
			
		||||
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
			
		||||
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
			
		||||
APP_URL = https://${APP_HOST}
 | 
			
		||||
APP_URL = "https://${APP_HOST}"
 | 
			
		||||
DOCKER_BUILD = 1
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
  echo "${webenv}" | tee ${WORKSPACE_DIR}/web/.env > /dev/null
 | 
			
		||||
  echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
 | 
			
		||||
 | 
			
		||||
  # chown everything to tactical user
 | 
			
		||||
  chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
 | 
			
		||||
@@ -164,3 +165,8 @@ if [ "$1" = 'tactical-celerybeat-dev' ]; then
 | 
			
		||||
  test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ "$1" = 'tactical-websockets-dev' ]; then
 | 
			
		||||
  check_tactical_ready
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
 | 
			
		||||
fi
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
 | 
			
		||||
asyncio-nats-client
 | 
			
		||||
celery
 | 
			
		||||
channels
 | 
			
		||||
Django
 | 
			
		||||
django-cors-headers
 | 
			
		||||
django-rest-knox
 | 
			
		||||
@@ -30,3 +31,5 @@ mkdocs-material
 | 
			
		||||
pymdown-extensions
 | 
			
		||||
Pygments
 | 
			
		||||
mypy
 | 
			
		||||
pysnooper
 | 
			
		||||
isort
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							@@ -2,7 +2,7 @@ name: Deploy Docs
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - develop
 | 
			
		||||
      - master
 | 
			
		||||
 | 
			
		||||
defaults:
 | 
			
		||||
  run:
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2 on 2021-04-11 01:43
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0013_user_client_tree_sort'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='client_tree_splitter',
 | 
			
		||||
            field=models.PositiveIntegerField(default=11),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2 on 2021-04-11 03:03
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0014_user_client_tree_splitter'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='loading_bar_color',
 | 
			
		||||
            field=models.CharField(default='red', max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -36,6 +36,8 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    client_tree_sort = models.CharField(
 | 
			
		||||
        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
			
		||||
    )
 | 
			
		||||
    client_tree_splitter = models.PositiveIntegerField(default=11)
 | 
			
		||||
    loading_bar_color = models.CharField(max_length=255, default="red")
 | 
			
		||||
 | 
			
		||||
    agent = models.OneToOneField(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ class UserUISerializer(ModelSerializer):
 | 
			
		||||
            "agent_dblclick_action",
 | 
			
		||||
            "default_agent_tbl_tab",
 | 
			
		||||
            "client_tree_sort",
 | 
			
		||||
            "client_tree_splitter",
 | 
			
		||||
            "loading_bar_color",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -278,6 +278,8 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
            "agent_dblclick_action": "editagent",
 | 
			
		||||
            "default_agent_tbl_tab": "mixed",
 | 
			
		||||
            "client_tree_sort": "alpha",
 | 
			
		||||
            "client_tree_splitter": 14,
 | 
			
		||||
            "loading_bar_color": "green",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
from typing import Any
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
 | 
			
		||||
import msgpack
 | 
			
		||||
import validators
 | 
			
		||||
@@ -14,6 +13,7 @@ from Crypto.Hash import SHA3_384
 | 
			
		||||
from Crypto.Random import get_random_bytes
 | 
			
		||||
from Crypto.Util.Padding import pad
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
@@ -166,7 +166,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def checks(self):
 | 
			
		||||
        total, passing, failing = 0, 0, 0
 | 
			
		||||
        total, passing, failing, warning, info = 0, 0, 0, 0, 0
 | 
			
		||||
 | 
			
		||||
        if self.agentchecks.exists():  # type: ignore
 | 
			
		||||
            for i in self.agentchecks.all():  # type: ignore
 | 
			
		||||
@@ -174,13 +174,20 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                if i.status == "passing":
 | 
			
		||||
                    passing += 1
 | 
			
		||||
                elif i.status == "failing":
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                    if i.alert_severity == "error":
 | 
			
		||||
                        failing += 1
 | 
			
		||||
                    elif i.alert_severity == "warning":
 | 
			
		||||
                        warning += 1
 | 
			
		||||
                    elif i.alert_severity == "info":
 | 
			
		||||
                        info += 1
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
            "total": total,
 | 
			
		||||
            "passing": passing,
 | 
			
		||||
            "failing": failing,
 | 
			
		||||
            "has_failing_checks": failing > 0,
 | 
			
		||||
            "warning": warning,
 | 
			
		||||
            "info": info,
 | 
			
		||||
            "has_failing_checks": failing > 0 or warning > 0,
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
@@ -195,6 +202,27 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except:
 | 
			
		||||
            return ["unknown cpu model"]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def graphics(self):
 | 
			
		||||
        ret, mrda = [], []
 | 
			
		||||
        try:
 | 
			
		||||
            graphics = self.wmi_detail["graphics"]
 | 
			
		||||
            for i in graphics:
 | 
			
		||||
                caption = [x["Caption"] for x in i if "Caption" in x][0]
 | 
			
		||||
                if "microsoft remote display adapter" in caption.lower():
 | 
			
		||||
                    mrda.append("yes")
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                ret.append([x["Caption"] for x in i if "Caption" in x][0])
 | 
			
		||||
 | 
			
		||||
            # only return this if no other graphics cards
 | 
			
		||||
            if not ret and mrda:
 | 
			
		||||
                return "Microsoft Remote Display Adapter"
 | 
			
		||||
 | 
			
		||||
            return ", ".join(ret)
 | 
			
		||||
        except:
 | 
			
		||||
            return "Graphics info requires agent v1.4.14"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def local_ips(self):
 | 
			
		||||
        ret = []
 | 
			
		||||
@@ -296,10 +324,13 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
        script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
 | 
			
		||||
        parsed_args = script.parse_script_args(self, script.shell, args)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "runscriptfull" if full else "runscript",
 | 
			
		||||
            "timeout": timeout,
 | 
			
		||||
            "script_args": args,
 | 
			
		||||
            "script_args": parsed_args,
 | 
			
		||||
            "payload": {
 | 
			
		||||
                "code": script.code,
 | 
			
		||||
                "shell": script.shell,
 | 
			
		||||
@@ -319,7 +350,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                online = [
 | 
			
		||||
                    agent
 | 
			
		||||
                    for agent in Agent.objects.only(
 | 
			
		||||
                        "pk", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
                        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
                    )
 | 
			
		||||
                    if agent.status == "online"
 | 
			
		||||
                ]
 | 
			
		||||
@@ -648,7 +679,11 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            except ErrTimeout:
 | 
			
		||||
                ret = "timeout"
 | 
			
		||||
            else:
 | 
			
		||||
                ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                try:
 | 
			
		||||
                    ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(e)
 | 
			
		||||
                    ret = str(e)
 | 
			
		||||
 | 
			
		||||
            await nc.close()
 | 
			
		||||
            return ret
 | 
			
		||||
@@ -812,12 +847,6 @@ class RecoveryAction(models.Model):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.agent.hostname} - {self.mode}"
 | 
			
		||||
 | 
			
		||||
    def send(self):
 | 
			
		||||
        ret = {"recovery": self.mode}
 | 
			
		||||
        if self.mode == "command":
 | 
			
		||||
            ret["cmd"] = self.command
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Note(models.Model):
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ class AgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    local_ips = serializers.ReadOnlyField()
 | 
			
		||||
    make_model = serializers.ReadOnlyField()
 | 
			
		||||
    physical_disks = serializers.ReadOnlyField()
 | 
			
		||||
    graphics = serializers.ReadOnlyField()
 | 
			
		||||
    checks = serializers.ReadOnlyField()
 | 
			
		||||
    timezone = serializers.ReadOnlyField()
 | 
			
		||||
    all_timezones = serializers.SerializerMethodField()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import json
 | 
			
		||||
import random
 | 
			
		||||
import subprocess
 | 
			
		||||
import tempfile
 | 
			
		||||
import urllib.parse
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
@@ -13,21 +11,21 @@ from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from core.models import CoreSettings, CodeSignToken
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.utils import run_nats_api_cmd
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def agent_update(pk: int) -> str:
 | 
			
		||||
def agent_update(pk: int, codesigntoken: str = None) -> str:
 | 
			
		||||
    from agents.utils import get_exegen_url
 | 
			
		||||
 | 
			
		||||
    agent = Agent.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
 | 
			
		||||
        logger.warning(
 | 
			
		||||
            f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
 | 
			
		||||
        )
 | 
			
		||||
    if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
 | 
			
		||||
        return "not supported"
 | 
			
		||||
 | 
			
		||||
    # skip if we can't determine the arch
 | 
			
		||||
@@ -37,18 +35,15 @@ def agent_update(pk: int) -> str:
 | 
			
		||||
        )
 | 
			
		||||
        return "noarch"
 | 
			
		||||
 | 
			
		||||
    # removed sqlite in 1.4.0 to get rid of cgo dependency
 | 
			
		||||
    # 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.3.0"):
 | 
			
		||||
        version = settings.LATEST_AGENT_VER
 | 
			
		||||
        url = agent.winagent_dl
 | 
			
		||||
        inno = agent.win_inno_exe
 | 
			
		||||
    version = settings.LATEST_AGENT_VER
 | 
			
		||||
    inno = agent.win_inno_exe
 | 
			
		||||
 | 
			
		||||
    if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
 | 
			
		||||
        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
			
		||||
        params = {"version": version, "arch": agent.arch, "token": codesigntoken}
 | 
			
		||||
        url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
    else:
 | 
			
		||||
        version = "1.3.0"
 | 
			
		||||
        inno = (
 | 
			
		||||
            "winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe"
 | 
			
		||||
        )
 | 
			
		||||
        url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
 | 
			
		||||
        url = agent.winagent_dl
 | 
			
		||||
 | 
			
		||||
    if agent.pendingactions.filter(
 | 
			
		||||
        action_type="agentupdate", status="pending"
 | 
			
		||||
@@ -81,10 +76,15 @@ def agent_update(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def send_agent_update_task(pks: list[int]) -> None:
 | 
			
		||||
    try:
 | 
			
		||||
        codesigntoken = CodeSignToken.objects.first().token
 | 
			
		||||
    except:
 | 
			
		||||
        codesigntoken = None
 | 
			
		||||
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk)
 | 
			
		||||
            agent_update(pk, codesigntoken)
 | 
			
		||||
            sleep(0.05)
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
@@ -95,6 +95,11 @@ def auto_self_agent_update_task() -> None:
 | 
			
		||||
    if not core.agent_auto_update:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        codesigntoken = CodeSignToken.objects.first().token
 | 
			
		||||
    except:
 | 
			
		||||
        codesigntoken = None
 | 
			
		||||
 | 
			
		||||
    q = Agent.objects.only("pk", "version")
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
@@ -105,7 +110,7 @@ def auto_self_agent_update_task() -> None:
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk)
 | 
			
		||||
            agent_update(pk, codesigntoken)
 | 
			
		||||
            sleep(0.05)
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
@@ -257,30 +262,13 @@ def run_script_email_results_task(
 | 
			
		||||
        logger.error(e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_nats_config() -> dict:
 | 
			
		||||
    return {
 | 
			
		||||
        "key": settings.SECRET_KEY,
 | 
			
		||||
        "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def monitor_agents_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ret = [i.agent_id for i in agents if i.status != "online"]
 | 
			
		||||
    config = _get_nats_config()
 | 
			
		||||
    config["agents"] = ret
 | 
			
		||||
    with tempfile.NamedTemporaryFile() as fp:
 | 
			
		||||
        with open(fp.name, "w") as f:
 | 
			
		||||
            json.dump(config, f)
 | 
			
		||||
 | 
			
		||||
        cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "monitor"]
 | 
			
		||||
        try:
 | 
			
		||||
            subprocess.run(cmd, capture_output=True, timeout=30)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(e)
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status != "online"]
 | 
			
		||||
    run_nats_api_cmd("monitor", ids)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -288,15 +276,5 @@ def get_wmi_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ret = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
    config = _get_nats_config()
 | 
			
		||||
    config["agents"] = ret
 | 
			
		||||
    with tempfile.NamedTemporaryFile() as fp:
 | 
			
		||||
        with open(fp.name, "w") as f:
 | 
			
		||||
            json.dump(config, f)
 | 
			
		||||
 | 
			
		||||
        cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "wmi"]
 | 
			
		||||
        try:
 | 
			
		||||
            subprocess.run(cmd, capture_output=True, timeout=30)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(e)
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
    run_nats_api_cmd("wmi", ids)
 | 
			
		||||
 
 | 
			
		||||
@@ -364,7 +364,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("os.path.exists")
 | 
			
		||||
    def test_install_agent(self, mock_file_exists):
 | 
			
		||||
        url = f"/agents/installagent/"
 | 
			
		||||
        url = "/agents/installagent/"
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -372,12 +372,13 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "arch": "64",
 | 
			
		||||
            "expires": 23,
 | 
			
		||||
            "installMethod": "exe",
 | 
			
		||||
            "installMethod": "manual",
 | 
			
		||||
            "api": "https://api.example.com",
 | 
			
		||||
            "agenttype": "server",
 | 
			
		||||
            "rdp": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
            "power": 0,
 | 
			
		||||
            "fileName": "rmm-client-site-server.exe",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = False
 | 
			
		||||
@@ -393,7 +394,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 415)
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "manual"
 | 
			
		||||
        data["arch"] = "64"
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -405,6 +405,9 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertIn("power", r.json()["cmd"])
 | 
			
		||||
        self.assertIn("ping", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "powershell"
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
@@ -840,7 +843,7 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_agent_counts(self):
 | 
			
		||||
    """ def test_agent_counts(self):
 | 
			
		||||
        url = "/agents/agent_counts/"
 | 
			
		||||
 | 
			
		||||
        # create some data
 | 
			
		||||
@@ -867,7 +870,7 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
        self.check_not_authenticated("post", url) """
 | 
			
		||||
 | 
			
		||||
    def test_agent_maintenance_mode(self):
 | 
			
		||||
        url = "/agents/maintenance/"
 | 
			
		||||
@@ -911,8 +914,9 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.utils.get_exegen_url")
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_agent_update(self, nats_cmd):
 | 
			
		||||
    def test_agent_update(self, nats_cmd, get_exe):
 | 
			
		||||
        from agents.tasks import agent_update
 | 
			
		||||
 | 
			
		||||
        agent_noarch = baker.make_recipe(
 | 
			
		||||
@@ -923,63 +927,96 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        r = agent_update(agent_noarch.pk)
 | 
			
		||||
        self.assertEqual(r, "noarch")
 | 
			
		||||
 | 
			
		||||
        agent_1111 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.1.11",
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_update(agent_1111.pk)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        agent64_1112 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.1.12",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_1112.pk)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["url"],
 | 
			
		||||
            "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
 | 
			
		||||
        self.assertEqual(action.details["version"], "1.3.0")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
 | 
			
		||||
                    "version": "1.3.0",
 | 
			
		||||
                    "inno": "winagent-v1.3.0.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agent_64_130 = baker.make_recipe(
 | 
			
		||||
        agent_130 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = agent_update(agent_64_130.pk)
 | 
			
		||||
        r = agent_update(agent_130.pk)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        # test __without__ code signing
 | 
			
		||||
        agent64_nosign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_nosign.pk, None)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_nosign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["url"],
 | 
			
		||||
            f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": settings.DL_64,
 | 
			
		||||
                    "url": f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (64 bit)
 | 
			
		||||
        codesign = baker.make("core.CodeSignToken", token="testtoken123")
 | 
			
		||||
        agent64_sign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        get_exe.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
        r = agent_update(agent64_sign.pk, codesign.token)  # type: ignore
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123",  # type: ignore
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (32 bit)
 | 
			
		||||
        agent32_sign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 32 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        get_exe.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
        r = agent_update(agent32_sign.pk, codesign.token)  # type: ignore
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123",  # type: ignore
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,6 @@ urlpatterns = [
 | 
			
		||||
    path("<int:pk>/notes/", views.GetAddNotes.as_view()),
 | 
			
		||||
    path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
 | 
			
		||||
    path("bulk/", views.bulk),
 | 
			
		||||
    path("agent_counts/", views.agent_counts),
 | 
			
		||||
    path("maintenance/", views.agent_maintenance),
 | 
			
		||||
    path("<int:pk>/wmi/", views.WMI.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import random
 | 
			
		||||
import requests
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_exegen_url() -> str:
 | 
			
		||||
    urls: list[str] = settings.EXE_GEN_URLS
 | 
			
		||||
    for url in urls:
 | 
			
		||||
        try:
 | 
			
		||||
            r = requests.get(url, timeout=10)
 | 
			
		||||
        except:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        if r.status_code == 200:
 | 
			
		||||
            return url
 | 
			
		||||
 | 
			
		||||
    return random.choice(urls)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_winagent_url(arch: str) -> str:
 | 
			
		||||
    from core.models import CodeSignToken
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        codetoken = CodeSignToken.objects.first().token
 | 
			
		||||
        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
			
		||||
        params = {
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
            "arch": arch,
 | 
			
		||||
            "token": codetoken,
 | 
			
		||||
        }
 | 
			
		||||
        dl_url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
    except:
 | 
			
		||||
        dl_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
			
		||||
 | 
			
		||||
    return dl_url
 | 
			
		||||
@@ -18,11 +18,7 @@ from core.models import CoreSettings
 | 
			
		||||
from logs.models import AuditLog, 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
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +40,7 @@ logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_agent_versions(request):
 | 
			
		||||
    agents = Agent.objects.only("pk")
 | 
			
		||||
    agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "versions": [settings.LATEST_AGENT_VER],
 | 
			
		||||
@@ -87,7 +83,7 @@ def uninstall(request):
 | 
			
		||||
    return Response(f"{name} will now be uninstalled.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["PATCH"])
 | 
			
		||||
@api_view(["PATCH", "PUT"])
 | 
			
		||||
def edit_agent(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["id"])
 | 
			
		||||
 | 
			
		||||
@@ -357,6 +353,7 @@ class Reboot(APIView):
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def install_agent(request):
 | 
			
		||||
    from agents.utils import get_winagent_url
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
 | 
			
		||||
    client_id = request.data["client"]
 | 
			
		||||
@@ -379,22 +376,27 @@ def install_agent(request):
 | 
			
		||||
    inno = (
 | 
			
		||||
        f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
 | 
			
		||||
    )
 | 
			
		||||
    download_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
			
		||||
    goarch = "amd64" if arch == "64" else "386"
 | 
			
		||||
    download_url = get_winagent_url(arch)
 | 
			
		||||
 | 
			
		||||
    _, token = AuthToken.objects.create(
 | 
			
		||||
        user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if request.data["installMethod"] == "exe":
 | 
			
		||||
        ret = {
 | 
			
		||||
            "token": token,
 | 
			
		||||
            "url": download_url,
 | 
			
		||||
            "inno": inno,
 | 
			
		||||
            "goarch": goarch,
 | 
			
		||||
            "genurl": settings.EXE_GEN_URL,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
        return generate_winagent_exe(
 | 
			
		||||
            client=client_id,
 | 
			
		||||
            site=site_id,
 | 
			
		||||
            agent_type=request.data["agenttype"],
 | 
			
		||||
            rdp=request.data["rdp"],
 | 
			
		||||
            ping=request.data["ping"],
 | 
			
		||||
            power=request.data["power"],
 | 
			
		||||
            arch=arch,
 | 
			
		||||
            token=token,
 | 
			
		||||
            api=request.data["api"],
 | 
			
		||||
            file_name=request.data["fileName"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "manual":
 | 
			
		||||
        cmd = [
 | 
			
		||||
@@ -670,49 +672,6 @@ def bulk(request):
 | 
			
		||||
    return notify_error("Something went wrong")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def agent_counts(request):
 | 
			
		||||
 | 
			
		||||
    server_offline_count = len(
 | 
			
		||||
        [
 | 
			
		||||
            agent
 | 
			
		||||
            for agent in Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            if not agent.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    workstation_offline_count = len(
 | 
			
		||||
        [
 | 
			
		||||
            agent
 | 
			
		||||
            for agent in Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            if not agent.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "total_server_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="server"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_server_offline_count": server_offline_count,
 | 
			
		||||
            "total_workstation_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="workstation"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_workstation_offline_count": workstation_offline_count,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def agent_maintenance(request):
 | 
			
		||||
    if request.data["type"] == "Client":
 | 
			
		||||
 
 | 
			
		||||
@@ -633,12 +633,15 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            percent_used = [
 | 
			
		||||
                d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
            ][0]
 | 
			
		||||
            percent_free = 100 - percent_used
 | 
			
		||||
            try:
 | 
			
		||||
                percent_used = [
 | 
			
		||||
                    d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
                ][0]
 | 
			
		||||
                percent_free = 100 - percent_used
 | 
			
		||||
 | 
			
		||||
            body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
                body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
 | 
			
		||||
@@ -667,16 +670,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                status = list(
 | 
			
		||||
                    filter(lambda x: x["name"] == self.svc_name, self.agent.services)
 | 
			
		||||
                )[0]["status"]
 | 
			
		||||
            # catch services that don't exist if policy check
 | 
			
		||||
            except:
 | 
			
		||||
                status = "Unknown"
 | 
			
		||||
 | 
			
		||||
            body = subject + f" - Status: {status.upper()}"
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
 | 
			
		||||
@@ -719,11 +713,15 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            percent_used = [
 | 
			
		||||
                d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
            ][0]
 | 
			
		||||
            percent_free = 100 - percent_used
 | 
			
		||||
            body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            try:
 | 
			
		||||
                percent_used = [
 | 
			
		||||
                    d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
                ][0]
 | 
			
		||||
                percent_free = 100 - percent_used
 | 
			
		||||
                body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
            body = subject + f" - Return code: {self.retcode}"
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
@@ -741,10 +739,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            elif self.check_type == "memory":
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            status = list(
 | 
			
		||||
                filter(lambda x: x["name"] == self.svc_name, self.agent.services)
 | 
			
		||||
            )[0]["status"]
 | 
			
		||||
            body = subject + f" - Status: {status.upper()}"
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
            body = subject
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,10 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        # remove fields that should not be changed when editing a check from the frontend
 | 
			
		||||
        if "check_alert" not in request.data.keys():
 | 
			
		||||
        if (
 | 
			
		||||
            "check_alert" not in request.data.keys()
 | 
			
		||||
            and "check_reset" not in request.data.keys()
 | 
			
		||||
        ):
 | 
			
		||||
            [request.data.pop(i) for i in check.non_editable_fields]
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
@@ -108,6 +111,11 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
        if check.policy:
 | 
			
		||||
            update_policy_check_fields_task(checkpk=pk)
 | 
			
		||||
 | 
			
		||||
        # resolve any alerts that are open
 | 
			
		||||
        if "check_reset" in request.data.keys():
 | 
			
		||||
            if obj.alert.filter(resolved=False).exists():
 | 
			
		||||
                obj.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{obj.readable_desc} was edited!")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
@@ -86,16 +86,24 @@ class Client(BaseAuditModel):
 | 
			
		||||
            .prefetch_related("agentchecks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        failing = 0
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
                failing += 1
 | 
			
		||||
 | 
			
		||||
                if agent.checks["warning"]:
 | 
			
		||||
                    data["warning"] = True
 | 
			
		||||
 | 
			
		||||
                if agent.checks["failing"]:
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
        return failing > 0
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(client):
 | 
			
		||||
@@ -184,16 +192,24 @@ class Site(BaseAuditModel):
 | 
			
		||||
            .prefetch_related("agentchecks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        failing = 0
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
                failing += 1
 | 
			
		||||
                if agent.checks["warning"]:
 | 
			
		||||
                    data["warning"] = True
 | 
			
		||||
 | 
			
		||||
                if agent.checks["failing"]:
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
        return failing > 0
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(site):
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ class SiteSerializer(ModelSerializer):
 | 
			
		||||
            "name",
 | 
			
		||||
            "server_policy",
 | 
			
		||||
            "workstation_policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "client_name",
 | 
			
		||||
            "client",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
@@ -75,6 +76,7 @@ class ClientSerializer(ModelSerializer):
 | 
			
		||||
            "name",
 | 
			
		||||
            "server_policy",
 | 
			
		||||
            "workstation_policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "sites",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
        )
 | 
			
		||||
@@ -93,7 +95,6 @@ class SiteTreeSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        ordering = ("failing_checks",)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientTreeSerializer(ModelSerializer):
 | 
			
		||||
@@ -104,7 +105,6 @@ class ClientTreeSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        ordering = ("failing_checks",)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeploymentSerializer(ModelSerializer):
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
@@ -24,6 +25,8 @@ from .serializers import (
 | 
			
		||||
    SiteSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddClients(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
@@ -167,13 +170,15 @@ class GetUpdateSite(APIView):
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
        if "client" in request.data["site"].keys() and (
 | 
			
		||||
            site.client.id != request.data["site"]["client"]
 | 
			
		||||
            and site.client.sites.count() == 1
 | 
			
		||||
        ):
 | 
			
		||||
            return notify_error("A client must have at least one site")
 | 
			
		||||
 | 
			
		||||
        serializer = SiteSerializer(instance=site, data=request.data["site"])
 | 
			
		||||
        serializer = SiteSerializer(
 | 
			
		||||
            instance=site, data=request.data["site"], partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
@@ -278,9 +283,7 @@ class GenerateAgent(APIView):
 | 
			
		||||
    permission_classes = (AllowAny,)
 | 
			
		||||
 | 
			
		||||
    def get(self, request, uid):
 | 
			
		||||
        import requests
 | 
			
		||||
        import tempfile
 | 
			
		||||
        from django.http import FileResponse
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            _ = uuid.UUID(uid, version=4)
 | 
			
		||||
@@ -289,11 +292,6 @@ class GenerateAgent(APIView):
 | 
			
		||||
 | 
			
		||||
        d = get_object_or_404(Deployment, uid=uid)
 | 
			
		||||
 | 
			
		||||
        inno = (
 | 
			
		||||
            f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
            if d.arch == "64"
 | 
			
		||||
            else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
 | 
			
		||||
        )
 | 
			
		||||
        client = d.client.name.replace(" ", "").lower()
 | 
			
		||||
        site = d.site.name.replace(" ", "").lower()
 | 
			
		||||
        client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
 | 
			
		||||
@@ -301,34 +299,15 @@ class GenerateAgent(APIView):
 | 
			
		||||
        ext = ".exe" if d.arch == "64" else "-x86.exe"
 | 
			
		||||
        file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "client": d.client.pk,
 | 
			
		||||
            "site": d.site.pk,
 | 
			
		||||
            "agenttype": d.mon_type,
 | 
			
		||||
            "rdp": str(d.install_flags["rdp"]),
 | 
			
		||||
            "ping": str(d.install_flags["ping"]),
 | 
			
		||||
            "power": str(d.install_flags["power"]),
 | 
			
		||||
            "goarch": "amd64" if d.arch == "64" else "386",
 | 
			
		||||
            "token": d.token_key,
 | 
			
		||||
            "inno": inno,
 | 
			
		||||
            "url": settings.DL_64 if d.arch == "64" else settings.DL_32,
 | 
			
		||||
            "api": f"https://{request.get_host()}",
 | 
			
		||||
        }
 | 
			
		||||
        headers = {"Content-type": "application/json"}
 | 
			
		||||
 | 
			
		||||
        with tempfile.NamedTemporaryFile() as fp:
 | 
			
		||||
            r = requests.post(
 | 
			
		||||
                settings.EXE_GEN_URL,
 | 
			
		||||
                json=data,
 | 
			
		||||
                headers=headers,
 | 
			
		||||
                stream=True,
 | 
			
		||||
            )
 | 
			
		||||
            with open(fp.name, "wb") as f:
 | 
			
		||||
                for chunk in r.iter_content(chunk_size=1024):
 | 
			
		||||
                    if chunk:
 | 
			
		||||
                        f.write(chunk)
 | 
			
		||||
            del r
 | 
			
		||||
            response = FileResponse(
 | 
			
		||||
                open(fp.name, "rb"), as_attachment=True, filename=file_name
 | 
			
		||||
            )
 | 
			
		||||
            return response
 | 
			
		||||
        return generate_winagent_exe(
 | 
			
		||||
            client=d.client.pk,
 | 
			
		||||
            site=d.site.pk,
 | 
			
		||||
            agent_type=d.mon_type,
 | 
			
		||||
            rdp=d.install_flags["rdp"],
 | 
			
		||||
            ping=d.install_flags["ping"],
 | 
			
		||||
            power=d.install_flags["power"],
 | 
			
		||||
            arch=d.arch,
 | 
			
		||||
            token=d.token_key,
 | 
			
		||||
            api=f"https://{request.get_host()}",
 | 
			
		||||
            file_name=file_name,
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
from .models import CoreSettings, CustomField, CodeSignToken
 | 
			
		||||
 | 
			
		||||
admin.site.register(CoreSettings)
 | 
			
		||||
admin.site.register(CustomField)
 | 
			
		||||
admin.site.register(CodeSignToken)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								api/tacticalrmm/core/consumers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								api/tacticalrmm/core/consumers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
 | 
			
		||||
from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DashInfo(AsyncJsonWebsocketConsumer):
 | 
			
		||||
    async def connect(self):
 | 
			
		||||
 | 
			
		||||
        self.user = self.scope["user"]
 | 
			
		||||
 | 
			
		||||
        if isinstance(self.user, AnonymousUser):
 | 
			
		||||
            await self.close()
 | 
			
		||||
 | 
			
		||||
        await self.accept()
 | 
			
		||||
        self.connected = True
 | 
			
		||||
        self.dash_info = asyncio.create_task(self.send_dash_info())
 | 
			
		||||
 | 
			
		||||
    async def disconnect(self, close_code):
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.dash_info.cancel()
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        self.connected = False
 | 
			
		||||
        await self.close()
 | 
			
		||||
 | 
			
		||||
    async def receive(self, json_data=None):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async
 | 
			
		||||
    def get_dashboard_info(self):
 | 
			
		||||
        server_offline_count = len(
 | 
			
		||||
            [
 | 
			
		||||
                agent
 | 
			
		||||
                for agent in Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
                    "pk",
 | 
			
		||||
                    "last_seen",
 | 
			
		||||
                    "overdue_time",
 | 
			
		||||
                    "offline_time",
 | 
			
		||||
                )
 | 
			
		||||
                if not agent.status == "online"
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        workstation_offline_count = len(
 | 
			
		||||
            [
 | 
			
		||||
                agent
 | 
			
		||||
                for agent in Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
                    "pk",
 | 
			
		||||
                    "last_seen",
 | 
			
		||||
                    "overdue_time",
 | 
			
		||||
                    "offline_time",
 | 
			
		||||
                )
 | 
			
		||||
                if not agent.status == "online"
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
            "total_server_offline_count": server_offline_count,
 | 
			
		||||
            "total_workstation_offline_count": workstation_offline_count,
 | 
			
		||||
            "total_server_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="server"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_workstation_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="workstation"
 | 
			
		||||
            ).count(),
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    async def send_dash_info(self):
 | 
			
		||||
        while self.connected:
 | 
			
		||||
            c = await self.get_dashboard_info()
 | 
			
		||||
            await self.send_json(c)
 | 
			
		||||
            await asyncio.sleep(30)
 | 
			
		||||
@@ -10,6 +10,8 @@ $ping = pingchange
 | 
			
		||||
$auth = '"tokenchange"'
 | 
			
		||||
$downloadlink = 'downloadchange'
 | 
			
		||||
 | 
			
		||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
 | 
			
		||||
 | 
			
		||||
$serviceName = 'tacticalagent'
 | 
			
		||||
If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
 | 
			
		||||
    write-host ('Tactical RMM Is Already Installed')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								api/tacticalrmm/core/migrations/0019_codesigntoken.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/tacticalrmm/core/migrations/0019_codesigntoken.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# Generated by Django 3.2 on 2021-04-13 05:41
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0018_auto_20210329_1709'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='CodeSignToken',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('token', models.CharField(blank=True, max_length=255, null=True)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -266,3 +266,16 @@ class CustomField(models.Model):
 | 
			
		||||
            return self.default_value_bool
 | 
			
		||||
        else:
 | 
			
		||||
            return self.default_value_string
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CodeSignToken(models.Model):
 | 
			
		||||
    token = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if not self.pk and CodeSignToken.objects.exists():
 | 
			
		||||
            raise ValidationError("There can only be one CodeSignToken instance")
 | 
			
		||||
 | 
			
		||||
        super(CodeSignToken, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "Code signing token"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
from .models import CoreSettings, CustomField, CodeSignToken
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoreSettingsSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -27,3 +27,9 @@ class CustomFieldSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CustomField
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CodeSignTokenSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CodeSignToken
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,63 @@
 | 
			
		||||
import requests
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.testing import WebsocketCommunicator
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .consumers import DashInfo
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
from .serializers import CustomFieldSerializer
 | 
			
		||||
from .tasks import core_maintenance_tasks
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCodeSign(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.url = "/core/codesign/"
 | 
			
		||||
 | 
			
		||||
    def test_get_codesign(self):
 | 
			
		||||
        r = self.client.get(self.url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", self.url)
 | 
			
		||||
 | 
			
		||||
    @patch("requests.post")
 | 
			
		||||
    def test_edit_codesign_timeout(self, mock_post):
 | 
			
		||||
        mock_post.side_effect = requests.exceptions.ConnectionError()
 | 
			
		||||
        data = {"token": "token123"}
 | 
			
		||||
        r = self.client.patch(self.url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", self.url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestConsumers(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async
 | 
			
		||||
    def get_token(self):
 | 
			
		||||
        from rest_framework.authtoken.models import Token
 | 
			
		||||
 | 
			
		||||
        token = Token.objects.create(user=self.john)
 | 
			
		||||
        return token.key
 | 
			
		||||
 | 
			
		||||
    async def test_dash_info(self):
 | 
			
		||||
        key = self.get_token()
 | 
			
		||||
        communicator = WebsocketCommunicator(
 | 
			
		||||
            DashInfo.as_asgi(), f"/ws/dashinfo/?access_token={key}"
 | 
			
		||||
        )
 | 
			
		||||
        communicator.scope["user"] = self.john
 | 
			
		||||
        connected, _ = await communicator.connect()
 | 
			
		||||
        assert connected
 | 
			
		||||
        await communicator.disconnect()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,4 +12,5 @@ urlpatterns = [
 | 
			
		||||
    path("servermaintenance/", views.server_maintenance),
 | 
			
		||||
    path("customfields/", views.GetAddCustomFields.as_view()),
 | 
			
		||||
    path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
 | 
			
		||||
    path("codesign/", views.CodeSign.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,12 @@ from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
 | 
			
		||||
from .models import CoreSettings, CustomField, CodeSignToken
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    CoreSettingsSerializer,
 | 
			
		||||
    CustomFieldSerializer,
 | 
			
		||||
    CodeSignTokenSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UploadMeshAgent(APIView):
 | 
			
		||||
@@ -65,6 +69,8 @@ def dashboard_info(request):
 | 
			
		||||
            "dbl_click_action": request.user.agent_dblclick_action,
 | 
			
		||||
            "default_agent_tbl_tab": request.user.default_agent_tbl_tab,
 | 
			
		||||
            "client_tree_sort": request.user.client_tree_sort,
 | 
			
		||||
            "client_tree_splitter": request.user.client_tree_splitter,
 | 
			
		||||
            "loading_bar_color": request.user.loading_bar_color,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -177,3 +183,48 @@ class GetUpdateDeleteCustomFields(APIView):
 | 
			
		||||
        get_object_or_404(CustomField, pk=pk).delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CodeSign(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        token = CodeSignToken.objects.first()
 | 
			
		||||
        return Response(CodeSignTokenSerializer(token).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        import requests
 | 
			
		||||
 | 
			
		||||
        errors = []
 | 
			
		||||
        for url in settings.EXE_GEN_URLS:
 | 
			
		||||
            try:
 | 
			
		||||
                r = requests.post(
 | 
			
		||||
                    f"{url}/api/v1/checktoken",
 | 
			
		||||
                    json={"token": request.data["token"]},
 | 
			
		||||
                    headers={"Content-type": "application/json"},
 | 
			
		||||
                    timeout=15,
 | 
			
		||||
                )
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                errors.append(str(e))
 | 
			
		||||
            else:
 | 
			
		||||
                errors = []
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if errors:
 | 
			
		||||
            return notify_error(", ".join(errors))
 | 
			
		||||
 | 
			
		||||
        if r.status_code == 400 or r.status_code == 401:  # type: ignore
 | 
			
		||||
            return notify_error(r.json()["ret"])  # type: ignore
 | 
			
		||||
        elif r.status_code == 200:  # type: ignore
 | 
			
		||||
            t = CodeSignToken.objects.first()
 | 
			
		||||
            if t is None:
 | 
			
		||||
                CodeSignToken.objects.create(token=request.data["token"])
 | 
			
		||||
            else:
 | 
			
		||||
                serializer = CodeSignTokenSerializer(instance=t, data=request.data)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
            return Response("Token was saved")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            ret = r.json()["ret"]  # type: ignore
 | 
			
		||||
        except:
 | 
			
		||||
            ret = "Something went wrong"
 | 
			
		||||
        return notify_error(ret)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,13 @@
 | 
			
		||||
amqp==5.0.5
 | 
			
		||||
asgiref==3.3.1
 | 
			
		||||
asgiref==3.3.4
 | 
			
		||||
asyncio-nats-client==0.11.4
 | 
			
		||||
billiard==3.6.3.0
 | 
			
		||||
celery==5.0.5
 | 
			
		||||
certifi==2020.12.5
 | 
			
		||||
cffi==1.14.5
 | 
			
		||||
channels==3.0.3
 | 
			
		||||
chardet==4.0.0
 | 
			
		||||
cryptography==3.4.7
 | 
			
		||||
decorator==4.4.2
 | 
			
		||||
Django==3.1.7
 | 
			
		||||
daphne==3.0.2
 | 
			
		||||
Django==3.2.0
 | 
			
		||||
django-cors-headers==3.7.0
 | 
			
		||||
django-rest-knox==4.1.0
 | 
			
		||||
djangorestframework==3.12.4
 | 
			
		||||
@@ -28,7 +27,7 @@ redis==3.5.3
 | 
			
		||||
requests==2.25.1
 | 
			
		||||
six==1.15.0
 | 
			
		||||
sqlparse==0.4.1
 | 
			
		||||
twilio==6.55.0
 | 
			
		||||
twilio==6.56.0
 | 
			
		||||
urllib3==1.26.4
 | 
			
		||||
uWSGI==2.0.19.1
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
 
 | 
			
		||||
@@ -1,254 +1,592 @@
 | 
			
		||||
[
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "ClearFirefoxCache.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Clear Firefox Cache",
 | 
			
		||||
        "description": "This script will clean up Mozilla Firefox for all users.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "ClearGoogleChromeCache.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Clear Google Chrome Cache",
 | 
			
		||||
        "description": "This script will clean up Google Chrome for all users.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "InstallAdobeReader.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Install Adobe Reader DC",
 | 
			
		||||
        "description": "Installs Adobe Reader DC.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Win_Install_Duplicati.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Install Duplicati",
 | 
			
		||||
        "description": "This script installs Duplicati 2.0.5.1 as a service.",
 | 
			
		||||
        "shell": "powershell",
 | 
			
		||||
        "category": "TRMM (Win):3rd Party Software"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Reset-WindowsUpdate.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Reset Windows Update",
 | 
			
		||||
        "description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Start-Cleanup.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Cleanup C: drive",
 | 
			
		||||
        "description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "WindowsDefenderFullScanBackground.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Windows Defender Full Scan",
 | 
			
		||||
        "description": "Runs a Windows Defender Full background scan.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "WindowsDefenderQuickScanBackground.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
        "name": "Windows Defender Quick Scan",
 | 
			
		||||
        "description": "Runs a Quick Scan using Windows Defender in the Background.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "speedtest.py",
 | 
			
		||||
        "submittedBy": "https://github.com/wh1te909",
 | 
			
		||||
        "name": "Speed Test",
 | 
			
		||||
        "description": "Runs a Speed Test",
 | 
			
		||||
        "shell": "python"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Rename-Installed-App.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/bradhawkins85",
 | 
			
		||||
        "name": "Rename Tactical RMM Agent",
 | 
			
		||||
        "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.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "bitlocker_encrypted_drive_c.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
        "name": "Check C Drive for Bitlocker Status",
 | 
			
		||||
        "description": "Runs a check on drive C for Bitlocker status.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Win_Bitlocker_Create_Status_Report.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
        "name": "Create Bitlocker Status Report",
 | 
			
		||||
        "description": "Creates a Bitlocker status report.",
 | 
			
		||||
        "shell": "powershell",
 | 
			
		||||
        "category": "TRMM (Win):Storage"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "bitlocker_retrieve_status_report.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
        "name": "Retreive Bitlocker Status Report",
 | 
			
		||||
        "description": "Retreives a Bitlocker status report.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Win_Bios_Check.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
        "name": "Check BIOS Information",
 | 
			
		||||
        "description": "Retreives and reports on BIOS make, version, and date.",
 | 
			
		||||
        "shell": "powershell",
 | 
			
		||||
        "category": "TRMM (Win):Hardware"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "ResetHighPerformancePowerProfiletoDefaults.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/azulskyknight",
 | 
			
		||||
        "name": "Reset High Perf Power Profile",
 | 
			
		||||
        "description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "SetHighPerformancePowerProfile.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/azulskyknight",
 | 
			
		||||
        "name": "Set High Perf Power Profile",
 | 
			
		||||
        "description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Windows10Upgrade.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/RVL-Solutions and https://github.com/darimm",
 | 
			
		||||
        "name": "Windows 10 Upgrade",
 | 
			
		||||
        "description": "Forces an upgrade to the latest release of Windows 10.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "DiskStatus.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Check Disks",
 | 
			
		||||
        "description": "Checks local disks for errors reported in event viewer within the last 24 hours",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "DuplicatiStatus.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Check Duplicati",
 | 
			
		||||
        "description": "Checks Duplicati Backup is running properly over the last 24 hours",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "EnableDefender.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Enable Windows Defender",
 | 
			
		||||
        "description": "Enables Windows Defender and sets preferences",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "OpenSSHServerInstall.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Install SSH",
 | 
			
		||||
        "description": "Installs and enabled OpenSSH Server",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "RDP_enable.bat",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Enable RDP",
 | 
			
		||||
        "description": "Enables RDP",
 | 
			
		||||
        "shell": "cmd"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Speedtest.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "PS Speed Test",
 | 
			
		||||
        "description": "Powershell speed test (win 10 or server2016+)",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "SyncTime.bat",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Sync DC Time",
 | 
			
		||||
        "description": "Syncs time with domain controller",
 | 
			
		||||
        "shell": "cmd"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "WinDefenderClearLogs.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Clear Defender Logs",
 | 
			
		||||
        "description": "Clears Windows Defender Logs",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "WinDefenderStatus.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Defender Status",
 | 
			
		||||
        "description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "disable_FastStartup.bat",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Disable Fast Startup",
 | 
			
		||||
        "description": "Disables Faststartup on Windows 10",
 | 
			
		||||
        "shell": "cmd"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "updatetacticalexclusion.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "TRMM Defender Exclusions",
 | 
			
		||||
        "description": "Windows Defender Exclusions for Tactical RMM",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Display_Message_To_User.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/bradhawkins85",
 | 
			
		||||
        "name": "Display Message To User",
 | 
			
		||||
        "description": "Displays a popup message to the currently logged on user",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "VerifyAntivirus.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/beejayzed",
 | 
			
		||||
        "name": "Verify Antivirus Status",
 | 
			
		||||
        "description": "Verify and display status for all installed Antiviruses",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "CreateAllUserLogonScript.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/nr-plaxon",
 | 
			
		||||
        "name": "Create User Logon Script",
 | 
			
		||||
        "description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.",
 | 
			
		||||
        "shell": "powershell"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Win_Chocolatey_Update_Installed.bat",
 | 
			
		||||
        "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
        "name": "Chocolatey Update Installed Apps",
 | 
			
		||||
        "description": "Update all apps that were installed using Chocolatey.",
 | 
			
		||||
        "shell": "cmd",
 | 
			
		||||
        "category": "TRMM (Win):3rd Party Software>Chocolatey"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Win_AD_Check_And_Enable_AD_Recycle_Bin.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
        "name": "AD - Check and Enable AD Recycle Bin",
 | 
			
		||||
        "description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
 | 
			
		||||
        "shell": "powershell",
 | 
			
		||||
        "category": "TRMM (Win):Active Directory"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Check_Events_for_Bluescreens.ps1",
 | 
			
		||||
        "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
        "name": "Event Viewer - Check for Bluescreens",
 | 
			
		||||
        "description": "This will check for Bluescreen events on your system",
 | 
			
		||||
        "shell": "powershell",
 | 
			
		||||
        "category": "TRMM (Win):Monitoring"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "filename": "Win_Rename_Computer.ps1",
 | 
			
		||||
        "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",
 | 
			
		||||
        "shell": "powershell",
 | 
			
		||||
        "category": "TRMM (Win):Other"
 | 
			
		||||
    }
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "6820cb5e-5a7f-4d9b-8c22-d54677e3cc04",
 | 
			
		||||
    "filename": "Win_Clear_Firefox_Cache.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Firefox - Clean Cache",
 | 
			
		||||
    "description": "This script will clean up Mozilla Firefox for all users.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Browsers"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443",
 | 
			
		||||
    "filename": "Win_Clear_Google_Chrome_Cache.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Chrome - Clear Cache for All Users",
 | 
			
		||||
    "description": "This script will clean up Google Chrome for all users.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Browsers"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc",
 | 
			
		||||
    "filename": "Win_Install_Adobe_Reader.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Adobe Reader DC - Install",
 | 
			
		||||
    "description": "Installs Adobe Reader DC.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):3rd Party Software>Chocolatey"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
 | 
			
		||||
    "filename": "Win_Install_Duplicati.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Duplicati - Install",
 | 
			
		||||
    "description": "This script installs Duplicati 2.0.5.1 as a service.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):3rd Party Software"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
 | 
			
		||||
    "filename": "Win_Reset_Windows_Update.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Windows Update - Reset",
 | 
			
		||||
    "description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Updates"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "8db87ff0-a9b4-4d9d-bc55-377bbcb85b6d",
 | 
			
		||||
    "filename": "Win_Start_Cleanup.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Disk - Cleanup C: drive",
 | 
			
		||||
    "description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Maintenance"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "2f28e8c1-ae0f-4b46-a826-f513974526a3",
 | 
			
		||||
    "filename": "Win_Defender_FullScan_Background.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Defender - Full Scan",
 | 
			
		||||
    "description": "Runs a Windows Defender Full background scan.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "adf81ddb-3b77-415c-a89b-2ccc826b5aa7",
 | 
			
		||||
    "filename": "Win_Defender_QuickScan_Background.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/Omnicef",
 | 
			
		||||
    "name": "Defender - Quick Scan",
 | 
			
		||||
    "description": "Runs a Quick Scan using Windows Defender in the Background.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "3c46290b-85db-4cd2-93a2-943c8c93b3b1",
 | 
			
		||||
    "filename": "Speedtest.py",
 | 
			
		||||
    "submittedBy": "https://github.com/wh1te909",
 | 
			
		||||
    "name": "Speed Test - Python",
 | 
			
		||||
    "description": "Runs a Speed Test using Python",
 | 
			
		||||
    "shell": "python",
 | 
			
		||||
    "category": "TRMM (Win):Network"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
 | 
			
		||||
    "filename": "Win_Rename_Installed_App.ps1",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):TacticalRMM Related"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5",
 | 
			
		||||
    "filename": "Win_Bitlocker_Encrypted_Drive_c.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
    "name": "Bitlocker - Check C Drive for Status",
 | 
			
		||||
    "description": "Runs a check on drive C for Bitlocker status.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "2ea35fa2-c227-4d17-a40e-4d39f252e27a",
 | 
			
		||||
    "filename": "Win_Bitlocker_Create_Status_Report.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
    "name": "Bitlocker - Create Status Report",
 | 
			
		||||
    "description": "Creates a Bitlocker status report.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "9e5769c1-3873-4941-bf70-e851e0afbd6d",
 | 
			
		||||
    "filename": "Win_Bitlocker_Retrieve_Status_Report.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
    "name": "Bitlocker - Retrieve Status Report",
 | 
			
		||||
    "description": "Retreives a Bitlocker status report.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "72b93487-0266-43f0-97cc-03d4c7ee0b44",
 | 
			
		||||
    "filename": "Win_Bitlocker_Get_Recovery_Keys.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Bitlocker - Get Recovery Keys",
 | 
			
		||||
    "description": "Retreives a Bitlocker Recovery Keys",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "cfa14c28-4dfc-4d4e-95ee-a380652e058d",
 | 
			
		||||
    "filename": "Win_Bios_Check.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/ThatsNASt",
 | 
			
		||||
    "name": "BIOS - Check Information",
 | 
			
		||||
    "description": "Retreives and reports on BIOS make, version, and date.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Hardware"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "e1c27982-b955-4766-85b6-d92527a177cf",
 | 
			
		||||
    "filename": "Win_Hardware_Monitor_Get_Info.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/MaxAnderson95/",
 | 
			
		||||
    "name": "Monitor - Get Info",
 | 
			
		||||
    "description": "Retreives and reports on Monitor info: Manufacturer, Model, Serial",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Hardware"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "ae231ac4-b01f-4a39-a9d2-3d817af75260",
 | 
			
		||||
    "filename": "Win_Hardware_RAM_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "RAM - Check Information",
 | 
			
		||||
    "description": "Retreives and reports on RAM info: DIMM's, total memory, slots total and used",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Hardware"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "95a2ee6f-b89b-4551-856e-3081b041caa7",
 | 
			
		||||
    "filename": "Win_Reset_High_Performance_Power_Profile_to_Defaults.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/azulskyknight",
 | 
			
		||||
    "name": "Power Profile - Reset High Perf Power Profile to defaults",
 | 
			
		||||
    "description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Power"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "2cbd30b0-84dd-4388-a36d-2e2e980f1a3e",
 | 
			
		||||
    "filename": "Win_Set_High_Performance_Power_Profile.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/azulskyknight",
 | 
			
		||||
    "name": "Power Profile - Set High Performance",
 | 
			
		||||
    "description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Power"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "553236d3-81bc-49f4-af8a-0cff925a7f6d",
 | 
			
		||||
    "filename": "Win_10_Upgrade.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/RVL-Solutions and https://github.com/darimm",
 | 
			
		||||
    "name": "Windows 10 Upgrade",
 | 
			
		||||
    "description": "Forces an upgrade to the latest release of Windows 10.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Updates"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
 | 
			
		||||
    "filename": "Win_Disk_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",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Hardware"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
 | 
			
		||||
    "filename": "Win_Duplicati_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Duplicati - Check",
 | 
			
		||||
    "description": "Checks Duplicati Backup is running properly over the last 24 hours",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):3rd Party Software"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "da51111c-aff6-4d87-9d76-0608e1f67fe5",
 | 
			
		||||
    "filename": "Win_Defender_Enable.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Defender - Enable",
 | 
			
		||||
    "description": "Enables Windows Defender and sets preferences",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "a223d03a-e22e-40e0-94f2-92dd8c481d14",
 | 
			
		||||
    "filename": "Win_Open_SSH_Server_Install.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "SSH - Install Feature and Enable",
 | 
			
		||||
    "description": "Installs and enabled OpenSSH Server Feature in Win10",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Windows Features"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "2435297a-6263-4e90-8688-1847400d0e22",
 | 
			
		||||
    "filename": "Win_RDP_enable.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "RDP - Enable",
 | 
			
		||||
    "description": "Enables RDP",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Windows Features"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "24f19ead-fdfe-46b4-9dcb-4cd0e12a3940",
 | 
			
		||||
    "filename": "Win_Speedtest.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Speed Test - Powershell",
 | 
			
		||||
    "description": "Speed Test with Powershell(win 10 or server2016+)",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Network"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0",
 | 
			
		||||
    "filename": "Win_Sync_Time.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "ADDC - Sync DC Time",
 | 
			
		||||
    "description": "Syncs time with domain controller",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Active Directory"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "b6b9912f-4274-4162-99cc-9fd47fbcb292",
 | 
			
		||||
    "filename": "Win_ADDC_Sync_Start.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "ADDC - Sync AD",
 | 
			
		||||
    "description": "Trigger AD Sync on domain controller",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Active Directory"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "b720e320-7755-4c89-9992-e1a6c43699ed",
 | 
			
		||||
    "filename": "Win_Defender_Clear_Logs.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Defender - Clear Logs",
 | 
			
		||||
    "description": "Clears Windows Defender Logs",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "d980fda3-a068-47eb-8495-1aab07a24e64",
 | 
			
		||||
    "filename": "Win_Defender_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Defender - Status",
 | 
			
		||||
    "description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "9956e936-6fdb-4488-a9d8-8b274658037f",
 | 
			
		||||
    "filename": "Win_Disable_Fast_Startup.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Power - Fast Startup Disable",
 | 
			
		||||
    "description": "Disables Faststartup on Windows 10",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Power"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "f628a02b-16c3-4ab5-b788-dec5bc2af1d9",
 | 
			
		||||
    "filename": "Win_Power_Disable_Hibernation.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Power - Hibernate Disable",
 | 
			
		||||
    "description": "Disables Hibernation",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Power"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "2472bbaf-1941-4722-8a58-d1dd0f528801",
 | 
			
		||||
    "filename": "Win_Update_Tactical_Exclusion.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "TRMM Defender Exclusions",
 | 
			
		||||
    "description": "Windows Defender Exclusions for Tactical RMM",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "b253dc76-41a0-48ca-9cea-bee4277402c4",
 | 
			
		||||
    "filename": "Win_Display_Message_To_User.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/bradhawkins85",
 | 
			
		||||
    "name": "Message Popup To User",
 | 
			
		||||
    "description": "Displays a popup message to the currently logged on user",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "19224d21-bd39-44bc-b9cf-8f1ba3ca9c11",
 | 
			
		||||
    "filename": "Win_Antivirus_Verify.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/beejayzed",
 | 
			
		||||
    "name": "Antivirus - Verify Status",
 | 
			
		||||
    "description": "Verify and display status for all installed Antiviruses",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security>Antivirus"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "f88c5c52-c6fe-44db-b727-b7912a4279ed",
 | 
			
		||||
    "filename": "Win_Create_All_User_Logon_Script.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/nr-plaxon",
 | 
			
		||||
    "name": "Template Example - Create User Logon Script",
 | 
			
		||||
    "description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "5615aa90-0272-427b-8acf-0ca019612501",
 | 
			
		||||
    "filename": "Win_Chocolatey_Update_Installed.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Update Installed Apps",
 | 
			
		||||
    "description": "Update all apps that were installed using Chocolatey.",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):3rd Party Software>Chocolatey"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "fff8024d-d72e-4457-84fa-6c780f69a16f",
 | 
			
		||||
    "filename": "Win_AD_Check_And_Enable_AD_Recycle_Bin.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "ADDC - Check and Enable AD Recycle Bin",
 | 
			
		||||
    "description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Active Directory"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "71090fc4-faa6-460b-adb0-95d7863544e1",
 | 
			
		||||
    "filename": "Win_Check_Events_for_Bluescreens.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Event Viewer - Bluescreen Notification",
 | 
			
		||||
    "description": "Event Viewer Monitor - Notify Bluescreen events on your system",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Monitoring"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "8373846f-facc-49b9-9891-3a780a394c89",
 | 
			
		||||
    "filename": "Win_Local_User_Created_Monitor.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Event Viewer - New User Notification",
 | 
			
		||||
    "description": "Event Viewer Monitor - Notify when new Local user is created",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Monitoring"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "65e5cef1-8338-4180-a0bc-cd54e62de690",
 | 
			
		||||
    "filename": "Win_Task_Scheduler_New_Items_Monitor.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Event Viewer - Task Scheduler New Item Notification",
 | 
			
		||||
    "description": "Event Viewer Monitor - Notify when new Task Scheduler item is created",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Monitoring"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "08ca81f2-f044-4dfc-ad47-090b19b19d76",
 | 
			
		||||
    "filename": "Win_User_Logged_in_with_Temp_Profile.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "User Logged in with temp profile check",
 | 
			
		||||
    "description": "Check if users are logged in with a temp profile",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "5d905886-9eb1-4129-8b81-a013f842eb24",
 | 
			
		||||
    "filename": "Win_Rename_Computer.ps1",
 | 
			
		||||
    "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",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other",
 | 
			
		||||
    "default_timeout": 30
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "f396dae2-c768-45c5-bd6c-176e56ed3614",
 | 
			
		||||
    "filename": "Win_Finish_updates_and_restart.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/tremor021",
 | 
			
		||||
    "name": "Updates - Finish and restart",
 | 
			
		||||
    "description": "Finish installing Windows updates and restart PC",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Updates"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "63f89be0-a9c9-4c61-9b55-bce0b28b90b2",
 | 
			
		||||
    "filename": "Win_Finish_updates_and_shutdown.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/tremor021",
 | 
			
		||||
    "name": "Updates - Finish and Shutdown",
 | 
			
		||||
    "description": "Finish installing Windows updates and shutdown PC",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Updates"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8",
 | 
			
		||||
    "filename": "Win_ScreenConnectAIO.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/bradhawkins85",
 | 
			
		||||
    "name": "ScreenConnect AIO",
 | 
			
		||||
    "description": "Install, Uninstall, Start and Stop ScreenConnect Access Agent",
 | 
			
		||||
    "args": [
 | 
			
		||||
      "-serviceName {{client.ScreenConnectService}}",
 | 
			
		||||
      "-url {{client.ScreenConnectInstaller}}",
 | 
			
		||||
      "-action install"
 | 
			
		||||
    ],
 | 
			
		||||
    "default_timeout": "90",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):3rd Party Software"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "3abbb62a-3757-492c-8979-b4fc6174845d",
 | 
			
		||||
    "filename": "Win_Disable_AutoRun.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Autorun - Disable",
 | 
			
		||||
    "description": "Disable Autorun System Wide",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Other",
 | 
			
		||||
    "default_timeout": "30"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "4a11877a-7555-494c-ac74-29d6df3c1989",
 | 
			
		||||
    "filename": "Win_Disable_Cortana.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Cortana - Disable",
 | 
			
		||||
    "description": "Disable Cortana System Wide",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Other",
 | 
			
		||||
    "default_timeout": "30"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "28ef1387-dd4f-4bab-b042-26250914e370",
 | 
			
		||||
    "filename": "Win_WOL_Enable_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Network WoL - Enable function",
 | 
			
		||||
    "description": "Wake on Lan enable on Dell, HP, Lenovo",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Network",
 | 
			
		||||
    "default_timeout": "90"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "685d5432-0b84-46d5-98e8-3ec2054150fe",
 | 
			
		||||
    "filename": "Win_WOL_Test_State.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Network WoL - Test State",
 | 
			
		||||
    "description": "Wake on Lan test status",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Network",
 | 
			
		||||
    "default_timeout": "90"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "abe78170-7cf9-435b-9666-c5ef6c11a106",
 | 
			
		||||
    "filename": "Win_Network_IPv6_Disable.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Network IPv6 - Disable",
 | 
			
		||||
    "description": "Disable IPv6 on all adapters",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Network",
 | 
			
		||||
    "default_timeout": "90"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
 | 
			
		||||
    "filename": "Win_Win10_Change_Key_and_Activate.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "Product Key in Win10 Change and Activate",
 | 
			
		||||
    "description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other",
 | 
			
		||||
    "default_timeout": "90"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
 | 
			
		||||
    "filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "TRMM Delete Start Menu Shortcut for App",
 | 
			
		||||
    "description": "Tactical RMM delete its application shortcut that's installed in the start menu",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):TacticalRMM Related",
 | 
			
		||||
    "default_timeout": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "60130fca-7636-446e-acd7-cc5d29d609c2",
 | 
			
		||||
    "filename": "Win_Firewall_Check_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Windows Firewall - Check Status",
 | 
			
		||||
    "description": "Windows Firewall - Check state, report status",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Network"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "93379675-c01c-433f-87df-a11597c959f0",
 | 
			
		||||
    "filename": "Win_UAC_Check_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Windows UAC - Check Status",
 | 
			
		||||
    "description": "Windows UAC - Report status",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Security"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "7ea6a11a-05c0-4151-b5c1-cb8af029299f",
 | 
			
		||||
    "filename": "Win_AzureAD_Check_Connection_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Azure AD - Check Status",
 | 
			
		||||
    "description": "Azure AD - Check if joined or not",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Azure>AD"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "7d81859a-1ba3-42b0-8664-29844f0dd765",
 | 
			
		||||
    "filename": "Win_Azure_Mars_Cloud_Backup_Status.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/dinger1986",
 | 
			
		||||
    "name": "Azure - Mars Cloud backup Status",
 | 
			
		||||
    "description": "Azure - Mars Cloud backup Check Status",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Azure>Backup"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "e18c64d0-b783-4b52-b44b-9bb7592b439b",
 | 
			
		||||
    "filename": "Win_FileSystem_Enable_Long_Paths.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "File System - Enable Long Paths",
 | 
			
		||||
    "description": "Enables NTFS Long paths greater than 260 characters",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "c6252ca8-5172-42ea-9114-e447f80868f5",
 | 
			
		||||
    "filename": "Win_USB_Disable_Access.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "USB - Disable Access",
 | 
			
		||||
    "description": "USB - Disable Plugged in USB devices",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "3785952f-69fb-4bda-b2fe-5e3e8642738a",
 | 
			
		||||
    "filename": "Win_USB_Enable_Access.bat",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "USB - Enable Access",
 | 
			
		||||
    "description": "USB - Enable Plugged in USB devices",
 | 
			
		||||
    "shell": "cmd",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "c6014da2-b188-4e1b-b96a-e3440ade3a6a",
 | 
			
		||||
    "filename": "Win_RecycleBin_Empty.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/silversword411",
 | 
			
		||||
    "name": "File System - Empty Recycle Bin",
 | 
			
		||||
    "description": "Empty the recycle bin",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Storage"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
 | 
			
		||||
    "filename": "Win_Get_Computer_Users.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/tremor021",
 | 
			
		||||
    "name": "Get Computer Users",
 | 
			
		||||
    "description": "Get list of computer users and show which one is enabled",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",
 | 
			
		||||
    "filename": "Win_Network_Set_To_Private.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/tremor021",
 | 
			
		||||
    "name": "Network Category - Set Network To Private",
 | 
			
		||||
    "description": "Sets current network type to Private",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Network"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "guid": "768f42d5-7b45-45ed-8233-254ae537aaa2",
 | 
			
		||||
    "filename": "Win_TaskScheduler_Add_Task.ps1",
 | 
			
		||||
    "submittedBy": "https://github.com/tremor021",
 | 
			
		||||
    "name": "Task Scheduler - Add a task",
 | 
			
		||||
    "description": "Add a task to Task Scheduler, needs editing",
 | 
			
		||||
    "shell": "powershell",
 | 
			
		||||
    "category": "TRMM (Win):Other"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										19
									
								
								api/tacticalrmm/scripts/migrations/0007_script_args.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/tacticalrmm/scripts/migrations/0007_script_args.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-01 14:52
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('scripts', '0006_script_default_timeout'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='script',
 | 
			
		||||
            name='args',
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/scripts/migrations/0008_script_guid.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/scripts/migrations/0008_script_guid.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-15 02:10
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('scripts', '0007_script_args'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='script',
 | 
			
		||||
            name='guid',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=64, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
import base64
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from typing import Any, List, Union
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
@@ -15,8 +19,11 @@ SCRIPT_TYPES = [
 | 
			
		||||
    ("builtin", "Built In"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Script(BaseAuditModel):
 | 
			
		||||
    guid = name = models.CharField(max_length=64, null=True, blank=True)
 | 
			
		||||
    name = models.CharField(max_length=255)
 | 
			
		||||
    description = models.TextField(null=True, blank=True)
 | 
			
		||||
    filename = models.CharField(max_length=255)  # deprecated
 | 
			
		||||
@@ -26,6 +33,12 @@ class Script(BaseAuditModel):
 | 
			
		||||
    script_type = models.CharField(
 | 
			
		||||
        max_length=100, choices=SCRIPT_TYPES, default="userdefined"
 | 
			
		||||
    )
 | 
			
		||||
    args = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    favorite = models.BooleanField(default=False)
 | 
			
		||||
    category = models.CharField(max_length=100, null=True, blank=True)
 | 
			
		||||
    code_base64 = models.TextField(null=True, blank=True)
 | 
			
		||||
@@ -66,15 +79,28 @@ class Script(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        for script in info:
 | 
			
		||||
            if os.path.exists(os.path.join(scripts_dir, script["filename"])):
 | 
			
		||||
                s = cls.objects.filter(script_type="builtin").filter(
 | 
			
		||||
                    name=script["name"]
 | 
			
		||||
                s = cls.objects.filter(script_type="builtin", guid=script["guid"])
 | 
			
		||||
 | 
			
		||||
                category = (
 | 
			
		||||
                    script["category"] if "category" in script.keys() else "Community"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                default_timeout = (
 | 
			
		||||
                    int(script["default_timeout"])
 | 
			
		||||
                    if "default_timeout" in script.keys()
 | 
			
		||||
                    else 90
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                args = script["args"] if "args" in script.keys() else []
 | 
			
		||||
 | 
			
		||||
                if s.exists():
 | 
			
		||||
                    i = s.first()
 | 
			
		||||
                    i.name = script["name"]
 | 
			
		||||
                    i.description = script["description"]
 | 
			
		||||
                    i.category = "Community"
 | 
			
		||||
                    i.category = category
 | 
			
		||||
                    i.shell = script["shell"]
 | 
			
		||||
                    i.default_timeout = default_timeout
 | 
			
		||||
                    i.args = args
 | 
			
		||||
 | 
			
		||||
                    with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
 | 
			
		||||
                        script_bytes = (
 | 
			
		||||
@@ -87,10 +113,52 @@ class Script(BaseAuditModel):
 | 
			
		||||
                            "name",
 | 
			
		||||
                            "description",
 | 
			
		||||
                            "category",
 | 
			
		||||
                            "default_timeout",
 | 
			
		||||
                            "code_base64",
 | 
			
		||||
                            "shell",
 | 
			
		||||
                            "args",
 | 
			
		||||
                        ]
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                # check if script was added without a guid
 | 
			
		||||
                elif cls.objects.filter(
 | 
			
		||||
                    script_type="builtin", name=script["name"]
 | 
			
		||||
                ).exists():
 | 
			
		||||
                    s = cls.objects.get(script_type="builtin", name=script["name"])
 | 
			
		||||
 | 
			
		||||
                    if not s.guid:
 | 
			
		||||
                        print(f"Updating GUID for: {script['name']}")
 | 
			
		||||
                        s.guid = script["guid"]
 | 
			
		||||
                        s.name = script["name"]
 | 
			
		||||
                        s.description = script["description"]
 | 
			
		||||
                        s.category = category
 | 
			
		||||
                        s.shell = script["shell"]
 | 
			
		||||
                        s.default_timeout = default_timeout
 | 
			
		||||
                        s.args = args
 | 
			
		||||
 | 
			
		||||
                        with open(
 | 
			
		||||
                            os.path.join(scripts_dir, script["filename"]), "rb"
 | 
			
		||||
                        ) as f:
 | 
			
		||||
                            script_bytes = (
 | 
			
		||||
                                f.read().decode("utf-8").encode("ascii", "ignore")
 | 
			
		||||
                            )
 | 
			
		||||
                            s.code_base64 = base64.b64encode(script_bytes).decode(
 | 
			
		||||
                                "ascii"
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                        s.save(
 | 
			
		||||
                            update_fields=[
 | 
			
		||||
                                "guid",
 | 
			
		||||
                                "name",
 | 
			
		||||
                                "description",
 | 
			
		||||
                                "category",
 | 
			
		||||
                                "default_timeout",
 | 
			
		||||
                                "code_base64",
 | 
			
		||||
                                "shell",
 | 
			
		||||
                                "args",
 | 
			
		||||
                            ]
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    print(f"Adding new community script: {script['name']}")
 | 
			
		||||
 | 
			
		||||
@@ -102,17 +170,128 @@ class Script(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
                        cls(
 | 
			
		||||
                            code_base64=code_base64,
 | 
			
		||||
                            guid=script["guid"],
 | 
			
		||||
                            name=script["name"],
 | 
			
		||||
                            description=script["description"],
 | 
			
		||||
                            filename=script["filename"],
 | 
			
		||||
                            shell=script["shell"],
 | 
			
		||||
                            script_type="builtin",
 | 
			
		||||
                            category="Community",
 | 
			
		||||
                            category=category,
 | 
			
		||||
                            default_timeout=default_timeout,
 | 
			
		||||
                            args=args,
 | 
			
		||||
                        ).save()
 | 
			
		||||
 | 
			
		||||
        # delete community scripts that had their name changed
 | 
			
		||||
        cls.objects.filter(script_type="builtin", guid=None).delete()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(script):
 | 
			
		||||
        # serializes the script and returns json
 | 
			
		||||
        from .serializers import ScriptSerializer
 | 
			
		||||
 | 
			
		||||
        return ScriptSerializer(script).data
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def parse_script_args(
 | 
			
		||||
        cls, agent, shell: str, args: List[str] = list()
 | 
			
		||||
    ) -> Union[List[str], None]:
 | 
			
		||||
        from core.models import CustomField
 | 
			
		||||
 | 
			
		||||
        if not list:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        temp_args = list()
 | 
			
		||||
 | 
			
		||||
        # pattern to match for injection
 | 
			
		||||
        pattern = re.compile(".*\\{\\{(.*)\\}\\}.*")
 | 
			
		||||
 | 
			
		||||
        for arg in args:
 | 
			
		||||
            match = pattern.match(arg)
 | 
			
		||||
            if match:
 | 
			
		||||
                # only get the match between the () in regex
 | 
			
		||||
                string = match.group(1)
 | 
			
		||||
 | 
			
		||||
                # split by period if exists. First should be model and second should be property
 | 
			
		||||
                temp = string.split(".")
 | 
			
		||||
 | 
			
		||||
                # check for model and property
 | 
			
		||||
                if len(temp) != 2:
 | 
			
		||||
                    # ignore arg since it is invalid
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                if temp[0] == "client":
 | 
			
		||||
                    model = "client"
 | 
			
		||||
                    obj = agent.client
 | 
			
		||||
                elif temp[0] == "site":
 | 
			
		||||
                    model = "site"
 | 
			
		||||
                    obj = agent.site
 | 
			
		||||
                elif temp[0] == "agent":
 | 
			
		||||
                    model = "agent"
 | 
			
		||||
                    obj = agent
 | 
			
		||||
                else:
 | 
			
		||||
                    # ignore arg since it is invalid
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                if hasattr(obj, temp[1]):
 | 
			
		||||
                    value = getattr(obj, temp[1])
 | 
			
		||||
 | 
			
		||||
                elif CustomField.objects.filter(model=model, name=temp[1]).exists():
 | 
			
		||||
 | 
			
		||||
                    field = CustomField.objects.get(model=model, name=temp[1])
 | 
			
		||||
                    model_fields = getattr(field, f"{model}_fields")
 | 
			
		||||
                    value = None
 | 
			
		||||
                    if model_fields.filter(**{model: obj}).exists():
 | 
			
		||||
                        value = model_fields.get(**{model: obj}).value
 | 
			
		||||
 | 
			
		||||
                    if not value and field.default_value:
 | 
			
		||||
                        value = field.default_value
 | 
			
		||||
 | 
			
		||||
                    # check if value exists and if not use defa
 | 
			
		||||
                    if value and field.type == "multiple":
 | 
			
		||||
                        value = format_shell_array(shell, value)
 | 
			
		||||
                    elif value and field.type == "checkbox":
 | 
			
		||||
                        value = format_shell_bool(shell, value)
 | 
			
		||||
 | 
			
		||||
                    if not value:
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    # ignore arg since property is invalid
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # replace the value in the arg and push to array
 | 
			
		||||
                # log any unhashable type errors
 | 
			
		||||
                try:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(e)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                temp_args.append(arg)
 | 
			
		||||
 | 
			
		||||
        return temp_args
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_shell_array(shell: str, value: Any) -> str:
 | 
			
		||||
    if shell == "cmd":
 | 
			
		||||
        return "array args are not supported with batch"
 | 
			
		||||
    elif shell == "powershell":
 | 
			
		||||
        temp_string = ""
 | 
			
		||||
        for item in value:
 | 
			
		||||
            temp_string += item + ","
 | 
			
		||||
        return temp_string.strip(",")
 | 
			
		||||
    else:  # python
 | 
			
		||||
        temp_string = ""
 | 
			
		||||
        for item in value:
 | 
			
		||||
            temp_string += item + ","
 | 
			
		||||
        return temp_string.strip(",")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_shell_bool(shell: str, value: Any) -> str:
 | 
			
		||||
    if shell == "cmd":
 | 
			
		||||
        return "1" if value else "0"
 | 
			
		||||
    elif shell == "powershell":
 | 
			
		||||
        return "$True" if value else "$False"
 | 
			
		||||
    else:  # python
 | 
			
		||||
        return "True" if value else "False"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ class ScriptTableSerializer(ModelSerializer):
 | 
			
		||||
            "description",
 | 
			
		||||
            "script_type",
 | 
			
		||||
            "shell",
 | 
			
		||||
            "args",
 | 
			
		||||
            "category",
 | 
			
		||||
            "favorite",
 | 
			
		||||
            "default_timeout",
 | 
			
		||||
@@ -26,6 +27,7 @@ class ScriptSerializer(ModelSerializer):
 | 
			
		||||
            "name",
 | 
			
		||||
            "description",
 | 
			
		||||
            "shell",
 | 
			
		||||
            "args",
 | 
			
		||||
            "category",
 | 
			
		||||
            "favorite",
 | 
			
		||||
            "code_base64",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
from email.policy import default
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
@@ -23,7 +24,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        serializer = ScriptTableSerializer(scripts, many=True)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(serializer.data, resp.data)
 | 
			
		||||
        self.assertEqual(serializer.data, resp.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -37,10 +38,12 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
            "category": "New",
 | 
			
		||||
            "code": "Some Test Code\nnew Line",
 | 
			
		||||
            "default_timeout": 99,
 | 
			
		||||
            "args": ["hello", "world", r"{{agent.public_ip}}"],
 | 
			
		||||
            "favorite": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # test without file upload
 | 
			
		||||
        resp = self.client.post(url, data)
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertTrue(Script.objects.filter(name="Name").exists())
 | 
			
		||||
        self.assertEqual(Script.objects.get(name="Name").code, data["code"])
 | 
			
		||||
@@ -57,6 +60,9 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
            "category": "New",
 | 
			
		||||
            "filename": file,
 | 
			
		||||
            "default_timeout": 4455,
 | 
			
		||||
            "args": json.dumps(
 | 
			
		||||
                ["hello", "world", r"{{agent.public_ip}}"]
 | 
			
		||||
            ),  # simulate javascript's JSON.stringify() for formData
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # test with file upload
 | 
			
		||||
@@ -124,11 +130,11 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        script = baker.make("scripts.Script")
 | 
			
		||||
        url = f"/scripts/{script.pk}/script/"
 | 
			
		||||
        url = f"/scripts/{script.pk}/script/"  # type: ignore
 | 
			
		||||
        serializer = ScriptSerializer(script)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(serializer.data, resp.data)
 | 
			
		||||
        self.assertEqual(serializer.data, resp.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -164,27 +170,27 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        script = baker.make(
 | 
			
		||||
            "scripts.Script", code_base64="VGVzdA==", shell="powershell"
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, {"filename": f"{script.name}.ps1", "code": "Test"})
 | 
			
		||||
        self.assertEqual(resp.data, {"filename": f"{script.name}.ps1", "code": "Test"})  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test batch file
 | 
			
		||||
        script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd")
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, {"filename": f"{script.name}.bat", "code": "Test"})
 | 
			
		||||
        self.assertEqual(resp.data, {"filename": f"{script.name}.bat", "code": "Test"})  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test python file
 | 
			
		||||
        script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python")
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, {"filename": f"{script.name}.py", "code": "Test"})
 | 
			
		||||
        self.assertEqual(resp.data, {"filename": f"{script.name}.py", "code": "Test"})  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -201,6 +207,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        ) as f:
 | 
			
		||||
            info = json.load(f)
 | 
			
		||||
 | 
			
		||||
        guids = []
 | 
			
		||||
        for script in info:
 | 
			
		||||
            fn: str = script["filename"]
 | 
			
		||||
            self.assertTrue(os.path.exists(os.path.join(scripts_dir, fn)))
 | 
			
		||||
@@ -217,6 +224,19 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
            elif fn.endswith(".py"):
 | 
			
		||||
                self.assertEqual(script["shell"], "python")
 | 
			
		||||
 | 
			
		||||
            if "args" in script.keys():
 | 
			
		||||
                self.assertIsInstance(script["args"], list)
 | 
			
		||||
 | 
			
		||||
            # allows strings as long as they can be type casted to int
 | 
			
		||||
            if "default_timeout" in script.keys():
 | 
			
		||||
                self.assertIsInstance(int(script["default_timeout"]), int)
 | 
			
		||||
 | 
			
		||||
            self.assertIn("guid", script.keys())
 | 
			
		||||
            guids.append(script["guid"])
 | 
			
		||||
 | 
			
		||||
        # check guids are unique
 | 
			
		||||
        self.assertEqual(len(guids), len(set(guids)))
 | 
			
		||||
 | 
			
		||||
    def test_load_community_scripts(self):
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
 | 
			
		||||
@@ -225,9 +245,46 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        Script.load_community_scripts()
 | 
			
		||||
 | 
			
		||||
        community_scripts = Script.objects.filter(script_type="builtin").count()
 | 
			
		||||
        self.assertEqual(len(info), community_scripts)
 | 
			
		||||
        community_scripts_count = Script.objects.filter(script_type="builtin").count()
 | 
			
		||||
        if len(info) != community_scripts_count:
 | 
			
		||||
            raise Exception(
 | 
			
		||||
                f"There are {len(info)} scripts in json file but only {community_scripts_count} in database"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # test updating already added community scripts
 | 
			
		||||
        Script.load_community_scripts()
 | 
			
		||||
        self.assertEqual(len(info), community_scripts)
 | 
			
		||||
        community_scripts_count2 = Script.objects.filter(script_type="builtin").count()
 | 
			
		||||
        self.assertEqual(len(info), community_scripts_count2)
 | 
			
		||||
 | 
			
		||||
    def test_community_script_has_jsonfile_entry(self):
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
 | 
			
		||||
        ) as f:
 | 
			
		||||
            info = json.load(f)
 | 
			
		||||
 | 
			
		||||
        filenames = [i["filename"] for i in info]
 | 
			
		||||
 | 
			
		||||
        # normal
 | 
			
		||||
        if not settings.DOCKER_BUILD:
 | 
			
		||||
            scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts")
 | 
			
		||||
        # docker
 | 
			
		||||
        else:
 | 
			
		||||
            scripts_dir = settings.SCRIPTS_DIR
 | 
			
		||||
 | 
			
		||||
        with os.scandir(scripts_dir) as it:
 | 
			
		||||
            for f in it:
 | 
			
		||||
                if not f.name.startswith(".") and f.is_file():
 | 
			
		||||
                    if f.name not in filenames:
 | 
			
		||||
                        raise Exception(
 | 
			
		||||
                            f"{f.name} is missing an entry in community_scripts.json"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
    def test_script_filenames_do_not_contain_spaces(self):
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
 | 
			
		||||
        ) as f:
 | 
			
		||||
            info = json.load(f)
 | 
			
		||||
            for script in info:
 | 
			
		||||
                fn: str = script["filename"]
 | 
			
		||||
                if " " in fn:
 | 
			
		||||
                    raise Exception(f"{fn} must not contain spaces in filename")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import base64
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
@@ -24,7 +25,6 @@ class GetAddScripts(APIView):
 | 
			
		||||
        return Response(ScriptTableSerializer(scripts, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request, format=None):
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": request.data["name"],
 | 
			
		||||
            "category": request.data["category"],
 | 
			
		||||
@@ -34,16 +34,24 @@ class GetAddScripts(APIView):
 | 
			
		||||
            "script_type": "userdefined",  # force all uploads to be userdefined. built in scripts cannot be edited by user
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if "favorite" in request.data:
 | 
			
		||||
        # code editor upload
 | 
			
		||||
        if "args" in request.data.keys() and isinstance(request.data["args"], list):
 | 
			
		||||
            data["args"] = request.data["args"]
 | 
			
		||||
 | 
			
		||||
        # file upload, have to json load it cuz it's formData
 | 
			
		||||
        if "args" in request.data.keys() and "file_upload" in request.data.keys():
 | 
			
		||||
            data["args"] = json.loads(request.data["args"])
 | 
			
		||||
 | 
			
		||||
        if "favorite" in request.data.keys():
 | 
			
		||||
            data["favorite"] = request.data["favorite"]
 | 
			
		||||
 | 
			
		||||
        if "filename" in request.data:
 | 
			
		||||
        if "filename" in request.data.keys():
 | 
			
		||||
            message_bytes = request.data["filename"].read()
 | 
			
		||||
            data["code_base64"] = base64.b64encode(message_bytes).decode(
 | 
			
		||||
                "ascii", "ignore"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif "code" in request.data:
 | 
			
		||||
        elif "code" in request.data.keys():
 | 
			
		||||
            message_bytes = request.data["code"].encode("ascii", "ignore")
 | 
			
		||||
            data["code_base64"] = base64.b64encode(message_bytes).decode("ascii")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
@@ -39,7 +40,7 @@ class TestSoftwareViews(TacticalTestCase):
 | 
			
		||||
        # test without agent software
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(resp.data, [])
 | 
			
		||||
        self.assertEquals(resp.data, [])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make some software
 | 
			
		||||
        software = baker.make(
 | 
			
		||||
@@ -51,6 +52,70 @@ class TestSoftwareViews(TacticalTestCase):
 | 
			
		||||
        serializer = InstalledSoftwareSerializer(software)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(resp.data, serializer.data)
 | 
			
		||||
        self.assertEquals(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_install(self, nats_cmd):
 | 
			
		||||
        url = "/software/install/"
 | 
			
		||||
        old_agent = baker.make_recipe("agents.online_agent", version="1.4.7")
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": old_agent.pk,
 | 
			
		||||
            "name": "duplicati",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent", version=settings.LATEST_AGENT_VER
 | 
			
		||||
        )
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "name": "duplicati",
 | 
			
		||||
        }
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_refresh_installed(self, nats_cmd):
 | 
			
		||||
        url = "/software/refresh/4827342/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "software.InstalledSoftware",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            software={},
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/software/refresh/{agent.pk}/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/software1.json")
 | 
			
		||||
        ) as f:
 | 
			
		||||
            sw = json.load(f)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.return_value = sw
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        s = agent.installedsoftware_set.first()
 | 
			
		||||
        s.delete()
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								api/tacticalrmm/tacticalrmm/asgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/tacticalrmm/tacticalrmm/asgi.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
import django
 | 
			
		||||
 | 
			
		||||
from channels.routing import ProtocolTypeRouter, URLRouter  # isort:skip
 | 
			
		||||
from django.core.asgi import get_asgi_application  # isort:skip
 | 
			
		||||
 | 
			
		||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings")
 | 
			
		||||
django.setup()
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import KnoxAuthMiddlewareStack  # isort:skip
 | 
			
		||||
from .urls import ws_urlpatterns  # isort:skip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
application = ProtocolTypeRouter(
 | 
			
		||||
    {
 | 
			
		||||
        "http": get_asgi_application(),
 | 
			
		||||
        "websocket": KnoxAuthMiddlewareStack(URLRouter(ws_urlpatterns)),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -15,25 +15,32 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
 | 
			
		||||
AUTH_USER_MODEL = "accounts.User"
 | 
			
		||||
 | 
			
		||||
# latest release
 | 
			
		||||
TRMM_VERSION = "0.4.31"
 | 
			
		||||
TRMM_VERSION = "0.6.1"
 | 
			
		||||
 | 
			
		||||
# bump this version everytime vue code is changed
 | 
			
		||||
# to alert user they need to manually refresh their browser
 | 
			
		||||
APP_VER = "0.0.124"
 | 
			
		||||
APP_VER = "0.0.130"
 | 
			
		||||
 | 
			
		||||
# https://github.com/wh1te909/rmmagent
 | 
			
		||||
LATEST_AGENT_VER = "1.4.13"
 | 
			
		||||
LATEST_AGENT_VER = "1.5.1"
 | 
			
		||||
 | 
			
		||||
MESH_VER = "0.7.93"
 | 
			
		||||
 | 
			
		||||
# for the update script, bump when need to recreate venv or npm install
 | 
			
		||||
PIP_VER = "13"
 | 
			
		||||
NPM_VER = "12"
 | 
			
		||||
PIP_VER = "15"
 | 
			
		||||
NPM_VER = "14"
 | 
			
		||||
 | 
			
		||||
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
 | 
			
		||||
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
 | 
			
		||||
 | 
			
		||||
EXE_GEN_URL = "https://exe.tacticalrmm.io/api/v1/exe"
 | 
			
		||||
EXE_GEN_URLS = [
 | 
			
		||||
    "https://exe2.tacticalrmm.io",
 | 
			
		||||
    "https://exe.tacticalrmm.io",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
 | 
			
		||||
 | 
			
		||||
ASGI_APPLICATION = "tacticalrmm.asgi.application"
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from .local_settings import *
 | 
			
		||||
@@ -45,6 +52,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    "django.contrib.contenttypes",
 | 
			
		||||
    "django.contrib.sessions",
 | 
			
		||||
    "django.contrib.staticfiles",
 | 
			
		||||
    "channels",
 | 
			
		||||
    "rest_framework",
 | 
			
		||||
    "rest_framework.authtoken",
 | 
			
		||||
    "knox",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2012
									
								
								api/tacticalrmm/tacticalrmm/test_data/software1.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2012
									
								
								api/tacticalrmm/tacticalrmm/test_data/software1.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										111
									
								
								api/tacticalrmm/tacticalrmm/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								api/tacticalrmm/tacticalrmm/tests.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from unittest.mock import mock_open, patch
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.test import TestCase, override_settings
 | 
			
		||||
 | 
			
		||||
from .utils import (
 | 
			
		||||
    bitdays_to_string,
 | 
			
		||||
    filter_software,
 | 
			
		||||
    generate_winagent_exe,
 | 
			
		||||
    get_bit_days,
 | 
			
		||||
    reload_nats,
 | 
			
		||||
    run_nats_api_cmd,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUtils(TestCase):
 | 
			
		||||
    @patch("requests.post")
 | 
			
		||||
    @patch("__main__.__builtins__.open", new_callable=mock_open)
 | 
			
		||||
    def test_generate_winagent_exe_success(self, m_open, mock_post):
 | 
			
		||||
        r = generate_winagent_exe(
 | 
			
		||||
            client=1,
 | 
			
		||||
            site=1,
 | 
			
		||||
            agent_type="server",
 | 
			
		||||
            rdp=1,
 | 
			
		||||
            ping=0,
 | 
			
		||||
            power=0,
 | 
			
		||||
            arch="64",
 | 
			
		||||
            token="abc123",
 | 
			
		||||
            api="https://api.example.com",
 | 
			
		||||
            file_name="rmm-client-site-server.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    @patch("requests.post")
 | 
			
		||||
    def test_generate_winagent_exe_timeout(self, mock_post):
 | 
			
		||||
        mock_post.side_effect = requests.exceptions.ConnectionError()
 | 
			
		||||
 | 
			
		||||
        r = generate_winagent_exe(
 | 
			
		||||
            client=1,
 | 
			
		||||
            site=1,
 | 
			
		||||
            agent_type="server",
 | 
			
		||||
            rdp=1,
 | 
			
		||||
            ping=0,
 | 
			
		||||
            power=0,
 | 
			
		||||
            arch="64",
 | 
			
		||||
            token="abc123",
 | 
			
		||||
            api="https://api.example.com",
 | 
			
		||||
            file_name="rmm-client-site-server.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
    @override_settings(
 | 
			
		||||
        CERT_FILE="/tmp/asdasd.pem",
 | 
			
		||||
        KEY_FILE="/tmp/asds55r.pem",
 | 
			
		||||
        ALLOWED_HOSTS=["api.example.com"],
 | 
			
		||||
        SECRET_KEY="sekret",
 | 
			
		||||
        DOCKER_BUILD=True,
 | 
			
		||||
    )
 | 
			
		||||
    @patch("subprocess.run")
 | 
			
		||||
    def test_reload_nats_docker(self, mock_subprocess):
 | 
			
		||||
        _ = reload_nats()
 | 
			
		||||
 | 
			
		||||
        mock_subprocess.assert_not_called()
 | 
			
		||||
 | 
			
		||||
    @override_settings(
 | 
			
		||||
        ALLOWED_HOSTS=["api.example.com"],
 | 
			
		||||
        SECRET_KEY="sekret",
 | 
			
		||||
    )
 | 
			
		||||
    @patch("subprocess.run")
 | 
			
		||||
    def test_reload_nats(self, mock_subprocess):
 | 
			
		||||
        _ = reload_nats()
 | 
			
		||||
 | 
			
		||||
        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("monitor", ids)
 | 
			
		||||
        mock_subprocess.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    def test_bitdays_to_string(self):
 | 
			
		||||
        a = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
 | 
			
		||||
        all_days = [
 | 
			
		||||
            "Monday",
 | 
			
		||||
            "Tuesday",
 | 
			
		||||
            "Wednesday",
 | 
			
		||||
            "Thursday",
 | 
			
		||||
            "Friday",
 | 
			
		||||
            "Saturday",
 | 
			
		||||
            "Sunday",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        bit_weekdays = get_bit_days(a)
 | 
			
		||||
        r = bitdays_to_string(bit_weekdays)
 | 
			
		||||
        self.assertEqual(r, ", ".join(a))
 | 
			
		||||
 | 
			
		||||
        bit_weekdays = get_bit_days(all_days)
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        r = filter_software(sw)
 | 
			
		||||
        self.assertIsInstance(r, list)
 | 
			
		||||
@@ -3,6 +3,7 @@ from django.urls import include, path
 | 
			
		||||
from knox import views as knox_views
 | 
			
		||||
 | 
			
		||||
from accounts.views import CheckCreds, LoginView
 | 
			
		||||
from core import consumers
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("checkcreds/", CheckCreds.as_view()),
 | 
			
		||||
@@ -29,3 +30,7 @@ if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
 | 
			
		||||
    from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
    urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
 | 
			
		||||
 | 
			
		||||
ws_urlpatterns = [
 | 
			
		||||
    path("ws/dashinfo/", consumers.DashInfo.as_asgi()),  # type: ignore
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,25 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
import subprocess
 | 
			
		||||
import tempfile
 | 
			
		||||
import time
 | 
			
		||||
import urllib.parse
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
import requests
 | 
			
		||||
from channels.auth import AuthMiddlewareStack
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
from django.http import FileResponse
 | 
			
		||||
from knox.auth import TokenAuthentication
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +39,87 @@ WEEK_DAYS = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_winagent_exe(
 | 
			
		||||
    client: int,
 | 
			
		||||
    site: int,
 | 
			
		||||
    agent_type: str,
 | 
			
		||||
    rdp: int,
 | 
			
		||||
    ping: int,
 | 
			
		||||
    power: int,
 | 
			
		||||
    arch: str,
 | 
			
		||||
    token: str,
 | 
			
		||||
    api: str,
 | 
			
		||||
    file_name: str,
 | 
			
		||||
) -> Union[Response, FileResponse]:
 | 
			
		||||
 | 
			
		||||
    from agents.utils import get_exegen_url
 | 
			
		||||
 | 
			
		||||
    inno = (
 | 
			
		||||
        f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
        if arch == "64"
 | 
			
		||||
        else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        codetoken = CodeSignToken.objects.first().token
 | 
			
		||||
        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
			
		||||
        params = {
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
            "arch": arch,
 | 
			
		||||
            "token": codetoken,
 | 
			
		||||
        }
 | 
			
		||||
        dl_url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
    except:
 | 
			
		||||
        codetoken = ""
 | 
			
		||||
        dl_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
			
		||||
 | 
			
		||||
    data = {
 | 
			
		||||
        "client": client,
 | 
			
		||||
        "site": site,
 | 
			
		||||
        "agenttype": agent_type,
 | 
			
		||||
        "rdp": str(rdp),
 | 
			
		||||
        "ping": str(ping),
 | 
			
		||||
        "power": str(power),
 | 
			
		||||
        "goarch": "amd64" if arch == "64" else "386",
 | 
			
		||||
        "token": token,
 | 
			
		||||
        "inno": inno,
 | 
			
		||||
        "url": dl_url,
 | 
			
		||||
        "api": api,
 | 
			
		||||
        "codesigntoken": codetoken,
 | 
			
		||||
    }
 | 
			
		||||
    headers = {"Content-type": "application/json"}
 | 
			
		||||
 | 
			
		||||
    errors = []
 | 
			
		||||
    with tempfile.NamedTemporaryFile() as fp:
 | 
			
		||||
        for url in settings.EXE_GEN_URLS:
 | 
			
		||||
            try:
 | 
			
		||||
                r = requests.post(
 | 
			
		||||
                    f"{url}/api/v1/exe",
 | 
			
		||||
                    json=data,
 | 
			
		||||
                    headers=headers,
 | 
			
		||||
                    stream=True,
 | 
			
		||||
                    timeout=900,
 | 
			
		||||
                )
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                errors.append(str(e))
 | 
			
		||||
            else:
 | 
			
		||||
                errors = []
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if errors:
 | 
			
		||||
            logger.error(errors)
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "Something went wrong. Check debug error log for exact error message"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        with open(fp.name, "wb") as f:
 | 
			
		||||
            for chunk in r.iter_content(chunk_size=1024):  # type: ignore
 | 
			
		||||
                if chunk:
 | 
			
		||||
                    f.write(chunk)
 | 
			
		||||
        del r
 | 
			
		||||
        return FileResponse(open(fp.name, "rb"), as_attachment=True, filename=file_name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_default_timezone():
 | 
			
		||||
    from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +129,7 @@ def get_default_timezone():
 | 
			
		||||
def get_bit_days(days: list[str]) -> int:
 | 
			
		||||
    bit_days = 0
 | 
			
		||||
    for day in days:
 | 
			
		||||
        bit_days |= WEEK_DAYS.get(day)
 | 
			
		||||
        bit_days |= WEEK_DAYS.get(day)  # type: ignore
 | 
			
		||||
    return bit_days
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -128,3 +219,47 @@ def reload_nats():
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            ["/usr/local/bin/nats-server", "-signal", "reload"], capture_output=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@database_sync_to_async
 | 
			
		||||
def get_user(access_token):
 | 
			
		||||
    try:
 | 
			
		||||
        auth = TokenAuthentication()
 | 
			
		||||
        token = access_token.decode().split("access_token=")[1]
 | 
			
		||||
        user = auth.authenticate_credentials(token.encode())
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return AnonymousUser()
 | 
			
		||||
    else:
 | 
			
		||||
        return user[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KnoxAuthMiddlewareInstance:
 | 
			
		||||
    def __init__(self, app):
 | 
			
		||||
        self.app = app
 | 
			
		||||
 | 
			
		||||
    async def __call__(self, scope, receive, send):
 | 
			
		||||
        scope["user"] = await get_user(scope["query_string"])
 | 
			
		||||
 | 
			
		||||
        return await self.app(scope, receive, send)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance(
 | 
			
		||||
    AuthMiddlewareStack(inner)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_nats_api_cmd(mode: str, ids: list[str], timeout: int = 30) -> None:
 | 
			
		||||
    config = {
 | 
			
		||||
        "key": settings.SECRET_KEY,
 | 
			
		||||
        "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
			
		||||
        "agents": ids,
 | 
			
		||||
    }
 | 
			
		||||
    with tempfile.NamedTemporaryFile() as fp:
 | 
			
		||||
        with open(fp.name, "w") as f:
 | 
			
		||||
            json.dump(config, f)
 | 
			
		||||
 | 
			
		||||
        cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", mode]
 | 
			
		||||
        try:
 | 
			
		||||
            subprocess.run(cmd, capture_output=True, timeout=timeout)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(e)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,29 @@ class TestWinUpdateViews(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_get_winupdates(self):
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_run_update_scan(self, nats_cmd):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        url = f"/winupdate/{agent.pk}/runupdatescan/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "getwinupdates"}, wait=False)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_install_updates(self, nats_cmd):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        baker.make("winupdate.WinUpdate", agent=agent, _quantity=4)
 | 
			
		||||
        baker.make("winupdate.WinUpdatePolicy", agent=agent)
 | 
			
		||||
        url = f"/winupdate/{agent.pk}/installnow/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_once()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_winupdates(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        baker.make("winupdate.WinUpdate", agent=agent, _quantity=4)
 | 
			
		||||
 | 
			
		||||
@@ -27,8 +48,8 @@ class TestWinUpdateViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = UpdateSerializer(agent)
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data["winupdates"]), 4)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data["winupdates"]), 4)  # type: ignore
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -99,7 +120,7 @@ class TestWinUpdateViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.patch(url, invalid_data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"pk": winupdate.pk, "policy": "inherit"}
 | 
			
		||||
        data = {"pk": winupdate.pk, "policy": "inherit"}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,11 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, notify_error
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone
 | 
			
		||||
 | 
			
		||||
from .models import WinUpdate
 | 
			
		||||
from .serializers import UpdateSerializer
 | 
			
		||||
@@ -24,9 +23,6 @@ def get_win_updates(request, pk):
 | 
			
		||||
def run_update_scan(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    agent.delete_superseded_updates()
 | 
			
		||||
    if pyver.parse(agent.version) < pyver.parse("1.3.0"):
 | 
			
		||||
        return notify_error("Requires agent version 1.3.0 or greater")
 | 
			
		||||
 | 
			
		||||
    asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
@@ -35,9 +31,6 @@ def run_update_scan(request, pk):
 | 
			
		||||
def install_updates(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    agent.delete_superseded_updates()
 | 
			
		||||
    if pyver.parse(agent.version) < pyver.parse("1.3.0"):
 | 
			
		||||
        return notify_error("Requires agent version 1.3.0 or greater")
 | 
			
		||||
 | 
			
		||||
    agent.approve_updates()
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "installwinupdates",
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ jobs:
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd /myagent/_work/1/s/api/tacticalrmm
 | 
			
		||||
          pip install --no-cache-dir --upgrade pip
 | 
			
		||||
          pip install --no-cache-dir setuptools==53.0.0 wheel==0.36.2
 | 
			
		||||
          pip install --no-cache-dir setuptools==54.2.0 wheel==0.36.2
 | 
			
		||||
          pip install --no-cache-dir -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
 | 
			
		||||
        displayName: "Install Python Dependencies"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="11"
 | 
			
		||||
SCRIPT_VERSION="12"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
 | 
			
		||||
 | 
			
		||||
GREEN='\033[0;32m'
 | 
			
		||||
@@ -73,6 +73,9 @@ sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx .
 | 
			
		||||
sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
 | 
			
		||||
 | 
			
		||||
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/
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz
 | 
			
		||||
cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ FROM node:14-alpine AS builder
 | 
			
		||||
WORKDIR /home/node/app
 | 
			
		||||
 | 
			
		||||
COPY ./web/package.json .
 | 
			
		||||
RUN npm install -g npm
 | 
			
		||||
RUN npm install -g npm@latest
 | 
			
		||||
RUN npm install
 | 
			
		||||
 | 
			
		||||
COPY ./web .
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,14 @@
 | 
			
		||||
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
: "${DEV:=0}"
 | 
			
		||||
 | 
			
		||||
if [ "${DEV}" = 1 ]; then
 | 
			
		||||
  NATS_CONFIG=/workspace/api/tacticalrmm/nats-rmm.conf
 | 
			
		||||
else
 | 
			
		||||
  NATS_CONFIG="${TACTICAL_DIR}/api/nats-rmm.conf"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
sleep 15
 | 
			
		||||
until [ -f "${TACTICAL_READY_FILE}" ]; do
 | 
			
		||||
  echo "waiting for init container to finish install or update..."
 | 
			
		||||
 
 | 
			
		||||
@@ -63,8 +63,19 @@ server  {
 | 
			
		||||
        alias ${TACTICAL_DIR}/api/tacticalrmm/private/;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location ~ ^/(natsapi) {
 | 
			
		||||
        deny all;
 | 
			
		||||
    location ~ ^/ws/ {
 | 
			
		||||
        set \$api http://tactical-websockets:8383;
 | 
			
		||||
        proxy_pass \$api;
 | 
			
		||||
 | 
			
		||||
        proxy_http_version 1.1;
 | 
			
		||||
        proxy_set_header Upgrade \$http_upgrade;
 | 
			
		||||
        proxy_set_header Connection "upgrade";
 | 
			
		||||
 | 
			
		||||
        proxy_redirect     off;
 | 
			
		||||
        proxy_set_header   Host \$host;
 | 
			
		||||
        proxy_set_header   X-Real-IP \$remote_addr;
 | 
			
		||||
        proxy_set_header   X-Forwarded-For \$proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header   X-Forwarded-Host \$server_name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    error_log  /var/log/nginx/api-error.log;
 | 
			
		||||
 
 | 
			
		||||
@@ -62,4 +62,4 @@ ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
 | 
			
		||||
WORKDIR ${TACTICAL_DIR}/api
 | 
			
		||||
 | 
			
		||||
EXPOSE 80
 | 
			
		||||
EXPOSE 80 443 8383
 | 
			
		||||
 
 | 
			
		||||
@@ -29,14 +29,15 @@ function check_tactical_ready {
 | 
			
		||||
# tactical-init
 | 
			
		||||
if [ "$1" = 'tactical-init' ]; then
 | 
			
		||||
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/tmp
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/scripts/userdefined
 | 
			
		||||
 | 
			
		||||
  test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
 | 
			
		||||
 | 
			
		||||
  # copy container data to volume
 | 
			
		||||
  rsync -a --no-perms --no-owner --delete --exclude "tmp/*" --exclude "certs/*" --exclude="api/tacticalrmm/private/*" "${TACTICAL_TMP_DIR}/" "${TACTICAL_DIR}/"
 | 
			
		||||
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/tmp
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/logs
 | 
			
		||||
  
 | 
			
		||||
  until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
 | 
			
		||||
    echo "waiting for postgresql container to be ready..."
 | 
			
		||||
    sleep 5
 | 
			
		||||
@@ -62,6 +63,9 @@ DOCKER_BUILD = True
 | 
			
		||||
CERT_FILE = '/opt/tactical/certs/fullchain.pem'
 | 
			
		||||
KEY_FILE = '/opt/tactical/certs/privkey.pem'
 | 
			
		||||
 | 
			
		||||
EXE_DIR = '/opt/tactical/api/tacticalrmm/private/exe'
 | 
			
		||||
LOG_DIR = '/opt/tactical/api/tacticalrmm/private/log'
 | 
			
		||||
 | 
			
		||||
SCRIPTS_DIR = '/opt/tactical/scripts'
 | 
			
		||||
 | 
			
		||||
ALLOWED_HOSTS = ['${API_HOST}', 'tactical-backend']
 | 
			
		||||
@@ -164,3 +168,12 @@ if [ "$1" = 'tactical-celerybeat' ]; then
 | 
			
		||||
  test -f "${TACTICAL_DIR}/api/celerybeat.pid" && rm "${TACTICAL_DIR}/api/celerybeat.pid"
 | 
			
		||||
  celery -A tacticalrmm beat -l info
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# backend container
 | 
			
		||||
if [ "$1" = 'tactical-websockets' ]; then
 | 
			
		||||
  check_tactical_ready
 | 
			
		||||
 | 
			
		||||
  export DJANGO_SETTINGS_MODULE=tacticalrmm.settings
 | 
			
		||||
 | 
			
		||||
  daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
 | 
			
		||||
fi
 | 
			
		||||
@@ -22,6 +22,7 @@ volumes:
 | 
			
		||||
services:
 | 
			
		||||
  # postgres database for api service
 | 
			
		||||
  tactical-postgres:
 | 
			
		||||
    container_name: trmm-postgres
 | 
			
		||||
    image: postgres:13-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -35,6 +36,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # redis container for celery tasks
 | 
			
		||||
  tactical-redis:
 | 
			
		||||
    container_name: trmm-redis
 | 
			
		||||
    image: redis:6.0-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -42,6 +44,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # used to initialize the docker environment
 | 
			
		||||
  tactical-init:
 | 
			
		||||
    container_name: trmm-init
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    restart: on-failure
 | 
			
		||||
    command: ["tactical-init"]
 | 
			
		||||
@@ -65,6 +68,7 @@ services:
 | 
			
		||||
  
 | 
			
		||||
  # nats
 | 
			
		||||
  tactical-nats:
 | 
			
		||||
    container_name: trmm-nats
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nats:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -80,6 +84,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # meshcentral container
 | 
			
		||||
  tactical-meshcentral:
 | 
			
		||||
    container_name: trmm-meshcentral
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment: 
 | 
			
		||||
@@ -101,6 +106,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # mongodb container for meshcentral
 | 
			
		||||
  tactical-mongodb:
 | 
			
		||||
    container_name: trmm-mongodb
 | 
			
		||||
    image: mongo:4.4
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -114,6 +120,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container that hosts vue frontend
 | 
			
		||||
  tactical-frontend:
 | 
			
		||||
    container_name: trmm-frontend
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-frontend:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -123,6 +130,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for django backend
 | 
			
		||||
  tactical-backend:
 | 
			
		||||
    container_name: trmm-backend
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-backend"]
 | 
			
		||||
    restart: always
 | 
			
		||||
@@ -135,8 +143,25 @@ services:
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - tactical-postgres
 | 
			
		||||
 | 
			
		||||
  tactical-nginx:
 | 
			
		||||
  # container for django websockets connections
 | 
			
		||||
  tactical-websockets:
 | 
			
		||||
    container_name: trmm-websockets
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-websockets"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      - proxy
 | 
			
		||||
      - api-db
 | 
			
		||||
      - redis
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical_data:/opt/tactical
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - tactical-postgres
 | 
			
		||||
      - tactical-backend
 | 
			
		||||
 | 
			
		||||
  # container for tactical reverse proxy
 | 
			
		||||
  tactical-nginx:
 | 
			
		||||
    container_name: trmm-nginx
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nginx:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -156,6 +181,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery worker service
 | 
			
		||||
  tactical-celery:
 | 
			
		||||
    container_name: trmm-celery
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-celery"]
 | 
			
		||||
    restart: always
 | 
			
		||||
@@ -171,6 +197,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery beat service
 | 
			
		||||
  tactical-celerybeat:
 | 
			
		||||
    container_name: trmm-celerybeat
 | 
			
		||||
    image: ${IMAGE_REPO}tactical:${VERSION}
 | 
			
		||||
    command: ["tactical-celerybeat"]
 | 
			
		||||
    restart: always
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ A backup script is provided for quick and easy way to backup all settings into o
 | 
			
		||||
 | 
			
		||||
Download the backup script:
 | 
			
		||||
```bash
 | 
			
		||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
 | 
			
		||||
wget -N https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
From the Web UI, click **Tools > Server Maintenance**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										104
									
								
								docs/docs/contributing_using_devbox.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								docs/docs/contributing_using_devbox.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
 | 
			
		||||
Hidden docs, needs work
 | 
			
		||||
 | 
			
		||||
For local Hyper-v Devbox notes
 | 
			
		||||
 | 
			
		||||
From https://raw.githubusercontent.com/silversword411/tacticalrmm-devdocs
 | 
			
		||||
 | 
			
		||||
Needs an official install_devbox.sh script
 | 
			
		||||
 | 
			
		||||
# Setup local devbox in hyper-v VM
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Install Ubuntu 20.04 LTS
 | 
			
		||||
Don't forget to 
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo apt-get updates && sudo apt-get upgrade
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Optional
 | 
			
		||||
Set all users in sudo group not to require password every time:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo visudo
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Add this:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
%sudo ALL=(ALL) NOPASSWD: ALL
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Download customized install script and tweak
 | 
			
		||||
 | 
			
		||||
Create folder to dump into
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo mkdir /rmm
 | 
			
		||||
sudo chown ${USER}:${USER} -R /rmm
 | 
			
		||||
cd /rmm
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Get dev install script
 | 
			
		||||
```
 | 
			
		||||
wget https://raw.githubusercontent.com/silversword411/tacticalrmm-devdocs/blob/main/install_devbox.sh
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Edit, and search for `REPLACEMEWITHYOURFORKEDREPOURL`
 | 
			
		||||
 | 
			
		||||
and replace with your forked repo URL (example commented out below)
 | 
			
		||||
 | 
			
		||||
## Run it
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./install_devbox.sh
 | 
			
		||||
```
 | 
			
		||||
## Watch for
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
!!!Note Unlike regular installs, don't worry about the QR code
 | 
			
		||||
 | 
			
		||||
## Celebrate
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Misc commands
 | 
			
		||||
 | 
			
		||||
### Start mkdocs on dev box
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cd /rmm/api
 | 
			
		||||
source env/bin/activate
 | 
			
		||||
pip install --upgrade pip
 | 
			
		||||
pip install --upgrade setuptools wheel
 | 
			
		||||
pip install -r tacticalrmm/requirements-dev.txt
 | 
			
		||||
cd /rmm/docs
 | 
			
		||||
mkdocs serve
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Running tests locally
 | 
			
		||||
 | 
			
		||||
Prep and update
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
source /rmm/api/env/bin/activate
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
pip install -r requirements.txt
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then run tests
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
python manage.py test
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										97
									
								
								docs/docs/contributing_using_vscode.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								docs/docs/contributing_using_vscode.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### 1. Install vscode
 | 
			
		||||
[https://code.visualstudio.com/download](https://code.visualstudio.com/download)
 | 
			
		||||
 | 
			
		||||
### 2. Fork Project in Github
 | 
			
		||||
 | 
			
		||||
This is making a duplicate of the code under your Github that you can edit
 | 
			
		||||
 | 
			
		||||
[https://github.com/wh1te909/tacticalrmm](https://github.com/wh1te909/tacticalrmm)
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 3. Add your (forked) repo to vscode
 | 
			
		||||
 | 
			
		||||
Clone repository
 | 
			
		||||
 | 
			
		||||
Login to your Github
 | 
			
		||||
 | 
			
		||||
Choose local folder
 | 
			
		||||
 | 
			
		||||
#### 3a. Install extra vscode Extensions
 | 
			
		||||
 | 
			
		||||
GitLens
 | 
			
		||||
 | 
			
		||||
Remote - SSH
 | 
			
		||||
 | 
			
		||||
### 4. Open Terminal
 | 
			
		||||
 | 
			
		||||
[https://code.visualstudio.com/docs/editor/integrated-terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
Ctrl+`
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 5. Configure a remote for your fork (in vscode)
 | 
			
		||||
 | 
			
		||||
[https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork)
 | 
			
		||||
 | 
			
		||||
Configure your local fork and tell it where the original code repo is so you can compare and merge updates later when official repo is updated
 | 
			
		||||
 | 
			
		||||
Check repos
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git remote -v
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Add upstream repo
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git remote add upstream https://github.com/wh1te909/tacticalrmm
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Confirm changes
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git remote -v
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### 6. Contribute code
 | 
			
		||||
 | 
			
		||||
Make changes to something.
 | 
			
		||||
 | 
			
		||||
`Commit` (update something) and notate what you did
 | 
			
		||||
 | 
			
		||||
`Push` (from your local vscode to your github fork)
 | 
			
		||||
 | 
			
		||||
Open browser and look at your repo (It should reflect your commit)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#### 6a. Request your changes to be pulled into the primary repo (Pull Request)
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
In browser create pull request
 | 
			
		||||
 | 
			
		||||
### 7. Sync your local fork
 | 
			
		||||
 | 
			
		||||
[https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork)
 | 
			
		||||
 | 
			
		||||
Bring changes from original repo to your local vscode copy so you're current with changes made in original Github repo
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git pull --rebase upstream develop
 | 
			
		||||
```
 | 
			
		||||
#### 7a. Push your local updated copy to your Github fork
 | 
			
		||||
 | 
			
		||||
Then you're `push`ing that updated local repo to your online Github fork
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 8. Verify and Repeat
 | 
			
		||||
 | 
			
		||||
Check your Github fork in browser, should be up to date now with original. Repeat 6 or 7 as necessary
 | 
			
		||||
							
								
								
									
										74
									
								
								docs/docs/example_nginx.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								docs/docs/example_nginx.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
example of `/etc/nginx/sites-available/rmm.conf`
 | 
			
		||||
 | 
			
		||||
**DO NOT COPY PASTE INTO YOUR SERVER ONLY USE AS A REFERENCE**
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
server_tokens off;
 | 
			
		||||
 | 
			
		||||
upstream tacticalrmm {
 | 
			
		||||
    server unix:////rmm/api/tacticalrmm/tacticalrmm.sock;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
map $http_user_agent $ignore_ua {
 | 
			
		||||
    "~python-requests.*" 0;
 | 
			
		||||
    "~go-resty.*" 0;
 | 
			
		||||
    default 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
server {
 | 
			
		||||
    listen 80;
 | 
			
		||||
    server_name api.example.com;
 | 
			
		||||
    return 301 https://$server_name$request_uri;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
server {
 | 
			
		||||
    listen 443 ssl;
 | 
			
		||||
    server_name api.example.com;
 | 
			
		||||
    client_max_body_size 300M;
 | 
			
		||||
    access_log /rmm/api/tacticalrmm/tacticalrmm/private/log/access.log combined if=$ignore_ua;
 | 
			
		||||
    error_log /rmm/api/tacticalrmm/tacticalrmm/private/log/error.log;
 | 
			
		||||
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
 | 
			
		||||
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
 | 
			
		||||
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
 | 
			
		||||
 | 
			
		||||
    location /static/ {
 | 
			
		||||
        root /rmm/api/tacticalrmm;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location /private/ {
 | 
			
		||||
        internal;
 | 
			
		||||
        add_header "Access-Control-Allow-Origin" "https://rmm.example.com";
 | 
			
		||||
        alias /rmm/api/tacticalrmm/tacticalrmm/private/;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location ~ ^/(natsapi) {
 | 
			
		||||
        allow 127.0.0.1;
 | 
			
		||||
        deny all;
 | 
			
		||||
        uwsgi_pass tacticalrmm;
 | 
			
		||||
        include     /etc/nginx/uwsgi_params;
 | 
			
		||||
        uwsgi_read_timeout 500s;
 | 
			
		||||
        uwsgi_ignore_client_abort on;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location ~ ^/ws/ {
 | 
			
		||||
        proxy_pass http://unix:/rmm/daphne.sock;
 | 
			
		||||
 | 
			
		||||
        proxy_http_version 1.1;
 | 
			
		||||
        proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
        proxy_set_header Connection "upgrade";
 | 
			
		||||
 | 
			
		||||
        proxy_redirect     off;
 | 
			
		||||
        proxy_set_header   Host $host;
 | 
			
		||||
        proxy_set_header   X-Real-IP $remote_addr;
 | 
			
		||||
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header   X-Forwarded-Host $server_name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location / {
 | 
			
		||||
        uwsgi_pass  tacticalrmm;
 | 
			
		||||
        include     /etc/nginx/uwsgi_params;
 | 
			
		||||
        uwsgi_read_timeout 9999s;
 | 
			
		||||
        uwsgi_ignore_client_abort on;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										16
									
								
								docs/docs/functions/custom_fields.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								docs/docs/functions/custom_fields.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# Custom Fields
 | 
			
		||||
 | 
			
		||||
**Settings > Global Settings > Custom Fields**
 | 
			
		||||
 | 
			
		||||
v0.5.0 adds support for custom fields to be used in scripts.
 | 
			
		||||
 | 
			
		||||
It also exposes some pre-defined fields that are already in the database.
 | 
			
		||||
 | 
			
		||||
Please check the following video for examples until proper docs are written:
 | 
			
		||||
 | 
			
		||||
[https://www.youtube.com/watch?v=0-5jGGL3FOM](https://www.youtube.com/watch?v=0-5jGGL3FOM)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/trmm_contribute-notice.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/trmm_contribute-notice.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/trmm_need_sync_local_fork.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/trmm_need_sync_local_fork.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 23 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/trmm_vscode_git_pending.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/trmm_vscode_git_pending.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/docs/images/vscode-forkit.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/images/vscode-forkit.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 9.9 KiB  | 
@@ -43,11 +43,6 @@ python manage.py get_mesh_exe_url
 | 
			
		||||
 | 
			
		||||
#### Bulk update agent offline/overdue time
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
wget -q https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/api/tacticalrmm/agents/management/commands/bulk_change_checkin.py -O /rmm/api/tacticalrmm/agents/management/commands/bulk_change_checkin.py
 | 
			
		||||
```
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
Change offline time on all agents to 5 minutes
 | 
			
		||||
```bash
 | 
			
		||||
python manage.py bulk_change_checkin --offline --all 5
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ Please then copy/paste the logs and post them either in our [Discord support cha
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
#### Web UI frozen or not loading / website errors / general errors
 | 
			
		||||
#### All other errors
 | 
			
		||||
 | 
			
		||||
First, run the [update script](update_server.md#updating-to-the-latest-rmm-version) with the `--force` flag. <br/>This will fix permissions and reinstall python/node packages that might have gotten corrupted.
 | 
			
		||||
 | 
			
		||||
@@ -61,10 +61,15 @@ First, run the [update script](update_server.md#updating-to-the-latest-rmm-versi
 | 
			
		||||
./update.sh --force
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Check the debug log from the web UI: **File > Debug Log**
 | 
			
		||||
 | 
			
		||||
Open your browser's dev tools (ctrl + shift + j on chrome) and check the Console tab for any errors
 | 
			
		||||
 | 
			
		||||
Check all the systemd services that the rmm uses to function and check to make sure they're all active/running and enabled:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl status rmm
 | 
			
		||||
sudo systemctl status daphne
 | 
			
		||||
sudo systemctl status celery
 | 
			
		||||
sudo systemctl status celerybeat
 | 
			
		||||
sudo systemctl status nginx
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,6 @@
 | 
			
		||||
 | 
			
		||||
You should periodically run `sudo apt update` and `sudo apt -y upgrade` to keep your server up to date.
 | 
			
		||||
 | 
			
		||||
You can also update `npm` if prompted to by a message that might appear when running the `update.sh` script.
 | 
			
		||||
 | 
			
		||||
Other than this, you should avoid making any changes to your server and let the `update.sh` script handle everything else for you.
 | 
			
		||||
#### Updating to the latest RMM version
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ nav:
 | 
			
		||||
      - "Updating the RMM (Docker)": update_docker.md
 | 
			
		||||
      - "Updating Agents": update_agents.md
 | 
			
		||||
  - Functionality:
 | 
			
		||||
      - "Custom Fields": functions/custom_fields.md
 | 
			
		||||
      - "Remote Background": functions/remote_bg.md
 | 
			
		||||
      - "Maintenance Mode": functions/maintenance_mode.md
 | 
			
		||||
      - "Alerting": alerting.md
 | 
			
		||||
@@ -22,7 +23,9 @@ nav:
 | 
			
		||||
  - FAQ: faq.md
 | 
			
		||||
  - Management Commands: management_cmds.md
 | 
			
		||||
  - MeshCentral Integration: mesh_integration.md
 | 
			
		||||
  - Contributing: contributing.md
 | 
			
		||||
  - Contributing: 
 | 
			
		||||
      - "Contributing to Docs": contributing.md
 | 
			
		||||
      - "Contributing using VSCode": contributing_using_vscode.md
 | 
			
		||||
  - License: license.md
 | 
			
		||||
site_description: "A remote monitoring and management tool"
 | 
			
		||||
site_author: "wh1te909"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								install.sh
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="44"
 | 
			
		||||
SCRIPT_VERSION="46"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt install -y curl wget dirmngr gnupg lsb-release
 | 
			
		||||
@@ -132,31 +132,14 @@ CHECK_HOSTS=$(grep 127.0.1.1 /etc/hosts | grep "$rmmdomain" | grep "$meshdomain"
 | 
			
		||||
HAS_11=$(grep 127.0.1.1 /etc/hosts)
 | 
			
		||||
 | 
			
		||||
if ! [[ $CHECK_HOSTS ]]; then
 | 
			
		||||
    echo -ne "${GREEN}We need to append your 3 subdomains to the line starting with 127.0.1.1 in your hosts file.${NC}\n"
 | 
			
		||||
    until [[ $edithosts =~ (y|n) ]]; do
 | 
			
		||||
        echo -ne "${GREEN}Would you like me to do this for you? [y/n]${NC}: "
 | 
			
		||||
        read edithosts
 | 
			
		||||
    done
 | 
			
		||||
 | 
			
		||||
    if [[ $edithosts == "y" ]]; then
 | 
			
		||||
        if [[ $HAS_11 ]]; then
 | 
			
		||||
          sudo sed -i "/127.0.1.1/s/$/ ${rmmdomain} $frontenddomain $meshdomain/" /etc/hosts
 | 
			
		||||
        else
 | 
			
		||||
          echo "127.0.1.1 ${rmmdomain} $frontenddomain $meshdomain" | sudo tee --append /etc/hosts > /dev/null
 | 
			
		||||
        fi
 | 
			
		||||
    else
 | 
			
		||||
        if [[ $HAS_11 ]]; then
 | 
			
		||||
          echo -ne "${GREEN}Please manually edit your /etc/hosts file to match the line below and re-run this script.${NC}\n"
 | 
			
		||||
          sed "/127.0.1.1/s/$/ ${rmmdomain} $frontenddomain $meshdomain/" /etc/hosts | grep 127.0.1.1
 | 
			
		||||
        else
 | 
			
		||||
          echo -ne "\n${GREEN}Append the following line to your /etc/hosts file${NC}\n"
 | 
			
		||||
          echo "127.0.1.1 ${rmmdomain} $frontenddomain $meshdomain"
 | 
			
		||||
        fi
 | 
			
		||||
        exit 1
 | 
			
		||||
    fi
 | 
			
		||||
  print_green 'Adding subdomains to hosts file'
 | 
			
		||||
  if [[ $HAS_11 ]]; then
 | 
			
		||||
    sudo sed -i "/127.0.1.1/s/$/ ${rmmdomain} ${frontenddomain} ${meshdomain}/" /etc/hosts
 | 
			
		||||
  else
 | 
			
		||||
    echo "127.0.1.1 ${rmmdomain} ${frontenddomain} ${meshdomain}" | sudo tee --append /etc/hosts > /dev/null
 | 
			
		||||
  fi
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BEHIND_NAT=false
 | 
			
		||||
IPV4=$(ip -4 addr | sed -ne 's|^.* inet \([^/]*\)/.* scope global.*$|\1|p' | head -1)
 | 
			
		||||
if echo "$IPV4" | grep -qE '^(10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168)'; then
 | 
			
		||||
@@ -376,7 +359,7 @@ python3.9 -m venv env
 | 
			
		||||
source /rmm/api/env/bin/activate
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
pip install --no-cache-dir --upgrade pip
 | 
			
		||||
pip install --no-cache-dir setuptools==53.0.0 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir setuptools==54.2.0 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
 | 
			
		||||
python manage.py migrate
 | 
			
		||||
python manage.py collectstatic --no-input
 | 
			
		||||
@@ -444,6 +427,26 @@ EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${rmmservice}" | sudo tee /etc/systemd/system/rmm.service > /dev/null
 | 
			
		||||
 | 
			
		||||
daphneservice="$(cat << EOF
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=django channels daemon
 | 
			
		||||
After=network.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
User=${USER}
 | 
			
		||||
Group=www-data
 | 
			
		||||
WorkingDirectory=/rmm/api/tacticalrmm
 | 
			
		||||
Environment="PATH=/rmm/api/env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
 | 
			
		||||
ExecStart=/rmm/api/env/bin/daphne -u /rmm/daphne.sock tacticalrmm.asgi:application
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=3s
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
echo "${daphneservice}" | sudo tee /etc/systemd/system/daphne.service > /dev/null
 | 
			
		||||
 | 
			
		||||
natsservice="$(cat << EOF
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=NATS Server
 | 
			
		||||
@@ -514,6 +517,20 @@ server {
 | 
			
		||||
        uwsgi_ignore_client_abort on;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location ~ ^/ws/ {
 | 
			
		||||
        proxy_pass http://unix:/rmm/daphne.sock;
 | 
			
		||||
 | 
			
		||||
        proxy_http_version 1.1;
 | 
			
		||||
        proxy_set_header Upgrade \$http_upgrade;
 | 
			
		||||
        proxy_set_header Connection "upgrade";
 | 
			
		||||
 | 
			
		||||
        proxy_redirect     off;
 | 
			
		||||
        proxy_set_header   Host \$host;
 | 
			
		||||
        proxy_set_header   X-Real-IP \$remote_addr;
 | 
			
		||||
        proxy_set_header   X-Forwarded-For \$proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header   X-Forwarded-Host \$server_name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location / {
 | 
			
		||||
        uwsgi_pass  tacticalrmm;
 | 
			
		||||
        include     /etc/nginx/uwsgi_params;
 | 
			
		||||
@@ -713,7 +730,7 @@ sudo ln -s /etc/nginx/sites-available/frontend.conf /etc/nginx/sites-enabled/fro
 | 
			
		||||
 | 
			
		||||
print_green 'Enabling Services'
 | 
			
		||||
 | 
			
		||||
for i in rmm.service celery.service celerybeat.service nginx
 | 
			
		||||
for i in rmm.service daphne.service celery.service celerybeat.service nginx
 | 
			
		||||
do
 | 
			
		||||
  sudo systemctl enable ${i}
 | 
			
		||||
  sudo systemctl stop ${i}
 | 
			
		||||
@@ -783,7 +800,7 @@ sudo systemctl start nats.service
 | 
			
		||||
sed -i 's/ADMIN_ENABLED = True/ADMIN_ENABLED = False/g' /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
 | 
			
		||||
 | 
			
		||||
print_green 'Restarting services'
 | 
			
		||||
for i in rmm.service celery.service celerybeat.service
 | 
			
		||||
for i in rmm.service daphne.service celery.service celerybeat.service
 | 
			
		||||
do
 | 
			
		||||
  sudo systemctl stop ${i}
 | 
			
		||||
  sudo systemctl start ${i}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
SCRIPT_VERSION="22"
 | 
			
		||||
SCRIPT_VERSION="24"
 | 
			
		||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
 | 
			
		||||
 | 
			
		||||
sudo apt update
 | 
			
		||||
@@ -291,8 +291,9 @@ python3.9 -m venv env
 | 
			
		||||
source /rmm/api/env/bin/activate
 | 
			
		||||
cd /rmm/api/tacticalrmm
 | 
			
		||||
pip install --no-cache-dir --upgrade pip
 | 
			
		||||
pip install --no-cache-dir setuptools==53.0.0 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir setuptools==54.2.0 wheel==0.36.2
 | 
			
		||||
pip install --no-cache-dir -r /rmm/api/tacticalrmm/requirements.txt
 | 
			
		||||
python manage.py migrate
 | 
			
		||||
python manage.py collectstatic --no-input
 | 
			
		||||
python manage.py reload_nats
 | 
			
		||||
deactivate
 | 
			
		||||
@@ -323,7 +324,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 nginx
 | 
			
		||||
for i in celery.service celerybeat.service rmm.service daphne.service nginx
 | 
			
		||||
do
 | 
			
		||||
  sudo systemctl enable ${i}
 | 
			
		||||
  sudo systemctl stop ${i}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
# 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})
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
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
 | 
			
		||||
							
								
								
									
										2
									
								
								scripts/Win_ADDC_Sync_Start.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								scripts/Win_ADDC_Sync_Start.bat
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
net start ADSync
 | 
			
		||||
exit
 | 
			
		||||
							
								
								
									
										15
									
								
								scripts/Win_AzureAD_Check_Connection_Status.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								scripts/Win_AzureAD_Check_Connection_Status.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
$ErrorActionPreference = 'silentlycontinue'
 | 
			
		||||
$aadchk = dsregcmd /status | Where-Object { $_ -match 'AzureAdJoined : ' } | ForEach-Object { $_.Trim() }
 | 
			
		||||
 | 
			
		||||
if ($aadchk -Eq 'AzureAdJoined : Yes') {
 | 
			
		||||
    Write-Output "Machine is Azure Ad Joined"
 | 
			
		||||
    exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
else {
 | 
			
		||||
    Write-Output "Machine is not Azure Ad Joined"
 | 
			
		||||
    exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
							
								
								
									
										23
									
								
								scripts/Win_Azure_Mars_Cloud_Backup_Status.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								scripts/Win_Azure_Mars_Cloud_Backup_Status.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
$ErrorActionPreference= 'silentlycontinue'
 | 
			
		||||
$TimeSpan = (Get-Date) - (New-TimeSpan -Day 1)
 | 
			
		||||
 | 
			
		||||
##Check for Errors in Backup
 | 
			
		||||
if (Get-WinEvent -FilterHashtable @{LogName='CloudBackup/Operational';ID='11','18';StartTime=$TimeSpan}) 
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
Write-Host "Cloud Backup Mars Ended with Errors"
 | 
			
		||||
Get-WinEvent -FilterHashtable @{LogName='CloudBackup/Operational';ID='1','14','11','18','16';StartTime=$TimeSpan}
 | 
			
		||||
exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
else 
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
Write-Host "Cloud Backup Mars Backup Is Working Correctly"
 | 
			
		||||
Get-WinEvent -FilterHashtable @{LogName='CloudBackup/Operational';ID='1','14','16'}
 | 
			
		||||
exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
							
								
								
									
										1
									
								
								scripts/Win_Bitlocker_Get_Recovery_Keys.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scripts/Win_Bitlocker_Get_Recovery_Keys.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
manage-bde -protectors C: -get
 | 
			
		||||
@@ -13,8 +13,8 @@ exit 1
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
else 
 | 
			
		||||
Write-Output "No bluescreen events detected"
 | 
			
		||||
Write-Output "No bluescreen events detected in the past 24 hours."
 | 
			
		||||
exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
							
								
								
									
										1
									
								
								scripts/Win_Disable_AutoRun.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scripts/Win_Disable_AutoRun.bat
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" /v NoDriveTypeAutoRun /t REG_DWORD /d 255 /f
 | 
			
		||||
							
								
								
									
										1
									
								
								scripts/Win_Disable_Cortana.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scripts/Win_Disable_Cortana.bat
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
reg add  "hklm\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /d "AllowCortana"=dword:00000000
 | 
			
		||||
							
								
								
									
										19
									
								
								scripts/Win_Disk_Status.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								scripts/Win_Disk_Status.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +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
 | 
			
		||||
							
								
								
									
										1
									
								
								scripts/Win_FileSystem_Enable_Long_Paths.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scripts/Win_FileSystem_Enable_Long_Paths.bat
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
REG ADD "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" /V LongPathsEnabled /T REG_DWORD /D 1 /F
 | 
			
		||||
							
								
								
									
										5
									
								
								scripts/Win_Finish_updates_and_restart.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								scripts/Win_Finish_updates_and_restart.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# This script will force the computer to finish installing updates
 | 
			
		||||
# and restart. Normal restart doesn't install updates before issuing
 | 
			
		||||
# a reboot command. This one does.
 | 
			
		||||
 | 
			
		||||
Restart-Computer -ComputerName $env:COMPUTERNAME -Force 
 | 
			
		||||
							
								
								
									
										5
									
								
								scripts/Win_Finish_updates_and_shutdown.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								scripts/Win_Finish_updates_and_shutdown.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# This script will force the computer to finish installing updates
 | 
			
		||||
# and shutdown. Normal shutdown doesn't install updates before issuing
 | 
			
		||||
# a shutdown command. This one does.
 | 
			
		||||
 | 
			
		||||
Stop-Computer -ComputerName $env:COMPUTERNAME -Force
 | 
			
		||||
							
								
								
									
										17
									
								
								scripts/Win_Firewall_Check_Status.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								scripts/Win_Firewall_Check_Status.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
$ErrorActionPreference = 'silentlycontinue'
 | 
			
		||||
$fwenabled = (get-netfirewallprofile -policystore activestore).Enabled
 | 
			
		||||
 | 
			
		||||
if ($fwenabled.Contains('True')) {
 | 
			
		||||
    Write-Output "Firewall is Enabled"
 | 
			
		||||
    netsh advfirewall show currentprofile
 | 
			
		||||
    exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
else {
 | 
			
		||||
    Write-Host "Firewall is Disabled"
 | 
			
		||||
    exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Exit $LASTEXITCODE
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user