Compare commits

..

108 Commits

Author SHA1 Message Date
wh1te909
6ae2da22c1 Release 0.13.4 2022-05-15 07:16:14 +00:00
wh1te909
cef1ab9512 bump versions 2022-05-15 07:01:47 +00:00
wh1te909
94f02bfca3 only if exists 2022-05-15 06:59:00 +00:00
sadnub
a941bb1744 disable auto quoting when using variable substitution on batch scripts. Fixes #1020 2022-05-14 22:45:25 -04:00
sadnub
6ff591427a Add watcher in agent view for route changes and set new active agent. Fixes #1110 2022-05-14 22:23:03 -04:00
sadnub
809e172280 Fixes check policy copy issue where copied checks are all of type diskspace 2022-05-14 22:01:29 -04:00
wh1te909
17aedae0a9 recreate env 2022-05-14 23:59:32 +00:00
wh1te909
ef817ccb3a isort 2022-05-14 23:30:01 +00:00
wh1te909
0fb55b0bee remove middleware 2022-05-14 23:23:24 +00:00
wh1te909
a1a6eddc31 update reqs 2022-05-14 23:00:31 +00:00
wh1te909
ff3d0b6b57 handle empty payload, add monitoring endpoint 2022-05-13 00:50:53 +00:00
wh1te909
dd64cef4c4 optimize endpoints, remove deprecated endpoint, start reworking tests 2022-05-11 22:38:03 +00:00
Dan
9796848079 Merge pull request #1127 from silversword411/develop
typo fix
2022-05-11 14:54:05 -07:00
silversword411
fea7eb4312 typo fix 2022-05-11 17:10:06 -04:00
wh1te909
c12cd0e755 fix task run doesn't show name in history tab fixes #1097 2022-05-10 19:21:49 +00:00
wh1te909
d86a72f858 remove py 3.10.2 2022-05-10 18:52:47 +00:00
wh1te909
50cd7f219a update reqs 2022-05-10 18:45:33 +00:00
wh1te909
8252b3eccc more enum 2022-05-10 17:09:06 +00:00
wh1te909
d0c6e3a158 move tests 2022-05-10 16:29:09 +00:00
wh1te909
1505fa547e update nats reqs and change mod name 2022-05-10 16:27:51 +00:00
wh1te909
9017bad884 fix test 2022-05-09 17:35:07 +00:00
wh1te909
2ac5e316a5 refactor recovery func 2022-05-09 06:43:04 +00:00
wh1te909
29f9113062 update coverage 2022-05-09 06:40:33 +00:00
wh1te909
46349672d8 optimize checkrunner result endpoint 2022-05-09 06:23:59 +00:00
wh1te909
4787be2db0 fix tabs in popout view #1110 2022-05-04 22:17:45 +00:00
wh1te909
f0a8c5d732 fix site sorting fixes #1118 2022-05-04 15:46:05 +00:00
wh1te909
9ad520bf7c remove unnecessary middleware 2022-05-02 00:57:40 +00:00
wh1te909
bd0cc51554 update asgi 2022-05-02 00:57:01 +00:00
wh1te909
12f599f974 add linux mint 2022-05-01 20:34:31 +00:00
wh1te909
0118d5fb40 log enums 2022-04-30 02:01:20 +00:00
wh1te909
65cadb311a more enum 2022-04-29 19:09:17 +00:00
wh1te909
dd75bd197d add back dummy cache 2022-04-29 17:22:10 +00:00
wh1te909
7e155bdb43 typing 2022-04-29 06:45:20 +00:00
wh1te909
993b6fddf4 redundant 2022-04-29 06:41:40 +00:00
wh1te909
6ba51df6a7 add sh to download 2022-04-29 06:29:43 +00:00
wh1te909
1185ac58e1 more enum 2022-04-29 06:21:48 +00:00
wh1te909
f835997f49 switch runners and use redis cache during testing 2022-04-29 05:23:50 +00:00
wh1te909
a597dba775 fix role cache 2022-04-29 05:21:51 +00:00
wh1te909
3194e83a66 update reqs 2022-04-29 00:27:01 +00:00
wh1te909
096c3cdd34 more enum 2022-04-28 18:01:11 +00:00
wh1te909
3a1ea42333 fix settings 2022-04-28 17:33:51 +00:00
wh1te909
64877d4299 fix black not auto formatting 2022-04-28 17:25:21 +00:00
wh1te909
e957dc5e2c black 2022-04-28 17:24:45 +00:00
wh1te909
578d5c5830 more enum 2022-04-28 17:07:58 +00:00
sadnub
96284f9508 make eslint error on warnings 2022-04-28 09:50:41 -04:00
wh1te909
698b38dcba fix warning 2022-04-28 07:07:33 +00:00
wh1te909
6db826befe fix dir again 2022-04-28 07:02:38 +00:00
wh1te909
1a3d412d73 pwd 2022-04-28 07:01:44 +00:00
wh1te909
b8461c9dd8 fix dir 2022-04-28 07:00:09 +00:00
wh1te909
699bd9de10 fix workflow 2022-04-28 06:56:03 +00:00
Dan
54b6866e21 Merge pull request #1102 from sadnub/typescript
Typescript prep and add some linting
2022-04-27 23:45:33 -07:00
sadnub
afd155e9c1 fix actions 2022-04-27 22:48:29 -04:00
sadnub
910a717230 add name to gh action 2022-04-27 22:34:52 -04:00
sadnub
70fbd33d61 add lint and formatting gh action 2022-04-27 21:18:52 -04:00
sadnub
2da0d5ee21 add typescript support and stricter formatting/linting 2022-04-27 20:47:32 -04:00
wh1te909
98f64e057a update reqs 2022-04-27 05:07:22 +00:00
wh1te909
3d9d936c56 update url 2022-04-27 05:06:21 +00:00
wh1te909
2b4cb59df8 add more typing to dev reqs 2022-04-26 23:34:10 +00:00
wh1te909
9d80da52e3 more demo stuff 2022-04-26 23:33:45 +00:00
wh1te909
fd176d2c64 start moving to enums for choicefields 2022-04-26 01:13:18 +00:00
sadnub
538b6de36b set default alert severities for checks and tasks so that blank alert templates being applied to agent don't stop all alerts 2022-04-25 14:38:34 -04:00
sadnub
f7eca8aee0 Ignore timezone when editing automated tasks 2022-04-25 14:17:57 -04:00
wh1te909
a754d94c2c remove test multiprocessing 2022-04-25 08:15:06 +00:00
wh1te909
5e3493e6a9 fix tests 2022-04-25 07:54:31 +00:00
wh1te909
619a14c26b refactor action_type 2022-04-25 07:47:58 +00:00
wh1te909
7d9a8decf0 start choice field refactor, remove deprecated model fields 2022-04-25 07:10:33 +00:00
wh1te909
d11e14ad89 add migration 2022-04-25 06:51:42 +00:00
wh1te909
69189cf2af isort 2022-04-25 06:50:48 +00:00
wh1te909
6e7d2f19d2 update middleware for django 4, refactor a func to fix circular import, start fixing fake_agents script 2022-04-25 06:48:14 +00:00
wh1te909
d99ebf5d6a remove deprecated model field 2022-04-25 06:43:58 +00:00
wh1te909
ef2d19e95b back to develop 2022-04-25 01:36:12 +00:00
wh1te909
e3a66f017e Release 0.13.3 2022-04-25 01:32:11 +00:00
wh1te909
9e544ad471 bump version 2022-04-25 01:31:09 +00:00
sadnub
5f19aa527a fix running policy script checks 2022-04-24 21:02:28 -04:00
sadnub
bfd5bc5c26 remove port changes for persistent mesh configurations 2022-04-24 20:51:18 -04:00
wh1te909
2d0ec3accd back to dev 2022-04-25 00:38:47 +00:00
wh1te909
0999d98225 Release 0.13.2 2022-04-25 00:33:40 +00:00
wh1te909
d8dd3e133f bump version 2022-04-25 00:31:14 +00:00
wh1te909
528470c37f fix slow query 2022-04-25 00:17:34 +00:00
wh1te909
c03cd53853 fix deprecated function 2022-04-24 23:20:39 +00:00
wh1te909
b57fc8a29c testing num queries 2022-04-24 23:14:37 +00:00
wh1te909
a04ed5c3ca remove duplicate settings 2022-04-24 23:09:44 +00:00
Dan
3ad1df14f6 Merge pull request #1085 from dinger1986/patch-1
Update troubleshoot_server.sh
2022-04-24 15:26:47 -07:00
wh1te909
d8caf12fdc optimize policy query 2022-04-24 22:07:24 +00:00
wh1te909
5ca9d30d5f add a test and optimize some queries 2022-04-24 21:23:33 +00:00
sadnub
a7a71b4a46 fix default tab not working if 'servers' is selected 2022-04-24 16:35:42 -04:00
sadnub
638603ac6b fix variable substitution when running policy tasks 2022-04-24 16:35:18 -04:00
wh1te909
1d70c15027 wrong related 2022-04-24 01:45:03 +00:00
wh1te909
7a5f03d672 fix slow query 2022-04-23 23:24:36 +00:00
wh1te909
39e97c5589 Release 0.13.1 2022-04-23 22:59:35 +00:00
wh1te909
1943d8367e bump version 2022-04-23 22:57:09 +00:00
wh1te909
f91c5af9a1 optimize query 2022-04-23 22:51:45 +00:00
wh1te909
2be71fc877 increase chunk size 2022-04-23 22:50:06 +00:00
wh1te909
f5f5b4a8db fixed 'Save and test email' always returning success even if it failed 2022-04-23 18:50:23 +00:00
sadnub
ac9cfd09ea fix init container not having access to the redis service 2022-04-23 12:28:13 -04:00
sadnub
4cfc85dbfd fix agent sorting by last response 2022-04-23 12:21:18 -04:00
sadnub
1f3d2f47b1 increase max_length on customfield name field to 100 2022-04-23 10:26:34 -04:00
dinger1986
653c482ff7 Update troubleshoot_server.sh
add in output of afew log log files
2022-04-23 15:03:58 +01:00
wh1te909
4b069cc2b0 fix pending actions showing agent pending update even though it was already updated 2022-04-23 07:43:30 +00:00
wh1te909
c89349a43a update badge 2022-04-23 03:49:37 +00:00
wh1te909
6e92d6c62c testing codecov 2022-04-23 03:44:45 +00:00
wh1te909
5d3d3e9076 fix tests? 2022-04-23 02:07:00 +00:00
wh1te909
b440c772d6 forgot pytest.ini 2022-04-23 02:01:34 +00:00
wh1te909
2895560b30 testing pytest/codecov 2022-04-23 02:00:12 +00:00
wh1te909
bedcecb2e1 fix deprecations 2022-04-23 00:19:00 +00:00
wh1te909
656ac829a4 black 2022-04-22 23:22:41 +00:00
wh1te909
4d83debc0e also sort in db 2022-04-22 23:19:42 +00:00
wh1te909
4ff5d19979 fix sorting of clients tree 2022-04-22 22:48:54 +00:00
294 changed files with 13593 additions and 7250 deletions

