Compare commits
	
		
			71 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6ae2da22c1 | ||
| 
						 | 
					cef1ab9512 | ||
| 
						 | 
					94f02bfca3 | ||
| 
						 | 
					a941bb1744 | ||
| 
						 | 
					6ff591427a | ||
| 
						 | 
					809e172280 | ||
| 
						 | 
					17aedae0a9 | ||
| 
						 | 
					ef817ccb3a | ||
| 
						 | 
					0fb55b0bee | ||
| 
						 | 
					a1a6eddc31 | ||
| 
						 | 
					ff3d0b6b57 | ||
| 
						 | 
					dd64cef4c4 | ||
| 
						 | 
					9796848079 | ||
| 
						 | 
					fea7eb4312 | ||
| 
						 | 
					c12cd0e755 | ||
| 
						 | 
					d86a72f858 | ||
| 
						 | 
					50cd7f219a | ||
| 
						 | 
					8252b3eccc | ||
| 
						 | 
					d0c6e3a158 | ||
| 
						 | 
					1505fa547e | ||
| 
						 | 
					9017bad884 | ||
| 
						 | 
					2ac5e316a5 | ||
| 
						 | 
					29f9113062 | ||
| 
						 | 
					46349672d8 | ||
| 
						 | 
					4787be2db0 | ||
| 
						 | 
					f0a8c5d732 | ||
| 
						 | 
					9ad520bf7c | ||
| 
						 | 
					bd0cc51554 | ||
| 
						 | 
					12f599f974 | ||
| 
						 | 
					0118d5fb40 | ||
| 
						 | 
					65cadb311a | ||
| 
						 | 
					dd75bd197d | ||
| 
						 | 
					7e155bdb43 | ||
| 
						 | 
					993b6fddf4 | ||
| 
						 | 
					6ba51df6a7 | ||
| 
						 | 
					1185ac58e1 | ||
| 
						 | 
					f835997f49 | ||
| 
						 | 
					a597dba775 | ||
| 
						 | 
					3194e83a66 | ||
| 
						 | 
					096c3cdd34 | ||
| 
						 | 
					3a1ea42333 | ||
| 
						 | 
					64877d4299 | ||
| 
						 | 
					e957dc5e2c | ||
| 
						 | 
					578d5c5830 | ||
| 
						 | 
					96284f9508 | ||
| 
						 | 
					698b38dcba | ||
| 
						 | 
					6db826befe | ||
| 
						 | 
					1a3d412d73 | ||
| 
						 | 
					b8461c9dd8 | ||
| 
						 | 
					699bd9de10 | ||
| 
						 | 
					54b6866e21 | ||
| 
						 | 
					afd155e9c1 | ||
| 
						 | 
					910a717230 | ||
| 
						 | 
					70fbd33d61 | ||
| 
						 | 
					2da0d5ee21 | ||
| 
						 | 
					98f64e057a | ||
| 
						 | 
					3d9d936c56 | ||
| 
						 | 
					2b4cb59df8 | ||
| 
						 | 
					9d80da52e3 | ||
| 
						 | 
					fd176d2c64 | ||
| 
						 | 
					538b6de36b | ||
| 
						 | 
					f7eca8aee0 | ||
| 
						 | 
					a754d94c2c | ||
| 
						 | 
					5e3493e6a9 | ||
| 
						 | 
					619a14c26b | ||
| 
						 | 
					7d9a8decf0 | ||
| 
						 | 
					d11e14ad89 | ||
| 
						 | 
					69189cf2af | ||
| 
						 | 
					6e7d2f19d2 | ||
| 
						 | 
					d99ebf5d6a | ||
| 
						 | 
					ef2d19e95b | 
							
								
								
									
										46
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,23 +10,36 @@ on:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: self-hosted
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    name: Tests
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version: ['3.10.4']
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - name: Setup virtual env and install requirements
 | 
			
		||||
      - uses: harmon758/postgresql-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          postgresql version: '14'
 | 
			
		||||
          postgresql db: 'pipeline'
 | 
			
		||||
          postgresql user: 'pipeline'
 | 
			
		||||
          postgresql password: 'pipeline123456'
 | 
			
		||||
      
 | 
			
		||||
      - name: Setup Python ${{ matrix.python-version }}
 | 
			
		||||
        uses: actions/setup-python@v3
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ matrix.python-version }}
 | 
			
		||||
 | 
			
		||||
      - name: Install redis
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt update
 | 
			
		||||
          sudo apt install -y redis
 | 
			
		||||
          redis-server --version
 | 
			
		||||
 | 
			
		||||
      - name: Install requirements
 | 
			
		||||
        working-directory: api/tacticalrmm
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'CREATE DATABASE pipeline'
 | 
			
		||||
          sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline
 | 
			
		||||
          pwd
 | 
			
		||||
          rm -rf /actions-runner/_work/trmm-actions/trmm-actions/api/env
 | 
			
		||||
          cd api
 | 
			
		||||
          python3.10 -m venv env
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd tacticalrmm
 | 
			
		||||
          python --version
 | 
			
		||||
          SETTINGS_FILE="tacticalrmm/settings.py"
 | 
			
		||||
          SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
 | 
			
		||||
@@ -38,19 +51,16 @@ jobs:
 | 
			
		||||
      - name: Run django tests
 | 
			
		||||
        env:
 | 
			
		||||
          GHACTIONS: "yes"
 | 
			
		||||
        working-directory: api/tacticalrmm
 | 
			
		||||
        run: |
 | 
			
		||||
          cd api/tacticalrmm
 | 
			
		||||
          source ../env/bin/activate
 | 
			
		||||
          rm -f .coverage coverage.lcov
 | 
			
		||||
          pytest
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Codestyle black
 | 
			
		||||
        working-directory: api/tacticalrmm
 | 
			
		||||
        run: |
 | 
			
		||||
          cd api
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          black --exclude migrations/ --check tacticalrmm
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
name: Frontend Linting and Formatting
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [develop]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [develop]
 | 
			
		||||
 | 
			
		||||