View File

@@ -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,29 +51,23 @@ 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
coverage run --concurrency=multiprocessing manage.py test -v 2 --parallel
coverage combine
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
fi
- name: Coveralls
uses: coverallsapp/github-action@master
- uses: codecov/codecov-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./api/tacticalrmm/coverage.lcov
base-path: ./api/tacticalrmm
directory: ./api/tacticalrmm
files: ./api/tacticalrmm/coverage.xml
verbose: true

27
.github/workflows/frontend-linting.yml vendored Normal file
View 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

2
.gitignore vendored
View File

@@ -53,3 +53,5 @@ nats-api.conf
ignore/
coverage.lcov
daphne.sock.lock
.pytest_cache
coverage.xml

23
.vscode/extensions.json vendored Normal file
View 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"
]
}

155
.vscode/settings.json vendored
View File

@@ -1,86 +1,75 @@
{
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
"python.languageServer": "Pylance",
"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": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.formatting.provider": "black",
"editor.formatOnSave": true,
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
}
},
"vetur.format.options.tabSize": 2,
"vetur.format.options.useTabs": false,
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
"python.languageServer": "Pylance",
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error",
"reportDuplicateImport": "error",
"reportGeneralTypeIssues": "none"
},
"python.analysis.typeCheckingMode": "basic",
"python.linting.enabled": true,
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--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,
"[vue][javascript][typescript][javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
},
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"typescript.tsdk": "node_modules/typescript/lib",
"files.watcherExclude": {
"files.watcherExclude": {
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/": true,
"/node_modules/**": true,
"**/env/": true,
"/env/**": true,
"**/__pycache__": true,
"/__pycache__/**": true,
"**/.cache": true,
"**/.eggs": true,
"**/.ipynb_checkpoints": true,
"**/.mypy_cache": true,
"**/.pytest_cache": true,
"**/*.egg-info": true,
"**/*.feather": true,
"**/*.parquet*": true,
"**/*.pyc": true,
"**/*.zip": true
},
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/": true,
"/node_modules/**": true,
"**/env/": true,
"/env/**": true,
"**/__pycache__": true,
"/__pycache__/**": true,
"**/.cache": true,
"**/.eggs": true,
"**/.ipynb_checkpoints": true,
"**/.mypy_cache": true,
"**/.pytest_cache": true,
"**/*.egg-info": true,
"**/*.feather": true,
"**/*.parquet*": true,
"**/*.pyc": true,
"**/*.zip": true
}
},
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false
},
"go.useLanguageServer": true,
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": false,
},
"editor.snippetSuggestions": "none",
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
},
"gopls": {
"usePlaceholders": true,
"completeUnimported": true,
"staticcheck": true,
},
"mypy.targets": [
"api/tacticalrmm"
],
"python.linting.ignorePatterns": [
"**/site-packages/**/*.py",
".vscode/*.py",
"**env/**"
]
}
"editor.snippetSuggestions": "none"
},
"[go.mod]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"gopls": {
"usePlaceholders": true,
"completeUnimported": true,
"staticcheck": true
}
}

View File

@@ -1,13 +1,13 @@
# Tactical RMM
![CI Tests](https://github.com/amidaware/tacticalrmm/actions/workflows/ci-tests.yml/badge.svg?branch=develop)
[![Coverage Status](https://coveralls.io/repos/github/amidaware/tacticalrmm/badge.svg?branch=develop)](https://coveralls.io/github/amidaware/tacticalrmm?branch=develop)
[![codecov](https://codecov.io/gh/amidaware/tacticalrmm/branch/develop/graph/badge.svg?token=8ACUPVPTH6)](https://codecov.io/gh/amidaware/tacticalrmm)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
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)

View File

@@ -20,6 +20,7 @@ omit =
*/urls.py
*/tests.py
*/test.py
*/tests/*
checks/utils.py
*/asgi.py
*/demo_views.py

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -69,17 +70,17 @@ class TestAccounts(TacticalTestCase):
self.assertEqual(r.status_code, 400)
self.assertIn("non_field_errors", r.data.keys())
@override_settings(DEBUG=True)
@patch("pyotp.TOTP.verify")
def test_debug_login_view(self, mock_verify):
url = "/login/"
mock_verify.return_value = True
# @override_settings(DEBUG=True)
# @patch("pyotp.TOTP.verify")
# def test_debug_login_view(self, mock_verify):
# url = "/login/"
# mock_verify.return_value = True
data = {"username": "bob", "password": "hunter2", "twofactor": "sekret"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertIn("expiry", r.data.keys())
self.assertIn("token", r.data.keys())
# data = {"username": "bob", "password": "hunter2", "twofactor": "sekret"}
# r = self.client.post(url, data, format="json")
# self.assertEqual(r.status_code, 200)
# self.assertIn("expiry", r.data.keys())
# self.assertIn("token", r.data.keys())
class TestGetAddUsers(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,
@@ -338,7 +339,7 @@ class TestAPIKeyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
apikey = APIKey.objects.get(pk=apikey.pk)
self.assertEquals(apikey.name, "New Name")
self.assertEqual(apikey.name, "New Name")
self.check_not_authenticated("put", url)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,74 +246,97 @@ 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()
mode = random.choice(modes)
if mode == "server":
agent.operating_system = random.choice(op_systems_servers)
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.operating_system = random.choice(op_systems_workstations)
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:
agent.operating_system = random.choice(op_systems_workstations)
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,35 +346,31 @@ 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()
InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
if agent.plat == "windows":
InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
if mode == "workstation":
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
else:
WinUpdatePolicy(agent=agent).save()
# windows updates load
guids = []
for k in windows_updates.keys():
guids.append(k)
for i in guids:
WinUpdate(
agent=agent,
guid=i,
kb=windows_updates[i]["KBs"][0],
title=windows_updates[i]["Title"],
installed=windows_updates[i]["Installed"],
downloaded=windows_updates[i]["Downloaded"],
description=windows_updates[i]["Description"],
severity=windows_updates[i]["Severity"],
).save()
if agent.plat == "windows":
# windows updates load
guids = [i for i in windows_updates.keys()]
for i in guids:
WinUpdate(
agent=agent,
guid=i,
kb=windows_updates[i]["KBs"][0],
title=windows_updates[i]["Title"],
installed=windows_updates[i]["Installed"],
downloaded=windows_updates[i]["Downloaded"],
description=windows_updates[i]["Description"],
severity=windows_updates[i]["Severity"],
).save()
# agent histories
hist = AgentHistory()
@@ -355,50 +394,57 @@ class Command(BaseCommand):
}
hist1.save()
# 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()
if agent.plat == "windows":
# disk space check
check1 = Check()
check1.agent = agent
check1.check_type = CheckType.DISK_SPACE
check1.warning_threshold = 25
check1.error_threshold = 10
check1.disk = "C:"
check1.email_alert = random.choice([True, False])
check1.text_alert = random.choice([True, False])
check1.save()
check1.warning_threshold = 25
check1.error_threshold = 10
check1.disk = "C:"
check1.email_alert = random.choice([True, False])
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.y = random.randint(13, 40)
check1_history.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 = 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,138 +666,147 @@ 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()
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.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.save()
if agent.plat == "windows":
check8 = Check()
check8.agent = agent
check8.check_type = CheckType.WINSVC
check8.email_alert = random.choice([True, False])
check8.text_alert = random.choice([True, False])
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()
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
)
if i == 10 or i == 18:
check8_history.y = 1
check8_history.results = "Status STOPPED"
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 = django_now - djangotime.timedelta(minutes=i * 2)
if i == 10 or i == 18:
check8_history.y = 1
check8_history.results = "Status STOPPED"
else:
check8_history.y = 0
check8_history.results = "Status RUNNING"
check8_history.save()
check9 = Check()
check9.agent = agent
check9.check_type = CheckType.EVENT_LOG
check9.name = "unexpected shutdown"
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 = CheckStatus.FAILING
else:
check8_history.y = 0
check8_history.results = "Status RUNNING"
check8_history.save()
check_result9.extra_details = {"log": []}
check_result9.status = CheckStatus.PASSING
check9 = Check()
check_result9 = CheckResult(assigned_check=check9, agent=agent)
check9.agent = agent
check9.check_type = "eventlog"
check9.name = "unexpected shutdown"
check9.save()
check_result9.save()
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
for i in range(30):
check9_history = CheckHistory()
check9_history.check_id = check9.pk
check9_history.agent_id = agent.agent_id
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"
else:
check9_history.y = 0
check9_history.results = "Events Found: 0"
check9_history.save()
if site in sites5:
check_result9.extra_details = eventlog_check_fail_data
check_result9.status = "failing"
else:
check_result9.extra_details = {"log": []}
check_result9.status = "passing"
pick = random.randint(1, 10)
check9.log_name = "Application"
check9.event_id = 1001
check9.event_type = "INFO"
check9.fail_when = "contains"
check9.search_last_days = 30
if pick == 5 or pick == 3:
check9.save()
check_result9.save()
reboot_time = django_now + djangotime.timedelta(
minutes=random.randint(1000, 500000)
)
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
for i in range(30):
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
)
if i == 10 or i == 18:
check9_history.y = 1
check9_history.results = "Events Found: 16"
else:
check9_history.y = 0
check9_history.results = "Events Found: 0"
check9_history.save()
obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
pick = random.randint(1, 10)
task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
if pick == 5 or pick == 3:
reboot_time = djangotime.now() + djangotime.timedelta(
minutes=random.randint(1000, 500000)
)
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
sched_reboot = PendingAction()
sched_reboot.agent = agent
sched_reboot.action_type = "schedreboot"
sched_reboot.details = {
"time": str(obj),
"taskname": task_name,
}
sched_reboot.save()
sched_reboot = PendingAction()
sched_reboot.agent = agent
sched_reboot.action_type = PAAction.SCHED_REBOOT
sched_reboot.details = {
"time": str(obj),
"taskname": task_name,
}
sched_reboot.save()
self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}"))
call_command("load_demo_scripts")
self.stdout.write("done")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import pytz
from rest_framework import serializers
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, AgentCustomField, AgentHistory, Note

View File

@@ -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,
@@ -68,7 +69,7 @@ def agent_update(agent_id: str, force: bool = False) -> str:
@app.task
def force_code_sign(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id=agent_id, force=True)
@@ -77,7 +78,7 @@ def force_code_sign(agent_ids: list[str]) -> None:
@app.task
def send_agent_update_task(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id)
@@ -97,7 +98,7 @@ def auto_self_agent_update_task() -> None:
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30))
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id)
@@ -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)