defaults:
 | 
			
		||||
  run:
 | 
			
		||||
    working-directory: web
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 16
 | 
			
		||||
      - run: npm install
 | 
			
		||||
 | 
			
		||||
      - name: Run Prettier formatting
 | 
			
		||||
        run: npm run format
 | 
			
		||||
 | 
			
		||||
      - name: Run ESLint
 | 
			
		||||
        run: npm run lint -- --max-warnings=0
 | 
			
		||||
							
								
								
									
										23
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": [
 | 
			
		||||
    // frontend
 | 
			
		||||
    "dbaeumer.vscode-eslint",
 | 
			
		||||
    "esbenp.prettier-vscode",
 | 
			
		||||
    "editorconfig.editorconfig",
 | 
			
		||||
    "vue.volar",
 | 
			
		||||
    "wayou.vscode-todo-highlight",
 | 
			
		||||
 | 
			
		||||
    // python
 | 
			
		||||
    "matangover.mypy",
 | 
			
		||||
    "ms-python.python",
 | 
			
		||||
 | 
			
		||||
    // golang
 | 
			
		||||
    "golang.go"
 | 
			
		||||
  ],
 | 
			
		||||
  "unwantedRecommendations": [
 | 
			
		||||
    "octref.vetur",
 | 
			
		||||
    "hookyqr.beautify",
 | 
			
		||||
    "dbaeumer.jshint",
 | 
			
		||||
    "ms-vscode.vscode-typescript-tslint-plugin"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										55
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,17 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  "python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
 | 
			
		||||
  "python.languageServer": "Pylance",
 | 
			
		||||
    "python.analysis.extraPaths": [
 | 
			
		||||
        "api/tacticalrmm",
 | 
			
		||||
        "api/env",
 | 
			
		||||
    ],
 | 
			
		||||
  "python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
 | 
			
		||||
  "python.analysis.diagnosticSeverityOverrides": {
 | 
			
		||||
    "reportUnusedImport": "error",
 | 
			
		||||
    "reportDuplicateImport": "error",
 | 
			
		||||
    "reportGeneralTypeIssues": "none"
 | 
			
		||||
  },
 | 
			
		||||
  "python.analysis.typeCheckingMode": "basic",
 | 
			
		||||
    "mypy.runUsingActiveInterpreter": true,
 | 
			
		||||
  "python.linting.enabled": true,
 | 
			
		||||
  "python.linting.mypyEnabled": true,
 | 
			
		||||
  "python.linting.mypyArgs": [
 | 
			
		||||
@@ -20,20 +16,23 @@
 | 
			
		||||
    "--show-column-numbers",
 | 
			
		||||
    "--strict"
 | 
			
		||||
  ],
 | 
			
		||||
  "python.linting.ignorePatterns": [
 | 
			
		||||
    "**/site-packages/**/*.py",
 | 
			
		||||
    ".vscode/*.py",
 | 
			
		||||
    "**env/**"
 | 
			
		||||
  ],
 | 
			
		||||
  "python.formatting.provider": "black",
 | 
			
		||||
  "mypy.targets": ["api/tacticalrmm"],
 | 
			
		||||
  "mypy.runUsingActiveInterpreter": true,
 | 
			
		||||
  "editor.bracketPairColorization.enabled": true,
 | 
			
		||||
  "editor.guides.bracketPairs": true,
 | 
			
		||||
  "editor.formatOnSave": true,
 | 
			
		||||
    "vetur.format.defaultFormatter.js": "prettier",
 | 
			
		||||
    "vetur.format.defaultFormatterOptions": {
 | 
			
		||||
        "prettier": {
 | 
			
		||||
            "semi": true,
 | 
			
		||||
            "printWidth": 120,
 | 
			
		||||
            "tabWidth": 2,
 | 
			
		||||
            "useTabs": false,
 | 
			
		||||
            "arrowParens": "avoid",
 | 
			
		||||
        }
 | 
			
		||||
  "[vue][javascript][typescript][javascriptreact]": {
 | 
			
		||||
    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
			
		||||
    "editor.codeActionsOnSave": ["source.fixAll.eslint"],
 | 
			
		||||
  },
 | 
			
		||||
    "vetur.format.options.tabSize": 2,
 | 
			
		||||
    "vetur.format.options.useTabs": false,
 | 
			
		||||
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
 | 
			
		||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
			
		||||
  "files.watcherExclude": {
 | 
			
		||||
    "files.watcherExclude": {
 | 
			
		||||
      "**/.git/objects/**": true,
 | 
			
		||||
@@ -54,33 +53,23 @@
 | 
			
		||||
      "**/*.parquet*": true,
 | 
			
		||||
      "**/*.pyc": true,
 | 
			
		||||
      "**/*.zip": true
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "go.useLanguageServer": true,
 | 
			
		||||
  "[go]": {
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
    "editor.codeActionsOnSave": {
 | 
			
		||||
            "source.organizeImports": false,
 | 
			
		||||
      "source.organizeImports": false
 | 
			
		||||
    },
 | 
			
		||||
        "editor.snippetSuggestions": "none",
 | 
			
		||||
    "editor.snippetSuggestions": "none"
 | 
			
		||||
  },
 | 
			
		||||
  "[go.mod]": {
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
    "editor.codeActionsOnSave": {
 | 
			
		||||
            "source.organizeImports": true,
 | 
			
		||||
        },
 | 
			
		||||
      "source.organizeImports": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "gopls": {
 | 
			
		||||
    "usePlaceholders": true,
 | 
			
		||||
    "completeUnimported": true,
 | 
			
		||||
        "staticcheck": true,
 | 
			
		||||
    },
 | 
			
		||||
    "mypy.targets": [
 | 
			
		||||
        "api/tacticalrmm"
 | 
			
		||||
    ],
 | 
			
		||||
    "python.linting.ignorePatterns": [
 | 
			
		||||
        "**/site-packages/**/*.py",
 | 
			
		||||
        ".vscode/*.py",
 | 
			
		||||
        "**env/**"
 | 
			
		||||
    ]
 | 
			
		||||
    "staticcheck": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\
 | 
			
		||||
It uses an [agent](https://github.com/amidaware/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
 | 
			
		||||
 | 
			
		||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
 | 
			
		||||
# [LIVE DEMO](https://demo.tacticalrmm.com/)
 | 
			
		||||
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
 | 
			
		||||
 | 
			
		||||
### [Discord Chat](https://discord.gg/upGTkWp)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ omit =
 | 
			
		||||
    */urls.py
 | 
			
		||||
    */tests.py
 | 
			
		||||
    */test.py
 | 
			
		||||
    */tests/*
 | 
			
		||||
    checks/utils.py
 | 
			
		||||
    */asgi.py
 | 
			
		||||
    */demo_views.py
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,23 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Creates the installer user"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
    def handle(self, *args, **kwargs):  # type: ignore
 | 
			
		||||
        self.stdout.write("Checking if installer user has been created...")
 | 
			
		||||
        if User.objects.filter(is_installer_user=True).exists():
 | 
			
		||||
            self.stdout.write("Installer user already exists")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        User.objects.create_user(  # type: ignore
 | 
			
		||||
        User.objects.create_user(
 | 
			
		||||
            username=uuid.uuid4().hex,
 | 
			
		||||
            is_installer_user=True,
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
            password=User.objects.make_random_password(60),
 | 
			
		||||
            block_dashboard_login=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write("Installer user has been created")
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from knox.models import AuthToken
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Deletes all knox web tokens"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
    def handle(self, *args, **kwargs):  # type: ignore
 | 
			
		||||
        # only delete web tokens, not any generated by the installer or deployments
 | 
			
		||||
        dont_delete = djangotime.now() + djangotime.timedelta(hours=23)
 | 
			
		||||
        tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import pyotp
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Generates barcode for Authenticator and creates totp for user"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,10 @@ import os
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import pyotp
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Reset 2fa"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Reset password for user"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,17 @@
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, DateTimeField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import ROLE_CACHE_PREFIX
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, DateTimeField
 | 
			
		||||
 | 
			
		||||
AGENT_DBLCLICK_CHOICES = [
 | 
			
		||||
    ("editagent", "Edit Agent"),
 | 
			
		||||
    ("takecontrol", "Take Control"),
 | 
			
		||||
    ("remotebg", "Remote Background"),
 | 
			
		||||
    ("urlaction", "URL Action"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AGENT_TBL_TAB_CHOICES = [
 | 
			
		||||
    ("server", "Servers"),
 | 
			
		||||
    ("workstation", "Workstations"),
 | 
			
		||||
    ("mixed", "Mixed"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CLIENT_TREE_SORT_CHOICES = [
 | 
			
		||||
    ("alphafail", "Move failing clients to the top"),
 | 
			
		||||
    ("alpha", "Sort alphabetically"),
 | 
			
		||||
]
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    ROLE_CACHE_PREFIX,
 | 
			
		||||
    AgentDblClick,
 | 
			
		||||
    AgentTableTabs,
 | 
			
		||||
    ClientTreeSort,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
@@ -34,7 +21,7 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    dark_mode = models.BooleanField(default=True)
 | 
			
		||||
    show_community_scripts = models.BooleanField(default=True)
 | 
			
		||||
    agent_dblclick_action = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
 | 
			
		||||
        max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT
 | 
			
		||||
    )
 | 
			
		||||
    url_action = models.ForeignKey(
 | 
			
		||||
        "core.URLAction",
 | 
			
		||||
@@ -44,11 +31,11 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    default_agent_tbl_tab = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
 | 
			
		||||
        max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.SERVER
 | 
			
		||||
    )
 | 
			
		||||
    agents_per_page = models.PositiveIntegerField(default=50)  # not currently used
 | 
			
		||||
    client_tree_sort = models.CharField(
 | 
			
		||||
        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
			
		||||
        max_length=50, choices=ClientTreeSort.choices, default=ClientTreeSort.ALPHA_FAIL
 | 
			
		||||
    )
 | 
			
		||||
    client_tree_splitter = models.PositiveIntegerField(default=11)
 | 
			
		||||
    loading_bar_color = models.CharField(max_length=255, default="red")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from accounts.models import APIKey, User
 | 
			
		||||
from accounts.serializers import APIKeySerializer
 | 
			
		||||
from django.test import override_settings
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from accounts.models import APIKey, User
 | 
			
		||||
from accounts.serializers import APIKeySerializer
 | 
			
		||||
from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -283,9 +284,9 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "dark_mode": True,
 | 
			
		||||
            "show_community_scripts": True,
 | 
			
		||||
            "agent_dblclick_action": "editagent",
 | 
			
		||||
            "default_agent_tbl_tab": "mixed",
 | 
			
		||||
            "client_tree_sort": "alpha",
 | 
			
		||||
            "agent_dblclick_action": AgentDblClick.EDIT_AGENT,
 | 
			
		||||
            "default_agent_tbl_tab": AgentTableTabs.MIXED,
 | 
			
		||||
            "client_tree_sort": ClientTreeSort.ALPHA,
 | 
			
		||||
            "client_tree_splitter": 14,
 | 
			
		||||
            "loading_bar_color": "green",
 | 
			
		||||
            "clear_search_when_switching": False,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,13 +5,13 @@ from django.db import IntegrityError
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
from knox.views import LoginView as KnoxLoginView
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import APIKey, Role, User
 | 
			
		||||
from .permissions import AccountsPerms, APIKeyPerms, RolesPerms
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from tacticalrmm.utils import reload_nats
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
# import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "stuff for demo site in cron"
 | 
			
		||||
@@ -23,14 +24,6 @@ class Command(BaseCommand):
 | 
			
		||||
            rand = now - djangotime.timedelta(minutes=random.randint(10, 20))
 | 
			
		||||
            random_dates.append(rand)
 | 
			
		||||
 | 
			
		||||
        """ for _ in range(5):
 | 
			
		||||
            rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10))
 | 
			
		||||
            random_dates.append(rand)
 | 
			
		||||
 | 
			
		||||
        for _ in range(5):
 | 
			
		||||
            rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90))
 | 
			
		||||
            random_dates.append(rand) """
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.only("last_seen")
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            agent.last_seen = random.choice(random_dates)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,30 +3,48 @@ import json
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from checks.models import Check, CheckResult, CheckHistory
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from checks.models import Check, CheckHistory, CheckResult
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from logs.models import AuditLog, PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    CheckType,
 | 
			
		||||
    EvtLogFailWhen,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    EvtLogTypes,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    ScriptShell,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.demo_data import (
 | 
			
		||||
    check_network_loc_aware_ps1,
 | 
			
		||||
    check_storage_pool_health_ps1,
 | 
			
		||||
    clear_print_spool_bat,
 | 
			
		||||
    disks,
 | 
			
		||||
    disks_linux_deb,
 | 
			
		||||
    disks_linux_pi,
 | 
			
		||||
    ping_fail_output,
 | 
			
		||||
    ping_success_output,
 | 
			
		||||
    restart_nla_ps1,
 | 
			
		||||
    show_temp_dir_py,
 | 
			
		||||
    spooler_stdout,
 | 
			
		||||
    temp_dir_stdout,
 | 
			
		||||
    wmi_deb,
 | 
			
		||||
    wmi_pi,
 | 
			
		||||
)
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
AGENTS_TO_GENERATE = 250
 | 
			
		||||
AGENTS_TO_GENERATE = 20
 | 
			
		||||
 | 
			
		||||
SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
 | 
			
		||||
WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json")
 | 
			
		||||
@@ -43,19 +61,19 @@ EVT_LOG_FAIL = settings.BASE_DIR.joinpath(
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "populate database with fake agents"
 | 
			
		||||
 | 
			
		||||
    def rand_string(self, length):
 | 
			
		||||
    def rand_string(self, length: int) -> str:
 | 
			
		||||
        chars = string.ascii_letters
 | 
			
		||||
        return "".join(random.choice(chars) for _ in range(length))
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
    def handle(self, *args, **kwargs) -> None:
 | 
			
		||||
 | 
			
		||||
        user = User.objects.first()
 | 
			
		||||
        if user:
 | 
			
		||||
            user.totp_key = "ABSA234234"
 | 
			
		||||
            user.save(update_fields=["totp_key"])
 | 
			
		||||
 | 
			
		||||
        Client.objects.all().delete()
 | 
			
		||||
        Agent.objects.all().delete()
 | 
			
		||||
        Client.objects.all().delete()
 | 
			
		||||
        Check.objects.all().delete()
 | 
			
		||||
        Script.objects.all().delete()
 | 
			
		||||
        AutomatedTask.objects.all().delete()
 | 
			
		||||
@@ -65,6 +83,9 @@ class Command(BaseCommand):
 | 
			
		||||
        PendingAction.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
        call_command("load_community_scripts")
 | 
			
		||||
        call_command("initial_db_setup")
 | 
			
		||||
        call_command("load_chocos")
 | 
			
		||||
        call_command("create_installer_user")
 | 
			
		||||
 | 
			
		||||
        # policies
 | 
			
		||||
        check_policy = Policy()
 | 
			
		||||
@@ -95,27 +116,27 @@ class Command(BaseCommand):
 | 
			
		||||
        update_policy.email_if_fail = True
 | 
			
		||||
        update_policy.save()
 | 
			
		||||
 | 
			
		||||
        clients = [
 | 
			
		||||
        clients = (
 | 
			
		||||
            "Company 1",
 | 
			
		||||
            "Company 2",
 | 
			
		||||
            "Company 3",
 | 
			
		||||
            "Company 1",
 | 
			
		||||
            "Company 4",
 | 
			
		||||
            "Company 5",
 | 
			
		||||
            "Company 6",
 | 
			
		||||
        ]
 | 
			
		||||
        sites1 = ["HQ1", "LA Office 1", "NY Office 1"]
 | 
			
		||||
        sites2 = ["HQ2", "LA Office 2", "NY Office 2"]
 | 
			
		||||
        sites3 = ["HQ3", "LA Office 3", "NY Office 3"]
 | 
			
		||||
        sites4 = ["HQ4", "LA Office 4", "NY Office 4"]
 | 
			
		||||
        sites5 = ["HQ5", "LA Office 5", "NY Office 5"]
 | 
			
		||||
        sites6 = ["HQ6", "LA Office 6", "NY Office 6"]
 | 
			
		||||
        )
 | 
			
		||||
        sites1 = ("HQ1", "LA Office 1", "NY Office 1")
 | 
			
		||||
        sites2 = ("HQ2", "LA Office 2", "NY Office 2")
 | 
			
		||||
        sites3 = ("HQ3", "LA Office 3", "NY Office 3")
 | 
			
		||||
        sites4 = ("HQ4", "LA Office 4", "NY Office 4")
 | 
			
		||||
        sites5 = ("HQ5", "LA Office 5", "NY Office 5")
 | 
			
		||||
        sites6 = ("HQ6", "LA Office 6", "NY Office 6")
 | 
			
		||||
 | 
			
		||||
        client1 = Client(name="Company 1")
 | 
			
		||||
        client2 = Client(name="Company 2")
 | 
			
		||||
        client3 = Client(name="Company 3")
 | 
			
		||||
        client4 = Client(name="Company 4")
 | 
			
		||||
        client5 = Client(name="Company 5")
 | 
			
		||||
        client6 = Client(name="Company 6")
 | 
			
		||||
        client1 = Client(name=clients[0])
 | 
			
		||||
        client2 = Client(name=clients[1])
 | 
			
		||||
        client3 = Client(name=clients[2])
 | 
			
		||||
        client4 = Client(name=clients[3])
 | 
			
		||||
        client5 = Client(name=clients[4])
 | 
			
		||||
        client6 = Client(name=clients[5])
 | 
			
		||||
 | 
			
		||||
        client1.save()
 | 
			
		||||
        client2.save()
 | 
			
		||||
@@ -142,7 +163,7 @@ class Command(BaseCommand):
 | 
			
		||||
        for site in sites6:
 | 
			
		||||
            Site(client=client6, name=site).save()
 | 
			
		||||
 | 
			
		||||
        hostnames = [
 | 
			
		||||
        hostnames = (
 | 
			
		||||
            "DC-1",
 | 
			
		||||
            "DC-2",
 | 
			
		||||
            "FSV-1",
 | 
			
		||||
@@ -150,26 +171,30 @@ class Command(BaseCommand):
 | 
			
		||||
            "WSUS",
 | 
			
		||||
            "DESKTOP-12345",
 | 
			
		||||
            "LAPTOP-55443",
 | 
			
		||||
        ]
 | 
			
		||||
        descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"]
 | 
			
		||||
        modes = ["server", "workstation"]
 | 
			
		||||
        op_systems_servers = [
 | 
			
		||||
        )
 | 
			
		||||
        descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop")
 | 
			
		||||
        modes = ("server", "workstation")
 | 
			
		||||
        op_systems_servers = (
 | 
			
		||||
            "Microsoft Windows Server 2016 Standard, 64bit (build 14393)",
 | 
			
		||||
            "Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)",
 | 
			
		||||
            "Microsoft Windows Server 2019 Standard, 64bit (build 17763)",
 | 
			
		||||
        ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        op_systems_workstations = [
 | 
			
		||||
        op_systems_workstations = (
 | 
			
		||||
            "Microsoft Windows 8.1 Pro, 64bit (build 9600)",
 | 
			
		||||
            "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)",
 | 
			
		||||
            "Microsoft Windows 10 Pro, 64bit (build 18363)",
 | 
			
		||||
        ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"]
 | 
			
		||||
        linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64"
 | 
			
		||||
        linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+"
 | 
			
		||||
 | 
			
		||||
        total_rams = [4, 8, 16, 32, 64, 128]
 | 
			
		||||
        public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45")
 | 
			
		||||
 | 
			
		||||
        total_rams = (4, 8, 16, 32, 64, 128)
 | 
			
		||||
 | 
			
		||||
        now = dt.datetime.now()
 | 
			
		||||
        django_now = djangotime.now()
 | 
			
		||||
 | 
			
		||||
        boot_times = []
 | 
			
		||||
 | 
			
		||||
@@ -181,7 +206,7 @@ class Command(BaseCommand):
 | 
			
		||||
            rand_days = now - dt.timedelta(days=random.randint(2, 50))
 | 
			
		||||
            boot_times.append(str(rand_days.timestamp()))
 | 
			
		||||
 | 
			
		||||
        user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"]
 | 
			
		||||
        user_names = ("None", "Karen", "Steve", "jsmith", "jdoe")
 | 
			
		||||
 | 
			
		||||
        with open(SVCS) as f:
 | 
			
		||||
            services = json.load(f)
 | 
			
		||||
@@ -196,10 +221,7 @@ class Command(BaseCommand):
 | 
			
		||||
        with open(WMI_3) as f:
 | 
			
		||||
            wmi3 = json.load(f)
 | 
			
		||||
 | 
			
		||||
        wmi_details = []
 | 
			
		||||
        wmi_details.append(wmi1)
 | 
			
		||||
        wmi_details.append(wmi2)
 | 
			
		||||
        wmi_details.append(wmi3)
 | 
			
		||||
        wmi_details = [i for i in (wmi1, wmi2, wmi3)]
 | 
			
		||||
 | 
			
		||||
        # software
 | 
			
		||||
        with open(SW_1) as f:
 | 
			
		||||
@@ -208,9 +230,7 @@ class Command(BaseCommand):
 | 
			
		||||
        with open(SW_2) as f:
 | 
			
		||||
            software2 = json.load(f)
 | 
			
		||||
 | 
			
		||||
        softwares = []
 | 
			
		||||
        softwares.append(software1)
 | 
			
		||||
        softwares.append(software2)
 | 
			
		||||
        softwares = [i for i in (software1, software2)]
 | 
			
		||||
 | 
			
		||||
        # windows updates
 | 
			
		||||
        with open(WIN_UPDATES) as f:
 | 
			
		||||
@@ -226,59 +246,83 @@ class Command(BaseCommand):
 | 
			
		||||
        clear_spool.name = "Clear Print Spooler"
 | 
			
		||||
        clear_spool.description = "clears the print spooler. Fuck printers"
 | 
			
		||||
        clear_spool.filename = "clear_print_spool.bat"
 | 
			
		||||
        clear_spool.shell = "cmd"
 | 
			
		||||
        clear_spool.shell = ScriptShell.CMD
 | 
			
		||||
        clear_spool.script_body = clear_print_spool_bat
 | 
			
		||||
        clear_spool.save()
 | 
			
		||||
 | 
			
		||||
        check_net_aware = Script()
 | 
			
		||||
        check_net_aware.name = "Check Network Location Awareness"
 | 
			
		||||
        check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails."
 | 
			
		||||
        check_net_aware.filename = "check_network_loc_aware.ps1"
 | 
			
		||||
        check_net_aware.shell = "powershell"
 | 
			
		||||
        check_net_aware.shell = ScriptShell.POWERSHELL
 | 
			
		||||
        check_net_aware.script_body = check_network_loc_aware_ps1
 | 
			
		||||
        check_net_aware.save()
 | 
			
		||||
 | 
			
		||||
        check_pool_health = Script()
 | 
			
		||||
        check_pool_health.name = "Check storage spool health"
 | 
			
		||||
        check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy"
 | 
			
		||||
        check_pool_health.filename = "check_storage_pool_health.ps1"
 | 
			
		||||
        check_pool_health.shell = "powershell"
 | 
			
		||||
        check_pool_health.shell = ScriptShell.POWERSHELL
 | 
			
		||||
        check_pool_health.script_body = check_storage_pool_health_ps1
 | 
			
		||||
        check_pool_health.save()
 | 
			
		||||
 | 
			
		||||
        restart_nla = Script()
 | 
			
		||||
        restart_nla.name = "Restart NLA Service"
 | 
			
		||||
        restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails"
 | 
			
		||||
        restart_nla.filename = "restart_nla.ps1"
 | 
			
		||||
        restart_nla.shell = "powershell"
 | 
			
		||||
        restart_nla.shell = ScriptShell.POWERSHELL
 | 
			
		||||
        restart_nla.script_body = restart_nla_ps1
 | 
			
		||||
        restart_nla.save()
 | 
			
		||||
 | 
			
		||||
        show_tmp_dir_script = Script()
 | 
			
		||||
        show_tmp_dir_script.name = "Check temp dir"
 | 
			
		||||
        show_tmp_dir_script.description = "shows files in temp dir using python"
 | 
			
		||||
        show_tmp_dir_script.filename = "show_temp_dir.py"
 | 
			
		||||
        show_tmp_dir_script.shell = "python"
 | 
			
		||||
        show_tmp_dir_script.shell = ScriptShell.PYTHON
 | 
			
		||||
        show_tmp_dir_script.script_body = show_temp_dir_py
 | 
			
		||||
        show_tmp_dir_script.save()
 | 
			
		||||
 | 
			
		||||
        for count_agents in range(AGENTS_TO_GENERATE):
 | 
			
		||||
 | 
			
		||||
            client = random.choice(clients)
 | 
			
		||||
 | 
			
		||||
            if client == "Company 1":
 | 
			
		||||
            if client == clients[0]:
 | 
			
		||||
                site = random.choice(sites1)
 | 
			
		||||
            elif client == "Company 2":
 | 
			
		||||
            elif client == clients[1]:
 | 
			
		||||
                site = random.choice(sites2)
 | 
			
		||||
            elif client == "Company 3":
 | 
			
		||||
            elif client == clients[2]:
 | 
			
		||||
                site = random.choice(sites3)
 | 
			
		||||
            elif client == "Company 4":
 | 
			
		||||
            elif client == clients[3]:
 | 
			
		||||
                site = random.choice(sites4)
 | 
			
		||||
            elif client == "Company 5":
 | 
			
		||||
            elif client == clients[4]:
 | 
			
		||||
                site = random.choice(sites5)
 | 
			
		||||
            elif client == "Company 6":
 | 
			
		||||
            elif client == clients[5]:
 | 
			
		||||
                site = random.choice(sites6)
 | 
			
		||||
            else:
 | 
			
		||||
                site = None
 | 
			
		||||
 | 
			
		||||
            agent = Agent()
 | 
			
		||||
 | 
			
		||||
            plat_pick = random.randint(1, 15)
 | 
			
		||||
            if plat_pick in (7, 11):
 | 
			
		||||
                agent.plat = "linux"
 | 
			
		||||
                mode = "server"
 | 
			
		||||
                # pi arm
 | 
			
		||||
                if plat_pick == 7:
 | 
			
		||||
                    agent.goarch = "arm"
 | 
			
		||||
                    agent.wmi_detail = wmi_pi
 | 
			
		||||
                    agent.disks = disks_linux_pi
 | 
			
		||||
                    agent.operating_system = linux_pi_os
 | 
			
		||||
                else:
 | 
			
		||||
                    agent.goarch = "amd64"
 | 
			
		||||
                    agent.wmi_detail = wmi_deb
 | 
			
		||||
                    agent.disks = disks_linux_deb
 | 
			
		||||
                    agent.operating_system = linux_deb_os
 | 
			
		||||
            else:
 | 
			
		||||
                agent.plat = "windows"
 | 
			
		||||
                agent.goarch = "amd64"
 | 
			
		||||
                mode = random.choice(modes)
 | 
			
		||||
                agent.wmi_detail = random.choice(wmi_details)
 | 
			
		||||
                agent.services = services
 | 
			
		||||
                agent.disks = random.choice(disks)
 | 
			
		||||
                if mode == "server":
 | 
			
		||||
                    agent.operating_system = random.choice(op_systems_servers)
 | 
			
		||||
                else:
 | 
			
		||||
@@ -287,13 +331,12 @@ class Command(BaseCommand):
 | 
			
		||||
            agent.hostname = random.choice(hostnames)
 | 
			
		||||
            agent.version = settings.LATEST_AGENT_VER
 | 
			
		||||
            agent.site = Site.objects.get(name=site)
 | 
			
		||||
            agent.agent_id = self.rand_string(25)
 | 
			
		||||
            agent.agent_id = self.rand_string(40)
 | 
			
		||||
            agent.description = random.choice(descriptions)
 | 
			
		||||
            agent.monitoring_type = mode
 | 
			
		||||
            agent.public_ip = random.choice(public_ips)
 | 
			
		||||
            agent.last_seen = djangotime.now()
 | 
			
		||||
            agent.plat = "windows"
 | 
			
		||||
            agent.plat_release = "windows-2019Server"
 | 
			
		||||
            agent.last_seen = django_now
 | 
			
		||||
 | 
			
		||||
            agent.total_ram = random.choice(total_rams)
 | 
			
		||||
            agent.boot_time = random.choice(boot_times)
 | 
			
		||||
            agent.logged_in_username = random.choice(user_names)
 | 
			
		||||
@@ -303,12 +346,10 @@ class Command(BaseCommand):
 | 
			
		||||
            agent.overdue_email_alert = random.choice([True, False])
 | 
			
		||||
            agent.overdue_text_alert = random.choice([True, False])
 | 
			
		||||
            agent.needs_reboot = random.choice([True, False])
 | 
			
		||||
            agent.wmi_detail = random.choice(wmi_details)
 | 
			
		||||
            agent.services = services
 | 
			
		||||
            agent.disks = random.choice(disks)
 | 
			
		||||
 | 
			
		||||
            agent.save()
 | 
			
		||||
 | 
			
		||||
            if agent.plat == "windows":
 | 
			
		||||
                InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
 | 
			
		||||
 | 
			
		||||
            if mode == "workstation":
 | 
			
		||||
@@ -316,11 +357,9 @@ class Command(BaseCommand):
 | 
			
		||||
            else:
 | 
			
		||||
                WinUpdatePolicy(agent=agent).save()
 | 
			
		||||
 | 
			
		||||
            if agent.plat == "windows":
 | 
			
		||||
                # windows updates load
 | 
			
		||||
            guids = []
 | 
			
		||||
            for k in windows_updates.keys():
 | 
			
		||||
                guids.append(k)
 | 
			
		||||
 | 
			
		||||
                guids = [i for i in windows_updates.keys()]
 | 
			
		||||
                for i in guids:
 | 
			
		||||
                    WinUpdate(
 | 
			
		||||
                        agent=agent,
 | 
			
		||||
@@ -355,16 +394,11 @@ class Command(BaseCommand):
 | 
			
		||||
            }
 | 
			
		||||
            hist1.save()
 | 
			
		||||
 | 
			
		||||
            if agent.plat == "windows":
 | 
			
		||||
                # disk space check
 | 
			
		||||
                check1 = Check()
 | 
			
		||||
            check_result1 = CheckResult(assigned_check=check1, agent=agent)
 | 
			
		||||
                check1.agent = agent
 | 
			
		||||
            check1.check_type = "diskspace"
 | 
			
		||||
            check_result1.status = "passing"
 | 
			
		||||
            check_result1.last_run = djangotime.now()
 | 
			
		||||
            check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
 | 
			
		||||
            check_result1.save()
 | 
			
		||||
 | 
			
		||||
                check1.check_type = CheckType.DISK_SPACE
 | 
			
		||||
                check1.warning_threshold = 25
 | 
			
		||||
                check1.error_threshold = 10
 | 
			
		||||
                check1.disk = "C:"
 | 
			
		||||
@@ -372,33 +406,45 @@ class Command(BaseCommand):
 | 
			
		||||
                check1.text_alert = random.choice([True, False])
 | 
			
		||||
                check1.save()
 | 
			
		||||
 | 
			
		||||
                check_result1 = CheckResult()
 | 
			
		||||
                check_result1.agent = agent
 | 
			
		||||
                check_result1.assigned_check = check1
 | 
			
		||||
                check_result1.status = CheckStatus.PASSING
 | 
			
		||||
                check_result1.last_run = django_now
 | 
			
		||||
                check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
 | 
			
		||||
                check_result1.save()
 | 
			
		||||
 | 
			
		||||
                for i in range(30):
 | 
			
		||||
                    check1_history = CheckHistory()
 | 
			
		||||
                    check1_history.check_id = check1.pk
 | 
			
		||||
                    check1_history.agent_id = agent.agent_id
 | 
			
		||||
                check1_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                    check1_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                    check1_history.y = random.randint(13, 40)
 | 
			
		||||
                    check1_history.save()
 | 
			
		||||
 | 
			
		||||
            # ping check
 | 
			
		||||
            check2 = Check()
 | 
			
		||||
            check_result2 = CheckResult(assigned_check=check2, agent=agent)
 | 
			
		||||
            check_result2 = CheckResult()
 | 
			
		||||
 | 
			
		||||
            check2.agent = agent
 | 
			
		||||
            check2.check_type = "ping"
 | 
			
		||||
            check_result2.last_run = djangotime.now()
 | 
			
		||||
            check2.check_type = CheckType.PING
 | 
			
		||||
 | 
			
		||||
            check2.email_alert = random.choice([True, False])
 | 
			
		||||
            check2.text_alert = random.choice([True, False])
 | 
			
		||||
 | 
			
		||||
            check_result2.agent = agent
 | 
			
		||||
            check_result2.assigned_check = check2
 | 
			
		||||
            check_result2.last_run = django_now
 | 
			
		||||
 | 
			
		||||
            if site in sites5:
 | 
			
		||||
                check2.name = "Synology NAS"
 | 
			
		||||
                check_result2.status = "failing"
 | 
			
		||||
                check2.alert_severity = "error"
 | 
			
		||||
                check_result2.status = CheckStatus.FAILING
 | 
			
		||||
                check2.ip = "172.17.14.26"
 | 
			
		||||
                check_result2.more_info = ping_fail_output
 | 
			
		||||
            else:
 | 
			
		||||
                check2.name = "Google"
 | 
			
		||||
                check_result2.status = "passing"
 | 
			
		||||
                check_result2.status = CheckStatus.PASSING
 | 
			
		||||
                check2.ip = "8.8.8.8"
 | 
			
		||||
                check_result2.more_info = ping_success_output
 | 
			
		||||
 | 
			
		||||
@@ -409,9 +455,7 @@ class Command(BaseCommand):
 | 
			
		||||
                check2_history = CheckHistory()
 | 
			
		||||
                check2_history.check_id = check2.pk
 | 
			
		||||
                check2_history.agent_id = agent.agent_id
 | 
			
		||||
                check2_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check2_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                if site in sites5:
 | 
			
		||||
                    check2_history.y = 1
 | 
			
		||||
                    check2_history.results = ping_fail_output
 | 
			
		||||
@@ -422,13 +466,19 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
            # cpu load check
 | 
			
		||||
            check3 = Check()
 | 
			
		||||
            check_result3 = CheckResult(assigned_check=check3, agent=agent)
 | 
			
		||||
            check3.agent = agent
 | 
			
		||||
            check3.check_type = "cpuload"
 | 
			
		||||
            check_result3.status = "passing"
 | 
			
		||||
            check_result3.last_run = djangotime.now()
 | 
			
		||||
            check3.check_type = CheckType.CPU_LOAD
 | 
			
		||||
            check3.warning_threshold = 70
 | 
			
		||||
            check3.error_threshold = 90
 | 
			
		||||
            check3.email_alert = random.choice([True, False])
 | 
			
		||||
            check3.text_alert = random.choice([True, False])
 | 
			
		||||
            check3.save()
 | 
			
		||||
 | 
			
		||||
            check_result3 = CheckResult()
 | 
			
		||||
            check_result3.agent = agent
 | 
			
		||||
            check_result3.assigned_check = check3
 | 
			
		||||
            check_result3.status = CheckStatus.PASSING
 | 
			
		||||
            check_result3.last_run = django_now
 | 
			
		||||
            check_result3.history = [
 | 
			
		||||
                15,
 | 
			
		||||
                23,
 | 
			
		||||
@@ -445,68 +495,69 @@ class Command(BaseCommand):
 | 
			
		||||
                13,
 | 
			
		||||
                34,
 | 
			
		||||
            ]
 | 
			
		||||
            check3.email_alert = random.choice([True, False])
 | 
			
		||||
            check3.text_alert = random.choice([True, False])
 | 
			
		||||
            check3.save()
 | 
			
		||||
            check_result3.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check3_history = CheckHistory()
 | 
			
		||||
                check3_history.check_id = check3.pk
 | 
			
		||||
                check3_history.agent_id = agent.agent_id
 | 
			
		||||
                check3_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check3_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check3_history.y = random.randint(2, 79)
 | 
			
		||||
                check3_history.save()
 | 
			
		||||
 | 
			
		||||
            # memory check
 | 
			
		||||
            check4 = Check()
 | 
			
		||||
            check_result4 = CheckResult(assigned_check=check4, agent=agent)
 | 
			
		||||
            check4.agent = agent
 | 
			
		||||
            check4.check_type = "memory"
 | 
			
		||||
            check_result4.status = "passing"
 | 
			
		||||
            check4.check_type = CheckType.MEMORY
 | 
			
		||||
            check4.warning_threshold = 70
 | 
			
		||||
            check4.error_threshold = 85
 | 
			
		||||
            check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
 | 
			
		||||
            check4.email_alert = random.choice([True, False])
 | 
			
		||||
            check4.text_alert = random.choice([True, False])
 | 
			
		||||
            check4.save()
 | 
			
		||||
 | 
			
		||||
            check_result4 = CheckResult()
 | 
			
		||||
            check_result4.agent = agent
 | 
			
		||||
            check_result4.assigned_check = check4
 | 
			
		||||
            check_result4.status = CheckStatus.PASSING
 | 
			
		||||
            check_result4.last_run = django_now
 | 
			
		||||
            check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
 | 
			
		||||
            check_result4.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check4_history = CheckHistory()
 | 
			
		||||
                check4_history.check_id = check4.pk
 | 
			
		||||
                check4_history.agent_id = agent.agent_id
 | 
			
		||||
                check4_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check4_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check4_history.y = random.randint(2, 79)
 | 
			
		||||
                check4_history.save()
 | 
			
		||||
 | 
			
		||||
            # script check storage pool
 | 
			
		||||
            check5 = Check()
 | 
			
		||||
            check_result5 = CheckResult(assigned_check=check5, agent=agent)
 | 
			
		||||
 | 
			
		||||
            check5.agent = agent
 | 
			
		||||
            check5.check_type = "script"
 | 
			
		||||
            check_result5.status = "passing"
 | 
			
		||||
            check_result5.last_run = djangotime.now()
 | 
			
		||||
            check5.check_type = CheckType.SCRIPT
 | 
			
		||||
 | 
			
		||||
            check5.email_alert = random.choice([True, False])
 | 
			
		||||
            check5.text_alert = random.choice([True, False])
 | 
			
		||||
            check5.timeout = 120
 | 
			
		||||
            check_result5.retcode = 0
 | 
			
		||||
            check_result5.execution_time = "4.0000"
 | 
			
		||||
 | 
			
		||||
            check5.script = check_pool_health
 | 
			
		||||
            check5.save()
 | 
			
		||||
 | 
			
		||||
            check_result5 = CheckResult()
 | 
			
		||||
            check_result5.agent = agent
 | 
			
		||||
            check_result5.assigned_check = check5
 | 
			
		||||
            check_result5.status = CheckStatus.PASSING
 | 
			
		||||
            check_result5.last_run = django_now
 | 
			
		||||
            check_result5.retcode = 0
 | 
			
		||||
            check_result5.execution_time = "4.0000"
 | 
			
		||||
            check_result5.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check5_history = CheckHistory()
 | 
			
		||||
                check5_history.check_id = check5.pk
 | 
			
		||||
                check5_history.agent_id = agent.agent_id
 | 
			
		||||
                check5_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check5_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                if i == 10 or i == 18:
 | 
			
		||||
                    check5_history.y = 1
 | 
			
		||||
                else:
 | 
			
		||||
@@ -514,32 +565,34 @@ class Command(BaseCommand):
 | 
			
		||||
                check5_history.save()
 | 
			
		||||
 | 
			
		||||
            check6 = Check()
 | 
			
		||||
            check_result6 = CheckResult(assigned_check=check6, agent=agent)
 | 
			
		||||
 | 
			
		||||
            check6.agent = agent
 | 
			
		||||
            check6.check_type = "script"
 | 
			
		||||
            check_result6.status = "passing"
 | 
			
		||||
            check_result6.last_run = djangotime.now()
 | 
			
		||||
            check6.check_type = CheckType.SCRIPT
 | 
			
		||||
            check6.email_alert = random.choice([True, False])
 | 
			
		||||
            check6.text_alert = random.choice([True, False])
 | 
			
		||||
            check6.timeout = 120
 | 
			
		||||
            check_result6.retcode = 0
 | 
			
		||||
            check_result6.execution_time = "4.0000"
 | 
			
		||||
            check6.script = check_net_aware
 | 
			
		||||
            check6.save()
 | 
			
		||||
 | 
			
		||||
            check_result6 = CheckResult()
 | 
			
		||||
            check_result6.agent = agent
 | 
			
		||||
            check_result6.assigned_check = check6
 | 
			
		||||
            check_result6.status = CheckStatus.PASSING
 | 
			
		||||
            check_result6.last_run = django_now
 | 
			
		||||
            check_result6.retcode = 0
 | 
			
		||||
            check_result6.execution_time = "4.0000"
 | 
			
		||||
            check_result6.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check6_history = CheckHistory()
 | 
			
		||||
                check6_history.check_id = check6.pk
 | 
			
		||||
                check6_history.agent_id = agent.agent_id
 | 
			
		||||
                check6_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check6_history.y = 0
 | 
			
		||||
                check6_history.save()
 | 
			
		||||
 | 
			
		||||
            nla_task = AutomatedTask()
 | 
			
		||||
            nla_task_result = TaskResult(task=nla_task, agent=agent)
 | 
			
		||||
 | 
			
		||||
            nla_task.agent = agent
 | 
			
		||||
            actions = [
 | 
			
		||||
                {
 | 
			
		||||
@@ -554,16 +607,20 @@ class Command(BaseCommand):
 | 
			
		||||
            nla_task.assigned_check = check6
 | 
			
		||||
            nla_task.name = "Restart NLA"
 | 
			
		||||
            nla_task.task_type = "checkfailure"
 | 
			
		||||
            nla_task.save()
 | 
			
		||||
 | 
			
		||||
            nla_task_result = TaskResult()
 | 
			
		||||
            nla_task_result.task = nla_task
 | 
			
		||||
            nla_task_result.agent = agent
 | 
			
		||||
            nla_task_result.execution_time = "1.8443"
 | 
			
		||||
            nla_task_result.last_run = djangotime.now()
 | 
			
		||||
            nla_task_result.last_run = django_now
 | 
			
		||||
            nla_task_result.stdout = "no stdout"
 | 
			
		||||
            nla_task_result.retcode = 0
 | 
			
		||||
            nla_task_result.sync_status = "synced"
 | 
			
		||||
            nla_task.save()
 | 
			
		||||
            nla_task_result.save()
 | 
			
		||||
 | 
			
		||||
            spool_task = AutomatedTask()
 | 
			
		||||
            spool_task_result = TaskResult(task=spool_task, agent=agent)
 | 
			
		||||
 | 
			
		||||
            spool_task.agent = agent
 | 
			
		||||
            actions = [
 | 
			
		||||
                {
 | 
			
		||||
@@ -577,24 +634,25 @@ class Command(BaseCommand):
 | 
			
		||||
            spool_task.actions = actions
 | 
			
		||||
            spool_task.name = "Clear the print spooler"
 | 
			
		||||
            spool_task.task_type = "daily"
 | 
			
		||||
            spool_task.run_time_date = djangotime.now() + djangotime.timedelta(
 | 
			
		||||
                minutes=10
 | 
			
		||||
            )
 | 
			
		||||
            spool_task.expire_date = djangotime.now() + djangotime.timedelta(days=753)
 | 
			
		||||
            spool_task.run_time_date = django_now + djangotime.timedelta(minutes=10)
 | 
			
		||||
            spool_task.expire_date = django_now + djangotime.timedelta(days=753)
 | 
			
		||||
            spool_task.daily_interval = 1
 | 
			
		||||
            spool_task.weekly_interval = 1
 | 
			
		||||
            spool_task.task_repetition_duration = "2h"
 | 
			
		||||
            spool_task.task_repetition_interval = "25m"
 | 
			
		||||
            spool_task.random_task_delay = "3m"
 | 
			
		||||
            spool_task_result.last_run = djangotime.now()
 | 
			
		||||
            spool_task.save()
 | 
			
		||||
 | 
			
		||||
            spool_task_result = TaskResult()
 | 
			
		||||
            spool_task_result.task = spool_task
 | 
			
		||||
            spool_task_result.agent = agent
 | 
			
		||||
            spool_task_result.last_run = django_now
 | 
			
		||||
            spool_task_result.retcode = 0
 | 
			
		||||
            spool_task_result.stdout = spooler_stdout
 | 
			
		||||
            spool_task_result.sync_status = "synced"
 | 
			
		||||
            spool_task.save()
 | 
			
		||||
            spool_task_result.save()
 | 
			
		||||
 | 
			
		||||
            tmp_dir_task = AutomatedTask()
 | 
			
		||||
            tmp_dir_task_result = TaskResult(task=tmp_dir_task, agent=agent)
 | 
			
		||||
            tmp_dir_task.agent = agent
 | 
			
		||||
            tmp_dir_task.name = "show temp dir files"
 | 
			
		||||
            actions = [
 | 
			
		||||
@@ -608,63 +666,74 @@ class Command(BaseCommand):
 | 
			
		||||
            ]
 | 
			
		||||
            tmp_dir_task.actions = actions
 | 
			
		||||
            tmp_dir_task.task_type = "manual"
 | 
			
		||||
            tmp_dir_task_result.last_run = djangotime.now()
 | 
			
		||||
            tmp_dir_task.save()
 | 
			
		||||
 | 
			
		||||
            tmp_dir_task_result = TaskResult()
 | 
			
		||||
            tmp_dir_task_result.task = tmp_dir_task
 | 
			
		||||
            tmp_dir_task_result.agent = agent
 | 
			
		||||
            tmp_dir_task_result.last_run = django_now
 | 
			
		||||
            tmp_dir_task_result.stdout = temp_dir_stdout
 | 
			
		||||
            tmp_dir_task_result.retcode = 0
 | 
			
		||||
            tmp_dir_task_result.sync_status = "synced"
 | 
			
		||||
            tmp_dir_task.save()
 | 
			
		||||
            tmp_dir_task_result.save()
 | 
			
		||||
 | 
			
		||||
            check7 = Check()
 | 
			
		||||
            check_result7 = CheckResult(assigned_check=check7, agent=agent)
 | 
			
		||||
 | 
			
		||||
            check7.agent = agent
 | 
			
		||||
            check7.check_type = "script"
 | 
			
		||||
            check_result7.status = "passing"
 | 
			
		||||
            check_result7.last_run = djangotime.now()
 | 
			
		||||
            check7.check_type = CheckType.SCRIPT
 | 
			
		||||
 | 
			
		||||
            check7.email_alert = random.choice([True, False])
 | 
			
		||||
            check7.text_alert = random.choice([True, False])
 | 
			
		||||
            check7.timeout = 120
 | 
			
		||||
 | 
			
		||||
            check7.script = clear_spool
 | 
			
		||||
 | 
			
		||||
            check7.save()
 | 
			
		||||
 | 
			
		||||
            check_result7 = CheckResult()
 | 
			
		||||
            check_result7.assigned_check = check7
 | 
			
		||||
            check_result7.agent = agent
 | 
			
		||||
            check_result7.status = CheckStatus.PASSING
 | 
			
		||||
            check_result7.last_run = django_now
 | 
			
		||||
            check_result7.retcode = 0
 | 
			
		||||
            check_result7.execution_time = "3.1337"
 | 
			
		||||
            check7.script = clear_spool
 | 
			
		||||
            check_result7.stdout = spooler_stdout
 | 
			
		||||
            check7.save()
 | 
			
		||||
            check_result7.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check7_history = CheckHistory()
 | 
			
		||||
                check7_history.check_id = check7.pk
 | 
			
		||||
                check7_history.agent_id = agent.agent_id
 | 
			
		||||
                check7_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check7_history.y = 0
 | 
			
		||||
                check7_history.save()
 | 
			
		||||
 | 
			
		||||
            if agent.plat == "windows":
 | 
			
		||||
                check8 = Check()
 | 
			
		||||
            check_result8 = CheckResult(assigned_check=check8, agent=agent)
 | 
			
		||||
                check8.agent = agent
 | 
			
		||||
            check8.check_type = "winsvc"
 | 
			
		||||
            check_result8.status = "passing"
 | 
			
		||||
            check_result8.last_run = djangotime.now()
 | 
			
		||||
                check8.check_type = CheckType.WINSVC
 | 
			
		||||
                check8.email_alert = random.choice([True, False])
 | 
			
		||||
                check8.text_alert = random.choice([True, False])
 | 
			
		||||
            check_result8.more_info = "Status RUNNING"
 | 
			
		||||
                check8.fails_b4_alert = 4
 | 
			
		||||
                check8.svc_name = "Spooler"
 | 
			
		||||
                check8.svc_display_name = "Print Spooler"
 | 
			
		||||
                check8.pass_if_start_pending = False
 | 
			
		||||
                check8.restart_if_stopped = True
 | 
			
		||||
                check8.save()
 | 
			
		||||
 | 
			
		||||
                check_result8 = CheckResult()
 | 
			
		||||
                check_result8.assigned_check = check8
 | 
			
		||||
                check_result8.agent = agent
 | 
			
		||||
                check_result8.status = CheckStatus.PASSING
 | 
			
		||||
                check_result8.last_run = django_now
 | 
			
		||||
                check_result8.more_info = "Status RUNNING"
 | 
			
		||||
                check_result8.save()
 | 
			
		||||
 | 
			
		||||
                for i in range(30):
 | 
			
		||||
                    check8_history = CheckHistory()
 | 
			
		||||
                    check8_history.check_id = check8.pk
 | 
			
		||||
                    check8_history.agent_id = agent.agent_id
 | 
			
		||||
                check8_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                    check8_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                    if i == 10 or i == 18:
 | 
			
		||||
                        check8_history.y = 1
 | 
			
		||||
                        check8_history.results = "Status STOPPED"
 | 
			
		||||
@@ -674,28 +743,29 @@ class Command(BaseCommand):
 | 
			
		||||
                    check8_history.save()
 | 
			
		||||
 | 
			
		||||
                check9 = Check()
 | 
			
		||||
            check_result9 = CheckResult(assigned_check=check9, agent=agent)
 | 
			
		||||
                check9.agent = agent
 | 
			
		||||
            check9.check_type = "eventlog"
 | 
			
		||||
                check9.check_type = CheckType.EVENT_LOG
 | 
			
		||||
                check9.name = "unexpected shutdown"
 | 
			
		||||
 | 
			
		||||
            check_result9.last_run = djangotime.now()
 | 
			
		||||
                check9.email_alert = random.choice([True, False])
 | 
			
		||||
                check9.text_alert = random.choice([True, False])
 | 
			
		||||
                check9.fails_b4_alert = 2
 | 
			
		||||
                check9.log_name = EvtLogNames.APPLICATION
 | 
			
		||||
                check9.event_id = 1001
 | 
			
		||||
                check9.event_type = EvtLogTypes.INFO
 | 
			
		||||
                check9.fail_when = EvtLogFailWhen.CONTAINS
 | 
			
		||||
                check9.search_last_days = 30
 | 
			
		||||
 | 
			
		||||
                check_result9 = CheckResult()
 | 
			
		||||
                check_result9.agent = agent
 | 
			
		||||
                check_result9.assigned_check = check9
 | 
			
		||||
 | 
			
		||||
                check_result9.last_run = django_now
 | 
			
		||||
                if site in sites5:
 | 
			
		||||
                    check_result9.extra_details = eventlog_check_fail_data
 | 
			
		||||
                check_result9.status = "failing"
 | 
			
		||||
                    check_result9.status = CheckStatus.FAILING
 | 
			
		||||
                else:
 | 
			
		||||
                    check_result9.extra_details = {"log": []}
 | 
			
		||||
                check_result9.status = "passing"
 | 
			
		||||
 | 
			
		||||
            check9.log_name = "Application"
 | 
			
		||||
            check9.event_id = 1001
 | 
			
		||||
            check9.event_type = "INFO"
 | 
			
		||||
            check9.fail_when = "contains"
 | 
			
		||||
            check9.search_last_days = 30
 | 
			
		||||
                    check_result9.status = CheckStatus.PASSING
 | 
			
		||||
 | 
			
		||||
                check9.save()
 | 
			
		||||
                check_result9.save()
 | 
			
		||||
@@ -704,9 +774,7 @@ class Command(BaseCommand):
 | 
			
		||||
                    check9_history = CheckHistory()
 | 
			
		||||
                    check9_history.check_id = check9.pk
 | 
			
		||||
                    check9_history.agent_id = agent.agent_id
 | 
			
		||||
                check9_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                    check9_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                    if i == 10 or i == 18:
 | 
			
		||||
                        check9_history.y = 1
 | 
			
		||||
                        check9_history.results = "Events Found: 16"
 | 
			
		||||
@@ -719,7 +787,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
                if pick == 5 or pick == 3:
 | 
			
		||||
 | 
			
		||||
                reboot_time = djangotime.now() + djangotime.timedelta(
 | 
			
		||||
                    reboot_time = django_now + djangotime.timedelta(
 | 
			
		||||
                        minutes=random.randint(1000, 500000)
 | 
			
		||||
                    )
 | 
			
		||||
                    date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
 | 
			
		||||
@@ -732,7 +800,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
                    sched_reboot = PendingAction()
 | 
			
		||||
                    sched_reboot.agent = agent
 | 
			
		||||
                sched_reboot.action_type = "schedreboot"
 | 
			
		||||
                    sched_reboot.action_type = PAAction.SCHED_REBOOT
 | 
			
		||||
                    sched_reboot.details = {
 | 
			
		||||
                        "time": str(obj),
 | 
			
		||||
                        "taskname": task_name,
 | 
			
		||||
@@ -741,5 +809,4 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
            self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}"))
 | 
			
		||||
 | 
			
		||||
        call_command("load_demo_scripts")
 | 
			
		||||
        self.stdout.write("done")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Shows online agents that are not on the latest version"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import send_agent_update_task
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import send_agent_update_task
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 4.0.3 on 2022-04-07 17:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-04-25 06:51
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0049_agent_agents_agen_monitor_df8816_idx'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='plat_release',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -2,29 +2,29 @@ import asyncio
 | 
			
		||||
import re
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
from typing import Any, Optional, List, Dict, Union, Sequence, cast, TYPE_CHECKING
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
 | 
			
		||||
 | 
			
		||||
import msgpack
 | 
			
		||||
import nats
 | 
			
		||||
import validators
 | 
			
		||||
from asgiref.sync import sync_to_async
 | 
			
		||||
from core.models import TZ_CHOICES
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from nats.errors import TimeoutError
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from core.models import TZ_CHOICES
 | 
			
		||||
from core.utils import get_core_settings, send_command_with_mesh
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.constants import ONLINE_AGENTS, CheckStatus, CheckType, DebugLogType
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
from tacticalrmm.constants import ONLINE_AGENTS
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from alerts.models import Alert, AlertTemplate
 | 
			
		||||
    from automation.models import Policy
 | 
			
		||||
    from alerts.models import AlertTemplate, Alert
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
    from clients.models import Client
 | 
			
		||||
@@ -46,7 +46,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    operating_system = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    plat = models.CharField(max_length=255, default="windows")
 | 
			
		||||
    goarch = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    plat_release = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    hostname = models.CharField(max_length=255)
 | 
			
		||||
    agent_id = models.CharField(max_length=200, unique=True)
 | 
			
		||||
    last_seen = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
@@ -168,16 +167,22 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            if (
 | 
			
		||||
                not hasattr(check.check_result, "status")
 | 
			
		||||
                or isinstance(check.check_result, CheckResult)
 | 
			
		||||
                and check.check_result.status == "passing"
 | 
			
		||||
                and check.check_result.status == CheckStatus.PASSING
 | 
			
		||||
            ):
 | 
			
		||||
                passing += 1
 | 
			
		||||
            elif (
 | 
			
		||||
                isinstance(check.check_result, CheckResult)
 | 
			
		||||
                and check.check_result.status == "failing"
 | 
			
		||||
                and check.check_result.status == CheckStatus.FAILING
 | 
			
		||||
            ):
 | 
			
		||||
                alert_severity = (
 | 
			
		||||
                    check.check_result.alert_severity
 | 
			
		||||
                    if check.check_type in ["memory", "cpuload", "diskspace", "script"]
 | 
			
		||||
                    if check.check_type
 | 
			
		||||
                    in [
 | 
			
		||||
                        CheckType.MEMORY,
 | 
			
		||||
                        CheckType.CPU_LOAD,
 | 
			
		||||
                        CheckType.DISK_SPACE,
 | 
			
		||||
                        CheckType.SCRIPT,
 | 
			
		||||
                    ]
 | 
			
		||||
                    else check.alert_severity
 | 
			
		||||
                )
 | 
			
		||||
                if alert_severity == "error":
 | 
			
		||||
@@ -718,7 +723,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            return tasks
 | 
			
		||||
 | 
			
		||||
    def _do_nats_debug(self, agent: "Agent", message: str) -> None:
 | 
			
		||||
        DebugLog.error(agent=agent, log_type="agent_issues", message=message)
 | 
			
		||||
        DebugLog.error(agent=agent, log_type=DebugLogType.AGENT_ISSUES, message=message)
 | 
			
		||||
 | 
			
		||||
    async def nats_cmd(
 | 
			
		||||
        self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True
 | 
			
		||||
@@ -759,6 +764,38 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            await nc.flush()
 | 
			
		||||
            await nc.close()
 | 
			
		||||
 | 
			
		||||
    def recover(self, mode: str, mesh_uri: str, wait: bool = True) -> tuple[str, bool]:
 | 
			
		||||
        """
 | 
			
		||||
        Return type: tuple(message: str, error: bool)
 | 
			
		||||
        """
 | 
			
		||||
        if mode == "tacagent":
 | 
			
		||||
            if self.is_posix:
 | 
			
		||||
                cmd = "systemctl restart tacticalagent.service"
 | 
			
		||||
                shell = 3
 | 
			
		||||
            else:
 | 
			
		||||
                cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
 | 
			
		||||
                shell = 1
 | 
			
		||||
 | 
			
		||||
            asyncio.run(
 | 
			
		||||
                send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
 | 
			
		||||
            )
 | 
			
		||||
            return ("ok", False)
 | 
			
		||||
 | 
			
		||||
        elif mode == "mesh":
 | 
			
		||||
            data = {"func": "recover", "payload": {"mode": mode}}
 | 
			
		||||
            if wait:
 | 
			
		||||
                r = asyncio.run(self.nats_cmd(data, timeout=20))
 | 
			
		||||
                if r == "ok":
 | 
			
		||||
                    return ("ok", False)
 | 
			
		||||
                else:
 | 
			
		||||
                    return (str(r), True)
 | 
			
		||||
            else:
 | 
			
		||||
                asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
 | 
			
		||||
 | 
			
		||||
            return ("ok", False)
 | 
			
		||||
 | 
			
		||||
        return ("invalid", True)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(agent: "Agent") -> Dict[str, Any]:
 | 
			
		||||
        # serializes the agent and returns json
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,17 @@ import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.utils import get_agent_url
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.constants import CheckStatus, DebugLogType, PAAction, PAStatus
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def agent_update(agent_id: str, force: bool = False) -> str:
 | 
			
		||||
@@ -27,7 +28,7 @@ def agent_update(agent_id: str, force: bool = False) -> str:
 | 
			
		||||
    if agent.arch is None:
 | 
			
		||||
        DebugLog.warning(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
            message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
 | 
			
		||||
        )
 | 
			
		||||
        return "noarch"
 | 
			
		||||
@@ -38,15 +39,15 @@ def agent_update(agent_id: str, force: bool = False) -> str:
 | 
			
		||||
 | 
			
		||||
    if not force:
 | 
			
		||||
        if agent.pendingactions.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
            action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
        ).exists():
 | 
			
		||||
            agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
                action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
            ).delete()
 | 
			
		||||
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            action_type=PAAction.AGENT_UPDATE,
 | 
			
		||||
            details={
 | 
			
		||||
                "url": url,
 | 
			
		||||
                "version": version,
 | 
			
		||||
@@ -235,7 +236,7 @@ def run_script_email_results_task(
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        DebugLog.error(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="scripting",
 | 
			
		||||
            log_type=DebugLogType.SCRIPTING,
 | 
			
		||||
            message=f"{agent.hostname}({agent.pk}) timed out running script.",
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
@@ -289,7 +290,7 @@ def clear_faults_task(older_than_days: int) -> None:
 | 
			
		||||
        for check in agent.get_checks_with_policies():
 | 
			
		||||
            # reset check status
 | 
			
		||||
            if check.check_result:
 | 
			
		||||
                check.check_result.status = "passing"
 | 
			
		||||
                check.check_result.status = CheckStatus.PASSING
 | 
			
		||||
                check.check_result.save(update_fields=["status"])
 | 
			
		||||
            if check.alert.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
                alert = Alert.create_or_return_check_alert(check, agent=agent)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								api/tacticalrmm/agents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/agents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -1,28 +1,28 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
import pytz
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.test import modify_settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
from .serializers import (
 | 
			
		||||
from agents.models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
from agents.serializers import (
 | 
			
		||||
    AgentHistorySerializer,
 | 
			
		||||
    AgentHostnameSerializer,
 | 
			
		||||
    AgentNoteSerializer,
 | 
			
		||||
    AgentSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import auto_self_agent_update_task
 | 
			
		||||
from agents.tasks import auto_self_agent_update_task
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.constants import EvtLogNames, PAAction, PAStatus
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from clients.models import Client, Site
 | 
			
		||||
@@ -30,11 +30,6 @@ if TYPE_CHECKING:
 | 
			
		||||
base_url = "/agents"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentsList(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -100,11 +95,6 @@ class TestAgentsList(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -374,7 +364,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
                "func": "eventlog",
 | 
			
		||||
                "timeout": 30,
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "logname": "Application",
 | 
			
		||||
                    "logname": EvtLogNames.APPLICATION,
 | 
			
		||||
                    "days": str(22),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
@@ -389,7 +379,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
                "func": "eventlog",
 | 
			
		||||
                "timeout": 180,
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "logname": "Security",
 | 
			
		||||
                    "logname": EvtLogNames.SECURITY,
 | 
			
		||||
                    "days": str(6),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
@@ -579,10 +569,9 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    @patch("agents.tasks.run_script_email_results_task.delay")
 | 
			
		||||
    @patch("agents.models.Agent.run_script")
 | 
			
		||||
    def test_run_script(self, run_script, email_task):
 | 
			
		||||
        from agents.models import AgentCustomField, AgentHistory, Note
 | 
			
		||||
        from clients.models import ClientCustomField, SiteCustomField
 | 
			
		||||
 | 
			
		||||
        from .models import AgentCustomField, AgentHistory, Note
 | 
			
		||||
 | 
			
		||||
        run_script.return_value = "ok"
 | 
			
		||||
        url = f"/agents/{self.agent.agent_id}/runscript/"
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
@@ -886,11 +875,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.data, data)  # type:ignore
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -925,11 +909,6 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_client()
 | 
			
		||||
@@ -1403,11 +1382,6 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
        self.check_authorized_superuser("get", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -1446,8 +1420,8 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        r = agent_update(agent64_nosign.agent_id)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__agent_id=agent64_nosign.agent_id)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(action.action_type, PAAction.AGENT_UPDATE)
 | 
			
		||||
        self.assertEqual(action.status, PAStatus.PENDING)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["url"],
 | 
			
		||||
            f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
@@ -1492,8 +1466,8 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(action.action_type, PAAction.AGENT_UPDATE)
 | 
			
		||||
        self.assertEqual(action.status, PAStatus.PENDING)
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (32 bit)
 | 
			
		||||
        agent32_sign = baker.make_recipe(
 | 
			
		||||
@@ -1518,8 +1492,8 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending") """
 | 
			
		||||
        self.assertEqual(action.action_type, PAAction.AGENT_UPDATE)
 | 
			
		||||
        self.assertEqual(action.status, PAStatus.PENDING) """
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.agent_update")
 | 
			
		||||
    @patch("agents.tasks.sleep", return_value=None)
 | 
			
		||||
@@ -1550,7 +1524,7 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(agent_update.call_count, 33)
 | 
			
		||||
 | 
			
		||||
    def test_agent_history_prune_task(self):
 | 
			
		||||
        from .tasks import prune_agent_history
 | 
			
		||||
        from agents.tasks import prune_agent_history
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
							
								
								
									
										62
									
								
								api/tacticalrmm/agents/tests/test_recovery.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								api/tacticalrmm/agents/tests/test_recovery.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from clients.models import Client, Site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRecovery(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.client1: "Client" = baker.make("clients.Client")
 | 
			
		||||
        self.site1: "Site" = baker.make("clients.Site", client=self.client1)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.recover")
 | 
			
		||||
    @patch("agents.views.get_mesh_ws_url")
 | 
			
		||||
    def test_recover(self, get_mesh_ws_url, recover) -> None:
 | 
			
		||||
        get_mesh_ws_url.return_value = "https://mesh.example.com"
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type="server",
 | 
			
		||||
            plat="windows",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/{agent.agent_id}/recover/"
 | 
			
		||||
 | 
			
		||||
        # test successfull tacticalagent recovery
 | 
			
		||||
        data = {"mode": "tacagent"}
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        recover.assert_called_with("tacagent", "https://mesh.example.com", wait=False)
 | 
			
		||||
        get_mesh_ws_url.assert_called_once()
 | 
			
		||||
 | 
			
		||||
        # reset mocks
 | 
			
		||||
        recover.reset_mock()
 | 
			
		||||
        get_mesh_ws_url.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test successfull mesh agent recovery
 | 
			
		||||
        data = {"mode": "mesh"}
 | 
			
		||||
        recover.return_value = ("ok", False)
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        get_mesh_ws_url.assert_not_called()
 | 
			
		||||
        recover.assert_called_with("mesh", "")
 | 
			
		||||
 | 
			
		||||
        # reset mocks
 | 
			
		||||
        recover.reset_mock()
 | 
			
		||||
        get_mesh_ws_url.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test failed mesh agent recovery
 | 
			
		||||
        data = {"mode": "mesh"}
 | 
			
		||||
        recover.return_value = ("Unable to contact the agent", True)
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from autotasks.views import GetAddAutoTasks
 | 
			
		||||
from checks.views import GetAddChecks
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from logs.views import PendingActions
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ import asyncio
 | 
			
		||||
import tempfile
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
from core.utils import get_mesh_device_id, get_mesh_ws_url, get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import FileResponse
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
from tacticalrmm.constants import MeshAgentIdent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,40 +4,36 @@ import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import time
 | 
			
		||||
from meshctrl.utils import get_login_token
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
    remove_mesh_agent,
 | 
			
		||||
    send_command_with_mesh,
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
)
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Q, Prefetch, Exists, Count, OuterRef
 | 
			
		||||
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from meshctrl.utils import get_login_token
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
from core.utils import get_core_settings, get_mesh_ws_url, remove_mesh_agent
 | 
			
		||||
from logs.models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
from winupdate.models import WinUpdate
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER, EvtLogNames, PAAction, PAStatus
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm_on_agent,
 | 
			
		||||
    _has_perm_on_client,
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
from .permissions import (
 | 
			
		||||
@@ -119,7 +115,8 @@ class GetAgents(APIView):
 | 
			
		||||
                )
 | 
			
		||||
                .annotate(
 | 
			
		||||
                    pending_actions_count=Count(
 | 
			
		||||
                        "pendingactions", filter=Q(pendingactions__status="pending")
 | 
			
		||||
                        "pendingactions",
 | 
			
		||||
                        filter=Q(pendingactions__status=PAStatus.PENDING),
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                .annotate(
 | 
			
		||||
@@ -356,7 +353,7 @@ def get_event_log(request, agent_id, logtype, days):
 | 
			
		||||
        return demo_get_eventlog()
 | 
			
		||||
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
    timeout = 180 if logtype == EvtLogNames.SECURITY else 30
 | 
			
		||||
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "eventlog",
 | 
			
		||||
@@ -430,6 +427,8 @@ class Reboot(APIView):
 | 
			
		||||
    # reboot later
 | 
			
		||||
    def patch(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        if agent.is_posix:
 | 
			
		||||
            return notify_error(f"Not currently implemented for {agent.plat}")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S")
 | 
			
		||||
@@ -471,7 +470,7 @@ class Reboot(APIView):
 | 
			
		||||
 | 
			
		||||
        details = {"taskname": task_name, "time": str(obj)}
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent, action_type="schedreboot", details=details
 | 
			
		||||
            agent=agent, action_type=PAAction.SCHED_REBOOT, details=details
 | 
			
		||||
        )
 | 
			
		||||
        nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
 | 
			
		||||
        return Response(
 | 
			
		||||
@@ -482,9 +481,10 @@ class Reboot(APIView):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
 | 
			
		||||
def install_agent(request):
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
 | 
			
		||||
    from accounts.models import User
 | 
			
		||||
    from agents.utils import get_agent_url
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
 | 
			
		||||
    client_id = request.data["client"]
 | 
			
		||||
    site_id = request.data["site"]
 | 
			
		||||
@@ -639,28 +639,23 @@ def install_agent(request):
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, RecoverAgentPerms])
 | 
			
		||||
def recover(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
def recover(request, agent_id: str) -> Response:
 | 
			
		||||
    agent: Agent = get_object_or_404(
 | 
			
		||||
        Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id
 | 
			
		||||
    )
 | 
			
		||||
    mode = request.data["mode"]
 | 
			
		||||
 | 
			
		||||
    if mode == "tacagent":
 | 
			
		||||
        if agent.is_posix:
 | 
			
		||||
            cmd = "systemctl restart tacticalagent.service"
 | 
			
		||||
            shell = 3
 | 
			
		||||
        else:
 | 
			
		||||
            cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
 | 
			
		||||
            shell = 1
 | 
			
		||||
        uri = get_mesh_ws_url()
 | 
			
		||||
        asyncio.run(send_command_with_mesh(cmd, uri, agent.mesh_node_id, shell, 0))
 | 
			
		||||
        agent.recover(mode, uri, wait=False)
 | 
			
		||||
        return Response("Recovery will be attempted shortly")
 | 
			
		||||
 | 
			
		||||
    elif mode == "mesh":
 | 
			
		||||
        data = {"func": "recover", "payload": {"mode": mode}}
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(data, timeout=20))
 | 
			
		||||
        if r == "ok":
 | 
			
		||||
            return Response("Successfully completed recovery")
 | 
			
		||||
        r, err = agent.recover(mode, "")
 | 
			
		||||
        if err:
 | 
			
		||||
            return notify_error(f"Unable to complete recovery: {r}")
 | 
			
		||||
 | 
			
		||||
    return notify_error("Something went wrong")
 | 
			
		||||
    return Response("Successfully completed recovery")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 4.0.3 on 2022-04-07 17:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_alerts_without_agent(apps, schema):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from typing import TYPE_CHECKING, Union, Optional, Dict, Any, List, cast
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.constants import CheckType, DebugLogType
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -85,10 +86,10 @@ class Alert(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.message
 | 
			
		||||
        return f"{self.alert_type} - {self.message}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def assigned_agent(self) -> "Agent":
 | 
			
		||||
    def assigned_agent(self) -> "Optional[Agent]":
 | 
			
		||||
        return self.agent
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@@ -176,7 +177,12 @@ class Alert(models.Model):
 | 
			
		||||
                    alert_type="check",
 | 
			
		||||
                    severity=check.alert_severity
 | 
			
		||||
                    if check.check_type
 | 
			
		||||
                    not in ["memory", "cpuload", "diskspace", "script"]
 | 
			
		||||
                    not in [
 | 
			
		||||
                        CheckType.MEMORY,
 | 
			
		||||
                        CheckType.CPU_LOAD,
 | 
			
		||||
                        CheckType.DISK_SPACE,
 | 
			
		||||
                        CheckType.SCRIPT,
 | 
			
		||||
                    ]
 | 
			
		||||
                    else alert_severity,
 | 
			
		||||
                    message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
 | 
			
		||||
                    hidden=True,
 | 
			
		||||
@@ -298,12 +304,12 @@ class Alert(models.Model):
 | 
			
		||||
            maintenance_mode = instance.maintenance_mode
 | 
			
		||||
            alert_severity = "error"
 | 
			
		||||
            agent = instance
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
            dashboard_severities = ["error"]
 | 
			
		||||
            email_severities = ["error"]
 | 
			
		||||
            text_severities = ["error"]
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                always_dashboard = alert_template.agent_always_alert
 | 
			
		||||
                always_email = alert_template.agent_always_email
 | 
			
		||||
                always_text = alert_template.agent_always_text
 | 
			
		||||
@@ -327,16 +333,33 @@ class Alert(models.Model):
 | 
			
		||||
            alert_severity = (
 | 
			
		||||
                instance.assigned_check.alert_severity
 | 
			
		||||
                if instance.assigned_check.check_type
 | 
			
		||||
                not in ["memory", "cpuload", "diskspace", "script"]
 | 
			
		||||
                not in [
 | 
			
		||||
                    CheckType.MEMORY,
 | 
			
		||||
                    CheckType.CPU_LOAD,
 | 
			
		||||
                    CheckType.DISK_SPACE,
 | 
			
		||||
                    CheckType.SCRIPT,
 | 
			
		||||
                ]
 | 
			
		||||
                else instance.alert_severity
 | 
			
		||||
            )
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.check_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.check_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.check_text_alert_severity
 | 
			
		||||
                dashboard_severities = (
 | 
			
		||||
                    alert_template.check_dashboard_alert_severity
 | 
			
		||||
                    if alert_template.check_dashboard_alert_severity
 | 
			
		||||
                    else ["error", "warning", "info"]
 | 
			
		||||
                )
 | 
			
		||||
                email_severities = (
 | 
			
		||||
                    alert_template.check_email_alert_severity
 | 
			
		||||
                    if alert_template.check_email_alert_severity
 | 
			
		||||
                    else ["error", "warning"]
 | 
			
		||||
                )
 | 
			
		||||
                text_severities = (
 | 
			
		||||
                    alert_template.check_text_alert_severity
 | 
			
		||||
                    if alert_template.check_text_alert_severity
 | 
			
		||||
                    else ["error", "warning"]
 | 
			
		||||
                )
 | 
			
		||||
                always_dashboard = alert_template.check_always_alert
 | 
			
		||||
                always_email = alert_template.check_always_email
 | 
			
		||||
                always_text = alert_template.check_always_text
 | 
			
		||||
@@ -359,9 +382,21 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.task_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.task_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.task_text_alert_severity
 | 
			
		||||
                dashboard_severities = (
 | 
			
		||||
                    alert_template.task_dashboard_alert_severity
 | 
			
		||||
                    if alert_template.task_dashboard_alert_severity
 | 
			
		||||
                    else ["error", "warning"]
 | 
			
		||||
                )
 | 
			
		||||
                email_severities = (
 | 
			
		||||
                    alert_template.task_email_alert_severity
 | 
			
		||||
                    if alert_template.task_email_alert_severity
 | 
			
		||||
                    else ["error", "warning"]
 | 
			
		||||
                )
 | 
			
		||||
                text_severities = (
 | 
			
		||||
                    alert_template.task_text_alert_severity
 | 
			
		||||
                    if alert_template.task_text_alert_severity
 | 
			
		||||
                    else ["error", "warning"]
 | 
			
		||||
                )
 | 
			
		||||
                always_dashboard = alert_template.task_always_alert
 | 
			
		||||
                always_email = alert_template.task_always_email
 | 
			
		||||
                always_text = alert_template.task_always_text
 | 
			
		||||
@@ -449,7 +484,7 @@ class Alert(models.Model):
 | 
			
		||||
            else:
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="scripting",
 | 
			
		||||
                    log_type=DebugLogType.SCRIPTING,
 | 
			
		||||
                    message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@@ -573,7 +608,7 @@ class Alert(models.Model):
 | 
			
		||||
            else:
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="scripting",
 | 
			
		||||
                    log_type=DebugLogType.SCRIPTING,
 | 
			
		||||
                    message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@@ -600,7 +635,7 @@ class Alert(models.Model):
 | 
			
		||||
                try:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    DebugLog.error(log_type="scripting", message=str(e))
 | 
			
		||||
                    DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e))
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
from automation.serializers import PolicySerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
 | 
			
		||||
 | 
			
		||||
from automation.serializers import PolicySerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from .models import Alert
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
from .models import Alert
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def unsnooze_alerts() -> str:
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,17 @@ from datetime import datetime, timedelta
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from tacticalrmm.constants import CheckStatus
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AlertSerializer,
 | 
			
		||||
@@ -841,7 +842,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test agent with check that has alert settings
 | 
			
		||||
        check_agent_result.alert_severity = "warning"
 | 
			
		||||
        check_agent_result.status = "failing"
 | 
			
		||||
        check_agent_result.status = CheckStatus.FAILING
 | 
			
		||||
 | 
			
		||||
        Alert.handle_alert_failure(check_agent_result)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .permissions import AlertPerms, AlertTemplatePerms
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								api/tacticalrmm/apiv3/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/apiv3/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -1,11 +1,7 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -70,24 +66,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_sysinfo(self):
 | 
			
		||||
        # TODO replace this with golang wmi sample data
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/sysinfo/"
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(
 | 
			
		||||
                settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
 | 
			
		||||
            )
 | 
			
		||||
        ) as f:
 | 
			
		||||
            wmi_py = json.load(f)
 | 
			
		||||
 | 
			
		||||
        payload = {"agent_id": self.agent.agent_id, "sysinfo": wmi_py}
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_checkrunner_interval(self):
 | 
			
		||||
        url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
@@ -137,8 +115,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(len(r.json()["checks"]), 15)
 | 
			
		||||
 | 
			
		||||
    def test_task_runner_get(self):
 | 
			
		||||
        from autotasks.serializers import TaskGOGetSerializer
 | 
			
		||||
 | 
			
		||||
        r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
@@ -163,7 +139,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskGOGetSerializer(task).data, r.data)
 | 
			
		||||
 | 
			
		||||
    def test_task_runner_results(self):
 | 
			
		||||
        from agents.models import AgentCustomField
 | 
			
		||||
@@ -9,7 +9,6 @@ urlpatterns = [
 | 
			
		||||
    path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
 | 
			
		||||
    path("meshexe/", views.MeshExe.as_view()),
 | 
			
		||||
    path("sysinfo/", views.SysInfo.as_view()),
 | 
			
		||||
    path("newagent/", views.NewAgent.as_view()),
 | 
			
		||||
    path("software/", views.Software.as_view()),
 | 
			
		||||
    path("installer/", views.Installer.as_view()),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,44 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from agents.serializers import AgentHistorySerializer
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from core.utils import download_mesh_agent, get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Prefetch
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.authentication import TokenAuthentication
 | 
			
		||||
from rest_framework.authtoken.models import Token
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import MeshAgentIdent
 | 
			
		||||
from tacticalrmm.utils import notify_error, reload_nats
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from agents.serializers import AgentHistorySerializer
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
 | 
			
		||||
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    download_mesh_agent,
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
    get_mesh_device_id,
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
)
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    AuditActionType,
 | 
			
		||||
    AuditObjType,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    MeshAgentIdent,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.utils import reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckIn(APIView):
 | 
			
		||||
@@ -34,7 +48,9 @@ class CheckIn(APIView):
 | 
			
		||||
 | 
			
		||||
    # called once during tacticalagent windows service startup
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        if not agent.choco_installed:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +63,9 @@ class SyncMeshNodeID(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        if agent.mesh_node_id != request.data["nodeid"]:
 | 
			
		||||
            agent.mesh_node_id = request.data["nodeid"]
 | 
			
		||||
            agent.save(update_fields=["mesh_node_id"])
 | 
			
		||||
@@ -60,7 +78,9 @@ class Choco(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        agent.choco_installed = request.data["installed"]
 | 
			
		||||
        agent.save(update_fields=["choco_installed"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
@@ -71,7 +91,9 @@ class WinUpdates(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        needs_reboot: bool = request.data["needs_reboot"]
 | 
			
		||||
        agent.needs_reboot = needs_reboot
 | 
			
		||||
@@ -89,7 +111,7 @@ class WinUpdates(APIView):
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="windows_updates",
 | 
			
		||||
                log_type=DebugLogType.WIN_UPDATES,
 | 
			
		||||
                message=f"{agent.hostname} is rebooting after updates were installed.",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +119,9 @@ class WinUpdates(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        u = agent.winupdates.filter(guid=request.data["guid"]).last()  # type: ignore
 | 
			
		||||
        if not u:
 | 
			
		||||
            raise WinUpdate.DoesNotExist
 | 
			
		||||
@@ -124,8 +148,14 @@ class WinUpdates(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        updates = request.data["wua_updates"]
 | 
			
		||||
        if not updates:
 | 
			
		||||
            return notify_error("Empty payload")
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for update in updates:
 | 
			
		||||
            if agent.winupdates.filter(guid=update["guid"]).exists():  # type: ignore
 | 
			
		||||
                u = agent.winupdates.filter(guid=update["guid"]).last()  # type: ignore
 | 
			
		||||
@@ -164,7 +194,9 @@ class SupersededWinUpdate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        updates = agent.winupdates.filter(guid=request.data["guid"])  # type: ignore
 | 
			
		||||
        for u in updates:
 | 
			
		||||
            u.delete()
 | 
			
		||||
@@ -177,7 +209,12 @@ class RunChecks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER).prefetch_related(
 | 
			
		||||
                Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
        checks = agent.get_checks_with_policies(exclude_overridden=True)
 | 
			
		||||
        ret = {
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
@@ -194,7 +231,12 @@ class CheckRunner(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER).prefetch_related(
 | 
			
		||||
                Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
        checks = agent.get_checks_with_policies(exclude_overridden=True)
 | 
			
		||||
 | 
			
		||||
        run_list = [
 | 
			
		||||
@@ -225,25 +267,31 @@ class CheckRunner(APIView):
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
			
		||||
 | 
			
		||||
        if "agent_id" not in request.data.keys():
 | 
			
		||||
            return notify_error("Agent upgrade required")
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        check = get_object_or_404(
 | 
			
		||||
            Check.objects.defer(*CHECK_DEFER),
 | 
			
		||||
            pk=request.data["id"],
 | 
			
		||||
        )
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check check result or create if doesn't exist
 | 
			
		||||
        try:
 | 
			
		||||
            check_result = CheckResult.objects.get(assigned_check=check, agent=agent)
 | 
			
		||||
        except CheckResult.DoesNotExist:
 | 
			
		||||
            check_result = CheckResult(assigned_check=check, agent=agent)
 | 
			
		||||
        # get check result or create if doesn't exist
 | 
			
		||||
        check_result, created = CheckResult.objects.defer(
 | 
			
		||||
            *CHECK_RESULT_DEFER
 | 
			
		||||
        ).get_or_create(
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        check_result.last_run = djangotime.now()
 | 
			
		||||
        if created:
 | 
			
		||||
            check_result.save()
 | 
			
		||||
 | 
			
		||||
        status = check_result.handle_check(request.data)
 | 
			
		||||
        if status == "failing" and check.assignedtasks.exists():  # type: ignore
 | 
			
		||||
            for task in check.assignedtasks.all():  # type: ignore
 | 
			
		||||
        status = check_result.handle_check(request.data, check, agent)
 | 
			
		||||
        if status == CheckStatus.FAILING and check.assignedtasks.exists():
 | 
			
		||||
            for task in check.assignedtasks.all():
 | 
			
		||||
                if task.enabled:
 | 
			
		||||
                    if task.policy:
 | 
			
		||||
                        task.run_win_task(agent)
 | 
			
		||||
@@ -258,7 +306,10 @@ class CheckRunnerInterval(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER).prefetch_related("agentchecks"),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            {"agent": agent.pk, "check_interval": agent.check_run_interval()}
 | 
			
		||||
@@ -270,19 +321,28 @@ class TaskRunner(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agentid)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
        return Response(TaskGOGetSerializer(task, context={"agent": agent}).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk, agentid):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
        task = get_object_or_404(
 | 
			
		||||
            AutomatedTask.objects.select_related("custom_field"), pk=pk
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check check result or create if doesn't exist
 | 
			
		||||
        # get task result or create if doesn't exist
 | 
			
		||||
        try:
 | 
			
		||||
            task_result = TaskResult.objects.get(task=task, agent=agent)
 | 
			
		||||
            task_result = (
 | 
			
		||||
                TaskResult.objects.select_related("agent")
 | 
			
		||||
                .defer("agent__services", "agent__wmi_detail")
 | 
			
		||||
                .get(task=task, agent=agent)
 | 
			
		||||
            )
 | 
			
		||||
            serializer = TaskResultSerializer(
 | 
			
		||||
                data=request.data, instance=task_result, partial=True
 | 
			
		||||
            )
 | 
			
		||||
@@ -294,7 +354,7 @@ class TaskRunner(APIView):
 | 
			
		||||
 | 
			
		||||
        AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type="task_run",
 | 
			
		||||
            type=AuditActionType.TASK_RUN,
 | 
			
		||||
            command=task.name,
 | 
			
		||||
            script_results=request.data,
 | 
			
		||||
        )
 | 
			
		||||
@@ -305,11 +365,13 @@ class TaskRunner(APIView):
 | 
			
		||||
 | 
			
		||||
                task_result.save_collector_results()
 | 
			
		||||
 | 
			
		||||
                status = "passing"
 | 
			
		||||
                status = CheckStatus.PASSING
 | 
			
		||||
            else:
 | 
			
		||||
                status = "failing"
 | 
			
		||||
                status = CheckStatus.FAILING
 | 
			
		||||
        else:
 | 
			
		||||
            status = "failing" if task_result.retcode != 0 else "passing"
 | 
			
		||||
            status = (
 | 
			
		||||
                CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if task_result:
 | 
			
		||||
            task_result.status = status
 | 
			
		||||
@@ -318,7 +380,7 @@ class TaskRunner(APIView):
 | 
			
		||||
            task_result.status = status
 | 
			
		||||
            task.save(update_fields=["status"])
 | 
			
		||||
 | 
			
		||||
        if status == "passing":
 | 
			
		||||
        if status == CheckStatus.PASSING:
 | 
			
		||||
            if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True):
 | 
			
		||||
                Alert.handle_alert_resolve(task_result)
 | 
			
		||||
        else:
 | 
			
		||||
@@ -327,21 +389,6 @@ class TaskRunner(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SysInfo(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
 | 
			
		||||
        if not isinstance(request.data["sysinfo"], dict):
 | 
			
		||||
            return notify_error("err")
 | 
			
		||||
 | 
			
		||||
        agent.wmi_detail = request.data["sysinfo"]
 | 
			
		||||
        agent.save(update_fields=["wmi_detail"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshExe(APIView):
 | 
			
		||||
    """Sends the mesh exe to the installer"""
 | 
			
		||||
 | 
			
		||||
@@ -418,8 +465,8 @@ class NewAgent(APIView):
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=request.user,
 | 
			
		||||
            agent=agent.hostname,
 | 
			
		||||
            object_type="agent",
 | 
			
		||||
            action="agent_install",
 | 
			
		||||
            object_type=AuditObjType.AGENT,
 | 
			
		||||
            action=AuditActionType.AGENT_INSTALL,
 | 
			
		||||
            message=f"{request.user} installed new agent {agent.hostname}",
 | 
			
		||||
            after_value=Agent.serialize(agent),
 | 
			
		||||
            debug_info={"ip": request._client_ip},
 | 
			
		||||
@@ -491,7 +538,7 @@ class ChocoResult(APIView):
 | 
			
		||||
 | 
			
		||||
        action.details["output"] = results
 | 
			
		||||
        action.details["installed"] = installed
 | 
			
		||||
        action.status = "completed"
 | 
			
		||||
        action.status = PAStatus.COMPLETED
 | 
			
		||||
        action.save(update_fields=["details", "status"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
@@ -501,8 +548,9 @@ class AgentHistoryResult(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, agentid, pk):
 | 
			
		||||
        _ = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        hist = get_object_or_404(AgentHistory, pk=pk)
 | 
			
		||||
        hist = get_object_or_404(
 | 
			
		||||
            AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
 | 
			
		||||
        )
 | 
			
		||||
        s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
 | 
			
		||||
        s.is_valid(raise_exception=True)
 | 
			
		||||
        s.save()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from typing import Optional, Dict, Any, List, TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY
 | 
			
		||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, CheckType
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Policy(BaseAuditModel):
 | 
			
		||||
@@ -279,7 +279,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # Loop over checks in with enforced policies first, then non-enforced policies
 | 
			
		||||
        for check in enforced_checks + agent_checks + policy_checks:
 | 
			
		||||
            if check.check_type == "diskspace" and agent.plat == "windows":
 | 
			
		||||
            if check.check_type == CheckType.DISK_SPACE and agent.plat == "windows":
 | 
			
		||||
                # Check if drive letter was already added
 | 
			
		||||
                if check.disk not in added_diskspace_checks:
 | 
			
		||||
                    added_diskspace_checks.append(check.disk)
 | 
			
		||||
@@ -289,7 +289,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "ping":
 | 
			
		||||
            elif check.check_type == CheckType.PING:
 | 
			
		||||
                # Check if IP/host was already added
 | 
			
		||||
                if check.ip not in added_ping_checks:
 | 
			
		||||
                    added_ping_checks.append(check.ip)
 | 
			
		||||
@@ -299,7 +299,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "cpuload" and agent.plat == "windows":
 | 
			
		||||
            elif check.check_type == CheckType.CPU_LOAD and agent.plat == "windows":
 | 
			
		||||
                # Check if cpuload list is empty
 | 
			
		||||
                if not added_cpuload_checks:
 | 
			
		||||
                    added_cpuload_checks.append(check.pk)
 | 
			
		||||
@@ -309,7 +309,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "memory" and agent.plat == "windows":
 | 
			
		||||
            elif check.check_type == CheckType.MEMORY and agent.plat == "windows":
 | 
			
		||||
                # Check if memory check list is empty
 | 
			
		||||
                if not added_memory_checks:
 | 
			
		||||
                    added_memory_checks.append(check.pk)
 | 
			
		||||
@@ -319,7 +319,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "winsvc" and agent.plat == "windows":
 | 
			
		||||
            elif check.check_type == CheckType.WINSVC and agent.plat == "windows":
 | 
			
		||||
                # Check if service name was already added
 | 
			
		||||
                if check.svc_name not in added_winsvc_checks:
 | 
			
		||||
                    added_winsvc_checks.append(check.svc_name)
 | 
			
		||||
@@ -329,7 +329,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "script" and agent.is_supported_script(
 | 
			
		||||
            elif check.check_type == CheckType.SCRIPT and agent.is_supported_script(
 | 
			
		||||
                check.script.supported_platforms
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if script id was already added
 | 
			
		||||
@@ -341,7 +341,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "eventlog" and agent.plat == "windows":
 | 
			
		||||
            elif check.check_type == CheckType.EVENT_LOG and agent.plat == "windows":
 | 
			
		||||
                # Check if events were already added
 | 
			
		||||
                if [check.log_name, check.event_id] not in added_eventlog_checks:
 | 
			
		||||
                    added_eventlog_checks.append([check.log_name, check.event_id])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    ReadOnlyField,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    PolicyCheckStatusSerializer,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from autotasks.views import GetAddAutoTasks
 | 
			
		||||
from checks.views import GetAddChecks
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
from .permissions import AutomationPolicyPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from autotasks.tasks import remove_orphaned_win_tasks
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from autotasks.tasks import remove_orphaned_win_tasks
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Checks for orphaned tasks on all agents and removes them"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 3.2.12 on 2022-04-01 22:44
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
# Generated by Django 4.0.3 on 2022-04-15 20:52
 | 
			
		||||
 | 
			
		||||
import autotasks.models
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import autotasks.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,24 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import pytz
 | 
			
		||||
from typing import TYPE_CHECKING, List, Dict, Any, Optional, Union
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
import pytz
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import DateTimeField
 | 
			
		||||
from django.db.models.fields.json import JSONField
 | 
			
		||||
from django.db.utils import DatabaseError
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    FIELDS_TRIGGER_TASK_UPDATE_AGENT,
 | 
			
		||||
    POLICY_TASK_FIELDS_TO_COPY,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -366,7 +368,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to create scheduled task {self.name} on {task_result.agent.hostname}. It will be created when the agent checks in.",
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
@@ -375,7 +377,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname} task {self.name} was successfully created",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -405,7 +407,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to modify scheduled task {self.name} on {task_result.agent.hostname}({task_result.agent.agent_id}). It will try again on next agent checkin",
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
@@ -414,7 +416,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname} task {self.name} was successfully modified",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -448,7 +450,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin",
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
@@ -456,7 +458,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            self.delete()
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask, TaskResult
 | 
			
		||||
 | 
			
		||||
@@ -9,6 +9,7 @@ class TaskResultSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = TaskResult
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        read_only_fields = ("agent", "task")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -198,7 +199,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    def get_task_actions(self, obj):
 | 
			
		||||
        tmp = []
 | 
			
		||||
        actions_to_remove = []
 | 
			
		||||
        agent = self.context["agent"] if "agent" in self.context.keys() else obj.agent
 | 
			
		||||
        agent = self.context["agent"]
 | 
			
		||||
        for action in obj.actions:
 | 
			
		||||
            if action["type"] == "cmd":
 | 
			
		||||
                tmp.append(
 | 
			
		||||
@@ -216,7 +217,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
            elif action["type"] == "script":
 | 
			
		||||
                try:
 | 
			
		||||
                    script = Script.objects.get(pk=action["script"])
 | 
			
		||||
                except ObjectDoesNotExist:
 | 
			
		||||
                except Script.DoesNotExist:
 | 
			
		||||
                    # script doesn't exist so remove it
 | 
			
		||||
                    actions_to_remove.append(action["script"])
 | 
			
		||||
                    continue
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,16 @@ import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Union, Optional
 | 
			
		||||
from typing import Optional, Union
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.constants import DebugLogType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -83,7 +84,7 @@ def remove_orphaned_win_tasks() -> None:
 | 
			
		||||
        if not isinstance(r, list):  # empty list
 | 
			
		||||
            DebugLog.error(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to pull list of scheduled tasks on {agent.hostname}: {r}",
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
@@ -114,13 +115,13 @@ def remove_orphaned_win_tasks() -> None:
 | 
			
		||||
                if ret != "ok":
 | 
			
		||||
                    DebugLog.error(
 | 
			
		||||
                        agent=agent,
 | 
			
		||||
                        log_type="agent_issues",
 | 
			
		||||
                        log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                        message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}",
 | 
			
		||||
                    )
 | 
			
		||||
                else:
 | 
			
		||||
                    DebugLog.info(
 | 
			
		||||
                        agent=agent,
 | 
			
		||||
                        log_type="agent_issues",
 | 
			
		||||
                        log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                        message=f"Removed orphaned task {task} from {agent.hostname}",
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,25 @@
 | 
			
		||||
from model_bakery.recipe import Recipe
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import CheckType, EvtLogTypes
 | 
			
		||||
 | 
			
		||||
check = Recipe("checks.Check")
 | 
			
		||||
 | 
			
		||||
diskspace_check = check.extend(
 | 
			
		||||
    check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=10
 | 
			
		||||
    check_type=CheckType.DISK_SPACE, disk="C:", warning_threshold=30, error_threshold=10
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
cpuload_check = check.extend(
 | 
			
		||||
    check_type="cpuload", warning_threshold=30, error_threshold=75
 | 
			
		||||
    check_type=CheckType.CPU_LOAD, warning_threshold=30, error_threshold=75
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
 | 
			
		||||
 | 
			
		||||
memory_check = check.extend(
 | 
			
		||||
    check_type="memory", warning_threshold=60, error_threshold=75
 | 
			
		||||
    check_type=CheckType.MEMORY, warning_threshold=60, error_threshold=75
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
winsvc_check = check.extend(
 | 
			
		||||
    check_type="winsvc",
 | 
			
		||||
    check_type=CheckType.WINSVC,
 | 
			
		||||
    svc_name="ServiceName",
 | 
			
		||||
    svc_display_name="ServiceName",
 | 
			
		||||
    svc_policy_mode="manual",
 | 
			
		||||
@@ -25,9 +27,9 @@ winsvc_check = check.extend(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
eventlog_check = check.extend(
 | 
			
		||||
    check_type="eventlog", event_id=5000, event_type="application"
 | 
			
		||||
    check_type=CheckType.EVENT_LOG, event_id=5000, event_type=EvtLogTypes.INFO
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
script_check = check.extend(
 | 
			
		||||
    name="Script Name", check_type="script", script__name="Script Name"
 | 
			
		||||
    name="Script Name", check_type=CheckType.SCRIPT, script__name="Script Name"
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								api/tacticalrmm/checks/constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								api/tacticalrmm/checks/constants.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
CHECK_DEFER = (
 | 
			
		||||
    "created_by",
 | 
			
		||||
    "created_time",
 | 
			
		||||
    "modified_by",
 | 
			
		||||
    "modified_time",
 | 
			
		||||
    "timeout",
 | 
			
		||||
    "svc_display_name",
 | 
			
		||||
    "svc_policy_mode",
 | 
			
		||||
    "log_name",
 | 
			
		||||
    "event_id",
 | 
			
		||||
    "event_id_is_wildcard",
 | 
			
		||||
    "event_type",
 | 
			
		||||
    "event_source",
 | 
			
		||||
    "event_message",
 | 
			
		||||
    "fail_when",
 | 
			
		||||
    "search_last_days",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CHECK_RESULT_DEFER = (
 | 
			
		||||
    "more_info",
 | 
			
		||||
    "outage_history",
 | 
			
		||||
    "extra_details",
 | 
			
		||||
    "stdout",
 | 
			
		||||
    "stderr",
 | 
			
		||||
    "execution_time",
 | 
			
		||||
)
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
# Generated by Django 3.2.12 on 2022-04-01 22:44
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
from django.db import migrations, transaction
 | 
			
		||||
from django.db.utils import IntegrityError
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import CheckType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate_check_results(apps, schema_editor):
 | 
			
		||||
    Check = apps.get_model("checks", "Check")
 | 
			
		||||
@@ -28,7 +30,12 @@ def migrate_check_results(apps, schema_editor):
 | 
			
		||||
                        history=check.history,
 | 
			
		||||
                        alert_severity=check.alert_severity
 | 
			
		||||
                        if check.check_type
 | 
			
		||||
                        in ["cpuload", "memory", "diskspace", "script"]
 | 
			
		||||
                        in [
 | 
			
		||||
                            CheckType.MEMORY,
 | 
			
		||||
                            CheckType.CPU_LOAD,
 | 
			
		||||
                            CheckType.DISK_SPACE,
 | 
			
		||||
                            CheckType.SCRIPT,
 | 
			
		||||
                        ]
 | 
			
		||||
                        else None,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +56,12 @@ def migrate_check_results(apps, schema_editor):
 | 
			
		||||
                        history=check.history,
 | 
			
		||||
                        alert_severity=check.alert_severity
 | 
			
		||||
                        if check.check_type
 | 
			
		||||
                        in ["cpuload", "memory", "diskspace", "script"]
 | 
			
		||||
                        in [
 | 
			
		||||
                            CheckType.MEMORY,
 | 
			
		||||
                            CheckType.CPU_LOAD,
 | 
			
		||||
                            CheckType.DISK_SPACE,
 | 
			
		||||
                            CheckType.SCRIPT,
 | 
			
		||||
                        ]
 | 
			
		||||
                        else None,
 | 
			
		||||
                    )
 | 
			
		||||
        except IntegrityError:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,59 +1,31 @@
 | 
			
		||||
from statistics import mean
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Union, Dict, Optional
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    CHECKS_NON_EDITABLE_FIELDS,
 | 
			
		||||
    POLICY_CHECK_FIELDS_TO_COPY,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    CheckType,
 | 
			
		||||
    EvtLogFailWhen,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    EvtLogTypes,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from agents.models import Agent  # pragma: no cover
 | 
			
		||||
    from alerts.models import Alert, AlertTemplate  # pragma: no cover
 | 
			
		||||
    from automation.models import Policy  # pragma: no cover
 | 
			
		||||
 | 
			
		||||
CHECK_TYPE_CHOICES = [
 | 
			
		||||
    ("diskspace", "Disk Space Check"),
 | 
			
		||||
    ("ping", "Ping Check"),
 | 
			
		||||
    ("cpuload", "CPU Load Check"),
 | 
			
		||||
    ("memory", "Memory Check"),
 | 
			
		||||
    ("winsvc", "Service Check"),
 | 
			
		||||
    ("script", "Script Check"),
 | 
			
		||||
    ("eventlog", "Event Log Check"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CHECK_STATUS_CHOICES = [
 | 
			
		||||
    ("passing", "Passing"),
 | 
			
		||||
    ("failing", "Failing"),
 | 
			
		||||
    ("pending", "Pending"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
EVT_LOG_NAME_CHOICES = [
 | 
			
		||||
    ("Application", "Application"),
 | 
			
		||||
    ("System", "System"),
 | 
			
		||||
    ("Security", "Security"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
EVT_LOG_TYPE_CHOICES = [
 | 
			
		||||
    ("INFO", "Information"),
 | 
			
		||||
    ("WARNING", "Warning"),
 | 
			
		||||
    ("ERROR", "Error"),
 | 
			
		||||
    ("AUDIT_SUCCESS", "Success Audit"),
 | 
			
		||||
    ("AUDIT_FAILURE", "Failure Audit"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
EVT_LOG_FAIL_WHEN_CHOICES = [
 | 
			
		||||
    ("contains", "Log contains"),
 | 
			
		||||
    ("not_contains", "Log does not contain"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Check(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
@@ -77,7 +49,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
    overridden_by_policy = models.BooleanField(default=False)
 | 
			
		||||
    name = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    check_type = models.CharField(
 | 
			
		||||
        max_length=50, choices=CHECK_TYPE_CHOICES, default="diskspace"
 | 
			
		||||
        max_length=50, choices=CheckType.choices, default=CheckType.DISK_SPACE
 | 
			
		||||
    )
 | 
			
		||||
    email_alert = models.BooleanField(default=False)
 | 
			
		||||
    text_alert = models.BooleanField(default=False)
 | 
			
		||||
@@ -151,17 +123,17 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    # event log checks
 | 
			
		||||
    log_name = models.CharField(
 | 
			
		||||
        max_length=255, choices=EVT_LOG_NAME_CHOICES, null=True, blank=True
 | 
			
		||||
        max_length=255, choices=EvtLogNames.choices, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    event_id = models.IntegerField(null=True, blank=True)
 | 
			
		||||
    event_id_is_wildcard = models.BooleanField(default=False)
 | 
			
		||||
    event_type = models.CharField(
 | 
			
		||||
        max_length=255, choices=EVT_LOG_TYPE_CHOICES, null=True, blank=True
 | 
			
		||||
        max_length=255, choices=EvtLogTypes.choices, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    event_source = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    event_message = models.TextField(null=True, blank=True)
 | 
			
		||||
    fail_when = models.CharField(
 | 
			
		||||
        max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
 | 
			
		||||
        max_length=255, choices=EvtLogFailWhen.choices, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    search_last_days = models.PositiveIntegerField(null=True, blank=True)
 | 
			
		||||
    number_of_events_b4_alert = models.PositiveIntegerField(
 | 
			
		||||
@@ -215,7 +187,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
    @property
 | 
			
		||||
    def readable_desc(self):
 | 
			
		||||
        display = self.get_check_type_display()  # type: ignore
 | 
			
		||||
        if self.check_type == "diskspace":
 | 
			
		||||
        if self.check_type == CheckType.DISK_SPACE:
 | 
			
		||||
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.warning_threshold:
 | 
			
		||||
@@ -224,9 +196,11 @@ class Check(BaseAuditModel):
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            return f"{display}: Drive {self.disk} - {text}"
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
        elif self.check_type == CheckType.PING:
 | 
			
		||||
            return f"{display}: {self.name}"
 | 
			
		||||
        elif self.check_type == "cpuload" or self.check_type == "memory":
 | 
			
		||||
        elif (
 | 
			
		||||
            self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY
 | 
			
		||||
        ):
 | 
			
		||||
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.warning_threshold:
 | 
			
		||||
@@ -235,11 +209,11 @@ class Check(BaseAuditModel):
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            return f"{display} - {text}"
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
        elif self.check_type == CheckType.WINSVC:
 | 
			
		||||
            return f"{display}: {self.svc_display_name}"
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
        elif self.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
            return f"{display}: {self.name}"
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
        elif self.check_type == CheckType.SCRIPT:
 | 
			
		||||
            return f"{display}: {self.script.name}"
 | 
			
		||||
        else:
 | 
			
		||||
            return "n/a"
 | 
			
		||||
@@ -295,25 +269,22 @@ class Check(BaseAuditModel):
 | 
			
		||||
        return CheckAuditSerializer(check).data
 | 
			
		||||
 | 
			
		||||
    def is_duplicate(self, check):
 | 
			
		||||
        if self.check_type == "diskspace":
 | 
			
		||||
        if self.check_type == CheckType.DISK_SPACE:
 | 
			
		||||
            return self.disk == check.disk
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
        elif self.check_type == CheckType.SCRIPT:
 | 
			
		||||
            return self.script == check.script
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
        elif self.check_type == CheckType.PING:
 | 
			
		||||
            return self.ip == check.ip
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "cpuload":
 | 
			
		||||
        elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "memory":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
        elif self.check_type == CheckType.WINSVC:
 | 
			
		||||
            return self.svc_name == check.svc_name
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
        elif self.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
            return [self.log_name, self.event_id] == [check.log_name, check.event_id]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -335,7 +306,7 @@ class CheckResult(models.Model):
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=100, choices=CHECK_STATUS_CHOICES, default="pending"
 | 
			
		||||
        max_length=100, choices=CheckStatus.choices, default=CheckStatus.PENDING
 | 
			
		||||
    )
 | 
			
		||||
    # for memory, diskspace, script, and cpu checks where severity changes
 | 
			
		||||
    alert_severity = models.CharField(
 | 
			
		||||
@@ -365,10 +336,10 @@ class CheckResult(models.Model):
 | 
			
		||||
 | 
			
		||||
        # if check is a policy check clear cache on everything
 | 
			
		||||
        if not self.alert_severity and self.assigned_check.check_type in [
 | 
			
		||||
            "cpuload",
 | 
			
		||||
            "memory",
 | 
			
		||||
            "diskspace",
 | 
			
		||||
            "script",
 | 
			
		||||
            CheckType.MEMORY,
 | 
			
		||||
            CheckType.CPU_LOAD,
 | 
			
		||||
            CheckType.DISK_SPACE,
 | 
			
		||||
            CheckType.SCRIPT,
 | 
			
		||||
        ]:
 | 
			
		||||
            self.alert_severity = "warning"
 | 
			
		||||
 | 
			
		||||
@@ -379,10 +350,7 @@ class CheckResult(models.Model):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def history_info(self):
 | 
			
		||||
        if (
 | 
			
		||||
            self.assigned_check.check_type == "cpuload"
 | 
			
		||||
            or self.assigned_check.check_type == "memory"
 | 
			
		||||
        ):
 | 
			
		||||
        if self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
            return ", ".join(str(f"{x}%") for x in self.history[-6:])
 | 
			
		||||
 | 
			
		||||
    def get_or_create_alert_if_needed(
 | 
			
		||||
@@ -397,68 +365,67 @@ class CheckResult(models.Model):
 | 
			
		||||
            skip_create=not self.assigned_check.should_create_alert(alert_template),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle_check(self, data):
 | 
			
		||||
    def handle_check(self, data, check: "Check", agent: "Agent"):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        check = self.assigned_check
 | 
			
		||||
 | 
			
		||||
        update_fields = []
 | 
			
		||||
        # cpuload or mem checks
 | 
			
		||||
        if check.check_type == "cpuload" or check.check_type == "memory":
 | 
			
		||||
        if check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
 | 
			
		||||
            self.history.append(data["percent"])
 | 
			
		||||
 | 
			
		||||
            if len(self.history) > 15:
 | 
			
		||||
                self.history = self.history[-15:]
 | 
			
		||||
 | 
			
		||||
            self.save(update_fields=["history"])
 | 
			
		||||
            update_fields.extend(["history"])
 | 
			
		||||
 | 
			
		||||
            avg = int(mean(self.history))
 | 
			
		||||
 | 
			
		||||
            if check.error_threshold and avg > check.error_threshold:
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
                self.alert_severity = "error"
 | 
			
		||||
            elif check.warning_threshold and avg > check.warning_threshold:
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
                self.alert_severity = "warning"
 | 
			
		||||
            else:
 | 
			
		||||
                self.status = "passing"
 | 
			
		||||
                self.status = CheckStatus.PASSING
 | 
			
		||||
 | 
			
		||||
            # add check history
 | 
			
		||||
            check.add_check_history(data["percent"], self.agent.agent_id)
 | 
			
		||||
            check.add_check_history(data["percent"], agent.agent_id)
 | 
			
		||||
 | 
			
		||||
        # diskspace checks
 | 
			
		||||
        elif check.check_type == "diskspace":
 | 
			
		||||
        elif check.check_type == CheckType.DISK_SPACE:
 | 
			
		||||
            if data["exists"]:
 | 
			
		||||
                percent_used = round(data["percent_used"])
 | 
			
		||||
                if (
 | 
			
		||||
                    check.error_threshold
 | 
			
		||||
                    and (100 - percent_used) < check.error_threshold
 | 
			
		||||
                ):
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                    self.status = CheckStatus.FAILING
 | 
			
		||||
                    self.alert_severity = "error"
 | 
			
		||||
                elif (
 | 
			
		||||
                    check.warning_threshold
 | 
			
		||||
                    and (100 - percent_used) < check.warning_threshold
 | 
			
		||||
                ):
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                    self.status = CheckStatus.FAILING
 | 
			
		||||
                    self.alert_severity = "warning"
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                    self.status = CheckStatus.PASSING
 | 
			
		||||
 | 
			
		||||
                self.more_info = data["more_info"]
 | 
			
		||||
 | 
			
		||||
                # add check history
 | 
			
		||||
                check.add_check_history(100 - percent_used, self.agent.agent_id)
 | 
			
		||||
                check.add_check_history(100 - percent_used, agent.agent_id)
 | 
			
		||||
            else:
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
                self.alert_severity = "error"
 | 
			
		||||
                self.more_info = f"Disk {check.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
            update_fields.extend(["more_info"])
 | 
			
		||||
 | 
			
		||||
        # script checks
 | 
			
		||||
        elif check.check_type == "script":
 | 
			
		||||
        elif check.check_type == CheckType.SCRIPT:
 | 
			
		||||
            self.stdout = data["stdout"]
 | 
			
		||||
            self.stderr = data["stderr"]
 | 
			
		||||
            self.retcode = data["retcode"]
 | 
			
		||||
@@ -466,18 +433,18 @@ class CheckResult(models.Model):
 | 
			
		||||
 | 
			
		||||
            if data["retcode"] in check.info_return_codes:
 | 
			
		||||
                self.alert_severity = "info"
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
            elif data["retcode"] in check.warning_return_codes:
 | 
			
		||||
                self.alert_severity = "warning"
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
            elif data["retcode"] != 0:
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
                self.alert_severity = "error"
 | 
			
		||||
            else:
 | 
			
		||||
                self.status = "passing"
 | 
			
		||||
                self.status = CheckStatus.PASSING
 | 
			
		||||
 | 
			
		||||
            self.save(
 | 
			
		||||
                update_fields=[
 | 
			
		||||
            update_fields.extend(
 | 
			
		||||
                [
 | 
			
		||||
                    "stdout",
 | 
			
		||||
                    "stderr",
 | 
			
		||||
                    "retcode",
 | 
			
		||||
@@ -487,8 +454,8 @@ class CheckResult(models.Model):
 | 
			
		||||
 | 
			
		||||
            # add check history
 | 
			
		||||
            check.add_check_history(
 | 
			
		||||
                1 if self.status == "failing" else 0,
 | 
			
		||||
                self.agent.agent_id,
 | 
			
		||||
                1 if self.status == CheckStatus.FAILING else 0,
 | 
			
		||||
                agent.agent_id,
 | 
			
		||||
                {
 | 
			
		||||
                    "retcode": data["retcode"],
 | 
			
		||||
                    "stdout": data["stdout"][:60],
 | 
			
		||||
@@ -498,67 +465,73 @@ class CheckResult(models.Model):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # ping checks
 | 
			
		||||
        elif check.check_type == "ping":
 | 
			
		||||
        elif check.check_type == CheckType.PING:
 | 
			
		||||
            self.status = data["status"]
 | 
			
		||||
            self.more_info = data["output"]
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
            update_fields.extend(["more_info"])
 | 
			
		||||
 | 
			
		||||
            check.add_check_history(
 | 
			
		||||
                1 if self.status == "failing" else 0,
 | 
			
		||||
                self.agent.agent_id,
 | 
			
		||||
                1 if self.status == CheckStatus.FAILING else 0,
 | 
			
		||||
                agent.agent_id,
 | 
			
		||||
                self.more_info[:60],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # windows service checks
 | 
			
		||||
        elif check.check_type == "winsvc":
 | 
			
		||||
        elif check.check_type == CheckType.WINSVC:
 | 
			
		||||
            self.status = data["status"]
 | 
			
		||||
            self.more_info = data["more_info"]
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
            update_fields.extend(["more_info"])
 | 
			
		||||
 | 
			
		||||
            check.add_check_history(
 | 
			
		||||
                1 if self.status == "failing" else 0,
 | 
			
		||||
                self.agent.agent_id,
 | 
			
		||||
                1 if self.status == CheckStatus.FAILING else 0,
 | 
			
		||||
                agent.agent_id,
 | 
			
		||||
                self.more_info[:60],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif check.check_type == "eventlog":
 | 
			
		||||
        elif check.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
            log = data["log"]
 | 
			
		||||
            if check.fail_when == "contains":
 | 
			
		||||
            if check.fail_when == EvtLogFailWhen.CONTAINS:
 | 
			
		||||
                if log and len(log) >= check.number_of_events_b4_alert:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                    self.status = CheckStatus.FAILING
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                    self.status = CheckStatus.PASSING
 | 
			
		||||
 | 
			
		||||
            elif check.fail_when == "not_contains":
 | 
			
		||||
            elif check.fail_when == EvtLogFailWhen.NOT_CONTAINS:
 | 
			
		||||
                if log and len(log) >= check.number_of_events_b4_alert:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                    self.status = CheckStatus.PASSING
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                    self.status = CheckStatus.FAILING
 | 
			
		||||
 | 
			
		||||
            self.extra_details = {"log": log}
 | 
			
		||||
            self.save(update_fields=["extra_details"])
 | 
			
		||||
            update_fields.extend(["extra_details"])
 | 
			
		||||
 | 
			
		||||
            check.add_check_history(
 | 
			
		||||
                1 if self.status == "failing" else 0,
 | 
			
		||||
                self.agent.agent_id,
 | 
			
		||||
                1 if self.status == CheckStatus.FAILING else 0,
 | 
			
		||||
                agent.agent_id,
 | 
			
		||||
                "Events Found:" + str(len(self.extra_details["log"])),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.last_run = djangotime.now()
 | 
			
		||||
        # handle status
 | 
			
		||||
        if self.status == "failing":
 | 
			
		||||
        if self.status == CheckStatus.FAILING:
 | 
			
		||||
            self.fail_count += 1
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
            update_fields.extend(["status", "fail_count", "alert_severity", "last_run"])
 | 
			
		||||
            self.save(update_fields=update_fields)
 | 
			
		||||
 | 
			
		||||
            if self.fail_count >= check.fails_b4_alert:
 | 
			
		||||
                Alert.handle_alert_failure(self)
 | 
			
		||||
 | 
			
		||||
        elif self.status == "passing":
 | 
			
		||||
        elif self.status == CheckStatus.PASSING:
 | 
			
		||||
            self.fail_count = 0
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
            update_fields.extend(["status", "fail_count", "alert_severity", "last_run"])
 | 
			
		||||
            self.save(update_fields=update_fields)
 | 
			
		||||
            if Alert.objects.filter(
 | 
			
		||||
                assigned_check=check, agent=self.agent, resolved=False
 | 
			
		||||
                assigned_check=check, agent=agent, resolved=False
 | 
			
		||||
            ).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(self)
 | 
			
		||||
        else:
 | 
			
		||||
            update_fields.extend(["last_run"])
 | 
			
		||||
            self.save(update_fields=update_fields)
 | 
			
		||||
 | 
			
		||||
        return self.status
 | 
			
		||||
 | 
			
		||||
@@ -572,7 +545,7 @@ class CheckResult(models.Model):
 | 
			
		||||
        else:
 | 
			
		||||
            subject = f"{self} Failed"
 | 
			
		||||
 | 
			
		||||
        if self.assigned_check.check_type == "diskspace":
 | 
			
		||||
        if self.assigned_check.check_type == CheckType.DISK_SPACE:
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.assigned_check.warning_threshold:
 | 
			
		||||
                text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
 | 
			
		||||
@@ -591,21 +564,18 @@ class CheckResult(models.Model):
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.assigned_check.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == "script":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.SCRIPT:
 | 
			
		||||
 | 
			
		||||
            body = (
 | 
			
		||||
                subject
 | 
			
		||||
                + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == "ping":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.PING:
 | 
			
		||||
 | 
			
		||||
            body = self.more_info
 | 
			
		||||
 | 
			
		||||
        elif (
 | 
			
		||||
            self.assigned_check.check_type == "cpuload"
 | 
			
		||||
            or self.assigned_check.check_type == "memory"
 | 
			
		||||
        ):
 | 
			
		||||
        elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.assigned_check.warning_threshold:
 | 
			
		||||
                text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
 | 
			
		||||
@@ -614,16 +584,16 @@ class CheckResult(models.Model):
 | 
			
		||||
 | 
			
		||||
            avg = int(mean(self.history))
 | 
			
		||||
 | 
			
		||||
            if self.assigned_check.check_type == "cpuload":
 | 
			
		||||
            if self.assigned_check.check_type == CheckType.CPU_LOAD:
 | 
			
		||||
                body = subject + f" - Average CPU utilization: {avg}%, {text}"
 | 
			
		||||
 | 
			
		||||
            elif self.assigned_check.check_type == "memory":
 | 
			
		||||
            elif self.assigned_check.check_type == CheckType.MEMORY:
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == "winsvc":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.WINSVC:
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == "eventlog":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
 | 
			
		||||
            if self.assigned_check.event_source and self.assigned_check.event_message:
 | 
			
		||||
                start = f"Event ID {self.assigned_check.event_id}, source {self.assigned_check.event_source}, containing string {self.assigned_check.event_message} "
 | 
			
		||||
@@ -655,7 +625,7 @@ class CheckResult(models.Model):
 | 
			
		||||
        else:
 | 
			
		||||
            subject = f"{self} Failed"
 | 
			
		||||
 | 
			
		||||
        if self.assigned_check.check_type == "diskspace":
 | 
			
		||||
        if self.assigned_check.check_type == CheckType.DISK_SPACE:
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.assigned_check.warning_threshold:
 | 
			
		||||
                text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
 | 
			
		||||
@@ -673,14 +643,11 @@ class CheckResult(models.Model):
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.assigned_check.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == "script":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.SCRIPT:
 | 
			
		||||
            body = subject + f" - Return code: {self.retcode}"
 | 
			
		||||
        elif self.assigned_check.check_type == "ping":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.PING:
 | 
			
		||||
            body = subject
 | 
			
		||||
        elif (
 | 
			
		||||
            self.assigned_check.check_type == "cpuload"
 | 
			
		||||
            or self.assigned_check.check_type == "memory"
 | 
			
		||||
        ):
 | 
			
		||||
        elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.assigned_check.warning_threshold:
 | 
			
		||||
                text += f" Warning Threshold: {self.assigned_check.warning_threshold}%"
 | 
			
		||||
@@ -688,13 +655,13 @@ class CheckResult(models.Model):
 | 
			
		||||
                text += f" Error Threshold: {self.assigned_check.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            avg = int(mean(self.history))
 | 
			
		||||
            if self.assigned_check.check_type == "cpuload":
 | 
			
		||||
            if self.assigned_check.check_type == CheckType.CPU_LOAD:
 | 
			
		||||
                body = subject + f" - Average CPU utilization: {avg}%, {text}"
 | 
			
		||||
            elif self.assigned_check.check_type == "memory":
 | 
			
		||||
            elif self.assigned_check.check_type == CheckType.MEMORY:
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
        elif self.assigned_check.check_type == "winsvc":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.WINSVC:
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
        elif self.assigned_check.check_type == "eventlog":
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
            body = subject
 | 
			
		||||
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
import validators as _v
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from scripts.serializers import ScriptCheckSerializer
 | 
			
		||||
from tacticalrmm.constants import CheckType
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory, CheckResult
 | 
			
		||||
 | 
			
		||||
@@ -67,9 +69,11 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        # disk checks
 | 
			
		||||
        # make sure no duplicate diskchecks exist for an agent/policy
 | 
			
		||||
        if check_type == "diskspace":
 | 
			
		||||
        if check_type == CheckType.DISK_SPACE:
 | 
			
		||||
            if not self.instance:  # only on create
 | 
			
		||||
                checks = Check.objects.filter(**filter).filter(check_type="diskspace")
 | 
			
		||||
                checks = Check.objects.filter(**filter).filter(
 | 
			
		||||
                    check_type=CheckType.DISK_SPACE
 | 
			
		||||
                )
 | 
			
		||||
                for check in checks:
 | 
			
		||||
                    if val["disk"] in check.disk:
 | 
			
		||||
                        raise serializers.ValidationError(
 | 
			
		||||
@@ -91,7 +95,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # ping checks
 | 
			
		||||
        if check_type == "ping":
 | 
			
		||||
        if check_type == CheckType.PING:
 | 
			
		||||
            if (
 | 
			
		||||
                not _v.ipv4(val["ip"])
 | 
			
		||||
                and not _v.ipv6(val["ip"])
 | 
			
		||||
@@ -101,8 +105,8 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
                    "Please enter a valid IP address or domain name"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        if check_type == "cpuload" and not self.instance:
 | 
			
		||||
            if Check.objects.filter(**filter, check_type="cpuload").exists():
 | 
			
		||||
        if check_type == CheckType.CPU_LOAD and not self.instance:
 | 
			
		||||
            if Check.objects.filter(**filter, check_type=CheckType.CPU_LOAD).exists():
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    "A cpuload check for this agent already exists"
 | 
			
		||||
                )
 | 
			
		||||
@@ -121,8 +125,8 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
                    f"Warning threshold must be less than Error Threshold"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        if check_type == "memory" and not self.instance:
 | 
			
		||||
            if Check.objects.filter(**filter, check_type="memory").exists():
 | 
			
		||||
        if check_type == CheckType.MEMORY and not self.instance:
 | 
			
		||||
            if Check.objects.filter(**filter, check_type=CheckType.MEMORY).exists():
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    "A memory check for this agent already exists"
 | 
			
		||||
                )
 | 
			
		||||
@@ -156,7 +160,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    script_args = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_script_args(self, obj):
 | 
			
		||||
        if obj.check_type != "script":
 | 
			
		||||
        if obj.check_type != CheckType.SCRIPT:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        agent = self.context["agent"] if "agent" in self.context.keys() else obj.agent
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from checks.models import CheckHistory, CheckResult
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.test import modify_settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from checks.models import CheckHistory, CheckResult
 | 
			
		||||
from tacticalrmm.constants import CheckStatus, CheckType, EvtLogFailWhen, EvtLogTypes
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .serializers import CheckSerializer
 | 
			
		||||
@@ -13,11 +13,6 @@ from .serializers import CheckSerializer
 | 
			
		||||
base_url = "/checks"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestCheckViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -84,7 +79,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent_payload = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "check_type": CheckType.DISK_SPACE,
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
@@ -93,7 +88,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        policy_payload = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "check_type": CheckType.DISK_SPACE,
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
@@ -133,7 +128,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent_payload = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "cpuload",
 | 
			
		||||
            "check_type": CheckType.CPU_LOAD,
 | 
			
		||||
            "error_threshold": 66,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 9,
 | 
			
		||||
@@ -141,7 +136,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        policy_payload = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "cpuload",
 | 
			
		||||
            "check_type": CheckType.CPU_LOAD,
 | 
			
		||||
            "error_threshold": 66,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 9,
 | 
			
		||||
@@ -180,7 +175,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent_payload = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "memory",
 | 
			
		||||
            "check_type": CheckType.MEMORY,
 | 
			
		||||
            "error_threshold": 78,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 1,
 | 
			
		||||
@@ -188,7 +183,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        policy_payload = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "memory",
 | 
			
		||||
            "check_type": CheckType.MEMORY,
 | 
			
		||||
            "error_threshold": 78,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 1,
 | 
			
		||||
@@ -347,7 +342,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
@@ -365,7 +360,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
        # test failing info
 | 
			
		||||
        check.info_return_codes = [20, 30, 50]
 | 
			
		||||
@@ -385,7 +380,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
@@ -406,7 +401,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
    def test_handle_diskspace_check(self):
 | 
			
		||||
@@ -435,7 +430,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test error failure
 | 
			
		||||
@@ -454,7 +449,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test disk not exist
 | 
			
		||||
@@ -465,7 +460,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test warning threshold 0
 | 
			
		||||
@@ -486,7 +481,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test error threshold 0
 | 
			
		||||
@@ -507,7 +502,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
@@ -526,7 +521,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
    def test_handle_cpuload_check(self):
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
@@ -545,7 +540,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
@@ -559,7 +554,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
@@ -573,7 +568,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
        # test warning threshold 0
 | 
			
		||||
        check.warning_threshold = 0
 | 
			
		||||
@@ -588,7 +583,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test error threshold 0
 | 
			
		||||
@@ -605,7 +600,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
    def test_handle_memory_check(self):
 | 
			
		||||
@@ -625,7 +620,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
@@ -639,7 +634,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
@@ -653,7 +648,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
        # test warning threshold 0
 | 
			
		||||
        check.warning_threshold = 0
 | 
			
		||||
@@ -668,7 +663,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test error threshold 0
 | 
			
		||||
@@ -685,7 +680,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check_result.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
    def test_handle_ping_check(self):
 | 
			
		||||
@@ -699,7 +694,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": check.id,
 | 
			
		||||
            "agent_id": self.agent.agent_id,
 | 
			
		||||
            "status": "failing",
 | 
			
		||||
            "status": CheckStatus.FAILING,
 | 
			
		||||
            "output": "reply from a.com",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -707,7 +702,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
@@ -718,7 +713,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
@@ -729,7 +724,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
@@ -737,14 +732,14 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": check.id,
 | 
			
		||||
            "agent_id": self.agent.agent_id,
 | 
			
		||||
            "status": "passing",
 | 
			
		||||
            "status": CheckStatus.PASSING,
 | 
			
		||||
            "output": "reply from a.com",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -752,7 +747,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_handle_winsvc_check(self, nats_cmd):
 | 
			
		||||
@@ -766,7 +761,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": check.id,
 | 
			
		||||
            "agent_id": self.agent.agent_id,
 | 
			
		||||
            "status": "passing",
 | 
			
		||||
            "status": CheckStatus.PASSING,
 | 
			
		||||
            "more_info": "ok",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -774,13 +769,13 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
        # test failing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": check.id,
 | 
			
		||||
            "agent_id": self.agent.agent_id,
 | 
			
		||||
            "status": "failing",
 | 
			
		||||
            "status": CheckStatus.FAILING,
 | 
			
		||||
            "more_info": "ok",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -788,7 +783,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
    def test_handle_eventlog_check(self):
 | 
			
		||||
@@ -796,8 +791,8 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check = baker.make_recipe(
 | 
			
		||||
            "checks.eventlog_check",
 | 
			
		||||
            event_type="warning",
 | 
			
		||||
            fail_when="contains",
 | 
			
		||||
            event_type=EvtLogTypes.WARNING,
 | 
			
		||||
            fail_when=EvtLogFailWhen.CONTAINS,
 | 
			
		||||
            event_id=123,
 | 
			
		||||
            alert_severity="warning",
 | 
			
		||||
            agent=self.agent,
 | 
			
		||||
@@ -843,7 +838,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check.alert_severity, "warning")
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
 | 
			
		||||
        # test passing when contains
 | 
			
		||||
        resp = self.client.patch(url, no_logs_data, format="json")
 | 
			
		||||
@@ -851,10 +846,10 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
        # test failing when not contains and message and source
 | 
			
		||||
        check.fail_when = "not_contains"
 | 
			
		||||
        check.fail_when = EvtLogFailWhen.NOT_CONTAINS
 | 
			
		||||
        check.alert_severity = "error"
 | 
			
		||||
        check.save()
 | 
			
		||||
 | 
			
		||||
@@ -863,7 +858,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "failing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.FAILING)
 | 
			
		||||
        self.assertEqual(check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing when contains with source and message
 | 
			
		||||
@@ -872,7 +867,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(check_result.status, "passing")
 | 
			
		||||
        self.assertEqual(check_result.status, CheckStatus.PASSING)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
@@ -945,7 +940,7 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        policy_data = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "check_type": CheckType.DISK_SPACE,
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
@@ -954,7 +949,7 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent_data = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "check_type": CheckType.DISK_SPACE,
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
@@ -963,7 +958,7 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        unauthorized_agent_data = {
 | 
			
		||||
            "agent": unauthorized_agent.agent_id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "check_type": CheckType.DISK_SPACE,
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from datetime import datetime as dt
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
@@ -13,8 +10,12 @@ from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from tacticalrmm.constants import CheckStatus, CheckType
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory, CheckResult
 | 
			
		||||
from .permissions import ChecksPerms, RunChecksPerms
 | 
			
		||||
@@ -48,7 +49,7 @@ class GetAddChecks(APIView):
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
        # will be ignored anyway by the agent when doing wildcard check
 | 
			
		||||
        if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]:
 | 
			
		||||
        if data["check_type"] == CheckType.EVENT_LOG and data["event_id_is_wildcard"]:
 | 
			
		||||
            data["event_id"] = 0
 | 
			
		||||
 | 
			
		||||
        serializer = CheckSerializer(data=data, partial=True)
 | 
			
		||||
@@ -81,7 +82,7 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
        # will be ignored anyway by the agent when doing wildcard check
 | 
			
		||||
        if check.check_type == "eventlog":
 | 
			
		||||
        if check.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
            try:
 | 
			
		||||
                data["event_id_is_wildcard"]
 | 
			
		||||
            except KeyError:
 | 
			
		||||
@@ -116,7 +117,7 @@ class ResetCheck(APIView):
 | 
			
		||||
        if result.agent and not _has_perm_on_agent(request.user, result.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        result.status = "passing"
 | 
			
		||||
        result.status = CheckStatus.PASSING
 | 
			
		||||
        result.save()
 | 
			
		||||
 | 
			
		||||
        # resolve any alerts that are open
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
# Generated by Django 3.2.10 on 2021-12-26 05:47
 | 
			
		||||
 | 
			
		||||
import clients.models
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import clients.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import uuid
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from typing import Dict
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,19 +2,19 @@ import datetime as dt
 | 
			
		||||
import re
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from django.db.models import Count, Exists, OuterRef, Prefetch, prefetch_related_objects
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from django.db.models import OuterRef, Exists, Count, Prefetch, prefetch_related_objects
 | 
			
		||||
from knox.models import AuthToken
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from knox.models import AuthToken
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .permissions import ClientsPerms, DeploymentPerms, SitesPerms
 | 
			
		||||
@@ -42,7 +42,8 @@ class GetAddClients(APIView):
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "sites",
 | 
			
		||||
                    queryset=Site.objects.select_related("client")
 | 
			
		||||
                    queryset=Site.objects.order_by("name")
 | 
			
		||||
                    .select_related("client")
 | 
			
		||||
                    .filter_by_role(request.user)
 | 
			
		||||
                    .prefetch_related("custom_fields__field")
 | 
			
		||||
                    .annotate(
 | 
			
		||||
@@ -119,7 +120,8 @@ class GetUpdateDeleteClient(APIView):
 | 
			
		||||
            [client],
 | 
			
		||||
            Prefetch(
 | 
			
		||||
                "sites",
 | 
			
		||||
                queryset=Site.objects.select_related("client")
 | 
			
		||||
                queryset=Site.objects.order_by("name")
 | 
			
		||||
                .select_related("client")
 | 
			
		||||
                .filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("custom_fields__field")
 | 
			
		||||
                .annotate(
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ meshSystemBin="${meshDir}/meshagent"
 | 
			
		||||
meshSvcName='meshagent.service'
 | 
			
		||||
meshSysD="/lib/systemd/system/${meshSvcName}"
 | 
			
		||||
 | 
			
		||||
deb=(ubuntu debian raspbian kali)
 | 
			
		||||
deb=(ubuntu debian raspbian kali linuxmint)
 | 
			
		||||
rhe=(fedora rocky centos rhel amzn arch opensuse)
 | 
			
		||||
 | 
			
		||||
set_locale_deb() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
 | 
			
		||||
from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DashInfo(AsyncJsonWebsocketConsumer):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,24 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    get_mesh_device_id,
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
)
 | 
			
		||||
from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Mesh troubleshooting script"
 | 
			
		||||
 | 
			
		||||
    def _success(self, *args):
 | 
			
		||||
    def _success(self, *args) -> None:
 | 
			
		||||
        self.stdout.write(self.style.SUCCESS(" ".join(args)))
 | 
			
		||||
 | 
			
		||||
    def _error(self, *args):
 | 
			
		||||
    def _error(self, *args) -> None:
 | 
			
		||||
        self.stdout.write(self.style.ERROR(" ".join(args)))
 | 
			
		||||
 | 
			
		||||
    def _warning(self, *args):
 | 
			
		||||
    def _warning(self, *args) -> None:
 | 
			
		||||
        self.stdout.write(self.style.WARNING(" ".join(args)))
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
    def handle(self, *args, **kwargs) -> None:
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        self._warning("Mesh site:", core.mesh_site)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,10 @@ import asyncio
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
import websockets
 | 
			
		||||
from core.utils import get_mesh_ws_url
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from core.utils import get_mesh_ws_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Sets up initial mesh central configuration"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Populates the global site settings on first install"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,11 @@ import asyncio
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
import websockets
 | 
			
		||||
from core.utils import get_mesh_ws_url, get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings, get_mesh_ws_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Sets up initial mesh central configuration"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "loads scripts for demo"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        scripts_dir = settings.BASE_DIR.joinpath("tacticalrmm/test_data/demo_scripts")
 | 
			
		||||
        scripts = Script.objects.filter(script_type="userdefined")
 | 
			
		||||
        for script in scripts:
 | 
			
		||||
            filepath = scripts_dir.joinpath(script.filename)
 | 
			
		||||
            with open(filepath, "rb") as f:
 | 
			
		||||
                script.script_body = f.read().decode("utf-8")
 | 
			
		||||
                script.save(update_fields=["script_body"])
 | 
			
		||||
 | 
			
		||||
        self.stdout.write(self.style.SUCCESS("Added userdefined scripts"))
 | 
			
		||||
@@ -1,14 +1,13 @@
 | 
			
		||||
import base64
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check, CheckHistory
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER, ScriptType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@@ -27,7 +26,7 @@ class Command(BaseCommand):
 | 
			
		||||
                user.save()
 | 
			
		||||
 | 
			
		||||
        # convert script base64 field to text field
 | 
			
		||||
        user_scripts = Script.objects.exclude(script_type="builtin").filter(
 | 
			
		||||
        user_scripts = Script.objects.exclude(script_type=ScriptType.BUILT_IN).filter(
 | 
			
		||||
            script_body=""
 | 
			
		||||
        )
 | 
			
		||||
        for script in user_scripts:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from core.utils import clear_entire_cache
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,12 @@ from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.tasks import agent_outages_task, auto_self_agent_update_task
 | 
			
		||||
from alerts.tasks import unsnooze_alerts
 | 
			
		||||
from autotasks.tasks import remove_orphaned_win_tasks
 | 
			
		||||
from core.tasks import (
 | 
			
		||||
    cache_db_fields_task,
 | 
			
		||||
    core_maintenance_tasks,
 | 
			
		||||
    handle_resolved_stuff,
 | 
			
		||||
)
 | 
			
		||||
from autotasks.tasks import remove_orphaned_win_tasks
 | 
			
		||||
from winupdate.tasks import auto_approve_updates_task, check_agent_update_schedule_task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
import smtplib
 | 
			
		||||
from email.message import EmailMessage
 | 
			
		||||
from typing import TYPE_CHECKING, List, Optional, Union, cast
 | 
			
		||||
 | 
			
		||||
from typing import Optional, Union, List, cast, TYPE_CHECKING
 | 
			
		||||
import pytz
 | 
			
		||||
import requests
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from logs.models import LOG_LEVEL_CHOICES, BaseAuditModel, DebugLog
 | 
			
		||||
from twilio.base.exceptions import TwilioRestException
 | 
			
		||||
from twilio.rest import Client as TwClient
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, DebugLogLevel
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from alerts.models import AlertTemplate
 | 
			
		||||
@@ -57,7 +57,7 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
    debug_log_prune_days = models.PositiveIntegerField(default=30)
 | 
			
		||||
    audit_log_prune_days = models.PositiveIntegerField(default=0)
 | 
			
		||||
    agent_debug_level = models.CharField(
 | 
			
		||||
        max_length=20, choices=LOG_LEVEL_CHOICES, default="info"
 | 
			
		||||
        max_length=20, choices=DebugLogLevel.choices, default=DebugLogLevel.INFO
 | 
			
		||||
    )
 | 
			
		||||
    clear_faults_days = models.IntegerField(default=0)
 | 
			
		||||
    mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,22 @@
 | 
			
		||||
from typing import Dict, Any, TYPE_CHECKING
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Prefetch
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import clear_faults_task, prune_agent_history
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from alerts.tasks import prune_resolved_alerts
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from checks.tasks import prune_check_history
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Prefetch
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from logs.tasks import prune_audit_log, prune_debug_log
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER, PAAction, PAStatus
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from django.db.models import QuerySet
 | 
			
		||||
@@ -58,7 +58,7 @@ def handle_resolved_stuff() -> None:
 | 
			
		||||
    actions = (
 | 
			
		||||
        PendingAction.objects.select_related("agent")
 | 
			
		||||
        .defer("agent__services", "agent__wmi_detail")
 | 
			
		||||
        .filter(action_type="agentupdate", status="pending")
 | 
			
		||||
        .filter(action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    to_update = [
 | 
			
		||||
@@ -68,7 +68,7 @@ def handle_resolved_stuff() -> None:
 | 
			
		||||
        and action.agent.status == "online"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    PendingAction.objects.filter(pk__in=to_update).update(status="completed")
 | 
			
		||||
    PendingAction.objects.filter(pk__in=to_update).update(status=PAStatus.COMPLETED)
 | 
			
		||||
 | 
			
		||||
    agent_queryset = (
 | 
			
		||||
        Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,22 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.testing import WebsocketCommunicator
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from rest_framework.authtoken.models import Token
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.constants import PAAction, PAStatus
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .consumers import DashInfo
 | 
			
		||||
from .models import CustomField, GlobalKVStore, URLAction
 | 
			
		||||
from .serializers import CustomFieldSerializer, KeyStoreSerializer, URLActionSerializer
 | 
			
		||||
from .tasks import core_maintenance_tasks, handle_resolved_stuff
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCodeSign(TacticalTestCase):
 | 
			
		||||
@@ -410,10 +412,10 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        handle_resolved_stuff()
 | 
			
		||||
 | 
			
		||||
        complete = PendingAction.objects.filter(
 | 
			
		||||
            action_type="agentupdate", status="completed"
 | 
			
		||||
            action_type=PAAction.AGENT_UPDATE, status=PAStatus.COMPLETED
 | 
			
		||||
        ).count()
 | 
			
		||||
        old = PendingAction.objects.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
            action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
        ).count()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(complete, 20)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
import json
 | 
			
		||||
import tempfile
 | 
			
		||||
from base64 import b64encode
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
from typing import TYPE_CHECKING, cast
 | 
			
		||||
 | 
			
		||||
from typing import cast, TYPE_CHECKING
 | 
			
		||||
import requests
 | 
			
		||||
import websockets
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.http import FileResponse
 | 
			
		||||
from meshctrl.utils import get_auth_token
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, ROLE_CACHE_PREFIX
 | 
			
		||||
 | 
			
		||||
@@ -21,14 +21,14 @@ class CoreSettingsNotFound(Exception):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clear_entire_cache() -> None:
 | 
			
		||||
    cache.delete(f"{ROLE_CACHE_PREFIX}*")
 | 
			
		||||
    cache.delete_many_pattern(f"{ROLE_CACHE_PREFIX}*")
 | 
			
		||||
    cache.delete(CORESETTINGS_CACHE_KEY)
 | 
			
		||||
    cache.delete_many_pattern("site_*")
 | 
			
		||||
    cache.delete_many_pattern("agent_*")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_core_settings() -> "CoreSettings":
 | 
			
		||||
    from core.models import CoreSettings, CORESETTINGS_CACHE_KEY
 | 
			
		||||
    from core.models import CORESETTINGS_CACHE_KEY, CoreSettings
 | 
			
		||||
 | 
			
		||||
    coresettings = cache.get(CORESETTINGS_CACHE_KEY)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,20 +2,21 @@ import re
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.constants import AuditActionType, PAStatus
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm_on_agent,
 | 
			
		||||
    _has_perm_on_client,
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
 | 
			
		||||
from .permissions import (
 | 
			
		||||
@@ -132,12 +133,12 @@ def server_maintenance(request):
 | 
			
		||||
        tables = request.data["prune_tables"]
 | 
			
		||||
        records_count = 0
 | 
			
		||||
        if "audit_logs" in tables:
 | 
			
		||||
            auditlogs = AuditLog.objects.filter(action="check_run")
 | 
			
		||||
            auditlogs = AuditLog.objects.filter(action=AuditActionType.CHECK_RUN)
 | 
			
		||||
            records_count += auditlogs.count()
 | 
			
		||||
            auditlogs.delete()
 | 
			
		||||
 | 
			
		||||
        if "pending_actions" in tables:
 | 
			
		||||
            pendingactions = PendingAction.objects.filter(status="completed")
 | 
			
		||||
            pendingactions = PendingAction.objects.filter(status=PAStatus.COMPLETED)
 | 
			
		||||
            records_count += pendingactions.count()
 | 
			
		||||
            pendingactions.delete()
 | 
			
		||||
 | 
			
		||||
@@ -332,10 +333,10 @@ class RunURLAction(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, URLActionPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from clients.models import Client, Site
 | 
			
		||||
        from requests.utils import requote_uri
 | 
			
		||||
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from clients.models import Client, Site
 | 
			
		||||
        from tacticalrmm.utils import replace_db_values
 | 
			
		||||
 | 
			
		||||
        if "agent_id" in request.data.keys():
 | 
			
		||||
 
 | 
			
		||||
@@ -2,31 +2,48 @@ from itertools import cycle
 | 
			
		||||
 | 
			
		||||
from model_bakery.recipe import Recipe
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AuditActionType, AuditObjType, PAAction, PAStatus
 | 
			
		||||
 | 
			
		||||
object_types = [
 | 
			
		||||
    "user",
 | 
			
		||||
    "script",
 | 
			
		||||
    "agent",
 | 
			
		||||
    "policy",
 | 
			
		||||
    "winupdatepolicy",
 | 
			
		||||
    "client",
 | 
			
		||||
    "site",
 | 
			
		||||
    "check",
 | 
			
		||||
    "automatedtask",
 | 
			
		||||
    "coresettings",
 | 
			
		||||
    AuditObjType.USER,
 | 
			
		||||
    AuditObjType.SCRIPT,
 | 
			
		||||
    AuditObjType.AGENT,
 | 
			
		||||
    AuditObjType.POLICY,
 | 
			
		||||
    AuditObjType.WINUPDATE,
 | 
			
		||||
    AuditObjType.CLIENT,
 | 
			
		||||
    AuditObjType.SITE,
 | 
			
		||||
    AuditObjType.CHECK,
 | 
			
		||||
    AuditObjType.AUTOTASK,
 | 
			
		||||
    AuditObjType.CORE,
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
object_actions = ["add", "modify", "view", "delete"]
 | 
			
		||||
agent_actions = ["remote_session", "execute_script", "execute_command"]
 | 
			
		||||
login_actions = ["failed_login", "login"]
 | 
			
		||||
object_actions = [
 | 
			
		||||
    AuditActionType.ADD,
 | 
			
		||||
    AuditActionType.MODIFY,
 | 
			
		||||
    AuditActionType.VIEW,
 | 
			
		||||
    AuditActionType.DELETE,
 | 
			
		||||
]
 | 
			
		||||
agent_actions = [
 | 
			
		||||
    AuditActionType.REMOTE_SESSION,
 | 
			
		||||
    AuditActionType.EXEC_SCRIPT,
 | 
			
		||||
    AuditActionType.EXEC_COMMAND,
 | 
			
		||||
]
 | 
			
		||||
login_actions = [AuditActionType.FAILED_LOGIN, AuditActionType.LOGIN]
 | 
			
		||||
 | 
			
		||||
agent_logs = Recipe("logs.AuditLog", action=cycle(agent_actions), object_type="agent")
 | 
			
		||||
agent_logs = Recipe(
 | 
			
		||||
    "logs.AuditLog", action=cycle(agent_actions), object_type=AuditObjType.AGENT
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
object_logs = Recipe(
 | 
			
		||||
    "logs.AuditLog", action=cycle(object_actions), object_type=cycle(object_types)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
login_logs = Recipe("logs.AuditLog", action=cycle(login_actions), object_type="user")
 | 
			
		||||
login_logs = Recipe(
 | 
			
		||||
    "logs.AuditLog", action=cycle(login_actions), object_type=AuditObjType.USER
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
pending_agentupdate_action = Recipe(
 | 
			
		||||
    "logs.PendingAction", action_type="agentupdate", status="pending"
 | 
			
		||||
    "logs.PendingAction",
 | 
			
		||||
    action_type=PAAction.AGENT_UPDATE,
 | 
			
		||||
    status=PAStatus.PENDING,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-04-25 06:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('logs', '0023_alter_pendingaction_action_type'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='pendingaction',
 | 
			
		||||
            name='cancelable',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='pendingaction',
 | 
			
		||||
            name='celery_id',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,80 +1,37 @@
 | 
			
		||||
from abc import abstractmethod
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from typing import Optional, Dict, Any, Union, cast, Tuple, TYPE_CHECKING
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AuditActionType,
 | 
			
		||||
    AuditObjType,
 | 
			
		||||
    DebugLogLevel,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.middleware import get_debug_info, get_username
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from clients.models import Client, Site
 | 
			
		||||
    from core.models import URLAction
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_debug_level() -> str:
 | 
			
		||||
    return get_core_settings().agent_debug_level
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ACTION_TYPE_CHOICES = [
 | 
			
		||||
    ("schedreboot", "Scheduled Reboot"),
 | 
			
		||||
    ("agentupdate", "Agent Update"),
 | 
			
		||||
    ("chocoinstall", "Chocolatey Software Install"),
 | 
			
		||||
    ("runcmd", "Run Command"),
 | 
			
		||||
    ("runscript", "Run Script"),
 | 
			
		||||
    ("runpatchscan", "Run Patch Scan"),
 | 
			
		||||
    ("runpatchinstall", "Run Patch Install"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AUDIT_ACTION_TYPE_CHOICES = [
 | 
			
		||||
    ("login", "User Login"),
 | 
			
		||||
    ("failed_login", "Failed User Login"),
 | 
			
		||||
    ("delete", "Delete Object"),
 | 
			
		||||
    ("modify", "Modify Object"),
 | 
			
		||||
    ("add", "Add Object"),
 | 
			
		||||
    ("view", "View Object"),
 | 
			
		||||
    ("check_run", "Check Run"),
 | 
			
		||||
    ("task_run", "Task Run"),
 | 
			
		||||
    ("agent_install", "Agent Install"),
 | 
			
		||||
    ("remote_session", "Remote Session"),
 | 
			
		||||
    ("execute_script", "Execute Script"),
 | 
			
		||||
    ("execute_command", "Execute Command"),
 | 
			
		||||
    ("bulk_action", "Bulk Action"),
 | 
			
		||||
    ("url_action", "URL Action"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AUDIT_OBJECT_TYPE_CHOICES = [
 | 
			
		||||
    ("user", "User"),
 | 
			
		||||
    ("script", "Script"),
 | 
			
		||||
    ("agent", "Agent"),
 | 
			
		||||
    ("policy", "Policy"),
 | 
			
		||||
    ("winupdatepolicy", "Patch Policy"),
 | 
			
		||||
    ("client", "Client"),
 | 
			
		||||
    ("site", "Site"),
 | 
			
		||||
    ("check", "Check"),
 | 
			
		||||
    ("automatedtask", "Automated Task"),
 | 
			
		||||
    ("coresettings", "Core Settings"),
 | 
			
		||||
    ("bulk", "Bulk"),
 | 
			
		||||
    ("alerttemplate", "Alert Template"),
 | 
			
		||||
    ("role", "Role"),
 | 
			
		||||
    ("urlaction", "URL Action"),
 | 
			
		||||
    ("keystore", "Global Key Store"),
 | 
			
		||||
    ("customfield", "Custom Field"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
STATUS_CHOICES = [
 | 
			
		||||
    ("pending", "Pending"),
 | 
			
		||||
    ("completed", "Completed"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuditLog(models.Model):
 | 
			
		||||
    username = models.CharField(max_length=255)
 | 
			
		||||
    agent = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    agent_id = models.CharField(max_length=255, blank=True, null=True)
 | 
			
		||||
    entry_time = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    action = models.CharField(max_length=100, choices=AUDIT_ACTION_TYPE_CHOICES)
 | 
			
		||||
    object_type = models.CharField(max_length=100, choices=AUDIT_OBJECT_TYPE_CHOICES)
 | 
			
		||||
    action = models.CharField(max_length=100, choices=AuditActionType.choices)
 | 
			
		||||
    object_type = models.CharField(max_length=100, choices=AuditObjType.choices)
 | 
			
		||||
    before_value = models.JSONField(null=True, blank=True)
 | 
			
		||||
    after_value = models.JSONField(null=True, blank=True)
 | 
			
		||||
    message = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
@@ -101,8 +58,8 @@ class AuditLog(models.Model):
 | 
			
		||||
            username=username,
 | 
			
		||||
            agent=agent.hostname,
 | 
			
		||||
            agent_id=agent.agent_id,
 | 
			
		||||
            object_type="agent",
 | 
			
		||||
            action="remote_session",
 | 
			
		||||
            object_type=AuditObjType.AGENT,
 | 
			
		||||
            action=AuditActionType.REMOTE_SESSION,
 | 
			
		||||
            message=f"{username} used Mesh Central to initiate a remote session to {agent.hostname}.",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
@@ -119,8 +76,8 @@ class AuditLog(models.Model):
 | 
			
		||||
            username=username,
 | 
			
		||||
            agent=agent.hostname,
 | 
			
		||||
            agent_id=agent.agent_id,
 | 
			
		||||
            object_type="agent",
 | 
			
		||||
            action="execute_command",
 | 
			
		||||
            object_type=AuditObjType.AGENT,
 | 
			
		||||
            action=AuditActionType.EXEC_COMMAND,
 | 
			
		||||
            message=f"{username} issued {shell} command on {agent.hostname}.",
 | 
			
		||||
            after_value=cmd,
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
@@ -138,9 +95,9 @@ class AuditLog(models.Model):
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type=object_type,
 | 
			
		||||
            agent=before["hostname"] if object_type == "agent" else None,
 | 
			
		||||
            agent_id=before["agent_id"] if object_type == "agent" else None,
 | 
			
		||||
            action="modify",
 | 
			
		||||
            agent=before["hostname"] if object_type == AuditObjType.AGENT else None,
 | 
			
		||||
            agent_id=before["agent_id"] if object_type == AuditObjType.AGENT else None,
 | 
			
		||||
            action=AuditActionType.MODIFY,
 | 
			
		||||
            message=f"{username} modified {object_type} {name}",
 | 
			
		||||
            before_value=before,
 | 
			
		||||
            after_value=after,
 | 
			
		||||
@@ -158,9 +115,9 @@ class AuditLog(models.Model):
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type=object_type,
 | 
			
		||||
            agent=after["hostname"] if object_type == "agent" else None,
 | 
			
		||||
            agent_id=after["agent_id"] if object_type == "agent" else None,
 | 
			
		||||
            action="add",
 | 
			
		||||
            agent=after["hostname"] if object_type == AuditObjType.AGENT else None,
 | 
			
		||||
            agent_id=after["agent_id"] if object_type == AuditObjType.AGENT else None,
 | 
			
		||||
            action=AuditActionType.ADD,
 | 
			
		||||
            message=f"{username} added {object_type} {name}",
 | 
			
		||||
            after_value=after,
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
@@ -177,9 +134,9 @@ class AuditLog(models.Model):
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type=object_type,
 | 
			
		||||
            agent=before["hostname"] if object_type == "agent" else None,
 | 
			
		||||
            agent_id=before["agent_id"] if object_type == "agent" else None,
 | 
			
		||||
            action="delete",
 | 
			
		||||
            agent=before["hostname"] if object_type == AuditObjType.AGENT else None,
 | 
			
		||||
            agent_id=before["agent_id"] if object_type == AuditObjType.AGENT else None,
 | 
			
		||||
            action=AuditActionType.DELETE,
 | 
			
		||||
            message=f"{username} deleted {object_type} {name}",
 | 
			
		||||
            before_value=before,
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
@@ -193,8 +150,8 @@ class AuditLog(models.Model):
 | 
			
		||||
            agent=agent.hostname,
 | 
			
		||||
            agent_id=agent.agent_id,
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type="agent",
 | 
			
		||||
            action="execute_script",
 | 
			
		||||
            object_type=AuditObjType.AGENT,
 | 
			
		||||
            action=AuditActionType.EXEC_SCRIPT,
 | 
			
		||||
            message=f'{username} ran script: "{script}" on {agent.hostname}',
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
@@ -203,8 +160,8 @@ class AuditLog(models.Model):
 | 
			
		||||
    def audit_user_failed_login(username: str, debug_info: Dict[Any, Any] = {}) -> None:
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type="user",
 | 
			
		||||
            action="failed_login",
 | 
			
		||||
            object_type=AuditObjType.USER,
 | 
			
		||||
            action=AuditActionType.FAILED_LOGIN,
 | 
			
		||||
            message=f"{username} failed to login: Credentials were rejected",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
@@ -215,8 +172,8 @@ class AuditLog(models.Model):
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type="user",
 | 
			
		||||
            action="failed_login",
 | 
			
		||||
            object_type=AuditObjType.USER,
 | 
			
		||||
            action=AuditActionType.FAILED_LOGIN,
 | 
			
		||||
            message=f"{username} failed to login: Two Factor token rejected",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
@@ -227,8 +184,8 @@ class AuditLog(models.Model):
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type="user",
 | 
			
		||||
            action="login",
 | 
			
		||||
            object_type=AuditObjType.USER,
 | 
			
		||||
            action=AuditActionType.LOGIN,
 | 
			
		||||
            message=f"{username} logged in successfully",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
@@ -249,7 +206,7 @@ class AuditLog(models.Model):
 | 
			
		||||
            agent=name if isinstance(instance, Agent) else None,
 | 
			
		||||
            agent_id=instance.agent_id if isinstance(instance, Agent) else None,
 | 
			
		||||
            object_type=classname.lower(),
 | 
			
		||||
            action="url_action",
 | 
			
		||||
            action=AuditActionType.URL_ACTION,
 | 
			
		||||
            message=f"{username} ran url action: {urlaction.pattern} on {classname}: {name}",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
        )
 | 
			
		||||
@@ -291,30 +248,14 @@ class AuditLog(models.Model):
 | 
			
		||||
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=username,
 | 
			
		||||
            object_type="bulk",
 | 
			
		||||
            action="bulk_action",
 | 
			
		||||
            object_type=AuditObjType.BULK,
 | 
			
		||||
            action=AuditActionType.BULK_ACTION,
 | 
			
		||||
            message=f"{username} executed bulk {action} {target}",
 | 
			
		||||
            debug_info=debug_info,
 | 
			
		||||
            after_value=affected,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
LOG_LEVEL_CHOICES = [
 | 
			
		||||
    ("info", "Info"),
 | 
			
		||||
    ("warning", "Warning"),
 | 
			
		||||
    ("error", "Error"),
 | 
			
		||||
    ("critical", "Critical"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
LOG_TYPE_CHOICES = [
 | 
			
		||||
    ("agent_update", "Agent Update"),
 | 
			
		||||
    ("agent_issues", "Agent Issues"),
 | 
			
		||||
    ("win_updates", "Windows Updates"),
 | 
			
		||||
    ("system_issues", "System Issues"),
 | 
			
		||||
    ("scripting", "Scripting"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DebugLog(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
@@ -327,10 +268,10 @@ class DebugLog(models.Model):
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    log_level = models.CharField(
 | 
			
		||||
        max_length=50, choices=LOG_LEVEL_CHOICES, default="info"
 | 
			
		||||
        max_length=50, choices=DebugLogLevel.choices, default=DebugLogLevel.INFO
 | 
			
		||||
    )
 | 
			
		||||
    log_type = models.CharField(
 | 
			
		||||
        max_length=50, choices=LOG_TYPE_CHOICES, default="system_issues"
 | 
			
		||||
        max_length=50, choices=DebugLogType.choices, default=DebugLogType.SYSTEM_ISSUES
 | 
			
		||||
    )
 | 
			
		||||
    message = models.TextField(null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
@@ -339,11 +280,14 @@ class DebugLog(models.Model):
 | 
			
		||||
        cls,
 | 
			
		||||
        message: str,
 | 
			
		||||
        agent: "Optional[Agent]" = None,
 | 
			
		||||
        log_type: str = "system_issues",
 | 
			
		||||
        log_type: str = DebugLogType.SYSTEM_ISSUES,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        if get_debug_level() in ["info"]:
 | 
			
		||||
        if get_debug_level() in [DebugLogLevel.INFO]:
 | 
			
		||||
            cls.objects.create(
 | 
			
		||||
                log_level="info", agent=agent, log_type=log_type, message=message
 | 
			
		||||
                log_level=DebugLogLevel.INFO,
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=log_type,
 | 
			
		||||
                message=message,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
@@ -351,11 +295,14 @@ class DebugLog(models.Model):
 | 
			
		||||
        cls,
 | 
			
		||||
        message: str,
 | 
			
		||||
        agent: "Optional[Agent]" = None,
 | 
			
		||||
        log_type: str = "system_issues",
 | 
			
		||||
        log_type: str = DebugLogType.SYSTEM_ISSUES,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        if get_debug_level() in ["info", "warning"]:
 | 
			
		||||
        if get_debug_level() in [DebugLogLevel.INFO, DebugLogLevel.WARN]:
 | 
			
		||||
            cls.objects.create(
 | 
			
		||||
                log_level="warning", agent=agent, log_type=log_type, message=message
 | 
			
		||||
                log_level=DebugLogLevel.INFO,
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=log_type,
 | 
			
		||||
                message=message,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
@@ -363,11 +310,18 @@ class DebugLog(models.Model):
 | 
			
		||||
        cls,
 | 
			
		||||
        message: str,
 | 
			
		||||
        agent: "Optional[Agent]" = None,
 | 
			
		||||
        log_type: str = "system_issues",
 | 
			
		||||
        log_type: str = DebugLogType.SYSTEM_ISSUES,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        if get_debug_level() in ["info", "warning", "error"]:
 | 
			
		||||
        if get_debug_level() in [
 | 
			
		||||
            DebugLogLevel.INFO,
 | 
			
		||||
            DebugLogLevel.WARN,
 | 
			
		||||
            DebugLogLevel.ERROR,
 | 
			
		||||
        ]:
 | 
			
		||||
            cls.objects.create(
 | 
			
		||||
                log_level="error", agent=agent, log_type=log_type, message=message
 | 
			
		||||
                log_level=DebugLogLevel.ERROR,
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=log_type,
 | 
			
		||||
                message=message,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
@@ -375,15 +329,24 @@ class DebugLog(models.Model):
 | 
			
		||||
        cls,
 | 
			
		||||
        message: str,
 | 
			
		||||
        agent: "Optional[Agent]" = None,
 | 
			
		||||
        log_type: str = "system_issues",
 | 
			
		||||
        log_type: str = DebugLogType.SYSTEM_ISSUES,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        if get_debug_level() in ["info", "warning", "error", "critical"]:
 | 
			
		||||
        if get_debug_level() in [
 | 
			
		||||
            DebugLogLevel.INFO,
 | 
			
		||||
            DebugLogLevel.WARN,
 | 
			
		||||
            DebugLogLevel.ERROR,
 | 
			
		||||
            DebugLogLevel.CRITICAL,
 | 
			
		||||
        ]:
 | 
			
		||||
            cls.objects.create(
 | 
			
		||||
                log_level="critical", agent=agent, log_type=log_type, message=message
 | 
			
		||||
                log_level=DebugLogLevel.CRITICAL,
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=log_type,
 | 
			
		||||
                message=message,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PendingAction(models.Model):
 | 
			
		||||
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
@@ -393,15 +356,13 @@ class PendingAction(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
    entry_time = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    action_type = models.CharField(
 | 
			
		||||
        max_length=255, choices=ACTION_TYPE_CHOICES, null=True, blank=True
 | 
			
		||||
        max_length=255, choices=PAAction.choices, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        choices=STATUS_CHOICES,
 | 
			
		||||
        default="pending",
 | 
			
		||||
        choices=PAStatus.choices,
 | 
			
		||||
        default=PAStatus.PENDING,
 | 
			
		||||
    )
 | 
			
		||||
    cancelable = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    celery_id = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    details = models.JSONField(null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
@@ -409,31 +370,31 @@ class PendingAction(models.Model):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def due(self) -> str:
 | 
			
		||||
        if self.action_type == "schedreboot":
 | 
			
		||||
        if self.action_type == PAAction.SCHED_REBOOT:
 | 
			
		||||
            return cast(str, self.details["time"])
 | 
			
		||||
        elif self.action_type == "agentupdate":
 | 
			
		||||
        elif self.action_type == PAAction.AGENT_UPDATE:
 | 
			
		||||
            return "Next update cycle"
 | 
			
		||||
        elif self.action_type == "chocoinstall":
 | 
			
		||||
        elif self.action_type == PAAction.CHOCO_INSTALL:
 | 
			
		||||
            return "ASAP"
 | 
			
		||||
        else:
 | 
			
		||||
            return "On next checkin"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self) -> Optional[str]:
 | 
			
		||||
        if self.action_type == "schedreboot":
 | 
			
		||||
        if self.action_type == PAAction.SCHED_REBOOT:
 | 
			
		||||
            return "Device pending reboot"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "agentupdate":
 | 
			
		||||
        elif self.action_type == PAAction.AGENT_UPDATE:
 | 
			
		||||
            return f"Agent update to {self.details['version']}"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "chocoinstall":
 | 
			
		||||
        elif self.action_type == PAAction.CHOCO_INSTALL:
 | 
			
		||||
            return f"{self.details['name']} software install"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type in [
 | 
			
		||||
            "runcmd",
 | 
			
		||||
            "runscript",
 | 
			
		||||
            "runpatchscan",
 | 
			
		||||
            "runpatchinstall",
 | 
			
		||||
            PAAction.RUN_CMD,
 | 
			
		||||
            PAAction.RUN_SCRIPT,
 | 
			
		||||
            PAAction.RUN_PATCH_SCAN,
 | 
			
		||||
            PAAction.RUN_PATCH_INSTALL,
 | 
			
		||||
        ]:
 | 
			
		||||
            return f"{self.action_type}"
 | 
			
		||||
        else:
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ from django.db.models.signals import post_init
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import PAAction, PAStatus
 | 
			
		||||
 | 
			
		||||
from .models import PendingAction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +14,10 @@ from .models import PendingAction
 | 
			
		||||
def handle_status(sender, instance: PendingAction, **kwargs):
 | 
			
		||||
    if instance.pk:
 | 
			
		||||
        # change status to completed once scheduled reboot date/time has expired
 | 
			
		||||
        if instance.action_type == "schedreboot" and instance.status == "pending":
 | 
			
		||||
        if (
 | 
			
		||||
            instance.action_type == PAAction.SCHED_REBOOT
 | 
			
		||||
            and instance.status == PAStatus.PENDING
 | 
			
		||||
        ):
 | 
			
		||||
 | 
			
		||||
            reboot_time = dt.datetime.strptime(
 | 
			
		||||
                instance.details["time"], "%Y-%m-%d %H:%M:%S"
 | 
			
		||||
@@ -26,5 +31,5 @@ def handle_status(sender, instance: PendingAction, **kwargs):
 | 
			
		||||
            reboot_time_utc = localized.astimezone(pytz.utc)
 | 
			
		||||
 | 
			
		||||
            if now > reboot_time_utc:
 | 
			
		||||
                instance.status = "completed"
 | 
			
		||||
                instance.status = PAStatus.COMPLETED
 | 
			
		||||
                instance.save(update_fields=["status"])
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import DebugLogLevel, DebugLogType, PAAction, PAStatus
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
base_url = "/logs"
 | 
			
		||||
@@ -168,15 +169,15 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent1,
 | 
			
		||||
            action_type="chocoinstall",
 | 
			
		||||
            action_type=PAAction.CHOCO_INSTALL,
 | 
			
		||||
            details={"name": "googlechrome", "output": None, "installed": False},
 | 
			
		||||
            _quantity=12,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent2,
 | 
			
		||||
            action_type="chocoinstall",
 | 
			
		||||
            status="completed",
 | 
			
		||||
            action_type=PAAction.CHOCO_INSTALL,
 | 
			
		||||
            status=PAStatus.COMPLETED,
 | 
			
		||||
            details={"name": "adobereader", "output": None, "installed": False},
 | 
			
		||||
            _quantity=14,
 | 
			
		||||
        )
 | 
			
		||||
@@ -194,7 +195,7 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
        action = baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="schedreboot",
 | 
			
		||||
            action_type=PAAction.SCHED_REBOOT,
 | 
			
		||||
            details={
 | 
			
		||||
                "time": "2021-01-13 18:20:00",
 | 
			
		||||
                "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
 | 
			
		||||
@@ -220,7 +221,7 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
        action2 = baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="schedreboot",
 | 
			
		||||
            action_type=PAAction.SCHED_REBOOT,
 | 
			
		||||
            details={
 | 
			
		||||
                "time": "2021-01-13 18:20:00",
 | 
			
		||||
                "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
 | 
			
		||||
@@ -243,16 +244,16 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.DebugLog",
 | 
			
		||||
            log_level=cycle(["error", "info", "warning", "critical"]),
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            log_level=cycle([i.value for i in DebugLogLevel]),
 | 
			
		||||
            log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        logs = baker.make(
 | 
			
		||||
            "logs.DebugLog",
 | 
			
		||||
            log_type="system_issues",
 | 
			
		||||
            log_level=cycle(["error", "info", "warning", "critical"]),
 | 
			
		||||
            log_type=DebugLogType.SYSTEM_ISSUES,
 | 
			
		||||
            log_level=cycle([i.value for i in DebugLogLevel]),
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -269,7 +270,10 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(len(resp.data), 1)
 | 
			
		||||
 | 
			
		||||
        # test time filter with other
 | 
			
		||||
        data = {"logTypeFilter": "system_issues", "logLevelFilter": "error"}
 | 
			
		||||
        data = {
 | 
			
		||||
            "logTypeFilter": DebugLogType.SYSTEM_ISSUES.value,
 | 
			
		||||
            "logLevelFilter": "error",
 | 
			
		||||
        }
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 4)
 | 
			
		||||
@@ -324,24 +328,24 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
        agent2 = baker.make_recipe("agents.agent")
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.DebugLog",
 | 
			
		||||
            log_level=cycle(["error", "info", "warning", "critical"]),
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            log_level=cycle([i.value for i in DebugLogLevel]),
 | 
			
		||||
            log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.DebugLog",
 | 
			
		||||
            log_level=cycle(["error", "info", "warning", "critical"]),
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            log_level=cycle([i.value for i in DebugLogLevel]),
 | 
			
		||||
            log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
            agent=agent2,
 | 
			
		||||
            _quantity=8,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.DebugLog",
 | 
			
		||||
            log_type="system_issues",
 | 
			
		||||
            log_level=cycle(["error", "info", "warning", "critical"]),
 | 
			
		||||
            log_type=DebugLogType.SYSTEM_ISSUES,
 | 
			
		||||
            log_level=cycle([i.value for i in DebugLogLevel]),
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from datetime import datetime as dt
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.core.paginator import Paginator
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
@@ -11,9 +10,11 @@ from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER, PAAction
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, notify_error
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone
 | 
			
		||||
 | 
			
		||||
from .models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from .permissions import AuditLogPerms, DebugLogPerms, PendingActionPerms
 | 
			
		||||
@@ -122,7 +123,7 @@ class PendingActions(APIView):
 | 
			
		||||
        if not _has_perm_on_agent(request.user, action.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        if action.action_type == "schedreboot":
 | 
			
		||||
        if action.action_type == PAAction.SCHED_REBOOT:
 | 
			
		||||
            nats_data = {
 | 
			
		||||
                "func": "delschedtask",
 | 
			
		||||
                "schedtaskpayload": {"name": action.details["taskname"]},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
[pytest]
 | 
			
		||||
DJANGO_SETTINGS_MODULE = tacticalrmm.settings
 | 
			
		||||
python_files = tests.py
 | 
			
		||||
addopts = -vvv --cov --cov-config=.coveragerc --cov-report=xml -n auto
 | 
			
		||||
python_files = tests.py test_*.py
 | 
			
		||||
addopts = --capture=tee-sys -vv --cov --cov-config=.coveragerc --cov-report=xml
 | 
			
		||||
 | 
			
		||||
filterwarnings =
 | 
			
		||||
    ignore::django.core.cache.CacheKeyWarning
 | 
			
		||||
 
 | 
			
		||||
@@ -7,3 +7,7 @@ django-silk
 | 
			
		||||
mypy
 | 
			
		||||
django-stubs
 | 
			
		||||
djangorestframework-stubs
 | 
			
		||||
django-types
 | 
			
		||||
djangorestframework-types
 | 
			
		||||
celery-types
 | 
			
		||||
msgpack-types
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
asgiref==3.5.0
 | 
			
		||||
asgiref==3.5.1
 | 
			
		||||
celery==5.2.6
 | 
			
		||||
certifi==2021.10.8
 | 
			
		||||
cffi==1.15.0
 | 
			
		||||
channels==3.0.4
 | 
			
		||||
channels_redis==3.4.0
 | 
			
		||||
chardet==4.0.0
 | 
			
		||||
cryptography==36.0.2
 | 
			
		||||
cryptography==37.0.2
 | 
			
		||||
daphne==3.0.2
 | 
			
		||||
Django==4.0.4
 | 
			
		||||
django-cors-headers==3.11.0
 | 
			
		||||
django-cors-headers==3.12.0
 | 
			
		||||
django-ipware==4.0.2
 | 
			
		||||
django-rest-knox==4.2.0
 | 
			
		||||
djangorestframework==3.13.1
 | 
			
		||||
@@ -19,20 +19,20 @@ psycopg2-binary==2.9.3
 | 
			
		||||
pycparser==2.21
 | 
			
		||||
pycryptodome==3.14.1
 | 
			
		||||
pyotp==2.6.0
 | 
			
		||||
pyparsing==3.0.8
 | 
			
		||||
pyparsing==3.0.9
 | 
			
		||||
pytz==2022.1
 | 
			
		||||
qrcode==7.3.1
 | 
			
		||||
redis==4.2.2
 | 
			
		||||
redis==4.3.1
 | 
			
		||||
hiredis==2.0.0
 | 
			
		||||
requests==2.27.1
 | 
			
		||||
six==1.16.0
 | 
			
		||||
sqlparse==0.4.2
 | 
			
		||||
twilio==7.8.2
 | 
			
		||||
twilio==7.9.0
 | 
			
		||||
urllib3==1.26.9
 | 
			
		||||
uWSGI==2.0.20
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
validators==0.19.0
 | 
			
		||||
vine==5.0.0
 | 
			
		||||
websockets==10.2
 | 
			
		||||
websockets==10.3
 | 
			
		||||
zipp==3.8.0
 | 
			
		||||
drf_spectacular==0.22.0
 | 
			
		||||
drf_spectacular==0.22.1
 | 
			
		||||
meshctrl==0.1.15
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,61 @@
 | 
			
		||||
from model_bakery.recipe import Recipe
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import ScriptShell, ScriptType
 | 
			
		||||
from tacticalrmm.demo_data import (
 | 
			
		||||
    check_storage_pool_health_ps1,
 | 
			
		||||
    clear_print_spool_bat,
 | 
			
		||||
    redhat_insights,
 | 
			
		||||
    show_temp_dir_py,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
script = Recipe(
 | 
			
		||||
    "scripts.Script",
 | 
			
		||||
    name="Test Script",
 | 
			
		||||
    description="Test Desc",
 | 
			
		||||
    shell="cmd",
 | 
			
		||||
    script_type="userdefined",
 | 
			
		||||
    shell=ScriptShell.CMD,
 | 
			
		||||
    script_type=ScriptType.USER_DEFINED,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
batch_script = Recipe(
 | 
			
		||||
    "scripts.Script",
 | 
			
		||||
    name="Test Batch Script",
 | 
			
		||||
    description="Test Batch Desc",
 | 
			
		||||
    shell=ScriptShell.CMD,
 | 
			
		||||
    script_type=ScriptType.USER_DEFINED,
 | 
			
		||||
    script_body=clear_print_spool_bat,
 | 
			
		||||
    args=["one", "two"],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ps_script = Recipe(
 | 
			
		||||
    "scripts.Script",
 | 
			
		||||
    name="Test Powershell Script",
 | 
			
		||||
    description="Test Powershell Desc",
 | 
			
		||||
    shell=ScriptShell.POWERSHELL,
 | 
			
		||||
    script_type=ScriptType.USER_DEFINED,
 | 
			
		||||
    script_body=check_storage_pool_health_ps1,
 | 
			
		||||
    args=["one"],
 | 
			
		||||
    supported_platforms=["windows"],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
py_script = Recipe(
 | 
			
		||||
    "scripts.Script",
 | 
			
		||||
    name="Test Python Script",
 | 
			
		||||
    description="Test Python Desc",
 | 
			
		||||
    shell=ScriptShell.PYTHON,
 | 
			
		||||
    script_body=show_temp_dir_py,
 | 
			
		||||
    script_type=ScriptType.USER_DEFINED,
 | 
			
		||||
    supported_platforms=["windows", "linux"],
 | 
			
		||||
    category="py stuff",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
bash_script = Recipe(
 | 
			
		||||
    "scripts.Script",
 | 
			
		||||
    name="Test Bash Script",
 | 
			
		||||
    description="Test Bash Desc",
 | 
			
		||||
    shell=ScriptShell.SHELL,
 | 
			
		||||
    script_body=redhat_insights,
 | 
			
		||||
    script_type=ScriptType.USER_DEFINED,
 | 
			
		||||
    args=["one"],
 | 
			
		||||
    supported_platforms=["linux"],
 | 
			
		||||
    category="RHSA",
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,22 +6,11 @@ from typing import List
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, TextField
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import ScriptShell, ScriptType
 | 
			
		||||
from tacticalrmm.utils import replace_db_values
 | 
			
		||||
 | 
			
		||||
SCRIPT_SHELLS = [
 | 
			
		||||
    ("powershell", "Powershell"),
 | 
			
		||||
    ("cmd", "Batch (CMD)"),
 | 
			
		||||
    ("python", "Python"),
 | 
			
		||||
    ("shell", "Shell"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
SCRIPT_TYPES = [
 | 
			
		||||
    ("userdefined", "User Defined"),
 | 
			
		||||
    ("builtin", "Built In"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Script(BaseAuditModel):
 | 
			
		||||
    guid = models.CharField(max_length=64, null=True, blank=True)
 | 
			
		||||
@@ -29,10 +18,10 @@ class Script(BaseAuditModel):
 | 
			
		||||
    description = models.TextField(null=True, blank=True, default="")
 | 
			
		||||
    filename = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    shell = models.CharField(
 | 
			
		||||
        max_length=100, choices=SCRIPT_SHELLS, default="powershell"
 | 
			
		||||
        max_length=100, choices=ScriptShell.choices, default=ScriptShell.POWERSHELL
 | 
			
		||||
    )
 | 
			
		||||
    script_type = models.CharField(
 | 
			
		||||
        max_length=100, choices=SCRIPT_TYPES, default="userdefined"
 | 
			
		||||
        max_length=100, choices=ScriptType.choices, default=ScriptType.USER_DEFINED
 | 
			
		||||
    )
 | 
			
		||||
    args = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
@@ -108,7 +97,9 @@ 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", guid=script["guid"])
 | 
			
		||||
                s = cls.objects.filter(
 | 
			
		||||
                    script_type=ScriptType.BUILT_IN, guid=script["guid"]
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                category = (
 | 
			
		||||
                    script["category"] if "category" in script.keys() else "Community"
 | 
			
		||||
@@ -163,7 +154,7 @@ class Script(BaseAuditModel):
 | 
			
		||||
                            name=script["name"],
 | 
			
		||||
                            description=script["description"],
 | 
			
		||||
                            shell=script["shell"],
 | 
			
		||||
                            script_type="builtin",
 | 
			
		||||
                            script_type=ScriptType.BUILT_IN,
 | 
			
		||||
                            category=category,
 | 
			
		||||
                            default_timeout=default_timeout,
 | 
			
		||||
                            args=args,
 | 
			
		||||
@@ -178,7 +169,7 @@ class Script(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # check for community scripts that were deleted from json and scripts folder
 | 
			
		||||
        count, _ = (
 | 
			
		||||
            Script.objects.filter(script_type="builtin")
 | 
			
		||||
            Script.objects.filter(script_type=ScriptType.BUILT_IN)
 | 
			
		||||
            .exclude(guid__in=community_scripts_processed)
 | 
			
		||||
            .delete()
 | 
			
		||||
        )
 | 
			
		||||
@@ -210,7 +201,12 @@ class Script(BaseAuditModel):
 | 
			
		||||
            if match:
 | 
			
		||||
                # only get the match between the () in regex
 | 
			
		||||
                string = match.group(1)
 | 
			
		||||
                value = replace_db_values(string=string, instance=agent, shell=shell)
 | 
			
		||||
                value = replace_db_values(
 | 
			
		||||
                    string=string,
 | 
			
		||||
                    instance=agent,
 | 
			
		||||
                    shell=shell,
 | 
			
		||||
                    quotes=True if shell != ScriptShell.CMD else False,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                if value:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
 | 
			
		||||
@@ -228,7 +224,9 @@ class ScriptSnippet(models.Model):
 | 
			
		||||
    name = CharField(max_length=40, unique=True)
 | 
			
		||||
    desc = CharField(max_length=50, blank=True, default="")
 | 
			
		||||
    code = TextField(default="")
 | 
			
		||||
    shell = CharField(max_length=15, choices=SCRIPT_SHELLS, default="powershell")
 | 
			
		||||
    shell = CharField(
 | 
			
		||||
        max_length=15, choices=ScriptShell.choices, default=ScriptShell.POWERSHELL
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ from unittest.mock import patch
 | 
			
		||||
from django.test import override_settings
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import ScriptShell, ScriptType
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Script, ScriptSnippet
 | 
			
		||||
@@ -36,7 +37,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": "Name",
 | 
			
		||||
            "description": "Description",
 | 
			
		||||
            "shell": "powershell",
 | 
			
		||||
            "shell": ScriptShell.POWERSHELL,
 | 
			
		||||
            "category": "New",
 | 
			
		||||
            "script_body": "Test Script",
 | 
			
		||||
            "default_timeout": 99,
 | 
			
		||||
@@ -93,7 +94,9 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
            "description": "New Desc",
 | 
			
		||||
            "script_body": "aasdfdsf",
 | 
			
		||||
        }  # Test
 | 
			
		||||
        builtin_script = baker.make_recipe("scripts.script", script_type="builtin")
 | 
			
		||||
        builtin_script = baker.make_recipe(
 | 
			
		||||
            "scripts.script", script_type=ScriptType.BUILT_IN
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        resp = self.client.put(f"/scripts/{builtin_script.pk}/", data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
@@ -136,7 +139,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
            "code": "some_code",
 | 
			
		||||
            "timeout": 90,
 | 
			
		||||
            "args": [],
 | 
			
		||||
            "shell": "powershell",
 | 
			
		||||
            "shell": ScriptShell.POWERSHELL,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -159,7 +162,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        self.assertFalse(Script.objects.filter(pk=script.pk).exists())
 | 
			
		||||
 | 
			
		||||
        # test delete community script
 | 
			
		||||
        script = baker.make_recipe("scripts.script", script_type="builtin")
 | 
			
		||||
        script = baker.make_recipe("scripts.script", script_type=ScriptType.BUILT_IN)
 | 
			
		||||
        url = f"/scripts/{script.pk}/"
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
@@ -175,7 +178,9 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test powershell file
 | 
			
		||||
        script = baker.make(
 | 
			
		||||
            "scripts.Script", script_body="Test Script Body", shell="powershell"
 | 
			
		||||
            "scripts.Script",
 | 
			
		||||
            script_body="Test Script Body",
 | 
			
		||||
            shell=ScriptShell.POWERSHELL,
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"
 | 
			
		||||
 | 
			
		||||
@@ -187,7 +192,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test batch file
 | 
			
		||||
        script = baker.make(
 | 
			
		||||
            "scripts.Script", script_body="Test Script Body", shell="cmd"
 | 
			
		||||
            "scripts.Script", script_body="Test Script Body", shell=ScriptShell.CMD
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"
 | 
			
		||||
 | 
			
		||||
@@ -199,7 +204,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test python file
 | 
			
		||||
        script = baker.make(
 | 
			
		||||
            "scripts.Script", script_body="Test Script Body", shell="python"
 | 
			
		||||
            "scripts.Script", script_body="Test Script Body", shell=ScriptShell.PYTHON
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/scripts/{script.pk}/download/"
 | 
			
		||||
 | 
			
		||||
@@ -228,7 +233,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
                f"-Client '{agent.client.name}'",
 | 
			
		||||
                f"-Site '{agent.site.name}'",
 | 
			
		||||
            ],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_script_arg_replacement_custom_field(self):
 | 
			
		||||
@@ -246,7 +251,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        # test default value
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'DEFAULT'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value
 | 
			
		||||
@@ -258,7 +263,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'CUSTOM VALUE'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_script_arg_replacement_client_custom_fields(self):
 | 
			
		||||
@@ -276,7 +281,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        # test default value
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'DEFAULT'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value
 | 
			
		||||
@@ -288,7 +293,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'CUSTOM VALUE'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_script_arg_replacement_site_custom_fields(self):
 | 
			
		||||
@@ -306,7 +311,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        # test default value
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'DEFAULT'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value
 | 
			
		||||
@@ -318,7 +323,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'CUSTOM VALUE'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set but empty field value
 | 
			
		||||
@@ -327,7 +332,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'DEFAULT'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test blank default and value
 | 
			
		||||
@@ -336,7 +341,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another ''"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_script_arg_replacement_array_fields(self):
 | 
			
		||||
@@ -354,7 +359,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        # test default value
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'this,is,an,array'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value and python shell
 | 
			
		||||
@@ -366,7 +371,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 'this,is,new'"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_script_arg_replacement_boolean_fields(self):
 | 
			
		||||
@@ -384,7 +389,7 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        # test default value with python
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 1"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value and python shell
 | 
			
		||||
@@ -396,19 +401,21 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 0"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="python", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value and cmd shell
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another 0"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="cmd", args=args),
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell=ScriptShell.CMD, args=args),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with set value and powershell
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another $False"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="powershell", args=args),
 | 
			
		||||
            Script.parse_script_args(
 | 
			
		||||
                agent=agent, shell=ScriptShell.POWERSHELL, args=args
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with True value powershell
 | 
			
		||||
@@ -417,7 +424,9 @@ class TestScriptViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", "-Another $True"],
 | 
			
		||||
            Script.parse_script_args(agent=agent, shell="powershell", args=args),
 | 
			
		||||
            Script.parse_script_args(
 | 
			
		||||
                agent=agent, shell=ScriptShell.POWERSHELL, args=args
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -443,7 +452,7 @@ class TestScriptSnippetViews(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": "Name",
 | 
			
		||||
            "description": "Description",
 | 
			
		||||
            "shell": "powershell",
 | 
			
		||||
            "shell": ScriptShell.POWERSHELL,
 | 
			
		||||
            "code": "Test",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from agents.permissions import RunScriptPerms
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from agents.permissions import RunScriptPerms
 | 
			
		||||
from tacticalrmm.constants import ScriptShell, ScriptType
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Script, ScriptSnippet
 | 
			
		||||
from .permissions import ScriptsPerms
 | 
			
		||||
@@ -27,7 +28,7 @@ class GetAddScripts(APIView):
 | 
			
		||||
        showHiddenScripts = request.GET.get("showHiddenScripts", False)
 | 
			
		||||
 | 
			
		||||
        if not showCommunityScripts or showCommunityScripts == "false":
 | 
			
		||||
            scripts = Script.objects.filter(script_type="userdefined")
 | 
			
		||||
            scripts = Script.objects.filter(script_type=ScriptType.USER_DEFINED)
 | 
			
		||||
        else:
 | 
			
		||||
            scripts = Script.objects.all()
 | 
			
		||||
 | 
			
		||||
@@ -61,7 +62,7 @@ class GetUpdateDeleteScript(APIView):
 | 
			
		||||
 | 
			
		||||
        data = request.data
 | 
			
		||||
 | 
			
		||||
        if script.script_type == "builtin":
 | 
			
		||||
        if script.script_type == ScriptType.BUILT_IN:
 | 
			
		||||
            # allow only favoriting builtin scripts
 | 
			
		||||
            if "favorite" in data:
 | 
			
		||||
                # overwrite request data
 | 
			
		||||
@@ -83,7 +84,7 @@ class GetUpdateDeleteScript(APIView):
 | 
			
		||||
        script = get_object_or_404(Script, pk=pk)
 | 
			
		||||
 | 
			
		||||
        # this will never trigger but check anyway
 | 
			
		||||
        if script.script_type == "builtin":
 | 
			
		||||
        if script.script_type == ScriptType.BUILT_IN:
 | 
			
		||||
            return notify_error("Community scripts cannot be deleted")
 | 
			
		||||
 | 
			
		||||
        script.delete()
 | 
			
		||||
@@ -172,16 +173,21 @@ def download(request, pk):
 | 
			
		||||
    if with_snippets == "false":
 | 
			
		||||
        with_snippets = False
 | 
			
		||||
 | 
			
		||||
    if script.shell == "powershell":
 | 
			
		||||
        filename = f"{script.name}.ps1"
 | 
			
		||||
    elif script.shell == "cmd":
 | 
			
		||||
        filename = f"{script.name}.bat"
 | 
			
		||||
    else:
 | 
			
		||||
        filename = f"{script.name}.py"
 | 
			
		||||
    match script.shell:
 | 
			
		||||
        case ScriptShell.POWERSHELL:
 | 
			
		||||
            ext = ".ps1"
 | 
			
		||||
        case ScriptShell.CMD:
 | 
			
		||||
            ext = ".bat"
 | 
			
		||||
        case ScriptShell.PYTHON:
 | 
			
		||||
            ext = ".py"
 | 
			
		||||
        case ScriptShell.SHELL:
 | 
			
		||||
            ext = ".sh"
 | 
			
		||||
        case _:
 | 
			
		||||
            ext = ""
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "filename": filename,
 | 
			
		||||
            "filename": f"{script.name}{ext}",
 | 
			
		||||
            "code": script.code if with_snippets else script.code_no_snippets,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,13 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.test import modify_settings
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
base_url = "/services"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestServiceViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from typing import Dict, Tuple, Union
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from typing import Union, Dict, Tuple
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
 | 
			
		||||
from .permissions import WinSvcsPerms
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_nats_response(data: Union[str, Dict]) -> Tuple[bool, bool, str]:
 | 
			
		||||
    natserror = True if isinstance(data, str) else False
 | 
			
		||||
    natserror = isinstance(data, str)
 | 
			
		||||
    success = (
 | 
			
		||||
        data["success"]
 | 
			
		||||
        if isinstance(data, dict) and isinstance(data["success"], bool)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Find all agents that have a certain software installed"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import os
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from software.models import ChocoSoftware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user