View File

View 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()
@@ -245,7 +235,10 @@ class TestAgentViews(TacticalTestCase):
def test_get_agent_versions(self):
url = "/agents/versions/"
r = self.client.get(url)
with self.assertNumQueries(1):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"])
@@ -371,7 +364,7 @@ class TestAgentViews(TacticalTestCase):
"func": "eventlog",
"timeout": 30,
"payload": {
"logname": "Application",
"logname": EvtLogNames.APPLICATION,
"days": str(22),
},
},
@@ -386,7 +379,7 @@ class TestAgentViews(TacticalTestCase):
"func": "eventlog",
"timeout": 180,
"payload": {
"logname": "Security",
"logname": EvtLogNames.SECURITY,
"days": str(6),
},
},
@@ -576,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")
@@ -883,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()
@@ -922,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()
@@ -1400,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()
@@ -1443,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",
@@ -1489,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(
@@ -1515,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)
@@ -1547,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")

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

View File

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

View File

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

View File

@@ -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(
@@ -136,10 +133,10 @@ class GetAgents(APIView):
else:
agents = (
Agent.objects.filter_by_role(request.user) # type: ignore
.select_related("site")
.defer(*AGENT_DEFER)
.select_related("site__client")
.filter(monitoring_type_filter)
.filter(client_site_filter)
.only("agent_id", "hostname", "site")
)
serializer = AgentHostnameSerializer(agents, many=True)
@@ -297,9 +294,9 @@ class AgentMeshCentral(APIView):
@permission_classes([IsAuthenticated, AgentPerms])
def get_agent_versions(request):
agents = (
Agent.objects.filter_by_role(request.user) # type: ignore
.prefetch_related("site")
.only("pk", "hostname")
Agent.objects.defer(*AGENT_DEFER)
.filter_by_role(request.user) # type: ignore
.select_related("site__client")
)
return Response(
{
@@ -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"])

View File

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

View File

@@ -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
dashboard_severities = ["error"]
email_severities = ["error"]
text_severities = ["error"]
# set alert_template settings
if alert_template:
dashboard_severities = ["error"]
email_severities = ["error"]
text_severities = ["error"]
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:

View File

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

View File

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

View File

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

View File

@@ -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,
@@ -70,8 +71,8 @@ class TestAlertsViews(TacticalTestCase):
data = {"top": 3}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
self.assertEquals(resp.data["alerts_count"], 10)
self.assertEqual(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
self.assertEqual(resp.data["alerts_count"], 10)
# test filter data
# test data and result counts
@@ -409,15 +410,15 @@ class TestAlertTasks(TacticalTestCase):
core.server_policy = policy
core.save()
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
# assign second Alert Template to as default alert template
core.alert_template = alert_templates[1]
core.save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[1].pk)
# assign third Alert Template to client
workstation.client.alert_template = alert_templates[2]
@@ -425,8 +426,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.client.save()
server.client.save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk)
# apply policy to client and should override
workstation.client.workstation_policy = policy
@@ -434,8 +435,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.client.save()
server.client.save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
# assign fouth Alert Template to site
workstation.site.alert_template = alert_templates[3]
@@ -443,8 +444,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.site.save()
server.site.save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
# apply policy to site
workstation.site.workstation_policy = policy
@@ -452,8 +453,8 @@ class TestAlertTasks(TacticalTestCase):
workstation.site.save()
server.site.save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
# apply policy to agents
workstation.policy = policy
@@ -461,35 +462,35 @@ class TestAlertTasks(TacticalTestCase):
workstation.save()
server.save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
# test disabling alert template
alert_templates[0].is_active = False
alert_templates[0].save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
# test policy exclusions
alert_templates[3].excluded_agents.set([workstation.pk])
self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
# test workstation exclusions
alert_templates[2].exclude_workstations = True
alert_templates[2].save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
# test server exclusions
alert_templates[3].exclude_servers = True
alert_templates[3].save()
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)
self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk)
@patch("agents.tasks.sleep")
@patch("core.models.CoreSettings.send_mail")
@@ -523,7 +524,7 @@ class TestAlertTasks(TacticalTestCase):
# call outages task and no alert should be created
agent_outages_task()
self.assertEquals(Alert.objects.count(), 0)
self.assertEqual(Alert.objects.count(), 0)
# set overdue_dashboard_alert and alert should be created
agent_dashboard_alert.overdue_dashboard_alert = True
@@ -574,22 +575,22 @@ class TestAlertTasks(TacticalTestCase):
agent_outages_task()
# should have created 6 alerts
self.assertEquals(Alert.objects.count(), 6)
self.assertEqual(Alert.objects.count(), 6)
# other specific agents should have created alerts
self.assertEquals(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1)
self.assertEquals(Alert.objects.filter(agent=agent_text_alert).count(), 1)
self.assertEquals(Alert.objects.filter(agent=agent_email_alert).count(), 1)
self.assertEquals(Alert.objects.filter(agent=agent_template_email).count(), 1)
self.assertEquals(
self.assertEqual(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1)
self.assertEqual(Alert.objects.filter(agent=agent_text_alert).count(), 1)
self.assertEqual(Alert.objects.filter(agent=agent_email_alert).count(), 1)
self.assertEqual(Alert.objects.filter(agent=agent_template_email).count(), 1)
self.assertEqual(
Alert.objects.filter(agent=agent_template_dashboard).count(), 1
)
self.assertEquals(Alert.objects.filter(agent=agent_template_text).count(), 1)
self.assertEquals(Alert.objects.filter(agent=agent_template_blank).count(), 0)
self.assertEqual(Alert.objects.filter(agent=agent_template_text).count(), 1)
self.assertEqual(Alert.objects.filter(agent=agent_template_blank).count(), 0)
# check if email and text tasks were called
self.assertEquals(outage_email.call_count, 2)
self.assertEquals(outage_sms.call_count, 2)
self.assertEqual(outage_email.call_count, 2)
self.assertEqual(outage_sms.call_count, 2)
outage_sms.assert_any_call(
pk=Alert.objects.get(agent=agent_text_alert).pk, alert_interval=None
@@ -630,7 +631,7 @@ class TestAlertTasks(TacticalTestCase):
# calling agent outage task again shouldn't create duplicate alerts and won't send alerts
agent_outages_task()
self.assertEquals(Alert.objects.count(), 6)
self.assertEqual(Alert.objects.count(), 6)
# test periodic notification
# change email/text sent to sometime in the past
@@ -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)
@@ -942,7 +943,7 @@ class TestAlertTasks(TacticalTestCase):
send_email.assert_not_called()
send_sms.assert_not_called()
self.assertEquals(
self.assertEqual(
Alert.objects.filter(assigned_check=check_template_email).count(), 1
)
@@ -1244,7 +1245,7 @@ class TestAlertTasks(TacticalTestCase):
send_email.assert_not_called()
send_sms.assert_not_called()
self.assertEquals(
self.assertEqual(
Alert.objects.filter(assigned_task=task_template_email).count(), 1
)

View File

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

View File

View 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
@@ -62,7 +58,7 @@ class TestAPIv3(TacticalTestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20)
self.assertEquals(len(r.data["checks"]), 2)
self.assertEqual(len(r.data["checks"]), 2)
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
r = self.client.get(url)
@@ -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

View File

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

View File

@@ -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,12 +209,19 @@ 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,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializer(checks, many=True).data,
"checks": CheckRunnerGetSerializer(
checks, context={"agent": agent}, many=True
).data,
}
return Response(ret)
@@ -192,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 = [
@@ -216,30 +260,38 @@ class CheckRunner(APIView):
ret = {
"agent": agent.pk,
"check_interval": agent.check_run_interval(),
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
"checks": CheckRunnerGetSerializer(
run_list, context={"agent": agent}, many=True
).data,
}
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()
check_result.save()
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)
@@ -254,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()}
@@ -266,19 +321,28 @@ class TaskRunner(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk, agentid):
_ = 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).data)
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
)
@@ -290,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,
)
@@ -301,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
@@ -314,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:
@@ -323,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"""
@@ -414,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},
@@ -487,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")
@@ -497,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()

View File

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

View File

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

View File

@@ -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,
@@ -410,11 +410,11 @@ class TestPolicyTasks(TacticalTestCase):
)
self.assertEqual(resp.status_code, 200)
self.assertEquals(len(resp.data["server_clients"]), 1)
self.assertEquals(len(resp.data["server_sites"]), 0)
self.assertEquals(len(resp.data["workstation_clients"]), 1)
self.assertEquals(len(resp.data["workstation_sites"]), 0)
self.assertEquals(len(resp.data["agents"]), 0)
self.assertEqual(len(resp.data["server_clients"]), 1)
self.assertEqual(len(resp.data["server_sites"]), 0)
self.assertEqual(len(resp.data["workstation_clients"]), 1)
self.assertEqual(len(resp.data["workstation_sites"]), 0)
self.assertEqual(len(resp.data["agents"]), 0)
# Add Site to Policy
policy.server_sites.add(server_agents[10].site)
@@ -422,9 +422,9 @@ class TestPolicyTasks(TacticalTestCase):
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json"
)
self.assertEquals(len(resp.data["server_sites"]), 1)
self.assertEquals(len(resp.data["workstation_sites"]), 1)
self.assertEquals(len(resp.data["agents"]), 0)
self.assertEqual(len(resp.data["server_sites"]), 1)
self.assertEqual(len(resp.data["workstation_sites"]), 1)
self.assertEqual(len(resp.data["agents"]), 0)
# Add Agent to Policy
policy.agents.add(server_agents[2])
@@ -432,7 +432,7 @@ class TestPolicyTasks(TacticalTestCase):
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json"
)
self.assertEquals(len(resp.data["agents"]), 2)
self.assertEqual(len(resp.data["agents"]), 2)
def test_getting_agent_policy_checks(self):
@@ -442,7 +442,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", policy=policy)
# test policy assigned to agent
self.assertEquals(len(agent.get_checks_from_policies()), 7)
self.assertEqual(len(agent.get_checks_from_policies()), 7)
def test_getting_agent_policy_checks_with_enforced(self):
# setup data

View File

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

View File

@@ -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 (
@@ -28,7 +28,9 @@ class GetAddPolicies(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request):
policies = Policy.objects.all()
policies = Policy.objects.select_related("alert_template").prefetch_related(
"excluded_agents", "excluded_sites", "excluded_clients"
)
return Response(
PolicyTableSerializer(

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,13 +199,14 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
def get_task_actions(self, obj):
tmp = []
actions_to_remove = []
agent = self.context["agent"]
for action in obj.actions:
if action["type"] == "cmd":
tmp.append(
{
"type": "cmd",
"command": Script.parse_script_args(
agent=obj.agent,
agent=agent,
shell=action["shell"],
args=[action["command"]],
)[0],
@@ -215,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
@@ -225,7 +227,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
"script_name": script.name,
"code": script.code,
"script_args": Script.parse_script_args(
agent=obj.agent,
agent=agent,
shell=script.shell,
args=action["script_args"],
),

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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,11 +160,12 @@ 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
return Script.parse_script_args(
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
agent=agent, shell=obj.script.shell, args=obj.script_args
)
class Meta:

View File

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

View File

@@ -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,
@@ -842,8 +837,8 @@ class TestCheckTasks(TacticalTestCase):
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
self.assertEquals(check.alert_severity, "warning")
self.assertEquals(check_result.status, "failing")
self.assertEqual(check.alert_severity, "warning")
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.assertEquals(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,8 +858,8 @@ class TestCheckTasks(TacticalTestCase):
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
self.assertEquals(check_result.status, "failing")
self.assertEquals(check.alert_severity, "error")
self.assertEqual(check_result.status, CheckStatus.FAILING)
self.assertEqual(check.alert_severity, "error")
# test passing when contains with source and message
resp = self.client.patch(url, data, format="json")
@@ -872,7 +867,7 @@ class TestCheckTasks(TacticalTestCase):
check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent)
self.assertEquals(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,

View File

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

View File

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

View File

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

View File

@@ -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
@@ -32,9 +32,8 @@ class GetAddClients(APIView):
def get(self, request):
clients = (
Client.objects.select_related(
"workstation_policy", "server_policy", "alert_template"
)
Client.objects.order_by("name")
.select_related("workstation_policy", "server_policy", "alert_template")
.filter_by_role(request.user) # type: ignore
.prefetch_related(
Prefetch(
@@ -43,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(
@@ -120,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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand
from core.utils import clear_entire_cache

View File

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

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-04-23 14:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0033_coresettings_mesh_disable_auto_login'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='name',
field=models.CharField(max_length=100),
),
]

View File

@@ -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="")
@@ -175,12 +175,12 @@ class CoreSettings(BaseAuditModel):
body: str,
alert_template: "Optional[AlertTemplate]" = None,
test: bool = False,
) -> Union[bool, str]:
) -> tuple[str, bool]:
if test and not self.email_is_configured:
return "There needs to be at least one email recipient configured"
return ("There needs to be at least one email recipient configured", False)
# return since email must be configured to continue
elif not self.email_is_configured:
return "SMTP messaging not configured."
return ("SMTP messaging not configured.", False)
# override email from if alert_template is passed and is set
if alert_template and alert_template.email_from:
@@ -194,7 +194,7 @@ class CoreSettings(BaseAuditModel):
elif self.email_alert_recipients:
email_recipients = ", ".join(cast(List[str], self.email_alert_recipients))
else:
return "There needs to be at least one email recipient configured"
return ("There needs to be at least one email recipient configured", False)
try:
msg = EmailMessage()
@@ -221,18 +221,21 @@ class CoreSettings(BaseAuditModel):
except Exception as e:
DebugLog.error(message=f"Sending email failed with error: {e}")
if test:
return str(e)
finally:
return True
return (str(e), False)
if test:
return ("Email test ok!", True)
return ("ok", True)
def send_sms(
self,
body: str,
alert_template: "Optional[AlertTemplate]" = None,
test: bool = False,
) -> Union[str, bool]:
) -> tuple[str, bool]:
if not self.sms_is_configured:
return "Sms alerting is not setup correctly."
return ("Sms alerting is not setup correctly.", False)
# override email recipients if alert_template is passed and is set
if alert_template and alert_template.text_recipients:
@@ -240,7 +243,7 @@ class CoreSettings(BaseAuditModel):
elif self.sms_alert_recipients:
text_recipients = cast(List[str], self.sms_alert_recipients)
else:
return "No sms recipients found"
return ("No sms recipients found", False)
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
for num in text_recipients:
@@ -249,9 +252,12 @@ class CoreSettings(BaseAuditModel):
except TwilioRestException as e:
DebugLog.error(message=f"SMS failed to send: {e}")
if test:
return str(e)
return (str(e), False)
return True
if test:
return ("SMS Test sent successfully!", True)
return ("ok", True)
@staticmethod
def serialize(core):
@@ -284,7 +290,7 @@ class CustomField(BaseAuditModel):
blank=True,
default=list,
)
name = models.CharField(max_length=30)
name = models.CharField(max_length=100)
required = models.BooleanField(blank=True, default=False)
default_value_string = models.TextField(null=True, blank=True)
default_value_bool = models.BooleanField(default=False)

View File

@@ -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, Exists, OuterRef
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
@@ -53,6 +53,23 @@ def core_maintenance_tasks() -> None:
@app.task
def handle_resolved_stuff() -> None:
# change agent update pending status to completed if agent has just updated
actions = (
PendingAction.objects.select_related("agent")
.defer("agent__services", "agent__wmi_detail")
.filter(action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING)
)
to_update = [
action.id
for action in actions
if pyver.parse(action.agent.version) == pyver.parse(settings.LATEST_AGENT_VER)
and action.agent.status == "online"
]
PendingAction.objects.filter(pk__in=to_update).update(status=PAStatus.COMPLETED)
agent_queryset = (
Agent.objects.defer(*AGENT_DEFER)
.select_related(
@@ -80,26 +97,11 @@ def handle_resolved_stuff() -> None:
)
)
for agent in agent_queryset.annotate(
has_pending_actions=Exists(
PendingAction.objects.filter(
pk=OuterRef("pk"), action_type="agent_update", status="pending"
)
)
):
for agent in agent_queryset:
if (
pyver.parse(agent.version) >= pyver.parse("1.6.0")
and agent.status == "online"
):
# change agent update pending status to completed if agent has just updated
if (
pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER)
and agent.has_pending_actions
):
agent.pendingactions.filter(
action_type="agent_update", status="pending"
).update(status="completed")
# sync scheduled tasks
for task in agent.get_tasks_with_policies():
if not task.task_result or task.task_result.sync_status == "initial":

View File

@@ -3,15 +3,20 @@ from unittest.mock import patch
import requests
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
from .tasks import core_maintenance_tasks, handle_resolved_stuff
class TestCodeSign(TacticalTestCase):
@@ -393,6 +398,29 @@ class TestCoreTasks(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_resolved_pending_agentupdate_task(self):
online = baker.make_recipe("agents.online_agent", version="2.0.0", _quantity=20)
offline = baker.make_recipe(
"agents.offline_agent", version="2.0.0", _quantity=20
)
agents = online + offline
for agent in agents:
baker.make_recipe("logs.pending_agentupdate_action", agent=agent)
Agent.objects.update(version=settings.LATEST_AGENT_VER)
handle_resolved_stuff()
complete = PendingAction.objects.filter(
action_type=PAAction.AGENT_UPDATE, status=PAStatus.COMPLETED
).count()
old = PendingAction.objects.filter(
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).count()
self.assertEqual(complete, 20)
self.assertEqual(old, 20)
class TestCorePermissions(TacticalTestCase):
def setUp(self):

View File

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

View File

@@ -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 (
@@ -93,14 +94,14 @@ def dashboard_info(request):
@permission_classes([IsAuthenticated, CoreSettingsPerms])
def email_test(request):
core = get_core_settings()
r = core.send_mail(
msg, ok = core.send_mail(
subject="Test from Tactical RMM", body="This is a test message", test=True
)
if not ok:
return notify_error(msg)
if not isinstance(r, bool) and isinstance(r, str):
return notify_error(r)
return Response("Email Test OK!")
return Response(msg)
@api_view(["POST"])
@@ -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():
@@ -388,9 +389,8 @@ class TwilioSMSTest(APIView):
"All fields are required, including at least 1 recipient"
)
r = core.send_sms("TacticalRMM Test SMS", test=True)
msg, ok = core.send_sms("TacticalRMM Test SMS", test=True)
if not ok:
return notify_error(msg)
if not isinstance(r, bool) and isinstance(r, str):
return notify_error(r)
return Response("SMS Test sent successfully!")
return Response(msg)

View File

@@ -1 +0,0 @@
default_app_config = "logs.apps.LogsConfig"

View File

@@ -2,27 +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=PAAction.AGENT_UPDATE,
status=PAStatus.PENDING,
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -96,9 +97,14 @@ class PendingActions(APIView):
def get(self, request, agent_id=None):
if agent_id:
agent = get_object_or_404(
Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id
Agent.objects.defer(*AGENT_DEFER).prefetch_related("pendingactions"),
agent_id=agent_id,
)
actions = (
PendingAction.objects.filter(agent=agent)
.select_related("agent__site", "agent__site__client")
.defer("agent__services", "agent__wmi_detail")
)
actions = PendingAction.objects.filter(agent=agent)
else:
actions = (
PendingAction.objects.filter_by_role(request.user) # type: ignore
@@ -117,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"]},

View File

@@ -0,0 +1,7 @@
[pytest]
DJANGO_SETTINGS_MODULE = tacticalrmm.settings
python_files = tests.py test_*.py
addopts = --capture=tee-sys -vv --cov --cov-config=.coveragerc --cov-report=xml
filterwarnings =
ignore::django.core.cache.CacheKeyWarning

View File

@@ -7,3 +7,7 @@ django-silk
mypy
django-stubs
djangorestframework-stubs
django-types
djangorestframework-types
celery-types
msgpack-types

View File

@@ -1,5 +1,8 @@
coverage
coveralls
model_bakery
black
tblib
pytest
pytest-django
pytest-xdist
pytest-cov
codecov

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand
from scripts.models import Script

View File

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

View File

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

View File

@@ -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,
@@ -80,7 +81,7 @@ class TestScriptViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
script = Script.objects.get(pk=script.pk)
self.assertEquals(script.description, "Description Change")
self.assertEqual(script.description, "Description Change")
# correct_hash = hmac.new(
# settings.SECRET_KEY.encode(), data["script_body"].encode(), hashlib.sha256
@@ -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",
}
@@ -467,7 +476,7 @@ class TestScriptSnippetViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
snippet = ScriptSnippet.objects.get(pk=snippet.pk)
self.assertEquals(snippet.name, "New Name")
self.assertEqual(snippet.name, "New Name")
self.check_not_authenticated("put", url)

View File

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

Some files were not shown because too many files have changed in this diff Show More