diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example index b92a9e98fc..a9fa3b37b6 100644 --- a/.devcontainer/.env.example +++ b/.devcontainer/.env.example @@ -23,7 +23,7 @@ POSTGRES_USER=postgres POSTGRES_PASS=postgrespass # DEV SETTINGS -APP_PORT=80 +APP_PORT=443 API_PORT=80 HTTP_PROTOCOL=https DOCKER_NETWORK=172.21.0.0/24 diff --git a/.devcontainer/api.dockerfile b/.devcontainer/api.dockerfile index dfc5e779f0..a792bf144c 100644 --- a/.devcontainer/api.dockerfile +++ b/.devcontainer/api.dockerfile @@ -1,4 +1,11 @@ -FROM python:3.9.9-slim +# pulls community scripts from git repo +FROM python:3.11.4-slim AS GET_SCRIPTS_STAGE + +RUN apt-get update && + apt-get install -y --no-install-recommends git && + git clone https://github.com/amidaware/community-scripts.git /community-scripts + +FROM python:3.11.4-slim ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready @@ -10,11 +17,17 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 8000 8383 8005 -RUN groupadd -g 1000 tactical && \ +RUN apt-get update && + apt-get install -y build-essential + +RUN groupadd -g 1000 tactical && useradd -u 1000 -g 1000 tactical +# copy community scripts +COPY --from=GET_SCRIPTS_STAGE /community-scripts /community-scripts + # Copy dev python reqs -COPY .devcontainer/requirements.txt / +COPY .devcontainer/requirements.txt / # Copy docker entrypoint.sh COPY .devcontainer/entrypoint.sh / diff --git a/.devcontainer/docker-compose.debug.yml b/.devcontainer/docker-compose.debug.yml deleted file mode 100644 index fe7b819cae..0000000000 --- a/.devcontainer/docker-compose.debug.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3.4' - -services: - api-dev: - image: api-dev - build: - context: . - dockerfile: ./api.dockerfile - command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --nothreading --noreload"] - ports: - - 8000:8000 - - 5678:5678 - volumes: - - tactical-data-dev:/opt/tactical - - ..:/workspace:cached - networks: - dev: - aliases: - - tactical-backend diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 8d7f5f6e5c..7639642fb1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,10 +5,11 @@ services: container_name: trmm-api-dev image: api-dev restart: always + user: 1000:1000 build: context: .. dockerfile: .devcontainer/api.dockerfile - command: ["tactical-api"] + command: [ "tactical-api" ] environment: API_PORT: ${API_PORT} ports: @@ -18,29 +19,15 @@ services: - ..:/workspace:cached networks: dev: - aliases: + aliases: - tactical-backend - app-dev: - container_name: trmm-app-dev - image: node:14-alpine - restart: always - command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}" - working_dir: /workspace/web - volumes: - - ..:/workspace:cached - ports: - - "8080:${APP_PORT}" - networks: - dev: - aliases: - - tactical-frontend - # nats nats-dev: container_name: trmm-nats-dev image: ${IMAGE_REPO}tactical-nats:${VERSION} restart: always + user: 1000:1000 environment: API_HOST: ${API_HOST} API_PORT: ${API_PORT} @@ -61,7 +48,8 @@ services: container_name: trmm-meshcentral-dev image: ${IMAGE_REPO}tactical-meshcentral:${VERSION} restart: always - environment: + user: 1000:1000 + environment: MESH_HOST: ${MESH_HOST} MESH_USER: ${MESH_USER} MESH_PASS: ${MESH_PASS} @@ -84,6 +72,7 @@ services: container_name: trmm-mongodb-dev image: mongo:4.4 restart: always + user: 1000:1000 environment: MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER} MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} @@ -101,7 +90,7 @@ services: image: postgres:13-alpine restart: always environment: - POSTGRES_DB: tacticalrmm + POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASS} volumes: @@ -115,9 +104,10 @@ services: redis-dev: container_name: trmm-redis-dev restart: always - command: redis-server --appendonly yes + user: 1000:1000 + command: redis-server image: redis:6.0-alpine - volumes: + volumes: - redis-data-dev:/data networks: dev: @@ -128,7 +118,7 @@ services: container_name: trmm-init-dev image: api-dev restart: on-failure - command: ["tactical-init-dev"] + command: [ "tactical-init-dev" ] environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASS: ${POSTGRES_PASS} @@ -140,6 +130,7 @@ services: TRMM_PASS: ${TRMM_PASS} HTTP_PROTOCOL: ${HTTP_PROTOCOL} APP_PORT: ${APP_PORT} + POSTGRES_DB: ${POSTGRES_DB} depends_on: - postgres-dev - meshcentral-dev @@ -147,14 +138,18 @@ services: - dev volumes: - tactical-data-dev:/opt/tactical + - mesh-data-dev:/meshcentral-data + - redis-data-dev:/redis/data + - mongo-dev-data:/mongo/data/db - ..:/workspace:cached # container for celery worker service celery-dev: container_name: trmm-celery-dev image: api-dev - command: ["tactical-celery-dev"] + command: [ "tactical-celery-dev" ] restart: always + user: 1000:1000 networks: - dev volumes: @@ -168,8 +163,9 @@ services: celerybeat-dev: container_name: trmm-celerybeat-dev image: api-dev - command: ["tactical-celerybeat-dev"] + command: [ "tactical-celerybeat-dev" ] restart: always + user: 1000:1000 networks: - dev volumes: @@ -183,8 +179,9 @@ services: websockets-dev: container_name: trmm-websockets-dev image: api-dev - command: ["tactical-websockets-dev"] + command: [ "tactical-websockets-dev" ] restart: always + user: 1000:1000 networks: dev: aliases: @@ -201,6 +198,7 @@ services: container_name: trmm-nginx-dev image: ${IMAGE_REPO}tactical-nginx:${VERSION} restart: always + user: 1000:1000 environment: APP_HOST: ${APP_HOST} API_HOST: ${API_HOST} @@ -214,29 +212,17 @@ services: dev: ipv4_address: ${DOCKER_NGINX_IP} ports: - - "80:80" - - "443:443" + - "80:8080" + - "443:4443" volumes: - tactical-data-dev:/opt/tactical - mkdocs-dev: - container_name: trmm-mkdocs-dev - image: api-dev - restart: always - command: ["tactical-mkdocs-dev"] - ports: - - "8005:8005" - volumes: - - ..:/workspace:cached - networks: - - dev - volumes: - tactical-data-dev: - postgres-data-dev: - mongo-dev-data: - mesh-data-dev: - redis-data-dev: + tactical-data-dev: null + postgres-data-dev: null + mongo-dev-data: null + mesh-data-dev: null + redis-data-dev: null networks: dev: diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index b977f952e7..d12c80deb5 100644 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -9,17 +9,18 @@ set -e : "${POSTGRES_USER:=tactical}" : "${POSTGRES_PASS:=tactical}" : "${POSTGRES_DB:=tacticalrmm}" -: "${MESH_CONTAINER:=tactical-meshcentral}" +: "${MESH_SERVICE:=tactical-meshcentral}" +: "${MESH_WS_URL:=ws://${MESH_SERVICE}:4443}" : "${MESH_USER:=meshcentral}" : "${MESH_PASS:=meshcentralpass}" : "${MESH_HOST:=tactical-meshcentral}" : "${API_HOST:=tactical-backend}" -: "${APP_HOST:=tactical-frontend}" : "${REDIS_HOST:=tactical-redis}" -: "${HTTP_PROTOCOL:=http}" -: "${APP_PORT:=8080}" : "${API_PORT:=8000}" +: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}" +: "${CERT_PUB_PATH:=${TACTICAL_DIR}/certs/fullchain.pem}" + # Add python venv to path export PATH="${VIRTUAL_ENV}/bin:$PATH" @@ -37,7 +38,7 @@ function django_setup { sleep 5 done - until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do + until (echo > /dev/tcp/"${MESH_SERVICE}"/4443) &> /dev/null; do echo "waiting for meshcentral container to be ready..." sleep 5 done @@ -56,10 +57,12 @@ DEBUG = True DOCKER_BUILD = True -CERT_FILE = '/opt/tactical/certs/fullchain.pem' -KEY_FILE = '/opt/tactical/certs/privkey.pem' +SWAGGER_ENABLED = True + +CERT_FILE = '${CERT_PUB_PATH}' +KEY_FILE = '${CERT_PRIV_PATH}' -SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts' +SCRIPTS_DIR = '/community-scripts' ALLOWED_HOSTS = ['${API_HOST}', '*'] @@ -82,6 +85,7 @@ MESH_USERNAME = '${MESH_USER}' MESH_SITE = 'https://${MESH_HOST}' MESH_TOKEN_KEY = '${MESH_TOKEN}' REDIS_HOST = '${REDIS_HOST}' +MESH_WS_URL = '${MESH_WS_URL}' ADMIN_ENABLED = True EOF )" @@ -89,6 +93,7 @@ EOF echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py # run migrations and init scripts + "${VIRTUAL_ENV}"/bin/python manage.py pre_update_tasks "${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input "${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input "${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup @@ -98,6 +103,8 @@ EOF "${VIRTUAL_ENV}"/bin/python manage.py reload_nats "${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf "${VIRTUAL_ENV}"/bin/python manage.py create_installer_user + "${VIRTUAL_ENV}"/bin/python manage.py post_update_tasks + # create super user echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell @@ -110,22 +117,28 @@ if [ "$1" = 'tactical-init-dev' ]; then test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}" + mkdir -p /meshcentral-data + mkdir -p ${TACTICAL_DIR}/tmp + mkdir -p ${TACTICAL_DIR}/certs + mkdir -p /mongo/data/db + mkdir -p /redis/data + touch /meshcentral-data/.initialized && chown -R 1000:1000 /meshcentral-data + touch ${TACTICAL_DIR}/tmp/.initialized && chown -R 1000:1000 ${TACTICAL_DIR} + touch ${TACTICAL_DIR}/certs/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}/certs + touch /mongo/data/db/.initialized && chown -R 1000:1000 /mongo/data/db + touch /redis/data/.initialized && chown -R 1000:1000 /redis/data + mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe + mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/log + touch ${TACTICAL_DIR}/api/tacticalrmm/private/log/django_debug.log + # setup Python virtual env and install dependencies ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV} + "${VIRTUAL_ENV}"/bin/python -m pip install --upgrade pip + "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir setuptools wheel "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt django_setup - # create .env file for frontend - webenv="$(cat << EOF -PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}" -DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}" -APP_URL = "https://${APP_HOST}" -DOCKER_BUILD = 1 -EOF -)" - echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null - # chown everything to tactical user chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}" chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}" @@ -154,8 +167,3 @@ if [ "$1" = 'tactical-websockets-dev' ]; then check_tactical_ready "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0 fi - -if [ "$1" = 'tactical-mkdocs-dev' ]; then - cd "${WORKSPACE_DIR}/docs" - "${VIRTUAL_ENV}"/bin/mkdocs serve -fi diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt index 10ac780896..1216cd23f4 100644 --- a/.devcontainer/requirements.txt +++ b/.devcontainer/requirements.txt @@ -1,38 +1,3 @@ -# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file -asyncio-nats-client -celery -channels -channels_redis -django-ipware -Django -django-cors-headers -django-rest-knox -djangorestframework -loguru -msgpack -psycopg2-binary -pycparser -pycryptodome -pyotp -pyparsing -pytz -qrcode -redis -twilio -packaging -validators -websockets -black -Werkzeug -django-extensions -coverage -coveralls -model_bakery -mkdocs -mkdocs-material -pymdown-extensions -Pygments -mypy -pysnooper -isort -drf_spectacular +-r /workspace/api/tacticalrmm/requirements.txt +-r /workspace/api/tacticalrmm/requirements-dev.txt +-r /workspace/api/tacticalrmm/requirements-test.txt diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d053da2b02..4c7add794d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,9 +1,9 @@ # These are supported funding model platforms -github: wh1te909 +github: amidaware patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: tacticalrmm +ko_fi: # tacticalrmm tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 0000000000..4acfc48897 --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,82 @@ +name: Tests CI + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + test: + runs-on: ubuntu-latest + name: Tests + strategy: + matrix: + python-version: ["3.11.4"] + + steps: + - uses: actions/checkout@v3 + + - uses: harmon758/postgresql-action@v1 + with: + postgresql version: "15" + postgresql db: "pipeline" + postgresql user: "pipeline" + postgresql password: "pipeline123456" + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + check-latest: true + + - name: Install redis + run: | + sudo apt update + sudo apt install -y redis + redis-server --version + + - name: Install requirements + working-directory: api/tacticalrmm + run: | + python --version + SETTINGS_FILE="tacticalrmm/settings.py" + SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') + WHEEL_VER=$(grep "^WHEEL_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') + pip install --upgrade pip + pip install setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER} + pip install -r requirements.txt -r requirements-test.txt + + - name: Codestyle black + working-directory: api + run: | + black --exclude migrations/ --check tacticalrmm + if [ $? -ne 0 ]; then + exit 1 + fi + + - name: Lint with flake8 + working-directory: api/tacticalrmm + run: | + flake8 --config .flake8 . + if [ $? -ne 0 ]; then + exit 1 + fi + + - name: Run django tests + env: + GHACTIONS: "yes" + working-directory: api/tacticalrmm + run: | + pytest + if [ $? -ne 0 ]; then + exit 1 + fi + + - uses: codecov/codecov-action@v3 + with: + directory: ./api/tacticalrmm + files: ./api/tacticalrmm/coverage.xml + verbose: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..31130a219c --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '19 14 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml deleted file mode 100644 index eebbe292c1..0000000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Deploy Docs -on: - push: - branches: - - master - -defaults: - run: - working-directory: docs - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.x - - run: pip install --upgrade pip - - run: pip install --upgrade setuptools wheel - - run: pip install mkdocs mkdocs-material pymdown-extensions - - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 7f7ae7b728..a86f0abae0 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,10 @@ docs/site/ reset_db.sh run_go_cmd.py nats-api.conf - +ignore/ +coverage.lcov +daphne.sock.lock +.pytest_cache +coverage.xml +setup_dev.yml +11env/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..b4f99f3c07 --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c7f149e3b..ad5cf19db7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,70 +1,78 @@ { - "python.pythonPath": "api/tacticalrmm/env/bin/python", - "python.languageServer": "Pylance", - "python.analysis.extraPaths": [ - "api/tacticalrmm", - "api/env", - ], - "python.analysis.diagnosticSeverityOverrides": { - "reportUnusedImport": "error", - "reportDuplicateImport": "error", - }, - "python.analysis.memory.keepLibraryAst": true, - "python.linting.mypyEnabled": true, - "python.analysis.typeCheckingMode": "basic", - "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/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": "none", + //"mypy.targets": [ + //"api/tacticalrmm" + //], + //"mypy.runUsingActiveInterpreter": true, + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": true, + "editor.formatOnSave": true, + "files.associations": { + "**/ansible/**/*.yml": "ansible", + "**/docker/**/docker-compose*.yml": "dockercompose" + }, + "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 - }, - }, - "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, - }, + "**/.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 }, - "gopls": { - "usePlaceholders": true, - "completeUnimported": true, - "staticcheck": true, + "editor.snippetSuggestions": "none" + }, + "[go.mod]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true } -} \ No newline at end of file + }, + "gopls": { + "usePlaceholders": true, + "completeUnimported": true, + "staticcheck": true + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 10e4059de2..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "docker debug", - "type": "shell", - "command": "docker-compose", - "args": [ - "-p", - "trmm", - "-f", - ".devcontainer/docker-compose.yml", - "-f", - ".devcontainer/docker-compose.debug.yml", - "up", - "-d", - "--build" - ] - } - ] -} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b2bc428f21..0000000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019-present wh1te909 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..530804b43d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,74 @@ +### Tactical RMM License Version 1.0 + +Text of license:   Copyright © 2022 AmidaWare LLC. All rights reserved.
+          Amending the text of this license is not permitted. + +Trade Mark:    "Tactical RMM" is a trade mark of AmidaWare LLC. + +Licensor:       AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA. + +Licensed Software:  The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions). + +### 1. Preamble +The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices. The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes. + +The Tactical RMM License is not an open-source software license. This license contains certain restrictions on the use of the Licensed Software. For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor. + +### 2. License Grant +Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions. No additional rights will be implied under this license. + +* The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted. + +This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following: +* a service allowing third parties to interact remotely through a computer network; +* as part of a SaaS service or product; +* as part of the provision of a managed hosting service or product; +* the offering of installation and/or configuration services; +* the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM). + +The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work. + +The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works. + +All use of the Licensed Software must immediately cease if use breaches the terms of this license. + +### 3. Derivative Works +If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online. + +### 4. Copyright Notice +The following copyright notice shall be included in all copies of the Licensed Software: + +   Copyright © 2022 AmidaWare LLC. + +   Licensed under the Tactical RMM License Version 1.0 (the “License”).
+   You may only use the Licensed Software in accordance with the License.
+   A copy of the License is available at: https://license.tacticalrmm.com + +### 5. Disclaimer of Warranty +THE LICENSED SOFTWARE IS PROVIDED "AS IS". TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT. + +### 6. Limits of Liability +THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW. NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE. THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA. + +### 7. Termination +This license shall terminate with immediate effect if there is a material breach of any of its terms. + +### 8. No partnership, agency or joint venture +Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person. + +### 9. No endorsement +The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent. + +### 10. Trademarks +No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice). + +### 11. Entire agreement +This license contains the whole agreement relating to its subject matter. + + + +### 12. Severance +If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license. + +### 13. Acceptance of these terms +The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software. \ No newline at end of file diff --git a/README.md b/README.md index 2e1cd4e669..f0e19970fc 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # Tactical RMM -[![Build Status](https://dev.azure.com/dcparsi/Tactical%20RMM/_apis/build/status/wh1te909.tacticalrmm?branchName=develop)](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop) -[![Coverage Status](https://coveralls.io/repos/github/wh1te909/tacticalrmm/badge.png?branch=develop&kill_cache=1)](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +![CI Tests](https://github.com/amidaware/tacticalrmm/actions/workflows/ci-tests.yml/badge.svg?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 for Windows computers, built with Django and Vue.\ -It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) +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) -### [Documentation](https://wh1te909.github.io/tacticalrmm/) +### [Documentation](https://docs.tacticalrmm.com) ## Features @@ -29,10 +28,16 @@ Demo database resets every hour. A lot of features are disabled for obvious reas - Remote software installation via chocolatey - Software and hardware inventory -## Windows versions supported +## Windows agent versions supported -- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019 +- Windows 7, 8.1, 10, 11, Server 2008R2, 2012R2, 2016, 2019, 2022 + +## Linux agent versions supported +- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more! + +## Mac agent versions supported +- 64 bit Intel and Apple Silicon (M1, M2) ## Installation / Backup / Restore / Usage -### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/) +### Refer to the [documentation](https://docs.tacticalrmm.com) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..f1518c0285 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +[Latest](https://github.com/amidaware/tacticalrmm/releases/latest) release + +## Reporting a Vulnerability + +https://docs.tacticalrmm.com/security diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000000..04d78e8a13 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,3 @@ +### tacticalrmm ansible WIP + +ansible role to setup a Debian 11 VM for tacticalrmm local development \ No newline at end of file diff --git a/ansible/roles/trmm_dev/defaults/main.yml b/ansible/roles/trmm_dev/defaults/main.yml new file mode 100644 index 0000000000..4c20167fbd --- /dev/null +++ b/ansible/roles/trmm_dev/defaults/main.yml @@ -0,0 +1,40 @@ +--- +user: "tactical" +python_ver: "3.11.4" +go_ver: "1.20.4" +backend_repo: "https://github.com/amidaware/tacticalrmm.git" +frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git" +scripts_repo: "https://github.com/amidaware/community-scripts.git" +backend_dir: "/opt/trmm" +frontend_dir: "/opt/trmm-web" +scripts_dir: "/opt/trmm-community-scripts" +trmm_dir: "{{ backend_dir }}/api/tacticalrmm/tacticalrmm" +mesh_dir: "/opt/meshcentral" +settings_file: "{{ trmm_dir }}/settings.py" +local_settings_file: "{{ trmm_dir }}/local_settings.py" +fullchain_dest: /etc/ssl/certs/fullchain.pem +privkey_dest: /etc/ssl/certs/privkey.pem + +base_pkgs: + - build-essential + - curl + - wget + - dirmngr + - gnupg + - openssl + - gcc + - g++ + - make + - ca-certificates + - git + +python_pkgs: + - zlib1g-dev + - libncurses5-dev + - libgdbm-dev + - libnss3-dev + - libssl-dev + - libreadline-dev + - libffi-dev + - libsqlite3-dev + - libbz2-dev diff --git a/ansible/roles/trmm_dev/files/nginx-default.conf b/ansible/roles/trmm_dev/files/nginx-default.conf new file mode 100644 index 0000000000..0ab64cd944 --- /dev/null +++ b/ansible/roles/trmm_dev/files/nginx-default.conf @@ -0,0 +1,31 @@ +worker_rlimit_nofile 1000000; +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 4096; +} + +http { + sendfile on; + server_tokens off; + tcp_nopush on; + types_hash_max_size 2048; + server_names_hash_bucket_size 64; + include /etc/nginx/mime.types; + default_type application/octet-stream; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers EECDH+AESGCM:EDH+AESGCM; + ssl_ecdh_curve secp384r1; + ssl_stapling on; + ssl_stapling_verify on; + add_header X-Content-Type-Options nosniff; + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + gzip on; + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} diff --git a/ansible/roles/trmm_dev/files/vimrc.local b/ansible/roles/trmm_dev/files/vimrc.local new file mode 100644 index 0000000000..f980e72701 --- /dev/null +++ b/ansible/roles/trmm_dev/files/vimrc.local @@ -0,0 +1,20 @@ +" This file loads the default vim options at the beginning and prevents +" that they are being loaded again later. All other options that will be set, +" are added, or overwrite the default settings. Add as many options as you +" whish at the end of this file. + +" Load the defaults +source $VIMRUNTIME/defaults.vim + +" Prevent the defaults from being loaded again later, if the user doesn't +" have a local vimrc (~/.vimrc) +let skip_defaults_vim = 1 + + +" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim) +" Add as many options as you whish + +" Set the mouse mode to 'r' +if has('mouse') + set mouse=r +endif \ No newline at end of file diff --git a/ansible/roles/trmm_dev/tasks/main.yml b/ansible/roles/trmm_dev/tasks/main.yml new file mode 100644 index 0000000000..a3d3e2d44c --- /dev/null +++ b/ansible/roles/trmm_dev/tasks/main.yml @@ -0,0 +1,634 @@ +--- +- name: Append subdomains to hosts + tags: hosts + become: yes + ansible.builtin.lineinfile: + path: /etc/hosts + backrefs: yes + regexp: '^(127\.0\.1\.1 .*)$' + line: "\\1 {{ api }} {{ mesh }} {{ rmm }}" + +- name: set mouse mode for vim + tags: vim + become: yes + ansible.builtin.copy: + src: vimrc.local + dest: /etc/vim/vimrc.local + owner: "root" + group: "root" + mode: "0644" + +- name: set max_user_watches + tags: sysctl + become: yes + ansible.builtin.lineinfile: + path: /etc/sysctl.conf + line: fs.inotify.max_user_watches=524288 + +- name: reload sysctl + tags: sysctl + become: yes + ansible.builtin.command: + cmd: sysctl -p + +- name: install base packages + tags: base + become: yes + ansible.builtin.apt: + pkg: "{{ item }}" + state: present + update_cache: yes + with_items: + - "{{ base_pkgs }}" + +- name: set arch fact + ansible.builtin.set_fact: + goarch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}" + +- name: download and install golang + tags: golang + become: yes + ansible.builtin.unarchive: + src: "https://go.dev/dl/go{{ go_ver }}.linux-{{ goarch }}.tar.gz" + dest: /usr/local + remote_src: yes + +- name: add golang to path + become: yes + tags: golang + ansible.builtin.copy: + dest: /etc/profile.d/golang.sh + content: "PATH=$PATH:/usr/local/go/bin" + +- name: install python prereqs + tags: python + become: yes + ansible.builtin.apt: + pkg: "{{ item }}" + state: present + with_items: + - "{{ python_pkgs }}" + +- name: get cpu core count + tags: python + ansible.builtin.command: nproc + register: numprocs + +- name: Create python tmpdir + tags: python + ansible.builtin.tempfile: + state: directory + suffix: python + register: python_tmp + +- name: download and extract python + tags: python + ansible.builtin.unarchive: + src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz" + dest: "{{ python_tmp.path }}" + remote_src: yes + +- name: compile python + tags: python + ansible.builtin.shell: + chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}" + cmd: | + ./configure --enable-optimizations + make -j {{ numprocs.stdout }} + +- name: alt install python + tags: python + become: yes + ansible.builtin.shell: + chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}" + cmd: | + make altinstall + +- name: install redis + tags: redis + become: yes + ansible.builtin.apt: + pkg: redis + state: present + +- name: create postgres repo + tags: postgres + become: yes + ansible.builtin.copy: + content: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main" + dest: /etc/apt/sources.list.d/pgdg.list + owner: root + group: root + mode: "0644" + +- name: import postgres repo signing key + tags: postgres + become: yes + ansible.builtin.apt_key: + url: https://www.postgresql.org/media/keys/ACCC4CF8.asc + state: present + +- name: install postgresql + tags: postgres + become: yes + ansible.builtin.apt: + pkg: postgresql-15 + state: present + update_cache: yes + +- name: ensure postgres enabled and started + tags: postgres + become: yes + ansible.builtin.service: + name: postgresql + enabled: yes + state: started + +- name: setup trmm database + tags: postgres + become: yes + become_user: postgres + ansible.builtin.shell: + cmd: | + psql -c "CREATE DATABASE tacticalrmm" + psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'" + psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'" + psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'" + psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'" + psql -c "ALTER ROLE {{ db_user }} CREATEDB" + psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}" + psql -c "ALTER DATABASE tacticalrmm OWNER TO {{ db_user }}" + psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ db_user }}" + +- name: setup mesh database + tags: postgres + become: yes + become_user: postgres + ansible.builtin.shell: + cmd: | + psql -c "CREATE DATABASE meshcentral" + psql -c "CREATE USER {{ mesh_db_user }} WITH PASSWORD '{{ mesh_db_passwd }}'" + psql -c "ALTER ROLE {{ mesh_db_user }} SET client_encoding TO 'utf8'" + psql -c "ALTER ROLE {{ mesh_db_user }} SET default_transaction_isolation TO 'read committed'" + psql -c "ALTER ROLE {{ mesh_db_user }} SET timezone TO 'UTC'" + psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO {{ mesh_db_user }}" + psql -c "ALTER DATABASE meshcentral OWNER TO {{ mesh_db_user }}" + psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ mesh_db_user }}" + +- name: create repo dirs + become: yes + tags: git + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ user }}" + group: "{{ user }}" + mode: "0755" + with_items: + - "{{ backend_dir }}" + - "{{ frontend_dir }}" + - "{{ scripts_dir }}" + +- name: git clone repos + tags: git + ansible.builtin.git: + repo: "{{ item.repo }}" + dest: "{{ item.dest }}" + version: "{{ item.version }}" + with_items: + - { + repo: "{{ backend_repo }}", + dest: "{{ backend_dir }}", + version: develop, + } + - { + repo: "{{ frontend_repo }}", + dest: "{{ frontend_dir }}", + version: develop, + } + - { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main } + +- name: get nats_server_ver + tags: nats + ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}' + register: nats_server_ver + +- name: Create nats tmpdir + tags: nats + ansible.builtin.tempfile: + state: directory + suffix: nats + register: nats_tmp + +- name: download and extract nats + tags: nats + ansible.builtin.unarchive: + src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}.tar.gz" + dest: "{{ nats_tmp.path }}" + remote_src: yes + +- name: install nats + tags: nats + become: yes + ansible.builtin.copy: + remote_src: yes + src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}/nats-server" + dest: /usr/local/bin/nats-server + owner: "{{ user }}" + group: "{{ user }}" + mode: "0755" + +- name: Create nodejs tmpdir + tags: nodejs + ansible.builtin.tempfile: + state: directory + suffix: nodejs + register: nodejs_tmp + +- name: download nodejs setup + tags: nodejs + ansible.builtin.get_url: + url: https://deb.nodesource.com/setup_18.x + dest: "{{ nodejs_tmp.path }}/setup_node.sh" + mode: "0755" + +- name: run node setup script + tags: nodejs + become: yes + ansible.builtin.command: + cmd: "{{ nodejs_tmp.path }}/setup_node.sh" + +- name: install nodejs + tags: nodejs + become: yes + ansible.builtin.apt: + pkg: nodejs + state: present + update_cache: yes + +- name: update npm + tags: nodejs + become: yes + ansible.builtin.shell: + cmd: npm install -g npm + +- name: install quasar cli + tags: quasar + become: yes + ansible.builtin.shell: + cmd: npm install -g @quasar/cli + +- name: install frontend + tags: quasar + ansible.builtin.shell: + chdir: "{{ frontend_dir }}" + cmd: npm install + +- name: add quasar env + tags: quasar + ansible.builtin.template: + src: quasar.env.j2 + dest: "{{ frontend_dir }}/.env" + owner: "{{ user }}" + group: "{{ user }}" + mode: "0644" + +- name: remove tempdirs + tags: cleanup + become: yes + ignore_errors: yes + ansible.builtin.file: + path: "{{ item }}" + state: absent + with_items: + - "{{ nats_tmp.path }}" + - "{{ python_tmp.path }}" + - "{{ nodejs_tmp.path }}" + +- name: deploy fullchain + tags: certs + become: yes + ansible.builtin.copy: + src: "{{ fullchain_src }}" + dest: "{{ fullchain_dest }}" + owner: "{{ user }}" + group: "{{ user }}" + mode: "0440" + +- name: deploy privkey + tags: certs + become: yes + ansible.builtin.copy: + src: "{{ privkey_src }}" + dest: "{{ privkey_dest }}" + owner: "{{ user }}" + group: "{{ user }}" + mode: "0440" + +- name: import nginx signing key + tags: nginx + become: yes + ansible.builtin.apt_key: + url: https://nginx.org/packages/keys/nginx_signing.key + state: present + +- name: add nginx repo + tags: nginx + become: yes + ansible.builtin.template: + src: nginx.repo.j2 + dest: /etc/apt/sources.list.d/nginx.list + owner: "root" + group: "root" + mode: "0644" + +- name: install nginx + tags: nginx + become: yes + ansible.builtin.apt: + pkg: nginx + state: present + update_cache: yes + +- name: set nginx default conf + tags: nginx + become: yes + ansible.builtin.copy: + src: nginx-default.conf + dest: /etc/nginx/nginx.conf + owner: "root" + group: "root" + mode: "0644" + +- name: create nginx dirs + become: yes + tags: nginx + ansible.builtin.file: + state: directory + path: "{{ item }}" + mode: "0755" + with_items: + - /etc/nginx/sites-available + - /etc/nginx/sites-enabled + +- name: deploy nginx sites + become: yes + tags: nginx + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "0644" + owner: root + group: root + with_items: + - { src: backend.nginx.j2, dest: /etc/nginx/sites-available/backend.conf } + - { src: mesh.nginx.j2, dest: /etc/nginx/sites-available/mesh.conf } + +- name: enable nginx sites + become: yes + tags: nginx + ansible.builtin.file: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "0644" + owner: root + group: root + state: link + with_items: + - { + src: /etc/nginx/sites-available/backend.conf, + dest: /etc/nginx/sites-enabled/backend.conf, + } + - { + src: /etc/nginx/sites-available/mesh.conf, + dest: /etc/nginx/sites-enabled/mesh.conf, + } + +- name: ensure nginx enabled and restarted + tags: nginx + become: yes + ansible.builtin.service: + name: nginx + enabled: yes + state: restarted + +- name: set natsapi fact + ansible.builtin.set_fact: + natsapi: "{{ 'nats-api' if ansible_architecture == 'x86_64' else 'nats-api-arm64' }}" + +- name: copy nats-api bin + tags: nats-api + become: yes + ansible.builtin.copy: + remote_src: yes + src: "{{ backend_dir }}/natsapi/bin/{{ natsapi }}" + dest: /usr/local/bin/nats-api + owner: "{{ user }}" + group: "{{ user }}" + mode: "0755" + +- name: get setuptools_ver + tags: pip + ansible.builtin.shell: grep "^SETUPTOOLS_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}' + register: setuptools_ver + +- name: get wheel_ver + tags: pip + ansible.builtin.shell: grep "^WHEEL_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}' + register: wheel_ver + +- name: setup virtual env + tags: pip + ansible.builtin.shell: + chdir: "{{ backend_dir }}/api" + cmd: python3.11 -m venv env + +- name: update pip to latest + tags: pip + ansible.builtin.pip: + virtualenv: "{{ backend_dir }}/api/env" + name: pip + state: latest + +- name: install setuptools and wheel + tags: pip + ansible.builtin.pip: + virtualenv: "{{ backend_dir }}/api/env" + name: "{{ item }}" + with_items: + - "setuptools=={{ setuptools_ver.stdout }}" + - "wheel=={{ wheel_ver.stdout }}" + +- name: install python packages + tags: pip + ansible.builtin.pip: + virtualenv: "{{ backend_dir }}/api/env" + chdir: "{{ backend_dir }}/api/tacticalrmm" + requirements: "{{ item }}" + with_items: + - requirements.txt + - requirements-dev.txt + - requirements-test.txt + +- name: deploy django local settings + tags: django + ansible.builtin.template: + src: local_settings.j2 + dest: "{{ local_settings_file }}" + mode: "0644" + owner: "{{ user }}" + group: "{{ user }}" + +- name: setup django + tags: django + ansible.builtin.shell: + chdir: "{{ backend_dir }}/api/tacticalrmm" + cmd: | + . ../env/bin/activate + python manage.py migrate --no-input + python manage.py collectstatic --no-input + python manage.py create_natsapi_conf + python manage.py load_chocos + python manage.py load_community_scripts + echo "from accounts.models import User; User.objects.create_superuser('{{ django_user }}', '{{ github_email }}', '{{ django_password }}') if not User.objects.filter(username='{{ django_user }}').exists() else 0;" | python manage.py shell + python manage.py create_installer_user + +- name: deploy services + tags: services + become: yes + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "0644" + owner: "root" + group: "root" + with_items: + - { src: nats-api.systemd.j2, dest: /etc/systemd/system/nats-api.service } + - { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service } + - { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service } + +- name: get mesh_ver + tags: mesh + ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}' + register: mesh_ver + +- name: create meshcentral data directory + tags: mesh + become: yes + ansible.builtin.file: + path: "{{ mesh_dir }}/meshcentral-data" + state: directory + owner: "{{ user }}" + group: "{{ user }}" + mode: "0755" + +- name: install meshcentral + tags: mesh + ansible.builtin.command: + chdir: "{{ mesh_dir }}" + cmd: "npm install meshcentral@{{ mesh_ver.stdout }}" + +- name: deploy mesh config + tags: mesh + ansible.builtin.template: + src: mesh.cfg.j2 + dest: "{{ mesh_dir }}/meshcentral-data/config.json" + mode: "0644" + owner: "{{ user }}" + group: "{{ user }}" + +- name: start meshcentral + tags: mesh + become: yes + ansible.builtin.systemd: + name: meshcentral.service + state: started + enabled: yes + daemon_reload: yes + +- name: wait for meshcentral to be ready + tags: mesh + uri: + url: "https://{{ mesh }}" + return_content: yes + validate_certs: yes + status_code: 200 + register: mesh_status + until: mesh_status.status == 200 + retries: 20 + delay: 3 + +- name: get meshcentral login token key + tags: mesh_key + ansible.builtin.command: + chdir: "{{ mesh_dir }}" + cmd: node node_modules/meshcentral --logintokenkey + register: mesh_token_key + +- name: add mesh key to django settings file + tags: mesh_key + ansible.builtin.lineinfile: + path: "{{ local_settings_file }}" + line: 'MESH_TOKEN_KEY = "{{ mesh_token_key.stdout }}"' + +- name: stop meshcentral service + tags: mesh_user + become: yes + ansible.builtin.service: + name: meshcentral.service + state: stopped + +- name: create mesh user + tags: mesh_user + ansible.builtin.shell: + chdir: "{{ mesh_dir }}" + cmd: | + node node_modules/meshcentral --createaccount {{ mesh_user }} --pass {{ mesh_password }} --email {{ github_email }} + node node_modules/meshcentral --adminaccount {{ mesh_user }} + +- name: start meshcentral service + tags: mesh_user + become: yes + ansible.builtin.service: + name: meshcentral.service + state: started + +- name: wait for meshcentral to be ready + tags: mesh_user + uri: + url: "https://{{ mesh }}" + return_content: yes + validate_certs: yes + status_code: 200 + register: mesh_status + until: mesh_status.status == 200 + retries: 20 + delay: 3 + +- name: create mesh device group + tags: mesh_user + ansible.builtin.shell: + chdir: "{{ mesh_dir }}" + cmd: | + node node_modules/meshcentral/meshctrl.js --url wss://{{ mesh }}:443 --loginuser {{ mesh_user }} --loginpass {{ mesh_password }} AddDeviceGroup --name TacticalRMM + +- name: finish up django + tags: mesh_user + ansible.builtin.shell: + chdir: "{{ backend_dir }}/api/tacticalrmm" + cmd: | + . ../env/bin/activate + python manage.py initial_db_setup + python manage.py reload_nats + +- name: restart services + tags: services + become: yes + ansible.builtin.systemd: + daemon_reload: yes + enabled: yes + state: restarted + name: "{{ item }}.service" + with_items: + - nats + - nats-api diff --git a/ansible/roles/trmm_dev/templates/backend.nginx.j2 b/ansible/roles/trmm_dev/templates/backend.nginx.j2 new file mode 100644 index 0000000000..07a981f3ed --- /dev/null +++ b/ansible/roles/trmm_dev/templates/backend.nginx.j2 @@ -0,0 +1,20 @@ +server { + listen 443 ssl reuseport; + listen [::]:443 ssl; + server_name {{ api }}; + client_max_body_size 300M; + ssl_certificate {{ fullchain_dest }}; + ssl_certificate_key {{ privkey_dest }}; + + + location ~ ^/natsws { + proxy_pass http://127.0.0.1:9235; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/ansible/roles/trmm_dev/templates/local_settings.j2 b/ansible/roles/trmm_dev/templates/local_settings.j2 new file mode 100644 index 0000000000..a231680d2b --- /dev/null +++ b/ansible/roles/trmm_dev/templates/local_settings.j2 @@ -0,0 +1,21 @@ +SECRET_KEY = "{{ django_secret }}" +DEBUG = True +ALLOWED_HOSTS = ['{{ api }}'] +ADMIN_URL = "admin/" +CORS_ORIGIN_ALLOW_ALL = True +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'tacticalrmm', + 'USER': '{{ db_user }}', + 'PASSWORD': '{{ db_passwd }}', + 'HOST': 'localhost', + 'PORT': '5432', + } +} +REDIS_HOST = "localhost" +ADMIN_ENABLED = True +CERT_FILE = "{{ fullchain_dest }}" +KEY_FILE = "{{ privkey_dest }}" +MESH_USERNAME = "{{ mesh_user }}" +MESH_SITE = "https://{{ mesh }}" diff --git a/ansible/roles/trmm_dev/templates/mesh.cfg.j2 b/ansible/roles/trmm_dev/templates/mesh.cfg.j2 new file mode 100644 index 0000000000..d720987936 --- /dev/null +++ b/ansible/roles/trmm_dev/templates/mesh.cfg.j2 @@ -0,0 +1,37 @@ +{ + "settings": { + "Cert": "{{ mesh }}", + "WANonly": true, + "Minify": 1, + "Port": 4430, + "AliasPort": 443, + "RedirPort": 800, + "AllowLoginToken": true, + "AllowFraming": true, + "AgentPing": 35, + "AllowHighQualityDesktop": true, + "TlsOffload": "127.0.0.1", + "agentCoreDump": false, + "Compression": true, + "WsCompression": true, + "AgentWsCompression": true, + "MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 }, + "postgres": { + "user": "{{ mesh_db_user }}", + "password": "{{ mesh_db_passwd }}", + "port": "5432", + "host": "localhost" + } + }, + "domains": { + "": { + "Title": "Tactical RMM Dev", + "Title2": "Tactical RMM Dev", + "NewAccounts": false, + "CertUrl": "https://{{ mesh }}:443/", + "GeoLocation": true, + "CookieIpCheck": false, + "mstsc": true + } + } +} \ No newline at end of file diff --git a/ansible/roles/trmm_dev/templates/mesh.nginx.j2 b/ansible/roles/trmm_dev/templates/mesh.nginx.j2 new file mode 100644 index 0000000000..a72eed31df --- /dev/null +++ b/ansible/roles/trmm_dev/templates/mesh.nginx.j2 @@ -0,0 +1,22 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + proxy_send_timeout 330s; + proxy_read_timeout 330s; + server_name {{ mesh }}; + ssl_certificate {{ fullchain_dest }}; + ssl_certificate_key {{ privkey_dest }}; + + ssl_session_cache shared:WEBSSL:10m; + + location / { + proxy_pass http://127.0.0.1:4430/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/ansible/roles/trmm_dev/templates/mesh.systemd.j2 b/ansible/roles/trmm_dev/templates/mesh.systemd.j2 new file mode 100644 index 0000000000..57c8240726 --- /dev/null +++ b/ansible/roles/trmm_dev/templates/mesh.systemd.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=MeshCentral Server +After=network.target postgresql.service nginx.service + +[Service] +Type=simple +LimitNOFILE=1000000 +ExecStart=/usr/bin/node node_modules/meshcentral +Environment=NODE_ENV=production +WorkingDirectory={{ mesh_dir }} +User={{ user }} +Group={{ user }} +Restart=always +RestartSec=10s + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/trmm_dev/templates/nats-api.systemd.j2 b/ansible/roles/trmm_dev/templates/nats-api.systemd.j2 new file mode 100644 index 0000000000..461effbe79 --- /dev/null +++ b/ansible/roles/trmm_dev/templates/nats-api.systemd.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=TacticalRMM Nats Api +After=nats.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/nats-api -config {{ backend_dir }}/api/tacticalrmm/nats-api.conf +User={{ user }} +Group={{ user }} +Restart=always +RestartSec=5s + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/ansible/roles/trmm_dev/templates/nats-server.systemd.j2 b/ansible/roles/trmm_dev/templates/nats-server.systemd.j2 new file mode 100644 index 0000000000..6e854dd66a --- /dev/null +++ b/ansible/roles/trmm_dev/templates/nats-server.systemd.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=NATS Server +After=network.target + +[Service] +PrivateTmp=true +Type=simple +ExecStart=/usr/local/bin/nats-server -c {{ backend_dir }}/api/tacticalrmm/nats-rmm.conf +ExecReload=/usr/bin/kill -s HUP $MAINPID +ExecStop=/usr/bin/kill -s SIGINT $MAINPID +User={{ user }} +Group={{ user }} +Restart=always +RestartSec=5s +LimitNOFILE=1000000 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/ansible/roles/trmm_dev/templates/nginx.repo.j2 b/ansible/roles/trmm_dev/templates/nginx.repo.j2 new file mode 100644 index 0000000000..29b225baf5 --- /dev/null +++ b/ansible/roles/trmm_dev/templates/nginx.repo.j2 @@ -0,0 +1,2 @@ +deb https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx +deb-src https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx \ No newline at end of file diff --git a/ansible/roles/trmm_dev/templates/quasar.env.j2 b/ansible/roles/trmm_dev/templates/quasar.env.j2 new file mode 100644 index 0000000000..53b01a98a7 --- /dev/null +++ b/ansible/roles/trmm_dev/templates/quasar.env.j2 @@ -0,0 +1,4 @@ +DEV_URL = "http://{{ api }}:8000" +DEV_HOST = "0.0.0.0" +DEV_PORT = "8080" +USE_HTTPS = false \ No newline at end of file diff --git a/ansible/setup_dev.yml.example b/ansible/setup_dev.yml.example new file mode 100644 index 0000000000..a5af879f58 --- /dev/null +++ b/ansible/setup_dev.yml.example @@ -0,0 +1,22 @@ +--- +- hosts: "{{ target }}" + vars: + ansible_user: tactical + fullchain_src: /path/to/fullchain.pem + privkey_src: /path/to/privkey.pem + api: "api.example.com" + rmm: "rmm.example.com" + mesh: "mesh.example.com" + github_username: "changeme" + github_email: "changeme@example.com" + mesh_user: "changeme" + mesh_password: "changeme" + db_user: "changeme" + db_passwd: "changeme" + mesh_db_user: "changeme" + mesh_db_passwd: "changeme" + django_secret: "changeme" + django_user: "changeme" + django_password: "changeme" + roles: + - trmm_dev diff --git a/api/tacticalrmm/.coveragerc b/api/tacticalrmm/.coveragerc index 6245fc7c38..630440c7ac 100644 --- a/api/tacticalrmm/.coveragerc +++ b/api/tacticalrmm/.coveragerc @@ -1,24 +1,15 @@ [run] -source = . -[report] -show_missing = True include = *.py omit = + tacticalrmm/asgi.py + tacticalrmm/wsgi.py + manage.py */__pycache__/* */env/* - */management/* - */migrations/* - */static/* - manage.py - */local_settings.py - */apps.py - */admin.py - */celery.py - */wsgi.py - */settings.py */baker_recipes.py - */urls.py - */tests.py - */test.py - checks/utils.py + /usr/local/lib/* + **/migrations/* + **/test*.py +[report] +show_missing = True diff --git a/api/tacticalrmm/.flake8 b/api/tacticalrmm/.flake8 new file mode 100644 index 0000000000..3f1049b11f --- /dev/null +++ b/api/tacticalrmm/.flake8 @@ -0,0 +1,12 @@ +[flake8] +ignore = E501,W503,E722,E203 +exclude = + .mypy* + .pytest* + .git + demo_data.py + manage.py + */__pycache__/* + */env/* + /usr/local/lib/* + **/migrations/* diff --git a/api/tacticalrmm/accounts/admin.py b/api/tacticalrmm/accounts/admin.py index 56cee27d5e..28c1b97792 100644 --- a/api/tacticalrmm/accounts/admin.py +++ b/api/tacticalrmm/accounts/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from rest_framework.authtoken.admin import TokenAdmin -from .models import User, Role +from .models import Role, User admin.site.register(User) TokenAdmin.raw_id_fields = ("user",) diff --git a/api/tacticalrmm/accounts/management/commands/create_installer_user.py b/api/tacticalrmm/accounts/management/commands/create_installer_user.py index 773275b89e..698aa3d0c9 100644 --- a/api/tacticalrmm/accounts/management/commands/create_installer_user.py +++ b/api/tacticalrmm/accounts/management/commands/create_installer_user.py @@ -1,19 +1,24 @@ import uuid from django.core.management.base import BaseCommand + from accounts.models import User +from tacticalrmm.helpers import make_random_password 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=make_random_password(len=60), block_dashboard_login=True, ) + self.stdout.write("Installer user has been created") diff --git a/api/tacticalrmm/accounts/management/commands/delete_tokens.py b/api/tacticalrmm/accounts/management/commands/delete_tokens.py index 87bb8a65c6..60daa407a6 100644 --- a/api/tacticalrmm/accounts/management/commands/delete_tokens.py +++ b/api/tacticalrmm/accounts/management/commands/delete_tokens.py @@ -6,7 +6,7 @@ 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( diff --git a/api/tacticalrmm/accounts/management/commands/reset_2fa.py b/api/tacticalrmm/accounts/management/commands/reset_2fa.py index f95c8483a0..4a71fb3dcc 100644 --- a/api/tacticalrmm/accounts/management/commands/reset_2fa.py +++ b/api/tacticalrmm/accounts/management/commands/reset_2fa.py @@ -1,10 +1,10 @@ -import os import subprocess import pyotp from django.core.management.base import BaseCommand from accounts.models import User +from tacticalrmm.helpers import get_webdomain class Command(BaseCommand): @@ -21,28 +21,13 @@ def handle(self, *args, **kwargs): self.stdout.write(self.style.ERROR(f"User {username} doesn't exist")) return - domain = "Tactical RMM" - nginx = "/etc/nginx/sites-available/frontend.conf" - found = None - if os.path.exists(nginx): - try: - with open(nginx, "r") as f: - for line in f: - if "server_name" in line: - found = line - break - - if found: - rep = found.replace("server_name", "").replace(";", "") - domain = "".join(rep.split()) - except: - pass - code = pyotp.random_base32() user.totp_key = code user.save(update_fields=["totp_key"]) - url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain) + url = pyotp.totp.TOTP(code).provisioning_uri( + username, issuer_name=get_webdomain() + ) subprocess.run(f'qr "{url}"', shell=True) self.stdout.write( self.style.WARNING("Scan the barcode above with your authenticator app") diff --git a/api/tacticalrmm/accounts/management/commands/reset_password.py b/api/tacticalrmm/accounts/management/commands/reset_password.py index 0c2a4b68e8..73ae3b34bc 100644 --- a/api/tacticalrmm/accounts/management/commands/reset_password.py +++ b/api/tacticalrmm/accounts/management/commands/reset_password.py @@ -1,3 +1,5 @@ +from getpass import getpass + from django.core.management.base import BaseCommand from accounts.models import User @@ -17,7 +19,13 @@ def handle(self, *args, **kwargs): self.stdout.write(self.style.ERROR(f"User {username} doesn't exist")) return - passwd = input("Enter new password: ") - user.set_password(passwd) + pass1, pass2 = "foo", "bar" + while pass1 != pass2: + pass1 = getpass() + pass2 = getpass(prompt="Confirm Password:") + if pass1 != pass2: + self.stdout.write(self.style.ERROR("Passwords don't match")) + + user.set_password(pass1) user.save() self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!")) diff --git a/api/tacticalrmm/accounts/migrations/0019_user_role.py b/api/tacticalrmm/accounts/migrations/0019_user_role.py index ca7224ee38..617d81aa1e 100644 --- a/api/tacticalrmm/accounts/migrations/0019_user_role.py +++ b/api/tacticalrmm/accounts/migrations/0019_user_role.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.1 on 2021-05-11 02:33 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py b/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py index 684cb63cf0..33b2317a7a 100644 --- a/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py +++ b/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.6 on 2021-09-03 00:54 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py b/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py index 0e5ad0fa7c..9ef05d34d8 100644 --- a/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py +++ b/api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.6 on 2021-10-10 02:49 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/tacticalrmm/accounts/migrations/0031_user_date_format.py b/api/tacticalrmm/accounts/migrations/0031_user_date_format.py new file mode 100644 index 0000000000..490740d7e1 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0031_user_date_format.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-04-02 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0030_auto_20211104_0221'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='date_format', + field=models.CharField(blank=True, max_length=30, null=True), + ), + ] diff --git a/api/tacticalrmm/accounts/migrations/0032_alter_user_default_agent_tbl_tab.py b/api/tacticalrmm/accounts/migrations/0032_alter_user_default_agent_tbl_tab.py new file mode 100644 index 0000000000..335aee9260 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0032_alter_user_default_agent_tbl_tab.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.1 on 2023-05-17 07:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0031_user_date_format"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="default_agent_tbl_tab", + field=models.CharField( + choices=[ + ("server", "Servers"), + ("workstation", "Workstations"), + ("mixed", "Mixed"), + ], + default="mixed", + max_length=50, + ), + ), + ] diff --git a/api/tacticalrmm/accounts/migrations/0033_user_dash_info_color_user_dash_negative_color_and_more.py b/api/tacticalrmm/accounts/migrations/0033_user_dash_info_color_user_dash_negative_color_and_more.py new file mode 100644 index 0000000000..7f36f56757 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0033_user_dash_info_color_user_dash_negative_color_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.1 on 2023-05-23 04:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0032_alter_user_default_agent_tbl_tab"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="dash_info_color", + field=models.CharField(default="info", max_length=255), + ), + migrations.AddField( + model_name="user", + name="dash_negative_color", + field=models.CharField(default="negative", max_length=255), + ), + migrations.AddField( + model_name="user", + name="dash_positive_color", + field=models.CharField(default="positive", max_length=255), + ), + migrations.AddField( + model_name="user", + name="dash_warning_color", + field=models.CharField(default="warning", max_length=255), + ), + ] diff --git a/api/tacticalrmm/accounts/migrations/0034_role_can_send_wol.py b/api/tacticalrmm/accounts/migrations/0034_role_can_send_wol.py new file mode 100644 index 0000000000..8edb54d71e --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0034_role_can_send_wol.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.9 on 2023-05-26 23:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0033_user_dash_info_color_user_dash_negative_color_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="role", + name="can_send_wol", + field=models.BooleanField(default=False), + ), + ] diff --git a/api/tacticalrmm/accounts/models.py b/api/tacticalrmm/accounts/models.py index d24bd21ed3..b31328268f 100644 --- a/api/tacticalrmm/accounts/models.py +++ b/api/tacticalrmm/accounts/models.py @@ -1,26 +1,17 @@ +from typing import Optional + 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 from logs.models import BaseAuditModel - -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 tacticalrmm.constants import ( + ROLE_CACHE_PREFIX, + AgentDblClick, + AgentTableTabs, + ClientTreeSort, +) class User(AbstractUser, BaseAuditModel): @@ -29,8 +20,8 @@ class User(AbstractUser, BaseAuditModel): totp_key = models.CharField(max_length=50, null=True, blank=True) 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" + agent_dblclick_action: "AgentDblClick" = models.CharField( + max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT ) url_action = models.ForeignKey( "core.URLAction", @@ -40,15 +31,20 @@ 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.MIXED ) 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") + dash_info_color = models.CharField(max_length=255, default="info") + dash_positive_color = models.CharField(max_length=255, default="positive") + dash_negative_color = models.CharField(max_length=255, default="negative") + dash_warning_color = models.CharField(max_length=255, default="warning") clear_search_when_switching = models.BooleanField(default=True) + date_format = models.CharField(max_length=30, blank=True, null=True) is_installer_user = models.BooleanField(default=False) last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True) @@ -75,6 +71,23 @@ def serialize(user): return UserSerializer(user).data + def get_and_set_role_cache(self) -> "Optional[Role]": + role = cache.get(f"{ROLE_CACHE_PREFIX}{self.role}") + + if role and isinstance(role, Role): + return role + elif not role and not self.role: + return None + else: + models.prefetch_related_objects( + [self.role], + "can_view_clients", + "can_view_sites", + ) + + cache.set(f"{ROLE_CACHE_PREFIX}{self.role}", self.role, 600) + return self.role + class Role(BaseAuditModel): name = models.CharField(max_length=255, unique=True) @@ -96,6 +109,7 @@ class Role(BaseAuditModel): can_run_bulk = models.BooleanField(default=False) can_recover_agents = models.BooleanField(default=False) can_list_agent_history = models.BooleanField(default=False) + can_send_wol = models.BooleanField(default=False) # core can_list_notes = models.BooleanField(default=False) @@ -175,6 +189,11 @@ class Role(BaseAuditModel): def __str__(self): return self.name + def save(self, *args, **kwargs) -> None: + # delete cache on save + cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}") + super(BaseAuditModel, self).save(*args, **kwargs) + @staticmethod def serialize(role): # serializes the agent and returns json diff --git a/api/tacticalrmm/accounts/permissions.py b/api/tacticalrmm/accounts/permissions.py index 9e8ce51b8b..e9e809ecb3 100644 --- a/api/tacticalrmm/accounts/permissions.py +++ b/api/tacticalrmm/accounts/permissions.py @@ -4,39 +4,38 @@ class AccountsPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_accounts") - else: - # allow users to reset their own password/2fa see issue #686 - base_path = "/accounts/users/" - paths = ["reset/", "reset_totp/"] + # allow users to reset their own password/2fa see issue #686 + base_path = "/accounts/users/" + paths = ("reset/", "reset_totp/") - if r.path in [base_path + i for i in paths]: - from accounts.models import User + if r.path in [base_path + i for i in paths]: + from accounts.models import User - try: - user = User.objects.get(pk=r.data["id"]) - except User.DoesNotExist: - pass - else: - if user == r.user: - return True + try: + user = User.objects.get(pk=r.data["id"]) + except User.DoesNotExist: + pass + else: + if user == r.user: + return True - return _has_perm(r, "can_manage_accounts") + return _has_perm(r, "can_manage_accounts") class RolesPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_roles") - else: - return _has_perm(r, "can_manage_roles") + + return _has_perm(r, "can_manage_roles") class APIKeyPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_api_keys") diff --git a/api/tacticalrmm/accounts/serializers.py b/api/tacticalrmm/accounts/serializers.py index 8c9ffca156..78186aef5b 100644 --- a/api/tacticalrmm/accounts/serializers.py +++ b/api/tacticalrmm/accounts/serializers.py @@ -1,11 +1,13 @@ import pyotp from rest_framework.serializers import ( ModelSerializer, - SerializerMethodField, ReadOnlyField, + SerializerMethodField, ) -from .models import APIKey, User, Role +from tacticalrmm.helpers import get_webdomain + +from .models import APIKey, Role, User class UserUISerializer(ModelSerializer): @@ -20,8 +22,13 @@ class Meta: "client_tree_sort", "client_tree_splitter", "loading_bar_color", + "dash_info_color", + "dash_positive_color", + "dash_negative_color", + "dash_warning_color", "clear_search_when_switching", "block_dashboard_login", + "date_format", ] @@ -39,11 +46,11 @@ class Meta: "last_login_ip", "role", "block_dashboard_login", + "date_format", ] class TOTPSetupSerializer(ModelSerializer): - qr_url = SerializerMethodField() class Meta: @@ -56,7 +63,7 @@ class Meta: def get_qr_url(self, obj): return pyotp.totp.TOTP(obj.totp_key).provisioning_uri( - obj.username, issuer_name="Tactical RMM" + obj.username, issuer_name=get_webdomain() ) @@ -78,7 +85,6 @@ class Meta: class APIKeySerializer(ModelSerializer): - username = ReadOnlyField(source="user.username") class Meta: diff --git a/api/tacticalrmm/accounts/tests.py b/api/tacticalrmm/accounts/tests.py index dac111f96b..5c48eb96bf 100644 --- a/api/tacticalrmm/accounts/tests.py +++ b/api/tacticalrmm/accounts/tests.py @@ -2,15 +2,16 @@ from django.test import override_settings from model_bakery import baker, seq -from accounts.models import User, APIKey -from tacticalrmm.test import TacticalTestCase +from accounts.models import APIKey, User from accounts.serializers import APIKeySerializer +from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort +from tacticalrmm.test import TacticalTestCase class TestAccounts(TacticalTestCase): def setUp(self): - self.client_setup() + self.setup_client() self.bob = User(username="bob") self.bob.set_password("hunter2") self.bob.save() @@ -69,17 +70,17 @@ def test_login_view(self, mock_verify): 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): @@ -196,7 +197,7 @@ def test_delete(self): r = self.client.delete(url) self.assertEqual(r.status_code, 200) - url = f"/accounts/893452/users/" + url = "/accounts/893452/users/" r = self.client.delete(url) self.assertEqual(r.status_code, 404) @@ -283,9 +284,9 @@ def test_user_ui(self): 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, @@ -296,6 +297,27 @@ def test_user_ui(self): self.check_not_authenticated("patch", url) +class TestUserReset(TacticalTestCase): + def setUp(self): + self.authenticate() + self.setup_coresettings() + + def test_reset_pw(self): + url = "/accounts/resetpw/" + data = {"password": "superSekret123456"} + r = self.client.put(url, data, format="json") + self.assertEqual(r.status_code, 200) + + self.check_not_authenticated("put", url) + + def test_reset_2fa(self): + url = "/accounts/reset2fa/" + r = self.client.put(url) + self.assertEqual(r.status_code, 200) + + self.check_not_authenticated("put", url) + + class TestAPIKeyViews(TacticalTestCase): def setUp(self): self.setup_coresettings() @@ -308,7 +330,7 @@ def test_get_api_keys(self): serializer = APIKeySerializer(apikeys, many=True) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(serializer.data, resp.data) # type: ignore + self.assertEqual(serializer.data, resp.data) self.check_not_authenticated("get", url) @@ -331,14 +353,14 @@ def test_modify_api_key(self): self.assertEqual(resp.status_code, 404) apikey = baker.make("accounts.APIKey", name="Test") - url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore + url = f"/accounts/apikeys/{apikey.pk}/" - data = {"name": "New Name"} # type: ignore + data = {"name": "New Name"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - apikey = APIKey.objects.get(pk=apikey.pk) # type: ignore - self.assertEquals(apikey.name, "New Name") + apikey = APIKey.objects.get(pk=apikey.pk) + self.assertEqual(apikey.name, "New Name") self.check_not_authenticated("put", url) @@ -349,11 +371,11 @@ def test_delete_api_key(self): # test delete api key apikey = baker.make("accounts.APIKey") - url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore + url = f"/accounts/apikeys/{apikey.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists()) # type: ignore + self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists()) self.check_not_authenticated("delete", url) @@ -393,7 +415,7 @@ def setUp(self): name="Test Token", key="123456", user=self.user ) - self.client_setup() + self.setup_client() def test_api_auth(self): url = "/clients/" diff --git a/api/tacticalrmm/accounts/urls.py b/api/tacticalrmm/accounts/urls.py index 97769dc991..5aeb2178e9 100644 --- a/api/tacticalrmm/accounts/urls.py +++ b/api/tacticalrmm/accounts/urls.py @@ -13,4 +13,6 @@ path("roles//", views.GetUpdateDeleteRole.as_view()), path("apikeys/", views.GetAddAPIKeys.as_view()), path("apikeys//", views.GetUpdateDeleteAPIKey.as_view()), + path("resetpw/", views.ResetPass.as_view()), + path("reset2fa/", views.Reset2FA.as_view()), ] diff --git a/api/tacticalrmm/accounts/utils.py b/api/tacticalrmm/accounts/utils.py new file mode 100644 index 0000000000..e87b786fcd --- /dev/null +++ b/api/tacticalrmm/accounts/utils.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING +from django.conf import settings + +if TYPE_CHECKING: + from django.http import HttpRequest + from accounts.models import User + + +def is_root_user(*, request: "HttpRequest", user: "User") -> bool: + root = ( + hasattr(settings, "ROOT_USER") + and request.user != user + and user.username == settings.ROOT_USER + ) + demo = ( + getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER + ) + return root or demo diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 046670319d..0dbe92337d 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -5,16 +5,16 @@ 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 import status 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 APIKeyPerms, AccountsPerms, RolesPerms +from .permissions import AccountsPerms, APIKeyPerms, RolesPerms from .serializers import ( APIKeySerializer, RoleSerializer, @@ -22,22 +22,13 @@ UserSerializer, UserUISerializer, ) - - -def _is_root_user(request, user) -> bool: - return ( - hasattr(settings, "ROOT_USER") - and request.user != user - and user.username == settings.ROOT_USER - ) +from accounts.utils import is_root_user class CheckCreds(KnoxLoginView): - permission_classes = (AllowAny,) def post(self, request, format=None): - # check credentials serializer = AuthTokenSerializer(data=request.data) if not serializer.is_valid(): @@ -62,7 +53,6 @@ def post(self, request, format=None): class LoginView(KnoxLoginView): - permission_classes = (AllowAny,) def post(self, request, format=None): @@ -80,6 +70,8 @@ def post(self, request, format=None): if settings.DEBUG and token == "sekret": valid = True + elif getattr(settings, "DEMO", False): + valid = True elif totp.verify(token, valid_window=10): valid = True @@ -87,7 +79,7 @@ def post(self, request, format=None): login(request, user) # save ip information - client_ip, is_routable = get_client_ip(request) + client_ip, _ = get_client_ip(request) user.last_login_ip = client_ip user.save() @@ -153,7 +145,7 @@ def get(self, request, pk): def put(self, request, pk): user = get_object_or_404(User, pk=pk) - if _is_root_user(request, user): + if is_root_user(request=request, user=user): return notify_error("The root user cannot be modified from the UI") serializer = UserSerializer(instance=user, data=request.data, partial=True) @@ -164,7 +156,7 @@ def put(self, request, pk): def delete(self, request, pk): user = get_object_or_404(User, pk=pk) - if _is_root_user(request, user): + if is_root_user(request=request, user=user): return notify_error("The root user cannot be deleted from the UI") user.delete() @@ -174,10 +166,11 @@ def delete(self, request, pk): class UserActions(APIView): permission_classes = [IsAuthenticated, AccountsPerms] + # reset password def post(self, request): user = get_object_or_404(User, pk=request.data["id"]) - if _is_root_user(request, user): + if is_root_user(request=request, user=user): return notify_error("The root user cannot be modified from the UI") user.set_password(request.data["password"]) @@ -188,7 +181,7 @@ def post(self, request): # reset two factor token def put(self, request): user = get_object_or_404(User, pk=request.data["id"]) - if _is_root_user(request, user): + if is_root_user(request=request, user=user): return notify_error("The root user cannot be modified from the UI") user.totp_key = "" @@ -200,10 +193,8 @@ def put(self, request): class TOTPSetup(APIView): - # totp setup def post(self, request): - user = request.user if not user.totp_key: code = pyotp.random_base32() @@ -272,7 +263,7 @@ def post(self, request): request.data["key"] = get_random_string(length=32).upper() serializer = APIKeySerializer(data=request.data) serializer.is_valid(raise_exception=True) - obj = serializer.save() + serializer.save() return Response("The API Key was added") @@ -295,3 +286,23 @@ def delete(self, request, pk): apikey = get_object_or_404(APIKey, pk=pk) apikey.delete() return Response("The API Key was deleted") + + +class ResetPass(APIView): + permission_classes = [IsAuthenticated] + + def put(self, request): + user = request.user + user.set_password(request.data["password"]) + user.save() + return Response("Password was reset.") + + +class Reset2FA(APIView): + permission_classes = [IsAuthenticated] + + def put(self, request): + user = request.user + user.totp_key = "" + user.save() + return Response("2FA was reset. Log out and back in to setup.") diff --git a/api/tacticalrmm/agents/admin.py b/api/tacticalrmm/agents/admin.py index 50a920c49a..ed3889e21c 100644 --- a/api/tacticalrmm/agents/admin.py +++ b/api/tacticalrmm/agents/admin.py @@ -1,9 +1,8 @@ from django.contrib import admin -from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory +from .models import Agent, AgentCustomField, AgentHistory, Note admin.site.register(Agent) -admin.site.register(RecoveryAction) admin.site.register(Note) admin.site.register(AgentCustomField) admin.site.register(AgentHistory) diff --git a/api/tacticalrmm/agents/baker_recipes.py b/api/tacticalrmm/agents/baker_recipes.py index cacb2657f3..bf97c4e2c9 100644 --- a/api/tacticalrmm/agents/baker_recipes.py +++ b/api/tacticalrmm/agents/baker_recipes.py @@ -1,6 +1,6 @@ import json import os -import random +import secrets import string from itertools import cycle @@ -8,10 +8,11 @@ from django.utils import timezone as djangotime from model_bakery.recipe import Recipe, foreign_key, seq +from tacticalrmm.constants import AgentMonType, AgentPlat -def generate_agent_id(hostname): - rand = "".join(random.choice(string.ascii_letters) for _ in range(35)) - return f"{rand}-{hostname}" + +def generate_agent_id() -> str: + return "".join(secrets.choice(string.ascii_letters) for i in range(39)) site = Recipe("clients.Site") @@ -24,25 +25,34 @@ def get_wmi_data(): return json.load(f) +def get_win_svcs(): + svcs = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") + with open(svcs) as f: + return json.load(f) + + agent = Recipe( "agents.Agent", site=foreign_key(site), hostname="DESKTOP-TEST123", version="1.3.0", - monitoring_type=cycle(["workstation", "server"]), - agent_id=seq(generate_agent_id("DESKTOP-TEST123")), + monitoring_type=cycle(AgentMonType.values), + agent_id=seq(generate_agent_id()), last_seen=djangotime.now() - djangotime.timedelta(days=5), + plat=AgentPlat.WINDOWS, ) server_agent = agent.extend( - monitoring_type="server", + monitoring_type=AgentMonType.SERVER, ) workstation_agent = agent.extend( - monitoring_type="workstation", + monitoring_type=AgentMonType.WORKSTATION, ) -online_agent = agent.extend(last_seen=djangotime.now()) +online_agent = agent.extend( + last_seen=djangotime.now(), services=get_win_svcs(), wmi_detail=get_wmi_data() +) offline_agent = agent.extend( last_seen=djangotime.now() - djangotime.timedelta(minutes=7) @@ -77,4 +87,4 @@ def get_wmi_data(): ], ) -agent_with_wmi = agent.extend(wmi=get_wmi_data()) +agent_with_wmi = agent.extend(wmi_detail=get_wmi_data()) diff --git a/api/tacticalrmm/agents/consumers.py b/api/tacticalrmm/agents/consumers.py new file mode 100644 index 0000000000..e7067abb98 --- /dev/null +++ b/api/tacticalrmm/agents/consumers.py @@ -0,0 +1,82 @@ +from agents.models import Agent, AgentHistory +from channels.db import database_sync_to_async +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from django.contrib.auth.models import AnonymousUser +from django.shortcuts import get_object_or_404 +from tacticalrmm.constants import AGENT_DEFER, AgentHistoryType +from tacticalrmm.permissions import _has_perm_on_agent + + +class SendCMD(AsyncJsonWebsocketConsumer): + async def connect(self): + self.user = self.scope["user"] + + if isinstance(self.user, AnonymousUser): + await self.close() + + await self.accept() + + async def receive_json(self, payload, **kwargs): + auth = await self.has_perm(payload["agent_id"]) + if not auth: + await self.send_json( + {"ret": "You do not have permission to perform this action."} + ) + return + + agent = await self.get_agent(payload["agent_id"]) + timeout = int(payload["timeout"]) + if payload["shell"] == "custom" and payload["custom_shell"]: + shell = payload["custom_shell"] + else: + shell = payload["shell"] + + hist_pk = await self.get_history_id(agent, payload["cmd"]) + + data = { + "func": "rawcmd", + "timeout": timeout, + "payload": { + "command": payload["cmd"], + "shell": shell, + }, + "id": hist_pk, + } + + ret = await agent.nats_cmd(data, timeout=timeout + 2) + await self.send_json({"ret": ret}) + + async def disconnect(self, _): + pass + + def _has_perm(self, perm: str) -> bool: + if self.user.is_superuser or ( + self.user.role and getattr(self.user.role, "is_superuser") + ): + return True + + # make sure non-superusers with empty roles aren't permitted + elif not self.user.role: + return False + + return self.user.role and getattr(self.user.role, perm) + + @database_sync_to_async # type: ignore + def get_agent(self, agent_id: str) -> "Agent": + return get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id) + + @database_sync_to_async # type: ignore + def get_history_id(self, agent: "Agent", cmd: str) -> int: + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.CMD_RUN, + command=cmd, + username=self.user.username[:50], + ) + return hist.pk + + @database_sync_to_async # type: ignore + def has_perm(self, agent_id: str) -> bool: + return self._has_perm("can_send_cmd") and _has_perm_on_agent( + self.user, agent_id + ) diff --git a/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py b/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py index 9c1cb15c2c..85b1ba8827 100644 --- a/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py +++ b/api/tacticalrmm/agents/management/commands/bulk_delete_agents.py @@ -5,11 +5,12 @@ from packaging import version as pyver from agents.models import Agent -from tacticalrmm.utils import AGENT_DEFER, reload_nats +from tacticalrmm.constants import AGENT_DEFER +from tacticalrmm.utils import reload_nats class Command(BaseCommand): - help = "Delete old agents" + help = "Delete multiple agents based on criteria" def add_arguments(self, parser): parser.add_argument( @@ -22,6 +23,21 @@ def add_arguments(self, parser): type=str, help="Delete agents that equal to or less than this version", ) + parser.add_argument( + "--site", + type=str, + help="Delete agents that belong to the specified site", + ) + parser.add_argument( + "--client", + type=str, + help="Delete agents that belong to the specified client", + ) + parser.add_argument( + "--hostname", + type=str, + help="Delete agents with hostname starting with argument", + ) parser.add_argument( "--delete", action="store_true", @@ -31,25 +47,40 @@ def add_arguments(self, parser): def handle(self, *args, **kwargs): days = kwargs["days"] agentver = kwargs["agentver"] + site = kwargs["site"] + client = kwargs["client"] + hostname = kwargs["hostname"] delete = kwargs["delete"] - if not days and not agentver: + if not days and not agentver and not site and not client and not hostname: self.stdout.write( - self.style.ERROR("Must have at least one parameter: days or agentver") + self.style.ERROR( + "Must have at least one parameter: days, agentver, site, client or hostname" + ) ) return - q = Agent.objects.defer(*AGENT_DEFER) + agents = Agent.objects.select_related("site__client").defer(*AGENT_DEFER) - agents = [] if days: overdue = djangotime.now() - djangotime.timedelta(days=days) - agents = [i for i in q if i.last_seen < overdue] + agents = agents.filter(last_seen__lt=overdue) + + if site: + agents = agents.filter(site__name=site) + + if client: + agents = agents.filter(site__client__name=client) + + if hostname: + agents = agents.filter(hostname__istartswith=hostname) if agentver: - agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)] + agents = [ + i for i in agents if pyver.parse(i.version) <= pyver.parse(agentver) + ] - if not agents: + if len(agents) == 0: self.stdout.write(self.style.ERROR("No agents matched")) return @@ -63,7 +94,7 @@ def handle(self, *args, **kwargs): try: agent.delete() except Exception as e: - err = f"Failed to delete agent {agent.hostname}: {str(e)}" + err = f"Failed to delete agent {agent.hostname}: {e}" self.stdout.write(self.style.ERROR(err)) else: deleted_count += 1 diff --git a/api/tacticalrmm/agents/management/commands/demo_cron.py b/api/tacticalrmm/agents/management/commands/demo_cron.py new file mode 100644 index 0000000000..f175859f99 --- /dev/null +++ b/api/tacticalrmm/agents/management/commands/demo_cron.py @@ -0,0 +1,31 @@ +# import datetime as dt +import random + +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 + + +class Command(BaseCommand): + help = "stuff for demo site in cron" + + def handle(self, *args, **kwargs): + random_dates = [] + now = djangotime.now() + + for _ in range(20): + rand = now - djangotime.timedelta(minutes=random.randint(1, 2)) + random_dates.append(rand) + + for _ in range(5): + rand = now - djangotime.timedelta(minutes=random.randint(10, 20)) + random_dates.append(rand) + + agents = Agent.objects.only("last_seen") + for agent in agents: + agent.last_seen = random.choice(random_dates) + agent.save(update_fields=["last_seen"]) + + cache_db_fields_task() diff --git a/api/tacticalrmm/agents/management/commands/fake_agents.py b/api/tacticalrmm/agents/management/commands/fake_agents.py new file mode 100644 index 0000000000..96ae14f821 --- /dev/null +++ b/api/tacticalrmm/agents/management/commands/fake_agents.py @@ -0,0 +1,846 @@ +import datetime as dt +import json +import random +import string + +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 tacticalrmm.constants import ( + AgentHistoryType, + AgentMonType, + AgentPlat, + AlertSeverity, + CheckStatus, + CheckType, + EvtLogFailWhen, + EvtLogNames, + EvtLogTypes, + GoArch, + PAAction, + ScriptShell, + TaskSyncStatus, + TaskType, +) +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, + wmi_mac, + disks_mac, +) +from winupdate.models import WinUpdate, WinUpdatePolicy + +AGENTS_TO_GENERATE = 250 + +SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") +WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json") +WMI_2 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi2.json") +WMI_3 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi3.json") +SW_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/software1.json") +SW_2 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/software2.json") +WIN_UPDATES = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winupdates.json") +EVT_LOG_FAIL = settings.BASE_DIR.joinpath( + "tacticalrmm/test_data/eventlog_check_fail.json" +) + + +class Command(BaseCommand): + help = "populate database with fake agents" + + 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) -> None: + user = User.objects.first() + if user: + user.totp_key = "ABSA234234" + user.save(update_fields=["totp_key"]) + + Agent.objects.all().delete() + Client.objects.all().delete() + Check.objects.all().delete() + Script.objects.all().delete() + AutomatedTask.objects.all().delete() + CheckHistory.objects.all().delete() + Policy.objects.all().delete() + AuditLog.objects.all().delete() + 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() + check_policy.name = "Demo Checks Policy" + check_policy.desc = "Demo Checks Policy" + check_policy.active = True + check_policy.enforced = True + check_policy.save() + + patch_policy = Policy() + patch_policy.name = "Demo Patch Policy" + patch_policy.desc = "Demo Patch Policy" + patch_policy.active = True + patch_policy.enforced = True + patch_policy.save() + + update_policy = WinUpdatePolicy() + update_policy.policy = patch_policy + update_policy.critical = "approve" + update_policy.important = "approve" + update_policy.moderate = "approve" + update_policy.low = "ignore" + update_policy.other = "ignore" + update_policy.run_time_days = [6, 0, 2] + update_policy.run_time_day = 1 + update_policy.reboot_after_install = "required" + update_policy.reprocess_failed = True + update_policy.email_if_fail = True + update_policy.save() + + clients = ( + "Company 1", + "Company 2", + "Company 3", + "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") + + 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() + client3.save() + client4.save() + client5.save() + client6.save() + + for site in sites1: + Site(client=client1, name=site).save() + + for site in sites2: + Site(client=client2, name=site).save() + + for site in sites3: + Site(client=client3, name=site).save() + + for site in sites4: + Site(client=client4, name=site).save() + + for site in sites5: + Site(client=client5, name=site).save() + + for site in sites6: + Site(client=client6, name=site).save() + + hostnames = ( + "DC-1", + "DC-2", + "FSV-1", + "FSV-2", + "WSUS", + "DESKTOP-12345", + "LAPTOP-55443", + "db-aws-01", + "Karens-MacBook-Air.local", + ) + descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop") + modes = AgentMonType.values + 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 = ( + "Microsoft Windows 8.1 Pro, 64bit (build 9600)", + "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)", + "Microsoft Windows 10 Pro, 64bit (build 18363)", + ) + + linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64" + linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+" + mac_os = "Darwin 12.5.1 arm64 21.6.0" + + 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 = [] + + for _ in range(15): + rand_hour = now - dt.timedelta(hours=random.randint(1, 22)) + boot_times.append(str(rand_hour.timestamp())) + + for _ in range(5): + rand_days = now - dt.timedelta(days=random.randint(2, 50)) + boot_times.append(str(rand_days.timestamp())) + + user_names = ("None", "Karen", "Steve", "jsmith", "jdoe") + + with open(SVCS) as f: + services = json.load(f) + + # WMI + with open(WMI_1) as f: + wmi1 = json.load(f) + + with open(WMI_2) as f: + wmi2 = json.load(f) + + with open(WMI_3) as f: + wmi3 = json.load(f) + + wmi_details = [i for i in (wmi1, wmi2, wmi3)] + + # software + with open(SW_1) as f: + software1 = json.load(f) + + with open(SW_2) as f: + software2 = json.load(f) + + softwares = [i for i in (software1, software2)] + + # windows updates + with open(WIN_UPDATES) as f: + windows_updates = json.load(f)["samplecomputer"] + + # event log check fail data + with open(EVT_LOG_FAIL) as f: + eventlog_check_fail_data = json.load(f) + + # create scripts + + clear_spool = Script() + 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 = 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 = 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 = 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 = 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 = 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 == clients[0]: + site = random.choice(sites1) + elif client == clients[1]: + site = random.choice(sites2) + elif client == clients[2]: + site = random.choice(sites3) + elif client == clients[3]: + site = random.choice(sites4) + elif client == clients[4]: + site = random.choice(sites5) + elif client == clients[5]: + site = random.choice(sites6) + + agent = Agent() + + plat_pick = random.randint(1, 15) + if plat_pick in (7, 11): + agent.plat = AgentPlat.LINUX + mode = AgentMonType.SERVER + # pi arm + if plat_pick == 7: + agent.goarch = GoArch.ARM32 + agent.wmi_detail = wmi_pi + agent.disks = disks_linux_pi + agent.operating_system = linux_pi_os + else: + agent.goarch = GoArch.AMD64 + agent.wmi_detail = wmi_deb + agent.disks = disks_linux_deb + agent.operating_system = linux_deb_os + elif plat_pick in (4, 14): + agent.plat = AgentPlat.DARWIN + mode = random.choice([AgentMonType.SERVER, AgentMonType.WORKSTATION]) + agent.goarch = GoArch.ARM64 + agent.wmi_detail = wmi_mac + agent.disks = disks_mac + agent.operating_system = mac_os + else: + agent.plat = AgentPlat.WINDOWS + agent.goarch = GoArch.AMD64 + mode = random.choice(modes) + agent.wmi_detail = random.choice(wmi_details) + agent.services = services + agent.disks = random.choice(disks) + if mode == AgentMonType.SERVER: + agent.operating_system = random.choice(op_systems_servers) + else: + agent.operating_system = random.choice(op_systems_workstations) + + agent.version = settings.LATEST_AGENT_VER + agent.hostname = random.choice(hostnames) + agent.site = Site.objects.get(name=site) + 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 = django_now + + agent.total_ram = random.choice(total_rams) + agent.boot_time = random.choice(boot_times) + agent.logged_in_username = random.choice(user_names) + agent.mesh_node_id = ( + "3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb" + ) + agent.overdue_email_alert = random.choice([True, False]) + agent.overdue_text_alert = random.choice([True, False]) + agent.needs_reboot = random.choice([True, False]) + + agent.save() + + if agent.plat == AgentPlat.WINDOWS: + InstalledSoftware(agent=agent, software=random.choice(softwares)).save() + + if mode == AgentMonType.WORKSTATION: + WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() + else: + WinUpdatePolicy(agent=agent).save() + + if agent.plat == AgentPlat.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() + hist.agent = agent + hist.type = AgentHistoryType.CMD_RUN + hist.command = "ping google.com" + hist.username = "demo" + hist.results = ping_success_output + hist.save() + + hist1 = AgentHistory() + hist1.agent = agent + hist1.type = AgentHistoryType.SCRIPT_RUN + hist1.script = clear_spool + hist1.script_results = { + "id": 1, + "stderr": "", + "stdout": spooler_stdout, + "execution_time": 3.5554593, + "retcode": 0, + } + hist1.save() + + if agent.plat == AgentPlat.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() + + 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 = django_now - djangotime.timedelta(minutes=i * 2) + check1_history.y = random.randint(13, 40) + check1_history.save() + + # ping check + check2 = Check() + check_result2 = CheckResult() + + check2.agent = agent + 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" + check2.alert_severity = AlertSeverity.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 = CheckStatus.PASSING + check2.ip = "8.8.8.8" + check_result2.more_info = ping_success_output + + check2.save() + check_result2.save() + + for i in range(30): + check2_history = CheckHistory() + check2_history.check_id = check2.pk + check2_history.agent_id = agent.agent_id + check2_history.x = django_now - djangotime.timedelta(minutes=i * 2) + if site in sites5: + check2_history.y = 1 + check2_history.results = ping_fail_output + else: + check2_history.y = 0 + check2_history.results = ping_success_output + check2_history.save() + + # cpu load check + check3 = Check() + check3.agent = agent + 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, + 16, + 22, + 22, + 27, + 15, + 23, + 23, + 20, + 10, + 10, + 13, + 34, + ] + 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 = django_now - djangotime.timedelta(minutes=i * 2) + check3_history.y = random.randint(2, 79) + check3_history.save() + + # memory check + check4 = Check() + check4.agent = agent + check4.check_type = CheckType.MEMORY + check4.warning_threshold = 70 + check4.error_threshold = 85 + 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 = django_now - djangotime.timedelta(minutes=i * 2) + check4_history.y = random.randint(2, 79) + check4_history.save() + + # script check storage pool + check5 = Check() + + check5.agent = agent + check5.check_type = CheckType.SCRIPT + + check5.email_alert = random.choice([True, False]) + check5.text_alert = random.choice([True, False]) + check5.timeout = 120 + + 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 = django_now - djangotime.timedelta(minutes=i * 2) + if i == 10 or i == 18: + check5_history.y = 1 + else: + check5_history.y = 0 + check5_history.results = { + "retcode": 0, + "stdout": None, + "stderr": None, + "execution_time": "4.0000", + } + check5_history.save() + + check6 = Check() + + check6.agent = agent + check6.check_type = CheckType.SCRIPT + check6.email_alert = random.choice([True, False]) + check6.text_alert = random.choice([True, False]) + check6.timeout = 120 + 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 = django_now - djangotime.timedelta(minutes=i * 2) + check6_history.y = 0 + check6_history.results = { + "retcode": 0, + "stdout": None, + "stderr": None, + "execution_time": "4.0000", + } + check6_history.save() + + nla_task = AutomatedTask() + + nla_task.agent = agent + actions = [ + { + "name": restart_nla.name, + "type": "script", + "script": restart_nla.pk, + "timeout": 90, + "script_args": [], + } + ] + nla_task.actions = actions + nla_task.assigned_check = check6 + nla_task.name = "Restart NLA" + nla_task.task_type = TaskType.CHECK_FAILURE + 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 = django_now + nla_task_result.stdout = "no stdout" + nla_task_result.retcode = 0 + nla_task_result.sync_status = TaskSyncStatus.SYNCED + nla_task_result.save() + + spool_task = AutomatedTask() + + spool_task.agent = agent + actions = [ + { + "name": clear_spool.name, + "type": "script", + "script": clear_spool.pk, + "timeout": 90, + "script_args": [], + } + ] + spool_task.actions = actions + spool_task.name = "Clear the print spooler" + spool_task.task_type = TaskType.DAILY + 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.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 = TaskSyncStatus.SYNCED + spool_task_result.save() + + tmp_dir_task = AutomatedTask() + tmp_dir_task.agent = agent + tmp_dir_task.name = "show temp dir files" + actions = [ + { + "name": show_tmp_dir_script.name, + "type": "script", + "script": show_tmp_dir_script.pk, + "timeout": 90, + "script_args": [], + } + ] + tmp_dir_task.actions = actions + tmp_dir_task.task_type = TaskType.MANUAL + 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 = TaskSyncStatus.SYNCED + tmp_dir_task_result.save() + + check7 = Check() + + check7.agent = agent + 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" + check_result7.stdout = spooler_stdout + 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 = django_now - djangotime.timedelta(minutes=i * 2) + check7_history.y = 0 + check7_history.results = { + "retcode": 0, + "stdout": spooler_stdout, + "stderr": None, + "execution_time": "3.1337", + } + check7_history.save() + + if agent.plat == AgentPlat.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() + + 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: + check_result9.extra_details = {"log": []} + check_result9.status = CheckStatus.PASSING + + check9.save() + check_result9.save() + + 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() + + pick = random.randint(1, 10) + + if pick == 5 or pick == 3: + reboot_time = django_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 = 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}")) + + self.stdout.write("done") diff --git a/api/tacticalrmm/agents/management/commands/find_services.py b/api/tacticalrmm/agents/management/commands/find_services.py new file mode 100644 index 0000000000..bee16799f6 --- /dev/null +++ b/api/tacticalrmm/agents/management/commands/find_services.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand + +from agents.models import Agent +from tacticalrmm.constants import AGENT_DEFER + + +class Command(BaseCommand): + help = "Find all agents that have a certain service installed" + + def add_arguments(self, parser): + parser.add_argument("name", type=str) + + def handle(self, *args, **kwargs): + search = kwargs["name"].lower() + + agents = Agent.objects.defer(*AGENT_DEFER) + for agent in agents: + try: + for svc in agent.services: + if ( + search in svc["name"].lower() + or search in svc["display_name"].lower() + ): + self.stdout.write( + self.style.SUCCESS( + f"{agent.hostname} - {svc['name']} ({svc['display_name']}) - {svc['status']}" + ) + ) + except: + continue diff --git a/api/tacticalrmm/agents/management/commands/fix_dupe_agent_customfields.py b/api/tacticalrmm/agents/management/commands/fix_dupe_agent_customfields.py new file mode 100644 index 0000000000..3b6a093ca2 --- /dev/null +++ b/api/tacticalrmm/agents/management/commands/fix_dupe_agent_customfields.py @@ -0,0 +1,24 @@ +from django.core.management.base import BaseCommand + +from agents.models import Agent +from tacticalrmm.constants import AGENT_DEFER + + +class Command(BaseCommand): + def find_duplicates(self, lst): + return list(set([item for item in lst if lst.count(item) > 1])) + + def handle(self, *args, **kwargs): + for agent in Agent.objects.defer(*AGENT_DEFER).prefetch_related( + "custom_fields__field" + ): + if dupes := self.find_duplicates( + [i.field.name for i in agent.custom_fields.all()] + ): + for dupe in dupes: + cf = list( + agent.custom_fields.filter(field__name=dupe).order_by("id") + ) + to_delete = cf[:-1] + for i in to_delete: + i.delete() diff --git a/api/tacticalrmm/agents/management/commands/fix_salt_key.py b/api/tacticalrmm/agents/management/commands/fix_salt_key.py deleted file mode 100644 index ad6ee84a6e..0000000000 --- a/api/tacticalrmm/agents/management/commands/fix_salt_key.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.core.management.base import BaseCommand - -from agents.models import Agent - - -class Command(BaseCommand): - help = "Changes existing agents salt_id from a property to a model field" - - def handle(self, *args, **kwargs): - agents = Agent.objects.filter(salt_id=None) - for agent in agents: - self.stdout.write( - self.style.SUCCESS(f"Setting salt_id on {agent.hostname}") - ) - agent.salt_id = f"{agent.hostname}-{agent.pk}" - agent.save(update_fields=["salt_id"]) diff --git a/api/tacticalrmm/agents/management/commands/show_outdated_agents.py b/api/tacticalrmm/agents/management/commands/show_outdated_agents.py index 8725e6ba6a..794a7e96a0 100644 --- a/api/tacticalrmm/agents/management/commands/show_outdated_agents.py +++ b/api/tacticalrmm/agents/management/commands/show_outdated_agents.py @@ -2,16 +2,16 @@ from django.core.management.base import BaseCommand from agents.models import Agent +from tacticalrmm.constants import AGENT_STATUS_ONLINE, ONLINE_AGENTS class Command(BaseCommand): help = "Shows online agents that are not on the latest version" def handle(self, *args, **kwargs): - q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only( - "pk", "version", "last_seen", "overdue_time", "offline_time" - ) - agents = [i for i in q if i.status == "online"] + only = ONLINE_AGENTS + ("hostname",) + q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(*only) + agents = [i for i in q if i.status == AGENT_STATUS_ONLINE] for agent in agents: self.stdout.write( self.style.SUCCESS(f"{agent.hostname} - v{agent.version}") diff --git a/api/tacticalrmm/agents/management/commands/update_agents.py b/api/tacticalrmm/agents/management/commands/update_agents.py index d5f52c4565..e99cec06a1 100644 --- a/api/tacticalrmm/agents/management/commands/update_agents.py +++ b/api/tacticalrmm/agents/management/commands/update_agents.py @@ -3,17 +3,17 @@ from packaging import version as pyver from agents.models import Agent -from core.models import CoreSettings from agents.tasks import send_agent_update_task -from tacticalrmm.utils import AGENT_DEFER +from core.utils import get_core_settings, token_is_valid +from tacticalrmm.constants import AGENT_DEFER class Command(BaseCommand): help = "Triggers an agent update task to run" def handle(self, *args, **kwargs): - core = CoreSettings.objects.first() - if not core.agent_auto_update: # type: ignore + core = get_core_settings() + if not core.agent_auto_update: return q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER) @@ -22,4 +22,5 @@ def handle(self, *args, **kwargs): for i in q if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) ] - send_agent_update_task.delay(agent_ids=agent_ids) + token, _ = token_is_valid() + send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False) diff --git a/api/tacticalrmm/agents/migrations/0038_agenthistory.py b/api/tacticalrmm/agents/migrations/0038_agenthistory.py index d9c8fba6ee..4d7c6ff3d3 100644 --- a/api/tacticalrmm/agents/migrations/0038_agenthistory.py +++ b/api/tacticalrmm/agents/migrations/0038_agenthistory.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.1 on 2021-07-06 02:01 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py b/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py index f5ad3b5d89..873ce0867f 100644 --- a/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py +++ b/api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.5 on 2021-07-14 07:38 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py b/api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py new file mode 100644 index 0000000000..907cc7e0e4 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2022-02-27 05:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0042_alter_agent_time_zone'), + ] + + operations = [ + migrations.RemoveField( + model_name='agent', + name='antivirus', + ), + migrations.RemoveField( + model_name='agent', + name='local_ip', + ), + migrations.RemoveField( + model_name='agent', + name='used_ram', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py b/api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py new file mode 100644 index 0000000000..22ccf9aada --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.12 on 2022-02-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0043_auto_20220227_0554'), + ] + + operations = [ + migrations.RenameField( + model_name='agent', + old_name='salt_id', + new_name='goarch', + ), + migrations.RemoveField( + model_name='agent', + name='salt_ver', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0045_delete_recoveryaction.py b/api/tacticalrmm/agents/migrations/0045_delete_recoveryaction.py new file mode 100644 index 0000000000..a13f21bcae --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0045_delete_recoveryaction.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.12 on 2022-03-12 02:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0044_auto_20220227_0717'), + ] + + operations = [ + migrations.DeleteModel( + name='RecoveryAction', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0046_alter_agenthistory_command.py b/api/tacticalrmm/agents/migrations/0046_alter_agenthistory_command.py new file mode 100644 index 0000000000..39e9b0ed2a --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0046_alter_agenthistory_command.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-03-17 17:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0045_delete_recoveryaction'), + ] + + operations = [ + migrations.AlterField( + model_name='agenthistory', + name='command', + field=models.TextField(blank=True, default='', null=True), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0047_alter_agent_plat_alter_agent_site.py b/api/tacticalrmm/agents/migrations/0047_alter_agent_plat_alter_agent_site.py new file mode 100644 index 0000000000..943701f69c --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0047_alter_agent_plat_alter_agent_site.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.3 on 2022-04-07 17:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clients', '0020_auto_20211226_0547'), + ('agents', '0046_alter_agenthistory_command'), + ] + + operations = [ + migrations.AlterField( + model_name='agent', + name='plat', + field=models.CharField(default='windows', max_length=255), + ), + migrations.AlterField( + model_name='agent', + name='site', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.RESTRICT, related_name='agents', to='clients.site'), + preserve_default=False, + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0048_remove_agent_has_patches_pending_and_more.py b/api/tacticalrmm/agents/migrations/0048_remove_agent_has_patches_pending_and_more.py new file mode 100644 index 0000000000..06b0bf7811 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0048_remove_agent_has_patches_pending_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.3 on 2022-04-16 17:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0047_alter_agent_plat_alter_agent_site'), + ] + + operations = [ + migrations.RemoveField( + model_name='agent', + name='has_patches_pending', + ), + migrations.RemoveField( + model_name='agent', + name='pending_actions_count', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0049_agent_agents_agen_monitor_df8816_idx.py b/api/tacticalrmm/agents/migrations/0049_agent_agents_agen_monitor_df8816_idx.py new file mode 100644 index 0000000000..66db0c7061 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0049_agent_agents_agen_monitor_df8816_idx.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.3 on 2022-04-18 14:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0048_remove_agent_has_patches_pending_and_more'), + ] + + operations = [ + migrations.AddIndex( + model_name='agent', + index=models.Index(fields=['monitoring_type'], name='agents_agen_monitor_df8816_idx'), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0050_remove_agent_plat_release.py b/api/tacticalrmm/agents/migrations/0050_remove_agent_plat_release.py new file mode 100644 index 0000000000..f0f8bd881d --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0050_remove_agent_plat_release.py @@ -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', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py b/api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py new file mode 100644 index 0000000000..8da73335d7 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-05-18 03:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0050_remove_agent_plat_release'), + ] + + operations = [ + migrations.AlterField( + model_name='agent', + name='plat', + field=models.CharField(choices=[('windows', 'Windows'), ('linux', 'Linux'), ('darwin', 'macOS')], default='windows', max_length=255), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0052_alter_agent_monitoring_type.py b/api/tacticalrmm/agents/migrations/0052_alter_agent_monitoring_type.py new file mode 100644 index 0000000000..66ba71d47c --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0052_alter_agent_monitoring_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-05-18 05:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0051_alter_agent_plat'), + ] + + operations = [ + migrations.AlterField( + model_name='agent', + name='monitoring_type', + field=models.CharField(choices=[('server', 'Server'), ('workstation', 'Workstation')], default='server', max_length=30), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0053_remove_agenthistory_status.py b/api/tacticalrmm/agents/migrations/0053_remove_agenthistory_status.py new file mode 100644 index 0000000000..28e57cf80c --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0053_remove_agenthistory_status.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.4 on 2022-05-18 06:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0052_alter_agent_monitoring_type'), + ] + + operations = [ + migrations.RemoveField( + model_name='agenthistory', + name='status', + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py b/api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py new file mode 100644 index 0000000000..d2f26e1c41 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-06-06 04:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0053_remove_agenthistory_status'), + ] + + operations = [ + migrations.AlterField( + model_name='agent', + name='goarch', + field=models.CharField(blank=True, choices=[('amd64', 'amd64'), ('386', '386'), ('arm64', 'arm64'), ('arm', 'arm')], max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py b/api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py new file mode 100644 index 0000000000..de4ef8b5b9 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py @@ -0,0 +1,631 @@ +# Generated by Django 4.1 on 2022-08-24 07:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("agents", "0054_alter_agent_goarch"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="time_zone", + field=models.CharField( + blank=True, + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "America/North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py b/api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py new file mode 100644 index 0000000000..4771cddabf --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py @@ -0,0 +1,631 @@ +# Generated by Django 4.1.7 on 2023-02-28 22:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("agents", "0055_alter_agent_time_zone"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="time_zone", + field=models.CharField( + blank=True, + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "America/North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/api/tacticalrmm/agents/migrations/0057_alter_agentcustomfield_unique_together.py b/api/tacticalrmm/agents/migrations/0057_alter_agentcustomfield_unique_together.py new file mode 100644 index 0000000000..68d8684c01 --- /dev/null +++ b/api/tacticalrmm/agents/migrations/0057_alter_agentcustomfield_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-07-18 01:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0037_coresettings_open_ai_model_and_more"), + ("agents", "0056_alter_agent_time_zone"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="agentcustomfield", + unique_together={("agent", "field")}, + ), + ] diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index 550cccacf2..fe2b4d7571 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -1,53 +1,89 @@ import asyncio -import base64 import re -import time from collections import Counter -from distutils.version import LooseVersion -from typing import Any +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast import msgpack +import nats import validators -from Crypto.Cipher import AES -from Crypto.Hash import SHA3_384 -from Crypto.Random import get_random_bytes -from Crypto.Util.Padding import pad +from asgiref.sync import sync_to_async 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 nats.aio.client import Client as NATS -from nats.aio.errors import ErrTimeout +from nats.errors import TimeoutError from packaging import version as pyver - -from core.models import TZ_CHOICES, CoreSettings -from logs.models import BaseAuditModel, DebugLog +from packaging.version import Version as LooseVersion + +from agents.utils import get_agent_url +from checks.models import CheckResult +from core.models import TZ_CHOICES +from core.utils import get_core_settings, send_command_with_mesh +from logs.models import BaseAuditModel, DebugLog, PendingAction +from tacticalrmm.constants import ( + AGENT_STATUS_OFFLINE, + AGENT_STATUS_ONLINE, + AGENT_STATUS_OVERDUE, + AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX, + ONLINE_AGENTS, + AgentHistoryType, + AgentMonType, + AgentPlat, + AlertSeverity, + CheckStatus, + CheckType, + CustomFieldType, + DebugLogType, + GoArch, + PAAction, + PAStatus, +) +from tacticalrmm.helpers import setup_nats_options from tacticalrmm.models import PermissionQuerySet +if TYPE_CHECKING: + from alerts.models import Alert, AlertTemplate + from automation.models import Policy + from autotasks.models import AutomatedTask + from checks.models import Check + from clients.models import Client + from winupdate.models import WinUpdatePolicy + +# type helpers +Disk = Union[Dict[str, Any], str] + class Agent(BaseAuditModel): + class Meta: + indexes = [ + models.Index(fields=["monitoring_type"]), + ] + objects = PermissionQuerySet.as_manager() version = models.CharField(default="0.1.0", max_length=255) - salt_ver = models.CharField(default="1.0.3", max_length=255) operating_system = models.CharField(null=True, blank=True, max_length=255) - plat = models.CharField(max_length=255, null=True, blank=True) - plat_release = models.CharField(max_length=255, null=True, blank=True) + plat: "AgentPlat" = models.CharField( # type: ignore + max_length=255, choices=AgentPlat.choices, default=AgentPlat.WINDOWS + ) + goarch: "GoArch" = models.CharField( # type: ignore + max_length=255, choices=GoArch.choices, null=True, blank=True + ) hostname = models.CharField(max_length=255) - salt_id = models.CharField(null=True, blank=True, max_length=255) - local_ip = models.TextField(null=True, blank=True) # deprecated agent_id = models.CharField(max_length=200, unique=True) last_seen = models.DateTimeField(null=True, blank=True) services = models.JSONField(null=True, blank=True) public_ip = models.CharField(null=True, max_length=255) total_ram = models.IntegerField(null=True, blank=True) - used_ram = models.IntegerField(null=True, blank=True) # deprecated disks = models.JSONField(null=True, blank=True) boot_time = models.FloatField(null=True, blank=True) logged_in_username = models.CharField(null=True, blank=True, max_length=255) last_logged_in_user = models.CharField(null=True, blank=True, max_length=255) - antivirus = models.CharField(default="n/a", max_length=255) # deprecated - monitoring_type = models.CharField(max_length=30) + monitoring_type = models.CharField( + max_length=30, choices=AgentMonType.choices, default=AgentMonType.SERVER + ) description = models.CharField(null=True, blank=True, max_length=255) mesh_node_id = models.CharField(null=True, blank=True, max_length=255) overdue_email_alert = models.BooleanField(default=False) @@ -65,8 +101,6 @@ class Agent(BaseAuditModel): ) maintenance_mode = models.BooleanField(default=False) block_policy_inheritance = models.BooleanField(default=False) - pending_actions_count = models.PositiveIntegerField(default=0) - has_patches_pending = models.BooleanField(default=False) alert_template = models.ForeignKey( "alerts.AlertTemplate", related_name="agents", @@ -77,9 +111,7 @@ class Agent(BaseAuditModel): site = models.ForeignKey( "clients.Site", related_name="agents", - null=True, - blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, ) policy = models.ForeignKey( "automation.Policy", @@ -89,45 +121,31 @@ class Agent(BaseAuditModel): on_delete=models.SET_NULL, ) - def save(self, *args, **kwargs): - from automation.tasks import generate_agent_checks_task - - # get old agent if exists - old_agent = Agent.objects.get(pk=self.pk) if self.pk else None - super(Agent, self).save(old_model=old_agent, *args, **kwargs) - - # check if new agent has been created - # or check if policy have changed on agent - # or if site has changed on agent and if so generate policies - # or if agent was changed from server or workstation - if ( - not old_agent - or (old_agent and old_agent.policy != self.policy) - or (old_agent.site != self.site) - or (old_agent.monitoring_type != self.monitoring_type) - or (old_agent.block_policy_inheritance != self.block_policy_inheritance) - ): - generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True) - - def __str__(self): + def __str__(self) -> str: return self.hostname @property - def client(self): + def client(self) -> "Client": return self.site.client @property - def timezone(self): + def timezone(self) -> str: # return the default timezone unless the timezone is explicity set per agent - if self.time_zone is not None: + if self.time_zone: return self.time_zone - else: - from core.models import CoreSettings - return CoreSettings.objects.first().default_time_zone # type: ignore + return get_core_settings().default_time_zone + + @property + def is_posix(self) -> bool: + return self.plat in {AgentPlat.LINUX, AgentPlat.DARWIN} + # DEPRECATED, use goarch instead @property - def arch(self): + def arch(self) -> Optional[str]: + if self.is_posix: + return self.goarch + if self.operating_system is not None: if "64 bit" in self.operating_system or "64bit" in self.operating_system: return "64" @@ -135,53 +153,101 @@ def arch(self): return "32" return None - @property - def winagent_dl(self): - if self.arch == "64": - return settings.DL_64 - elif self.arch == "32": - return settings.DL_32 - return None + def do_update(self, *, token: str = "", force: bool = False) -> str: + ver = settings.LATEST_AGENT_VER - @property - def win_inno_exe(self): - if self.arch == "64": - return f"winagent-v{settings.LATEST_AGENT_VER}.exe" - elif self.arch == "32": - return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe" - return None + if not self.goarch: + DebugLog.warning( + agent=self, + log_type=DebugLogType.AGENT_ISSUES, + message=f"Unable to determine arch on {self.hostname}({self.agent_id}). Skipping agent update.", + ) + return "noarch" + + if pyver.parse(self.version) <= pyver.parse("1.3.0"): + return "not supported" + + url = get_agent_url(goarch=self.goarch, plat=self.plat, token=token) + bin = f"tacticalagent-v{ver}-{self.plat}-{self.goarch}.exe" + + if not force: + if self.pendingactions.filter( # type: ignore + action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING + ).exists(): + self.pendingactions.filter( # type: ignore + action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING + ).delete() + + PendingAction.objects.create( + agent=self, + action_type=PAAction.AGENT_UPDATE, + details={ + "url": url, + "version": ver, + "inno": bin, + }, + ) + + nats_data = { + "func": "agentupdate", + "payload": { + "url": url, + "version": ver, + "inno": bin, + }, + } + asyncio.run(self.nats_cmd(nats_data, wait=False)) + return "created" @property - def status(self): - offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time) - overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time) + def status(self) -> str: + now = djangotime.now() + offline = now - djangotime.timedelta(minutes=self.offline_time) + overdue = now - djangotime.timedelta(minutes=self.overdue_time) if self.last_seen is not None: if (self.last_seen < offline) and (self.last_seen > overdue): - return "offline" + return AGENT_STATUS_OFFLINE elif (self.last_seen < offline) and (self.last_seen < overdue): - return "overdue" + return AGENT_STATUS_OVERDUE else: - return "online" + return AGENT_STATUS_ONLINE else: - return "offline" + return AGENT_STATUS_OFFLINE @property - def checks(self): + def checks(self) -> Dict[str, Any]: total, passing, failing, warning, info = 0, 0, 0, 0, 0 - if self.agentchecks.exists(): # type: ignore - for i in self.agentchecks.all(): # type: ignore - total += 1 - if i.status == "passing": - passing += 1 - elif i.status == "failing": - if i.alert_severity == "error": - failing += 1 - elif i.alert_severity == "warning": - warning += 1 - elif i.alert_severity == "info": - info += 1 + for check in self.get_checks_with_policies(exclude_overridden=True): + total += 1 + if ( + not hasattr(check.check_result, "status") + or isinstance(check.check_result, CheckResult) + and check.check_result.status == CheckStatus.PASSING + ): + passing += 1 + elif ( + isinstance(check.check_result, CheckResult) + and check.check_result.status == CheckStatus.FAILING + ): + alert_severity = ( + check.check_result.alert_severity + if check.check_type + in ( + CheckType.MEMORY, + CheckType.CPU_LOAD, + CheckType.DISK_SPACE, + CheckType.SCRIPT, + ) + else check.alert_severity + ) + if alert_severity == AlertSeverity.ERROR: + failing += 1 + elif alert_severity == AlertSeverity.WARNING: + warning += 1 + elif alert_severity == AlertSeverity.INFO: + info += 1 ret = { "total": total, @@ -194,7 +260,22 @@ def checks(self): return ret @property - def cpu_model(self): + def pending_actions_count(self) -> int: + ret = cache.get(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}") + if ret is None: + ret = self.pendingactions.filter(status=PAStatus.PENDING).count() + cache.set(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}", ret, 600) + + return ret + + @property + def cpu_model(self) -> List[str]: + if self.is_posix: + try: + return cast(List[str], self.wmi_detail["cpus"]) + except: + return ["unknown cpu model"] + ret = [] try: cpus = self.wmi_detail["cpu"] @@ -205,7 +286,16 @@ def cpu_model(self): return ["unknown cpu model"] @property - def graphics(self): + def graphics(self) -> str: + if self.is_posix: + try: + if not self.wmi_detail["gpus"]: + return "No graphics cards" + + return ", ".join(self.wmi_detail["gpus"]) + except: + return "Error getting graphics cards" + ret, mrda = [], [] try: graphics = self.wmi_detail["graphics"] @@ -226,7 +316,13 @@ def graphics(self): return "Graphics info requires agent v1.4.14" @property - def local_ips(self): + def local_ips(self) -> str: + if self.is_posix: + try: + return ", ".join(self.wmi_detail["local_ips"]) + except: + return "error getting local ips" + ret = [] try: ips = self.wmi_detail["network_config"] @@ -247,13 +343,19 @@ def local_ips(self): ret.append(ip) if len(ret) == 1: - return ret[0] - else: - return ", ".join(ret) if ret else "error getting local ips" + return cast(str, ret[0]) + + return ", ".join(ret) if ret else "error getting local ips" @property - def make_model(self): - try: + def make_model(self) -> str: + if self.is_posix: + try: + return cast(str, self.wmi_detail["make_model"]) + except: + return "error getting make/model" + + with suppress(Exception): comp_sys = self.wmi_detail["comp_sys"][0] comp_sys_prod = self.wmi_detail["comp_sys_prod"][0] make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0] @@ -270,19 +372,21 @@ def make_model(self): model = sysfam return f"{make} {model}" - except: - pass - try: + with suppress(Exception): comp_sys_prod = self.wmi_detail["comp_sys_prod"][0] - return [x["Version"] for x in comp_sys_prod if "Version" in x][0] - except: - pass + return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0]) return "unknown make/model" @property - def physical_disks(self): + def physical_disks(self) -> Sequence[Disk]: + if self.is_posix: + try: + return cast(List[Disk], self.wmi_detail["disks"]) + except: + return ["unknown disk"] + try: disks = self.wmi_detail["disk"] ret = [] @@ -304,35 +408,153 @@ def physical_disks(self): except: return ["unknown disk"] + @property + def serial_number(self) -> str: + if self.is_posix: + return "" + + try: + return self.wmi_detail["bios"][0][0]["SerialNumber"] + except: + return "" + + @classmethod + def online_agents(cls, min_version: str = "") -> "List[Agent]": + if min_version: + return [ + i + for i in cls.objects.only(*ONLINE_AGENTS) + if pyver.parse(i.version) >= pyver.parse(min_version) + and i.status == AGENT_STATUS_ONLINE + ] + + return [ + i + for i in cls.objects.only(*ONLINE_AGENTS) + if i.status == AGENT_STATUS_ONLINE + ] + + def is_supported_script(self, platforms: List[str]) -> bool: + return self.plat.lower() in platforms if platforms else True + + def get_checks_with_policies( + self, exclude_overridden: bool = False + ) -> "List[Check]": + if exclude_overridden: + checks = ( + list( + check + for check in self.agentchecks.all() + if not check.overridden_by_policy + ) + + self.get_checks_from_policies() + ) + else: + checks = list(self.agentchecks.all()) + self.get_checks_from_policies() + return self.add_check_results(checks) + + def get_tasks_with_policies(self) -> "List[AutomatedTask]": + tasks = list(self.autotasks.all()) + self.get_tasks_from_policies() + return self.add_task_results(tasks) + + def add_task_results(self, tasks: "List[AutomatedTask]") -> "List[AutomatedTask]": + results = self.taskresults.all() # type: ignore + + for task in tasks: + for result in results: + if result.task.id == task.pk: + task.task_result = result + break + + return tasks + + def add_check_results(self, checks: "List[Check]") -> "List[Check]": + results = self.checkresults.all() # type: ignore + + for check in checks: + for result in results: + if result.assigned_check.id == check.pk: + check.check_result = result + break + + return checks + + def get_agent_policies(self) -> "Dict[str, Optional[Policy]]": + from checks.models import Check + + site_policy = getattr(self.site, f"{self.monitoring_type}_policy", None) + client_policy = getattr(self.client, f"{self.monitoring_type}_policy", None) + default_policy = getattr( + get_core_settings(), f"{self.monitoring_type}_policy", None + ) + + # prefetch excluded objects on polices only if policy is not Non + models.prefetch_related_objects( + [ + policy + for policy in (self.policy, site_policy, client_policy, default_policy) + if policy + ], + "excluded_agents", + "excluded_sites", + "excluded_clients", + models.Prefetch( + "policychecks", queryset=Check.objects.select_related("script") + ), + "autotasks", + ) + + return { + "agent_policy": self.policy + if self.policy and not self.policy.is_agent_excluded(self) + else None, + "site_policy": site_policy + if (site_policy and not site_policy.is_agent_excluded(self)) + and not self.block_policy_inheritance + else None, + "client_policy": client_policy + if (client_policy and not client_policy.is_agent_excluded(self)) + and not self.block_policy_inheritance + and not self.site.block_policy_inheritance + else None, + "default_policy": default_policy + if (default_policy and not default_policy.is_agent_excluded(self)) + and not self.block_policy_inheritance + and not self.site.block_policy_inheritance + and not self.client.block_policy_inheritance + else None, + } + def check_run_interval(self) -> int: interval = self.check_interval # determine if any agent checks have a custom interval and set the lowest interval - for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore + for check in self.get_checks_with_policies(): if check.run_interval and check.run_interval < interval: - # don't allow check runs less than 15s - if check.run_interval < 15: - interval = 15 - else: - interval = check.run_interval + interval = 15 if check.run_interval < 15 else check.run_interval return interval def run_script( self, scriptpk: int, - args: list[str] = [], + args: List[str] = [], timeout: int = 120, full: bool = False, wait: bool = False, run_on_any: bool = False, history_pk: int = 0, + run_as_user: bool = False, + env_vars: list[str] = [], ) -> Any: - from scripts.models import Script script = Script.objects.get(pk=scriptpk) + # always override if set on script model + if script.run_as_user: + run_as_user = True + parsed_args = script.parse_script_args(self, script.shell, args) data = { @@ -343,9 +565,11 @@ def run_script( "code": script.code, "shell": script.shell, }, + "run_as_user": run_as_user, + "env_vars": env_vars, } - if history_pk != 0 and pyver.parse(self.version) >= pyver.parse("1.6.0"): + if history_pk != 0: data["id"] = history_pk running_agent = self @@ -358,15 +582,7 @@ def run_script( if r == "pong": running_agent = self else: - online = [ - agent - for agent in Agent.objects.only( - "pk", "agent_id", "last_seen", "overdue_time", "offline_time" - ) - if agent.status == "online" - ] - - for agent in online: + for agent in Agent.online_agents(): r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1)) if r == "pong": running_agent = agent @@ -383,123 +599,49 @@ def run_script( return "ok" # auto approves updates - def approve_updates(self): + def approve_updates(self) -> None: patch_policy = self.get_patch_policy() - updates = list() + severity_list = [] if patch_policy.critical == "approve": - updates += self.winupdates.filter( # type: ignore - severity="Critical", installed=False - ).exclude(action="approve") + severity_list.append("Critical") if patch_policy.important == "approve": - updates += self.winupdates.filter( # type: ignore - severity="Important", installed=False - ).exclude(action="approve") + severity_list.append("Important") if patch_policy.moderate == "approve": - updates += self.winupdates.filter( # type: ignore - severity="Moderate", installed=False - ).exclude(action="approve") + severity_list.append("Moderate") if patch_policy.low == "approve": - updates += self.winupdates.filter(severity="Low", installed=False).exclude( # type: ignore - action="approve" - ) + severity_list.append("Low") if patch_policy.other == "approve": - updates += self.winupdates.filter(severity="", installed=False).exclude( # type: ignore - action="approve" - ) + severity_list.append("") - for update in updates: - update.action = "approve" - update.save(update_fields=["action"]) + self.winupdates.filter(severity__in=severity_list, installed=False).exclude( + action="approve" + ).update(action="approve") # returns agent policy merged with a client or site specific policy - def get_patch_policy(self): + def get_patch_policy(self) -> "WinUpdatePolicy": + from winupdate.models import WinUpdatePolicy # check if site has a patch policy and if so use it - site = self.site - core_settings = CoreSettings.objects.first() patch_policy = None - agent_policy = self.winupdatepolicy.get() # type: ignore - if self.monitoring_type == "server": - # check agent policy first which should override client or site policy - if self.policy and self.policy.winupdatepolicy.exists(): - patch_policy = self.policy.winupdatepolicy.get() + agent_policy = self.winupdatepolicy.first() - # check site policy if agent policy doesn't have one - elif site.server_policy and site.server_policy.winupdatepolicy.exists(): - # make sure agent isn;t blocking policy inheritance - if not self.block_policy_inheritance: - patch_policy = site.server_policy.winupdatepolicy.get() + if not agent_policy: + agent_policy = WinUpdatePolicy.objects.create(agent=self) - # if site doesn't have a patch policy check the client - elif ( - site.client.server_policy - and site.client.server_policy.winupdatepolicy.exists() - ): - # make sure agent and site are not blocking inheritance - if ( - not self.block_policy_inheritance - and not site.block_policy_inheritance - ): - patch_policy = site.client.server_policy.winupdatepolicy.get() - - # if patch policy still doesn't exist check default policy - elif ( - core_settings.server_policy # type: ignore - and core_settings.server_policy.winupdatepolicy.exists() # type: ignore - ): - # make sure agent site and client are not blocking inheritance - if ( - not self.block_policy_inheritance - and not site.block_policy_inheritance - and not site.client.block_policy_inheritance - ): - patch_policy = core_settings.server_policy.winupdatepolicy.get() # type: ignore - - elif self.monitoring_type == "workstation": - # check agent policy first which should override client or site policy - if self.policy and self.policy.winupdatepolicy.exists(): - patch_policy = self.policy.winupdatepolicy.get() - - elif ( - site.workstation_policy - and site.workstation_policy.winupdatepolicy.exists() - ): - # make sure agent isn;t blocking policy inheritance - if not self.block_policy_inheritance: - patch_policy = site.workstation_policy.winupdatepolicy.get() + # Get the list of policies applied to the agent and select the + # highest priority one. + policies = self.get_agent_policies() - # if site doesn't have a patch policy check the client - elif ( - site.client.workstation_policy - and site.client.workstation_policy.winupdatepolicy.exists() - ): - # make sure agent and site are not blocking inheritance - if ( - not self.block_policy_inheritance - and not site.block_policy_inheritance - ): - patch_policy = site.client.workstation_policy.winupdatepolicy.get() - - # if patch policy still doesn't exist check default policy - elif ( - core_settings.workstation_policy # type: ignore - and core_settings.workstation_policy.winupdatepolicy.exists() # type: ignore - ): - # make sure agent site and client are not blocking inheritance - if ( - not self.block_policy_inheritance - and not site.block_policy_inheritance - and not site.client.block_policy_inheritance - ): - patch_policy = ( - core_settings.workstation_policy.winupdatepolicy.get() # type: ignore - ) + for _, policy in policies.items(): + if policy and policy.active and policy.winupdatepolicy.exists(): + patch_policy = policy.winupdatepolicy.first() + break # if policy still doesn't exist return the agent patch policy if not patch_policy: @@ -545,191 +687,125 @@ def get_approved_update_guids(self) -> list[str]: # sets alert template assigned in the following order: policy, site, client, global # sets None if nothing is found - def set_alert_template(self): - - site = self.site - client = self.client - core = CoreSettings.objects.first() - - templates = list() - # check if alert template is on a policy assigned to agent - if ( - self.policy - and self.policy.alert_template - and self.policy.alert_template.is_active - ): - templates.append(self.policy.alert_template) - - # check if policy with alert template is assigned to the site - if ( - self.monitoring_type == "server" - and site.server_policy - and site.server_policy.alert_template - and site.server_policy.alert_template.is_active - and not self.block_policy_inheritance - ): - templates.append(site.server_policy.alert_template) - if ( - self.monitoring_type == "workstation" - and site.workstation_policy - and site.workstation_policy.alert_template - and site.workstation_policy.alert_template.is_active - and not self.block_policy_inheritance - ): - templates.append(site.workstation_policy.alert_template) - - # check if alert template is assigned to site - if site.alert_template and site.alert_template.is_active: - templates.append(site.alert_template) - - # check if policy with alert template is assigned to the client - if ( - self.monitoring_type == "server" - and client.server_policy - and client.server_policy.alert_template - and client.server_policy.alert_template.is_active - and not self.block_policy_inheritance - and not site.block_policy_inheritance - ): - templates.append(client.server_policy.alert_template) - if ( - self.monitoring_type == "workstation" - and client.workstation_policy - and client.workstation_policy.alert_template - and client.workstation_policy.alert_template.is_active - and not self.block_policy_inheritance - and not site.block_policy_inheritance - ): - templates.append(client.workstation_policy.alert_template) - - # check if alert template is on client and return - if ( - client.alert_template - and client.alert_template.is_active - and not self.block_policy_inheritance - and not site.block_policy_inheritance - ): - templates.append(client.alert_template) + def set_alert_template(self) -> "Optional[AlertTemplate]": + core = get_core_settings() - # check if alert template is applied globally and return - if ( - core.alert_template # type: ignore - and core.alert_template.is_active # type: ignore - and not self.block_policy_inheritance - and not site.block_policy_inheritance - and not client.block_policy_inheritance - ): - templates.append(core.alert_template) # type: ignore - - # if agent is a workstation, check if policy with alert template is assigned to the site, client, or core - if ( - self.monitoring_type == "server" - and core.server_policy # type: ignore - and core.server_policy.alert_template # type: ignore - and core.server_policy.alert_template.is_active # type: ignore - and not self.block_policy_inheritance - and not site.block_policy_inheritance - and not client.block_policy_inheritance - ): - templates.append(core.server_policy.alert_template) # type: ignore - if ( - self.monitoring_type == "workstation" - and core.workstation_policy # type: ignore - and core.workstation_policy.alert_template # type: ignore - and core.workstation_policy.alert_template.is_active # type: ignore - and not self.block_policy_inheritance - and not site.block_policy_inheritance - and not client.block_policy_inheritance - ): - templates.append(core.workstation_policy.alert_template) # type: ignore + policies = self.get_agent_policies() - # go through the templates and return the first one that isn't excluded - for template in templates: - # check if client, site, or agent has been excluded from template + # loop through all policies applied to agent and return an alert_template if found + processed_policies: List[int] = [] + for key, policy in policies.items(): + # default alert_template will override a default policy with alert template applied if ( - client.pk - in template.excluded_clients.all().values_list("pk", flat=True) - or site.pk in template.excluded_sites.all().values_list("pk", flat=True) - or self.pk - in template.excluded_agents.all() - .only("pk") - .values_list("pk", flat=True) + "default" in key + and core.alert_template + and core.alert_template.is_active + and not core.alert_template.is_agent_excluded(self) ): - continue - - # check if template is excluding desktops + self.alert_template = core.alert_template + self.save(update_fields=["alert_template"]) + return core.alert_template elif ( - self.monitoring_type == "workstation" and template.exclude_workstations + policy + and policy.active + and policy.pk not in processed_policies + and policy.alert_template + and policy.alert_template.is_active + and not policy.alert_template.is_agent_excluded(self) ): - continue - - # check if template is excluding servers - elif self.monitoring_type == "server" and template.exclude_servers: - continue - - else: - # save alert_template to agent cache field - self.alert_template = template - self.save() - - return template + self.alert_template = policy.alert_template + self.save(update_fields=["alert_template"]) + return policy.alert_template + elif ( + "site" in key + and self.site.alert_template + and self.site.alert_template.is_active + and not self.site.alert_template.is_agent_excluded(self) + ): + self.alert_template = self.site.alert_template + self.save(update_fields=["alert_template"]) + return self.site.alert_template + elif ( + "client" in key + and self.site.client.alert_template + and self.site.client.alert_template.is_active + and not self.site.client.alert_template.is_agent_excluded(self) + ): + self.alert_template = self.site.client.alert_template + self.save(update_fields=["alert_template"]) + return self.site.client.alert_template # no alert templates found or agent has been excluded self.alert_template = None - self.save() + self.save(update_fields=["alert_template"]) return None - def generate_checks_from_policies(self): + def get_or_create_alert_if_needed( + self, alert_template: "Optional[AlertTemplate]" + ) -> "Optional[Alert]": + from alerts.models import Alert + + return Alert.create_or_return_availability_alert( + self, skip_create=not self.should_create_alert(alert_template) + ) + + def get_checks_from_policies(self) -> "List[Check]": from automation.models import Policy - # Clear agent checks that have overriden_by_policy set - self.agentchecks.update(overriden_by_policy=False) # type: ignore + # check if agent is blocking inheritance + if self.block_policy_inheritance or self.agentchecks.exists(): + cache_key = f"agent_{self.agent_id}_checks" + + elif self.policy: + cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_checks" - # Generate checks based on policies - Policy.generate_policy_checks(self) + else: + cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_checks" + + cached_checks = cache.get(cache_key) + if isinstance(cached_checks, list): + return cached_checks + else: + # clear agent checks that have overridden_by_policy set + self.agentchecks.update(overridden_by_policy=False) # type: ignore - def generate_tasks_from_policies(self): + # get agent checks based on policies + checks = Policy.get_policy_checks(self) + cache.set(cache_key, checks, 600) + return checks + + def get_tasks_from_policies(self) -> "List[AutomatedTask]": from automation.models import Policy - # Generate tasks based on policies - Policy.generate_policy_tasks(self) + # check if agent is blocking inheritance + if self.block_policy_inheritance: + cache_key = f"agent_{self.agent_id}_tasks" - # https://github.com/Ylianst/MeshCentral/issues/59#issuecomment-521965347 - def get_login_token(self, key, user, action=3): - try: - key = bytes.fromhex(key) - key1 = key[0:48] - key2 = key[48:] - msg = '{{"a":{}, "u":"{}","time":{}}}'.format( - action, user.lower(), int(time.time()) - ) - iv = get_random_bytes(16) - - # sha - h = SHA3_384.new() - h.update(key1) - hashed_msg = h.digest() + msg.encode() - - # aes - cipher = AES.new(key2, AES.MODE_CBC, iv) - msg = cipher.encrypt(pad(hashed_msg, 16)) - - return base64.b64encode(iv + msg, altchars=b"@$").decode("utf-8") - except Exception: - return "err" - - async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True): - nc = NATS() - options = { - "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", - "user": "tacticalrmm", - "password": settings.SECRET_KEY, - "connect_timeout": 3, - "max_reconnect_attempts": 2, - } + elif self.policy: + cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_tasks" + + else: + cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_tasks" + + cached_tasks = cache.get(cache_key) + if isinstance(cached_tasks, list): + return cached_tasks + else: + # get agent tasks based on policies + tasks = Policy.get_policy_tasks(self) + cache.set(cache_key, tasks, 600) + return tasks + + def _do_nats_debug(self, agent: "Agent", message: str) -> None: + 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 + ) -> Any: + opts = setup_nats_options() try: - await nc.connect(**options) + nc = await nats.connect(**opts) except: return "natsdown" @@ -738,14 +814,16 @@ async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True): msg = await nc.request( self.agent_id, msgpack.dumps(data), timeout=timeout ) - except ErrTimeout: + except TimeoutError: ret = "timeout" else: try: - ret = msgpack.loads(msg.data) # type: ignore + ret = msgpack.loads(msg.data) except Exception as e: ret = str(e) - DebugLog.error(agent=self, log_type="agent_issues", message=ret) + await sync_to_async(self._do_nats_debug, thread_sensitive=False)( + agent=self, message=ret + ) await nc.close() return ret @@ -754,27 +832,64 @@ async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True): 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.plat == AgentPlat.LINUX: + cmd = "systemctl restart tacticalagent.service" + shell = 3 + elif self.plat == AgentPlat.DARWIN: + cmd = "launchctl kickstart -k system/tacticalagent" + 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): + def serialize(agent: "Agent") -> Dict[str, Any]: # serializes the agent and returns json from .serializers import AgentAuditSerializer return AgentAuditSerializer(agent).data - def delete_superseded_updates(self): - try: + def delete_superseded_updates(self) -> None: + with suppress(Exception): pks = [] # list of pks to delete - kbs = list(self.winupdates.values_list("kb", flat=True)) # type: ignore + kbs = list(self.winupdates.values_list("kb", flat=True)) d = Counter(kbs) dupes = [k for k, v in d.items() if v > 1] for dupe in dupes: - titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True) # type: ignore + titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True) # extract the version from the title and sort from oldest to newest # skip if no version info is available therefore nothing to parse try: + matches = r"(Version|Versão)" + pattern = r"\(" + matches + r"(.*?)\)" vers = [ - re.search(r"\(Version(.*?)\)", i).group(1).strip() # type: ignore + re.search(pattern, i, flags=re.IGNORECASE).group(2).strip() for i in titles ] sorted_vers = sorted(vers, key=LooseVersion) @@ -782,16 +897,16 @@ def delete_superseded_updates(self): continue # append all but the latest version to our list of pks to delete for ver in sorted_vers[:-1]: - q = self.winupdates.filter(kb=dupe).filter(title__contains=ver) # type: ignore + q = self.winupdates.filter(kb=dupe).filter(title__contains=ver) pks.append(q.first().pk) pks = list(set(pks)) - self.winupdates.filter(pk__in=pks).delete() # type: ignore - except: - pass + self.winupdates.filter(pk__in=pks).delete() - def should_create_alert(self, alert_template=None): - return ( + def should_create_alert( + self, alert_template: "Optional[AlertTemplate]" = None + ) -> bool: + return bool( self.overdue_dashboard_alert or self.overdue_email_alert or self.overdue_text_alert @@ -805,11 +920,10 @@ def should_create_alert(self, alert_template=None): ) ) - def send_outage_email(self): - from core.models import CoreSettings + def send_outage_email(self) -> None: + CORE = get_core_settings() - CORE = CoreSettings.objects.first() - CORE.send_mail( # type: ignore + CORE.send_mail( f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue", ( f"Data has not been received from client {self.client.name}, " @@ -820,11 +934,10 @@ def send_outage_email(self): alert_template=self.alert_template, ) - def send_recovery_email(self): - from core.models import CoreSettings + def send_recovery_email(self) -> None: + CORE = get_core_settings() - CORE = CoreSettings.objects.first() - CORE.send_mail( # type: ignore + CORE.send_mail( f"{self.client.name}, {self.site.name}, {self.hostname} - data received", ( f"Data has been received from client {self.client.name}, " @@ -835,50 +948,23 @@ def send_recovery_email(self): alert_template=self.alert_template, ) - def send_outage_sms(self): - from core.models import CoreSettings + def send_outage_sms(self) -> None: + CORE = get_core_settings() - CORE = CoreSettings.objects.first() - CORE.send_sms( # type: ignore + CORE.send_sms( f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue", alert_template=self.alert_template, ) - def send_recovery_sms(self): - from core.models import CoreSettings + def send_recovery_sms(self) -> None: + CORE = get_core_settings() - CORE = CoreSettings.objects.first() - CORE.send_sms( # type: ignore + CORE.send_sms( f"{self.client.name}, {self.site.name}, {self.hostname} - data received", alert_template=self.alert_template, ) -RECOVERY_CHOICES = [ - ("salt", "Salt"), - ("mesh", "Mesh"), - ("command", "Command"), - ("rpc", "Nats RPC"), - ("checkrunner", "Checkrunner"), -] - - -class RecoveryAction(models.Model): - objects = PermissionQuerySet.as_manager() - - agent = models.ForeignKey( - Agent, - related_name="recoveryactions", - on_delete=models.CASCADE, - ) - mode = models.CharField(max_length=50, choices=RECOVERY_CHOICES, default="mesh") - command = models.TextField(null=True, blank=True) - last_run = models.DateTimeField(null=True, blank=True) - - def __str__(self): - return f"{self.agent.hostname} - {self.mode}" - - class Note(models.Model): objects = PermissionQuerySet.as_manager() @@ -897,7 +983,7 @@ class Note(models.Model): note = models.TextField(null=True, blank=True) entry_time = models.DateTimeField(auto_now_add=True) - def __str__(self): + def __str__(self) -> str: return self.agent.hostname @@ -925,44 +1011,38 @@ class AgentCustomField(models.Model): default=list, ) - def __str__(self): - return self.field + class Meta: + unique_together = (("agent", "field"),) + + def __str__(self) -> str: + return self.field.name @property - def value(self): - if self.field.type == "multiple": - return self.multiple_value - elif self.field.type == "checkbox": + def value(self) -> Union[List[Any], bool, str]: + if self.field.type == CustomFieldType.MULTIPLE: + return cast(List[str], self.multiple_value) + elif self.field.type == CustomFieldType.CHECKBOX: return self.bool_value - else: - return self.string_value - - def save_to_field(self, value): - if self.field.type in [ - "text", - "number", - "single", - "datetime", - ]: - self.string_value = value + + return cast(str, self.string_value) + + def save_to_field(self, value: Union[List[Any], bool, str]) -> None: + if self.field.type in ( + CustomFieldType.TEXT, + CustomFieldType.NUMBER, + CustomFieldType.SINGLE, + CustomFieldType.DATETIME, + ): + self.string_value = cast(str, value) self.save() - elif self.field.type == "multiple": + elif self.field.type == CustomFieldType.MULTIPLE: self.multiple_value = value.split(",") self.save() - elif self.field.type == "checkbox": + elif self.field.type == CustomFieldType.CHECKBOX: self.bool_value = bool(value) self.save() -AGENT_HISTORY_TYPES = ( - ("task_run", "Task Run"), - ("script_run", "Script Run"), - ("cmd_run", "CMD Run"), -) - -AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure")) - - class AgentHistory(models.Model): objects = PermissionQuerySet.as_manager() @@ -972,13 +1052,12 @@ class AgentHistory(models.Model): on_delete=models.CASCADE, ) time = models.DateTimeField(auto_now_add=True) - type = models.CharField( - max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run" - ) - command = models.TextField(null=True, blank=True) - status = models.CharField( - max_length=50, choices=AGENT_HISTORY_STATUS, default="success" + type: "AgentHistoryType" = models.CharField( + max_length=50, + choices=AgentHistoryType.choices, + default=AgentHistoryType.CMD_RUN, ) + command = models.TextField(null=True, blank=True, default="") username = models.CharField(max_length=255, default="system") results = models.TextField(null=True, blank=True) script = models.ForeignKey( @@ -990,5 +1069,5 @@ class AgentHistory(models.Model): ) script_results = models.JSONField(null=True, blank=True) - def __str__(self): + def __str__(self) -> str: return f"{self.agent.hostname} - {self.type}" diff --git a/api/tacticalrmm/agents/permissions.py b/api/tacticalrmm/agents/permissions.py index 308245f6b8..4aa5166c6e 100644 --- a/api/tacticalrmm/agents/permissions.py +++ b/api/tacticalrmm/agents/permissions.py @@ -4,7 +4,7 @@ class AgentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_list_agents") and _has_perm_on_agent( @@ -26,77 +26,78 @@ def has_permission(self, r, view): class RecoverAgentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: + if "agent_id" not in view.kwargs.keys(): + return _has_perm(r, "can_recover_agents") + return _has_perm(r, "can_recover_agents") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class MeshPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_use_mesh") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class UpdateAgentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_update_agents") class PingAgentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_ping_agents") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class ManageProcPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_manage_procs") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class EvtLogPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class SendCMDPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_send_cmd") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class RebootAgentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class InstallAgentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_install_agents") class RunScriptPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_run_scripts") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) class AgentNotesPerms(permissions.BasePermission): - def has_permission(self, r, view): - + def has_permission(self, r, view) -> bool: # permissions for GET /agents/notes/ endpoint if r.method == "GET": - # permissions for /agents//notes endpoint if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_list_notes") and _has_perm_on_agent( @@ -109,15 +110,25 @@ def has_permission(self, r, view): class RunBulkPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_run_bulk") class AgentHistoryPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) - else: - return _has_perm(r, "can_list_agent_history") + + return _has_perm(r, "can_list_agent_history") + + +class AgentWOLPerms(permissions.BasePermission): + def has_permission(self, r, view) -> bool: + if "agent_id" in view.kwargs.keys(): + return _has_perm(r, "can_send_wol") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) + + return _has_perm(r, "can_send_wol") diff --git a/api/tacticalrmm/agents/serializers.py b/api/tacticalrmm/agents/serializers.py index 4d19b17fb0..1a3fb9b5ba 100644 --- a/api/tacticalrmm/agents/serializers.py +++ b/api/tacticalrmm/agents/serializers.py @@ -1,8 +1,10 @@ import pytz from rest_framework import serializers + +from tacticalrmm.constants import AGENT_STATUS_ONLINE from winupdate.serializers import WinUpdatePolicySerializer -from .models import Agent, AgentCustomField, Note, AgentHistory +from .models import Agent, AgentCustomField, AgentHistory, Note class AgentCustomFieldSerializer(serializers.ModelSerializer): @@ -40,6 +42,33 @@ class AgentSerializer(serializers.ModelSerializer): custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) patches_last_installed = serializers.ReadOnlyField() last_seen = serializers.ReadOnlyField() + applied_policies = serializers.SerializerMethodField() + effective_patch_policy = serializers.SerializerMethodField() + alert_template = serializers.SerializerMethodField() + + def get_alert_template(self, obj): + from alerts.serializers import AlertTemplateSerializer + + return ( + AlertTemplateSerializer(obj.alert_template).data + if obj.alert_template + else None + ) + + def get_effective_patch_policy(self, obj): + return WinUpdatePolicySerializer(obj.get_patch_policy()).data + + def get_applied_policies(self, obj): + from automation.serializers import PolicySerializer + + policies = obj.get_agent_policies() + + # need to serialize model objects manually + for key, policy in policies.items(): + if policy: + policies[key] = PolicySerializer(policy).data + + return policies def get_all_timezones(self, obj): return pytz.all_timezones @@ -52,44 +81,44 @@ class Meta: class AgentTableSerializer(serializers.ModelSerializer): status = serializers.ReadOnlyField() checks = serializers.ReadOnlyField() - last_seen = serializers.SerializerMethodField() client_name = serializers.ReadOnlyField(source="client.name") site_name = serializers.ReadOnlyField(source="site.name") logged_username = serializers.SerializerMethodField() italic = serializers.SerializerMethodField() policy = serializers.ReadOnlyField(source="policy.id") alert_template = serializers.SerializerMethodField() + last_seen = serializers.ReadOnlyField() + pending_actions_count = serializers.ReadOnlyField() + has_patches_pending = serializers.ReadOnlyField() + cpu_model = serializers.ReadOnlyField() + graphics = serializers.ReadOnlyField() + local_ips = serializers.ReadOnlyField() + make_model = serializers.ReadOnlyField() + physical_disks = serializers.ReadOnlyField() + serial_number = serializers.ReadOnlyField() + custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) def get_alert_template(self, obj): - if not obj.alert_template: return None - else: - return { - "name": obj.alert_template.name, - "always_email": obj.alert_template.agent_always_email, - "always_text": obj.alert_template.agent_always_text, - "always_alert": obj.alert_template.agent_always_alert, - } - - def get_last_seen(self, obj) -> str: - if obj.time_zone is not None: - agent_tz = pytz.timezone(obj.time_zone) - else: - agent_tz = self.context["default_tz"] - - return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M") + + return { + "name": obj.alert_template.name, + "always_email": obj.alert_template.agent_always_email, + "always_text": obj.alert_template.agent_always_text, + "always_alert": obj.alert_template.agent_always_alert, + } def get_logged_username(self, obj) -> str: - if obj.logged_in_username == "None" and obj.status == "online": + if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE: return obj.last_logged_in_user elif obj.logged_in_username != "None": return obj.logged_in_username - else: - return "-" + + return "-" def get_italic(self, obj) -> bool: - return obj.logged_in_username == "None" and obj.status == "online" + return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE class Meta: model = Agent @@ -102,7 +131,6 @@ class Meta: "monitoring_type", "description", "needs_reboot", - "has_patches_pending", "pending_actions_count", "status", "overdue_text_alert", @@ -116,16 +144,23 @@ class Meta: "italic", "policy", "block_policy_inheritance", + "plat", + "goarch", + "has_patches_pending", + "version", + "operating_system", + "public_ip", + "cpu_model", + "graphics", + "local_ips", + "make_model", + "physical_disks", + "custom_fields", + "serial_number", ] depth = 2 -class WinAgentSerializer(serializers.ModelSerializer): - class Meta: - model = Agent - fields = "__all__" - - class AgentHostnameSerializer(serializers.ModelSerializer): client = serializers.ReadOnlyField(source="client.name") site = serializers.ReadOnlyField(source="site.name") @@ -152,17 +187,12 @@ class Meta: class AgentHistorySerializer(serializers.ModelSerializer): - time = serializers.SerializerMethodField(read_only=True) script_name = serializers.ReadOnlyField(source="script.name") class Meta: model = AgentHistory fields = "__all__" - def get_time(self, history): - tz = self.context["default_tz"] - return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S") - class AgentAuditSerializer(serializers.ModelSerializer): class Meta: diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index bd98daad93..6ee0bc23e8 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -1,122 +1,54 @@ -import asyncio import datetime as dt -import random from time import sleep -from typing import Union +from typing import TYPE_CHECKING, Optional -from alerts.models import Alert -from core.models import CoreSettings -from django.conf import settings +from django.core.management import call_command 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 agents.models import Agent -from agents.utils import get_winagent_url -from tacticalrmm.utils import AGENT_DEFER - - -def agent_update(agent_id: str, force: bool = False) -> str: - - agent = Agent.objects.get(agent_id=agent_id) - - if pyver.parse(agent.version) <= pyver.parse("1.3.0"): - return "not supported" - - # skip if we can't determine the arch - if agent.arch is None: - DebugLog.warning( - agent=agent, - log_type="agent_issues", - message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.", - ) - return "noarch" - - version = settings.LATEST_AGENT_VER - inno = agent.win_inno_exe - url = get_winagent_url(agent.arch) - - if not force: - if agent.pendingactions.filter( - action_type="agentupdate", status="pending" - ).exists(): - agent.pendingactions.filter( - action_type="agentupdate", status="pending" - ).delete() - - PendingAction.objects.create( - agent=agent, - action_type="agentupdate", - details={ - "url": url, - "version": version, - "inno": inno, - }, - ) - - nats_data = { - "func": "agentupdate", - "payload": { - "url": url, - "version": version, - "inno": inno, - }, - } - asyncio.run(agent.nats_cmd(nats_data, wait=False)) - return "created" +from core.utils import get_core_settings +from logs.models import DebugLog +from scripts.models import Script +from tacticalrmm.celery import app +from tacticalrmm.constants import ( + AGENT_DEFER, + AGENT_OUTAGES_LOCK, + AGENT_STATUS_OVERDUE, + CheckStatus, + DebugLogType, +) +from tacticalrmm.helpers import rand_range +from tacticalrmm.utils import redis_lock - -@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)) - for chunk in chunks: - for agent_id in chunk: - agent_update(agent_id=agent_id, force=True) - sleep(0.05) - sleep(4) +if TYPE_CHECKING: + from django.db.models.query import QuerySet @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)) - for chunk in chunks: - for agent_id in chunk: - agent_update(agent_id) - sleep(0.05) - sleep(4) +def send_agent_update_task(*, agent_ids: list[str], token: str, force: bool) -> None: + agents: "QuerySet[Agent]" = Agent.objects.defer(*AGENT_DEFER).filter( + agent_id__in=agent_ids + ) + for agent in agents: + agent.do_update(token=token, force=force) @app.task def auto_self_agent_update_task() -> None: - core = CoreSettings.objects.first() - if not core.agent_auto_update: # type:ignore - return - - q = Agent.objects.only("agent_id", "version") - agent_ids: list[str] = [ - i.agent_id - for i in q - 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)) - for chunk in chunks: - for agent_id in chunk: - agent_update(agent_id) - sleep(0.05) - sleep(4) + call_command("update_agents") @app.task -def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str: +def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) -> str: from alerts.models import Alert - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" if not alert.email_sent: - sleep(random.randint(1, 15)) + sleep(rand_range(100, 1500)) alert.agent.send_outage_email() alert.email_sent = djangotime.now() alert.save(update_fields=["email_sent"]) @@ -125,7 +57,7 @@ def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) # send an email only if the last email sent is older than alert interval delta = djangotime.now() - dt.timedelta(days=alert_interval) if alert.email_sent < delta: - sleep(random.randint(1, 10)) + sleep(rand_range(100, 1500)) alert.agent.send_outage_email() alert.email_sent = djangotime.now() alert.save(update_fields=["email_sent"]) @@ -137,8 +69,13 @@ def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) def agent_recovery_email_task(pk: int) -> str: from alerts.models import Alert - sleep(random.randint(1, 15)) - alert = Alert.objects.get(pk=pk) + sleep(rand_range(100, 1500)) + + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" + alert.agent.send_recovery_email() alert.resolved_email_sent = djangotime.now() alert.save(update_fields=["resolved_email_sent"]) @@ -147,13 +84,16 @@ def agent_recovery_email_task(pk: int) -> str: @app.task -def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str: +def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> str: from alerts.models import Alert - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" if not alert.sms_sent: - sleep(random.randint(1, 15)) + sleep(rand_range(100, 1500)) alert.agent.send_outage_sms() alert.sms_sent = djangotime.now() alert.save(update_fields=["sms_sent"]) @@ -162,7 +102,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> # send an sms only if the last sms sent is older than alert interval delta = djangotime.now() - dt.timedelta(days=alert_interval) if alert.sms_sent < delta: - sleep(random.randint(1, 10)) + sleep(rand_range(100, 1500)) alert.agent.send_outage_sms() alert.sms_sent = djangotime.now() alert.save(update_fields=["sms_sent"]) @@ -174,8 +114,12 @@ def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> def agent_recovery_sms_task(pk: int) -> str: from alerts.models import Alert - sleep(random.randint(1, 3)) - alert = Alert.objects.get(pk=pk) + sleep(rand_range(100, 1500)) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" + alert.agent.send_recovery_sms() alert.resolved_sms_sent = djangotime.now() alert.save(update_fields=["resolved_sms_sent"]) @@ -183,24 +127,20 @@ def agent_recovery_sms_task(pk: int) -> str: return "ok" -@app.task -def agent_outages_task() -> None: - from alerts.models import Alert +@app.task(bind=True) +def agent_outages_task(self) -> str: + with redis_lock(AGENT_OUTAGES_LOCK, self.app.oid) as acquired: + if not acquired: + return f"{self.app.oid} still running" - agents = Agent.objects.only( - "pk", - "agent_id", - "last_seen", - "offline_time", - "overdue_time", - "overdue_email_alert", - "overdue_text_alert", - "overdue_dashboard_alert", - ) + from alerts.models import Alert + from core.tasks import _get_agent_qs - for agent in agents: - if agent.status == "overdue": - Alert.handle_alert_failure(agent) + for agent in _get_agent_qs(): + if agent.status == AGENT_STATUS_OVERDUE: + Alert.handle_alert_failure(agent) + + return "completed" @app.task @@ -211,6 +151,8 @@ def run_script_email_results_task( emails: list[str], args: list[str] = [], history_pk: int = 0, + run_as_user: bool = False, + env_vars: list[str] = [], ): agent = Agent.objects.get(pk=agentpk) script = Script.objects.get(pk=scriptpk) @@ -221,16 +163,18 @@ def run_script_email_results_task( timeout=nats_timeout, wait=True, history_pk=history_pk, + run_as_user=run_as_user, + env_vars=env_vars, ) 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 - CORE = CoreSettings.objects.first() + CORE = get_core_settings() subject = f"{agent.hostname} {script.name} Results" exec_time = "{:.4f}".format(r["execution_time"]) body = ( @@ -243,25 +187,21 @@ def run_script_email_results_task( msg = EmailMessage() msg["Subject"] = subject - msg["From"] = CORE.smtp_from_email # type:ignore + msg["From"] = CORE.smtp_from_email if emails: msg["To"] = ", ".join(emails) else: - msg["To"] = ", ".join(CORE.email_alert_recipients) # type:ignore + msg["To"] = ", ".join(CORE.email_alert_recipients) msg.set_content(body) try: - with smtplib.SMTP( - CORE.smtp_host, CORE.smtp_port, timeout=20 # type:ignore - ) as server: # type:ignore - if CORE.smtp_requires_auth: # type:ignore + with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server: + if CORE.smtp_requires_auth: server.ehlo() server.starttls() - server.login( - CORE.smtp_host_user, CORE.smtp_host_password # type:ignore - ) # type:ignore + server.login(CORE.smtp_host_user, CORE.smtp_host_password) server.send_message(msg) server.quit() else: @@ -273,18 +213,22 @@ def run_script_email_results_task( @app.task def clear_faults_task(older_than_days: int) -> None: - # https://github.com/wh1te909/tacticalrmm/issues/484 + from alerts.models import Alert + + # https://github.com/amidaware/tacticalrmm/issues/484 agents = Agent.objects.exclude(last_seen__isnull=True).filter( last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) ) for agent in agents: - if agent.agentchecks.exists(): - for check in agent.agentchecks.all(): - # reset check status - check.status = "passing" - check.save(update_fields=["status"]) - if check.alert.filter(resolved=False).exists(): - check.alert.get(resolved=False).resolve() + for check in agent.get_checks_with_policies(): + # reset check status + if check.check_result: + 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) + if alert: + alert.resolve() # reset overdue alerts agent.overdue_email_alert = False @@ -311,40 +255,5 @@ def prune_agent_history(older_than_days: int) -> str: @app.task -def handle_agents_task() -> None: - q = Agent.objects.defer(*AGENT_DEFER) - agents = [ - i - for i in q - if pyver.parse(i.version) >= pyver.parse("1.6.0") and i.status == "online" - ] - for agent in agents: - # 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.pendingactions.filter( - action_type="agentupdate", status="pending" - ).exists() - ): - agent.pendingactions.filter( - action_type="agentupdate", status="pending" - ).update(status="completed") - - # sync scheduled tasks - if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore - tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore - - for task in tasks: - if task.sync_status == "pendingdeletion": - task.delete_task_on_agent() - elif task.sync_status == "initial": - task.modify_task_on_agent() - elif task.sync_status == "notsynced": - task.create_task_on_agent() - - # handles any alerting actions - if Alert.objects.filter(agent=agent, resolved=False).exists(): - try: - Alert.handle_alert_resolve(agent) - except: - continue +def bulk_recover_agents_task() -> None: + call_command("bulk_restart_agents") diff --git a/scripts/.gitkeep b/api/tacticalrmm/agents/tests/__init__.py similarity index 100% rename from scripts/.gitkeep rename to api/tacticalrmm/agents/tests/__init__.py diff --git a/api/tacticalrmm/agents/tests/test_agent_installs.py b/api/tacticalrmm/agents/tests/test_agent_installs.py new file mode 100644 index 0000000000..35a541d433 --- /dev/null +++ b/api/tacticalrmm/agents/tests/test_agent_installs.py @@ -0,0 +1,106 @@ +from unittest.mock import patch + +from rest_framework.response import Response + +from tacticalrmm.test import TacticalTestCase + + +class TestAgentInstalls(TacticalTestCase): + def setUp(self) -> None: + self.authenticate() + self.setup_coresettings() + self.setup_base_instance() + + @patch("agents.utils.generate_linux_install") + @patch("knox.models.AuthToken.objects.create") + @patch("tacticalrmm.utils.generate_winagent_exe") + @patch("core.utils.token_is_valid") + @patch("agents.utils.get_agent_url") + def test_install_agent( + self, + mock_agent_url, + mock_token_valid, + mock_gen_win_exe, + mock_auth, + mock_linux_install, + ): + mock_agent_url.return_value = "https://example.com" + mock_token_valid.return_value = "", False + mock_gen_win_exe.return_value = Response("ok") + mock_auth.return_value = "", "token" + mock_linux_install.return_value = Response("ok") + + url = "/agents/installer/" + + # test windows dynamic exe + data = { + "installMethod": "exe", + "client": self.site2.client.pk, + "site": self.site2.pk, + "expires": 24, + "agenttype": "server", + "power": 0, + "rdp": 1, + "ping": 0, + "goarch": "amd64", + "api": "https://api.example.com", + "fileName": "rmm-client-site-server.exe", + "plat": "windows", + } + + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + + mock_gen_win_exe.assert_called_with( + client=self.site2.client.pk, + site=self.site2.pk, + agent_type="server", + rdp=1, + ping=0, + power=0, + goarch="amd64", + token="token", + api="https://api.example.com", + file_name="rmm-client-site-server.exe", + ) + + # test linux no code sign + data["plat"] = "linux" + data["installMethod"] = "bash" + data["rdp"] = 0 + data["agenttype"] = "workstation" + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 400) + + # test linux + mock_token_valid.return_value = "token123", True + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + mock_linux_install.assert_called_with( + client=str(self.site2.client.pk), + site=str(self.site2.pk), + agent_type="workstation", + arch="amd64", + token="token", + api="https://api.example.com", + download_url="https://example.com", + ) + + # test manual + data["rdp"] = 1 + data["installMethod"] = "manual" + r = self.client.post(url, data, format="json") + self.assertIn("rdp", r.json()["cmd"]) + self.assertNotIn("power", r.json()["cmd"]) + + data.update({"ping": 1, "power": 1}) + r = self.client.post(url, data, format="json") + self.assertIn("power", r.json()["cmd"]) + self.assertIn("ping", r.json()["cmd"]) + + # test powershell + data["installMethod"] = "powershell" + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + + self.check_not_authenticated("post", url) diff --git a/api/tacticalrmm/agents/tests/test_agent_update.py b/api/tacticalrmm/agents/tests/test_agent_update.py new file mode 100644 index 0000000000..d73e00a694 --- /dev/null +++ b/api/tacticalrmm/agents/tests/test_agent_update.py @@ -0,0 +1,313 @@ +from unittest.mock import patch + +from django.conf import settings +from django.core.management import call_command +from model_bakery import baker +from packaging import version as pyver + +from agents.models import Agent +from agents.tasks import auto_self_agent_update_task, send_agent_update_task +from logs.models import PendingAction +from tacticalrmm.constants import ( + AGENT_DEFER, + AgentMonType, + AgentPlat, + GoArch, + PAAction, + PAStatus, +) +from tacticalrmm.test import TacticalTestCase + + +class TestAgentUpdate(TacticalTestCase): + def setUp(self) -> None: + self.authenticate() + self.setup_coresettings() + self.setup_base_instance() + + @patch("agents.management.commands.update_agents.send_agent_update_task.delay") + @patch("agents.management.commands.update_agents.token_is_valid") + @patch("agents.management.commands.update_agents.get_core_settings") + def test_update_agents_mgmt_command(self, mock_core, mock_token, mock_update): + mock_token.return_value = ("token123", True) + + baker.make_recipe( + "agents.online_agent", + site=self.site1, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version="2.0.3", + _quantity=6, + ) + + baker.make_recipe( + "agents.online_agent", + site=self.site3, + monitoring_type=AgentMonType.WORKSTATION, + plat=AgentPlat.LINUX, + version="2.0.3", + _quantity=5, + ) + + baker.make_recipe( + "agents.online_agent", + site=self.site2, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version=settings.LATEST_AGENT_VER, + _quantity=8, + ) + + mock_core.return_value.agent_auto_update = False + call_command("update_agents") + mock_update.assert_not_called() + + mock_core.return_value.agent_auto_update = True + call_command("update_agents") + + ids = list( + Agent.objects.defer(*AGENT_DEFER) + .exclude(version=settings.LATEST_AGENT_VER) + .values_list("agent_id", flat=True) + ) + + mock_update.assert_called_with(agent_ids=ids, token="token123", force=False) + + @patch("agents.models.Agent.nats_cmd") + @patch("agents.models.get_agent_url") + def test_do_update(self, mock_agent_url, mock_nats_cmd): + mock_agent_url.return_value = "https://example.com/123" + + # test noarch + agent_noarch = baker.make_recipe( + "agents.online_agent", + site=self.site1, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version="2.1.1", + ) + r = agent_noarch.do_update(token="", force=True) + self.assertEqual(r, "noarch") + + # test too old + agent_old = baker.make_recipe( + "agents.online_agent", + site=self.site2, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version="1.3.0", + goarch=GoArch.AMD64, + ) + r = agent_old.do_update(token="", force=True) + self.assertEqual(r, "not supported") + + win = baker.make_recipe( + "agents.online_agent", + site=self.site1, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version="2.1.1", + goarch=GoArch.AMD64, + ) + + lin = baker.make_recipe( + "agents.online_agent", + site=self.site3, + monitoring_type=AgentMonType.WORKSTATION, + plat=AgentPlat.LINUX, + version="2.1.1", + goarch=GoArch.ARM32, + ) + + # test windows agent update + r = win.do_update(token="", force=False) + self.assertEqual(r, "created") + mock_nats_cmd.assert_called_with( + { + "func": "agentupdate", + "payload": { + "url": "https://example.com/123", + "version": settings.LATEST_AGENT_VER, + "inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe", + }, + }, + wait=False, + ) + action1 = PendingAction.objects.get(agent__agent_id=win.agent_id) + self.assertEqual(action1.action_type, PAAction.AGENT_UPDATE) + self.assertEqual(action1.status, PAStatus.PENDING) + self.assertEqual(action1.details["url"], "https://example.com/123") + self.assertEqual( + action1.details["inno"], + f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe", + ) + self.assertEqual(action1.details["version"], settings.LATEST_AGENT_VER) + + mock_nats_cmd.reset_mock() + + # test linux agent update + r = lin.do_update(token="", force=False) + mock_nats_cmd.assert_called_with( + { + "func": "agentupdate", + "payload": { + "url": "https://example.com/123", + "version": settings.LATEST_AGENT_VER, + "inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe", + }, + }, + wait=False, + ) + action2 = PendingAction.objects.get(agent__agent_id=lin.agent_id) + self.assertEqual(action2.action_type, PAAction.AGENT_UPDATE) + self.assertEqual(action2.status, PAStatus.PENDING) + self.assertEqual(action2.details["url"], "https://example.com/123") + self.assertEqual( + action2.details["inno"], + f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe", + ) + self.assertEqual(action2.details["version"], settings.LATEST_AGENT_VER) + + # check if old agent update pending actions are being deleted + # should only be 1 pending action at all times + pa_count = win.pendingactions.filter( + action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING + ).count() + self.assertEqual(pa_count, 1) + + for _ in range(4): + win.do_update(token="", force=False) + + pa_count = win.pendingactions.filter( + action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING + ).count() + self.assertEqual(pa_count, 1) + + def test_auto_self_agent_update_task(self): + auto_self_agent_update_task() + + @patch("agents.models.Agent.do_update") + def test_send_agent_update_task(self, mock_update): + baker.make_recipe( + "agents.online_agent", + site=self.site2, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version="2.1.1", + goarch=GoArch.AMD64, + _quantity=6, + ) + ids = list( + Agent.objects.defer(*AGENT_DEFER) + .exclude(version=settings.LATEST_AGENT_VER) + .values_list("agent_id", flat=True) + ) + send_agent_update_task(agent_ids=ids, token="", force=False) + self.assertEqual(mock_update.call_count, 6) + + @patch("agents.views.token_is_valid") + @patch("agents.tasks.send_agent_update_task.delay") + def test_update_agents(self, mock_update, mock_token): + mock_token.return_value = ("", False) + url = "/agents/update/" + baker.make_recipe( + "agents.online_agent", + site=self.site2, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version="2.1.1", + goarch=GoArch.AMD64, + _quantity=7, + ) + baker.make_recipe( + "agents.online_agent", + site=self.site2, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + version=settings.LATEST_AGENT_VER, + goarch=GoArch.AMD64, + _quantity=3, + ) + baker.make_recipe( + "agents.online_agent", + site=self.site2, + monitoring_type=AgentMonType.WORKSTATION, + plat=AgentPlat.LINUX, + version="2.0.1", + goarch=GoArch.ARM32, + _quantity=9, + ) + + agent_ids: list[str] = list( + Agent.objects.only("agent_id").values_list("agent_id", flat=True) + ) + + data = {"agent_ids": agent_ids} + expected: list[str] = [ + i.agent_id + for i in Agent.objects.only("agent_id", "version") + if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) + ] + + r = self.client.post(url, data, format="json") + self.assertEqual(r.status_code, 200) + + mock_update.assert_called_with(agent_ids=expected, token="", force=False) + + self.check_not_authenticated("post", url) + + @patch("agents.views.token_is_valid") + @patch("agents.tasks.send_agent_update_task.delay") + def test_agent_update_permissions(self, update_task, mock_token): + mock_token.return_value = ("", False) + + agents = baker.make_recipe("agents.agent", _quantity=5) + other_agents = baker.make_recipe("agents.agent", _quantity=7) + + url = "/agents/update/" + + data = { + "agent_ids": [agent.agent_id for agent in agents] + + [agent.agent_id for agent in other_agents] + } + + # test superuser access + self.check_authorized_superuser("post", url, data) + update_task.assert_called_with( + agent_ids=data["agent_ids"], token="", force=False + ) + update_task.reset_mock() + + user = self.create_user_with_roles([]) + self.client.force_authenticate(user=user) + + self.check_not_authorized("post", url, data) + update_task.assert_not_called() + + user.role.can_update_agents = True + user.role.save() + + self.check_authorized("post", url, data) + update_task.assert_called_with( + agent_ids=data["agent_ids"], token="", force=False + ) + update_task.reset_mock() + + # limit to client + # user.role.can_view_clients.set([agents[0].client]) + # self.check_authorized("post", url, data) + # update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents]) + # update_task.reset_mock() + + # add site + # user.role.can_view_sites.set([other_agents[0].site]) + # self.check_authorized("post", url, data) + # update_task.assert_called_with(agent_ids=data["agent_ids"]) + # update_task.reset_mock() + + # remove client permissions + # user.role.can_view_clients.clear() + # self.check_authorized("post", url, data) + # update_task.assert_called_with( + # agent_ids=[agent.agent_id for agent in other_agents] + # ) diff --git a/api/tacticalrmm/agents/tests/test_agent_utils.py b/api/tacticalrmm/agents/tests/test_agent_utils.py new file mode 100644 index 0000000000..a6bdc7cc41 --- /dev/null +++ b/api/tacticalrmm/agents/tests/test_agent_utils.py @@ -0,0 +1,59 @@ +from unittest.mock import patch + +from django.conf import settings + +from agents.utils import generate_linux_install, get_agent_url +from tacticalrmm.test import TacticalTestCase + + +class TestAgentUtils(TacticalTestCase): + def setUp(self) -> None: + self.authenticate() + self.setup_coresettings() + self.setup_base_instance() + + def test_get_agent_url(self): + ver = settings.LATEST_AGENT_VER + + # test without token + r = get_agent_url(goarch="amd64", plat="windows", token="") + expected = f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-windows-amd64.exe" + self.assertEqual(r, expected) + + # test with token + r = get_agent_url(goarch="386", plat="linux", token="token123") + expected = f"https://{settings.AGENTS_URL}version={ver}&arch=386&token=token123&plat=linux&api=api.example.com" + + @patch("agents.utils.get_mesh_device_id") + @patch("agents.utils.asyncio.run") + @patch("agents.utils.get_mesh_ws_url") + @patch("agents.utils.get_core_settings") + def test_generate_linux_install( + self, mock_core, mock_mesh, mock_async_run, mock_mesh_device_id + ): + mock_mesh_device_id.return_value = "meshdeviceid" + mock_core.return_value.mesh_site = "meshsite" + mock_async_run.return_value = "meshid" + mock_mesh.return_value = "meshws" + r = generate_linux_install( + client="1", + site="1", + agent_type="server", + arch="amd64", + token="token123", + api="api.example.com", + download_url="asdasd3423", + ) + + ret = r.getvalue().decode("utf-8") + + self.assertIn(r"agentDL='asdasd3423'", ret) + self.assertIn( + r"meshDL='meshsite/meshagents?id=meshid&installflags=2&meshinstall=6'", ret + ) + self.assertIn(r"apiURL='api.example.com'", ret) + self.assertIn(r"agentDL='asdasd3423'", ret) + self.assertIn(r"token='token123'", ret) + self.assertIn(r"clientID='1'", ret) + self.assertIn(r"siteID='1'", ret) + self.assertIn(r"agentType='server'", ret) diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests/test_agents.py similarity index 64% rename from api/tacticalrmm/agents/tests.py rename to api/tacticalrmm/agents/tests/test_agents.py index 52d3f9743c..0a5a89c644 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests/test_agents.py @@ -1,92 +1,103 @@ import json import os -import pytz -from django.utils import timezone as djangotime -from unittest.mock import patch from itertools import cycle +from typing import TYPE_CHECKING +from unittest.mock import patch +from zoneinfo import ZoneInfo from django.conf import settings -from logs.models import PendingAction +from django.utils import timezone as djangotime from model_bakery import baker -from packaging import version as pyver -from tacticalrmm.test import TacticalTestCase -from winupdate.models import WinUpdatePolicy -from winupdate.serializers import WinUpdatePolicySerializer -from .models import Agent, AgentCustomField, AgentHistory, Note -from .serializers import ( +from agents.models import Agent, AgentCustomField, AgentHistory, Note +from agents.serializers import ( AgentHistorySerializer, - AgentSerializer, AgentHostnameSerializer, AgentNoteSerializer, + AgentSerializer, +) +from tacticalrmm.constants import ( + AGENT_STATUS_OFFLINE, + AGENT_STATUS_ONLINE, + AgentMonType, + CustomFieldModel, + CustomFieldType, + EvtLogNames, ) -from .tasks import auto_self_agent_update_task +from tacticalrmm.test import TacticalTestCase +from winupdate.models import WinUpdatePolicy +from winupdate.serializers import WinUpdatePolicySerializer +if TYPE_CHECKING: + from clients.models import Client, Site base_url = "/agents" class TestAgentsList(TacticalTestCase): - def setUp(self): + def setUp(self) -> None: self.authenticate() self.setup_coresettings() - def test_get_agents(self): + def test_get_agents(self) -> None: url = f"{base_url}/" # 36 total agents - company1 = baker.make("clients.Client") - company2 = baker.make("clients.Client") - site1 = baker.make("clients.Site", client=company1) - site2 = baker.make("clients.Site", client=company1) - site3 = baker.make("clients.Site", client=company2) + company1: "Client" = baker.make("clients.Client") + company2: "Client" = baker.make("clients.Client") + site1: "Site" = baker.make("clients.Site", client=company1) + site2: "Site" = baker.make("clients.Site", client=company1) + site3: "Site" = baker.make("clients.Site", client=company2) baker.make_recipe( - "agents.online_agent", site=site1, monitoring_type="server", _quantity=15 + "agents.online_agent", + site=site1, + monitoring_type=AgentMonType.SERVER, + _quantity=15, ) baker.make_recipe( "agents.online_agent", site=site2, - monitoring_type="workstation", + monitoring_type=AgentMonType.WORKSTATION, _quantity=10, ) baker.make_recipe( "agents.online_agent", site=site3, - monitoring_type="server", + monitoring_type=AgentMonType.SERVER, _quantity=4, ) baker.make_recipe( "agents.online_agent", site=site3, - monitoring_type="workstation", + monitoring_type=AgentMonType.WORKSTATION, _quantity=7, ) # test all agents r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 36) # type: ignore + self.assertEqual(len(r.data), 36) # test client1 r = self.client.get(f"{url}?client={company1.pk}", format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 25) # type: ignore + self.assertEqual(len(r.data), 25) # test site3 r = self.client.get(f"{url}?site={site3.pk}", format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 11) # type: ignore + self.assertEqual(len(r.data), 11) # test with no details r = self.client.get(f"{url}?site={site3.pk}&detail=false", format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 11) # type: ignore + self.assertEqual(len(r.data), 11) # make sure data is returned with the AgentHostnameSerializer agents = Agent.objects.filter(site=site3) serializer = AgentHostnameSerializer(agents, many=True) - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -103,6 +114,17 @@ def setUp(self): ) baker.make_recipe("winupdate.winupdate_policy", agent=self.agent) + @patch("agents.tasks.bulk_recover_agents_task.delay") + def test_bulk_agent_recovery(self, mock_task): + mock_task.return_value = None + url = f"{base_url}/bulkrecovery/" + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + mock_task.assert_called_once() + + self.check_not_authenticated("get", url) + def test_get_agent(self): url = f"{base_url}/{self.agent.agent_id}/" @@ -118,7 +140,7 @@ def test_edit_agent(self): url = f"{base_url}/{self.agent.agent_id}/" data = { - "site": site.id, # type: ignore + "site": site.pk, "monitoring_type": "workstation", "description": "asjdk234andasd", "offline_time": 4, @@ -149,18 +171,22 @@ def test_edit_agent(self): agent = Agent.objects.get(pk=self.agent.pk) data = AgentSerializer(agent).data - self.assertEqual(data["site"], site.id) # type: ignore + self.assertEqual(data["site"], site.pk) policy = WinUpdatePolicy.objects.get(agent=self.agent) data = WinUpdatePolicySerializer(policy).data self.assertEqual(data["run_time_days"], [2, 3, 6]) # test adding custom fields - field = baker.make("core.CustomField", model="agent", type="number") + field = baker.make( + "core.CustomField", + model=CustomFieldModel.AGENT, + type=CustomFieldType.NUMBER, + ) data = { - "site": site.id, # type: ignore + "site": site.pk, "description": "asjdk234andasd", - "custom_fields": [{"field": field.id, "string_value": "123"}], # type: ignore + "custom_fields": [{"field": field.pk, "string_value": "123"}], } r = self.client.put(url, data, format="json") @@ -171,9 +197,9 @@ def test_edit_agent(self): # test edit custom field data = { - "site": site.id, # type: ignore + "site": site.pk, "description": "asjdk234andasd", - "custom_fields": [{"field": field.id, "string_value": "456"}], # type: ignore + "custom_fields": [{"field": field.pk, "string_value": "456"}], } r = self.client.put(url, data, format="json") @@ -184,15 +210,23 @@ def test_edit_agent(self): ) self.check_not_authenticated("put", url) + @patch("asyncio.run") + @patch("asyncio.run") + @patch("core.utils._b64_to_hex") @patch("agents.models.Agent.nats_cmd") @patch("agents.views.reload_nats") - def test_agent_uninstall(self, reload_nats, nats_cmd): + def test_agent_uninstall( + self, reload_nats, nats_cmd, b64_to_hex, asyncio_run1, asyncio_run2 + ): + asyncio_run1.return_value = "ok" + asyncio_run2.return_value = "ok" + b64_to_hex.return_value = "nodeid" url = f"{base_url}/{self.agent.agent_id}/" r = self.client.delete(url, format="json") self.assertEqual(r.status_code, 200) - nats_cmd.assert_called_with({"func": "uninstall"}, wait=False) + nats_cmd.assert_called_with({"func": "uninstall", "code": "foo"}, wait=False) reload_nats.assert_called_once() self.check_not_authenticated("delete", url) @@ -205,7 +239,7 @@ def test_get_patch_policy(self): self.agent.save(update_fields=["policy"]) _ = self.agent.get_patch_policy() - self.agent.monitoring_type = "workstation" + self.agent.monitoring_type = AgentMonType.WORKSTATION self.agent.save(update_fields=["monitoring_type"]) _ = self.agent.get_patch_policy() @@ -217,51 +251,20 @@ def test_get_patch_policy(self): self.coresettings.save(update_fields=["server_policy", "workstation_policy"]) _ = self.agent.get_patch_policy() - self.agent.monitoring_type = "server" + self.agent.monitoring_type = AgentMonType.SERVER self.agent.save(update_fields=["monitoring_type"]) _ = self.agent.get_patch_policy() def test_get_agent_versions(self): url = "/agents/versions/" - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"]) - - self.check_not_authenticated("get", url) - - @patch("agents.tasks.send_agent_update_task.delay") - def test_update_agents(self, mock_task): - url = f"{base_url}/update/" - baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version=settings.LATEST_AGENT_VER, - _quantity=15, - ) - baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version="1.3.0", - _quantity=15, - ) - - agent_ids: list[str] = list( - Agent.objects.only("agent_id", "version").values_list("agent_id", flat=True) - ) - data = {"agent_ids": agent_ids} - expected: list[str] = [ - i.agent_id - for i in Agent.objects.only("agent_id", "version") - if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) - ] + with self.assertNumQueries(1): + r = self.client.get(url) - r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"]) - mock_task.assert_called_with(agent_ids=expected) - - self.check_not_authenticated("post", url) + self.check_not_authenticated("get", url) @patch("time.sleep", return_value=None) @patch("agents.models.Agent.nats_cmd") @@ -271,25 +274,25 @@ def test_agent_ping(self, nats_cmd, mock_sleep): nats_cmd.return_value = "timeout" r = self.client.get(url) self.assertEqual(r.status_code, 200) - ret = {"name": self.agent.hostname, "status": "offline"} + ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE} self.assertEqual(r.json(), ret) nats_cmd.return_value = "natsdown" r = self.client.get(url) self.assertEqual(r.status_code, 200) - ret = {"name": self.agent.hostname, "status": "offline"} + ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE} self.assertEqual(r.json(), ret) nats_cmd.return_value = "pong" r = self.client.get(url) self.assertEqual(r.status_code, 200) - ret = {"name": self.agent.hostname, "status": "online"} + ret = {"name": self.agent.hostname, "status": AGENT_STATUS_ONLINE} self.assertEqual(r.json(), ret) nats_cmd.return_value = "asdasjdaksdasd" r = self.client.get(url) self.assertEqual(r.status_code, 200) - ret = {"name": self.agent.hostname, "status": "offline"} + ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE} self.assertEqual(r.json(), ret) self.check_not_authenticated("get", url) @@ -306,8 +309,8 @@ def test_get_processes(self, mock_ret): r = self.client.get(url) self.assertEqual(r.status_code, 200) - assert any(i["name"] == "Registry" for i in mock_ret.return_value) - assert any(i["membytes"] == 434655234324 for i in mock_ret.return_value) + assert any(i["name"] == "spoolsv.exe" for i in mock_ret.return_value) + assert any(i["membytes"] == 17305600 for i in mock_ret.return_value) mock_ret.return_value = "timeout" r = self.client.get(url) @@ -349,7 +352,7 @@ def test_get_event_log(self, nats_cmd): "func": "eventlog", "timeout": 30, "payload": { - "logname": "Application", + "logname": EvtLogNames.APPLICATION, "days": str(22), }, }, @@ -364,7 +367,7 @@ def test_get_event_log(self, nats_cmd): "func": "eventlog", "timeout": 180, "payload": { - "logname": "Security", + "logname": EvtLogNames.SECURITY, "days": str(6), }, }, @@ -400,11 +403,12 @@ def test_send_raw_cmd(self, mock_ret): "cmd": "ipconfig", "shell": "cmd", "timeout": 30, + "run_as_user": False, } mock_ret.return_value = "nt authority\\system" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) - self.assertIsInstance(r.data, str) # type: ignore + self.assertIsInstance(r.data, str) mock_ret.return_value = "timeout" r = self.client.post(url, data, format="json") @@ -414,30 +418,42 @@ def test_send_raw_cmd(self, mock_ret): @patch("agents.models.Agent.nats_cmd") def test_reboot_later(self, nats_cmd): + nats_cmd.return_value = "ok" url = f"{base_url}/{self.agent.agent_id}/reboot/" - data = { - "datetime": "2025-08-29 18:41", - } + # ensure we don't allow dates in past + data = {"datetime": "2022-07-11T01:51"} + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 400) + self.assertEqual(r.data, "Date cannot be set in the past") - nats_cmd.return_value = "ok" + # test with date in future + data["datetime"] = "2027-08-29T18:41" r = self.client.patch(url, data, format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") # type: ignore - self.assertEqual(r.data["agent"], self.agent.hostname) # type: ignore + self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM") + self.assertEqual(r.data["agent"], self.agent.hostname) nats_data = { "func": "schedtask", "schedtaskpayload": { "type": "schedreboot", - "deleteafter": True, - "trigger": "once", - "name": r.data["task_name"], # type: ignore - "year": 2025, - "month": "August", - "day": 29, - "hour": 18, - "min": 41, + "enabled": True, + "delete_expired_task_after": True, + "start_when_available": False, + "multiple_instances": 2, + "trigger": "runonce", + "name": r.data["task_name"], + "start_year": 2027, + "start_month": 8, + "start_day": 29, + "start_hour": 18, + "start_min": 41, + "expire_year": 2027, + "expire_month": 8, + "expire_day": 29, + "expire_hour": 18, + "expire_min": 46, }, } nats_cmd.assert_called_with(nats_data, timeout=10) @@ -452,123 +468,11 @@ def test_reboot_later(self, nats_cmd): r = self.client.patch(url, data_invalid, format="json") self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "Invalid date") # type: ignore + self.assertEqual(r.data, "Invalid date") self.check_not_authenticated("patch", url) - @patch("os.path.exists") - def test_install_agent(self, mock_file_exists): - url = f"{base_url}/installer/" - - site = baker.make("clients.Site") - data = { - "client": site.client.id, # type: ignore - "site": site.id, # type: ignore - "arch": "64", - "expires": 23, - "installMethod": "manual", - "api": "https://api.example.com", - "agenttype": "server", - "rdp": 1, - "ping": 0, - "power": 0, - "fileName": "rmm-client-site-server.exe", - } - - mock_file_exists.return_value = False - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 400) - - mock_file_exists.return_value = True - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 200) - - data["arch"] = "32" - mock_file_exists.return_value = False - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 400) - - data["arch"] = "64" - mock_file_exists.return_value = True - r = self.client.post(url, data, format="json") - self.assertIn("rdp", r.json()["cmd"]) - self.assertNotIn("power", r.json()["cmd"]) - - data.update({"ping": 1, "power": 1}) - r = self.client.post(url, data, format="json") - self.assertIn("power", r.json()["cmd"]) - self.assertIn("ping", r.json()["cmd"]) - - data["installMethod"] = "powershell" - self.assertEqual(r.status_code, 200) - - self.check_not_authenticated("post", url) - - @patch("agents.models.Agent.nats_cmd") - def test_recover(self, nats_cmd): - from agents.models import RecoveryAction - - RecoveryAction.objects.all().delete() - agent = baker.make_recipe("agents.online_agent") - url = f"{base_url}/{agent.agent_id}/recover/" - - # test mesh realtime - data = {"cmd": None, "mode": "mesh"} - nats_cmd.return_value = "ok" - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(RecoveryAction.objects.count(), 0) - nats_cmd.assert_called_with( - {"func": "recover", "payload": {"mode": "mesh"}}, timeout=10 - ) - nats_cmd.reset_mock() - - # test mesh with agent rpc not working - data = {"cmd": None, "mode": "mesh"} - nats_cmd.return_value = "timeout" - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(RecoveryAction.objects.count(), 1) - mesh_recovery = RecoveryAction.objects.first() - self.assertEqual(mesh_recovery.mode, "mesh") # type: ignore - nats_cmd.reset_mock() - RecoveryAction.objects.all().delete() - - # test tacagent realtime - data = {"cmd": None, "mode": "tacagent"} - nats_cmd.return_value = "ok" - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(RecoveryAction.objects.count(), 0) - nats_cmd.assert_called_with( - {"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10 - ) - nats_cmd.reset_mock() - - # test tacagent with rpc not working - data = {"cmd": None, "mode": "tacagent"} - nats_cmd.return_value = "timeout" - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 400) - self.assertEqual(RecoveryAction.objects.count(), 0) - nats_cmd.reset_mock() - - # test shell cmd without command - data = {"cmd": None, "mode": "command"} - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 400) - self.assertEqual(RecoveryAction.objects.count(), 0) - - # test shell cmd - data = {"cmd": "shutdown /r /t 10 /f", "mode": "command"} - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 200) - self.assertEqual(RecoveryAction.objects.count(), 1) - cmd_recovery = RecoveryAction.objects.first() - self.assertEqual(cmd_recovery.mode, "command") # type: ignore - self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f") # type: ignore - - @patch("agents.models.Agent.get_login_token") + @patch("meshctrl.utils.get_login_token") def test_meshcentral_tabs(self, mock_token): url = f"{base_url}/{self.agent.agent_id}/meshcentral/" mock_token.return_value = "askjh1k238uasdhk487234jadhsajksdhasd" @@ -578,28 +482,24 @@ def test_meshcentral_tabs(self, mock_token): # TODO # decode the cookie - self.assertIn("&viewmode=13", r.data["file"]) # type: ignore - self.assertIn("&viewmode=12", r.data["terminal"]) # type: ignore - self.assertIn("&viewmode=11", r.data["control"]) # type: ignore + self.assertIn("&viewmode=13", r.data["file"]) + self.assertIn("&viewmode=12", r.data["terminal"]) + self.assertIn("&viewmode=11", r.data["control"]) - self.assertIn("&gotonode=", r.data["file"]) # type: ignore - self.assertIn("&gotonode=", r.data["terminal"]) # type: ignore - self.assertIn("&gotonode=", r.data["control"]) # type: ignore + self.assertIn("&gotonode=", r.data["file"]) + self.assertIn("&gotonode=", r.data["terminal"]) + self.assertIn("&gotonode=", r.data["control"]) - self.assertIn("?login=", r.data["file"]) # type: ignore - self.assertIn("?login=", r.data["terminal"]) # type: ignore - self.assertIn("?login=", r.data["control"]) # type: ignore + self.assertIn("?login=", r.data["file"]) + self.assertIn("?login=", r.data["terminal"]) + self.assertIn("?login=", r.data["control"]) - self.assertEqual(self.agent.hostname, r.data["hostname"]) # type: ignore - self.assertEqual(self.agent.client.name, r.data["client"]) # type: ignore - self.assertEqual(self.agent.site.name, r.data["site"]) # type: ignore + self.assertEqual(self.agent.hostname, r.data["hostname"]) + self.assertEqual(self.agent.client.name, r.data["client"]) + self.assertEqual(self.agent.site.name, r.data["site"]) self.assertEqual(r.status_code, 200) - mock_token.return_value = "err" - r = self.client.get(url) - self.assertEqual(r.status_code, 400) - self.check_not_authenticated("get", url) @patch("agents.models.Agent.nats_cmd") @@ -608,7 +508,7 @@ def test_recover_mesh(self, nats_cmd): nats_cmd.return_value = "ok" r = self.client.post(url) self.assertEqual(r.status_code, 200) - self.assertIn(self.agent.hostname, r.data) # type: ignore + self.assertIn(self.agent.hostname, r.data) nats_cmd.assert_called_with( {"func": "recover", "payload": {"mode": "mesh"}}, timeout=90 ) @@ -626,7 +526,7 @@ def test_recover_mesh(self, nats_cmd): @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 .models import AgentCustomField, Note + from agents.models import AgentCustomField, AgentHistory, Note from clients.models import ClientCustomField, SiteCustomField run_script.return_value = "ok" @@ -639,12 +539,24 @@ def test_run_script(self, run_script, email_task): "output": "wait", "args": [], "timeout": 15, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() + if not hist: + raise AgentHistory.DoesNotExist + run_script.assert_called_with( - scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=0 + scriptpk=script.pk, + args=[], + timeout=18, + wait=True, + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -656,15 +568,21 @@ def test_run_script(self, run_script, email_task): "timeout": 15, "emailMode": "default", "emails": ["admin@example.com", "bob@example.com"], + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() email_task.assert_called_with( agentpk=self.agent.pk, scriptpk=script.pk, nats_timeout=18, emails=[], args=["abc", "123"], + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) email_task.reset_mock() @@ -672,12 +590,16 @@ def test_run_script(self, run_script, email_task): data["emailMode"] = "custom" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() email_task.assert_called_with( agentpk=self.agent.pk, scriptpk=script.pk, nats_timeout=18, emails=["admin@example.com", "bob@example.com"], args=["abc", "123"], + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) # test fire and forget @@ -686,36 +608,55 @@ def test_run_script(self, run_script, email_task): "output": "forget", "args": ["hello", "world"], "timeout": 22, + "run_as_user": True, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() + if not hist: + raise AgentHistory.DoesNotExist + run_script.assert_called_with( - scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=0 + scriptpk=script.pk, + args=["hello", "world"], + timeout=25, + history_pk=hist.pk, + run_as_user=True, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() # test collector # save to agent custom field - custom_field = baker.make("core.CustomField", model="agent") + custom_field = baker.make("core.CustomField", model=CustomFieldModel.AGENT) data = { "script": script.pk, "output": "collector", "args": ["hello", "world"], "timeout": 22, - "custom_field": custom_field.id, # type: ignore + "custom_field": custom_field.pk, "save_all_output": True, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() + if not hist: + raise AgentHistory.DoesNotExist + run_script.assert_called_with( scriptpk=script.pk, args=["hello", "world"], timeout=25, wait=True, - history_pk=0, + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -731,18 +672,26 @@ def test_run_script(self, run_script, email_task): "output": "collector", "args": ["hello", "world"], "timeout": 22, - "custom_field": custom_field.id, # type: ignore + "custom_field": custom_field.pk, "save_all_output": False, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() + if not hist: + raise AgentHistory.DoesNotExist + run_script.assert_called_with( scriptpk=script.pk, args=["hello", "world"], timeout=25, wait=True, - history_pk=0, + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -754,24 +703,32 @@ def test_run_script(self, run_script, email_task): ) # save to client custom field - custom_field = baker.make("core.CustomField", model="client") + custom_field = baker.make("core.CustomField", model=CustomFieldModel.CLIENT) data = { "script": script.pk, "output": "collector", "args": ["hello", "world"], "timeout": 22, - "custom_field": custom_field.id, # type: ignore + "custom_field": custom_field.pk, "save_all_output": False, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() + if not hist: + raise AgentHistory.DoesNotExist + run_script.assert_called_with( scriptpk=script.pk, args=["hello", "world"], timeout=25, wait=True, - history_pk=0, + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -788,16 +745,24 @@ def test_run_script(self, run_script, email_task): "output": "note", "args": ["hello", "world"], "timeout": 22, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) + hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() + if not hist: + raise AgentHistory.DoesNotExist + run_script.assert_called_with( scriptpk=script.pk, args=["hello", "world"], timeout=25, wait=True, - history_pk=0, + history_pk=hist.pk, + run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -813,8 +778,8 @@ def test_get_notes(self): r = self.client.get(url) serializer = AgentNoteSerializer(notes, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 4) # type: ignore - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(len(r.data), 4) + self.assertEqual(r.data, serializer.data) # test with agent_id url = f"{base_url}/{agent.agent_id}/notes/" @@ -822,8 +787,8 @@ def test_get_notes(self): r = self.client.get(url) serializer = AgentNoteSerializer(notes, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 4) # type: ignore - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(len(r.data), 4) + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -834,7 +799,7 @@ def test_add_note(self): data = {"note": "This is a note", "agent_id": agent.agent_id} r = self.client.post(url, data) self.assertEqual(r.status_code, 200) - self.assertTrue(Note.objects.filter(agent=agent).exists()) # type: ignore + self.assertTrue(Note.objects.filter(agent=agent).exists()) self.check_not_authenticated("post", url) @@ -842,7 +807,7 @@ def test_get_note(self): # setup agent = baker.make_recipe("agents.agent") note = baker.make("agents.Note", agent=agent) - url = f"{base_url}/notes/{note.id}/" + url = f"{base_url}/notes/{note.pk}/" # test not found r = self.client.get(f"{base_url}/notes/500/") @@ -857,7 +822,7 @@ def test_update_note(self): # setup agent = baker.make_recipe("agents.agent") note = baker.make("agents.Note", agent=agent) - url = f"{base_url}/notes/{note.id}/" + url = f"{base_url}/notes/{note.pk}/" # test not found r = self.client.put(f"{base_url}/notes/500/") @@ -867,7 +832,7 @@ def test_update_note(self): r = self.client.put(url, data) self.assertEqual(r.status_code, 200) - new_note = Note.objects.get(pk=note.id) # type: ignore + new_note = Note.objects.get(pk=note.pk) self.assertEqual(new_note.note, data["note"]) self.check_not_authenticated("put", url) @@ -876,7 +841,7 @@ def test_delete_note(self): # setup agent = baker.make_recipe("agents.agent") note = baker.make("agents.Note", agent=agent) - url = f"{base_url}/notes/{note.id}/" + url = f"{base_url}/notes/{note.pk}/" # test not found r = self.client.delete(f"{base_url}/notes/500/") @@ -885,12 +850,11 @@ def test_delete_note(self): r = self.client.delete(url) self.assertEqual(r.status_code, 200) - self.assertFalse(Note.objects.filter(pk=note.id).exists()) # type: ignore + self.assertFalse(Note.objects.filter(pk=note.pk).exists()) self.check_not_authenticated("delete", url) def test_get_agent_history(self): - # setup data agent = baker.make_recipe("agents.agent") history = baker.make("agents.AgentHistory", agent=agent, _quantity=30) @@ -902,7 +866,7 @@ def test_get_agent_history(self): # test pulling data r = self.client.get(url, format="json") - ctx = {"default_tz": pytz.timezone("America/Los_Angeles")} + ctx = {"default_tz": ZoneInfo("America/Los_Angeles")} data = AgentHistorySerializer(history, many=True, context=ctx).data self.assertEqual(r.status_code, 200) self.assertEqual(r.data, data) # type:ignore @@ -920,14 +884,14 @@ def test_agent_maintenance_mode(self): agent = baker.make_recipe("agents.agent") # Test client toggle maintenance mode - data = {"type": "Client", "id": agent.site.client.id, "action": True} # type: ignore + data = {"type": "Client", "id": agent.site.client.id, "action": True} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode) # Test site toggle maintenance mode - data = {"type": "Site", "id": agent.site.id, "action": False} # type: ignore + data = {"type": "Site", "id": agent.site.id, "action": False} r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -944,13 +908,13 @@ def test_agent_maintenance_mode(self): class TestAgentPermissions(TacticalTestCase): def setUp(self): - self.client_setup() + self.setup_client() self.setup_coresettings() def test_list_agents_permissions(self): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) url = f"{base_url}/" @@ -968,29 +932,35 @@ def test_list_agents_permissions(self): # all agents should be returned response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 10) # type: ignore + self.assertEqual(len(response.data), 10) # limit user to specific client. only 1 agent should be returned user.role.can_view_clients.set([agents[4].client]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 2) # type: ignore + self.assertEqual(len(response.data), 2) # limit agent to specific site. 2 should be returned now user.role.can_view_sites.set([agents[6].site]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 4) # type: ignore + self.assertEqual(len(response.data), 4) # make sure superusers work self.check_authorized_superuser("get", url) + @patch("asyncio.run") + @patch("core.utils._b64_to_hex") @patch("agents.models.Agent.nats_cmd") @patch("agents.views.reload_nats") - def test_get_edit_uninstall_permissions(self, reload_nats, nats_cmd): + def test_get_edit_uninstall_permissions( + self, reload_nats, nats_cmd, b64_to_hex, asyncio_run + ): + b64_to_hex.return_value = "nodeid" # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) agent = baker.make_recipe("agents.agent") + baker.make_recipe("winupdate.winupdate_policy", agent=agent) methods = ["get", "put", "delete"] url = f"{base_url}/{agent.agent_id}/" @@ -1040,7 +1010,6 @@ def test_get_edit_uninstall_permissions(self, reload_nats, nats_cmd): @patch("time.sleep") @patch("agents.models.Agent.nats_cmd", return_value="ok") def test_agent_actions_permissions(self, nats_cmd, sleep): - agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") @@ -1070,7 +1039,7 @@ def test_agent_actions_permissions(self, nats_cmd, sleep): self.check_authorized_superuser(test["method"], url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized(test["method"], url) @@ -1097,9 +1066,9 @@ def test_agent_maintenance_permissions(self): site = baker.make("clients.Site") client = baker.make("clients.Client") - site_data = {"id": site.id, "type": "Site", "action": True} + site_data = {"id": site.pk, "type": "Site", "action": True} - client_data = {"id": client.id, "type": "Client", "action": True} + client_data = {"id": client.pk, "type": "Client", "action": True} url = f"{base_url}/maintenance/bulk/" @@ -1108,7 +1077,7 @@ def test_agent_maintenance_permissions(self): self.check_authorized_superuser("post", url, client_data) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("post", url, site_data) @@ -1131,55 +1100,6 @@ def test_agent_maintenance_permissions(self): self.check_authorized("post", url, site_data) self.check_authorized("post", url, client_data) - @patch("agents.tasks.send_agent_update_task.delay") - def test_agent_update_permissions(self, update_task): - agents = baker.make_recipe("agents.agent", _quantity=5) - other_agents = baker.make_recipe("agents.agent", _quantity=7) - - url = f"{base_url}/update/" - - data = { - "agent_ids": [agent.agent_id for agent in agents] - + [agent.agent_id for agent in other_agents] - } - - # test superuser access - self.check_authorized_superuser("post", url, data) - update_task.assert_called_with(agent_ids=data["agent_ids"]) - update_task.reset_mock() - - user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore - - self.check_not_authorized("post", url, data) - update_task.assert_not_called() - - user.role.can_update_agents = True - user.role.save() - - self.check_authorized("post", url, data) - update_task.assert_called_with(agent_ids=data["agent_ids"]) - update_task.reset_mock() - - # limit to client - user.role.can_view_clients.set([agents[0].client]) - self.check_authorized("post", url, data) - update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents]) - update_task.reset_mock() - - # add site - user.role.can_view_sites.set([other_agents[0].site]) - self.check_authorized("post", url, data) - update_task.assert_called_with(agent_ids=data["agent_ids"]) - update_task.reset_mock() - - # remove client permissions - user.role.can_view_clients.clear() - self.check_authorized("post", url, data) - update_task.assert_called_with( - agent_ids=[agent.agent_id for agent in other_agents] - ) - def test_get_agent_version_permissions(self): agents = baker.make_recipe("agents.agent", _quantity=5) other_agents = baker.make_recipe("agents.agent", _quantity=7) @@ -1188,10 +1108,10 @@ def test_get_agent_version_permissions(self): # test superuser access response = self.check_authorized_superuser("get", url) - self.assertEqual(len(response.data["agents"]), 12) # type: ignore + self.assertEqual(len(response.data["agents"]), 12) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) self.check_not_authorized("get", url) @@ -1199,25 +1119,24 @@ def test_get_agent_version_permissions(self): user.role.save() response = self.check_authorized("get", url) - self.assertEqual(len(response.data["agents"]), 12) # type: ignore + self.assertEqual(len(response.data["agents"]), 12) # limit to client user.role.can_view_clients.set([agents[0].client]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data["agents"]), 5) # type: ignore + self.assertEqual(len(response.data["agents"]), 5) # add site user.role.can_view_sites.set([other_agents[0].site]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data["agents"]), 12) # type: ignore + self.assertEqual(len(response.data["agents"]), 12) # remove client permissions user.role.can_view_clients.clear() response = self.check_authorized("get", url) - self.assertEqual(len(response.data["agents"]), 7) # type: ignore + self.assertEqual(len(response.data["agents"]), 7) def test_generating_agent_installer_permissions(self): - client = baker.make("clients.Client") client_site = baker.make("clients.Site", client=client) site = baker.make("clients.Site") @@ -1228,7 +1147,7 @@ def test_generating_agent_installer_permissions(self): self.check_authorized_superuser("post", url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) self.check_not_authorized("post", url) @@ -1241,8 +1160,8 @@ def test_generating_agent_installer_permissions(self): user.role.can_view_clients.set([client]) data = { - "client": client.id, - "site": client_site.id, + "client": client.pk, + "site": client_site.pk, "version": settings.LATEST_AGENT_VER, "arch": "64", } @@ -1250,8 +1169,8 @@ def test_generating_agent_installer_permissions(self): self.check_authorized("post", url, data) data = { - "client": site.client.id, - "site": site.id, + "client": site.client.pk, + "site": site.pk, "version": settings.LATEST_AGENT_VER, "arch": "64", } @@ -1262,8 +1181,8 @@ def test_generating_agent_installer_permissions(self): user.role.can_view_clients.clear() user.role.can_view_sites.set([site]) data = { - "client": site.client.id, - "site": site.id, + "client": site.client.pk, + "site": site.pk, "version": settings.LATEST_AGENT_VER, "arch": "64", } @@ -1271,8 +1190,8 @@ def test_generating_agent_installer_permissions(self): self.check_authorized("post", url, data) data = { - "client": client.id, - "site": client_site.id, + "client": client.pk, + "site": client_site.pk, "version": settings.LATEST_AGENT_VER, "arch": "64", } @@ -1280,7 +1199,6 @@ def test_generating_agent_installer_permissions(self): self.check_not_authorized("post", url, data) def test_agent_notes_permissions(self): - agent = baker.make_recipe("agents.agent") notes = baker.make("agents.Note", agent=agent, _quantity=5) @@ -1293,17 +1211,17 @@ def test_agent_notes_permissions(self): {"url": f"{base_url}/notes/", "method": "get", "role": "can_list_notes"}, {"url": f"{base_url}/notes/", "method": "post", "role": "can_manage_notes"}, { - "url": f"{base_url}/notes/{notes[0].id}/", + "url": f"{base_url}/notes/{notes[0].pk}/", "method": "get", "role": "can_list_notes", }, { - "url": f"{base_url}/notes/{notes[0].id}/", + "url": f"{base_url}/notes/{notes[0].pk}/", "method": "put", "role": "can_manage_notes", }, { - "url": f"{base_url}/notes/{notes[0].id}/", + "url": f"{base_url}/notes/{notes[0].pk}/", "method": "delete", "role": "can_manage_notes", }, @@ -1314,7 +1232,7 @@ def test_agent_notes_permissions(self): self.check_authorized_superuser(test["method"], test["url"]) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) self.check_not_authorized(test["method"], test["url"]) setattr(user.role, test["role"], True) @@ -1325,7 +1243,7 @@ def test_agent_notes_permissions(self): user = self.create_user_with_roles(["can_list_notes", "can_manage_notes"]) user.role.can_view_sites.set([agent.site]) user.role.save() - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) authorized_data = {"note": "Test not here", "agent_id": agent.agent_id} @@ -1336,7 +1254,7 @@ def test_agent_notes_permissions(self): # should only return the 4 allowed agent notes (one got deleted above in loop) r = self.client.get(f"{base_url}/notes/") - self.assertEqual(len(r.data), 4) # type: ignore + self.assertEqual(len(r.data), 4) # test with agent_id in url self.check_authorized("get", f"{base_url}/{agent.agent_id}/notes/") @@ -1347,31 +1265,31 @@ def test_agent_notes_permissions(self): # test post get, put, and delete and make sure unauthorized is returned with unauthorized agent and works for authorized self.check_authorized("post", f"{base_url}/notes/", authorized_data) self.check_not_authorized("post", f"{base_url}/notes/", unauthorized_data) - self.check_authorized("get", f"{base_url}/notes/{notes[2].id}/") + self.check_authorized("get", f"{base_url}/notes/{notes[2].pk}/") self.check_not_authorized( - "get", f"{base_url}/notes/{unauthorized_notes[2].id}/" + "get", f"{base_url}/notes/{unauthorized_notes[2].pk}/" ) self.check_authorized( - "put", f"{base_url}/notes/{notes[3].id}/", authorized_data + "put", f"{base_url}/notes/{notes[3].pk}/", authorized_data ) self.check_not_authorized( - "put", f"{base_url}/notes/{unauthorized_notes[3].id}/", unauthorized_data + "put", f"{base_url}/notes/{unauthorized_notes[3].pk}/", unauthorized_data ) - self.check_authorized("delete", f"{base_url}/notes/{notes[3].id}/") + self.check_authorized("delete", f"{base_url}/notes/{notes[3].pk}/") self.check_not_authorized( - "delete", f"{base_url}/notes/{unauthorized_notes[3].id}/" + "delete", f"{base_url}/notes/{unauthorized_notes[3].pk}/" ) def test_get_agent_history_permissions(self): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) sites = baker.make("clients.Site", _quantity=2) agent = baker.make_recipe("agents.agent", site=sites[0]) - history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) + history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) # noqa unauthorized_agent = baker.make_recipe("agents.agent", site=sites[1]) - unauthorized_history = baker.make( + unauthorized_history = baker.make( # noqa "agents.AgentHistory", agent=unauthorized_agent, _quantity=6 ) @@ -1394,14 +1312,14 @@ def test_get_agent_history_permissions(self): r = self.check_authorized("get", url) self.check_authorized("get", authorized_url) self.check_authorized("get", unauthorized_url) - self.assertEqual(len(r.data), 11) # type: ignore + self.assertEqual(len(r.data), 11) # limit user to specific client. user.role.can_view_clients.set([agent.client]) self.check_authorized("get", authorized_url) self.check_not_authorized("get", unauthorized_url) r = self.check_authorized("get", url) - self.assertEqual(len(r.data), 5) # type: ignore + self.assertEqual(len(r.data), 5) # make sure superusers work self.check_authorized_superuser("get", url) @@ -1414,144 +1332,8 @@ def setUp(self): self.authenticate() self.setup_coresettings() - @patch("agents.utils.get_winagent_url") - @patch("agents.models.Agent.nats_cmd") - def test_agent_update(self, nats_cmd, get_url): - get_url.return_value = "https://exe.tacticalrmm.io" - - from agents.tasks import agent_update - - agent_noarch = baker.make_recipe( - "agents.agent", - operating_system="Error getting OS", - version=settings.LATEST_AGENT_VER, - ) - r = agent_update(agent_noarch.agent_id) - self.assertEqual(r, "noarch") - - agent_130 = baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version="1.3.0", - ) - r = agent_update(agent_130.agent_id) - self.assertEqual(r, "not supported") - - # test __without__ code signing - agent64_nosign = baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version="1.4.14", - ) - - r = agent_update(agent64_nosign.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.details["url"], - f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe", - ) - self.assertEqual( - action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe" - ) - self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER) - nats_cmd.assert_called_with( - { - "func": "agentupdate", - "payload": { - "url": f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe", - "version": settings.LATEST_AGENT_VER, - "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe", - }, - }, - wait=False, - ) - - # test __with__ code signing (64 bit) - """ codesign = baker.make("core.CodeSignToken", token="testtoken123") - agent64_sign = baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version="1.4.14", - ) - - nats_cmd.return_value = "ok" - get_exe.return_value = "https://exe.tacticalrmm.io" - r = agent_update(agent64_sign.pk, codesign.token) # type: ignore - self.assertEqual(r, "created") - nats_cmd.assert_called_with( - { - "func": "agentupdate", - "payload": { - "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123", # type: ignore - "version": settings.LATEST_AGENT_VER, - "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe", - }, - }, - wait=False, - ) - action = PendingAction.objects.get(agent__pk=agent64_sign.pk) - self.assertEqual(action.action_type, "agentupdate") - self.assertEqual(action.status, "pending") - - # test __with__ code signing (32 bit) - agent32_sign = baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 32 bit (build 19041.450)", - version="1.4.14", - ) - - nats_cmd.return_value = "ok" - get_exe.return_value = "https://exe.tacticalrmm.io" - r = agent_update(agent32_sign.pk, codesign.token) # type: ignore - self.assertEqual(r, "created") - nats_cmd.assert_called_with( - { - "func": "agentupdate", - "payload": { - "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123", # type: ignore - "version": settings.LATEST_AGENT_VER, - "inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe", - }, - }, - wait=False, - ) - action = PendingAction.objects.get(agent__pk=agent32_sign.pk) - self.assertEqual(action.action_type, "agentupdate") - self.assertEqual(action.status, "pending") """ - - @patch("agents.tasks.agent_update") - @patch("agents.tasks.sleep", return_value=None) - def test_auto_self_agent_update_task(self, mock_sleep, agent_update): - baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version=settings.LATEST_AGENT_VER, - _quantity=23, - ) - baker.make_recipe( - "agents.agent", - operating_system="Windows 10 Pro, 64 bit (build 19041.450)", - version="1.3.0", - _quantity=33, - ) - - self.coresettings.agent_auto_update = False - self.coresettings.save(update_fields=["agent_auto_update"]) - - r = auto_self_agent_update_task.s().apply() - self.assertEqual(agent_update.call_count, 0) - - self.coresettings.agent_auto_update = True - self.coresettings.save(update_fields=["agent_auto_update"]) - - r = auto_self_agent_update_task.s().apply() - 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") @@ -1562,7 +1344,7 @@ def test_agent_history_prune_task(self): ) days = 0 - for item in history: # type: ignore + for item in history: item.time = djangotime.now() - djangotime.timedelta(days=days) item.save() days = days + 5 diff --git a/api/tacticalrmm/agents/tests/test_mgmt_commands.py b/api/tacticalrmm/agents/tests/test_mgmt_commands.py new file mode 100644 index 0000000000..68fcc376aa --- /dev/null +++ b/api/tacticalrmm/agents/tests/test_mgmt_commands.py @@ -0,0 +1,46 @@ +from unittest.mock import call, patch + +from django.core.management import call_command +from model_bakery import baker + +from tacticalrmm.constants import AgentMonType, AgentPlat +from tacticalrmm.test import TacticalTestCase + + +class TestBulkRestartAgents(TacticalTestCase): + def setUp(self) -> None: + self.authenticate() + self.setup_coresettings() + self.setup_base_instance() + + @patch("core.management.commands.bulk_restart_agents.sleep") + @patch("agents.models.Agent.recover") + @patch("core.management.commands.bulk_restart_agents.get_mesh_ws_url") + def test_bulk_restart_agents_mgmt_cmd( + self, get_mesh_ws_url, recover, mock_sleep + ) -> None: + get_mesh_ws_url.return_value = "https://mesh.example.com/test" + + baker.make_recipe( + "agents.online_agent", + site=self.site1, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.WINDOWS, + ) + + baker.make_recipe( + "agents.online_agent", + site=self.site3, + monitoring_type=AgentMonType.SERVER, + plat=AgentPlat.LINUX, + ) + + calls = [ + call("tacagent", "https://mesh.example.com/test", wait=False), + call("mesh", "", wait=False), + ] + + call_command("bulk_restart_agents") + + recover.assert_has_calls(calls) + mock_sleep.assert_called_with(10) diff --git a/api/tacticalrmm/agents/tests/test_recovery.py b/api/tacticalrmm/agents/tests/test_recovery.py new file mode 100644 index 0000000000..360da7df43 --- /dev/null +++ b/api/tacticalrmm/agents/tests/test_recovery.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING +from unittest.mock import patch + +from model_bakery import baker + +from tacticalrmm.constants import AgentMonType, AgentPlat +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=AgentMonType.SERVER, + plat=AgentPlat.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) diff --git a/api/tacticalrmm/agents/urls.py b/api/tacticalrmm/agents/urls.py index 321cf253c6..1127be8418 100644 --- a/api/tacticalrmm/agents/urls.py +++ b/api/tacticalrmm/agents/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from . import views -from checks.views import GetAddChecks from autotasks.views import GetAddAutoTasks +from checks.views import GetAddChecks from logs.views import PendingActions +from . import views + urlpatterns = [ # agent views path("", views.GetAgents.as_view()), @@ -40,5 +41,7 @@ path("versions/", views.get_agent_versions), path("update/", views.update_agents), path("installer/", views.install_agent), - path("/getmeshexe/", views.get_mesh_exe), + path("bulkrecovery/", views.bulk_agent_recovery), + path("scripthistory/", views.ScriptRunHistory.as_view()), + path("/wol/", views.wol), ] diff --git a/api/tacticalrmm/agents/utils.py b/api/tacticalrmm/agents/utils.py index 9d745e94ba..c48c1b0753 100644 --- a/api/tacticalrmm/agents/utils.py +++ b/api/tacticalrmm/agents/utils.py @@ -1,40 +1,76 @@ -import random +import asyncio import urllib.parse -import requests +from io import StringIO +from pathlib import Path from django.conf import settings -from core.models import CodeSignToken +from django.http import FileResponse +from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url +from tacticalrmm.constants import MeshAgentIdent -def get_exegen_url() -> str: - urls: list[str] = settings.EXE_GEN_URLS - for url in urls: - try: - r = requests.get(url, timeout=10) - except: - continue - if r.status_code == 200: - return url +def get_agent_url(*, goarch: str, plat: str, token: str = "") -> str: + ver = settings.LATEST_AGENT_VER + if token: + params = { + "version": ver, + "arch": goarch, + "token": token, + "plat": plat, + "api": settings.ALLOWED_HOSTS[0], + } + return settings.AGENTS_URL + urllib.parse.urlencode(params) - return random.choice(urls) + return f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-{plat}-{goarch}.exe" -def get_winagent_url(arch: str) -> str: +def generate_linux_install( + client: str, + site: str, + agent_type: str, + arch: str, + token: str, + api: str, + download_url: str, +) -> FileResponse: + match arch: + case "amd64": + arch_id = MeshAgentIdent.LINUX64 + case "386": + arch_id = MeshAgentIdent.LINUX32 + case "arm64": + arch_id = MeshAgentIdent.LINUX_ARM_64 + case "arm": + arch_id = MeshAgentIdent.LINUX_ARM_HF + case _: + arch_id = "not_found" - dl_url = settings.DL_32 if arch == "32" else settings.DL_64 + core = get_core_settings() - try: - t: CodeSignToken = CodeSignToken.objects.first() # type: ignore - if t.is_valid: - base_url = get_exegen_url() + "/api/v1/winagents/?" - params = { - "version": settings.LATEST_AGENT_VER, - "arch": arch, - "token": t.token, - } - dl_url = base_url + urllib.parse.urlencode(params) - except: - pass + uri = get_mesh_ws_url() + mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) + mesh_dl = ( + f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=2&meshinstall={arch_id}" + ) - return dl_url + text = Path(settings.LINUX_AGENT_SCRIPT).read_text() + + replace = { + "agentDLChange": download_url, + "meshDLChange": mesh_dl, + "clientIDChange": client, + "siteIDChange": site, + "agentTypeChange": agent_type, + "tokenChange": token, + "apiURLChange": api, + } + + for i, j in replace.items(): + text = text.replace(i, j) + + text += "\n" + with StringIO(text) as fp: + return FileResponse( + fp.read(), as_attachment=True, filename="linux_agent_install.sh" + ) diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 0a1dccc2ec..3153ae052b 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -1,77 +1,113 @@ import asyncio import datetime as dt -import os import random import string import time +from io import StringIO +from pathlib import Path from django.conf import settings +from django.db.models import Exists, OuterRef, Prefetch, Q from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from django.db.models import Q +from django.utils import timezone as djangotime +from django.utils.dateparse import parse_datetime +from meshctrl.utils import get_login_token from packaging import version as pyver +from rest_framework import serializers 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 rest_framework.exceptions import PermissionDenied -from core.models import CoreSettings +from core.utils import ( + get_core_settings, + get_mesh_ws_url, + remove_mesh_agent, + token_is_valid, + wake_on_lan, +) from logs.models import AuditLog, DebugLog, PendingAction from scripts.models import Script -from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task -from tacticalrmm.utils import ( - get_default_timezone, - notify_error, - reload_nats, +from scripts.tasks import bulk_command_task, bulk_script_task +from tacticalrmm.constants import ( AGENT_DEFER, + AGENT_STATUS_OFFLINE, + AGENT_STATUS_ONLINE, + AGENT_TABLE_DEFER, + AgentHistoryType, + AgentMonType, + AgentPlat, + CustomFieldModel, + DebugLogType, + EvtLogNames, + PAAction, ) -from winupdate.serializers import WinUpdatePolicySerializer -from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task +from tacticalrmm.helpers import date_is_in_past, 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, reload_nats +from winupdate.models import WinUpdate, WinUpdatePolicy +from winupdate.serializers import WinUpdatePolicySerializer +from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task -from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory +from .models import Agent, AgentCustomField, AgentHistory, Note from .permissions import ( AgentHistoryPerms, + AgentNotesPerms, AgentPerms, + AgentWOLPerms, EvtLogPerms, InstallAgentPerms, - RecoverAgentPerms, - AgentNotesPerms, ManageProcPerms, MeshPerms, + PingAgentPerms, RebootAgentPerms, + RecoverAgentPerms, RunBulkPerms, RunScriptPerms, SendCMDPerms, - PingAgentPerms, UpdateAgentPerms, ) from .serializers import ( AgentCustomFieldSerializer, AgentHistorySerializer, AgentHostnameSerializer, + AgentNoteSerializer, AgentSerializer, AgentTableSerializer, - AgentNoteSerializer, ) -from .tasks import run_script_email_results_task, send_agent_update_task +from .tasks import ( + bulk_recover_agents_task, + run_script_email_results_task, + send_agent_update_task, +) class GetAgents(APIView): permission_classes = [IsAuthenticated, AgentPerms] def get(self, request): + from checks.models import Check, CheckResult + + monitoring_type_filter = Q() + client_site_filter = Q() + + monitoring_type = request.query_params.get("monitoring_type", None) + if monitoring_type: + if monitoring_type in AgentMonType.values: + monitoring_type_filter = Q(monitoring_type=monitoring_type) + else: + return notify_error("monitoring type does not exist") + if "site" in request.query_params.keys(): - filter = Q(site_id=request.query_params["site"]) + client_site_filter = Q(site_id=request.query_params["site"]) elif "client" in request.query_params.keys(): - filter = Q(site__client_id=request.query_params["client"]) - else: - filter = Q() + client_site_filter = Q(site__client_id=request.query_params["client"]) # by default detail=true if ( @@ -79,24 +115,51 @@ def get(self, request): or "detail" in request.query_params.keys() and request.query_params["detail"] == "true" ): - agents = ( Agent.objects.filter_by_role(request.user) # type: ignore - .select_related("site", "policy", "alert_template") - .prefetch_related("agentchecks") - .filter(filter) - .defer(*AGENT_DEFER) + .filter(monitoring_type_filter) + .filter(client_site_filter) + .defer(*AGENT_TABLE_DEFER) + .select_related( + "site__server_policy", + "site__workstation_policy", + "site__client__server_policy", + "site__client__workstation_policy", + "policy", + "alert_template", + ) + .prefetch_related( + Prefetch( + "agentchecks", + queryset=Check.objects.select_related("script"), + ), + Prefetch( + "checkresults", + queryset=CheckResult.objects.select_related("assigned_check"), + ), + Prefetch( + "custom_fields", + queryset=AgentCustomField.objects.select_related("field"), + ), + ) + .annotate( + has_patches_pending=Exists( + WinUpdate.objects.filter( + agent_id=OuterRef("pk"), action="approve", installed=False + ) + ), + ) ) - ctx = {"default_tz": get_default_timezone()} - serializer = AgentTableSerializer(agents, many=True, context=ctx) + serializer = AgentTableSerializer(agents, many=True) # if detail=false else: agents = ( Agent.objects.filter_by_role(request.user) # type: ignore - .select_related("site") - .filter(filter) - .only("agent_id", "hostname", "site") + .defer(*AGENT_DEFER) + .select_related("site__client") + .filter(monitoring_type_filter) + .filter(client_site_filter) ) serializer = AgentHostnameSerializer(agents, many=True) @@ -106,18 +169,66 @@ def get(self, request): class GetUpdateDeleteAgent(APIView): permission_classes = [IsAuthenticated, AgentPerms] + class InputSerializer(serializers.ModelSerializer): + class Meta: + model = Agent + fields = [ + "maintenance_mode", # TODO separate this + "policy", # TODO separate this + "block_policy_inheritance", # TODO separate this + "monitoring_type", + "description", + "overdue_email_alert", + "overdue_text_alert", + "overdue_dashboard_alert", + "offline_time", + "overdue_time", + "check_interval", + "time_zone", + "site", + ] + # get agent details def get(self, request, agent_id): - agent = get_object_or_404(Agent, agent_id=agent_id) + from checks.models import Check, CheckResult + + agent = get_object_or_404( + Agent.objects.select_related( + "site__server_policy", + "site__workstation_policy", + "site__client__server_policy", + "site__client__workstation_policy", + "policy", + "alert_template", + ).prefetch_related( + Prefetch( + "agentchecks", + queryset=Check.objects.select_related("script"), + ), + Prefetch( + "checkresults", + queryset=CheckResult.objects.select_related("assigned_check"), + ), + Prefetch( + "custom_fields", + queryset=AgentCustomField.objects.select_related("field"), + ), + Prefetch( + "winupdatepolicy", + queryset=WinUpdatePolicy.objects.select_related("agent", "policy"), + ), + ), + agent_id=agent_id, + ) return Response(AgentSerializer(agent).data) # edit agent def put(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) - a_serializer.is_valid(raise_exception=True) - a_serializer.save() + s = self.InputSerializer(instance=agent, data=request.data, partial=True) + s.is_valid(raise_exception=True) + s.save() if "winupdatepolicy" in request.data.keys(): policy = agent.winupdatepolicy.get() # type: ignore @@ -128,17 +239,15 @@ def put(self, request, agent_id): p_serializer.save() if "custom_fields" in request.data.keys(): - for field in request.data["custom_fields"]: - custom_field = field - custom_field["agent"] = agent.id # type: ignore + custom_field["agent"] = agent.pk if AgentCustomField.objects.filter( - field=field["field"], agent=agent.id # type: ignore + field=field["field"], agent=agent.pk ): value = AgentCustomField.objects.get( - field=field["field"], agent=agent.id # type: ignore + field=field["field"], agent=agent.pk ) serializer = AgentCustomFieldSerializer( instance=value, data=custom_field @@ -155,10 +264,26 @@ def put(self, request, agent_id): # uninstall agent def delete(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) + + code = "foo" # stub for windows + if agent.plat == AgentPlat.LINUX: + code = Path(settings.LINUX_AGENT_SCRIPT).read_text() + elif agent.plat == AgentPlat.DARWIN: + code = Path(settings.MAC_UNINSTALL).read_text() + + asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False)) name = agent.hostname + mesh_id = agent.mesh_node_id agent.delete() reload_nats() + try: + uri = get_mesh_ws_url() + asyncio.run(remove_mesh_agent(uri, mesh_id)) + except Exception as e: + DebugLog.error( + message=f"Unable to remove agent {name} from meshcentral database: {e}", + log_type=DebugLogType.AGENT_ISSUES, + ) return Response(f"{name} will now be uninstalled.") @@ -167,9 +292,14 @@ class AgentProcesses(APIView): # list agent processes def get(self, request, agent_id): + if getattr(settings, "DEMO", False): + from tacticalrmm.demo_views import demo_get_procs + + return demo_get_procs() + agent = get_object_or_404(Agent, agent_id=agent_id) r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5)) - if r == "timeout" or r == "natsdown": + if r in ("timeout", "natsdown"): return notify_error("Unable to contact the agent") return Response(r) @@ -180,7 +310,7 @@ def delete(self, request, agent_id, pid): agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15) ) - if r == "timeout" or r == "natsdown": + if r in ("timeout", "natsdown"): return notify_error("Unable to contact the agent") elif r != "ok": return notify_error(r) @@ -194,19 +324,19 @@ class AgentMeshCentral(APIView): # get mesh urls def get(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - core = CoreSettings.objects.first() + core = get_core_settings() - token = agent.get_login_token( - key=core.mesh_token, - user=f"user//{core.mesh_username.lower()}", # type:ignore - ) - - if token == "err": - return notify_error("Invalid mesh token") + if not core.mesh_disable_auto_login: + token = get_login_token( + key=core.mesh_token, user=f"user//{core.mesh_username}" + ) + token_param = f"login={token}&" + else: + token_param = "" - control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore - terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore - file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore + control = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=11&hide=31" + terminal = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=12&hide=31" + file = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=13&hide=31" AuditLog.audit_mesh_session( username=request.user.username, @@ -240,9 +370,9 @@ def post(self, request, agent_id): @permission_classes([IsAuthenticated, AgentPerms]) def get_agent_versions(request): agents = ( - Agent.objects.filter_by_role(request.user) - .prefetch_related("site") - .only("pk", "hostname") + Agent.objects.defer(*AGENT_DEFER) + .filter_by_role(request.user) # type: ignore + .select_related("site__client") ) return Response( { @@ -256,7 +386,7 @@ def get_agent_versions(request): @permission_classes([IsAuthenticated, UpdateAgentPerms]) def update_agents(request): q = ( - Agent.objects.filter_by_role(request.user) + Agent.objects.filter_by_role(request.user) # type: ignore .filter(agent_id__in=request.data["agent_ids"]) .only("agent_id", "version") ) @@ -265,7 +395,9 @@ def update_agents(request): for i in q if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) ] - send_agent_update_task.delay(agent_ids=agent_ids) + + token, _ = token_is_valid() + send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False) return Response("ok") @@ -273,18 +405,18 @@ def update_agents(request): @permission_classes([IsAuthenticated, PingAgentPerms]) def ping(request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - status = "offline" + status = AGENT_STATUS_OFFLINE attempts = 0 while 1: r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) if r == "pong": - status = "online" + status = AGENT_STATUS_ONLINE break else: attempts += 1 - time.sleep(1) + time.sleep(0.5) - if attempts >= 5: + if attempts >= 3: break return Response({"name": agent.hostname, "status": status}) @@ -293,8 +425,13 @@ def ping(request, agent_id): @api_view(["GET"]) @permission_classes([IsAuthenticated, EvtLogPerms]) def get_event_log(request, agent_id, logtype, days): + if getattr(settings, "DEMO", False): + from tacticalrmm.demo_views import demo_get_eventlog + + 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", @@ -305,7 +442,7 @@ def get_event_log(request, agent_id, logtype, days): }, } r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2)) - if r == "timeout" or r == "natsdown": + if r in ("timeout", "natsdown"): return notify_error("Unable to contact the agent") return Response(r) @@ -316,23 +453,28 @@ def get_event_log(request, agent_id, logtype, days): def send_raw_cmd(request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) timeout = int(request.data["timeout"]) + if request.data["shell"] == "custom" and request.data["custom_shell"]: + shell = request.data["custom_shell"] + else: + shell = request.data["shell"] + data = { "func": "rawcmd", "timeout": timeout, "payload": { "command": request.data["cmd"], - "shell": request.data["shell"], + "shell": shell, }, + "run_as_user": request.data["run_as_user"], } - if pyver.parse(agent.version) >= pyver.parse("1.6.0"): - hist = AgentHistory.objects.create( - agent=agent, - type="cmd_run", - command=request.data["cmd"], - username=request.user.username[:50], - ) - data["id"] = hist.pk + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.CMD_RUN, + command=request.data["cmd"], + username=request.user.username[:50], + ) + data["id"] = hist.pk r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2)) @@ -343,7 +485,7 @@ def send_raw_cmd(request, agent_id): username=request.user.username, agent=agent, cmd=request.data["cmd"], - shell=request.data["shell"], + shell=shell, debug_info={"ip": request._client_ip}, ) @@ -352,6 +494,7 @@ def send_raw_cmd(request, agent_id): class Reboot(APIView): permission_classes = [IsAuthenticated, RebootAgentPerms] + # reboot now def post(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) @@ -364,28 +507,43 @@ def post(self, request, agent_id): # 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-%d %H:%M") + obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M") except Exception: return notify_error("Invalid date") + if date_is_in_past(datetime_obj=obj, agent_tz=agent.timezone): + return notify_error("Date cannot be set in the past") + task_name = "TacticalRMM_SchedReboot_" + "".join( random.choice(string.ascii_letters) for _ in range(10) ) + expire_date = obj + djangotime.timedelta(minutes=5) + nats_data = { "func": "schedtask", "schedtaskpayload": { "type": "schedreboot", - "deleteafter": True, - "trigger": "once", + "enabled": True, + "delete_expired_task_after": True, + "start_when_available": False, + "multiple_instances": 2, + "trigger": "runonce", "name": task_name, - "year": int(dt.datetime.strftime(obj, "%Y")), - "month": dt.datetime.strftime(obj, "%B"), - "day": int(dt.datetime.strftime(obj, "%d")), - "hour": int(dt.datetime.strftime(obj, "%H")), - "min": int(dt.datetime.strftime(obj, "%M")), + "start_year": int(dt.datetime.strftime(obj, "%Y")), + "start_month": int(dt.datetime.strftime(obj, "%-m")), + "start_day": int(dt.datetime.strftime(obj, "%-d")), + "start_hour": int(dt.datetime.strftime(obj, "%-H")), + "start_min": int(dt.datetime.strftime(obj, "%-M")), + "expire_year": int(expire_date.strftime("%Y")), + "expire_month": int(expire_date.strftime("%-m")), + "expire_day": int(expire_date.strftime("%-d")), + "expire_hour": int(expire_date.strftime("%-H")), + "expire_min": int(expire_date.strftime("%-M")), }, } @@ -395,7 +553,7 @@ def patch(self, request, agent_id): 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( @@ -407,45 +565,69 @@ def patch(self, request, agent_id): @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 core.utils import token_is_valid - from agents.utils import get_winagent_url + if getattr(settings, "TRMM_INSECURE", False) and request.data["installMethod"] in { + "exe", + "powershell", + }: + return notify_error( + "Not available in insecure mode. Please use the 'Manual' method." + ) + + # TODO rework this ghetto validation hack + # https://github.com/amidaware/tacticalrmm/issues/1461 + try: + int(request.data["expires"]) + except ValueError: + return notify_error("Please enter a valid number of hours") client_id = request.data["client"] site_id = request.data["site"] version = settings.LATEST_AGENT_VER - arch = request.data["arch"] + goarch = request.data["goarch"] + plat = request.data["plat"] if not _has_perm_on_site(request.user, site_id): raise PermissionDenied() - # response type is blob so we have to use - # status codes and render error message on the frontend - if arch == "64" and not os.path.exists( - os.path.join(settings.EXE_DIR, "meshagent.exe") - ): - return notify_error( - "Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" - ) + codesign_token, is_valid = token_is_valid() - if arch == "32" and not os.path.exists( - os.path.join(settings.EXE_DIR, "meshagent-x86.exe") - ): + if request.data["installMethod"] in {"bash", "mac"} and not is_valid: return notify_error( - "Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral" + "Linux/Mac agents require code signing. Please see https://docs.tacticalrmm.com/code_signing/ for more info." ) - inno = ( - f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe" - ) - download_url = get_winagent_url(arch) + inno = f"tacticalagent-v{version}-{plat}-{goarch}" + if plat == AgentPlat.WINDOWS: + inno += ".exe" + + download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token) installer_user = User.objects.filter(is_installer_user=True).first() _, token = AuthToken.objects.create( - user=installer_user, expiry=dt.timedelta(hours=request.data["expires"]) + user=installer_user, expiry=dt.timedelta(hours=int(request.data["expires"])) ) + install_flags = [ + "-m", + "install", + "--api", + request.data["api"], + "--client-id", + client_id, + "--site-id", + site_id, + "--agent-type", + request.data["agenttype"], + "--auth", + token, + ] + if request.data["installMethod"] == "exe": from tacticalrmm.utils import generate_winagent_exe @@ -456,58 +638,66 @@ def install_agent(request): rdp=request.data["rdp"], ping=request.data["ping"], power=request.data["power"], - arch=arch, + goarch=goarch, token=token, api=request.data["api"], file_name=request.data["fileName"], ) - elif request.data["installMethod"] == "manual": - cmd = [ - inno, - "/VERYSILENT", - "/SUPPRESSMSGBOXES", - "&&", - "ping", - "127.0.0.1", - "-n", - "5", - "&&", - r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"', - "-m", - "install", - "--api", - request.data["api"], - "--client-id", - client_id, - "--site-id", - site_id, - "--agent-type", - request.data["agenttype"], - "--auth", - token, - ] - - if int(request.data["rdp"]): - cmd.append("--rdp") - if int(request.data["ping"]): - cmd.append("--ping") - if int(request.data["power"]): - cmd.append("--power") - - resp = { - "cmd": " ".join(str(i) for i in cmd), - "url": download_url, - } + elif request.data["installMethod"] == "bash": + from agents.utils import generate_linux_install - return Response(resp) + return generate_linux_install( + client=str(client_id), + site=str(site_id), + agent_type=request.data["agenttype"], + arch=goarch, + token=token, + api=request.data["api"], + download_url=download_url, + ) - elif request.data["installMethod"] == "powershell": + elif request.data["installMethod"] in {"manual", "mac"}: + resp = {} + if request.data["installMethod"] == "manual": + cmd = [ + inno, + "/VERYSILENT", + "/SUPPRESSMSGBOXES", + "&&", + "ping", + "127.0.0.1", + "-n", + "5", + "&&", + r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"', + ] + install_flags + + if int(request.data["rdp"]): + cmd.append("--rdp") + if int(request.data["ping"]): + cmd.append("--ping") + if int(request.data["power"]): + cmd.append("--power") + + if getattr(settings, "TRMM_INSECURE", False): + cmd.append("--insecure") + + resp["cmd"] = " ".join(str(i) for i in cmd) + else: + install_flags.insert(0, f"sudo ./{inno}") + cmd = install_flags.copy() + dl = f"curl -L -o {inno} '{download_url}'" + resp["cmd"] = ( + dl + f" && chmod +x {inno} && " + " ".join(str(i) for i in cmd) + ) - ps = os.path.join(settings.BASE_DIR, "core/installer.ps1") + resp["url"] = download_url - with open(ps, "r") as f: - text = f.read() + return Response(resp) + + elif request.data["installMethod"] == "powershell": + text = Path(settings.BASE_DIR / "core" / "installer.ps1").read_text() replace_dict = { "innosetupchange": inno, @@ -525,66 +715,31 @@ def install_agent(request): for i, j in replace_dict.items(): text = text.replace(i, j) - file_name = "rmm-installer.ps1" - ps1 = os.path.join(settings.EXE_DIR, file_name) - - if os.path.exists(ps1): - try: - os.remove(ps1) - except Exception as e: - DebugLog.error(message=str(e)) - - with open(ps1, "w") as f: - f.write(text) - - if settings.DEBUG: - with open(ps1, "r") as f: - response = HttpResponse(f.read(), content_type="text/plain") - response["Content-Disposition"] = f"inline; filename={file_name}" - return response - else: - response = HttpResponse() - response["Content-Disposition"] = f"attachment; filename={file_name}" - response["X-Accel-Redirect"] = f"/private/exe/{file_name}" + with StringIO(text) as fp: + response = HttpResponse(fp.read(), content_type="text/plain") + response["Content-Disposition"] = "attachment; filename=rmm-installer.ps1" return response @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"] - # attempt a realtime recovery, otherwise fall back to old recovery method - if mode == "tacagent" or mode == "mesh": - data = {"func": "recover", "payload": {"mode": mode}} - r = asyncio.run(agent.nats_cmd(data, timeout=10)) - if r == "ok": - return Response("Successfully completed recovery") - - if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore - return notify_error( - "A recovery action is currently pending. Please wait for the next agent check-in." - ) - - if mode == "command" and not request.data["cmd"]: - return notify_error("Command is required") - - # if we've made it this far and realtime recovery didn't work, - # tacagent service is the fallback recovery so we obv can't use that to recover itself if it's down if mode == "tacagent": - return notify_error( - "Requires RPC service to be functional. Please recover that first" - ) + uri = get_mesh_ws_url() + agent.recover(mode, uri, wait=False) + return Response("Recovery will be attempted shortly") - # we should only get here if all other methods fail - RecoveryAction( - agent=agent, - mode=mode, - command=request.data["cmd"] if mode == "command" else None, - ).save() + elif mode == "mesh": + r, err = agent.recover(mode, "") + if err: + return notify_error(f"Unable to complete recovery: {r}") - return Response("Recovery will be attempted on the agent's next check-in") + return Response("Successfully completed recovery") @api_view(["POST"]) @@ -594,6 +749,8 @@ def run_script(request, agent_id): script = get_object_or_404(Script, pk=request.data["script"]) output = request.data["output"] args = request.data["args"] + run_as_user: bool = request.data["run_as_user"] + env_vars: list[str] = request.data["env_vars"] req_timeout = int(request.data["timeout"]) + 3 AuditLog.audit_script_run( @@ -603,15 +760,13 @@ def run_script(request, agent_id): debug_info={"ip": request._client_ip}, ) - history_pk = 0 - if pyver.parse(agent.version) >= pyver.parse("1.6.0"): - hist = AgentHistory.objects.create( - agent=agent, - type="script_run", - script=script, - username=request.user.username[:50], - ) - history_pk = hist.pk + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.SCRIPT_RUN, + script=script, + username=request.user.username[:50], + ) + history_pk = hist.pk if output == "wait": r = agent.run_script( @@ -620,6 +775,8 @@ def run_script(request, agent_id): timeout=req_timeout, wait=True, history_pk=history_pk, + run_as_user=run_as_user, + env_vars=env_vars, ) return Response(r) @@ -633,6 +790,9 @@ def run_script(request, agent_id): nats_timeout=req_timeout, emails=emails, args=args, + history_pk=history_pk, + run_as_user=run_as_user, + env_vars=env_vars, ) elif output == "collector": from core.models import CustomField @@ -643,15 +803,17 @@ def run_script(request, agent_id): timeout=req_timeout, wait=True, history_pk=history_pk, + run_as_user=run_as_user, + env_vars=env_vars, ) custom_field = CustomField.objects.get(pk=request.data["custom_field"]) - if custom_field.model == "agent": + if custom_field.model == CustomFieldModel.AGENT: field = custom_field.get_or_create_field_value(agent) - elif custom_field.model == "client": + elif custom_field.model == CustomFieldModel.CLIENT: field = custom_field.get_or_create_field_value(agent.client) - elif custom_field.model == "site": + elif custom_field.model == CustomFieldModel.SITE: field = custom_field.get_or_create_field_value(agent.site) else: return notify_error("Custom Field was invalid") @@ -671,39 +833,25 @@ def run_script(request, agent_id): timeout=req_timeout, wait=True, history_pk=history_pk, + run_as_user=run_as_user, + env_vars=env_vars, ) Note.objects.create(agent=agent, user=request.user, note=r) return Response(r) else: agent.run_script( - scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk + scriptpk=script.pk, + args=args, + timeout=req_timeout, + history_pk=history_pk, + run_as_user=run_as_user, + env_vars=env_vars, ) return Response(f"{script.name} will now be run on {agent.hostname}") -@api_view(["POST"]) -def get_mesh_exe(request, arch): - filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe" - mesh_exe = os.path.join(settings.EXE_DIR, filename) - if not os.path.exists(mesh_exe): - return notify_error(f"File {filename} has not been uploaded.") - - if settings.DEBUG: - with open(mesh_exe, "rb") as f: - response = HttpResponse( - f.read(), content_type="application/vnd.microsoft.portable-executable" - ) - response["Content-Disposition"] = f"inline; filename={filename}" - return response - else: - response = HttpResponse() - response["Content-Disposition"] = f"attachment; filename={filename}" - response["X-Accel-Redirect"] = f"/private/exe/{filename}" - return response - - class GetAddNotes(APIView): permission_classes = [IsAuthenticated, AgentNotesPerms] @@ -712,7 +860,7 @@ def get(self, request, agent_id=None): agent = get_object_or_404(Agent, agent_id=agent_id) notes = Note.objects.filter(agent=agent) else: - notes = Note.objects.filter_by_role(request.user) + notes = Note.objects.filter_by_role(request.user) # type: ignore return Response(AgentNoteSerializer(notes, many=True).data) @@ -721,6 +869,9 @@ def post(self, request): if not _has_perm_on_agent(request.user, agent.agent_id): raise PermissionDenied() + if "note" not in request.data.keys(): + return notify_error("Cannot add an empty note") + data = { "note": request.data["note"], "agent": agent.pk, @@ -774,37 +925,44 @@ def bulk(request): if request.data["target"] == "client": if not _has_perm_on_client(request.user, request.data["client"]): raise PermissionDenied() - q = Agent.objects.filter_by_role(request.user).filter( + q = Agent.objects.filter_by_role(request.user).filter( # type: ignore site__client_id=request.data["client"] ) elif request.data["target"] == "site": if not _has_perm_on_site(request.user, request.data["site"]): raise PermissionDenied() - q = Agent.objects.filter_by_role(request.user).filter( + q = Agent.objects.filter_by_role(request.user).filter( # type: ignore site_id=request.data["site"] ) elif request.data["target"] == "agents": - q = Agent.objects.filter_by_role(request.user).filter( + q = Agent.objects.filter_by_role(request.user).filter( # type: ignore agent_id__in=request.data["agents"] ) elif request.data["target"] == "all": - q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type") + q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type") # type: ignore else: return notify_error("Something went wrong") if request.data["monType"] == "servers": - q = q.filter(monitoring_type="server") + q = q.filter(monitoring_type=AgentMonType.SERVER) elif request.data["monType"] == "workstations": - q = q.filter(monitoring_type="workstation") + q = q.filter(monitoring_type=AgentMonType.WORKSTATION) + + if request.data["osType"] == AgentPlat.WINDOWS: + q = q.filter(plat=AgentPlat.WINDOWS) + elif request.data["osType"] == AgentPlat.LINUX: + q = q.filter(plat=AgentPlat.LINUX) + elif request.data["osType"] == AgentPlat.DARWIN: + q = q.filter(plat=AgentPlat.DARWIN) agents: list[int] = [agent.pk for agent in q] if not agents: - return notify_error("No agents where found meeting the selected criteria") + return notify_error("No agents were found meeting the selected criteria") AuditLog.audit_bulk_action( request.user, @@ -814,29 +972,37 @@ def bulk(request): ) if request.data["mode"] == "command": - handle_bulk_command_task.delay( - agents, - request.data["cmd"], - request.data["shell"], - request.data["timeout"], - request.user.username[:50], - run_on_offline=request.data["offlineAgents"], + if request.data["shell"] == "custom" and request.data["custom_shell"]: + shell = request.data["custom_shell"] + else: + shell = request.data["shell"] + + bulk_command_task.delay( + agent_pks=agents, + cmd=request.data["cmd"], + shell=shell, + timeout=request.data["timeout"], + username=request.user.username[:50], + run_as_user=request.data["run_as_user"], ) return Response(f"Command will now be run on {len(agents)} agents") elif request.data["mode"] == "script": script = get_object_or_404(Script, pk=request.data["script"]) - handle_bulk_script_task.delay( - script.pk, - agents, - request.data["args"], - request.data["timeout"], - request.user.username[:50], + + bulk_script_task.delay( + script_pk=script.pk, + agent_pks=agents, + args=request.data["args"], + timeout=request.data["timeout"], + username=request.user.username[:50], + run_as_user=request.data["run_as_user"], + env_vars=request.data["env_vars"], ) + return Response(f"{script.name} will now be run on {len(agents)} agents") elif request.data["mode"] == "patch": - if request.data["patchMode"] == "install": bulk_install_updates_task.delay(agents) return Response( @@ -852,13 +1018,12 @@ def bulk(request): @api_view(["POST"]) @permission_classes([IsAuthenticated, AgentPerms]) def agent_maintenance(request): - if request.data["type"] == "Client": if not _has_perm_on_client(request.user, request.data["id"]): raise PermissionDenied() count = ( - Agent.objects.filter_by_role(request.user) + Agent.objects.filter_by_role(request.user) # type: ignore .filter(site__client_id=request.data["id"]) .update(maintenance_mode=request.data["action"]) ) @@ -868,7 +1033,7 @@ def agent_maintenance(request): raise PermissionDenied() count = ( - Agent.objects.filter_by_role(request.user) + Agent.objects.filter_by_role(request.user) # type: ignore .filter(site_id=request.data["id"]) .update(maintenance_mode=request.data["action"]) ) @@ -879,10 +1044,17 @@ def agent_maintenance(request): if count: action = "disabled" if not request.data["action"] else "enabled" return Response(f"Maintenance mode has been {action} on {count} agents") - else: - return Response( - f"No agents have been put in maintenance mode. You might not have permissions to the resources." - ) + + return Response( + "No agents have been put in maintenance mode. You might not have permissions to the resources." + ) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated, RecoverAgentPerms]) +def bulk_agent_recovery(request): + bulk_recover_agents_task.delay() + return Response("Agents will now be recovered") class WMI(APIView): @@ -904,6 +1076,116 @@ def get(self, request, agent_id=None): agent = get_object_or_404(Agent, agent_id=agent_id) history = AgentHistory.objects.filter(agent=agent) else: - history = AgentHistory.objects.filter_by_role(request.user) + history = AgentHistory.objects.filter_by_role(request.user) # type: ignore ctx = {"default_tz": get_default_timezone()} return Response(AgentHistorySerializer(history, many=True, context=ctx).data) + + +class ScriptRunHistory(APIView): + permission_classes = [IsAuthenticated, AgentHistoryPerms] + + class OutputSerializer(serializers.ModelSerializer): + script_name = serializers.ReadOnlyField(source="script.name") + agent_id = serializers.ReadOnlyField(source="agent.agent_id") + + class Meta: + model = AgentHistory + fields = ( + "id", + "time", + "username", + "script", + "script_results", + "agent", + "script_name", + "agent_id", + ) + read_only_fields = fields + + def get(self, request): + date_range_filter = Q() + script_name_filter = Q() + + start = request.query_params.get("start", None) + end = request.query_params.get("end", None) + limit = request.query_params.get("limit", None) + script_name = request.query_params.get("scriptname", None) + if start and end: + start_dt = parse_datetime(start) + end_dt = parse_datetime(end) + djangotime.timedelta(days=1) + date_range_filter = Q(time__range=[start_dt, end_dt]) + + if script_name: + script_name_filter = Q(script__name=script_name) + + AGENT_R_DEFER = ( + "agent__wmi_detail", + "agent__services", + "agent__created_by", + "agent__created_time", + "agent__modified_by", + "agent__modified_time", + "agent__disks", + "agent__operating_system", + "agent__mesh_node_id", + "agent__description", + "agent__patches_last_installed", + "agent__time_zone", + "agent__alert_template_id", + "agent__policy_id", + "agent__site_id", + "agent__version", + "agent__plat", + "agent__goarch", + "agent__hostname", + "agent__last_seen", + "agent__public_ip", + "agent__total_ram", + "agent__boot_time", + "agent__logged_in_username", + "agent__last_logged_in_user", + "agent__monitoring_type", + "agent__overdue_email_alert", + "agent__overdue_text_alert", + "agent__overdue_dashboard_alert", + "agent__offline_time", + "agent__overdue_time", + "agent__check_interval", + "agent__needs_reboot", + "agent__choco_installed", + "agent__maintenance_mode", + "agent__block_policy_inheritance", + ) + hists = ( + AgentHistory.objects.filter(type=AgentHistoryType.SCRIPT_RUN) + .select_related("agent") + .select_related("script") + .defer(*AGENT_R_DEFER) + .filter(date_range_filter) + .filter(script_name_filter) + .order_by("-time") + ) + if limit: + try: + lim = int(limit) + except KeyError: + return notify_error("Invalid limit") + hists = hists[:lim] + + ret = self.OutputSerializer(hists, many=True).data + return Response(ret) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated, AgentWOLPerms]) +def wol(request, agent_id): + agent = get_object_or_404( + Agent.objects.defer(*AGENT_DEFER), + agent_id=agent_id, + ) + try: + uri = get_mesh_ws_url() + asyncio.run(wake_on_lan(uri=uri, mesh_node_id=agent.mesh_node_id)) + except Exception as e: + return notify_error(str(e)) + return Response(f"Wake-on-LAN sent to {agent.hostname}") diff --git a/api/tacticalrmm/alerts/migrations/0011_alter_alert_agent.py b/api/tacticalrmm/alerts/migrations/0011_alter_alert_agent.py new file mode 100644 index 0000000000..1b4724b42f --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0011_alter_alert_agent.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.3 on 2022-04-07 17:28 + +import django.db.models.deletion +from django.db import migrations, models + + +def delete_alerts_without_agent(apps, schema): + Alert = apps.get_model("alerts", "Alert") + + Alert.objects.filter(agent=None).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("agents", "0047_alter_agent_plat_alter_agent_site"), + ("alerts", "0010_auto_20210917_1954"), + ] + + operations = [ + migrations.RunPython( + delete_alerts_without_agent, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/api/tacticalrmm/alerts/migrations/0012_alter_alert_action_retcode_and_more.py b/api/tacticalrmm/alerts/migrations/0012_alter_alert_action_retcode_and_more.py new file mode 100644 index 0000000000..a2493a4bb4 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0012_alter_alert_action_retcode_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.5 on 2022-06-29 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0011_alter_alert_agent'), + ] + + operations = [ + migrations.AlterField( + model_name='alert', + name='action_retcode', + field=models.BigIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='alert', + name='resolved_action_retcode', + field=models.BigIntegerField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py b/api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py new file mode 100644 index 0000000000..f066481052 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.3 on 2022-11-26 20:22 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("alerts", "0012_alter_alert_action_retcode_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="alerttemplate", + name="action_env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + migrations.AddField( + model_name="alerttemplate", + name="resolved_action_env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/api/tacticalrmm/alerts/models.py b/api/tacticalrmm/alerts/models.py index 50f386cbee..6bb9488eeb 100644 --- a/api/tacticalrmm/alerts/models.py +++ b/api/tacticalrmm/alerts/models.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast from django.contrib.postgres.fields import ArrayField from django.db import models @@ -9,26 +9,21 @@ from django.utils import timezone as djangotime from logs.models import BaseAuditModel, DebugLog +from tacticalrmm.constants import ( + AgentHistoryType, + AgentMonType, + AlertSeverity, + AlertType, + CheckType, + DebugLogType, +) from tacticalrmm.models import PermissionQuerySet if TYPE_CHECKING: from agents.models import Agent - from autotasks.models import AutomatedTask - from checks.models import Check - - -SEVERITY_CHOICES = [ - ("info", "Informational"), - ("warning", "Warning"), - ("error", "Error"), -] - -ALERT_TYPE_CHOICES = [ - ("availability", "Availability"), - ("check", "Check"), - ("task", "Task"), - ("custom", "Custom"), -] + from autotasks.models import AutomatedTask, TaskResult + from checks.models import Check, CheckResult + from clients.models import Client, Site class Alert(models.Model): @@ -56,7 +51,7 @@ class Alert(models.Model): blank=True, ) alert_type = models.CharField( - max_length=20, choices=ALERT_TYPE_CHOICES, default="availability" + max_length=20, choices=AlertType.choices, default=AlertType.AVAILABILITY ) message = models.TextField(null=True, blank=True) alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) @@ -64,7 +59,9 @@ class Alert(models.Model): snooze_until = models.DateTimeField(null=True, blank=True) resolved = models.BooleanField(default=False) resolved_on = models.DateTimeField(null=True, blank=True) - severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info") + severity = models.CharField( + max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO + ) email_sent = models.DateTimeField(null=True, blank=True) resolved_email_sent = models.DateTimeField(null=True, blank=True) sms_sent = models.DateTimeField(null=True, blank=True) @@ -73,72 +70,206 @@ class Alert(models.Model): action_run = models.DateTimeField(null=True, blank=True) action_stdout = models.TextField(null=True, blank=True) action_stderr = models.TextField(null=True, blank=True) - action_retcode = models.IntegerField(null=True, blank=True) + action_retcode = models.BigIntegerField(null=True, blank=True) action_execution_time = models.CharField(max_length=100, null=True, blank=True) resolved_action_run = models.DateTimeField(null=True, blank=True) resolved_action_stdout = models.TextField(null=True, blank=True) resolved_action_stderr = models.TextField(null=True, blank=True) - resolved_action_retcode = models.IntegerField(null=True, blank=True) + resolved_action_retcode = models.BigIntegerField(null=True, blank=True) resolved_action_execution_time = models.CharField( max_length=100, null=True, blank=True ) - def __str__(self): - return self.message + def __str__(self) -> str: + return f"{self.alert_type} - {self.message}" + + @property + def assigned_agent(self) -> "Optional[Agent]": + return self.agent + + @property + def site(self) -> "Site": + return self.agent.site + + @property + def client(self) -> "Client": + return self.agent.client - def resolve(self): + def resolve(self) -> None: self.resolved = True self.resolved_on = djangotime.now() self.snoozed = False self.snooze_until = None - self.save() + self.save(update_fields=["resolved", "resolved_on", "snoozed", "snooze_until"]) @classmethod - def create_or_return_availability_alert(cls, agent): - if not cls.objects.filter(agent=agent, resolved=False).exists(): - return cls.objects.create( - agent=agent, - alert_type="availability", - severity="error", - message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.", - hidden=True, + def create_or_return_availability_alert( + cls, agent: Agent, skip_create: bool = False + ) -> Optional[Alert]: + if not cls.objects.filter( + agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False + ).exists(): + if skip_create: + return None + + return cast( + Alert, + cls.objects.create( + agent=agent, + alert_type=AlertType.AVAILABILITY, + severity=AlertSeverity.ERROR, + message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.", + hidden=True, + ), ) else: - return cls.objects.get(agent=agent, resolved=False) + try: + return cast( + Alert, + cls.objects.get( + agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False + ), + ) + except cls.MultipleObjectsReturned: + alerts = cls.objects.filter( + agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False + ) + + last_alert = cast(Alert, alerts.last()) + + # cycle through other alerts and resolve + for alert in alerts: + if alert.id != last_alert.pk: + alert.resolve() + + return last_alert + except cls.DoesNotExist: + return None @classmethod - def create_or_return_check_alert(cls, check): - - if not cls.objects.filter(assigned_check=check, resolved=False).exists(): - return cls.objects.create( - assigned_check=check, - alert_type="check", - severity=check.alert_severity, - message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.", - hidden=True, + def create_or_return_check_alert( + cls, + check: "Check", + agent: "Agent", + alert_severity: Optional[str] = None, + skip_create: bool = False, + ) -> "Optional[Alert]": + # need to pass agent if the check is a policy + if not cls.objects.filter( + assigned_check=check, + agent=agent, + resolved=False, + ).exists(): + if skip_create: + return None + + return cast( + Alert, + cls.objects.create( + assigned_check=check, + agent=agent, + alert_type=AlertType.CHECK, + severity=check.alert_severity + if check.check_type + 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, + ), ) else: - return cls.objects.get(assigned_check=check, resolved=False) + try: + return cast( + Alert, + cls.objects.get( + assigned_check=check, + agent=agent, + resolved=False, + ), + ) + except cls.MultipleObjectsReturned: + alerts = cls.objects.filter( + assigned_check=check, + agent=agent, + resolved=False, + ) + last_alert = cast(Alert, alerts.last()) + + # cycle through other alerts and resolve + for alert in alerts: + if alert.id != last_alert.pk: + alert.resolve() + + return last_alert + except cls.DoesNotExist: + return None @classmethod - def create_or_return_task_alert(cls, task): - - if not cls.objects.filter(assigned_task=task, resolved=False).exists(): - return cls.objects.create( - assigned_task=task, - alert_type="task", - severity=task.alert_severity, - message=f"{task.agent.hostname} has task: {task.name} that failed.", - hidden=True, + def create_or_return_task_alert( + cls, + task: "AutomatedTask", + agent: "Agent", + skip_create: bool = False, + ) -> "Optional[Alert]": + if not cls.objects.filter( + assigned_task=task, + agent=agent, + resolved=False, + ).exists(): + if skip_create: + return None + + return cast( + Alert, + cls.objects.create( + assigned_task=task, + agent=agent, + alert_type=AlertType.TASK, + severity=task.alert_severity, + message=f"{agent.hostname} has task: {task.name} that failed.", + hidden=True, + ), ) + else: - return cls.objects.get(assigned_task=task, resolved=False) + try: + return cast( + Alert, + cls.objects.get( + assigned_task=task, + agent=agent, + resolved=False, + ), + ) + except cls.MultipleObjectsReturned: + alerts = cls.objects.filter( + assigned_task=task, + agent=agent, + resolved=False, + ) + last_alert = cast(Alert, alerts.last()) + + # cycle through other alerts and resolve + for alert in alerts: + if alert.id != last_alert.pk: + alert.resolve() + + return last_alert + except cls.DoesNotExist: + return None @classmethod - def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None: - from agents.models import Agent - from autotasks.models import AutomatedTask - from checks.models import Check + def handle_alert_failure( + cls, instance: Union[Agent, TaskResult, CheckResult] + ) -> None: + from agents.models import Agent, AgentHistory + from autotasks.models import TaskResult + from checks.models import CheckResult # set variables dashboard_severities = None @@ -150,6 +281,7 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N alert_interval = None email_task = None text_task = None + run_script_action = None # check what the instance passed is if isinstance(instance, Agent): @@ -163,30 +295,21 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N dashboard_alert = instance.overdue_dashboard_alert alert_template = instance.alert_template maintenance_mode = instance.maintenance_mode - alert_severity = "error" + alert_severity = AlertSeverity.ERROR agent = instance + dashboard_severities = [AlertSeverity.ERROR] + email_severities = [AlertSeverity.ERROR] + text_severities = [AlertSeverity.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 alert_interval = alert_template.agent_periodic_alert_days run_script_action = alert_template.agent_script_actions - if instance.should_create_alert(alert_template): - alert = cls.create_or_return_availability_alert(instance) - else: - # check if there is an alert that exists - if cls.objects.filter(agent=instance, resolved=False).exists(): - alert = cls.objects.get(agent=instance, resolved=False) - else: - alert = None - - elif isinstance(instance, Check): + elif isinstance(instance, CheckResult): from checks.tasks import ( handle_check_email_alert_task, handle_check_sms_alert_task, @@ -195,96 +318,117 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N email_task = handle_check_email_alert_task text_task = handle_check_sms_alert_task - email_alert = instance.email_alert - text_alert = instance.text_alert - dashboard_alert = instance.dashboard_alert + email_alert = instance.assigned_check.email_alert + text_alert = instance.assigned_check.text_alert + dashboard_alert = instance.assigned_check.dashboard_alert alert_template = instance.agent.alert_template maintenance_mode = instance.agent.maintenance_mode - alert_severity = instance.alert_severity + alert_severity = ( + instance.assigned_check.alert_severity + if instance.assigned_check.check_type + 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 + or [ + AlertSeverity.ERROR, + AlertSeverity.WARNING, + AlertSeverity.INFO, + ] + ) + email_severities = alert_template.check_email_alert_severity or [ + AlertSeverity.ERROR, + AlertSeverity.WARNING, + ] + text_severities = alert_template.check_text_alert_severity or [ + AlertSeverity.ERROR, + AlertSeverity.WARNING, + ] always_dashboard = alert_template.check_always_alert always_email = alert_template.check_always_email always_text = alert_template.check_always_text alert_interval = alert_template.check_periodic_alert_days run_script_action = alert_template.check_script_actions - if instance.should_create_alert(alert_template): - alert = cls.create_or_return_check_alert(instance) - else: - # check if there is an alert that exists - if cls.objects.filter(assigned_check=instance, resolved=False).exists(): - alert = cls.objects.get(assigned_check=instance, resolved=False) - else: - alert = None - - elif isinstance(instance, AutomatedTask): + elif isinstance(instance, TaskResult): from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert email_task = handle_task_email_alert text_task = handle_task_sms_alert - email_alert = instance.email_alert - text_alert = instance.text_alert - dashboard_alert = instance.dashboard_alert + email_alert = instance.task.email_alert + text_alert = instance.task.text_alert + dashboard_alert = instance.task.dashboard_alert alert_template = instance.agent.alert_template maintenance_mode = instance.agent.maintenance_mode - alert_severity = instance.alert_severity + alert_severity = instance.task.alert_severity agent = instance.agent # 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 or [ + AlertSeverity.ERROR, + AlertSeverity.WARNING, + ] + email_severities = alert_template.task_email_alert_severity or [ + AlertSeverity.ERROR, + AlertSeverity.WARNING, + ] + text_severities = alert_template.task_text_alert_severity or [ + AlertSeverity.ERROR, + AlertSeverity.WARNING, + ] always_dashboard = alert_template.task_always_alert always_email = alert_template.task_always_email always_text = alert_template.task_always_text alert_interval = alert_template.task_periodic_alert_days run_script_action = alert_template.task_script_actions - if instance.should_create_alert(alert_template): - alert = cls.create_or_return_task_alert(instance) - else: - # check if there is an alert that exists - if cls.objects.filter(assigned_task=instance, resolved=False).exists(): - alert = cls.objects.get(assigned_task=instance, resolved=False) - else: - alert = None else: return + alert = instance.get_or_create_alert_if_needed(alert_template) + # return if agent is in maintenance mode - if maintenance_mode or not alert: + if not alert or maintenance_mode: return - # check if alert severity changed on check and update the alert + # check if alert severity changed and update the alert if alert_severity != alert.severity: alert.severity = alert_severity alert.save(update_fields=["severity"]) # create alert in dashboard if enabled if dashboard_alert or always_dashboard: - # check if alert template is set and specific severities are configured - if alert_template and alert.severity not in dashboard_severities: # type: ignore - pass - else: + if ( + not alert_template + or alert_template + and dashboard_severities + and alert.severity in dashboard_severities + ): alert.hidden = False - alert.save() + alert.save(update_fields=["hidden"]) # send email if enabled if email_alert or always_email: - # check if alert template is set and specific severities are configured - if alert_template and alert.severity not in email_severities: # type: ignore - pass - else: + if ( + not alert_template + or alert_template + and email_severities + and alert.severity in email_severities + ): email_task.delay( pk=alert.pk, alert_interval=alert_interval, @@ -292,26 +436,42 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N # send text if enabled if text_alert or always_text: - # check if alert template is set and specific severities are configured - if alert_template and alert.severity not in text_severities: # type: ignore - pass - else: + if ( + not alert_template + or alert_template + and text_severities + and alert.severity in text_severities + ): text_task.delay(pk=alert.pk, alert_interval=alert_interval) # check if any scripts should be run - if alert_template and alert_template.action and run_script_action and not alert.action_run: # type: ignore + if ( + alert_template + and alert_template.action + and run_script_action + and not alert.action_run + ): + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.SCRIPT_RUN, + script=alert_template.action, + username="alert-action-failure", + ) r = agent.run_script( scriptpk=alert_template.action.pk, args=alert.parse_script_args(alert_template.action_args), timeout=alert_template.action_timeout, wait=True, + history_pk=hist.pk, full=True, run_on_any=True, + run_as_user=False, + env_vars=alert_template.action_env_vars, ) # command was successful - if type(r) == dict: + if isinstance(r, dict): alert.action_retcode = r["retcode"] alert.action_stdout = r["stdout"] alert.action_stderr = r["stderr"] @@ -321,21 +481,24 @@ def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> N 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", ) @classmethod - def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None: - from agents.models import Agent - from autotasks.models import AutomatedTask - from checks.models import Check + def handle_alert_resolve( + cls, instance: Union[Agent, TaskResult, CheckResult] + ) -> None: + from agents.models import Agent, AgentHistory + from autotasks.models import TaskResult + from checks.models import CheckResult # set variables email_on_resolved = False text_on_resolved = False resolved_email_task = None resolved_text_task = None + run_script_action = None # check what the instance passed is if isinstance(instance, Agent): @@ -345,7 +508,6 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N resolved_text_task = agent_recovery_sms_task alert_template = instance.alert_template - alert = cls.objects.get(agent=instance, resolved=False) maintenance_mode = instance.maintenance_mode agent = instance @@ -354,7 +516,12 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N text_on_resolved = alert_template.agent_text_on_resolved run_script_action = alert_template.agent_script_actions - elif isinstance(instance, Check): + if agent.overdue_email_alert: + email_on_resolved = True + if agent.overdue_text_alert: + text_on_resolved = True + + elif isinstance(instance, CheckResult): from checks.tasks import ( handle_resolved_check_email_alert_task, handle_resolved_check_sms_alert_task, @@ -364,7 +531,6 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N resolved_text_task = handle_resolved_check_sms_alert_task alert_template = instance.agent.alert_template - alert = cls.objects.get(assigned_check=instance, resolved=False) maintenance_mode = instance.agent.maintenance_mode agent = instance.agent @@ -373,7 +539,7 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N text_on_resolved = alert_template.check_text_on_resolved run_script_action = alert_template.check_script_actions - elif isinstance(instance, AutomatedTask): + elif isinstance(instance, TaskResult): from autotasks.tasks import ( handle_resolved_task_email_alert, handle_resolved_task_sms_alert, @@ -383,7 +549,6 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N resolved_text_task = handle_resolved_task_sms_alert alert_template = instance.agent.alert_template - alert = cls.objects.get(assigned_task=instance, resolved=False) maintenance_mode = instance.agent.maintenance_mode agent = instance.agent @@ -395,8 +560,10 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N else: return + alert = instance.get_or_create_alert_if_needed(alert_template) + # return if agent is in maintenance mode - if maintenance_mode: + if not alert or maintenance_mode: return alert.resolve() @@ -413,20 +580,29 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N if ( alert_template and alert_template.resolved_action - and run_script_action # type: ignore + and run_script_action and not alert.resolved_action_run ): + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.SCRIPT_RUN, + script=alert_template.action, + username="alert-action-resolved", + ) r = agent.run_script( scriptpk=alert_template.resolved_action.pk, args=alert.parse_script_args(alert_template.resolved_action_args), timeout=alert_template.resolved_action_timeout, wait=True, + history_pk=hist.pk, full=True, run_on_any=True, + run_as_user=False, + env_vars=alert_template.resolved_action_env_vars, ) # command was successful - if type(r) == dict: + if isinstance(r, dict): alert.resolved_action_retcode = r["retcode"] alert.resolved_action_stdout = r["stdout"] alert.resolved_action_stderr = r["stderr"] @@ -438,16 +614,15 @@ def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> N 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", ) - def parse_script_args(self, args: list[str]): - + def parse_script_args(self, args: List[str]) -> List[str]: if not args: return [] - temp_args = list() + temp_args = [] # pattern to match for injection pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*") @@ -456,15 +631,18 @@ def parse_script_args(self, args: list[str]): if match: name = match.group(1) - if hasattr(self, name): + # check if attr exists and isn't a function + if hasattr(self, name) and not callable(getattr(self, name)): value = f"'{getattr(self, name)}'" else: continue try: - temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore + temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) + except re.error: + temp_args.append(re.sub("\\{\\{.*\\}\\}", re.escape(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: @@ -490,6 +668,12 @@ class AlertTemplate(BaseAuditModel): blank=True, default=list, ) + action_env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) action_timeout = models.PositiveIntegerField(default=15) resolved_action = models.ForeignKey( "scripts.Script", @@ -504,6 +688,12 @@ class AlertTemplate(BaseAuditModel): blank=True, default=list, ) + resolved_action_env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) resolved_action_timeout = models.PositiveIntegerField(default=15) # overrides the global recipients @@ -534,17 +724,17 @@ class AlertTemplate(BaseAuditModel): # check alert settings check_email_alert_severity = ArrayField( - models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), + models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), blank=True, default=list, ) check_text_alert_severity = ArrayField( - models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), + models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), blank=True, default=list, ) check_dashboard_alert_severity = ArrayField( - models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), + models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), blank=True, default=list, ) @@ -558,17 +748,17 @@ class AlertTemplate(BaseAuditModel): # task alert settings task_email_alert_severity = ArrayField( - models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), + models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), blank=True, default=list, ) task_text_alert_severity = ArrayField( - models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), + models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), blank=True, default=list, ) task_dashboard_alert_severity = ArrayField( - models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), + models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices), blank=True, default=list, ) @@ -594,11 +784,22 @@ class AlertTemplate(BaseAuditModel): "agents.Agent", related_name="alert_exclusions", blank=True ) - def __str__(self): + def __str__(self) -> str: return self.name + def is_agent_excluded(self, agent: "Agent") -> bool: + return ( + agent in self.excluded_agents.all() + or agent.site in self.excluded_sites.all() + or agent.client in self.excluded_clients.all() + or agent.monitoring_type == AgentMonType.WORKSTATION + and self.exclude_workstations + or agent.monitoring_type == AgentMonType.SERVER + and self.exclude_servers + ) + @staticmethod - def serialize(alert_template): + def serialize(alert_template: AlertTemplate) -> Dict[str, Any]: # serializes the agent and returns json from .serializers import AlertTemplateAuditSerializer diff --git a/api/tacticalrmm/alerts/permissions.py b/api/tacticalrmm/alerts/permissions.py index 84e09697c5..f7060151ea 100644 --- a/api/tacticalrmm/alerts/permissions.py +++ b/api/tacticalrmm/alerts/permissions.py @@ -1,10 +1,15 @@ +from typing import TYPE_CHECKING + from django.shortcuts import get_object_or_404 from rest_framework import permissions from tacticalrmm.permissions import _has_perm, _has_perm_on_agent +if TYPE_CHECKING: + from accounts.models import User + -def _has_perm_on_alert(user, id: int): +def _has_perm_on_alert(user: "User", id: int) -> bool: from alerts.models import Alert role = user.role @@ -19,10 +24,6 @@ def _has_perm_on_alert(user, id: int): if alert.agent: agent_id = alert.agent.agent_id - elif alert.assigned_check: - agent_id = alert.assigned_check.agent.agent_id - elif alert.assigned_task: - agent_id = alert.assigned_task.agent.agent_id else: return True @@ -30,8 +31,8 @@ def _has_perm_on_alert(user, id: int): class AlertPerms(permissions.BasePermission): - def has_permission(self, r, view): - if r.method == "GET" or r.method == "PATCH": + def has_permission(self, r, view) -> bool: + if r.method in ("GET", "PATCH"): if "pk" in view.kwargs.keys(): return _has_perm(r, "can_list_alerts") and _has_perm_on_alert( r.user, view.kwargs["pk"] @@ -48,8 +49,8 @@ def has_permission(self, r, view): class AlertTemplatePerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_alerttemplates") - else: - return _has_perm(r, "can_manage_alerttemplates") + + return _has_perm(r, "can_manage_alerttemplates") diff --git a/api/tacticalrmm/alerts/serializers.py b/api/tacticalrmm/alerts/serializers.py index b7b79add36..d1ad23075a 100644 --- a/api/tacticalrmm/alerts/serializers.py +++ b/api/tacticalrmm/alerts/serializers.py @@ -3,86 +3,16 @@ from automation.serializers import PolicySerializer from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer -from tacticalrmm.utils import get_default_timezone from .models import Alert, AlertTemplate class AlertSerializer(ModelSerializer): - - hostname = SerializerMethodField(read_only=True) - client = SerializerMethodField(read_only=True) - site = SerializerMethodField(read_only=True) - alert_time = SerializerMethodField(read_only=True) - resolve_on = SerializerMethodField(read_only=True) - snoozed_until = SerializerMethodField(read_only=True) - - def get_hostname(self, instance): - if instance.alert_type == "availability": - return instance.agent.hostname if instance.agent else "" - elif instance.alert_type == "check": - return ( - instance.assigned_check.agent.hostname - if instance.assigned_check - else "" - ) - elif instance.alert_type == "task": - return ( - instance.assigned_task.agent.hostname if instance.assigned_task else "" - ) - else: - return "" - - def get_client(self, instance): - if instance.alert_type == "availability": - return instance.agent.client.name if instance.agent else "" - elif instance.alert_type == "check": - return ( - instance.assigned_check.agent.client.name - if instance.assigned_check - else "" - ) - elif instance.alert_type == "task": - return ( - instance.assigned_task.agent.client.name - if instance.assigned_task - else "" - ) - else: - return "" - - def get_site(self, instance): - if instance.alert_type == "availability": - return instance.agent.site.name if instance.agent else "" - elif instance.alert_type == "check": - return ( - instance.assigned_check.agent.site.name - if instance.assigned_check - else "" - ) - elif instance.alert_type == "task": - return ( - instance.assigned_task.agent.site.name if instance.assigned_task else "" - ) - else: - return "" - - def get_alert_time(self, instance): - if instance.alert_time: - return instance.alert_time.astimezone(get_default_timezone()).timestamp() - else: - return None - - def get_resolve_on(self, instance): - if instance.resolved_on: - return instance.resolved_on.astimezone(get_default_timezone()).timestamp() - else: - return None - - def get_snoozed_until(self, instance): - if instance.snooze_until: - return instance.snooze_until.astimezone(get_default_timezone()).timestamp() - return None + hostname = ReadOnlyField(source="assigned_agent.hostname") + agent_id = ReadOnlyField(source="assigned_agent.agent_id") + client = ReadOnlyField(source="client.name") + site = ReadOnlyField(source="site.name") + alert_time = ReadOnlyField() class Meta: model = Alert @@ -104,11 +34,11 @@ class Meta: fields = "__all__" def get_applied_count(self, instance): - count = 0 - count += instance.policies.count() - count += instance.clients.count() - count += instance.sites.count() - return count + return ( + instance.policies.count() + + instance.clients.count() + + instance.sites.count() + ) class AlertTemplateRelationSerializer(ModelSerializer): diff --git a/api/tacticalrmm/alerts/tasks.py b/api/tacticalrmm/alerts/tasks.py index 42835102e9..db071c6532 100644 --- a/api/tacticalrmm/alerts/tasks.py +++ b/api/tacticalrmm/alerts/tasks.py @@ -1,11 +1,13 @@ from django.utils import timezone as djangotime + +from agents.models import Agent from tacticalrmm.celery import app +from .models import Alert + @app.task def unsnooze_alerts() -> str: - from .models import Alert - Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update( snoozed=False, snooze_until=None ) @@ -14,10 +16,10 @@ def unsnooze_alerts() -> str: @app.task -def cache_agents_alert_template(): - from agents.models import Agent - - for agent in Agent.objects.only("pk"): +def cache_agents_alert_template() -> str: + for agent in Agent.objects.only( + "pk", "site", "policy", "alert_template" + ).select_related("site", "policy", "alert_template"): agent.set_alert_template() return "ok" @@ -25,8 +27,6 @@ def cache_agents_alert_template(): @app.task def prune_resolved_alerts(older_than_days: int) -> str: - from .models import Alert - Alert.objects.filter(resolved=True).filter( alert_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) ).delete() diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py index d7791f515b..4ca21ac45e 100644 --- a/api/tacticalrmm/alerts/tests.py +++ b/api/tacticalrmm/alerts/tests.py @@ -1,15 +1,17 @@ from datetime import datetime, timedelta -from unittest.mock import patch from itertools import cycle +from unittest.mock import patch -from core.models import CoreSettings from django.conf import settings from django.utils import timezone as djangotime from model_bakery import baker, seq -from tacticalrmm.test import TacticalTestCase from alerts.tasks import cache_agents_alert_template -from agents.tasks import handle_agents_task +from autotasks.models import TaskResult +from core.tasks import cache_db_fields_task, resolve_alerts_task +from core.utils import get_core_settings +from tacticalrmm.constants import AgentMonType, AlertSeverity, AlertType, CheckStatus +from tacticalrmm.test import TacticalTestCase from .models import Alert, AlertTemplate from .serializers import ( @@ -38,14 +40,14 @@ def test_get_alerts(self): "alerts.Alert", agent=agent, alert_time=seq(datetime.now(), timedelta(days=15)), - severity="warning", + severity=AlertSeverity.WARNING, _quantity=3, ) baker.make( "alerts.Alert", assigned_check=check, alert_time=seq(datetime.now(), timedelta(days=15)), - severity="error", + severity=AlertSeverity.ERROR, _quantity=7, ) baker.make( @@ -69,8 +71,8 @@ def test_get_alerts(self): 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) # type: ignore - self.assertEquals(resp.data["alerts_count"], 10) # type: ignore + 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 @@ -115,7 +117,7 @@ def test_get_alerts(self): for req in data: resp = self.client.patch(url, req["filter"], format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), req["count"]) # type: ignore + self.assertEqual(len(resp.data), req["count"]) self.check_not_authenticated("patch", url) @@ -141,13 +143,13 @@ def test_get_alert(self): self.assertEqual(resp.status_code, 404) alert = baker.make("alerts.Alert") - url = f"/alerts/{alert.pk}/" # type: ignore + url = f"/alerts/{alert.pk}/" resp = self.client.get(url, format="json") serializer = AlertSerializer(alert) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) @@ -158,7 +160,7 @@ def test_update_alert(self): alert = baker.make("alerts.Alert", resolved=False, snoozed=False) - url = f"/alerts/{alert.pk}/" # type: ignore + url = f"/alerts/{alert.pk}/" # test resolving alert data = { @@ -166,30 +168,30 @@ def test_update_alert(self): } resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertTrue(Alert.objects.get(pk=alert.pk).resolved) # type: ignore - self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on) # type: ignore + self.assertTrue(Alert.objects.get(pk=alert.pk).resolved) + self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on) # test snoozing alert - data = {"type": "snooze", "snooze_days": "30"} # type: ignore + data = {"type": "snooze", "snooze_days": "30"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore - self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore + self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed) + self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until) # test snoozing alert without snooze_days - data = {"type": "snooze"} # type: ignore + data = {"type": "snooze"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) # test unsnoozing alert - data = {"type": "unsnooze"} # type: ignore + data = {"type": "unsnooze"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore - self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore + self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed) + self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until) # test invalid type - data = {"type": "invalid"} # type: ignore + data = {"type": "invalid"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) @@ -203,11 +205,11 @@ def test_delete_alert(self): alert = baker.make("alerts.Alert") # test delete alert - url = f"/alerts/{alert.pk}/" # type: ignore + url = f"/alerts/{alert.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertFalse(Alert.objects.filter(pk=alert.pk).exists()) # type: ignore + self.assertFalse(Alert.objects.filter(pk=alert.pk).exists()) self.check_not_authenticated("delete", url) def test_bulk_alert_actions(self): @@ -229,7 +231,7 @@ def test_bulk_alert_actions(self): # test bulk snoozing alerts data = { "bulk_action": "snooze", - "alerts": [alert.pk for alert in alerts], # type: ignore + "alerts": [alert.pk for alert in alerts], "snooze_days": "30", } resp = self.client.post(url, data, format="json") @@ -237,7 +239,7 @@ def test_bulk_alert_actions(self): self.assertFalse(Alert.objects.filter(snoozed=False).exists()) # test bulk resolving alerts - data = {"bulk_action": "resolve", "alerts": [alert.pk for alert in alerts]} # type: ignore + data = {"bulk_action": "resolve", "alerts": [alert.pk for alert in alerts]} resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -252,7 +254,7 @@ def test_get_alert_templates(self): serializer = AlertTemplateSerializer(alert_templates, many=True) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) @@ -274,13 +276,13 @@ def test_get_alert_template(self): self.assertEqual(resp.status_code, 404) alert_template = baker.make("alerts.AlertTemplate") - url = f"/alerts/templates/{alert_template.pk}/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/" resp = self.client.get(url, format="json") serializer = AlertTemplateSerializer(alert_template) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) @@ -291,7 +293,7 @@ def test_update_alert_template(self): alert_template = baker.make("alerts.AlertTemplate") - url = f"/alerts/templates/{alert_template.pk}/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/" # test data data = { @@ -316,11 +318,11 @@ def test_delete_alert_template(self): alert_template = baker.make("alerts.AlertTemplate") # test delete alert - url = f"/alerts/templates/{alert_template.pk}/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertFalse(AlertTemplate.objects.filter(pk=alert_template.pk).exists()) # type: ignore + self.assertFalse(AlertTemplate.objects.filter(pk=alert_template.pk).exists()) self.check_not_authenticated("delete", url) @@ -330,22 +332,22 @@ def test_alert_template_related(self): baker.make("clients.Client", alert_template=alert_template, _quantity=2) baker.make("clients.Site", alert_template=alert_template, _quantity=3) baker.make("automation.Policy", alert_template=alert_template) - core = CoreSettings.objects.first() - core.alert_template = alert_template # type: ignore - core.save() # type: ignore + core = get_core_settings() + core.alert_template = alert_template + core.save() - url = f"/alerts/templates/{alert_template.pk}/related/" # type: ignore + url = f"/alerts/templates/{alert_template.pk}/related/" resp = self.client.get(url, format="json") serializer = AlertTemplateRelationSerializer(alert_template) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore - self.assertEqual(len(resp.data["policies"]), 1) # type: ignore - self.assertEqual(len(resp.data["clients"]), 2) # type: ignore - self.assertEqual(len(resp.data["sites"]), 3) # type: ignore + self.assertEqual(resp.data, serializer.data) + self.assertEqual(len(resp.data["policies"]), 1) + self.assertEqual(len(resp.data["clients"]), 2) + self.assertEqual(len(resp.data["sites"]), 3) self.assertTrue( - AlertTemplate.objects.get(pk=alert_template.pk).is_default_template # type: ignore + AlertTemplate.objects.get(pk=alert_template.pk).is_default_template ) @@ -377,21 +379,22 @@ def test_unsnooze_alert_task(self): self.assertFalse( Alert.objects.filter( - pk__in=[alert.pk for alert in not_snoozed], snoozed=False # type: ignore + pk__in=[alert.pk for alert in not_snoozed], snoozed=False ).exists() ) self.assertTrue( Alert.objects.filter( - pk__in=[alert.pk for alert in snoozed], snoozed=False # type: ignore + pk__in=[alert.pk for alert in snoozed], snoozed=False ).exists() ) def test_agent_gets_correct_alert_template(self): - - core = CoreSettings.objects.first() + core = get_core_settings() # setup data - workstation = baker.make_recipe("agents.agent", monitoring_type="workstation") - server = baker.make_recipe("agents.agent", monitoring_type="server") + workstation = baker.make_recipe( + "agents.agent", monitoring_type=AgentMonType.WORKSTATION + ) + server = baker.make_recipe("agents.agent", monitoring_type=AgentMonType.SERVER) policy = baker.make("automation.Policy", active=True) @@ -402,30 +405,30 @@ def test_agent_gets_correct_alert_template(self): self.assertFalse(server.set_alert_template()) # assign first Alert Template as to a policy and apply it as default - policy.alert_template = alert_templates[0] # type: ignore - policy.save() # type: ignore - core.workstation_policy = policy # type: ignore - core.server_policy = policy # type: ignore - core.save() # type: ignore + policy.alert_template = alert_templates[0] + policy.save() + core.workstation_policy = policy + core.server_policy = policy + core.save() - self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore - self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore + 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] # type: ignore - core.save() # type: ignore + core.alert_template = alert_templates[1] + core.save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk) # type: ignore + 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] # type: ignore - server.client.alert_template = alert_templates[2] # type: ignore + workstation.client.alert_template = alert_templates[2] + server.client.alert_template = alert_templates[2] workstation.client.save() server.client.save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) # type: ignore + 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 @@ -433,17 +436,17 @@ def test_agent_gets_correct_alert_template(self): workstation.client.save() server.client.save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore + 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] # type: ignore - server.site.alert_template = alert_templates[3] # type: ignore + workstation.site.alert_template = alert_templates[3] + server.site.alert_template = alert_templates[3] workstation.site.save() server.site.save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore + 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 @@ -451,8 +454,8 @@ def test_agent_gets_correct_alert_template(self): workstation.site.save() server.site.save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore + 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 @@ -460,35 +463,35 @@ def test_agent_gets_correct_alert_template(self): workstation.save() server.save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore + 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 # type: ignore - alert_templates[0].save() # type: ignore + alert_templates[0].is_active = False + alert_templates[0].save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore + 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]) # type: ignore + alert_templates[3].excluded_agents.set([workstation.pk]) - self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore + 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 # type: ignore - alert_templates[2].save() # type: ignore + alert_templates[2].exclude_workstations = True + alert_templates[2].save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) # type: ignore + 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 # type: ignore - alert_templates[3].save() # type: ignore + alert_templates[3].exclude_servers = True + alert_templates[3].save() - self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore - self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) # type: ignore + 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") @@ -515,7 +518,6 @@ def test_handle_agent_alerts( agent_recovery_email_task, agent_recovery_sms_task, ) - from alerts.models import Alert agent_dashboard_alert = baker.make_recipe("agents.overdue_agent") @@ -523,7 +525,7 @@ def test_handle_agent_alerts( # 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 +576,22 @@ def test_handle_agent_alerts( 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 +632,7 @@ def test_handle_agent_alerts( # 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 @@ -665,17 +667,15 @@ def test_handle_agent_alerts( # test resolved alerts # alter the alert template to email and test on resolved - alert_template_always_email.agent_email_on_resolved = True # type: ignore - alert_template_always_email.save() # type: ignore - alert_template_always_text.agent_text_on_resolved = True # type: ignore - alert_template_always_text.save() # type: ignore + alert_template_always_email.agent_email_on_resolved = True + alert_template_always_email.save() + alert_template_always_text.agent_text_on_resolved = True + alert_template_always_text.save() agent_template_text = Agent.objects.get(pk=agent_template_text.pk) agent_template_email = Agent.objects.get(pk=agent_template_email.pk) # have the two agents checkin - url = "/api/v3/checkin/" - agent_template_text.version = settings.LATEST_AGENT_VER agent_template_text.last_seen = djangotime.now() agent_template_text.save() @@ -684,7 +684,8 @@ def test_handle_agent_alerts( agent_template_email.last_seen = djangotime.now() agent_template_email.save() - handle_agents_task() + cache_db_fields_task() + resolve_alerts_task() recovery_sms.assert_called_with( pk=Alert.objects.get(agent=agent_template_text).pk @@ -718,7 +719,8 @@ def test_handle_check_alerts( send_email, sleep, ): - from checks.models import Check + from alerts.tasks import cache_agents_alert_template + from checks.models import Check, CheckResult from checks.tasks import ( handle_check_email_alert_task, handle_check_sms_alert_task, @@ -726,8 +728,6 @@ def test_handle_check_alerts( handle_resolved_check_sms_alert_task, ) - from alerts.tasks import cache_agents_alert_template - # create test data agent = baker.make_recipe("agents.agent") agent_no_settings = baker.make_recipe("agents.agent") @@ -753,7 +753,7 @@ def test_handle_check_alerts( "alerts.AlertTemplate", is_active=True, check_always_email=True, - check_email_alert_severity=["warning"], + check_email_alert_severity=[AlertSeverity.WARNING], ) agent_template_email.client.alert_template = alert_template_email agent_template_email.client.save() @@ -764,8 +764,12 @@ def test_handle_check_alerts( is_active=True, check_always_alert=True, check_always_text=True, - check_dashboard_alert_severity=["info", "warning", "error"], - check_text_alert_severity=["error"], + check_dashboard_alert_severity=[ + AlertSeverity.INFO, + AlertSeverity.WARNING, + AlertSeverity.ERROR, + ], + check_text_alert_severity=[AlertSeverity.ERROR], ) agent_template_dashboard_text.client.alert_template = ( alert_template_dashboard_text @@ -784,36 +788,66 @@ def test_handle_check_alerts( email_alert=True, text_alert=True, dashboard_alert=True, - alert_severity="warning", + ) + check_agent_result = baker.make( + "checks.CheckResult", + assigned_check=check_agent, + agent=agent, + alert_severity=AlertSeverity.WARNING, ) check_template_email = baker.make_recipe( - "checks.cpuload_check", agent=agent_template_email, history=[50, 40, 30] + "checks.cpuload_check", agent=agent_template_email + ) + check_template_email_result = baker.make( + "checks.CheckResult", + assigned_check=check_template_email, + agent=agent_template_email, + history=[50, 40, 30], ) check_template_dashboard_text = baker.make_recipe( "checks.memory_check", agent=agent_template_dashboard_text, + ) + check_template_dashboard_text_result = baker.make( + "checks.CheckResult", + assigned_check=check_template_dashboard_text, + agent=agent_template_dashboard_text, history=[50, 40, 30], ) check_template_blank = baker.make_recipe( "checks.ping_check", agent=agent_template_blank ) + check_template_blank_result = baker.make( + "checks.CheckResult", + agent=agent_template_blank, + assigned_check=check_template_blank, + ) check_no_settings = baker.make_recipe( "checks.script_check", agent=agent_no_settings ) + check_no_settings_result = baker.make( + "checks.CheckResult", + agent=agent_no_settings, + assigned_check=check_no_settings, + ) # update alert template and pull new checks from DB cache_agents_alert_template() - check_template_email = Check.objects.get(pk=check_template_email.pk) - check_template_dashboard_text = Check.objects.get( - pk=check_template_dashboard_text.pk + check_template_email_result = CheckResult.objects.get( + pk=check_template_email_result.pk + ) + check_template_dashboard_text_result = CheckResult.objects.get( + pk=check_template_dashboard_text_result.pk + ) + check_template_blank_result = CheckResult.objects.get( + pk=check_template_blank_result.pk ) - check_template_blank = Check.objects.get(pk=check_template_blank.pk) # test agent with check that has alert settings - check_agent.alert_severity = "warning" - check_agent.status = "failing" + check_agent_result.alert_severity = AlertSeverity.WARNING + check_agent_result.status = CheckStatus.FAILING - Alert.handle_alert_failure(check_agent) + Alert.handle_alert_failure(check_agent_result) # alert should have been created and sms, email notifications sent self.assertTrue(Alert.objects.filter(assigned_check=check_agent).exists()) @@ -841,9 +875,11 @@ def test_handle_check_alerts( send_sms.reset_mock() # test check with an agent that has an email always alert template - Alert.handle_alert_failure(check_template_email) + Alert.handle_alert_failure(check_template_email_result) - self.assertTrue(Alert.objects.filter(assigned_check=check_template_email)) + self.assertTrue( + Alert.objects.filter(assigned_check=check_template_email).exists() + ) alertpk = Alert.objects.get(assigned_check=check_template_email).pk outage_sms.assert_not_called() outage_email.assert_called_with(pk=alertpk, alert_interval=0) @@ -859,7 +895,7 @@ def test_handle_check_alerts( send_email.reset_mock() # test check with an agent that has an email always alert template - Alert.handle_alert_failure(check_template_dashboard_text) + Alert.handle_alert_failure(check_template_dashboard_text_result) self.assertTrue( Alert.objects.filter(assigned_check=check_template_dashboard_text).exists() @@ -869,12 +905,12 @@ def test_handle_check_alerts( # should only trigger when alert with severity of error outage_sms.assert_not_called - # update check alert seveity to error - check_template_dashboard_text.alert_severity = "error" - check_template_dashboard_text.save() + # update check alert severity to error + check_template_dashboard_text_result.alert_severity = AlertSeverity.ERROR + check_template_dashboard_text_result.save() # now should trigger alert - Alert.handle_alert_failure(check_template_dashboard_text) + Alert.handle_alert_failure(check_template_dashboard_text_result) outage_sms.assert_called_with(pk=alertpk, alert_interval=0) outage_sms.reset_mock() @@ -890,14 +926,14 @@ def test_handle_check_alerts( send_sms.reset_mock() # test check with an agent that has a blank alert template - Alert.handle_alert_failure(check_template_blank) + Alert.handle_alert_failure(check_template_blank_result) self.assertFalse( Alert.objects.filter(assigned_check=check_template_blank).exists() ) # test check that has no template and no settings - Alert.handle_alert_failure(check_no_settings) + Alert.handle_alert_failure(check_no_settings_result) self.assertFalse( Alert.objects.filter(assigned_check=check_no_settings).exists() @@ -906,19 +942,19 @@ def test_handle_check_alerts( # test periodic notifications # make sure a failing check won't trigger another notification and only create a single alert - Alert.handle_alert_failure(check_template_email) + Alert.handle_alert_failure(check_template_email_result) send_email.assert_not_called() send_sms.assert_not_called() - self.assertEquals( + self.assertEqual( Alert.objects.filter(assigned_check=check_template_email).count(), 1 ) - alert_template_email.check_periodic_alert_days = 1 # type: ignore - alert_template_email.save() # type: ignore + alert_template_email.check_periodic_alert_days = 1 + alert_template_email.save() - alert_template_dashboard_text.check_periodic_alert_days = 1 # type: ignore - alert_template_dashboard_text.save() # type: ignore + alert_template_dashboard_text.check_periodic_alert_days = 1 + alert_template_dashboard_text.save() # set last email time for alert in the past alert_email = Alert.objects.get(assigned_check=check_template_email) @@ -931,14 +967,16 @@ def test_handle_check_alerts( alert_sms.save() # refresh checks to get alert template changes - check_template_email = Check.objects.get(pk=check_template_email.pk) - check_template_dashboard_text = Check.objects.get( - pk=check_template_dashboard_text.pk + check_template_email_result = CheckResult.objects.get( + pk=check_template_email_result.pk + ) + check_template_dashboard_text_result = CheckResult.objects.get( + pk=check_template_dashboard_text_result.pk ) check_template_blank = Check.objects.get(pk=check_template_blank.pk) - Alert.handle_alert_failure(check_template_email) - Alert.handle_alert_failure(check_template_dashboard_text) + Alert.handle_alert_failure(check_template_email_result) + Alert.handle_alert_failure(check_template_dashboard_text_result) outage_email.assert_called_with(pk=alert_email.pk, alert_interval=1) outage_sms.assert_called_with(pk=alert_sms.pk, alert_interval=1) @@ -946,7 +984,7 @@ def test_handle_check_alerts( outage_sms.reset_mock() # test resolving alerts - Alert.handle_alert_resolve(check_agent) + Alert.handle_alert_resolve(check_agent_result) self.assertTrue(Alert.objects.get(assigned_check=check_agent).resolved) self.assertTrue(Alert.objects.get(assigned_check=check_agent).resolved_on) @@ -955,26 +993,30 @@ def test_handle_check_alerts( resolved_email.assert_not_called() # test resolved notifications - alert_template_email.check_email_on_resolved = True # type: ignore - alert_template_email.save() # type: ignore + alert_template_email.check_email_on_resolved = True + alert_template_email.save() - alert_template_dashboard_text.check_text_on_resolved = True # type: ignore - alert_template_dashboard_text.save() # type: ignore + alert_template_dashboard_text.check_text_on_resolved = True + alert_template_dashboard_text.save() # refresh checks to get alert template changes - check_template_email = Check.objects.get(pk=check_template_email.pk) - check_template_dashboard_text = Check.objects.get( - pk=check_template_dashboard_text.pk + check_template_email_result = CheckResult.objects.get( + pk=check_template_email_result.pk + ) + check_template_dashboard_text_result = CheckResult.objects.get( + pk=check_template_dashboard_text_result.pk + ) + check_template_blank_result = CheckResult.objects.get( + pk=check_template_blank_result.pk ) - check_template_blank = Check.objects.get(pk=check_template_blank.pk) - Alert.handle_alert_resolve(check_template_email) + Alert.handle_alert_resolve(check_template_email_result) resolved_email.assert_called_with(pk=alert_email.pk) resolved_sms.assert_not_called() resolved_email.reset_mock() - Alert.handle_alert_resolve(check_template_dashboard_text) + Alert.handle_alert_resolve(check_template_dashboard_text_result) resolved_sms.assert_called_with(pk=alert_sms.pk) resolved_email.assert_not_called() @@ -1003,7 +1045,7 @@ def test_handle_task_alerts( send_email, sleep, ): - from autotasks.models import AutomatedTask + from alerts.tasks import cache_agents_alert_template from autotasks.tasks import ( handle_resolved_task_email_alert, handle_resolved_task_sms_alert, @@ -1011,8 +1053,6 @@ def test_handle_task_alerts( handle_task_sms_alert, ) - from alerts.tasks import cache_agents_alert_template - # create test data agent = baker.make_recipe("agents.agent") agent_no_settings = baker.make_recipe("agents.agent") @@ -1025,7 +1065,7 @@ def test_handle_task_alerts( "alerts.AlertTemplate", is_active=True, task_always_email=True, - task_email_alert_severity=["warning"], + task_email_alert_severity=[AlertSeverity.WARNING], ) agent_template_email.client.alert_template = alert_template_email agent_template_email.client.save() @@ -1036,8 +1076,12 @@ def test_handle_task_alerts( is_active=True, task_always_alert=True, task_always_text=True, - task_dashboard_alert_severity=["info", "warning", "error"], - task_text_alert_severity=["error"], + task_dashboard_alert_severity=[ + AlertSeverity.INFO, + AlertSeverity.WARNING, + AlertSeverity.ERROR, + ], + task_text_alert_severity=[AlertSeverity.ERROR], ) agent_template_dashboard_text.client.alert_template = ( alert_template_dashboard_text @@ -1056,35 +1100,60 @@ def test_handle_task_alerts( email_alert=True, text_alert=True, dashboard_alert=True, - alert_severity="warning", + alert_severity=AlertSeverity.WARNING, + ) + task_agent_result = baker.make( + "autotasks.TaskResult", agent=agent, task=task_agent ) task_template_email = baker.make( "autotasks.AutomatedTask", agent=agent_template_email, - alert_severity="warning", + alert_severity=AlertSeverity.WARNING, + ) + task_template_email_result = baker.make( + "autotasks.TaskResult", agent=agent_template_email, task=task_template_email ) task_template_dashboard_text = baker.make( "autotasks.AutomatedTask", agent=agent_template_dashboard_text, - alert_severity="info", + alert_severity=AlertSeverity.INFO, + ) + task_template_dashboard_text_result = baker.make( + "autotasks.TaskResult", + agent=agent_template_dashboard_text, + task=task_template_dashboard_text, ) task_template_blank = baker.make( "autotasks.AutomatedTask", agent=agent_template_blank, - alert_severity="error", + alert_severity=AlertSeverity.ERROR, + ) + task_template_blank_result = baker.make( + "autotasks.TaskResult", agent=agent_template_blank, task=task_template_blank ) task_no_settings = baker.make( - "autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning" + "autotasks.AutomatedTask", + agent=agent_no_settings, + alert_severity=AlertSeverity.WARNING, + ) + task_no_settings_result = baker.make( + "autotasks.TaskResult", agent=agent_no_settings, task=task_no_settings ) # update alert template and pull new checks from DB cache_agents_alert_template() - task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore - task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore - task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore + task_template_email_result = TaskResult.objects.get( + pk=task_template_email_result.pk + ) + task_template_dashboard_text_result = TaskResult.objects.get( + pk=task_template_dashboard_text_result.pk + ) + task_template_blank_result = TaskResult.objects.get( + pk=task_template_blank_result.pk + ) # test agent with task that has alert settings - Alert.handle_alert_failure(task_agent) # type: ignore + Alert.handle_alert_failure(task_agent_result) # alert should have been created and sms, email notifications sent self.assertTrue(Alert.objects.filter(assigned_task=task_agent).exists()) @@ -1112,9 +1181,11 @@ def test_handle_task_alerts( send_sms.reset_mock() # test task with an agent that has an email always alert template - Alert.handle_alert_failure(task_template_email) # type: ignore + Alert.handle_alert_failure(task_template_email_result) - self.assertTrue(Alert.objects.filter(assigned_task=task_template_email)) + self.assertTrue( + Alert.objects.filter(assigned_task=task_template_email).exists() + ) alertpk = Alert.objects.get(assigned_task=task_template_email).pk outage_sms.assert_not_called() outage_email.assert_called_with(pk=alertpk, alert_interval=0) @@ -1130,7 +1201,7 @@ def test_handle_task_alerts( send_email.reset_mock() # test task with an agent that has an email always alert template - Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore + Alert.handle_alert_failure(task_template_dashboard_text_result) self.assertTrue( Alert.objects.filter(assigned_task=task_template_dashboard_text).exists() @@ -1141,11 +1212,15 @@ def test_handle_task_alerts( outage_sms.assert_not_called # update task alert seveity to error - task_template_dashboard_text.alert_severity = "error" # type: ignore - task_template_dashboard_text.save() # type: ignore + task_template_dashboard_text.alert_severity = AlertSeverity.ERROR + task_template_dashboard_text.save() # now should trigger alert - Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore + # get fresh task result + task_template_dashboard_text_result = TaskResult.objects.get( + pk=task_template_dashboard_text_result.pk + ) + Alert.handle_alert_failure(task_template_dashboard_text_result) outage_sms.assert_called_with(pk=alertpk, alert_interval=0) outage_sms.reset_mock() @@ -1161,33 +1236,33 @@ def test_handle_task_alerts( send_sms.reset_mock() # test task with an agent that has a blank alert template - Alert.handle_alert_failure(task_template_blank) # type: ignore + Alert.handle_alert_failure(task_template_blank_result) self.assertFalse( Alert.objects.filter(assigned_task=task_template_blank).exists() ) # test task that has no template and no settings - Alert.handle_alert_failure(task_no_settings) # type: ignore + Alert.handle_alert_failure(task_no_settings_result) self.assertFalse(Alert.objects.filter(assigned_task=task_no_settings).exists()) # test periodic notifications # make sure a failing task won't trigger another notification and only create a single alert - Alert.handle_alert_failure(task_template_email) # type: ignore + Alert.handle_alert_failure(task_template_email_result) send_email.assert_not_called() send_sms.assert_not_called() - self.assertEquals( + self.assertEqual( Alert.objects.filter(assigned_task=task_template_email).count(), 1 ) - alert_template_email.task_periodic_alert_days = 1 # type: ignore - alert_template_email.save() # type: ignore + alert_template_email.task_periodic_alert_days = 1 + alert_template_email.save() - alert_template_dashboard_text.task_periodic_alert_days = 1 # type: ignore - alert_template_dashboard_text.save() # type: ignore + alert_template_dashboard_text.task_periodic_alert_days = 1 + alert_template_dashboard_text.save() # set last email time for alert in the past alert_email = Alert.objects.get(assigned_task=task_template_email) @@ -1199,13 +1274,19 @@ def test_handle_task_alerts( alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20) alert_sms.save() - # refresh automated tasks to get new alert templates - task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore - task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore - task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore + # refresh task results to get new alert templates + task_template_email_result = TaskResult.objects.get( + pk=task_template_email_result.pk + ) + task_template_dashboard_text_result = TaskResult.objects.get( + pk=task_template_dashboard_text_result.pk + ) + task_template_blank_result = TaskResult.objects.get( + pk=task_template_blank_result.pk + ) - Alert.handle_alert_failure(task_template_email) # type: ignore - Alert.handle_alert_failure(task_template_dashboard_text) # type: ignore + Alert.handle_alert_failure(task_template_email_result) + Alert.handle_alert_failure(task_template_dashboard_text_result) outage_email.assert_called_with(pk=alert_email.pk, alert_interval=1) outage_sms.assert_called_with(pk=alert_sms.pk, alert_interval=1) @@ -1213,7 +1294,7 @@ def test_handle_task_alerts( outage_sms.reset_mock() # test resolving alerts - Alert.handle_alert_resolve(task_agent) # type: ignore + Alert.handle_alert_resolve(task_agent_result) self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved) self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved_on) @@ -1222,24 +1303,30 @@ def test_handle_task_alerts( resolved_email.assert_not_called() # test resolved notifications - alert_template_email.task_email_on_resolved = True # type: ignore - alert_template_email.save() # type: ignore + alert_template_email.task_email_on_resolved = True + alert_template_email.save() - alert_template_dashboard_text.task_text_on_resolved = True # type: ignore - alert_template_dashboard_text.save() # type: ignore + alert_template_dashboard_text.task_text_on_resolved = True + alert_template_dashboard_text.save() - # refresh automated tasks to get new alert templates - task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk) # type: ignore - task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk) # type: ignore - task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk) # type: ignore + # refresh atask results to get new alert templates + task_template_email_result = TaskResult.objects.get( + pk=task_template_email_result.pk + ) + task_template_dashboard_text_result = TaskResult.objects.get( + pk=task_template_dashboard_text_result.pk + ) + task_template_blank_result = TaskResult.objects.get( + pk=task_template_blank_result.pk + ) - Alert.handle_alert_resolve(task_template_email) # type: ignore + Alert.handle_alert_resolve(task_template_email_result) resolved_email.assert_called_with(pk=alert_email.pk) resolved_sms.assert_not_called() resolved_email.reset_mock() - Alert.handle_alert_resolve(task_template_dashboard_text) # type: ignore + Alert.handle_alert_resolve(task_template_dashboard_text_result) resolved_sms.assert_called_with(pk=alert_sms.pk) resolved_email.assert_not_called() @@ -1254,8 +1341,6 @@ def test_handle_task_alerts( @patch("core.models.TwClient") @patch("smtplib.SMTP") def test_override_core_settings(self, smtp, sms): - from core.models import CoreSettings - # setup data alert_template = baker.make( "alerts.AlertTemplate", @@ -1264,18 +1349,18 @@ def test_override_core_settings(self, smtp, sms): email_from="from@email.com", ) - core = CoreSettings.objects.first() - core.smtp_host = "test.test.com" # type: ignore - core.smtp_port = 587 # type: ignore - core.smtp_recipients = ["recipient@test.com"] # type: ignore - core.twilio_account_sid = "test" # type: ignore - core.twilio_auth_token = "1234123412341234" # type: ignore - core.sms_alert_recipients = ["+1234567890"] # type: ignore + core = get_core_settings() + core.smtp_host = "test.test.com" + core.smtp_port = 587 + core.email_alert_recipients = ["recipient@test.com"] + core.twilio_account_sid = "test" + core.twilio_auth_token = "1234123412341234" + core.sms_alert_recipients = ["+1234567890"] # test sending email with alert template settings - core.send_mail("Test", "Test", alert_template=alert_template) # type: ignore + core.send_mail("Test", "Test", alert_template=alert_template) - core.send_sms("Test", alert_template=alert_template) # type: ignore + core.send_sms("Test", alert_template=alert_template) @patch("agents.models.Agent.nats_cmd") @patch("agents.tasks.agent_outage_sms_task.delay") @@ -1285,7 +1370,7 @@ def test_override_core_settings(self, smtp, sms): def test_alert_actions( self, recovery_sms, recovery_email, outage_email, outage_sms, nats_cmd ): - + from agents.models import AgentHistory from agents.tasks import agent_outages_task # Setup cmd mock @@ -1311,9 +1396,12 @@ def test_alert_actions( agent_script_actions=False, action=failure_action, action_timeout=30, + action_args=["hello", "world"], + action_env_vars=["hello=world", "foo=bar"], resolved_action=resolved_action, resolved_action_timeout=35, resolved_action_args=["nice_arg"], + resolved_action_env_vars=["resolved=action", "env=vars"], ) agent.client.alert_template = alert_template agent.client.save() @@ -1325,8 +1413,8 @@ def test_alert_actions( # should not have been called since agent_script_actions is set to False nats_cmd.assert_not_called() - alert_template.agent_script_actions = True # type: ignore - alert_template.save() # type: ignore + alert_template.agent_script_actions = True + alert_template.save() agent_outages_task() @@ -1334,8 +1422,11 @@ def test_alert_actions( data = { "func": "runscriptfull", "timeout": 30, - "script_args": [], + "script_args": ["hello", "world"], "payload": {"code": failure_action.code, "shell": failure_action.shell}, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], + "id": AgentHistory.objects.last().pk, # type: ignore } nats_cmd.assert_called_with(data, timeout=30, wait=True) @@ -1355,7 +1446,8 @@ def test_alert_actions( agent.last_seen = djangotime.now() agent.save() - handle_agents_task() + cache_db_fields_task() + resolve_alerts_task() # this is what data should be data = { @@ -1363,6 +1455,9 @@ def test_alert_actions( "timeout": 35, "script_args": ["nice_arg"], "payload": {"code": resolved_action.code, "shell": resolved_action.shell}, + "run_as_user": False, + "env_vars": ["resolved=action", "env=vars"], + "id": AgentHistory.objects.last().pk, # type: ignore } nats_cmd.assert_called_with(data, timeout=35, wait=True) @@ -1381,8 +1476,8 @@ def test_parse_script_args(self): # test default value self.assertEqual( - ["-Parameter", f"-Another '{alert.id}'"], # type: ignore - alert.parse_script_args(args=args), # type: ignore + ["-Parameter", f"-Another '{alert.id}'"], + alert.parse_script_args(args=args), ) def test_prune_resolved_alerts(self): @@ -1402,13 +1497,13 @@ def test_prune_resolved_alerts(self): ) days = 0 - for alert in resolved_alerts: # type: ignore + for alert in resolved_alerts: alert.alert_time = djangotime.now() - djangotime.timedelta(days=days) alert.save() days = days + 5 days = 0 - for alert in alerts: # type: ignore + for alert in alerts: alert.alert_time = djangotime.now() - djangotime.timedelta(days=days) alert.save() days = days + 5 @@ -1422,7 +1517,7 @@ def test_prune_resolved_alerts(self): class TestAlertPermissions(TacticalTestCase): def setUp(self): self.setup_coresettings() - self.client_setup() + self.setup_client() def test_get_alerts_permissions(self): agent = baker.make_recipe("agents.agent") @@ -1432,25 +1527,33 @@ def test_get_alerts_permissions(self): checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3) tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3) baker.make( - "alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3 + "alerts.Alert", + alert_type=AlertType.TASK, + agent=cycle(agents), + assigned_task=cycle(tasks), + _quantity=3, ) baker.make( "alerts.Alert", - alert_type="check", + alert_type=AlertType.CHECK, + agent=cycle(agents), assigned_check=cycle(checks), _quantity=3, ) baker.make( - "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3 + "alerts.Alert", + alert_type=AlertType.AVAILABILITY, + agent=cycle(agents), + _quantity=3, ) - baker.make("alerts.Alert", alert_type="custom", _quantity=4) + baker.make("alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4) # test super user access r = self.check_authorized_superuser("patch", f"{base_url}/") - self.assertEqual(len(r.data), 13) # type: ignore + self.assertEqual(len(r.data), 13) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) self.check_not_authorized("patch", f"{base_url}/") @@ -1459,23 +1562,23 @@ def test_get_alerts_permissions(self): user.role.save() r = self.check_authorized("patch", f"{base_url}/") - self.assertEqual(len(r.data), 13) # type: ignore + self.assertEqual(len(r.data), 13) # test limiting to client user.role.can_view_clients.set([agent.client]) r = self.check_authorized("patch", f"{base_url}/") - self.assertEqual(len(r.data), 7) # type: ignore + self.assertEqual(len(r.data), 7) # test limiting to site user.role.can_view_clients.clear() user.role.can_view_sites.set([agent1.site]) r = self.client.patch(f"{base_url}/") - self.assertEqual(len(r.data), 7) # type: ignore + self.assertEqual(len(r.data), 7) # test limiting to site and client user.role.can_view_clients.set([agent2.client]) r = self.client.patch(f"{base_url}/") - self.assertEqual(len(r.data), 10) # type: ignore + self.assertEqual(len(r.data), 10) @patch("alerts.models.Alert.delete", return_value=1) def test_edit_delete_get_alert_permissions(self, delete): @@ -1486,18 +1589,28 @@ def test_edit_delete_get_alert_permissions(self, delete): checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3) tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3) alert_tasks = baker.make( - "alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3 + "alerts.Alert", + alert_type=AlertType.TASK, + agent=cycle(agents), + assigned_task=cycle(tasks), + _quantity=3, ) alert_checks = baker.make( "alerts.Alert", - alert_type="check", + alert_type=AlertType.CHECK, + agent=cycle(agents), assigned_check=cycle(checks), _quantity=3, ) alert_agents = baker.make( - "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3 + "alerts.Alert", + alert_type=AlertType.AVAILABILITY, + agent=cycle(agents), + _quantity=3, + ) + alert_custom = baker.make( + "alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4 ) - alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4) # alert task url task_url = f"{base_url}/{alert_tasks[0].id}/" # for agent @@ -1518,8 +1631,7 @@ def test_edit_delete_get_alert_permissions(self, delete): unauthorized_task_url, ] - for method in ["get", "put", "delete"]: - + for method in ("get", "put", "delete"): # test superuser access for url in authorized_urls: self.check_authorized_superuser(method, url) @@ -1528,7 +1640,7 @@ def test_edit_delete_get_alert_permissions(self, delete): self.check_authorized_superuser(method, url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role for url in authorized_urls: @@ -1569,3 +1681,76 @@ def test_edit_delete_get_alert_permissions(self, delete): for url in unauthorized_urls: self.check_authorized(method, url) + + def test_handling_multiple_availability_alerts_returned(self): + agent = baker.make_recipe("agents.agent") + alerts = baker.make( + "alerts.Alert", + alert_type=AlertType.AVAILABILITY, + agent=agent, + resolved=False, + _quantity=3, + ) + + alert = Alert.create_or_return_availability_alert(agent, skip_create=True) + + # make sure last alert is returned + self.assertEqual(alert, alerts[-1]) + + # make sure only 1 alert is not resolved + self.assertEqual( + Alert.objects.filter( + alert_type=AlertType.AVAILABILITY, agent=agent, resolved=False + ).count(), + 1, + ) + + def test_handling_multiple_check_alerts_returned(self): + agent = baker.make_recipe("agents.agent") + check = baker.make_recipe("checks.diskspace_check", agent=agent) + alerts = baker.make( + "alerts.Alert", + alert_type=AlertType.CHECK, + assigned_check=check, + agent=agent, + resolved=False, + _quantity=3, + ) + + alert = Alert.create_or_return_check_alert(check, agent=agent, skip_create=True) + + # make sure last alert is returned + self.assertEqual(alert, alerts[-1]) + + # make sure only 1 alert is not resolved + self.assertEqual( + Alert.objects.filter( + alert_type=AlertType.CHECK, agent=agent, resolved=False + ).count(), + 1, + ) + + def test_handling_multiple_task_alerts_returned(self): + agent = baker.make_recipe("agents.agent") + task = baker.make("autotasks.AutomatedTask", agent=agent) + alerts = baker.make( + "alerts.Alert", + alert_type=AlertType.TASK, + assigned_task=task, + agent=agent, + resolved=False, + _quantity=3, + ) + + alert = Alert.create_or_return_task_alert(task, agent=agent, skip_create=True) + + # make sure last alert is returned + self.assertEqual(alert, alerts[-1]) + + # make sure only 1 alert is not resolved + self.assertEqual( + Alert.objects.filter( + alert_type=AlertType.TASK, agent=agent, resolved=False + ).count(), + 1, + ) diff --git a/api/tacticalrmm/alerts/views.py b/api/tacticalrmm/alerts/views.py index bd988a9d18..88e12e5542 100644 --- a/api/tacticalrmm/alerts/views.py +++ b/api/tacticalrmm/alerts/views.py @@ -7,7 +7,7 @@ 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 @@ -23,15 +23,18 @@ class GetAddAlerts(APIView): permission_classes = [IsAuthenticated, AlertPerms] def patch(self, request): - # top 10 alerts for dashboard icon if "top" in request.data.keys(): - alerts = Alert.objects.filter( - resolved=False, snoozed=False, hidden=False - ).order_by("alert_time")[: int(request.data["top"])] - count = Alert.objects.filter( - resolved=False, snoozed=False, hidden=False - ).count() + alerts = ( + Alert.objects.filter_by_role(request.user) + .filter(resolved=False, snoozed=False, hidden=False) + .order_by("alert_time")[: int(request.data["top"])] + ) + count = ( + Alert.objects.filter_by_role(request.user) + .filter(resolved=False, snoozed=False, hidden=False) + .count() + ) return Response( { "alerts_count": count, @@ -41,13 +44,13 @@ def patch(self, request): elif any( key - in [ + in ( "timeFilter", "clientFilter", "severityFilter", "resolvedFilter", "snoozedFilter", - ] + ) for key in request.data.keys() ): clientFilter = Q() @@ -92,7 +95,7 @@ def patch(self, request): ) alerts = ( - Alert.objects.filter_by_role(request.user) + Alert.objects.filter_by_role(request.user) # type: ignore .filter(clientFilter) .filter(severityFilter) .filter(resolvedFilter) @@ -102,7 +105,7 @@ def patch(self, request): return Response(AlertSerializer(alerts, many=True).data) else: - alerts = Alert.objects.filter_by_role(request.user) + alerts = Alert.objects.filter_by_role(request.user) # type: ignore return Response(AlertSerializer(alerts, many=True).data) def post(self, request): diff --git a/api/tacticalrmm/apiv3/tests/__init__.py b/api/tacticalrmm/apiv3/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tacticalrmm/apiv3/tests.py b/api/tacticalrmm/apiv3/tests/tests.py similarity index 53% rename from api/tacticalrmm/apiv3/tests.py rename to api/tacticalrmm/apiv3/tests/tests.py index c31d1dd2e4..110979bf9f 100644 --- a/api/tacticalrmm/apiv3/tests.py +++ b/api/tacticalrmm/apiv3/tests/tests.py @@ -1,12 +1,8 @@ -import json -import os -from unittest.mock import patch - -from django.conf import settings from django.utils import timezone as djangotime from model_bakery import baker -from autotasks.models import AutomatedTask +from autotasks.models import TaskResult +from tacticalrmm.constants import CustomFieldModel, CustomFieldType, TaskStatus from tacticalrmm.test import TacticalTestCase @@ -17,46 +13,53 @@ def setUp(self): self.agent = baker.make_recipe("agents.agent") def test_get_checks(self): - url = f"/api/v3/{self.agent.agent_id}/checkrunner/" + agent = baker.make_recipe("agents.agent") + url = f"/api/v3/{agent.agent_id}/checkrunner/" # add a check - check1 = baker.make_recipe("checks.ping_check", agent=self.agent) + check1 = baker.make_recipe("checks.ping_check", agent=agent) + check_result1 = baker.make( + "checks.CheckResult", agent=agent, assigned_check=check1 + ) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore - self.assertEqual(len(r.data["checks"]), 1) # type: ignore + self.assertEqual(r.data["check_interval"], self.agent.check_interval) + self.assertEqual(len(r.data["checks"]), 1) # override check run interval check2 = baker.make_recipe( - "checks.ping_check", agent=self.agent, run_interval=20 + "checks.diskspace_check", agent=agent, run_interval=20 + ) + check_result2 = baker.make( + "checks.CheckResult", agent=agent, assigned_check=check2 ) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data["check_interval"], 20) # type: ignore - self.assertEqual(len(r.data["checks"]), 2) # type: ignore + self.assertEqual(len(r.data["checks"]), 2) + self.assertEqual(r.data["check_interval"], 20) # Set last_run on both checks and should return an empty list - check1.last_run = djangotime.now() - check1.save() - check2.last_run = djangotime.now() - check2.save() + check_result1.last_run = djangotime.now() + check_result1.save() + check_result2.last_run = djangotime.now() + check_result2.save() r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data["check_interval"], 20) # type: ignore - self.assertFalse(r.data["checks"]) # type: ignore + self.assertEqual(r.data["check_interval"], 20) + self.assertFalse(r.data["checks"]) # set last_run greater than interval - check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200) - check1.save() - check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200) - check2.save() + check_result1.last_run = djangotime.now() - djangotime.timedelta(seconds=200) + check_result1.save() + check_result2.last_run = djangotime.now() - djangotime.timedelta(seconds=200) + check_result2.save() r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data["check_interval"], 20) # type: ignore - self.assertEquals(len(r.data["checks"]), 2) # type: ignore + self.assertEqual(r.data["check_interval"], 20) + self.assertEqual(len(r.data["checks"]), 2) url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/" r = self.client.get(url) @@ -64,24 +67,6 @@ def test_get_checks(self): 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") @@ -92,9 +77,7 @@ def test_checkrunner_interval(self): ) # add check to agent with check interval set - check = baker.make_recipe( - "checks.ping_check", agent=self.agent, run_interval=30 - ) + baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=30) r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) @@ -104,7 +87,7 @@ def test_checkrunner_interval(self): ) # minimum check run interval is 15 seconds - check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5) + baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5) r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) @@ -130,61 +113,38 @@ def test_run_checks(self): self.assertIsInstance(r.json()["check_interval"], int) self.assertEqual(len(r.json()["checks"]), 15) - @patch("apiv3.views.reload_nats") - def test_agent_recovery(self, reload_nats): - reload_nats.return_value = "ok" - r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/") - self.assertEqual(r.status_code, 404) - - agent = baker.make_recipe("agents.online_agent") - url = f"/api/v3/{agent.agent_id}/recovery/" - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""}) - reload_nats.assert_not_called() - - baker.make("agents.RecoveryAction", agent=agent, mode="mesh") - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""}) - reload_nats.assert_not_called() - - baker.make( - "agents.RecoveryAction", - agent=agent, - mode="command", - command="shutdown /r /t 5 /f", - ) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertEqual( - r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"} - ) - reload_nats.assert_not_called() - - baker.make("agents.RecoveryAction", agent=agent, mode="rpc") - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""}) - reload_nats.assert_called_once() - 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) + script = baker.make("scripts.script") + # setup data + task_actions = [ + {"type": "cmd", "command": "whoami", "timeout": 10, "shell": "cmd"}, + { + "type": "script", + "script": script.id, + "script_args": ["test"], + "timeout": 30, + "env_vars": ["hello=world", "foo=bar"], + }, + { + "type": "script", + "script": 3, + "script_args": [], + "timeout": 30, + "env_vars": ["hello=world", "foo=bar"], + }, + ] + agent = baker.make_recipe("agents.agent") - script = baker.make_recipe("scripts.script") - task = baker.make("autotasks.AutomatedTask", agent=agent, script=script) + task = baker.make("autotasks.AutomatedTask", agent=agent, actions=task_actions) - url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore + url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertEqual(TaskGOGetSerializer(task).data, r.data) # type: ignore def test_task_runner_results(self): from agents.models import AgentCustomField @@ -195,8 +155,9 @@ def test_task_runner_results(self): # setup data agent = baker.make_recipe("agents.agent") task = baker.make("autotasks.AutomatedTask", agent=agent) + task_result = baker.make("autotasks.TaskResult", agent=agent, task=task) - url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore + url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # test passing task data = { @@ -208,7 +169,9 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing") # type: ignore + self.assertTrue( + TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.PASSING + ) # test failing task data = { @@ -220,20 +183,33 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore + self.assertTrue( + TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING + ) # test collector task - text = baker.make("core.CustomField", model="agent", type="text", name="Test") + text = baker.make( + "core.CustomField", + model=CustomFieldModel.AGENT, + type=CustomFieldType.TEXT, + name="Test", + ) boolean = baker.make( - "core.CustomField", model="agent", type="checkbox", name="Test1" + "core.CustomField", + model=CustomFieldModel.AGENT, + type=CustomFieldType.CHECKBOX, + name="Test1", ) multiple = baker.make( - "core.CustomField", model="agent", type="multiple", name="Test2" + "core.CustomField", + model=CustomFieldModel.AGENT, + type=CustomFieldType.MULTIPLE, + name="Test2", ) # test text fields - task.custom_field = text # type: ignore - task.save() # type: ignore + task.custom_field = text + task.save() # test failing failing with stderr data = { @@ -245,7 +221,9 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore + self.assertTrue( + TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING + ) # test saving to text field data = { @@ -257,12 +235,17 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore - self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line") # type: ignore + self.assertEqual( + TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING + ) + self.assertEqual( + AgentCustomField.objects.get(field=text, agent=task.agent).value, + "the last line", + ) # test saving to checkbox field - task.custom_field = boolean # type: ignore - task.save() # type: ignore + task.custom_field = boolean + task.save() data = { "stdout": "1", @@ -273,12 +256,16 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore - self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value) # type: ignore + self.assertEqual( + TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING + ) + self.assertTrue( + AgentCustomField.objects.get(field=boolean, agent=task.agent).value + ) # test saving to multiple field with commas - task.custom_field = multiple # type: ignore - task.save() # type: ignore + task.custom_field = multiple + task.save() data = { "stdout": "this,is,an,array", @@ -289,8 +276,13 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore - self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"]) # type: ignore + self.assertEqual( + TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING + ) + self.assertEqual( + AgentCustomField.objects.get(field=multiple, agent=task.agent).value, + ["this", "is", "an", "array"], + ) # test mutiple with a single value data = { @@ -302,5 +294,16 @@ def test_task_runner_results(self): r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) - self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore - self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"]) # type: ignore + self.assertEqual( + TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING + ) + self.assertEqual( + AgentCustomField.objects.get(field=multiple, agent=task.agent).value, + ["this"], + ) + + def test_get_agent_config(self): + agent = baker.make_recipe("agents.online_agent") + url = f"/api/v3/{agent.agent_id}/config/" + r = self.client.get(url) + self.assertEqual(r.status_code, 200) diff --git a/api/tacticalrmm/apiv3/urls.py b/api/tacticalrmm/apiv3/urls.py index 4b732818c9..6506b85036 100644 --- a/api/tacticalrmm/apiv3/urls.py +++ b/api/tacticalrmm/apiv3/urls.py @@ -9,7 +9,6 @@ path("/checkinterval/", views.CheckRunnerInterval.as_view()), path("//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()), @@ -19,6 +18,6 @@ path("winupdates/", views.WinUpdates.as_view()), path("superseded/", views.SupersededWinUpdate.as_view()), path("/chocoresult/", views.ChocoResult.as_view()), - path("/recovery/", views.AgentRecovery.as_view()), path("//histresult/", views.AgentHistoryResult.as_view()), + path("/config/", views.AgentConfig.as_view()), ] diff --git a/api/tacticalrmm/apiv3/utils.py b/api/tacticalrmm/apiv3/utils.py new file mode 100644 index 0000000000..4464f633d8 --- /dev/null +++ b/api/tacticalrmm/apiv3/utils.py @@ -0,0 +1,25 @@ +import random + +from django.conf import settings + +from tacticalrmm.structs import AgentCheckInConfig + + +def get_agent_config() -> AgentCheckInConfig: + return AgentCheckInConfig( + checkin_hello=random.randint(*getattr(settings, "CHECKIN_HELLO", (30, 60))), + checkin_agentinfo=random.randint( + *getattr(settings, "CHECKIN_AGENTINFO", (200, 400)) + ), + checkin_winsvc=random.randint( + *getattr(settings, "CHECKIN_WINSVC", (2400, 3000)) + ), + checkin_pubip=random.randint(*getattr(settings, "CHECKIN_PUBIP", (300, 500))), + checkin_disks=random.randint(*getattr(settings, "CHECKIN_DISKS", (1000, 2000))), + checkin_sw=random.randint(*getattr(settings, "CHECKIN_SW", (2800, 3500))), + checkin_wmi=random.randint(*getattr(settings, "CHECKIN_WMI", (3000, 4000))), + checkin_syncmesh=random.randint( + *getattr(settings, "CHECKIN_SYNCMESH", (800, 1200)) + ), + limit_data=getattr(settings, "LIMIT_DATA", False), + ) diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 6a4926bf17..42f95deec5 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -1,9 +1,7 @@ import asyncio -import os -import time from django.conf import settings -from django.http import HttpResponse +from django.db.models import Prefetch from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime from packaging import version as pyver @@ -15,78 +13,51 @@ from accounts.models import User from agents.models import Agent, AgentHistory -from agents.serializers import WinAgentSerializer, AgentHistorySerializer -from autotasks.models import AutomatedTask -from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer -from checks.models import Check +from agents.serializers import AgentHistorySerializer +from apiv3.utils import get_agent_config +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 checks.utils import bytes2human -from logs.models import PendingAction, DebugLog +from core.utils import ( + download_mesh_agent, + get_core_settings, + get_mesh_device_id, + get_mesh_ws_url, + get_meshagent_url, +) +from logs.models import DebugLog, PendingAction from software.models import InstalledSoftware -from tacticalrmm.utils import notify_error, reload_nats +from tacticalrmm.constants import ( + AGENT_DEFER, + AgentMonType, + AgentPlat, + AuditActionType, + AuditObjType, + CheckStatus, + DebugLogType, + GoArch, + MeshAgentIdent, + PAStatus, +) +from tacticalrmm.helpers import make_random_password, notify_error +from tacticalrmm.utils import reload_nats from winupdate.models import WinUpdate, WinUpdatePolicy class CheckIn(APIView): - authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] - def put(self, request): - """ - !!! DEPRECATED AS OF AGENT 1.7.0 !!! - Endpoint be removed in a future release - """ - agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) - serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) - - if request.data["func"] == "disks": - disks = request.data["disks"] - new = [] - for disk in disks: - tmp = {} - for _, _ in disk.items(): - tmp["device"] = disk["device"] - tmp["fstype"] = disk["fstype"] - tmp["total"] = bytes2human(disk["total"]) - tmp["used"] = bytes2human(disk["used"]) - tmp["free"] = bytes2human(disk["free"]) - tmp["percent"] = int(disk["percent"]) - new.append(tmp) - - serializer.is_valid(raise_exception=True) - serializer.save(disks=new) - return Response("ok") - - if request.data["func"] == "loggedonuser": - if request.data["logged_in_username"] != "None": - serializer.is_valid(raise_exception=True) - serializer.save(last_logged_in_user=request.data["logged_in_username"]) - return Response("ok") - - if request.data["func"] == "software": - sw = request.data["software"] - - if not InstalledSoftware.objects.filter(agent=agent).exists(): - InstalledSoftware(agent=agent, software=sw).save() - else: - s = agent.installedsoftware_set.first() # type: ignore - s.software = sw - s.save(update_fields=["software"]) - - return Response("ok") - - serializer.is_valid(raise_exception=True) - serializer.save() - return Response("ok") - # 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)) - time.sleep(0.5) asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) return Response("ok") @@ -96,7 +67,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"]) @@ -109,7 +82,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") @@ -120,25 +95,27 @@ 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 + agent.save(update_fields=["needs_reboot"]) + reboot_policy: str = agent.get_patch_policy().reboot_after_install reboot = False if reboot_policy == "always": reboot = True - - if request.data["needs_reboot"]: - if reboot_policy == "required": - reboot = True - elif reboot_policy == "never": - agent.needs_reboot = True - agent.save(update_fields=["needs_reboot"]) + elif needs_reboot and reboot_policy == "required": + reboot = True if reboot: 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.", ) @@ -146,8 +123,13 @@ def put(self, request): 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 + success: bool = request.data["success"] if success: u.result = "success" @@ -170,8 +152,14 @@ def patch(self, request): 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 @@ -202,14 +190,6 @@ def post(self, request): ).save() agent.delete_superseded_updates() - - # more superseded updates cleanup - if pyver.parse(agent.version) <= pyver.parse("1.4.2"): - for u in agent.winupdates.filter( # type: ignore - date_installed__isnull=True, result="failed" - ).exclude(installed=True): - u.delete() - return Response("ok") @@ -218,7 +198,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() @@ -231,12 +213,19 @@ class RunChecks(APIView): permission_classes = [IsAuthenticated] def get(self, request, agentid): - agent = get_object_or_404(Agent, agent_id=agentid) - checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False) + 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) @@ -246,47 +235,70 @@ class CheckRunner(APIView): permission_classes = [IsAuthenticated] def get(self, request, agentid): - agent = get_object_or_404(Agent, agent_id=agentid) - checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore + 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 = [ check for check in checks # always run if check hasn't run yet - if not check.last_run - # if a check interval is set, see if the correct amount of seconds have passed + if not isinstance(check.check_result, CheckResult) + or not check.check_result.last_run + # see if the correct amount of seconds have passed or ( - check.run_interval - and ( - check.last_run - < djangotime.now() - - djangotime.timedelta(seconds=check.run_interval) + check.check_result.last_run + < djangotime.now() + - djangotime.timedelta( + seconds=check.run_interval or agent.check_interval ) ) - # if check interval isn't set, make sure the agent's check interval has passed before running - or ( - not check.run_interval - and check.last_run - < djangotime.now() - djangotime.timedelta(seconds=agent.check_interval) - ) ] + 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 pyver.parse(check.agent.version) < pyver.parse("1.5.7"): - return notify_error("unsupported") + if "agent_id" not in request.data.keys(): + return notify_error("Agent upgrade required") + + 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"] + ) + + # 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, + ) + + if created: + check_result.save() - check.last_run = djangotime.now() - check.save(update_fields=["last_run"]) - status = check.handle_check(request.data) - if status == "failing" and check.assignedtask.exists(): # type: ignore - check.handle_assigned_task() + 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) + else: + task.run_win_task() return Response("ok") @@ -296,7 +308,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()} @@ -308,65 +323,70 @@ 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) - - serializer = TaskRunnerPatchSerializer( - instance=task, data=request.data, partial=True + 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 ) + + # get task result or create if doesn't exist + try: + 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 + ) + except TaskResult.DoesNotExist: + serializer = TaskResultSerializer(data=request.data, partial=True) + serializer.is_valid(raise_exception=True) - new_task = serializer.save(last_run=djangotime.now()) + task_result = serializer.save(last_run=djangotime.now()) AgentHistory.objects.create( agent=agent, - type="task_run", - script=task.script, + type=AuditActionType.TASK_RUN, + command=task.name, script_results=request.data, ) # check if task is a collector and update the custom field if task.custom_field: - if not task.stderr: + if not task_result.stderr: + task_result.save_collector_results() - task.save_collector_results() - - status = "passing" + status = CheckStatus.PASSING else: - status = "failing" + status = CheckStatus.FAILING else: - status = "failing" if task.retcode != 0 else "passing" - - new_task.status = status - new_task.save() + status = ( + CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING + ) - if status == "passing": - if Alert.objects.filter(assigned_task=new_task, resolved=False).exists(): - Alert.handle_alert_resolve(new_task) + if task_result: + task_result.status = status + task_result.save(update_fields=["status"]) else: - Alert.handle_alert_failure(new_task) + task_result.status = status + task.save(update_fields=["status"]) - 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") + if status == CheckStatus.PASSING: + if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True): + Alert.handle_alert_resolve(task_result) + else: + Alert.handle_alert_failure(task_result) - agent.wmi_detail = request.data["sysinfo"] - agent.save(update_fields=["wmi_detail"]) return Response("ok") @@ -374,25 +394,40 @@ class MeshExe(APIView): """Sends the mesh exe to the installer""" def post(self, request): - exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe" - mesh_exe = os.path.join(settings.EXE_DIR, exe) + match request.data: + case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}: + ident = MeshAgentIdent.WIN64 + case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}: + ident = MeshAgentIdent.WIN32 + case {"goarch": GoArch.AMD64, "plat": AgentPlat.DARWIN} | { + "goarch": GoArch.ARM64, + "plat": AgentPlat.DARWIN, + }: + ident = MeshAgentIdent.DARWIN_UNIVERSAL + case _: + return notify_error("Arch not supported") + + core = get_core_settings() + + try: + uri = get_mesh_ws_url() + mesh_device_id: str = asyncio.run( + get_mesh_device_id(uri, core.mesh_device_group) + ) + except: + return notify_error("Unable to connect to mesh to get group id information") + + dl_url = get_meshagent_url( + ident=ident, + plat=request.data["plat"], + mesh_site=core.mesh_site, + mesh_device_id=mesh_device_id, + ) - if not os.path.exists(mesh_exe): - return notify_error("Mesh Agent executable not found") - - if settings.DEBUG: - with open(mesh_exe, "rb") as f: - response = HttpResponse( - f.read(), - content_type="application/vnd.microsoft.portable-executable", - ) - response["Content-Disposition"] = f"inline; filename={exe}" - return response - else: - response = HttpResponse() - response["Content-Disposition"] = f"attachment; filename={exe}" - response["X-Accel-Redirect"] = f"/private/exe/{exe}" - return response + try: + return download_mesh_agent(dl_url) + except: + return notify_error("Unable to download mesh agent exe") class NewAgent(APIView): @@ -413,21 +448,21 @@ def post(self, request): monitoring_type=request.data["monitoring_type"], description=request.data["description"], mesh_node_id=request.data["mesh_node_id"], + goarch=request.data["goarch"], + plat=request.data["plat"], last_seen=djangotime.now(), ) agent.save() - agent.salt_id = f"{agent.hostname}-{agent.pk}" - agent.save(update_fields=["salt_id"]) user = User.objects.create_user( # type: ignore username=request.data["agent_id"], agent=agent, - password=User.objects.make_random_password(60), # type: ignore + password=make_random_password(len=60), ) token = Token.objects.create(user=user) - if agent.monitoring_type == "workstation": + if agent.monitoring_type == AgentMonType.WORKSTATION: WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() else: WinUpdatePolicy(agent=agent).save() @@ -438,20 +473,15 @@ def post(self, request): 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}, ) - return Response( - { - "pk": agent.pk, - "saltid": f"{agent.hostname}-{agent.pk}", - "token": token.key, - } - ) + ret = {"pk": agent.pk, "token": token.key} + return Response(ret) class Software(APIView): @@ -481,7 +511,10 @@ def post(self, request): return notify_error("Invalid data") ver = request.data["version"] - if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER): + if ( + pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER) + and "-dev" not in settings.LATEST_AGENT_VER + ): return notify_error( f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM" ) @@ -516,54 +549,29 @@ def patch(self, request, pk): action.details["output"] = results action.details["installed"] = installed - action.status = "completed" + action.status = PAStatus.COMPLETED action.save(update_fields=["details", "status"]) return Response("ok") -class AgentRecovery(APIView): - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - - def get(self, request, agentid): - agent = get_object_or_404( - Agent.objects.prefetch_related("recoveryactions").only( - "pk", "agent_id", "last_seen" - ), - agent_id=agentid, - ) - - # TODO remove these 2 lines after agent v1.7.0 has been out for a while - # this is handled now by nats-api service - agent.last_seen = djangotime.now() - agent.save(update_fields=["last_seen"]) - - recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore - ret = {"mode": "pass", "shellcmd": ""} - if recovery is None: - return Response(ret) - - recovery.last_run = djangotime.now() - recovery.save(update_fields=["last_run"]) - - ret["mode"] = recovery.mode - - if recovery.mode == "command": - ret["shellcmd"] = recovery.command - elif recovery.mode == "rpc": - reload_nats() - - return Response(ret) - - class AgentHistoryResult(APIView): authentication_classes = [TokenAuthentication] 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() return Response("ok") + + +class AgentConfig(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, agentid): + ret = get_agent_config() + return Response(ret._to_dict()) diff --git a/api/tacticalrmm/automation/models.py b/api/tacticalrmm/automation/models.py index 96701c2222..3ce995544d 100644 --- a/api/tacticalrmm/automation/models.py +++ b/api/tacticalrmm/automation/models.py @@ -1,8 +1,21 @@ +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 core.models import CoreSettings +from clients.models import Client, Site from logs.models import BaseAuditModel +from tacticalrmm.constants import ( + CORESETTINGS_CACHE_KEY, + AgentMonType, + AgentPlat, + CheckType, +) + +if TYPE_CHECKING: + from autotasks.models import AutomatedTask + from checks.models import Check class Policy(BaseAuditModel): @@ -27,366 +40,299 @@ class Policy(BaseAuditModel): "agents.Agent", related_name="policy_exclusions", blank=True ) - def save(self, *args, **kwargs): + def save(self, *args: Any, **kwargs: Any) -> None: from alerts.tasks import cache_agents_alert_template - from automation.tasks import generate_agent_checks_task # get old policy if exists - old_policy = type(self).objects.get(pk=self.pk) if self.pk else None + old_policy: Optional[Policy] = ( + type(self).objects.get(pk=self.pk) if self.pk else None + ) super(Policy, self).save(old_model=old_policy, *args, **kwargs) - # generate agent checks only if active and enforced were changed + # check if alert template was changes and cache on agents if old_policy: - if old_policy.active != self.active or old_policy.enforced != self.enforced: - generate_agent_checks_task.delay( - policy=self.pk, - create_tasks=True, - ) - if old_policy.alert_template != self.alert_template: cache_agents_alert_template.delay() + elif self.alert_template and old_policy.active != self.active: + cache_agents_alert_template.delay() - def delete(self, *args, **kwargs): - from automation.tasks import generate_agent_checks_task - - agents = list(self.related_agents().only("pk").values_list("pk", flat=True)) - super(Policy, self).delete(*args, **kwargs) + if old_policy.active != self.active or old_policy.enforced != self.enforced: + cache.delete(CORESETTINGS_CACHE_KEY) + cache.delete_many_pattern("site_workstation_*") + cache.delete_many_pattern("site_server_*") + cache.delete_many_pattern("agent_*") - generate_agent_checks_task.delay(agents=agents, create_tasks=True) + def delete(self, *args, **kwargs): + cache.delete(CORESETTINGS_CACHE_KEY) + cache.delete_many_pattern("site_workstation_*") + cache.delete_many_pattern("site_server_*") + cache.delete_many_pattern("agent_*") + + super(Policy, self).delete( + *args, + **kwargs, + ) - def __str__(self): + def __str__(self) -> str: return self.name @property - def is_default_server_policy(self): - return self.default_server_policy.exists() # type: ignore + def is_default_server_policy(self) -> bool: + return self.default_server_policy.exists() @property - def is_default_workstation_policy(self): - return self.default_workstation_policy.exists() # type: ignore + def is_default_workstation_policy(self) -> bool: + return self.default_workstation_policy.exists() - def is_agent_excluded(self, agent): + def is_agent_excluded(self, agent: "Agent") -> bool: return ( agent in self.excluded_agents.all() or agent.site in self.excluded_sites.all() or agent.client in self.excluded_clients.all() ) - def related_agents(self): - return self.get_related("server") | self.get_related("workstation") - - def get_related(self, mon_type): - explicit_agents = ( - self.agents.filter(monitoring_type=mon_type) # type: ignore - .exclude( - pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True) - ) - .exclude(site__in=self.excluded_sites.all()) - .exclude(site__client__in=self.excluded_clients.all()) + def related_agents( + self, mon_type: Optional[str] = None + ) -> "models.QuerySet[Agent]": + models.prefetch_related_objects( + [self], + "excluded_agents", + "excluded_sites", + "excluded_clients", + "workstation_clients", + "server_clients", + "workstation_sites", + "server_sites", + "agents", ) - explicit_clients = getattr(self, f"{mon_type}_clients").exclude( - pk__in=self.excluded_clients.all() + agent_filter = {} + filtered_agents_ids = Agent.objects.none() + + if mon_type: + agent_filter["monitoring_type"] = mon_type + + excluded_clients_ids = self.excluded_clients.only("pk").values_list( + "id", flat=True ) - explicit_sites = getattr(self, f"{mon_type}_sites").exclude( - pk__in=self.excluded_sites.all() + excluded_sites_ids = self.excluded_sites.only("pk").values_list("id", flat=True) + excluded_agents_ids = self.excluded_agents.only("pk").values_list( + "id", flat=True + ) + + if self.is_default_server_policy: + filtered_agents_ids |= ( + Agent.objects.exclude(block_policy_inheritance=True) + .exclude(site__block_policy_inheritance=True) + .exclude(site__client__block_policy_inheritance=True) + .exclude(id__in=excluded_agents_ids) + .exclude(site_id__in=excluded_sites_ids) + .exclude(site__client_id__in=excluded_clients_ids) + .filter(monitoring_type=AgentMonType.SERVER) + .only("id") + .values_list("id", flat=True) + ) + + if self.is_default_workstation_policy: + filtered_agents_ids |= ( + Agent.objects.exclude(block_policy_inheritance=True) + .exclude(site__block_policy_inheritance=True) + .exclude(site__client__block_policy_inheritance=True) + .exclude(id__in=excluded_agents_ids) + .exclude(site_id__in=excluded_sites_ids) + .exclude(site__client_id__in=excluded_clients_ids) + .filter(monitoring_type=AgentMonType.WORKSTATION) + .only("id") + .values_list("id", flat=True) + ) + + # if this is the default policy for servers and workstations and skip the other calculations + if self.is_default_server_policy and self.is_default_workstation_policy: + return Agent.objects.filter(models.Q(id__in=filtered_agents_ids)) + + explicit_agents = ( + self.agents.filter(**agent_filter) # type: ignore + .exclude(id__in=excluded_agents_ids) + .exclude(site_id__in=excluded_sites_ids) + .exclude(site__client_id__in=excluded_clients_ids) ) - filtered_agents_pks = Policy.objects.none() + explicit_clients_qs = Client.objects.none() + explicit_sites_qs = Site.objects.none() - filtered_agents_pks |= ( + if not mon_type or mon_type == AgentMonType.WORKSTATION: + explicit_clients_qs |= self.workstation_clients.exclude( # type: ignore + id__in=excluded_clients_ids + ) + explicit_sites_qs |= self.workstation_sites.exclude( # type: ignore + id__in=excluded_sites_ids + ) + + if not mon_type or mon_type == AgentMonType.SERVER: + explicit_clients_qs |= self.server_clients.exclude( # type: ignore + id__in=excluded_clients_ids + ) + explicit_sites_qs |= self.server_sites.exclude( # type: ignore + id__in=excluded_sites_ids + ) + + filtered_agents_ids |= ( Agent.objects.exclude(block_policy_inheritance=True) .filter( - site__in=[ - site - for site in explicit_sites - if site.client not in explicit_clients - and site.client not in self.excluded_clients.all() + site_id__in=[ + site.id + for site in explicit_sites_qs + if site.client not in explicit_clients_qs + and site.client.id not in excluded_clients_ids ], - monitoring_type=mon_type, + **agent_filter, ) - .values_list("pk", flat=True) + .only("id") + .values_list("id", flat=True) ) - filtered_agents_pks |= ( + filtered_agents_ids |= ( Agent.objects.exclude(block_policy_inheritance=True) .exclude(site__block_policy_inheritance=True) .filter( - site__client__in=[client for client in explicit_clients], - monitoring_type=mon_type, + site__client__in=explicit_clients_qs, + **agent_filter, ) - .values_list("pk", flat=True) + .only("id") + .values_list("id", flat=True) ) return Agent.objects.filter( - models.Q(pk__in=filtered_agents_pks) - | models.Q(pk__in=explicit_agents.only("pk")) + models.Q(id__in=filtered_agents_ids) + | models.Q(id__in=explicit_agents.only("id")) ) @staticmethod - def serialize(policy): + def serialize(policy: "Policy") -> Dict[str, Any]: # serializes the policy and returns json from .serializers import PolicyAuditSerializer return PolicyAuditSerializer(policy).data @staticmethod - def cascade_policy_tasks(agent): - + def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]": # List of all tasks to be applied - tasks = list() - added_task_pks = list() - - agent_tasks_parent_pks = [ - task.parent_task for task in agent.autotasks.filter(managed_by_policy=True) - ] + tasks = [] # Get policies applied to agent and agent site and client - client = agent.client - site = agent.site - - default_policy = None - client_policy = None - site_policy = None - agent_policy = agent.policy - - # Get the Client/Site policy based on if the agent is server or workstation - if agent.monitoring_type == "server": - default_policy = CoreSettings.objects.first().server_policy - client_policy = client.server_policy - site_policy = site.server_policy - elif agent.monitoring_type == "workstation": - default_policy = CoreSettings.objects.first().workstation_policy - client_policy = client.workstation_policy - site_policy = site.workstation_policy - - # check if client/site/agent is blocking inheritance and blank out policies - if agent.block_policy_inheritance: - site_policy = None - client_policy = None - default_policy = None - elif site.block_policy_inheritance: - client_policy = None - default_policy = None - elif client.block_policy_inheritance: - default_policy = None - - if ( - agent_policy - and agent_policy.active - and not agent_policy.is_agent_excluded(agent) - ): - for task in agent_policy.autotasks.all(): - if task.pk not in added_task_pks: - tasks.append(task) - added_task_pks.append(task.pk) - if ( - site_policy - and site_policy.active - and not site_policy.is_agent_excluded(agent) - ): - for task in site_policy.autotasks.all(): - if task.pk not in added_task_pks: - tasks.append(task) - added_task_pks.append(task.pk) - if ( - client_policy - and client_policy.active - and not client_policy.is_agent_excluded(agent) - ): - for task in client_policy.autotasks.all(): - if task.pk not in added_task_pks: - tasks.append(task) - added_task_pks.append(task.pk) - - if ( - default_policy - and default_policy.active - and not default_policy.is_agent_excluded(agent) - ): - for task in default_policy.autotasks.all(): - if task.pk not in added_task_pks: + policies = agent.get_agent_policies() + + processed_policies = [] + + for policy in policies.values(): + if policy and policy.active and policy.pk not in processed_policies: + processed_policies.append(policy.pk) + for task in policy.autotasks.all(): tasks.append(task) - added_task_pks.append(task.pk) - - # remove policy tasks from agent not included in policy - for task in agent.autotasks.filter( - parent_task__in=[ - taskpk - for taskpk in agent_tasks_parent_pks - if taskpk not in added_task_pks - ] - ): - if task.sync_status == "initial": - task.delete() - else: - task.sync_status = "pendingdeletion" - task.save() - - # change tasks from pendingdeletion to notsynced if policy was added or changed - agent.autotasks.filter(sync_status="pendingdeletion").filter( - parent_task__in=[taskpk for taskpk in added_task_pks] - ).update(sync_status="notsynced") - - return [task for task in tasks if task.pk not in agent_tasks_parent_pks] + + return tasks @staticmethod - def cascade_policy_checks(agent): + def get_policy_checks(agent: "Agent") -> "List[Check]": # Get checks added to agent directly - agent_checks = list(agent.agentchecks.filter(managed_by_policy=False)) - - agent_checks_parent_pks = [ - check.parent_check - for check in agent.agentchecks.filter(managed_by_policy=True) - ] + agent_checks = list(agent.agentchecks.all()) # Get policies applied to agent and agent site and client - client = agent.client - site = agent.site - - default_policy = None - client_policy = None - site_policy = None - agent_policy = agent.policy - - if agent.monitoring_type == "server": - default_policy = CoreSettings.objects.first().server_policy - client_policy = client.server_policy - site_policy = site.server_policy - elif agent.monitoring_type == "workstation": - default_policy = CoreSettings.objects.first().workstation_policy - client_policy = client.workstation_policy - site_policy = site.workstation_policy - - # check if client/site/agent is blocking inheritance and blank out policies - if agent.block_policy_inheritance: - site_policy = None - client_policy = None - default_policy = None - elif site.block_policy_inheritance: - client_policy = None - default_policy = None - elif client.block_policy_inheritance: - default_policy = None + policies = agent.get_agent_policies() # Used to hold the policies that will be applied and the order in which they are applied # Enforced policies are applied first - enforced_checks = list() - policy_checks = list() - - if ( - agent_policy - and agent_policy.active - and not agent_policy.is_agent_excluded(agent) - ): - if agent_policy.enforced: - for check in agent_policy.policychecks.all(): - enforced_checks.append(check) - else: - for check in agent_policy.policychecks.all(): - policy_checks.append(check) - - if ( - site_policy - and site_policy.active - and not site_policy.is_agent_excluded(agent) - ): - if site_policy.enforced: - for check in site_policy.policychecks.all(): - enforced_checks.append(check) - else: - for check in site_policy.policychecks.all(): - policy_checks.append(check) - - if ( - client_policy - and client_policy.active - and not client_policy.is_agent_excluded(agent) - ): - if client_policy.enforced: - for check in client_policy.policychecks.all(): - enforced_checks.append(check) - else: - for check in client_policy.policychecks.all(): - policy_checks.append(check) - - if ( - default_policy - and default_policy.active - and not default_policy.is_agent_excluded(agent) - ): - if default_policy.enforced: - for check in default_policy.policychecks.all(): - enforced_checks.append(check) - else: - for check in default_policy.policychecks.all(): - policy_checks.append(check) + enforced_checks = [] + policy_checks = [] + + processed_policies = [] + + for policy in policies.values(): + if policy and policy.active and policy.pk not in processed_policies: + processed_policies.append(policy.pk) + if policy.enforced: + for check in policy.policychecks.all(): + enforced_checks.append(check) + else: + for check in policy.policychecks.all(): + policy_checks.append(check) + + if not enforced_checks and not policy_checks: + return [] # Sorted Checks already added - added_diskspace_checks = list() - added_ping_checks = list() - added_winsvc_checks = list() - added_script_checks = list() - added_eventlog_checks = list() - added_cpuload_checks = list() - added_memory_checks = list() - - # Lists all agent and policy checks that will be created - diskspace_checks = list() - ping_checks = list() - winsvc_checks = list() - script_checks = list() - eventlog_checks = list() - cpuload_checks = list() - memory_checks = list() + added_diskspace_checks: List[str] = [] + added_ping_checks: List[str] = [] + added_winsvc_checks: List[str] = [] + added_script_checks: List[int] = [] + added_eventlog_checks: List[List[str]] = [] + added_cpuload_checks: List[int] = [] + added_memory_checks: List[int] = [] + + # Lists all agent and policy checks that will be returned + diskspace_checks: "List[Check]" = [] + ping_checks: "List[Check]" = [] + winsvc_checks: "List[Check]" = [] + script_checks: "List[Check]" = [] + eventlog_checks: "List[Check]" = [] + cpuload_checks: "List[Check]" = [] + memory_checks: "List[Check]" = [] + + overridden_checks: List[int] = [] # 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": + if ( + check.check_type == CheckType.DISK_SPACE + and agent.plat == AgentPlat.WINDOWS + ): # Check if drive letter was already added if check.disk not in added_diskspace_checks: added_diskspace_checks.append(check.disk) - # Dont create the check if it is an agent check + # Dont add if check if it is an agent check if not check.agent: diskspace_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - if 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) - # Dont create the check if it is an agent check + # Dont add if the check if it is an agent check if not check.agent: ping_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - if check.check_type == "cpuload": + elif ( + check.check_type == CheckType.CPU_LOAD + and agent.plat == AgentPlat.WINDOWS + ): # Check if cpuload list is empty if not added_cpuload_checks: - added_cpuload_checks.append(check) + added_cpuload_checks.append(check.pk) # Dont create the check if it is an agent check if not check.agent: cpuload_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - if check.check_type == "memory": + elif ( + check.check_type == CheckType.MEMORY and agent.plat == AgentPlat.WINDOWS + ): # Check if memory check list is empty if not added_memory_checks: - added_memory_checks.append(check) + added_memory_checks.append(check.pk) # Dont create the check if it is an agent check if not check.agent: memory_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - if check.check_type == "winsvc": + elif ( + check.check_type == CheckType.WINSVC and agent.plat == AgentPlat.WINDOWS + ): # Check if service name was already added if check.svc_name not in added_winsvc_checks: added_winsvc_checks.append(check.svc_name) @@ -394,10 +340,11 @@ def cascade_policy_checks(agent): if not check.agent: winsvc_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - if check.check_type == "script": + elif check.check_type == CheckType.SCRIPT and agent.is_supported_script( + check.script.supported_platforms + ): # Check if script id was already added if check.script.id not in added_script_checks: added_script_checks.append(check.script.id) @@ -405,20 +352,28 @@ def cascade_policy_checks(agent): if not check.agent: script_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - if check.check_type == "eventlog": + elif ( + check.check_type == CheckType.EVENT_LOG + and agent.plat == AgentPlat.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]) if not check.agent: eventlog_checks.append(check) elif check.agent: - check.overriden_by_policy = True - check.save() + overridden_checks.append(check.pk) - final_list = ( + if overridden_checks: + from checks.models import Check + + Check.objects.filter(pk__in=overridden_checks).update( + overridden_by_policy=True + ) + + return ( diskspace_checks + ping_checks + cpuload_checks @@ -427,33 +382,3 @@ def cascade_policy_checks(agent): + script_checks + eventlog_checks ) - - # remove policy checks from agent that fell out of policy scope - agent.agentchecks.filter( - managed_by_policy=True, - parent_check__in=[ - checkpk - for checkpk in agent_checks_parent_pks - if checkpk not in [check.pk for check in final_list] - ], - ).delete() - - return [ - check for check in final_list if check.pk not in agent_checks_parent_pks - ] - - @staticmethod - def generate_policy_checks(agent): - checks = Policy.cascade_policy_checks(agent) - - if checks: - for check in checks: - check.create_policy_check(agent) - - @staticmethod - def generate_policy_tasks(agent): - tasks = Policy.cascade_policy_tasks(agent) - - if tasks: - for task in tasks: - task.create_policy_task(agent) diff --git a/api/tacticalrmm/automation/permissions.py b/api/tacticalrmm/automation/permissions.py index 460ef27bf1..eba3a82286 100644 --- a/api/tacticalrmm/automation/permissions.py +++ b/api/tacticalrmm/automation/permissions.py @@ -4,8 +4,8 @@ class AutomationPolicyPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_automation_policies") - else: - return _has_perm(r, "can_manage_automation_policies") + + return _has_perm(r, "can_manage_automation_policies") diff --git a/api/tacticalrmm/automation/serializers.py b/api/tacticalrmm/automation/serializers.py index f9d1063c87..9ea41fce52 100644 --- a/api/tacticalrmm/automation/serializers.py +++ b/api/tacticalrmm/automation/serializers.py @@ -5,10 +5,13 @@ ) from agents.serializers import AgentHostnameSerializer -from autotasks.models import AutomatedTask -from checks.models import Check -from clients.models import Client -from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer +from autotasks.models import TaskResult +from checks.models import CheckResult +from clients.models import Client, Site +from clients.serializers import ( + ClientMinimumSerializer, + SiteMinimumSerializer, +) from winupdate.serializers import WinUpdatePolicySerializer from .models import Policy @@ -85,18 +88,36 @@ class Meta: ) +class PolicyOverviewSiteSerializer(ModelSerializer): + workstation_policy = PolicySerializer(read_only=True) + server_policy = PolicySerializer(read_only=True) + + class Meta: + model = Site + fields = ("pk", "name", "workstation_policy", "server_policy") + + class PolicyOverviewSerializer(ModelSerializer): + sites = SerializerMethodField() + workstation_policy = PolicySerializer(read_only=True) + server_policy = PolicySerializer(read_only=True) + + def get_sites(self, obj): + return PolicyOverviewSiteSerializer( + obj.filtered_sites, + many=True, + ).data + class Meta: model = Client fields = ("pk", "name", "sites", "workstation_policy", "server_policy") - depth = 2 class PolicyCheckStatusSerializer(ModelSerializer): hostname = ReadOnlyField(source="agent.hostname") class Meta: - model = Check + model = CheckResult fields = "__all__" @@ -104,7 +125,7 @@ class PolicyTaskStatusSerializer(ModelSerializer): hostname = ReadOnlyField(source="agent.hostname") class Meta: - model = AutomatedTask + model = TaskResult fields = "__all__" diff --git a/api/tacticalrmm/automation/tasks.py b/api/tacticalrmm/automation/tasks.py index a19c7456a4..57ac0a76d5 100644 --- a/api/tacticalrmm/automation/tasks.py +++ b/api/tacticalrmm/automation/tasks.py @@ -1,155 +1,20 @@ -from typing import Any, Dict, List, Union - from tacticalrmm.celery import app -@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}) -def generate_agent_checks_task( - policy: int = None, - site: int = None, - client: int = None, - agents: List[int] = list(), - all: bool = False, - create_tasks: bool = False, -) -> Union[str, None]: - from agents.models import Agent - from automation.models import Policy - - p = Policy.objects.get(pk=policy) if policy else None - - # generate checks on all agents if all is specified or if policy is default server/workstation policy - if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all: - a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type") - - # generate checks on all servers if policy is a default servers policy - elif p and p.is_default_server_policy: - a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type") - - # generate checks on all workstations if policy is a default workstations policy - elif p and p.is_default_workstation_policy: - a = Agent.objects.filter(monitoring_type="workstation").only( - "pk", "monitoring_type" - ) - - # generate checks on a list of supplied agents - elif agents: - a = Agent.objects.filter(pk__in=agents) - - # generate checks on agents affected by supplied policy - elif policy: - a = p.related_agents().only("pk") - - # generate checks that has specified site - elif site: - a = Agent.objects.filter(site_id=site) - - # generate checks that has specified client - elif client: - a = Agent.objects.filter(site__client_id=client) - else: - a = [] - - for agent in a: - agent.generate_checks_from_policies() - if create_tasks: - agent.generate_tasks_from_policies() - - agent.set_alert_template() - - return "ok" - - -@app.task( - acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5} -) -# updates policy managed check fields on agents -def update_policy_check_fields_task(check: int) -> str: - from checks.models import Check - - c: Check = Check.objects.get(pk=check) - update_fields: Dict[Any, Any] = {} - - for field in c.policy_fields_to_copy: - update_fields[field] = getattr(c, field) - - Check.objects.filter(parent_check=check).update(**update_fields) - - return "ok" - - -@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}) -# generates policy tasks on agents affected by a policy -def generate_agent_autotasks_task(policy: int = None) -> str: - from agents.models import Agent - from automation.models import Policy - - p: Policy = Policy.objects.get(pk=policy) - - if p and p.is_default_server_policy and p.is_default_workstation_policy: - agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type") - elif p and p.is_default_server_policy: - agents = Agent.objects.filter(monitoring_type="server").only( - "pk", "monitoring_type" - ) - elif p and p.is_default_workstation_policy: - agents = Agent.objects.filter(monitoring_type="workstation").only( - "pk", "monitoring_type" - ) - else: - agents = p.related_agents().only("pk") - - for agent in agents: - agent.generate_tasks_from_policies() - - return "ok" - - -@app.task( - acks_late=True, - retry_backoff=5, - retry_jitter=True, - retry_kwargs={"max_retries": 5}, -) -def delete_policy_autotasks_task(task: int) -> str: - from autotasks.models import AutomatedTask - - for t in AutomatedTask.objects.filter(parent_task=task): - t.delete_task_on_agent() - - return "ok" - - @app.task def run_win_policy_autotasks_task(task: int) -> str: from autotasks.models import AutomatedTask - for t in AutomatedTask.objects.filter(parent_task=task): - t.run_win_task() - - return "ok" - - -@app.task( - acks_late=True, - retry_backoff=5, - retry_jitter=True, - retry_kwargs={"max_retries": 5}, -) -def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str: - from autotasks.models import AutomatedTask - - t = AutomatedTask.objects.get(pk=task) - update_fields: Dict[str, Any] = {} - - for field in t.policy_fields_to_copy: - update_fields[field] = getattr(t, field) + try: + policy_task = AutomatedTask.objects.get(pk=task) + except AutomatedTask.DoesNotExist: + return "AutomatedTask not found" - AutomatedTask.objects.filter(parent_task=task).update(**update_fields) + if not policy_task.policy: + return "AutomatedTask must be a policy" - if update_agent: - for t in AutomatedTask.objects.filter(parent_task=task).exclude( - sync_status="initial" - ): - t.modify_task_on_agent() + # get related agents from policy + for agent in policy_task.policy.related_agents(): + policy_task.run_win_task(agent) return "ok" diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index 2257afe4e0..139d954c0d 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -1,9 +1,12 @@ from itertools import cycle from unittest.mock import patch -from agents.models import Agent -from core.models import CoreSettings from model_bakery import baker, seq +from django.db.models import Prefetch +from agents.models import Agent +from clients.models import Site +from core.utils import get_core_settings +from tacticalrmm.constants import AgentMonType, TaskSyncStatus from tacticalrmm.test import TacticalTestCase from winupdate.models import WinUpdatePolicy @@ -36,13 +39,13 @@ def test_get_policy(self): self.assertEqual(resp.status_code, 404) policy = baker.make("automation.Policy") - url = f"/automation/policies/{policy.pk}/" # type: ignore + url = f"/automation/policies/{policy.pk}/" resp = self.client.get(url, format="json") serializer = PolicySerializer(policy) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) @@ -68,12 +71,12 @@ def test_add_policy(self, create_task): # create policy with tasks and checks policy = baker.make("automation.Policy") - checks = self.create_checks(policy=policy) - tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) + checks = self.create_checks(parent=policy) + tasks = baker.make_recipe("autotasks.task", policy=policy, _quantity=3) # assign a task to a check - tasks[0].assigned_check = checks[0] # type: ignore - tasks[0].save() # type: ignore + tasks[0].assigned_check = checks[0] + tasks[0].save() # # test copy tasks and checks to another policy data = { @@ -81,32 +84,27 @@ def test_add_policy(self, create_task): "desc": "policy desc", "active": True, "enforced": False, - "copyId": policy.pk, # type: ignore + "copyId": policy.pk, } - resp = self.client.post(f"/automation/policies/", data, format="json") + resp = self.client.post("/automation/policies/", data, format="json") self.assertEqual(resp.status_code, 200) copied_policy = Policy.objects.get(name=data["name"]) - self.assertEqual(copied_policy.autotasks.count(), 3) # type: ignore - self.assertEqual(copied_policy.policychecks.count(), 7) # type: ignore - - # make sure correct task was assign to the check - self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type) # type: ignore - - create_task.assert_not_called() + self.assertEqual(copied_policy.autotasks.count(), 3) + self.assertEqual(copied_policy.policychecks.count(), 7) self.check_not_authenticated("post", url) - @patch("automation.tasks.generate_agent_checks_task.delay") - def test_update_policy(self, generate_agent_checks_task): + @patch("alerts.tasks.cache_agents_alert_template.delay") + def test_update_policy(self, cache_alert_template): # returns 404 for invalid policy pk resp = self.client.put("/automation/policies/500/", format="json") self.assertEqual(resp.status_code, 404) policy = baker.make("automation.Policy", active=True, enforced=False) - url = f"/automation/policies/{policy.pk}/" # type: ignore + url = f"/automation/policies/{policy.pk}/" data = { "name": "Test Policy Update", @@ -118,83 +116,51 @@ def test_update_policy(self, generate_agent_checks_task): resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - # only called if active, enforced, or excluded objects are updated - generate_agent_checks_task.assert_not_called() - + alert_template = baker.make("alerts.AlertTemplate") data = { "name": "Test Policy Update", "desc": "policy desc Update", - "active": False, - "enforced": False, + "alert_template": alert_template.pk, } resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - generate_agent_checks_task.assert_called_with( - policy=policy.pk, create_tasks=True # type: ignore - ) - generate_agent_checks_task.reset_mock() - # make sure policies are re-evaluated when excluded changes - agents = baker.make_recipe("agents.agent", _quantity=2) - clients = baker.make("clients.Client", _quantity=2) - sites = baker.make("clients.Site", _quantity=2) - data = { - "excluded_agents": [agent.pk for agent in agents], # type: ignore - "excluded_sites": [site.pk for site in sites], # type: ignore - "excluded_clients": [client.pk for client in clients], # type: ignore - } - - resp = self.client.put(url, data, format="json") - self.assertEqual(resp.status_code, 200) - generate_agent_checks_task.assert_called_with( - policy=policy.pk, create_tasks=True # type: ignore - ) + cache_alert_template.called_once() self.check_not_authenticated("put", url) - @patch("automation.tasks.generate_agent_checks_task.delay") - def test_delete_policy(self, generate_agent_checks_task): + def test_delete_policy(self): # returns 404 for invalid policy pk resp = self.client.delete("/automation/policies/500/", format="json") self.assertEqual(resp.status_code, 404) # setup data policy = baker.make("automation.Policy") - site = baker.make("clients.Site") - agents = baker.make_recipe( - "agents.agent", site=site, policy=policy, _quantity=3 - ) - url = f"/automation/policies/{policy.pk}/" # type: ignore + + url = f"/automation/policies/{policy.pk}/" # resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - generate_agent_checks_task.assert_called_with( - agents=[agent.pk for agent in agents], create_tasks=True - ) - self.check_not_authenticated("delete", url) def test_get_policy_check_status(self): # setup data - site = baker.make("clients.Site") - agent = baker.make_recipe("agents.agent", site=site) policy = baker.make("automation.Policy") + agent = baker.make_recipe("agents.agent", policy=policy) policy_diskcheck = baker.make_recipe("checks.diskspace_check", policy=policy) - managed_check = baker.make_recipe( - "checks.diskspace_check", - agent=agent, - managed_by_policy=True, - parent_check=policy_diskcheck.pk, + result = baker.make( + "checks.CheckResult", agent=agent, assigned_check=policy_diskcheck ) + url = f"/automation/checks/{policy_diskcheck.pk}/status/" resp = self.client.get(url, format="json") - serializer = PolicyCheckStatusSerializer([managed_check], many=True) + serializer = PolicyCheckStatusSerializer([result], many=True) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) def test_policy_overview(self): @@ -207,80 +173,81 @@ def test_policy_overview(self): ) clients = baker.make( "clients.Client", - server_policy=cycle(policies), # type: ignore - workstation_policy=cycle(policies), # type: ignore + server_policy=cycle(policies), + workstation_policy=cycle(policies), _quantity=5, ) baker.make( "clients.Site", - client=cycle(clients), # type: ignore - server_policy=cycle(policies), # type: ignore - workstation_policy=cycle(policies), # type: ignore + client=cycle(clients), + server_policy=cycle(policies), + workstation_policy=cycle(policies), _quantity=4, ) - baker.make("clients.Site", client=cycle(clients), _quantity=3) # type: ignore + baker.make("clients.Site", client=cycle(clients), _quantity=3) resp = self.client.get(url, format="json") - clients = Client.objects.all() + clients = Client.objects.select_related( + "workstation_policy", "server_policy" + ).prefetch_related( + Prefetch( + "sites", + queryset=Site.objects.select_related( + "workstation_policy", "server_policy" + ), + to_attr="filtered_sites", + ) + ) serializer = PolicyOverviewSerializer(clients, many=True) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) def test_get_related(self): policy = baker.make("automation.Policy") - url = f"/automation/policies/{policy.pk}/related/" # type: ignore + url = f"/automation/policies/{policy.pk}/related/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertIsInstance(resp.data["server_clients"], list) # type: ignore - self.assertIsInstance(resp.data["workstation_clients"], list) # type: ignore - self.assertIsInstance(resp.data["server_sites"], list) # type: ignore - self.assertIsInstance(resp.data["workstation_sites"], list) # type: ignore - self.assertIsInstance(resp.data["agents"], list) # type: ignore + self.assertIsInstance(resp.data["server_clients"], list) + self.assertIsInstance(resp.data["workstation_clients"], list) + self.assertIsInstance(resp.data["server_sites"], list) + self.assertIsInstance(resp.data["workstation_sites"], list) + self.assertIsInstance(resp.data["agents"], list) self.check_not_authenticated("get", url) def test_get_policy_task_status(self): - # policy with a task policy = baker.make("automation.Policy") - task = baker.make("autotasks.AutomatedTask", policy=policy) - - # create policy managed tasks - policy_tasks = baker.make( - "autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore - ) + agent = baker.make_recipe("agents.agent", policy=policy) + task = baker.make_recipe("autotasks.task", policy=policy) + result = baker.make("autotasks.TaskResult", task=task, agent=agent) - url = f"/automation/tasks/{task.id}/status/" # type: ignore + url = f"/automation/tasks/{task.id}/status/" - serializer = PolicyTaskStatusSerializer(policy_tasks, many=True) + serializer = PolicyTaskStatusSerializer([result], many=True) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore - self.assertEqual(len(resp.data), 5) # type: ignore + self.assertEqual(resp.data, serializer.data) + self.assertEqual(len(resp.data), 1) self.check_not_authenticated("get", url) @patch("automation.tasks.run_win_policy_autotasks_task.delay") def test_run_win_task(self, mock_task): - + policy = baker.make("automation.Policy") # create managed policy tasks - tasks = baker.make( - "autotasks.AutomatedTask", - managed_by_policy=True, - parent_task=1, - _quantity=6, - ) + task = baker.make_recipe("autotasks.task", policy=policy) - url = "/automation/tasks/1/run/" + url = f"/automation/tasks/{task.id}/run/" resp = self.client.post(url, format="json") self.assertEqual(resp.status_code, 200) - mock_task.assert_called() # type: ignore + mock_task.assert_called_with(task=task.id) self.check_not_authenticated("post", url) @@ -295,7 +262,7 @@ def test_create_new_patch_policy(self): policy = baker.make("automation.Policy") data = { - "policy": policy.pk, # type: ignore + "policy": policy.pk, "critical": "approve", "important": "approve", "moderate": "ignore", @@ -314,17 +281,16 @@ def test_create_new_patch_policy(self): self.check_not_authenticated("post", url) def test_update_patch_policy(self): - # test policy doesn't exist resp = self.client.put("/automation/patchpolicy/500/", format="json") self.assertEqual(resp.status_code, 404) policy = baker.make("automation.Policy") patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy) - url = f"/automation/patchpolicy/{patch_policy.pk}/" # type: ignore + url = f"/automation/patchpolicy/{patch_policy.pk}/" data = { - "policy": policy.pk, # type: ignore + "policy": policy.pk, "critical": "approve", "important": "approve", "moderate": "ignore", @@ -353,10 +319,10 @@ def test_reset_patch_policy(self): } clients = baker.make("clients.Client", _quantity=6) - sites = baker.make("clients.Site", client=cycle(clients), _quantity=10) # type: ignore + sites = baker.make("clients.Site", client=cycle(clients), _quantity=10) agents = baker.make_recipe( "agents.agent", - site=cycle(sites), # type: ignore + site=cycle(sites), _quantity=6, ) @@ -366,24 +332,24 @@ def test_reset_patch_policy(self): ) # test reset agents in site - data = {"site": sites[0].id} # type: ignore + data = {"site": sites[0].id} resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) - agents = Agent.objects.filter(site=sites[0]) # type: ignore + agents = Agent.objects.filter(site=sites[0]) for agent in agents: for k, v in inherit_fields.items(): self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v) # test reset agents in client - data = {"client": clients[1].id} # type: ignore + data = {"client": clients[1].id} resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) - agents = Agent.objects.filter(site__client=clients[1]) # type: ignore + agents = Agent.objects.filter(site__client=clients[1]) for agent in agents: for k, v in inherit_fields.items(): @@ -420,25 +386,6 @@ def test_delete_patch_policy(self): self.check_not_authenticated("delete", url) - @patch("automation.tasks.generate_agent_checks_task.delay") - def test_sync_policy(self, generate_checks): - url = "/automation/sync/" - - # test invalid data - data = {"invalid": 7} - - resp = self.client.post(url, data, format="json") - self.assertEqual(resp.status_code, 400) - - policy = baker.make("automation.Policy", active=True) - data = {"policy": policy.pk} # type: ignore - - resp = self.client.post(url, data, format="json") - self.assertEqual(resp.status_code, 200) - generate_checks.assert_called_with(policy=policy.pk, create_tasks=True) # type: ignore - - self.check_not_authenticated("post", url) - class TestPolicyTasks(TacticalTestCase): def setUp(self): @@ -446,708 +393,314 @@ def setUp(self): self.setup_coresettings() def test_policy_related(self): - # Get Site and Client from an agent in list clients = baker.make("clients.Client", _quantity=5) - sites = baker.make("clients.Site", client=cycle(clients), _quantity=25) # type: ignore + sites = baker.make("clients.Site", client=cycle(clients), _quantity=25) server_agents = baker.make_recipe( "agents.server_agent", - site=cycle(sites), # type: ignore + site=cycle(sites), _quantity=25, ) workstation_agents = baker.make_recipe( "agents.workstation_agent", - site=cycle(sites), # type: ignore + site=cycle(sites), _quantity=25, ) policy = baker.make("automation.Policy", active=True) # Add Client to Policy - policy.server_clients.add(server_agents[13].client) # type: ignore - policy.workstation_clients.add(workstation_agents[13].client) # type: ignore + policy.server_clients.add(server_agents[13].client) + policy.workstation_clients.add(workstation_agents[13].client) resp = self.client.get( - f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore + f"/automation/policies/{policy.pk}/related/", format="json" ) self.assertEqual(resp.status_code, 200) - self.assertEquals(len(resp.data["server_clients"]), 1) # type: ignore - self.assertEquals(len(resp.data["server_sites"]), 0) # type: ignore - self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore - self.assertEquals(len(resp.data["workstation_sites"]), 0) # type: ignore - self.assertEquals(len(resp.data["agents"]), 0) # type: ignore + 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) # type: ignore - policy.workstation_sites.add(workstation_agents[10].site) # type: ignore + policy.server_sites.add(server_agents[10].site) + policy.workstation_sites.add(workstation_agents[10].site) resp = self.client.get( - f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore + f"/automation/policies/{policy.pk}/related/", format="json" ) - self.assertEquals(len(resp.data["server_sites"]), 1) # type: ignore - self.assertEquals(len(resp.data["workstation_sites"]), 1) # type: ignore - self.assertEquals(len(resp.data["agents"]), 0) # type: ignore + 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]) # type: ignore - policy.agents.add(workstation_agents[2]) # type: ignore + policy.agents.add(server_agents[2]) + policy.agents.add(workstation_agents[2]) resp = self.client.get( - f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore + f"/automation/policies/{policy.pk}/related/", format="json" ) - self.assertEquals(len(resp.data["agents"]), 2) # type: ignore - - def test_generating_agent_policy_checks(self): - from .tasks import generate_agent_checks_task + self.assertEqual(len(resp.data["agents"]), 2) + def test_getting_agent_policy_checks(self): # setup data policy = baker.make("automation.Policy", active=True) - checks = self.create_checks(policy=policy) + self.create_checks(parent=policy) agent = baker.make_recipe("agents.agent", policy=policy) # test policy assigned to agent - generate_agent_checks_task(policy=policy.id) # type: ignore - - # make sure all checks were created. should be 7 - agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all() - self.assertEquals(len(agent_checks), 7) - - # make sure checks were copied correctly - for check in agent_checks: - - self.assertTrue(check.managed_by_policy) - if check.check_type == "diskspace": - self.assertEqual(check.parent_check, checks[0].id) - self.assertEqual(check.disk, checks[0].disk) - self.assertEqual(check.error_threshold, checks[0].error_threshold) - self.assertEqual(check.warning_threshold, checks[0].warning_threshold) - elif check.check_type == "ping": - self.assertEqual(check.parent_check, checks[1].id) - self.assertEqual(check.ip, checks[1].ip) - elif check.check_type == "cpuload": - self.assertEqual(check.parent_check, checks[2].id) - self.assertEqual(check.error_threshold, checks[2].error_threshold) - self.assertEqual(check.warning_threshold, checks[2].warning_threshold) - elif check.check_type == "memory": - self.assertEqual(check.parent_check, checks[3].id) - self.assertEqual(check.error_threshold, checks[3].error_threshold) - self.assertEqual(check.warning_threshold, checks[3].warning_threshold) - elif check.check_type == "winsvc": - self.assertEqual(check.parent_check, checks[4].id) - self.assertEqual(check.svc_name, checks[4].svc_name) - self.assertEqual(check.svc_display_name, checks[4].svc_display_name) - self.assertEqual(check.svc_policy_mode, checks[4].svc_policy_mode) - elif check.check_type == "script": - self.assertEqual(check.parent_check, checks[5].id) - self.assertEqual(check.script, checks[5].script) - elif check.check_type == "eventlog": - self.assertEqual(check.parent_check, checks[6].id) - self.assertEqual(check.event_id, checks[6].event_id) - self.assertEqual(check.event_type, checks[6].event_type) - - def test_generating_agent_policy_checks_with_enforced(self): - from .tasks import generate_agent_checks_task + self.assertEqual(len(agent.get_checks_from_policies()), 7) + def test_getting_agent_policy_checks_with_enforced(self): # setup data policy = baker.make("automation.Policy", active=True, enforced=True) script = baker.make_recipe("scripts.script") - self.create_checks(policy=policy, script=script) + self.create_checks(parent=policy, script=script) + site = baker.make("clients.Site") agent = baker.make_recipe("agents.agent", site=site, policy=policy) - self.create_checks(agent=agent, script=script) - - generate_agent_checks_task(policy=policy.id, create_tasks=True) # type: ignore - - # make sure each agent check says overriden_by_policy - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14) - self.assertEqual( - Agent.objects.get(pk=agent.id) - .agentchecks.filter(overriden_by_policy=True) - .count(), - 7, - ) - - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - @patch("automation.tasks.generate_agent_checks_task.delay") - def test_generating_agent_policy_checks_by_location( - self, generate_agent_checks_mock, create_task - ): - from automation.tasks import generate_agent_checks_task - - # setup data - policy = baker.make("automation.Policy", active=True) - self.create_checks(policy=policy) - - baker.make( - "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 - ) - - server_agent = baker.make_recipe("agents.server_agent") - workstation_agent = baker.make_recipe("agents.workstation_agent") - - # no checks should be preset on agents - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - - # set workstation policy on client and policy checks should be there - workstation_agent.client.workstation_policy = policy - workstation_agent.client.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - client=workstation_agent.client.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - client=workstation_agent.client.pk, - create_tasks=True, - ) - - # make sure the checks were added - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7 - ) - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) - - # remove workstation policy from client - workstation_agent.client.workstation_policy = None - workstation_agent.client.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - client=workstation_agent.client.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - client=workstation_agent.client.pk, - create_tasks=True, - ) - - # make sure the checks were removed - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) - - # set server policy on client and policy checks should be there - server_agent.client.server_policy = policy - server_agent.client.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - client=server_agent.client.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - client=server_agent.client.pk, - create_tasks=True, - ) - - # make sure checks were added - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7) - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - - # remove server policy from client - server_agent.client.server_policy = None - server_agent.client.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - client=server_agent.client.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - client=server_agent.client.pk, - create_tasks=True, - ) - - # make sure checks were removed - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - - # set workstation policy on site and policy checks should be there - workstation_agent.site.workstation_policy = policy - workstation_agent.site.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - site=workstation_agent.site.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - site=workstation_agent.site.pk, - create_tasks=True, - ) - - # make sure checks were added on workstation - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7 - ) - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) - - # remove workstation policy from site - workstation_agent.site.workstation_policy = None - workstation_agent.site.save() + self.create_checks(parent=agent, script=script) - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - site=workstation_agent.site.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - site=workstation_agent.site.pk, - create_tasks=True, - ) - - # make sure checks were removed - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) + overridden_checks = agent.get_checks_with_policies() + checks = agent.get_checks_with_policies(exclude_overridden=True) - # set server policy on site and policy checks should be there - server_agent.site.server_policy = policy - server_agent.site.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - site=server_agent.site.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - site=server_agent.site.pk, - create_tasks=True, - ) - - # make sure checks were added - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7) - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - - # remove server policy from site - server_agent.site.server_policy = None - server_agent.site.save() - - # should trigger task in save method on core - generate_agent_checks_mock.assert_called_with( - site=server_agent.site.pk, - create_tasks=True, - ) - generate_agent_checks_mock.reset_mock() - - generate_agent_checks_task( - site=server_agent.site.pk, - create_tasks=True, - ) - - # make sure checks were removed - self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0) - self.assertEqual( - Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 - ) - - @patch("automation.tasks.generate_agent_checks_task.delay") - def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock): - from core.models import CoreSettings - - from .tasks import generate_agent_checks_task + # make sure each agent check says overridden_by_policy + self.assertEqual(len(checks), 7) + self.assertEqual(len(overridden_checks), 14) + def test_getting_agent_tasks(self): # setup data policy = baker.make("automation.Policy", active=True) - self.create_checks(policy=policy) - - server_agents = baker.make_recipe("agents.server_agent", _quantity=3) - workstation_agents = baker.make_recipe("agents.workstation_agent", _quantity=4) - core = CoreSettings.objects.first() - core.server_policy = policy - core.save() - - generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True) - generate_agent_checks_mock.reset_mock() - generate_agent_checks_task(all=True, create_tasks=True) - - # all servers should have 7 checks - for agent in server_agents: - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) - - for agent in workstation_agents: - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0) - - core.server_policy = None - core.workstation_policy = policy - core.save() - - generate_agent_checks_mock.assert_any_call(all=True, create_tasks=True) - generate_agent_checks_mock.reset_mock() - generate_agent_checks_task(all=True, create_tasks=True) - - # all workstations should have 7 checks - for agent in server_agents: - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0) - - for agent in workstation_agents: - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) - - core.workstation_policy = None - core.save() - - generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True) - generate_agent_checks_mock.reset_mock() - generate_agent_checks_task(all=True, create_tasks=True) - - # nothing should have the checks - for agent in server_agents: - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0) - - for agent in workstation_agents: - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0) - - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - def update_policy_check_fields(self, create_task): - from .models import Policy - from .tasks import update_policy_check_fields_task - - policy = baker.make("automation.Policy", active=True) - self.create_checks(policy=policy) - agent = baker.make_recipe("agents.server_agent", policy=policy) - - # make sure agent has 7 checks - self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) - - # pick a policy check and update it with new values - ping_check = ( - Policy.objects.get(pk=policy.id) # type: ignore - .policychecks.filter(check_type="ping") - .first() - ) - ping_check.ip = "12.12.12.12" - ping_check.save() - - update_policy_check_fields_task(ping_check.id) - - # make sure policy check was updated on the agent - self.assertEquals( - Agent.objects.get(pk=agent.id) - .agentchecks.filter(parent_check=ping_check.id) - .ip, - "12.12.12.12", - ) - - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - def test_generate_agent_tasks(self, create_task): - from .tasks import generate_agent_autotasks_task - - # create test data - policy = baker.make("automation.Policy", active=True) - tasks = baker.make( - "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 + tasks = baker.make_recipe( + "autotasks.task", policy=policy, name=seq("Task"), _quantity=3 ) agent = baker.make_recipe("agents.server_agent", policy=policy) - generate_agent_autotasks_task(policy=policy.id) # type: ignore - - agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all() + tasks = agent.get_tasks_from_policies() # make sure there are 3 agent tasks - self.assertEqual(len(agent_tasks), 3) - - for task in agent_tasks: - self.assertTrue(task.managed_by_policy) - if task.name == "Task1": - self.assertEqual(task.parent_task, tasks[0].id) # type: ignore - self.assertEqual(task.name, tasks[0].name) # type: ignore - if task.name == "Task2": - self.assertEqual(task.parent_task, tasks[1].id) # type: ignore - self.assertEqual(task.name, tasks[1].name) # type: ignore - if task.name == "Task3": - self.assertEqual(task.parent_task, tasks[2].id) # type: ignore - self.assertEqual(task.name, tasks[2].name) # type: ignore + self.assertEqual(len(tasks), 3) - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - @patch("autotasks.models.AutomatedTask.delete_task_on_agent") - def test_delete_policy_tasks(self, delete_task_on_agent, create_task): - from .tasks import delete_policy_autotasks_task, generate_agent_checks_task - - policy = baker.make("automation.Policy", active=True) - tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) - agent = baker.make_recipe("agents.server_agent", policy=policy) - - generate_agent_checks_task(agents=[agent.pk], create_tasks=True) - - delete_policy_autotasks_task(task=tasks[0].id) # type: ignore - - delete_task_on_agent.assert_called() - - @patch("autotasks.models.AutomatedTask.create_task_on_agent") @patch("autotasks.models.AutomatedTask.run_win_task") - def test_run_policy_task(self, run_win_task, create_task): - from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task + def test_run_policy_task(self, run_win_task): + from .tasks import run_win_policy_autotasks_task policy = baker.make("automation.Policy", active=True) - tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) - agent = baker.make_recipe("agents.server_agent", policy=policy) + task = baker.make_recipe("autotasks.task", policy=policy) + baker.make_recipe("agents.server_agent", policy=policy, _quantity=3) - generate_agent_checks_task(agents=[agent.pk], create_tasks=True) + run_win_policy_autotasks_task(task=task.id) - run_win_policy_autotasks_task(task=tasks[0].id) # type: ignore + # should run for each agent under the policy + self.assertEqual(run_win_task.call_count, 3) - run_win_task.assert_called_once() - - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - @patch("autotasks.models.AutomatedTask.modify_task_on_agent") - def test_update_policy_tasks(self, modify_task_on_agent, create_task): - from .tasks import ( - update_policy_autotasks_fields_task, - generate_agent_checks_task, - ) + def test_update_policy_tasks(self): + from autotasks.models import TaskResult # setup data policy = baker.make("automation.Policy", active=True) - tasks = baker.make( - "autotasks.AutomatedTask", - enabled=True, - policy=policy, - _quantity=3, - ) - agent = baker.make_recipe("agents.server_agent", policy=policy) - - generate_agent_checks_task(agents=[agent.pk], create_tasks=True) - - tasks[0].enabled = False # type: ignore - tasks[0].save() # type: ignore - - update_policy_autotasks_fields_task(task=tasks[0].id) # type: ignore - modify_task_on_agent.assert_not_called() + task = baker.make_recipe("autotasks.task", enabled=True, policy=policy) - self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore - - update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore - modify_task_on_agent.assert_not_called() - - agent.autotasks.update(sync_status="synced") - update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore - modify_task_on_agent.assert_called_once() - - @patch("agents.models.Agent.generate_tasks_from_policies") - @patch("agents.models.Agent.generate_checks_from_policies") - def test_generate_agent_checks_with_agentpks(self, generate_checks, generate_tasks): - from automation.tasks import generate_agent_checks_task - - agents = baker.make_recipe("agents.agent", _quantity=5) - - # reset because creating agents triggers it - generate_checks.reset_mock() - generate_tasks.reset_mock() + agent = baker.make_recipe("agents.server_agent", policy=policy) + task_result = baker.make( + "autotasks.TaskResult", + task=task, + agent=agent, + sync_status=TaskSyncStatus.SYNCED, + ) - generate_agent_checks_task(agents=[agent.pk for agent in agents]) - self.assertEquals(generate_checks.call_count, 5) - generate_tasks.assert_not_called() - generate_checks.reset_mock() + # this change shouldn't trigger the task_result field to sync_status = "notsynced" + task.actions = { + "type": "cmd", + "command": "whoami", + "timeout": 90, + "shell": "cmd", + } + task.save() - generate_agent_checks_task( - agents=[agent.pk for agent in agents], create_tasks=True + self.assertEqual( + TaskResult.objects.get(pk=task_result.id).sync_status, TaskSyncStatus.SYNCED ) - self.assertEquals(generate_checks.call_count, 5) - self.assertEquals(generate_checks.call_count, 5) - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - def test_policy_exclusions(self, create_task): - from .tasks import generate_agent_checks_task + # task result should now be "notsynced" + task.enabled = False + task.save() + self.assertEqual( + TaskResult.objects.get(pk=task_result.id).sync_status, + TaskSyncStatus.NOT_SYNCED, + ) + def test_policy_exclusions(self): # setup data policy = baker.make("automation.Policy", active=True) baker.make_recipe("checks.memory_check", policy=policy) - task = baker.make("autotasks.AutomatedTask", policy=policy) + baker.make_recipe("autotasks.task", policy=policy) agent = baker.make_recipe( - "agents.agent", policy=policy, monitoring_type="server" + "agents.agent", policy=policy, monitoring_type=AgentMonType.SERVER ) - generate_agent_checks_task(agents=[agent.pk], create_tasks=True) + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # make sure related agents on policy returns correctly - self.assertEqual(policy.related_agents().count(), 1) # type: ignore - self.assertEqual(agent.agentchecks.count(), 1) # type: ignore - self.assertEqual(agent.autotasks.count(), 1) # type: ignore + self.assertEqual(policy.related_agents().count(), 1) + self.assertEqual(len(checks), 1) + self.assertEqual(len(tasks), 1) # add agent to policy exclusions - policy.excluded_agents.set([agent]) # type: ignore + policy.excluded_agents.set([agent]) - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertEqual(policy.related_agents().count(), 0) # type: ignore - self.assertEqual(agent.agentchecks.count(), 0) # type: ignore + self.assertEqual(policy.related_agents().count(), 0) + self.assertEqual(len(checks), 0) + self.assertEqual(len(tasks), 0) # delete agent tasks - agent.autotasks.all().delete() - policy.excluded_agents.clear() # type: ignore + policy.excluded_agents.clear() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # make sure related agents on policy returns correctly - self.assertEqual(policy.related_agents().count(), 1) # type: ignore - self.assertEqual(agent.agentchecks.count(), 1) # type: ignore - self.assertEqual(agent.autotasks.count(), 1) # type: ignore + self.assertEqual(policy.related_agents().count(), 1) + self.assertEqual(len(checks), 1) + self.assertEqual(len(tasks), 1) # add policy exclusions to site - policy.excluded_sites.set([agent.site]) # type: ignore + policy.excluded_sites.set([agent.site]) - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertEqual(policy.related_agents().count(), 0) # type: ignore - self.assertEqual(agent.agentchecks.count(), 0) # type: ignore + self.assertEqual(policy.related_agents().count(), 0) + self.assertEqual(len(checks), 0) + self.assertEqual(len(tasks), 0) # delete agent tasks and reset - agent.autotasks.all().delete() - policy.excluded_sites.clear() # type: ignore + policy.excluded_sites.clear() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # make sure related agents on policy returns correctly - self.assertEqual(policy.related_agents().count(), 1) # type: ignore - self.assertEqual(agent.agentchecks.count(), 1) # type: ignore - self.assertEqual(agent.autotasks.count(), 1) # type: ignore + self.assertEqual(policy.related_agents().count(), 1) + self.assertEqual(len(checks), 1) + self.assertEqual(len(tasks), 1) # add policy exclusions to client - policy.excluded_clients.set([agent.client]) # type: ignore + policy.excluded_clients.set([agent.client]) - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertEqual(policy.related_agents().count(), 0) # type: ignore - self.assertEqual(agent.agentchecks.count(), 0) # type: ignore + self.assertEqual(policy.related_agents().count(), 0) + self.assertEqual(len(checks), 0) + self.assertEqual(len(tasks), 0) # delete agent tasks and reset - agent.autotasks.all().delete() - policy.excluded_clients.clear() # type: ignore + policy.excluded_clients.clear() agent.policy = None agent.save() # test on default policy - core = CoreSettings.objects.first() + core = get_core_settings() core.server_policy = policy core.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # make sure related agents on policy returns correctly - self.assertEqual(agent.agentchecks.count(), 1) # type: ignore - self.assertEqual(agent.autotasks.count(), 1) # type: ignore + self.assertEqual(policy.related_agents().count(), 1) + self.assertEqual(len(checks), 1) + self.assertEqual(len(tasks), 1) # add policy exclusions to client - policy.excluded_clients.set([agent.client]) # type: ignore + policy.excluded_clients.set([agent.client]) - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertEqual(policy.related_agents().count(), 0) # type: ignore - self.assertEqual(agent.agentchecks.count(), 0) # type: ignore + self.assertEqual(policy.related_agents().count(), 0) + self.assertEqual(len(checks), 0) + self.assertEqual(len(tasks), 0) - @patch("autotasks.models.AutomatedTask.create_task_on_agent") - def test_policy_inheritance_blocking(self, create_task): + def test_policy_inheritance_blocking(self): # setup data policy = baker.make("automation.Policy", active=True) baker.make_recipe("checks.memory_check", policy=policy) - baker.make("autotasks.AutomatedTask", policy=policy) - agent = baker.make_recipe("agents.agent", monitoring_type="server") + baker.make_recipe("autotasks.task", policy=policy) + agent = baker.make_recipe("agents.agent", monitoring_type=AgentMonType.SERVER) - core = CoreSettings.objects.first() + core = get_core_settings() core.server_policy = policy core.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # should get policies from default policy - self.assertTrue(agent.autotasks.all()) - self.assertTrue(agent.agentchecks.all()) + self.assertTrue(checks) + self.assertTrue(tasks) # test client blocking inheritance agent.site.client.block_policy_inheritance = True agent.site.client.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertFalse(agent.autotasks.all()) - self.assertFalse(agent.agentchecks.all()) + self.assertFalse(checks) + self.assertFalse(tasks) agent.site.client.server_policy = policy agent.site.client.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # should get policies from client policy - self.assertTrue(agent.autotasks.all()) - self.assertTrue(agent.agentchecks.all()) + self.assertTrue(tasks) + self.assertTrue(checks) # test site blocking inheritance agent.site.block_policy_inheritance = True agent.site.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertFalse(agent.autotasks.all()) - self.assertFalse(agent.agentchecks.all()) + self.assertFalse(tasks) + self.assertFalse(checks) agent.site.server_policy = policy agent.site.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # should get policies from site policy - self.assertTrue(agent.autotasks.all()) - self.assertTrue(agent.agentchecks.all()) + self.assertTrue(tasks) + self.assertTrue(checks) # test agent blocking inheritance agent.block_policy_inheritance = True agent.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() - self.assertFalse(agent.autotasks.all()) - self.assertFalse(agent.agentchecks.all()) + self.assertFalse(tasks) + self.assertFalse(checks) agent.policy = policy agent.save() - agent.generate_checks_from_policies() - agent.generate_tasks_from_policies() + checks = agent.get_checks_with_policies() + tasks = agent.get_tasks_with_policies() # should get policies from agent policy - self.assertTrue(agent.autotasks.all()) - self.assertTrue(agent.agentchecks.all()) - - -class TestAutomationPermission(TacticalTestCase): - def setUp(self): - self.client_setup() - self.setup_coresettings() + self.assertTrue(tasks) + self.assertTrue(checks) diff --git a/api/tacticalrmm/automation/urls.py b/api/tacticalrmm/automation/urls.py index c60695d4e2..10dff0e8e6 100644 --- a/api/tacticalrmm/automation/urls.py +++ b/api/tacticalrmm/automation/urls.py @@ -1,15 +1,15 @@ from django.urls import path -from . import views -from checks.views import GetAddChecks from autotasks.views import GetAddAutoTasks +from checks.views import GetAddChecks + +from . import views urlpatterns = [ path("policies/", views.GetAddPolicies.as_view()), path("policies//related/", views.GetRelated.as_view()), path("policies/overview/", views.OverviewPolicy.as_view()), path("policies//", views.GetUpdateDeletePolicy.as_view()), - path("sync/", views.PolicySync.as_view()), # alias to get policy checks path("policies//checks/", GetAddChecks.as_view()), # alias to get policy tasks diff --git a/api/tacticalrmm/automation/views.py b/api/tacticalrmm/automation/views.py index 4daa00e46e..316131ae3c 100644 --- a/api/tacticalrmm/automation/views.py +++ b/api/tacticalrmm/automation/views.py @@ -1,23 +1,24 @@ -from agents.models import Agent -from autotasks.models import AutomatedTask -from checks.models import Check -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 rest_framework.exceptions import PermissionDenied -from tacticalrmm.utils import notify_error + +from agents.models import Agent +from autotasks.models import TaskResult +from checks.models import CheckResult +from clients.models import Client, Site from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site from winupdate.models import WinUpdatePolicy from winupdate.serializers import WinUpdatePolicySerializer +from django.db.models import Prefetch from .models import Policy from .permissions import AutomationPolicyPerms from .serializers import ( PolicyCheckStatusSerializer, - PolicyRelatedSerializer, PolicyOverviewSerializer, + PolicyRelatedSerializer, PolicySerializer, PolicyTableSerializer, PolicyTaskStatusSerializer, @@ -28,7 +29,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( @@ -50,9 +53,9 @@ def post(self, request): check.create_policy_check(policy=policy) tasks = copyPolicy.autotasks.all() - for task in tasks: - task.create_policy_task(policy=policy) + if not task.assigned_check: + task.create_policy_task(policy=policy) return Response("ok") @@ -66,22 +69,12 @@ def get(self, request, pk): return Response(PolicySerializer(policy).data) def put(self, request, pk): - from .tasks import generate_agent_checks_task - policy = get_object_or_404(Policy, pk=pk) serializer = PolicySerializer(instance=policy, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() - # check for excluding objects and in the request and if present generate policies - if ( - "excluded_sites" in request.data.keys() - or "excluded_clients" in request.data.keys() - or "excluded_agents" in request.data.keys() - ): - generate_agent_checks_task.delay(policy=pk, create_tasks=True) - return Response("ok") def delete(self, request, pk): @@ -90,25 +83,10 @@ def delete(self, request, pk): return Response("ok") -class PolicySync(APIView): - def post(self, request): - if "policy" in request.data.keys(): - from automation.tasks import generate_agent_checks_task - - generate_agent_checks_task.delay( - policy=request.data["policy"], create_tasks=True - ) - return Response("ok") - - else: - return notify_error("The request was invalid") - - class PolicyAutoTask(APIView): - # get status of all tasks def get(self, request, task): - tasks = AutomatedTask.objects.filter(parent_task=task) + tasks = TaskResult.objects.filter(task=task) return Response(PolicyTaskStatusSerializer(tasks, many=True).data) # bulk run win tasks associated with policy @@ -123,20 +101,30 @@ class PolicyCheck(APIView): permission_classes = [IsAuthenticated, AutomationPolicyPerms] def get(self, request, check): - checks = Check.objects.filter(parent_check=check) + checks = CheckResult.objects.filter(assigned_check=check) return Response(PolicyCheckStatusSerializer(checks, many=True).data) class OverviewPolicy(APIView): def get(self, request): - - clients = Client.objects.all() + clients = ( + Client.objects.filter_by_role(request.user) + .select_related("workstation_policy", "server_policy") + .prefetch_related( + Prefetch( + "sites", + queryset=Site.objects.select_related( + "workstation_policy", "server_policy" + ), + to_attr="filtered_sites", + ) + ) + ) return Response(PolicyOverviewSerializer(clients, many=True).data) class GetRelated(APIView): def get(self, request, pk): - policy = ( Policy.objects.filter(pk=pk) .prefetch_related( @@ -155,13 +143,14 @@ def get(self, request, pk): class UpdatePatchPolicy(APIView): permission_classes = [IsAuthenticated, AutomationPolicyPerms] + # create new patch policy def post(self, request): policy = get_object_or_404(Policy, pk=request.data["policy"]) serializer = WinUpdatePolicySerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) - serializer.policy = policy # type: ignore + serializer.policy = policy serializer.save() return Response("ok") @@ -188,13 +177,12 @@ def delete(self, request, pk): class ResetPatchPolicy(APIView): # bulk reset agent patch policy def post(self, request): - if "client" in request.data: if not _has_perm_on_client(request.user, request.data["client"]): raise PermissionDenied() agents = ( - Agent.objects.filter_by_role(request.user) + Agent.objects.filter_by_role(request.user) # type: ignore .prefetch_related("winupdatepolicy") .filter(site__client_id=request.data["client"]) ) @@ -203,13 +191,13 @@ def post(self, request): raise PermissionDenied() agents = ( - Agent.objects.filter_by_role(request.user) + Agent.objects.filter_by_role(request.user) # type: ignore .prefetch_related("winupdatepolicy") .filter(site_id=request.data["site"]) ) else: agents = ( - Agent.objects.filter_by_role(request.user) + Agent.objects.filter_by_role(request.user) # type: ignore .prefetch_related("winupdatepolicy") .only("pk") ) diff --git a/api/tacticalrmm/autotasks/admin.py b/api/tacticalrmm/autotasks/admin.py index ed01ccd062..dacbc8364d 100644 --- a/api/tacticalrmm/autotasks/admin.py +++ b/api/tacticalrmm/autotasks/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin -from .models import AutomatedTask +from .models import AutomatedTask, TaskResult admin.site.register(AutomatedTask) +admin.site.register(TaskResult) diff --git a/api/tacticalrmm/autotasks/baker_recipes.py b/api/tacticalrmm/autotasks/baker_recipes.py new file mode 100644 index 0000000000..1435ee31b9 --- /dev/null +++ b/api/tacticalrmm/autotasks/baker_recipes.py @@ -0,0 +1,5 @@ +from model_bakery.recipe import Recipe + +task = Recipe( + "autotasks.AutomatedTask", +) diff --git a/api/tacticalrmm/autotasks/management/commands/remove_orphaned_tasks.py b/api/tacticalrmm/autotasks/management/commands/remove_orphaned_tasks.py index 5de98dc9f1..8820022fe2 100644 --- a/api/tacticalrmm/autotasks/management/commands/remove_orphaned_tasks.py +++ b/api/tacticalrmm/autotasks/management/commands/remove_orphaned_tasks.py @@ -1,6 +1,5 @@ from django.core.management.base import BaseCommand -from agents.models import Agent from autotasks.tasks import remove_orphaned_win_tasks @@ -8,10 +7,7 @@ class Command(BaseCommand): help = "Checks for orphaned tasks on all agents and removes them" def handle(self, *args, **kwargs): - agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time") - online = [i for i in agents if i.status == "online"] - for agent in online: - remove_orphaned_win_tasks.delay(agent.pk) + remove_orphaned_win_tasks.s() self.stdout.write( self.style.SUCCESS( diff --git a/api/tacticalrmm/autotasks/migrations/0024_auto_20211214_0040.py b/api/tacticalrmm/autotasks/migrations/0024_auto_20211214_0040.py new file mode 100644 index 0000000000..8085ba7408 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0024_auto_20211214_0040.py @@ -0,0 +1,87 @@ +# Generated by Django 3.2.9 on 2021-12-14 00:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0023_auto_20210917_1954'), + ] + + operations = [ + migrations.RemoveField( + model_name='automatedtask', + name='run_time_days', + ), + migrations.AddField( + model_name='automatedtask', + name='actions', + field=models.JSONField(default=dict), + ), + migrations.AddField( + model_name='automatedtask', + name='daily_interval', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='expire_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='monthly_days_of_month', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='monthly_months_of_year', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='monthly_weeks_of_month', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='random_task_delay', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='stop_task_at_duration_end', + field=models.BooleanField(blank=True, default=False), + ), + migrations.AddField( + model_name='automatedtask', + name='task_instance_policy', + field=models.PositiveSmallIntegerField(blank=True, default=1), + ), + migrations.AddField( + model_name='automatedtask', + name='task_repetition_duration', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='task_repetition_interval', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name='automatedtask', + name='weekly_interval', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='automatedtask', + name='task_type', + field=models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('monthlydow', 'Monthly Day of Week'), ('checkfailure', 'On Check Failure'), ('manual', 'Manual'), ('runonce', 'Run Once')], default='manual', max_length=100), + ), + migrations.AlterField( + model_name='automatedtask', + name='timeout', + field=models.PositiveIntegerField(blank=True, default=120), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0025_automatedtask_continue_on_error.py b/api/tacticalrmm/autotasks/migrations/0025_automatedtask_continue_on_error.py new file mode 100644 index 0000000000..b54bc50462 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0025_automatedtask_continue_on_error.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2021-12-29 14:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0024_auto_20211214_0040'), + ] + + operations = [ + migrations.AddField( + model_name='automatedtask', + name='continue_on_error', + field=models.BooleanField(default=True), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0026_alter_automatedtask_monthly_days_of_month.py b/api/tacticalrmm/autotasks/migrations/0026_alter_automatedtask_monthly_days_of_month.py new file mode 100644 index 0000000000..bceed23a8c --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0026_alter_automatedtask_monthly_days_of_month.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2021-12-30 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0025_automatedtask_continue_on_error'), + ] + + operations = [ + migrations.AlterField( + model_name='automatedtask', + name='monthly_days_of_month', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0027_auto_20220107_0643.py b/api/tacticalrmm/autotasks/migrations/0027_auto_20220107_0643.py new file mode 100644 index 0000000000..a5bd91d9de --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0027_auto_20220107_0643.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.11 on 2022-01-07 06:43 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0026_alter_automatedtask_monthly_days_of_month'), + ] + + operations = [ + migrations.AlterField( + model_name='automatedtask', + name='daily_interval', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(255)]), + ), + migrations.AlterField( + model_name='automatedtask', + name='weekly_interval', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(52)]), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0028_alter_automatedtask_actions.py b/api/tacticalrmm/autotasks/migrations/0028_alter_automatedtask_actions.py new file mode 100644 index 0000000000..5e6262447b --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0028_alter_automatedtask_actions.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-01-09 21:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0027_auto_20220107_0643'), + ] + + operations = [ + migrations.AlterField( + model_name='automatedtask', + name='actions', + field=models.JSONField(default=list), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0029_alter_automatedtask_task_type.py b/api/tacticalrmm/autotasks/migrations/0029_alter_automatedtask_task_type.py new file mode 100644 index 0000000000..7b388e6ad9 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0029_alter_automatedtask_task_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2022-01-10 01:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0028_alter_automatedtask_actions'), + ] + + operations = [ + migrations.AlterField( + model_name='automatedtask', + name='task_type', + field=models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('monthlydow', 'Monthly Day of Week'), ('checkfailure', 'On Check Failure'), ('manual', 'Manual'), ('runonce', 'Run Once'), ('scheduled', 'Scheduled')], default='manual', max_length=100), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0030_auto_20220401_2244.py b/api/tacticalrmm/autotasks/migrations/0030_auto_20220401_2244.py new file mode 100644 index 0000000000..4aec71b87c --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0030_auto_20220401_2244.py @@ -0,0 +1,99 @@ +# Generated by Django 3.2.12 on 2022-04-01 22:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("checks", "0025_auto_20210917_1954"), + ("agents", "0046_alter_agenthistory_command"), + ("autotasks", "0029_alter_automatedtask_task_type"), + ] + + operations = [ + migrations.RemoveField( + model_name="automatedtask", + name="retvalue", + ), + migrations.AlterField( + model_name="automatedtask", + name="assigned_check", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="assignedtasks", + to="checks.check", + ), + ), + migrations.AlterField( + model_name="automatedtask", + name="win_task_name", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.CreateModel( + name="TaskResult", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("retcode", models.IntegerField(blank=True, null=True)), + ("stdout", models.TextField(blank=True, null=True)), + ("stderr", models.TextField(blank=True, null=True)), + ("execution_time", models.CharField(default="0.0000", max_length=100)), + ("last_run", models.DateTimeField(blank=True, null=True)), + ( + "status", + models.CharField( + choices=[ + ("passing", "Passing"), + ("failing", "Failing"), + ("pending", "Pending"), + ], + default="pending", + max_length=30, + ), + ), + ( + "sync_status", + models.CharField( + choices=[ + ("synced", "Synced With Agent"), + ("notsynced", "Waiting On Agent Checkin"), + ("pendingdeletion", "Pending Deletion on Agent"), + ("initial", "Initial Task Sync"), + ], + default="initial", + max_length=100, + ), + ), + ( + "agent", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="taskresults", + to="agents.agent", + ), + ), + ( + "task", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="taskresults", + to="autotasks.automatedtask", + ), + ), + ], + options={ + "unique_together": {("agent", "task")}, + }, + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0031_auto_20220401_2249.py b/api/tacticalrmm/autotasks/migrations/0031_auto_20220401_2249.py new file mode 100644 index 0000000000..14cb28749d --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0031_auto_20220401_2249.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.12 on 2022-04-01 22:49 + +from django.db import migrations, transaction +from django.db.utils import IntegrityError + + +def migrate_task_results(apps, schema_editor): + AutomatedTask = apps.get_model("autotasks", "AutomatedTask") + TaskResult = apps.get_model("autotasks", "TaskResult") + for task in AutomatedTask.objects.exclude(agent=None): + + try: + with transaction.atomic(): + if task.managed_by_policy: + TaskResult.objects.create( + task_id=task.parent_task, + agent_id=task.agent_id, + retcode=task.retcode, + stdout=task.stdout, + stderr=task.stderr, + execution_time=task.execution_time, + last_run=task.last_run, + status=task.status, + sync_status=task.sync_status, + ) + else: + TaskResult.objects.create( + task_id=task.id, + agent_id=task.agent.id, + retcode=task.retcode, + stdout=task.stdout, + stderr=task.stderr, + execution_time=task.execution_time, + last_run=task.last_run, + status=task.status, + sync_status=task.sync_status, + ) + except IntegrityError: + continue + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("autotasks", "0030_auto_20220401_2244"), + ] + + operations = [ + migrations.RunPython(migrate_task_results), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0032_auto_20220401_2301.py b/api/tacticalrmm/autotasks/migrations/0032_auto_20220401_2301.py new file mode 100644 index 0000000000..06e4c1ea41 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0032_auto_20220401_2301.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.12 on 2022-04-01 23:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0031_auto_20220401_2249'), + ] + + operations = [ + migrations.RemoveField( + model_name='automatedtask', + name='execution_time', + ), + migrations.RemoveField( + model_name='automatedtask', + name='last_run', + ), + migrations.RemoveField( + model_name='automatedtask', + name='parent_task', + ), + migrations.RemoveField( + model_name='automatedtask', + name='retcode', + ), + migrations.RemoveField( + model_name='automatedtask', + name='status', + ), + migrations.RemoveField( + model_name='automatedtask', + name='stderr', + ), + migrations.RemoveField( + model_name='automatedtask', + name='stdout', + ), + migrations.RemoveField( + model_name='automatedtask', + name='sync_status', + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0033_auto_20220402_0041.py b/api/tacticalrmm/autotasks/migrations/0033_auto_20220402_0041.py new file mode 100644 index 0000000000..64a3eab820 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0033_auto_20220402_0041.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.12 on 2022-04-02 00:41 + +from django.db import migrations +from django.utils.timezone import make_aware + +from tacticalrmm.constants import TaskType + + +def migrate_script_data(apps, schema_editor): + AutomatedTask = apps.get_model("autotasks", "AutomatedTask") + # convert autotask to the new format + for task in AutomatedTask.objects.all(): + try: + edited = False + + # convert scheduled task_type + if task.task_type == TaskType.SCHEDULED: + task.task_type = TaskType.DAILY + task.run_time_date = make_aware(task.run_time_minute.strptime("%H:%M")) + task.daily_interval = 1 + edited = True + + # convert actions + if not task.actions: + if not task.script: + task.delete() + + task.actions = [ + { + "type": "script", + "script": task.script.pk, + "script_args": task.script_args, + "timeout": task.timeout, + "name": task.script.name, + } + ] + edited = True + + if edited: + task.save() + except: + continue + + +class Migration(migrations.Migration): + + dependencies = [ + ("autotasks", "0032_auto_20220401_2301"), + ] + + operations = [ + migrations.RunPython(migrate_script_data), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0034_auto_20220402_0046.py b/api/tacticalrmm/autotasks/migrations/0034_auto_20220402_0046.py new file mode 100644 index 0000000000..a6fac59620 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0034_auto_20220402_0046.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2022-04-02 00:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0033_auto_20220402_0041'), + ] + + operations = [ + migrations.RemoveField( + model_name='automatedtask', + name='script', + ), + migrations.RemoveField( + model_name='automatedtask', + name='script_args', + ), + migrations.RemoveField( + model_name='automatedtask', + name='timeout', + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0035_auto_20220415_1818.py b/api/tacticalrmm/autotasks/migrations/0035_auto_20220415_1818.py new file mode 100644 index 0000000000..1b41506979 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0035_auto_20220415_1818.py @@ -0,0 +1,39 @@ +# Generated by Django 4.0.3 on 2022-04-15 18:18 + +from django.db import migrations +from django.db.models import Count + +from autotasks.models import generate_task_name +from tacticalrmm.constants import TaskSyncStatus + + +def check_for_win_task_name_duplicates(apps, schema_editor): + AutomatedTask = apps.get_model("autotasks", "AutomatedTask") + TaskResult = apps.get_model("autotasks", "TaskResult") + + duplicate_tasks = ( + AutomatedTask.objects.values("win_task_name") + .annotate(records=Count("win_task_name")) + .filter(records__gt=1) + ) + for task in duplicate_tasks: + dups = list(AutomatedTask.objects.filter(win_task_name=task["win_task_name"])) + for x in range(task["records"] - 1): + + dups[x].win_task_name = generate_task_name() + dups[x].save(update_fields=["win_task_name"]) + # update task_result sync status + TaskResult.objects.filter(task=dups[x]).update( + sync_status=TaskSyncStatus.NOT_SYNCED + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("autotasks", "0034_auto_20220402_0046"), + ] + + operations = [ + migrations.RunPython(check_for_win_task_name_duplicates), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0036_alter_automatedtask_win_task_name.py b/api/tacticalrmm/autotasks/migrations/0036_alter_automatedtask_win_task_name.py new file mode 100644 index 0000000000..3974d79a61 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0036_alter_automatedtask_win_task_name.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.3 on 2022-04-15 20:52 + +from django.db import migrations, models + +import autotasks.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0035_auto_20220415_1818'), + ] + + operations = [ + migrations.AlterField( + model_name='automatedtask', + name='win_task_name', + field=models.CharField(blank=True, default=autotasks.models.generate_task_name, max_length=255, unique=True), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0037_alter_taskresult_retcode.py b/api/tacticalrmm/autotasks/migrations/0037_alter_taskresult_retcode.py new file mode 100644 index 0000000000..26d56e2a51 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0037_alter_taskresult_retcode.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2022-06-29 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0036_alter_automatedtask_win_task_name'), + ] + + operations = [ + migrations.AlterField( + model_name='taskresult', + name='retcode', + field=models.BigIntegerField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0038_add_missing_env_vars.py b/api/tacticalrmm/autotasks/migrations/0038_add_missing_env_vars.py new file mode 100644 index 0000000000..1c168c9d96 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0038_add_missing_env_vars.py @@ -0,0 +1,33 @@ +from django.db import migrations + + +def migrate_env_vars(apps, schema_editor): + AutomatedTask = apps.get_model("autotasks", "AutomatedTask") + for task in AutomatedTask.objects.iterator(chunk_size=30): + try: + tmp = [] + if isinstance(task.actions, list) and task.actions: + for t in task.actions: + if isinstance(t, dict): + if t["type"] == "script": + try: + t["env_vars"] + except KeyError: + t["env_vars"] = [] + tmp.append(t) + if tmp: + task.actions = tmp + task.save(update_fields=["actions"]) + except Exception as e: + print(f"ERROR: {e}") + + +class Migration(migrations.Migration): + + dependencies = [ + ("autotasks", "0037_alter_taskresult_retcode"), + ] + + operations = [ + migrations.RunPython(migrate_env_vars), + ] diff --git a/api/tacticalrmm/autotasks/models.py b/api/tacticalrmm/autotasks/models.py index 1891134668..0389d9a443 100644 --- a/api/tacticalrmm/autotasks/models.py +++ b/api/tacticalrmm/autotasks/models.py @@ -1,50 +1,49 @@ import asyncio -import datetime as dt import random import string -from typing import List +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from zoneinfo import ZoneInfo -import pytz -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 django.db.models.fields import DateTimeField +from django.db.models.fields.json import JSONField from django.db.utils import DatabaseError from django.utils import timezone as djangotime + +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, + AlertSeverity, + DebugLogType, + TaskStatus, + TaskSyncStatus, + TaskType, +) + +if TYPE_CHECKING: + from automation.models import Policy + from alerts.models import Alert, AlertTemplate + from agents.models import Agent + from checks.models import Check + from tacticalrmm.models import PermissionQuerySet -from packaging import version as pyver -from tacticalrmm.utils import bitdays_to_string - -RUN_TIME_DAY_CHOICES = [ - (0, "Monday"), - (1, "Tuesday"), - (2, "Wednesday"), - (3, "Thursday"), - (4, "Friday"), - (5, "Saturday"), - (6, "Sunday"), -] - -TASK_TYPE_CHOICES = [ - ("scheduled", "Scheduled"), - ("checkfailure", "On Check Failure"), - ("manual", "Manual"), - ("runonce", "Run Once"), -] - -SYNC_STATUS_CHOICES = [ - ("synced", "Synced With Agent"), - ("notsynced", "Waiting On Agent Checkin"), - ("pendingdeletion", "Pending Deletion on Agent"), - ("initial", "Initial Task Sync"), -] - -TASK_STATUS_CHOICES = [ - ("passing", "Passing"), - ("failing", "Failing"), - ("pending", "Pending"), -] +from tacticalrmm.utils import ( + bitdays_to_string, + bitmonthdays_to_string, + bitmonths_to_string, + bitweeks_to_string, + convert_to_iso_duration, +) + + +def generate_task_name() -> str: + chars = string.ascii_letters + return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35)) class AutomatedTask(BaseAuditModel): @@ -71,150 +70,149 @@ class AutomatedTask(BaseAuditModel): blank=True, on_delete=models.SET_NULL, ) - script = models.ForeignKey( - "scripts.Script", - null=True, - blank=True, - related_name="autoscript", - on_delete=models.SET_NULL, - ) - script_args = ArrayField( - models.CharField(max_length=255, null=True, blank=True), - null=True, - blank=True, - default=list, - ) + + # format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": [], "env_vars": []}, {"type": "cmd", "command": "whoami", "timeout": 90}] + actions = JSONField(default=list) assigned_check = models.ForeignKey( "checks.Check", null=True, blank=True, - related_name="assignedtask", + related_name="assignedtasks", on_delete=models.SET_NULL, ) name = models.CharField(max_length=255) - run_time_bit_weekdays = models.IntegerField(null=True, blank=True) - # run_time_days is deprecated, use bit weekdays - run_time_days = ArrayField( - models.IntegerField(choices=RUN_TIME_DAY_CHOICES, null=True, blank=True), - null=True, - blank=True, - default=list, - ) - run_time_minute = models.CharField(max_length=5, null=True, blank=True) - task_type = models.CharField( - max_length=100, choices=TASK_TYPE_CHOICES, default="manual" - ) collector_all_output = models.BooleanField(default=False) - run_time_date = DateTimeField(null=True, blank=True) - remove_if_not_scheduled = models.BooleanField(default=False) - run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7 - managed_by_policy = models.BooleanField(default=False) - parent_task = models.PositiveIntegerField(null=True, blank=True) - win_task_name = models.CharField(max_length=255, null=True, blank=True) - timeout = models.PositiveIntegerField(default=120) - retvalue = models.TextField(null=True, blank=True) - retcode = models.IntegerField(null=True, blank=True) - stdout = models.TextField(null=True, blank=True) - stderr = models.TextField(null=True, blank=True) - execution_time = models.CharField(max_length=100, default="0.0000") - last_run = models.DateTimeField(null=True, blank=True) enabled = models.BooleanField(default=True) - status = models.CharField( - max_length=30, choices=TASK_STATUS_CHOICES, default="pending" - ) - sync_status = models.CharField( - max_length=100, choices=SYNC_STATUS_CHOICES, default="initial" - ) + continue_on_error = models.BooleanField(default=True) alert_severity = models.CharField( - max_length=30, choices=SEVERITY_CHOICES, default="info" + max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO ) email_alert = models.BooleanField(default=False) text_alert = models.BooleanField(default=False) dashboard_alert = models.BooleanField(default=False) - def __str__(self): + # options sent to agent for task creation + # general task settings + task_type = models.CharField( + max_length=100, choices=TaskType.choices, default=TaskType.MANUAL + ) + win_task_name = models.CharField( + max_length=255, unique=True, blank=True, default=generate_task_name + ) # should be changed to unique=True + run_time_date = DateTimeField(null=True, blank=True) + expire_date = DateTimeField(null=True, blank=True) + + # daily + daily_interval = models.PositiveSmallIntegerField( + blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(255)] + ) + + # weekly + run_time_bit_weekdays = models.IntegerField(null=True, blank=True) + weekly_interval = models.PositiveSmallIntegerField( + blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(52)] + ) + run_time_minute = models.CharField( + max_length=5, null=True, blank=True + ) # deprecated + + # monthly + monthly_days_of_month = models.PositiveBigIntegerField(blank=True, null=True) + monthly_months_of_year = models.PositiveIntegerField(blank=True, null=True) + + # monthly days of week + monthly_weeks_of_month = models.PositiveSmallIntegerField(blank=True, null=True) + + # additional task settings + task_repetition_duration = models.CharField(max_length=10, null=True, blank=True) + task_repetition_interval = models.CharField(max_length=10, null=True, blank=True) + stop_task_at_duration_end = models.BooleanField(blank=True, default=False) + random_task_delay = models.CharField(max_length=10, null=True, blank=True) + remove_if_not_scheduled = models.BooleanField(default=False) + run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7 + task_instance_policy = models.PositiveSmallIntegerField(blank=True, default=1) + + # deprecated + managed_by_policy = models.BooleanField(default=False) + + # non-database property + task_result: "Union[TaskResult, Dict[None, None]]" = {} + + def __str__(self) -> str: return self.name - def save(self, *args, **kwargs): - from autotasks.tasks import enable_or_disable_win_task - from automation.tasks import update_policy_autotasks_fields_task + def save(self, *args, **kwargs) -> None: + # if task is a policy task clear cache on everything + if self.policy: + cache.delete_many_pattern("site_*_tasks") + cache.delete_many_pattern("agent_*_tasks") - # get old agent if exists + # get old task if exists old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs) - # check if automated task was enabled/disabled and send celery task - if old_task and old_task.enabled != self.enabled: - if self.agent: - enable_or_disable_win_task.delay(pk=self.pk) - - # check if automated task was enabled/disabled and send celery task - elif old_task.policy: - update_policy_autotasks_fields_task.delay( - task=self.pk, update_agent=True - ) - # check if policy task was edited and then check if it was a field worth copying to rest of agent tasks - elif old_task and old_task.policy: - for field in self.policy_fields_to_copy: + # check if fields were updated that require a sync to the agent and set status to notsynced + if old_task: + for field in self.fields_that_trigger_task_update_on_agent: if getattr(self, field) != getattr(old_task, field): - update_policy_autotasks_fields_task.delay(task=self.pk) - break + if self.policy: + TaskResult.objects.exclude( + sync_status=TaskSyncStatus.INITIAL + ).filter(task__policy_id=self.policy.id).update( + sync_status=TaskSyncStatus.NOT_SYNCED + ) + else: + TaskResult.objects.filter(agent=self.agent, task=self).update( + sync_status=TaskSyncStatus.NOT_SYNCED + ) + + def delete(self, *args, **kwargs): + # if task is a policy task clear cache on everything + if self.policy: + cache.delete_many_pattern("site_*_tasks") + cache.delete_many_pattern("agent_*_tasks") + + super(AutomatedTask, self).delete( + *args, + **kwargs, + ) @property - def schedule(self): - if self.task_type == "manual": + def schedule(self) -> Optional[str]: + if self.task_type == TaskType.MANUAL: return "Manual" - elif self.task_type == "checkfailure": + elif self.task_type == TaskType.CHECK_FAILURE: return "Every time check fails" - elif self.task_type == "runonce": + elif self.task_type == TaskType.RUN_ONCE: return f'Run once on {self.run_time_date.strftime("%m/%d/%Y %I:%M%p")}' - elif self.task_type == "scheduled": - run_time_nice = dt.datetime.strptime( - self.run_time_minute, "%H:%M" - ).strftime("%I:%M %p") - + elif self.task_type == TaskType.DAILY: + run_time_nice = self.run_time_date.strftime("%I:%M%p") + if self.daily_interval == 1: + return f"Daily at {run_time_nice}" + else: + return f"Every {self.daily_interval} days at {run_time_nice}" + elif self.task_type == TaskType.WEEKLY: + run_time_nice = self.run_time_date.strftime("%I:%M%p") days = bitdays_to_string(self.run_time_bit_weekdays) - return f"{days} at {run_time_nice}" - - @property - def last_run_as_timezone(self): - if self.last_run is not None and self.agent is not None: - return self.last_run.astimezone( - pytz.timezone(self.agent.timezone) - ).strftime("%b-%d-%Y - %H:%M") - - return self.last_run + if self.weekly_interval != 1: + return f"{days} at {run_time_nice}" + else: + return f"{days} at {run_time_nice} every {self.weekly_interval} weeks" + elif self.task_type == TaskType.MONTHLY: + run_time_nice = self.run_time_date.strftime("%I:%M%p") + months = bitmonths_to_string(self.monthly_months_of_year) + days = bitmonthdays_to_string(self.monthly_days_of_month) + return f"Runs on {months} on days {days} at {run_time_nice}" + elif self.task_type == TaskType.MONTHLY_DOW: + run_time_nice = self.run_time_date.strftime("%I:%M%p") + months = bitmonths_to_string(self.monthly_months_of_year) + weeks = bitweeks_to_string(self.monthly_weeks_of_month) + days = bitdays_to_string(self.run_time_bit_weekdays) + return f"Runs on {months} on {weeks} on {days} at {run_time_nice}" - # These fields will be duplicated on the agent tasks that are managed by a policy @property - def policy_fields_to_copy(self) -> List[str]: - return [ - "alert_severity", - "email_alert", - "text_alert", - "dashboard_alert", - "script", - "script_args", - "assigned_check", - "name", - "run_time_days", - "run_time_minute", - "run_time_bit_weekdays", - "run_time_date", - "task_type", - "win_task_name", - "timeout", - "enabled", - "remove_if_not_scheduled", - "run_asap_after_missed", - "custom_field", - "collector_all_output", - ] - - @staticmethod - def generate_task_name(): - chars = string.ascii_letters - return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35)) + def fields_that_trigger_task_update_on_agent(self) -> List[str]: + return FIELDS_TRIGGER_TASK_UPDATE_AGENT @staticmethod def serialize(task): @@ -223,237 +221,248 @@ def serialize(task): return TaskAuditSerializer(task).data - def create_policy_task(self, agent=None, policy=None, assigned_check=None): - - # added to allow new policy tasks to be assigned to check only when the agent check exists already - if ( - self.assigned_check - and agent - and agent.agentchecks.filter(parent_check=self.assigned_check.id).exists() - ): - assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.id) - - # if policy is present, then this task is being copied to another policy - # if agent is present, then this task is being created on an agent from a policy - # exit if neither are set or if both are set - # also exit if assigned_check is set because this task will be created when the check is - if ( - (not agent and not policy) - or (agent and policy) - or (self.assigned_check and not assigned_check) - ): - return - + def create_policy_task( + self, policy: "Policy", assigned_check: "Optional[Check]" = None + ) -> None: + # Copies certain properties on this task (self) to a new task and sets it to the supplied Policy task = AutomatedTask.objects.create( - agent=agent, policy=policy, - managed_by_policy=bool(agent), - parent_task=(self.pk if agent else None), assigned_check=assigned_check, ) - for field in self.policy_fields_to_copy: - if field != "assigned_check": - setattr(task, field, getattr(self, field)) + for field in POLICY_TASK_FIELDS_TO_COPY: + setattr(task, field, getattr(self, field)) task.save() - if agent: - task.create_task_on_agent() + # agent version >= 1.8.0 + def generate_nats_task_payload( + self, agent: "Optional[Agent]" = None, editing: bool = False + ) -> Dict[str, Any]: + task = { + "pk": self.pk, + "type": "rmm", + "name": self.win_task_name, + "overwrite_task": editing, + "enabled": self.enabled, + "trigger": self.task_type + if self.task_type != TaskType.CHECK_FAILURE + else TaskType.MANUAL, + "multiple_instances": self.task_instance_policy or 0, + "delete_expired_task_after": self.remove_if_not_scheduled + if self.expire_date + else False, + "start_when_available": self.run_asap_after_missed + if self.task_type != TaskType.RUN_ONCE + else True, + } + + if self.task_type in ( + TaskType.RUN_ONCE, + TaskType.DAILY, + TaskType.WEEKLY, + TaskType.MONTHLY, + TaskType.MONTHLY_DOW, + ): + # set runonce task in future if creating and run_asap_after_missed is set + if ( + not editing + and self.task_type == TaskType.RUN_ONCE + and self.run_asap_after_missed + and agent + and self.run_time_date.replace(tzinfo=ZoneInfo(agent.timezone)) + < djangotime.now().astimezone(ZoneInfo(agent.timezone)) + ): + self.run_time_date = ( + djangotime.now() + djangotime.timedelta(minutes=5) + ).astimezone(ZoneInfo(agent.timezone)) + + task["start_year"] = int(self.run_time_date.strftime("%Y")) + task["start_month"] = int(self.run_time_date.strftime("%-m")) + task["start_day"] = int(self.run_time_date.strftime("%-d")) + task["start_hour"] = int(self.run_time_date.strftime("%-H")) + task["start_min"] = int(self.run_time_date.strftime("%-M")) + + if self.expire_date: + task["expire_year"] = int(self.expire_date.strftime("%Y")) + task["expire_month"] = int(self.expire_date.strftime("%-m")) + task["expire_day"] = int(self.expire_date.strftime("%-d")) + task["expire_hour"] = int(self.expire_date.strftime("%-H")) + task["expire_min"] = int(self.expire_date.strftime("%-M")) + + if self.random_task_delay: + task["random_delay"] = convert_to_iso_duration(self.random_task_delay) + + if self.task_repetition_interval: + task["repetition_interval"] = convert_to_iso_duration( + self.task_repetition_interval + ) + task["repetition_duration"] = convert_to_iso_duration( + self.task_repetition_duration + ) + task["stop_at_duration_end"] = self.stop_task_at_duration_end + + if self.task_type == TaskType.DAILY: + task["day_interval"] = self.daily_interval - def create_task_on_agent(self): - from agents.models import Agent + elif self.task_type == TaskType.WEEKLY: + task["week_interval"] = self.weekly_interval + task["days_of_week"] = self.run_time_bit_weekdays - agent = ( - Agent.objects.filter(pk=self.agent.pk) - .only("pk", "version", "hostname", "agent_id") - .first() - ) + elif self.task_type == TaskType.MONTHLY: + # check if "last day is configured" + if self.monthly_days_of_month >= 0x80000000: + task["days_of_month"] = self.monthly_days_of_month - 0x80000000 + task["run_on_last_day_of_month"] = True + else: + task["days_of_month"] = self.monthly_days_of_month + task["run_on_last_day_of_month"] = False - if self.task_type == "scheduled": - nats_data = { - "func": "schedtask", - "schedtaskpayload": { - "type": "rmm", - "trigger": "weekly", - "weekdays": self.run_time_bit_weekdays, - "pk": self.pk, - "name": self.win_task_name, - "hour": dt.datetime.strptime(self.run_time_minute, "%H:%M").hour, - "min": dt.datetime.strptime(self.run_time_minute, "%H:%M").minute, - }, - } - - elif self.task_type == "runonce": - # check if scheduled time is in the past - agent_tz = pytz.timezone(agent.timezone) # type: ignore - task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone( - pytz.utc - ) - now = djangotime.now() - if task_time_utc < now: - self.run_time_date = now.astimezone(agent_tz).replace( - tzinfo=pytz.utc - ) + djangotime.timedelta(minutes=5) - self.save(update_fields=["run_time_date"]) - - nats_data = { - "func": "schedtask", - "schedtaskpayload": { - "type": "rmm", - "trigger": "once", - "pk": self.pk, - "name": self.win_task_name, - "year": int(dt.datetime.strftime(self.run_time_date, "%Y")), - "month": dt.datetime.strftime(self.run_time_date, "%B"), - "day": int(dt.datetime.strftime(self.run_time_date, "%d")), - "hour": int(dt.datetime.strftime(self.run_time_date, "%H")), - "min": int(dt.datetime.strftime(self.run_time_date, "%M")), - }, - } - - if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse( # type: ignore - "1.4.7" - ): - nats_data["schedtaskpayload"]["run_asap_after_missed"] = True - - if self.remove_if_not_scheduled: - nats_data["schedtaskpayload"]["deleteafter"] = True - - elif self.task_type == "checkfailure" or self.task_type == "manual": - nats_data = { - "func": "schedtask", - "schedtaskpayload": { - "type": "rmm", - "trigger": "manual", - "pk": self.pk, - "name": self.win_task_name, - }, - } + task["months_of_year"] = self.monthly_months_of_year + + elif self.task_type == TaskType.MONTHLY_DOW: + task["days_of_week"] = self.run_time_bit_weekdays + task["months_of_year"] = self.monthly_months_of_year + task["weeks_of_month"] = self.monthly_weeks_of_month + + return task + + def create_task_on_agent(self, agent: "Optional[Agent]" = None) -> str: + if self.policy and not agent: + return "agent parameter needs to be passed with policy task" else: - return "error" + agent = agent if self.policy else self.agent + + try: + task_result = TaskResult.objects.get(agent=agent, task=self) + except TaskResult.DoesNotExist: + task_result = TaskResult(agent=agent, task=self) + task_result.save() + + nats_data = { + "func": "schedtask", + "schedtaskpayload": self.generate_nats_task_payload(agent), + } - r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) # type: ignore + r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5)) if r != "ok": - self.sync_status = "initial" - self.save(update_fields=["sync_status"]) + task_result.sync_status = TaskSyncStatus.INITIAL + task_result.save(update_fields=["sync_status"]) DebugLog.warning( agent=agent, - log_type="agent_issues", - message=f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in.", # type: ignore + 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" else: - self.sync_status = "synced" - self.save(update_fields=["sync_status"]) + task_result.sync_status = TaskSyncStatus.SYNCED + task_result.save(update_fields=["sync_status"]) DebugLog.info( agent=agent, - log_type="agent_issues", - message=f"{agent.hostname} task {self.name} was successfully created", # type: ignore + log_type=DebugLogType.AGENT_ISSUES, + message=f"{task_result.agent.hostname} task {self.name} was successfully created", ) return "ok" - def modify_task_on_agent(self): - from agents.models import Agent + def modify_task_on_agent(self, agent: "Optional[Agent]" = None) -> str: + if self.policy and not agent: + return "agent parameter needs to be passed with policy task" + else: + agent = agent if self.policy else self.agent - agent = ( - Agent.objects.filter(pk=self.agent.pk) - .only("pk", "version", "hostname", "agent_id") - .first() - ) + try: + task_result = TaskResult.objects.get(agent=agent, task=self) + except TaskResult.DoesNotExist: + task_result = TaskResult(agent=agent, task=self) + task_result.save() nats_data = { - "func": "enableschedtask", - "schedtaskpayload": { - "name": self.win_task_name, - "enabled": self.enabled, - }, + "func": "schedtask", + "schedtaskpayload": self.generate_nats_task_payload(editing=True), } - r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) # type: ignore + + r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5)) if r != "ok": - self.sync_status = "notsynced" - self.save(update_fields=["sync_status"]) + task_result.sync_status = TaskSyncStatus.NOT_SYNCED + task_result.save(update_fields=["sync_status"]) DebugLog.warning( agent=agent, - log_type="agent_issues", - message=f"Unable to modify scheduled task {self.name} on {agent.hostname}({agent.pk}). It will try again on next agent checkin", # type: ignore + 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" else: - self.sync_status = "synced" - self.save(update_fields=["sync_status"]) + task_result.sync_status = TaskSyncStatus.SYNCED + task_result.save(update_fields=["sync_status"]) DebugLog.info( agent=agent, - log_type="agent_issues", - message=f"{agent.hostname} task {self.name} was successfully modified", # type: ignore + log_type=DebugLogType.AGENT_ISSUES, + message=f"{task_result.agent.hostname} task {self.name} was successfully modified", ) return "ok" - def delete_task_on_agent(self): - from agents.models import Agent + def delete_task_on_agent(self, agent: "Optional[Agent]" = None) -> str: + if self.policy and not agent: + return "agent parameter needs to be passed with policy task" + else: + agent = agent if self.policy else self.agent - agent = ( - Agent.objects.filter(pk=self.agent.pk) - .only("pk", "version", "hostname", "agent_id") - .first() - ) + try: + task_result = TaskResult.objects.get(agent=agent, task=self) + except TaskResult.DoesNotExist: + task_result = TaskResult(agent=agent, task=self) + task_result.save() nats_data = { "func": "delschedtask", "schedtaskpayload": {"name": self.win_task_name}, } - r = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) # type: ignore + r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=10)) if r != "ok" and "The system cannot find the file specified" not in r: - self.sync_status = "pendingdeletion" + task_result.sync_status = TaskSyncStatus.PENDING_DELETION - try: - self.save(update_fields=["sync_status"]) - except DatabaseError: - pass + with suppress(DatabaseError): + task_result.save(update_fields=["sync_status"]) DebugLog.warning( agent=agent, - log_type="agent_issues", - message=f"{agent.hostname} task {self.name} will be deleted on next checkin", # type: ignore + log_type=DebugLogType.AGENT_ISSUES, + message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin", ) return "timeout" else: self.delete() DebugLog.info( agent=agent, - log_type="agent_issues", - message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted", # type: ignore + log_type=DebugLogType.AGENT_ISSUES, + message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted", ) return "ok" - def run_win_task(self): - from agents.models import Agent - - agent = ( - Agent.objects.filter(pk=self.agent.pk) - .only("pk", "version", "hostname", "agent_id") - .first() - ) - - asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False)) # type: ignore - return "ok" - - def save_collector_results(self): + def run_win_task(self, agent: "Optional[Agent]" = None) -> str: + if self.policy and not agent: + return "agent parameter needs to be passed with policy task" + else: + agent = agent if self.policy else self.agent - agent_field = self.custom_field.get_or_create_field_value(self.agent) + try: + task_result = TaskResult.objects.get(agent=agent, task=self) + except TaskResult.DoesNotExist: + task_result = TaskResult(agent=agent, task=self) + task_result.save() - value = ( - self.stdout.strip() - if self.collector_all_output - else self.stdout.strip().split("\n")[-1].strip() + asyncio.run( + task_result.agent.nats_cmd( + {"func": "runtask", "taskpk": self.pk}, wait=False + ) ) - agent_field.save_to_field(value) + return "ok" def should_create_alert(self, alert_template=None): return ( @@ -470,10 +479,63 @@ def should_create_alert(self, alert_template=None): ) ) + +class TaskResult(models.Model): + class Meta: + unique_together = (("agent", "task"),) + + objects = PermissionQuerySet.as_manager() + + agent = models.ForeignKey( + "agents.Agent", + related_name="taskresults", + on_delete=models.CASCADE, + ) + task = models.ForeignKey( + "autotasks.AutomatedTask", + related_name="taskresults", + on_delete=models.CASCADE, + ) + + retcode = models.BigIntegerField(null=True, blank=True) + stdout = models.TextField(null=True, blank=True) + stderr = models.TextField(null=True, blank=True) + execution_time = models.CharField(max_length=100, default="0.0000") + last_run = models.DateTimeField(null=True, blank=True) + status = models.CharField( + max_length=30, choices=TaskStatus.choices, default=TaskStatus.PENDING + ) + sync_status = models.CharField( + max_length=100, choices=TaskSyncStatus.choices, default=TaskSyncStatus.INITIAL + ) + + def __str__(self): + return f"{self.agent.hostname} - {self.task}" + + def get_or_create_alert_if_needed( + self, alert_template: "Optional[AlertTemplate]" + ) -> "Optional[Alert]": + from alerts.models import Alert + + return Alert.create_or_return_task_alert( + self.task, + agent=self.agent, + skip_create=not self.task.should_create_alert(alert_template), + ) + + def save_collector_results(self) -> None: + agent_field = self.task.custom_field.get_or_create_field_value(self.agent) + + value = ( + self.stdout.strip() + if self.task.collector_all_output + else self.stdout.strip().split("\n")[-1].strip() + ) + agent_field.save_to_field(value) + def send_email(self): - from core.models import CoreSettings + CORE = get_core_settings() - CORE = CoreSettings.objects.first() # Format of Email sent when Task has email alert if self.agent: subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed" @@ -485,12 +547,11 @@ def send_email(self): + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_mail(subject, body, self.agent.alert_template) # type: ignore + CORE.send_mail(subject, body, self.agent.alert_template) def send_sms(self): - from core.models import CoreSettings + CORE = get_core_settings() - CORE = CoreSettings.objects.first() # Format of SMS sent when Task has SMS alert if self.agent: subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed" @@ -502,27 +563,24 @@ def send_sms(self): + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore + CORE.send_sms(body, alert_template=self.agent.alert_template) def send_resolved_email(self): - from core.models import CoreSettings + CORE = get_core_settings() - CORE = CoreSettings.objects.first() subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" body = ( subject + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_mail(subject, body, alert_template=self.agent.alert_template) # type: ignore + CORE.send_mail(subject, body, alert_template=self.agent.alert_template) def send_resolved_sms(self): - from core.models import CoreSettings - - CORE = CoreSettings.objects.first() + CORE = get_core_settings() subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" body = ( subject + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore + CORE.send_sms(body, alert_template=self.agent.alert_template) diff --git a/api/tacticalrmm/autotasks/permissions.py b/api/tacticalrmm/autotasks/permissions.py index 222fc58ddb..48c10350d5 100644 --- a/api/tacticalrmm/autotasks/permissions.py +++ b/api/tacticalrmm/autotasks/permissions.py @@ -4,7 +4,7 @@ class AutoTaskPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent( @@ -17,5 +17,5 @@ def has_permission(self, r, view): class RunAutoTaskPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_run_autotasks") diff --git a/api/tacticalrmm/autotasks/serializers.py b/api/tacticalrmm/autotasks/serializers.py index be4ed83a61..ceb390a06c 100644 --- a/api/tacticalrmm/autotasks/serializers.py +++ b/api/tacticalrmm/autotasks/serializers.py @@ -1,76 +1,277 @@ +from datetime import datetime + +from django.utils import timezone as djangotime from rest_framework import serializers -from agents.models import Agent -from checks.serializers import CheckSerializer from scripts.models import Script -from scripts.serializers import ScriptCheckSerializer +from tacticalrmm.constants import TaskType -from .models import AutomatedTask +from .models import AutomatedTask, TaskResult -class TaskSerializer(serializers.ModelSerializer): +class TaskResultSerializer(serializers.ModelSerializer): + class Meta: + model = TaskResult + fields = "__all__" + read_only_fields = ("agent", "task") + +class TaskSerializer(serializers.ModelSerializer): check_name = serializers.ReadOnlyField(source="assigned_check.readable_desc") schedule = serializers.ReadOnlyField() - last_run = serializers.ReadOnlyField(source="last_run_as_timezone") alert_template = serializers.SerializerMethodField() + run_time_date = serializers.DateTimeField(required=False) + expire_date = serializers.DateTimeField(allow_null=True, required=False) + task_result = serializers.SerializerMethodField() - def get_alert_template(self, obj): + def get_task_result(self, obj): + return ( + TaskResultSerializer(obj.task_result).data + if isinstance(obj.task_result, TaskResult) + else {} + ) - if obj.agent: - alert_template = obj.agent.alert_template - else: - alert_template = None + def validate_actions(self, value): + if not value: + raise serializers.ValidationError( + "There must be at least one action configured" + ) - if not alert_template: - return None - else: - return { - "name": alert_template.name, - "always_email": alert_template.task_always_email, - "always_text": alert_template.task_always_text, - "always_alert": alert_template.task_always_alert, - } + for action in value: + if "type" not in action: + raise serializers.ValidationError( + "Each action must have a type field of either 'script' or 'cmd'" + ) - class Meta: - model = AutomatedTask - fields = "__all__" + if action["type"] == "script": + if "script" not in action: + raise serializers.ValidationError( + "A script action type must have a 'script' field with primary key of script" + ) + if "script_args" not in action: + raise serializers.ValidationError( + "A script action type must have a 'script_args' field with an array of arguments" + ) -# below is for the windows agent -class TaskRunnerScriptField(serializers.ModelSerializer): - class Meta: - model = Script - fields = ["id", "filepath", "filename", "shell", "script_type"] + if "timeout" not in action: + raise serializers.ValidationError( + "A script action type must have a 'timeout' field" + ) + if action["type"] == "cmd": + if "command" not in action: + raise serializers.ValidationError( + "A command action type must have a 'command' field" + ) -class TaskRunnerGetSerializer(serializers.ModelSerializer): + if "timeout" not in action: + raise serializers.ValidationError( + "A command action type must have a 'timeout' field" + ) - script = TaskRunnerScriptField(read_only=True) + return value - class Meta: - model = AutomatedTask - fields = ["id", "script", "timeout", "enabled", "script_args"] + def validate(self, data): + # allow editing with task_type not specified + if self.instance and "task_type" not in data: + # remove schedule related fields from data + if "run_time_date" in data: + del data["run_time_date"] + if "expire_date" in data: + del data["expire_date"] + if "daily_interval" in data: + del data["daily_interval"] + if "weekly_interval" in data: + del data["weekly_interval"] + if "run_time_bit_weekdays" in data: + del data["run_time_bit_weekdays"] + if "monthly_months_of_year" in data: + del data["monthly_months_of_year"] + if "monthly_days_of_month" in data: + del data["monthly_days_of_month"] + if "monthly_weeks_of_month" in data: + del data["monthly_weeks_of_month"] + if "assigned_check" in data: + del data["assigned_check"] + return data + if ( + "expire_date" in data + and isinstance(data["expire_date"], datetime) + and djangotime.now() > data["expire_date"] + ): + raise serializers.ValidationError("Expires date/time is in the past") -class TaskGOGetSerializer(serializers.ModelSerializer): - script = ScriptCheckSerializer(read_only=True) - script_args = serializers.SerializerMethodField() + # run_time_date required + if ( + data["task_type"] + in ( + TaskType.RUN_ONCE, + TaskType.DAILY, + TaskType.WEEKLY, + TaskType.MONTHLY, + TaskType.MONTHLY_DOW, + ) + and not data["run_time_date"] + ): + raise serializers.ValidationError( + f"run_time_date is required for task_type '{data['task_type']}'" + ) - def get_script_args(self, obj): - return Script.parse_script_args( - agent=obj.agent, shell=obj.script.shell, args=obj.script_args - ) + # daily task type validation + if data["task_type"] == TaskType.DAILY: + if "daily_interval" not in data or not data["daily_interval"]: + raise serializers.ValidationError( + f"daily_interval is required for task_type '{data['task_type']}'" + ) + + # weekly task type validation + elif data["task_type"] == TaskType.WEEKLY: + if "weekly_interval" not in data or not data["weekly_interval"]: + raise serializers.ValidationError( + f"weekly_interval is required for task_type '{data['task_type']}'" + ) + + if "run_time_bit_weekdays" not in data or not data["run_time_bit_weekdays"]: + raise serializers.ValidationError( + f"run_time_bit_weekdays is required for task_type '{data['task_type']}'" + ) + + # monthly task type validation + elif data["task_type"] == TaskType.MONTHLY: + if ( + "monthly_months_of_year" not in data + or not data["monthly_months_of_year"] + ): + raise serializers.ValidationError( + f"monthly_months_of_year is required for task_type '{data['task_type']}'" + ) + + if "monthly_days_of_month" not in data or not data["monthly_days_of_month"]: + raise serializers.ValidationError( + f"monthly_days_of_month is required for task_type '{data['task_type']}'" + ) + + # monthly day of week task type validation + elif data["task_type"] == TaskType.MONTHLY_DOW: + if ( + "monthly_months_of_year" not in data + or not data["monthly_months_of_year"] + ): + raise serializers.ValidationError( + f"monthly_months_of_year is required for task_type '{data['task_type']}'" + ) + + if ( + "monthly_weeks_of_month" not in data + or not data["monthly_weeks_of_month"] + ): + raise serializers.ValidationError( + f"monthly_weeks_of_month is required for task_type '{data['task_type']}'" + ) + + if "run_time_bit_weekdays" not in data or not data["run_time_bit_weekdays"]: + raise serializers.ValidationError( + f"run_time_bit_weekdays is required for task_type '{data['task_type']}'" + ) + + # check failure task type validation + elif data["task_type"] == TaskType.CHECK_FAILURE: + if "assigned_check" not in data or not data["assigned_check"]: + raise serializers.ValidationError( + f"assigned_check is required for task_type '{data['task_type']}'" + ) + + return data + + def get_alert_template(self, obj): + if obj.agent: + alert_template = obj.agent.alert_template + else: + alert_template = None + + if not alert_template: + return None + return { + "name": alert_template.name, + "always_email": alert_template.task_always_email, + "always_text": alert_template.task_always_text, + "always_alert": alert_template.task_always_alert, + } class Meta: model = AutomatedTask - fields = ["id", "script", "timeout", "enabled", "script_args"] + fields = "__all__" + +class TaskGOGetSerializer(serializers.ModelSerializer): + task_actions = serializers.SerializerMethodField() + + 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=agent, + shell=action["shell"], + args=[action["command"]], + )[0], + "shell": action["shell"], + "timeout": action["timeout"], + } + ) + elif action["type"] == "script": + try: + script = Script.objects.get(pk=action["script"]) + except Script.DoesNotExist: + # script doesn't exist so remove it + actions_to_remove.append(action["script"]) + continue + # wrote a custom migration for env_vars but leaving this just in case. + # can be removed later + try: + env_vars = action["env_vars"] + except KeyError: + env_vars = [] + tmp.append( + { + "type": "script", + "script_name": script.name, + "code": script.code, + "script_args": Script.parse_script_args( + agent=agent, + shell=script.shell, + args=action["script_args"], + ), + "shell": script.shell, + "timeout": action["timeout"], + "run_as_user": script.run_as_user, + "env_vars": env_vars, + } + ) + if actions_to_remove: + task = AutomatedTask.objects.get(pk=obj.pk) + task.actions = [ + action + for action in task.actions + if action["type"] == "cmd" + or ( + "script" in action.keys() + and action["script"] not in actions_to_remove + ) + ] + task.save(update_fields=["actions"]) + return tmp -class TaskRunnerPatchSerializer(serializers.ModelSerializer): class Meta: model = AutomatedTask - fields = "__all__" + fields = ["id", "continue_on_error", "enabled", "task_actions"] class TaskAuditSerializer(serializers.ModelSerializer): diff --git a/api/tacticalrmm/autotasks/tasks.py b/api/tacticalrmm/autotasks/tasks.py index 6c6efd6c5b..38c57e752e 100644 --- a/api/tacticalrmm/autotasks/tasks.py +++ b/api/tacticalrmm/autotasks/tasks.py @@ -1,123 +1,175 @@ import asyncio import datetime as dt -from logging import log -import random +from collections import namedtuple +from contextlib import suppress from time import sleep -from typing import Union +from typing import TYPE_CHECKING, Optional, Union +import msgpack +import nats from django.utils import timezone as djangotime +from nats.errors import TimeoutError -from autotasks.models import AutomatedTask -from logs.models import DebugLog +from agents.models import Agent +from alerts.models import Alert +from autotasks.models import AutomatedTask, TaskResult from tacticalrmm.celery import app +from tacticalrmm.constants import AGENT_STATUS_ONLINE, ORPHANED_WIN_TASK_LOCK +from tacticalrmm.helpers import rand_range, setup_nats_options +from tacticalrmm.utils import redis_lock +if TYPE_CHECKING: + from nats.aio.client import Client as NATSClient -@app.task -def create_win_task_schedule(pk): - task = AutomatedTask.objects.get(pk=pk) - task.create_task_on_agent() +@app.task +def create_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str: + with suppress( + AutomatedTask.DoesNotExist, + Agent.DoesNotExist, + ): + task = AutomatedTask.objects.get(pk=pk) + + if agent_id: + task.create_task_on_agent(Agent.objects.get(agent_id=agent_id)) + else: + task.create_task_on_agent() return "ok" @app.task -def enable_or_disable_win_task(pk): - task = AutomatedTask.objects.get(pk=pk) - - task.modify_task_on_agent() +def modify_win_task(pk: int, agent_id: Optional[str] = None) -> str: + with suppress( + AutomatedTask.DoesNotExist, + Agent.DoesNotExist, + ): + task = AutomatedTask.objects.get(pk=pk) + + if agent_id: + task.modify_task_on_agent(Agent.objects.get(agent_id=agent_id)) + else: + task.modify_task_on_agent() return "ok" @app.task -def delete_win_task_schedule(pk): - task = AutomatedTask.objects.get(pk=pk) +def delete_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str: + with suppress( + AutomatedTask.DoesNotExist, + Agent.DoesNotExist, + ): + task = AutomatedTask.objects.get(pk=pk) + + if agent_id: + task.delete_task_on_agent(Agent.objects.get(agent_id=agent_id)) + else: + task.delete_task_on_agent() - task.delete_task_on_agent() return "ok" @app.task -def run_win_task(pk): - task = AutomatedTask.objects.get(pk=pk) - task.run_win_task() +def run_win_task(pk: int, agent_id: Optional[str] = None) -> str: + with suppress( + AutomatedTask.DoesNotExist, + Agent.DoesNotExist, + ): + task = AutomatedTask.objects.get(pk=pk) + + if agent_id: + task.run_win_task(Agent.objects.get(agent_id=agent_id)) + else: + task.run_win_task() + return "ok" -@app.task -def remove_orphaned_win_tasks(agentpk): - from agents.models import Agent +@app.task(bind=True) +def remove_orphaned_win_tasks(self) -> str: + with redis_lock(ORPHANED_WIN_TASK_LOCK, self.app.oid) as acquired: + if not acquired: + return f"{self.app.oid} still running" - agent = Agent.objects.get(pk=agentpk) + from core.tasks import _get_agent_qs - DebugLog.info( - agent=agent, - log_type="agent_issues", - message=f"Orphaned task cleanup initiated on {agent.hostname}.", - ) + AgentTup = namedtuple("AgentTup", ["agent_id", "task_names"]) + items: "list[AgentTup]" = [] + exclude_tasks = ("TacticalRMM_SchedReboot",) - r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10)) + for agent in _get_agent_qs(): + if agent.status == AGENT_STATUS_ONLINE: + names = [task.win_task_name for task in agent.get_tasks_with_policies()] + items.append(AgentTup._make([agent.agent_id, names])) - if not isinstance(r, list) and not r: # empty list - DebugLog.error( - agent=agent, - log_type="agent_issues", - message=f"Unable to clean up scheduled tasks on {agent.hostname}: {r}", - ) - return "notlist" - - agent_task_names = list(agent.autotasks.values_list("win_task_name", flat=True)) - - exclude_tasks = ( - "TacticalRMM_fixmesh", - "TacticalRMM_SchedReboot", - "TacticalRMM_sync", - "TacticalRMM_agentupdate", - ) - - for task in r: - if task.startswith(exclude_tasks): - # skip system tasks or any pending reboots - continue - - if task.startswith("TacticalRMM_") and task not in agent_task_names: - # delete task since it doesn't exist in UI - nats_data = { - "func": "delschedtask", - "schedtaskpayload": {"name": task}, - } - ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) - if ret != "ok": - DebugLog.error( - agent=agent, - log_type="agent_issues", - message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}", + async def _handle_task(nc: "NATSClient", sub, data, names) -> str: + try: + msg = await nc.request( + subject=sub, payload=msgpack.dumps(data), timeout=5 ) - else: - DebugLog.info( - agent=agent, - log_type="agent_issues", - message=f"Removed orphaned task {task} from {agent.hostname}", + except TimeoutError: + return "timeout" + + try: + r = msgpack.loads(msg.data) + except Exception as e: + return str(e) + + if not isinstance(r, list): + return "notlist" + + for name in r: + if name.startswith(exclude_tasks): + # skip system tasks or any pending reboots + continue + + if name.startswith("TacticalRMM_") and name not in names: + nats_data = { + "func": "delschedtask", + "schedtaskpayload": {"name": name}, + } + print(f"Deleting orphaned task: {name} on agent {sub}") + await nc.publish(subject=sub, payload=msgpack.dumps(nats_data)) + + return "ok" + + async def _run() -> None: + opts = setup_nats_options() + try: + nc = await nats.connect(**opts) + except Exception as e: + return str(e) + + payload = {"func": "listschedtasks"} + tasks = [ + _handle_task( + nc=nc, sub=item.agent_id, data=payload, names=item.task_names ) + for item in items + ] + await asyncio.gather(*tasks) + await nc.flush() + await nc.close() - DebugLog.info( - agent=agent, - log_type="agent_issues", - message=f"Orphaned task cleanup finished on {agent.hostname}", - ) + asyncio.run(_run()) + return "completed" @app.task def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending email if not alert.email_sent: - sleep(random.randint(1, 10)) - alert.assigned_task.send_email() + task_result = TaskResult.objects.get( + task=alert.assigned_task, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + task_result.send_email() alert.email_sent = djangotime.now() alert.save(update_fields=["email_sent"]) else: @@ -125,8 +177,11 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) # send an email only if the last email sent is older than alert interval delta = djangotime.now() - dt.timedelta(days=alert_interval) if alert.email_sent < delta: - sleep(random.randint(1, 10)) - alert.assigned_task.send_email() + task_result = TaskResult.objects.get( + task=alert.assigned_task, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + task_result.send_email() alert.email_sent = djangotime.now() alert.save(update_fields=["email_sent"]) @@ -135,14 +190,18 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) @app.task def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending text if not alert.sms_sent: - sleep(random.randint(1, 3)) - alert.assigned_task.send_sms() + task_result = TaskResult.objects.get( + task=alert.assigned_task, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + task_result.send_sms() alert.sms_sent = djangotime.now() alert.save(update_fields=["sms_sent"]) else: @@ -150,8 +209,11 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> # send a text only if the last text sent is older than alert interval delta = djangotime.now() - dt.timedelta(days=alert_interval) if alert.sms_sent < delta: - sleep(random.randint(1, 3)) - alert.assigned_task.send_sms() + task_result = TaskResult.objects.get( + task=alert.assigned_task, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + task_result.send_sms() alert.sms_sent = djangotime.now() alert.save(update_fields=["sms_sent"]) @@ -160,14 +222,18 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> @app.task def handle_resolved_task_sms_alert(pk: int) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending text if not alert.resolved_sms_sent: - sleep(random.randint(1, 3)) - alert.assigned_task.send_resolved_sms() + task_result = TaskResult.objects.get( + task=alert.assigned_task, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + task_result.send_resolved_sms() alert.resolved_sms_sent = djangotime.now() alert.save(update_fields=["resolved_sms_sent"]) @@ -176,14 +242,18 @@ def handle_resolved_task_sms_alert(pk: int) -> str: @app.task def handle_resolved_task_email_alert(pk: int) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending email if not alert.resolved_email_sent: - sleep(random.randint(1, 10)) - alert.assigned_task.send_resolved_email() + task_result = TaskResult.objects.get( + task=alert.assigned_task, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + task_result.send_resolved_email() alert.resolved_email_sent = djangotime.now() alert.save(update_fields=["resolved_email_sent"]) diff --git a/api/tacticalrmm/autotasks/tests.py b/api/tacticalrmm/autotasks/tests.py index b74a901087..ef44eded7d 100644 --- a/api/tacticalrmm/autotasks/tests.py +++ b/api/tacticalrmm/autotasks/tests.py @@ -1,14 +1,14 @@ -import datetime as dt -from unittest.mock import call, patch +from unittest.mock import patch from django.utils import timezone as djangotime from model_bakery import baker +from tacticalrmm.constants import TaskType from tacticalrmm.test import TacticalTestCase -from .models import AutomatedTask +from .models import AutomatedTask, TaskResult, TaskSyncStatus from .serializers import TaskSerializer -from .tasks import create_win_task_schedule, remove_orphaned_win_tasks, run_win_task +from .tasks import create_win_task_schedule, run_win_task base_url = "/tasks" @@ -44,18 +44,21 @@ def test_get_autotasks(self): self.assertEqual(resp.status_code, 200) self.assertEqual(len(resp.data), 4) - @patch("automation.tasks.generate_agent_autotasks_task.delay") @patch("autotasks.tasks.create_win_task_schedule.delay") - def test_add_autotask( - self, create_win_task_schedule, generate_agent_autotasks_task - ): + def test_add_autotask(self, create_win_task_schedule): url = f"{base_url}/" # setup data script = baker.make_recipe("scripts.script") agent = baker.make_recipe("agents.agent") - policy = baker.make("automation.Policy") + policy = baker.make("automation.Policy") # noqa check = baker.make_recipe("checks.diskspace_check", agent=agent) + custom_field = baker.make("core.CustomField") + + actions = [ + {"type": "cmd", "command": "command", "timeout": 30}, + {"type": "script", "script": script.id, "script_args": [], "timeout": 90}, + ] # test invalid agent data = { @@ -65,47 +68,176 @@ def test_add_autotask( resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 404) - # test add task to agent + # test add task without actions data = { "agent": agent.agent_id, "name": "Test Task Scheduled with Assigned Check", - "run_time_days": ["Sunday", "Monday", "Friday"], - "run_time_minute": "10:00", - "timeout": 120, + "run_time_days": 56, "enabled": True, - "script": script.id, - "script_args": None, - "task_type": "scheduled", - "assigned_check": check.id, + "actions": [], + "task_type": "manual", + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 400) + + # test add checkfailure task_type to agent without check + data = { + "agent": agent.agent_id, + "name": "Check Failure", + "enabled": True, + "actions": actions, + "task_type": "checkfailure", + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 400) + + create_win_task_schedule.not_assert_called() + + # test add manual task_type to agent + data = { + "agent": agent.agent_id, + "name": "Manual", + "enabled": True, + "actions": actions, + "task_type": "manual", } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) create_win_task_schedule.assert_called() + create_win_task_schedule.reset_mock() - # test add task to policy + # test add daily task_type to agent data = { - "policy": policy.id, # type: ignore - "name": "Test Task Manual", - "run_time_days": [], - "timeout": 120, + "agent": agent.agent_id, + "name": "Daily", "enabled": True, - "script": script.id, - "script_args": None, - "task_type": "manual", - "assigned_check": None, + "actions": actions, + "task_type": "daily", + "daily_interval": 1, + "run_time_date": djangotime.now(), + "repetition_interval": "30M", + "repetition_duration": "1D", + "random_task_delay": "5M", + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + # test add weekly task_type to agent + data = { + "agent": agent.agent_id, + "name": "Weekly", + "enabled": True, + "actions": actions, + "task_type": "weekly", + "weekly_interval": 2, + "run_time_bit_weekdays": 26, + "run_time_date": djangotime.now(), + "expire_date": djangotime.now() + djangotime.timedelta(weeks=5), + "repetition_interval": "30S", + "repetition_duration": "1H", + "random_task_delay": "5M", + "task_instance_policy": 2, + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + create_win_task_schedule.assert_called() + create_win_task_schedule.reset_mock() + + # test add monthly task_type to agent + data = { + "agent": agent.agent_id, + "name": "Monthly", + "enabled": True, + "actions": actions, + "task_type": "monthly", + "monthly_months_of_year": 56, + "monthly_days_of_month": 350, + "run_time_date": djangotime.now(), + "expire_date": djangotime.now() + djangotime.timedelta(weeks=5), + "repetition_interval": "30S", + "repetition_duration": "1H", + "random_task_delay": "5M", } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) - generate_agent_autotasks_task.assert_called_with(policy=policy.id) # type: ignore + create_win_task_schedule.assert_called() + create_win_task_schedule.reset_mock() + + # test add monthly day-of-week task_type to agent + data = { + "agent": agent.agent_id, + "name": "Monthly", + "enabled": True, + "actions": actions, + "task_type": "monthlydow", + "monthly_months_of_year": 500, + "monthly_weeks_of_month": 4, + "run_time_bit_weekdays": 15, + "run_time_date": djangotime.now(), + "expire_date": djangotime.now() + djangotime.timedelta(weeks=5), + "repetition_interval": "30S", + "repetition_duration": "1H", + "random_task_delay": "5M", + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + create_win_task_schedule.assert_called() + create_win_task_schedule.reset_mock() + + # test add monthly day-of-week task_type to agent with custom field + data = { + "agent": agent.agent_id, + "name": "Monthly", + "enabled": True, + "actions": actions, + "task_type": "monthlydow", + "monthly_months_of_year": 500, + "monthly_weeks_of_month": 4, + "run_time_bit_weekdays": 15, + "run_time_date": djangotime.now(), + "expire_date": djangotime.now() + djangotime.timedelta(weeks=5), + "repetition_interval": "30S", + "repetition_duration": "1H", + "random_task_delay": "5M", + "custom_field": custom_field.id, + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + create_win_task_schedule.assert_called() + create_win_task_schedule.reset_mock() + + # test add checkfailure task_type to agent + data = { + "agent": agent.agent_id, + "name": "Check Failure", + "enabled": True, + "actions": actions, + "task_type": "checkfailure", + "assigned_check": check.id, + } + + resp = self.client.post(url, data, format="json") + self.assertEqual(resp.status_code, 200) + + create_win_task_schedule.assert_called() + create_win_task_schedule.reset_mock() self.check_not_authenticated("post", url) def test_get_autotask(self): - # setup data agent = baker.make_recipe("agents.agent") task = baker.make("autotasks.AutomatedTask", agent=agent) @@ -116,67 +248,88 @@ def test_get_autotask(self): serializer = TaskSerializer(task) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) - @patch("autotasks.tasks.enable_or_disable_win_task.delay") - @patch("automation.tasks.update_policy_autotasks_fields_task.delay") - def test_update_autotask( - self, update_policy_autotasks_fields_task, enable_or_disable_win_task - ): + def test_update_autotask(self): # setup data agent = baker.make_recipe("agents.agent") agent_task = baker.make("autotasks.AutomatedTask", agent=agent) policy = baker.make("automation.Policy") - policy_task = baker.make("autotasks.AutomatedTask", enabled=True, policy=policy) + policy_task = baker.make( # noqa + "autotasks.AutomatedTask", enabled=True, policy=policy + ) + custom_field = baker.make("core.CustomField") + script = baker.make("scripts.Script") + + actions = [ + {"type": "cmd", "command": "command", "timeout": 30}, + {"type": "script", "script": script.id, "script_args": [], "timeout": 90}, + ] # test invalid url resp = self.client.put(f"{base_url}/500/", format="json") self.assertEqual(resp.status_code, 404) - url = f"{base_url}/{agent_task.id}/" # type: ignore + url = f"{base_url}/{agent_task.id}/" - # test editing task with no task called + # test editing agent task with no task update data = {"name": "New Name"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - enable_or_disable_win_task.not_called() # type: ignore - # test editing task + # test editing agent task with agent task update data = {"enabled": False} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - enable_or_disable_win_task.assert_called_with(pk=agent_task.id) # type: ignore - - url = f"{base_url}/{policy_task.id}/" # type: ignore - # test editing policy task - data = {"enabled": False} + # test editing agent task with task_type + data = { + "name": "Monthly", + "actions": actions, + "task_type": "monthlydow", + "monthly_months_of_year": 500, + "monthly_weeks_of_month": 4, + "run_time_bit_weekdays": 15, + "run_time_date": djangotime.now(), + "expire_date": djangotime.now() + djangotime.timedelta(weeks=5), + "repetition_interval": "30S", + "repetition_duration": "1H", + "random_task_delay": "5M", + "custom_field": custom_field.id, + "run_asap_after_missed": False, + } resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - update_policy_autotasks_fields_task.assert_called_with( - task=policy_task.id, update_agent=True # type: ignore - ) - update_policy_autotasks_fields_task.reset_mock() - # test editing policy task with no agent update - data = {"name": "New Name"} + # test trying to edit with empty actions + data = { + "name": "Monthly", + "actions": [], + "task_type": "monthlydow", + "monthly_months_of_year": 500, + "monthly_weeks_of_month": 4, + "run_time_bit_weekdays": 15, + "run_time_date": djangotime.now(), + "expire_date": djangotime.now() + djangotime.timedelta(weeks=5), + "repetition_interval": "30S", + "repetition_duration": "1H", + "random_task_delay": "5M", + "run_asap_afteR_missed": False, + } resp = self.client.put(url, data, format="json") - self.assertEqual(resp.status_code, 200) - update_policy_autotasks_fields_task.assert_called_with(task=policy_task.id) + self.assertEqual(resp.status_code, 400) self.check_not_authenticated("put", url) + @patch("autotasks.tasks.remove_orphaned_win_tasks.delay") @patch("autotasks.tasks.delete_win_task_schedule.delay") - @patch("automation.tasks.delete_policy_autotasks_task.delay") - def test_delete_autotask( - self, delete_policy_autotasks_task, delete_win_task_schedule - ): + def test_delete_autotask(self, delete_win_task_schedule, remove_orphaned_win_tasks): # setup data agent = baker.make_recipe("agents.agent") agent_task = baker.make("autotasks.AutomatedTask", agent=agent) @@ -188,17 +341,20 @@ def test_delete_autotask( self.assertEqual(resp.status_code, 404) # test delete agent task - url = f"{base_url}/{agent_task.id}/" # type: ignore + url = f"{base_url}/{agent_task.id}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - delete_win_task_schedule.assert_called_with(pk=agent_task.id) # type: ignore + delete_win_task_schedule.assert_called_with(pk=agent_task.id) + remove_orphaned_win_tasks.assert_not_called() + delete_win_task_schedule.reset_mock() + remove_orphaned_win_tasks.reset_mock() # test delete policy task - url = f"{base_url}/{policy_task.id}/" # type: ignore + url = f"{base_url}/{policy_task.id}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertFalse(AutomatedTask.objects.filter(pk=policy_task.id)) # type: ignore - delete_policy_autotasks_task.assert_called_with(task=policy_task.id) # type: ignore + remove_orphaned_win_tasks.assert_called_once() + delete_win_task_schedule.assert_not_called() self.check_not_authenticated("delete", url) @@ -213,7 +369,7 @@ def test_run_autotask(self, run_win_task): self.assertEqual(resp.status_code, 404) # test run agent task - url = f"{base_url}/{task.id}/run/" # type: ignore + url = f"{base_url}/{task.id}/run/" resp = self.client.post(url, format="json") self.assertEqual(resp.status_code, 200) run_win_task.assert_called() @@ -227,232 +383,344 @@ def setUp(self): self.setup_coresettings() @patch("agents.models.Agent.nats_cmd") - def test_remove_orphaned_win_task(self, nats_cmd): + def test_run_win_task(self, nats_cmd): self.agent = baker.make_recipe("agents.agent") self.task1 = AutomatedTask.objects.create( agent=self.agent, name="test task 1", - win_task_name=AutomatedTask.generate_task_name(), ) + nats_cmd.return_value = "ok" + ret = run_win_task.s(self.task1.pk).apply() + self.assertEqual(ret.status, "SUCCESS") - # test removing an orphaned task - win_tasks = [ - "Adobe Acrobat Update Task", - "AdobeGCInvoker-1.0", - "GoogleUpdateTaskMachineCore", - "GoogleUpdateTaskMachineUA", - "OneDrive Standalone Update Task-S-1-5-21-717461175-241712648-1206041384-1001", - self.task1.win_task_name, - "TacticalRMM_fixmesh", - "TacticalRMM_SchedReboot_jk324kajd", - "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb", # orphaned task - ] - - self.calls = [ - call({"func": "listschedtasks"}, timeout=10), - call( - { - "func": "delschedtask", - "schedtaskpayload": { - "name": "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb" - }, - }, - timeout=10, - ), - ] + @patch("agents.models.Agent.nats_cmd") + def test_create_win_task_schedule(self, nats_cmd): + agent = baker.make_recipe("agents.agent", time_zone="UTC") - nats_cmd.side_effect = [win_tasks, "ok"] - ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() - self.assertEqual(nats_cmd.call_count, 2) - nats_cmd.assert_has_calls(self.calls) - self.assertEqual(ret.status, "SUCCESS") + # test daily task + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, + name="test task 1", + task_type=TaskType.DAILY, + daily_interval=1, + run_time_date=djangotime.now() + djangotime.timedelta(hours=3, minutes=30), + ) + self.assertFalse(TaskResult.objects.filter(agent=agent, task=task1).exists()) - # test nats delete task fail + nats_cmd.return_value = "ok" + create_win_task_schedule(pk=task1.pk) + nats_cmd.assert_called_with( + { + "func": "schedtask", + "schedtaskpayload": { + "pk": task1.pk, + "type": "rmm", + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, + "trigger": "daily", + "multiple_instances": 1, + "delete_expired_task_after": False, + "start_when_available": False, + "start_year": int(task1.run_time_date.strftime("%Y")), + "start_month": int(task1.run_time_date.strftime("%-m")), + "start_day": int(task1.run_time_date.strftime("%-d")), + "start_hour": int(task1.run_time_date.strftime("%-H")), + "start_min": int(task1.run_time_date.strftime("%-M")), + "day_interval": 1, + }, + }, + timeout=5, + ) nats_cmd.reset_mock() - nats_cmd.side_effect = [win_tasks, "error deleting task"] - ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() - nats_cmd.assert_has_calls(self.calls) - self.assertEqual(nats_cmd.call_count, 2) - self.assertEqual(ret.status, "SUCCESS") + self.assertEqual( + TaskResult.objects.get(task=task1, agent=agent).sync_status, + TaskSyncStatus.SYNCED, + ) - # no orphaned tasks + nats_cmd.return_value = "timeout" + create_win_task_schedule(pk=task1.pk) + self.assertEqual( + TaskResult.objects.get(task=task1, agent=agent).sync_status, + TaskSyncStatus.INITIAL, + ) nats_cmd.reset_mock() - win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb") - nats_cmd.side_effect = [win_tasks, "ok"] - ret = remove_orphaned_win_tasks.s(self.agent.pk).apply() - self.assertEqual(nats_cmd.call_count, 1) - self.assertEqual(ret.status, "SUCCESS") - @patch("agents.models.Agent.nats_cmd") - def test_run_win_task(self, nats_cmd): - self.agent = baker.make_recipe("agents.agent") - self.task1 = AutomatedTask.objects.create( - agent=self.agent, + # test weekly task + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, name="test task 1", - win_task_name=AutomatedTask.generate_task_name(), + task_type=TaskType.WEEKLY, + weekly_interval=1, + run_asap_after_missed=True, + run_time_bit_weekdays=127, + run_time_date=djangotime.now() + djangotime.timedelta(hours=3, minutes=30), + expire_date=djangotime.now() + djangotime.timedelta(days=100), + task_instance_policy=2, ) - nats_cmd.return_value = "ok" - ret = run_win_task.s(self.task1.pk).apply() - self.assertEqual(ret.status, "SUCCESS") - @patch("agents.models.Agent.nats_cmd") - def test_create_win_task_schedule(self, nats_cmd): - self.agent = baker.make_recipe("agents.agent") + nats_cmd.return_value = "ok" + create_win_task_schedule(pk=task1.pk) + nats_cmd.assert_called_with( + { + "func": "schedtask", + "schedtaskpayload": { + "pk": task1.pk, + "type": "rmm", + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, + "trigger": "weekly", + "multiple_instances": 2, + "delete_expired_task_after": False, + "start_when_available": True, + "start_year": int(task1.run_time_date.strftime("%Y")), + "start_month": int(task1.run_time_date.strftime("%-m")), + "start_day": int(task1.run_time_date.strftime("%-d")), + "start_hour": int(task1.run_time_date.strftime("%-H")), + "start_min": int(task1.run_time_date.strftime("%-M")), + "expire_year": int(task1.expire_date.strftime("%Y")), + "expire_month": int(task1.expire_date.strftime("%-m")), + "expire_day": int(task1.expire_date.strftime("%-d")), + "expire_hour": int(task1.expire_date.strftime("%-H")), + "expire_min": int(task1.expire_date.strftime("%-M")), + "week_interval": 1, + "days_of_week": 127, + }, + }, + timeout=5, + ) + nats_cmd.reset_mock() - task_name = AutomatedTask.generate_task_name() - # test scheduled task - self.task1 = AutomatedTask.objects.create( - agent=self.agent, + # test monthly task + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, name="test task 1", - win_task_name=task_name, - task_type="scheduled", - run_time_bit_weekdays=127, - run_time_minute="21:55", + task_type=TaskType.MONTHLY, + random_task_delay="3M", + task_repetition_interval="15M", + task_repetition_duration="1D", + stop_task_at_duration_end=True, + monthly_days_of_month=0x80000030, + monthly_months_of_year=0x400, + run_time_date=djangotime.now() + djangotime.timedelta(hours=3, minutes=30), ) - self.assertEqual(self.task1.sync_status, "initial") + nats_cmd.return_value = "ok" - ret = create_win_task_schedule.s(pk=self.task1.pk).apply() - self.assertEqual(nats_cmd.call_count, 1) + create_win_task_schedule(pk=task1.pk) nats_cmd.assert_called_with( { "func": "schedtask", "schedtaskpayload": { + "pk": task1.pk, "type": "rmm", - "trigger": "weekly", - "weekdays": 127, - "pk": self.task1.pk, - "name": task_name, - "hour": 21, - "min": 55, + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, + "trigger": "monthly", + "multiple_instances": 1, + "delete_expired_task_after": False, + "start_when_available": False, + "start_year": int(task1.run_time_date.strftime("%Y")), + "start_month": int(task1.run_time_date.strftime("%-m")), + "start_day": int(task1.run_time_date.strftime("%-d")), + "start_hour": int(task1.run_time_date.strftime("%-H")), + "start_min": int(task1.run_time_date.strftime("%-M")), + "random_delay": "PT3M", + "repetition_interval": "PT15M", + "repetition_duration": "P1DT", + "stop_at_duration_end": True, + "days_of_month": 0x30, + "run_on_last_day_of_month": True, + "months_of_year": 1024, }, }, timeout=5, ) - self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) - self.assertEqual(self.task1.sync_status, "synced") + nats_cmd.reset_mock() - nats_cmd.return_value = "timeout" - ret = create_win_task_schedule.s(pk=self.task1.pk).apply() - self.assertEqual(ret.status, "SUCCESS") - self.task1 = AutomatedTask.objects.get(pk=self.task1.pk) - self.assertEqual(self.task1.sync_status, "initial") + # test monthly dow + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, + name="test task 1", + task_type=TaskType.MONTHLY_DOW, + run_time_bit_weekdays=56, + monthly_months_of_year=0x400, + monthly_weeks_of_month=3, + run_time_date=djangotime.now() + djangotime.timedelta(hours=3, minutes=30), + ) + nats_cmd.return_value = "ok" + create_win_task_schedule(pk=task1.pk) + nats_cmd.assert_called_with( + { + "func": "schedtask", + "schedtaskpayload": { + "pk": task1.pk, + "type": "rmm", + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, + "trigger": "monthlydow", + "multiple_instances": 1, + "delete_expired_task_after": False, + "start_when_available": False, + "start_year": int(task1.run_time_date.strftime("%Y")), + "start_month": int(task1.run_time_date.strftime("%-m")), + "start_day": int(task1.run_time_date.strftime("%-d")), + "start_hour": int(task1.run_time_date.strftime("%-H")), + "start_min": int(task1.run_time_date.strftime("%-M")), + "days_of_week": 56, + "months_of_year": 0x400, + "weeks_of_month": 3, + }, + }, + timeout=5, + ) + nats_cmd.reset_mock() # test runonce with future date - nats_cmd.reset_mock() - task_name = AutomatedTask.generate_task_name() - run_time_date = djangotime.now() + djangotime.timedelta(hours=22) - self.task2 = AutomatedTask.objects.create( - agent=self.agent, + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, name="test task 2", - win_task_name=task_name, - task_type="runonce", - run_time_date=run_time_date, + task_type=TaskType.RUN_ONCE, + run_time_date=djangotime.now() + djangotime.timedelta(hours=22), + run_asap_after_missed=True, ) nats_cmd.return_value = "ok" - ret = create_win_task_schedule.s(pk=self.task2.pk).apply() + create_win_task_schedule(pk=task1.pk) nats_cmd.assert_called_with( { "func": "schedtask", "schedtaskpayload": { + "pk": task1.pk, "type": "rmm", - "trigger": "once", - "pk": self.task2.pk, - "name": task_name, - "year": int(dt.datetime.strftime(self.task2.run_time_date, "%Y")), - "month": dt.datetime.strftime(self.task2.run_time_date, "%B"), - "day": int(dt.datetime.strftime(self.task2.run_time_date, "%d")), - "hour": int(dt.datetime.strftime(self.task2.run_time_date, "%H")), - "min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")), + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, + "trigger": "runonce", + "multiple_instances": 1, + "delete_expired_task_after": False, + "start_when_available": True, + "start_year": int(task1.run_time_date.strftime("%Y")), + "start_month": int(task1.run_time_date.strftime("%-m")), + "start_day": int(task1.run_time_date.strftime("%-d")), + "start_hour": int(task1.run_time_date.strftime("%-H")), + "start_min": int(task1.run_time_date.strftime("%-M")), }, }, timeout=5, ) - self.assertEqual(ret.status, "SUCCESS") + nats_cmd.reset_mock() # test runonce with date in the past - nats_cmd.reset_mock() - task_name = AutomatedTask.generate_task_name() - run_time_date = djangotime.now() - djangotime.timedelta(days=13) - self.task3 = AutomatedTask.objects.create( - agent=self.agent, + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, name="test task 3", - win_task_name=task_name, - task_type="runonce", - run_time_date=run_time_date, + task_type=TaskType.RUN_ONCE, + run_asap_after_missed=True, + run_time_date=djangotime.datetime(2018, 6, 1, 23, 23, 23), ) nats_cmd.return_value = "ok" - ret = create_win_task_schedule.s(pk=self.task3.pk).apply() - self.task3 = AutomatedTask.objects.get(pk=self.task3.pk) - self.assertEqual(ret.status, "SUCCESS") + create_win_task_schedule(pk=task1.pk) + nats_cmd.assert_called() + + # check if task is scheduled for at most 5min in the future + _, args, _ = nats_cmd.mock_calls[0] + + current_minute = int(djangotime.now().strftime("%-M")) + + if current_minute >= 55 and current_minute < 60: + self.assertLess( + args[0]["schedtaskpayload"]["start_min"], + int(djangotime.now().strftime("%-M")), + ) + else: + self.assertGreater( + args[0]["schedtaskpayload"]["start_min"], + int(djangotime.now().strftime("%-M")), + ) - # test checkfailure + # test checkfailure task nats_cmd.reset_mock() - self.check = baker.make_recipe("checks.diskspace_check", agent=self.agent) - task_name = AutomatedTask.generate_task_name() - self.task4 = AutomatedTask.objects.create( - agent=self.agent, + check = baker.make_recipe("checks.diskspace_check", agent=agent) + task1 = baker.make( + "autotasks.AutomatedTask", + agent=agent, name="test task 4", - win_task_name=task_name, - task_type="checkfailure", - assigned_check=self.check, + task_type=TaskType.CHECK_FAILURE, + assigned_check=check, ) nats_cmd.return_value = "ok" - ret = create_win_task_schedule.s(pk=self.task4.pk).apply() + create_win_task_schedule(pk=task1.pk) nats_cmd.assert_called_with( { "func": "schedtask", "schedtaskpayload": { + "pk": task1.pk, "type": "rmm", + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, "trigger": "manual", - "pk": self.task4.pk, - "name": task_name, + "multiple_instances": 1, + "delete_expired_task_after": False, + "start_when_available": False, }, }, timeout=5, ) - self.assertEqual(ret.status, "SUCCESS") + nats_cmd.reset_mock() # test manual - nats_cmd.reset_mock() - task_name = AutomatedTask.generate_task_name() - self.task5 = AutomatedTask.objects.create( - agent=self.agent, + task1 = AutomatedTask.objects.create( + agent=agent, name="test task 5", - win_task_name=task_name, - task_type="manual", + task_type=TaskType.MANUAL, ) nats_cmd.return_value = "ok" - ret = create_win_task_schedule.s(pk=self.task5.pk).apply() + create_win_task_schedule(pk=task1.pk) nats_cmd.assert_called_with( { "func": "schedtask", "schedtaskpayload": { + "pk": task1.pk, "type": "rmm", + "name": task1.win_task_name, + "overwrite_task": False, + "enabled": True, "trigger": "manual", - "pk": self.task5.pk, - "name": task_name, + "multiple_instances": 1, + "delete_expired_task_after": False, + "start_when_available": False, }, }, timeout=5, ) - self.assertEqual(ret.status, "SUCCESS") class TestTaskPermissions(TacticalTestCase): def setUp(self): self.setup_coresettings() - self.client_setup() + self.setup_client() def test_get_tasks_permissions(self): agent = baker.make_recipe("agents.agent") policy = baker.make("automation.Policy") unauthorized_agent = baker.make_recipe("agents.agent") - task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5) - unauthorized_task = baker.make( + task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5) # noqa + unauthorized_task = baker.make( # noqa "autotasks.AutomatedTask", agent=unauthorized_agent, _quantity=7 ) - policy_tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=2) + policy_tasks = baker.make( # noqa + "autotasks.AutomatedTask", policy=policy, _quantity=2 + ) # test super user access self.check_authorized_superuser("get", f"{base_url}/") @@ -508,7 +776,7 @@ def test_add_task_permissions(self): script = baker.make("scripts.Script") policy_data = { - "policy": policy.id, # type: ignore + "policy": policy.id, "name": "Test Task Manual", "run_time_days": [], "timeout": 120, @@ -545,7 +813,7 @@ def test_add_task_permissions(self): url = f"{base_url}/" - for data in [policy_data, agent_data]: + for data in (policy_data, agent_data): # test superuser access self.check_authorized_superuser("post", url, data) @@ -581,8 +849,7 @@ def test_task_get_edit_delete_permissions(self, delete_task): ) policy_task = baker.make("autotasks.AutomatedTask", policy=policy) - for method in ["get", "put", "delete"]: - + for method in ("get", "put", "delete"): url = f"{base_url}/{task.id}/" unauthorized_url = f"{base_url}/{unauthorized_task.id}/" policy_url = f"{base_url}/{policy_task.id}/" @@ -620,7 +887,6 @@ def test_task_get_edit_delete_permissions(self, delete_task): self.check_authorized(method, policy_url) def test_task_action_permissions(self): - agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") task = baker.make("autotasks.AutomatedTask", agent=agent) @@ -654,9 +920,3 @@ def test_task_action_permissions(self): self.check_authorized("post", url) self.check_not_authorized("post", unauthorized_url) - - def test_policy_fields_to_copy_exists(self): - fields = [i.name for i in AutomatedTask._meta.get_fields()] - task = baker.make("autotasks.AutomatedTask") - for i in task.policy_fields_to_copy: # type: ignore - self.assertIn(i, fields) diff --git a/api/tacticalrmm/autotasks/views.py b/api/tacticalrmm/autotasks/views.py index 1ddb1676be..7e3bb0e0a9 100644 --- a/api/tacticalrmm/autotasks/views.py +++ b/api/tacticalrmm/autotasks/views.py @@ -1,36 +1,34 @@ 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 rest_framework.exceptions import PermissionDenied from agents.models import Agent from automation.models import Policy -from tacticalrmm.utils import get_bit_days from tacticalrmm.permissions import _has_perm_on_agent from .models import AutomatedTask from .permissions import AutoTaskPerms, RunAutoTaskPerms from .serializers import TaskSerializer +from .tasks import remove_orphaned_win_tasks class GetAddAutoTasks(APIView): permission_classes = [IsAuthenticated, AutoTaskPerms] def get(self, request, agent_id=None, policy=None): - if agent_id: agent = get_object_or_404(Agent, agent_id=agent_id) - tasks = AutomatedTask.objects.filter(agent=agent) + tasks = agent.get_tasks_with_policies() elif policy: policy = get_object_or_404(Policy, id=policy) tasks = AutomatedTask.objects.filter(policy=policy) else: - tasks = AutomatedTask.objects.filter_by_role(request.user) + tasks = AutomatedTask.objects.filter_by_role(request.user) # type: ignore return Response(TaskSerializer(tasks, many=True).data) def post(self, request): - from automation.tasks import generate_agent_autotasks_task from autotasks.tasks import create_win_task_schedule data = request.data.copy() @@ -44,25 +42,13 @@ def post(self, request): data["agent"] = agent.pk - bit_weekdays = None - if "run_time_days" in data.keys(): - if data["run_time_days"]: - bit_weekdays = get_bit_days(data["run_time_days"]) - data.pop("run_time_days") - serializer = TaskSerializer(data=data) serializer.is_valid(raise_exception=True) - task = serializer.save( - win_task_name=AutomatedTask.generate_task_name(), - run_time_bit_weekdays=bit_weekdays, - ) + task = serializer.save() if task.agent: create_win_task_schedule.delay(pk=task.pk) - elif task.policy: - generate_agent_autotasks_task.delay(policy=task.policy.pk) - return Response( "The task has been created. It will show up on the agent on next checkin" ) @@ -72,7 +58,6 @@ class GetEditDeleteAutoTask(APIView): permission_classes = [IsAuthenticated, AutoTaskPerms] def get(self, request, pk): - task = get_object_or_404(AutomatedTask, pk=pk) if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): @@ -81,7 +66,6 @@ def get(self, request, pk): return Response(TaskSerializer(task).data) def put(self, request, pk): - task = get_object_or_404(AutomatedTask, pk=pk) if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): @@ -94,7 +78,6 @@ def put(self, request, pk): return Response("The task was updated") def delete(self, request, pk): - from automation.tasks import delete_policy_autotasks_task from autotasks.tasks import delete_win_task_schedule task = get_object_or_404(AutomatedTask, pk=pk) @@ -104,9 +87,9 @@ def delete(self, request, pk): if task.agent: delete_win_task_schedule.delay(pk=task.pk) - elif task.policy: - delete_policy_autotasks_task.delay(task=task.pk) + else: task.delete() + remove_orphaned_win_tasks.delay() return Response(f"{task.name} will be deleted shortly") @@ -122,5 +105,14 @@ def post(self, request, pk): if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id): raise PermissionDenied() - run_win_task.delay(pk=pk) - return Response(f"{task.name} will now be run on {task.agent.hostname}") + # run policy task on agent + if "agent_id" in request.data.keys(): + if not _has_perm_on_agent(request.user, request.data["agent_id"]): + raise PermissionDenied() + + run_win_task.delay(pk=pk, agent_id=request.data["agent_id"]) + + # run normal task on agent + else: + run_win_task.delay(pk=pk) + return Response(f"{task.name} will now be run.") diff --git a/api/tacticalrmm/checks/admin.py b/api/tacticalrmm/checks/admin.py index 755b9ce17f..62d9d56506 100644 --- a/api/tacticalrmm/checks/admin.py +++ b/api/tacticalrmm/checks/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from .models import Check, CheckHistory +from .models import Check, CheckHistory, CheckResult admin.site.register(Check) admin.site.register(CheckHistory) +admin.site.register(CheckResult) diff --git a/api/tacticalrmm/checks/baker_recipes.py b/api/tacticalrmm/checks/baker_recipes.py index 0887e19aba..9ff5f1a34a 100644 --- a/api/tacticalrmm/checks/baker_recipes.py +++ b/api/tacticalrmm/checks/baker_recipes.py @@ -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 @@ ) 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" ) diff --git a/api/tacticalrmm/checks/constants.py b/api/tacticalrmm/checks/constants.py new file mode 100644 index 0000000000..702bec392a --- /dev/null +++ b/api/tacticalrmm/checks/constants.py @@ -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", +) diff --git a/api/tacticalrmm/checks/migrations/0026_auto_20220401_2244.py b/api/tacticalrmm/checks/migrations/0026_auto_20220401_2244.py new file mode 100644 index 0000000000..b704accb9c --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0026_auto_20220401_2244.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.12 on 2022-04-01 22:44 + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agents', '0046_alter_agenthistory_command'), + ('checks', '0025_auto_20210917_1954'), + ] + + operations = [ + migrations.RenameField( + model_name='check', + old_name='overriden_by_policy', + new_name='overridden_by_policy', + ), + migrations.AddField( + model_name='checkhistory', + name='agent_id', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.CreateModel( + name='CheckResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('passing', 'Passing'), ('failing', 'Failing'), ('pending', 'Pending')], default='pending', max_length=100)), + ('alert_severity', models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15, null=True)), + ('more_info', models.TextField(blank=True, null=True)), + ('last_run', models.DateTimeField(blank=True, null=True)), + ('fail_count', models.PositiveIntegerField(default=0)), + ('outage_history', models.JSONField(blank=True, null=True)), + ('extra_details', models.JSONField(blank=True, null=True)), + ('stdout', models.TextField(blank=True, null=True)), + ('stderr', models.TextField(blank=True, null=True)), + ('retcode', models.IntegerField(blank=True, null=True)), + ('execution_time', models.CharField(blank=True, max_length=100, null=True)), + ('history', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(blank=True), blank=True, default=list, null=True, size=None)), + ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkresults', to='agents.agent')), + ('assigned_check', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkresults', to='checks.check')), + ], + options={ + 'unique_together': {('agent', 'assigned_check')}, + }, + ), + ] diff --git a/api/tacticalrmm/checks/migrations/0027_auto_20220401_2248.py b/api/tacticalrmm/checks/migrations/0027_auto_20220401_2248.py new file mode 100644 index 0000000000..400910476a --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0027_auto_20220401_2248.py @@ -0,0 +1,80 @@ +# Generated by Django 3.2.12 on 2022-04-01 22:48 + +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") + CheckResult = apps.get_model("checks", "CheckResult") + for check in Check.objects.exclude(agent=None).iterator(): + + try: + with transaction.atomic(): + if check.managed_by_policy: + CheckResult.objects.create( + assigned_check_id=check.parent_check, + agent_id=check.agent.id, + status=check.status, + more_info=check.more_info, + last_run=check.last_run, + fail_count=check.fail_count, + outage_history=check.outage_history, + extra_details=check.extra_details, + stdout=check.stdout, + stderr=check.stderr, + retcode=check.retcode, + execution_time=check.execution_time, + history=check.history, + alert_severity=check.alert_severity + if check.check_type + in [ + CheckType.MEMORY, + CheckType.CPU_LOAD, + CheckType.DISK_SPACE, + CheckType.SCRIPT, + ] + else None, + ) + + else: + CheckResult.objects.create( + assigned_check_id=check.id, + agent_id=check.agent.id, + status=check.status, + more_info=check.more_info, + last_run=check.last_run, + fail_count=check.fail_count, + outage_history=check.outage_history, + extra_details=check.extra_details, + stdout=check.stdout, + stderr=check.stderr, + retcode=check.retcode, + execution_time=check.execution_time, + history=check.history, + alert_severity=check.alert_severity + if check.check_type + in [ + CheckType.MEMORY, + CheckType.CPU_LOAD, + CheckType.DISK_SPACE, + CheckType.SCRIPT, + ] + else None, + ) + except IntegrityError: + pass + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("checks", "0026_auto_20220401_2244"), + ] + + operations = [ + migrations.RunPython(migrate_check_results), + ] diff --git a/api/tacticalrmm/checks/migrations/0028_auto_20220401_2301.py b/api/tacticalrmm/checks/migrations/0028_auto_20220401_2301.py new file mode 100644 index 0000000000..67c2efb13b --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0028_auto_20220401_2301.py @@ -0,0 +1,61 @@ +# Generated by Django 3.2.12 on 2022-04-01 23:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('checks', '0027_auto_20220401_2248'), + ] + + operations = [ + migrations.RemoveField( + model_name='check', + name='execution_time', + ), + migrations.RemoveField( + model_name='check', + name='extra_details', + ), + migrations.RemoveField( + model_name='check', + name='fail_count', + ), + migrations.RemoveField( + model_name='check', + name='history', + ), + migrations.RemoveField( + model_name='check', + name='last_run', + ), + migrations.RemoveField( + model_name='check', + name='more_info', + ), + migrations.RemoveField( + model_name='check', + name='outage_history', + ), + migrations.RemoveField( + model_name='check', + name='parent_check', + ), + migrations.RemoveField( + model_name='check', + name='retcode', + ), + migrations.RemoveField( + model_name='check', + name='status', + ), + migrations.RemoveField( + model_name='check', + name='stderr', + ), + migrations.RemoveField( + model_name='check', + name='stdout', + ), + ] diff --git a/api/tacticalrmm/checks/migrations/0029_alter_checkresult_alert_severity.py b/api/tacticalrmm/checks/migrations/0029_alter_checkresult_alert_severity.py new file mode 100644 index 0000000000..79d9fcd1af --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0029_alter_checkresult_alert_severity.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.3 on 2022-04-15 21:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('checks', '0028_auto_20220401_2301'), + ] + + operations = [ + migrations.AlterField( + model_name='checkresult', + name='alert_severity', + field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=15, null=True), + ), + ] diff --git a/api/tacticalrmm/checks/migrations/0030_alter_checkresult_retcode.py b/api/tacticalrmm/checks/migrations/0030_alter_checkresult_retcode.py new file mode 100644 index 0000000000..7caaae4ad5 --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0030_alter_checkresult_retcode.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2022-06-29 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('checks', '0029_alter_checkresult_alert_severity'), + ] + + operations = [ + migrations.AlterField( + model_name='checkresult', + name='retcode', + field=models.BigIntegerField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/checks/migrations/0031_check_env_vars.py b/api/tacticalrmm/checks/migrations/0031_check_env_vars.py new file mode 100644 index 0000000000..a939562452 --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0031_check_env_vars.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.3 on 2022-12-03 09:38 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("checks", "0030_alter_checkresult_retcode"), + ] + + operations = [ + migrations.AddField( + model_name="check", + name="env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/api/tacticalrmm/checks/models.py b/api/tacticalrmm/checks/models.py index 3c14bc5138..e845cd5db4 100644 --- a/api/tacticalrmm/checks/models.py +++ b/api/tacticalrmm/checks/models.py @@ -1,53 +1,30 @@ -import json -import os -import string from statistics import mean -from typing import Any +from typing import TYPE_CHECKING, Any, Dict, Optional, Union -import pytz -from alerts.models import SEVERITY_CHOICES -from core.models import CoreSettings -from django.conf import settings 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 django.utils import timezone as djangotime + +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, + AlertSeverity, + CheckStatus, + CheckType, + EvtLogFailWhen, + EvtLogNames, + EvtLogTypes, +) from tacticalrmm.models import PermissionQuerySet -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"), -] +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 class Check(BaseAuditModel): @@ -69,33 +46,23 @@ class Check(BaseAuditModel): blank=True, on_delete=models.CASCADE, ) - managed_by_policy = models.BooleanField(default=False) - overriden_by_policy = models.BooleanField(default=False) - parent_check = models.PositiveIntegerField(null=True, blank=True) + 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" - ) - status = models.CharField( - max_length=100, choices=CHECK_STATUS_CHOICES, default="pending" + max_length=50, choices=CheckType.choices, default=CheckType.DISK_SPACE ) - more_info = models.TextField(null=True, blank=True) - last_run = models.DateTimeField(null=True, blank=True) email_alert = models.BooleanField(default=False) text_alert = models.BooleanField(default=False) dashboard_alert = models.BooleanField(default=False) fails_b4_alert = models.PositiveIntegerField(default=1) - fail_count = models.PositiveIntegerField(default=0) - outage_history = models.JSONField(null=True, blank=True) # store - extra_details = models.JSONField(null=True, blank=True) run_interval = models.PositiveIntegerField(blank=True, default=0) # check specific fields # for eventlog, script, ip, and service alert severity alert_severity = models.CharField( max_length=15, - choices=SEVERITY_CHOICES, - default="warning", + choices=AlertSeverity.choices, + default=AlertSeverity.WARNING, null=True, blank=True, ) @@ -131,6 +98,12 @@ class Check(BaseAuditModel): blank=True, default=list, ) + env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) info_return_codes = ArrayField( models.PositiveIntegerField(), null=True, @@ -144,14 +117,6 @@ class Check(BaseAuditModel): default=list, ) timeout = models.PositiveIntegerField(null=True, blank=True) - stdout = models.TextField(null=True, blank=True) - stderr = models.TextField(null=True, blank=True) - retcode = models.IntegerField(null=True, blank=True) - execution_time = models.CharField(max_length=100, null=True, blank=True) - # cpu and mem check history - history = ArrayField( - models.IntegerField(blank=True), null=True, blank=True, default=list - ) # win service checks svc_name = models.CharField(max_length=255, null=True, blank=True) svc_display_name = models.CharField(max_length=255, null=True, blank=True) @@ -164,138 +129,113 @@ 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( null=True, blank=True, default=1 ) + # deprecated + managed_by_policy = models.BooleanField(default=False) + + # non-database property + check_result: "Union[CheckResult, Dict[None, None]]" = {} + def __str__(self): if self.agent: return f"{self.agent.hostname} - {self.readable_desc}" - else: - return f"{self.policy.name} - {self.readable_desc}" + + return f"{self.policy.name} - {self.readable_desc}" + + def save(self, *args, **kwargs): + # if check is a policy check clear cache on everything + if self.policy: + cache.delete_many_pattern("site_*_checks") + cache.delete_many_pattern("agent_*_checks") + + # if check is an agent check + elif self.agent: + cache.delete(f"agent_{self.agent.agent_id}_checks") + + super(Check, self).save( + *args, + **kwargs, + ) + + def delete(self, *args, **kwargs): + # if check is a policy check clear cache on everything + if self.policy: + cache.delete_many_pattern("site_*_checks") + cache.delete_many_pattern("agent_*_checks") + + # if check is an agent check + elif self.agent: + cache.delete(f"agent_{self.agent.agent_id}_checks") + + super(Check, self).delete( + *args, + **kwargs, + ) @property def readable_desc(self): - if self.check_type == "diskspace": - + display = self.get_check_type_display() # type: ignore + if self.check_type == CheckType.DISK_SPACE: text = "" if self.warning_threshold: text += f" Warning Threshold: {self.warning_threshold}%" if self.error_threshold: text += f" Error Threshold: {self.error_threshold}%" - return f"{self.get_check_type_display()}: Drive {self.disk} - {text}" # type: ignore - elif self.check_type == "ping": - return f"{self.get_check_type_display()}: {self.name}" # type: ignore - elif self.check_type == "cpuload" or self.check_type == "memory": - + return f"{display}: Drive {self.disk} - {text}" + elif self.check_type == CheckType.PING: + return f"{display}: {self.name}" + elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): text = "" if self.warning_threshold: text += f" Warning Threshold: {self.warning_threshold}%" if self.error_threshold: text += f" Error Threshold: {self.error_threshold}%" - return f"{self.get_check_type_display()} - {text}" # type: ignore - elif self.check_type == "winsvc": - return f"{self.get_check_type_display()}: {self.svc_display_name}" # type: ignore - elif self.check_type == "eventlog": - return f"{self.get_check_type_display()}: {self.name}" # type: ignore - elif self.check_type == "script": - return f"{self.get_check_type_display()}: {self.script.name}" # type: ignore - else: - return "n/a" + return f"{display} - {text}" + elif self.check_type == CheckType.WINSVC: + return f"{display}: {self.svc_display_name}" + elif self.check_type == CheckType.EVENT_LOG: + return f"{display}: {self.name}" + elif self.check_type == CheckType.SCRIPT: + return f"{display}: {self.script.name}" - @property - def history_info(self): - if self.check_type == "cpuload" or self.check_type == "memory": - return ", ".join(str(f"{x}%") for x in self.history[-6:]) - - @property - def last_run_as_timezone(self): - if self.last_run is not None and self.agent is not None: - return self.last_run.astimezone( - pytz.timezone(self.agent.timezone) - ).strftime("%b-%d-%Y - %H:%M") - - return self.last_run + return "n/a" @staticmethod def non_editable_fields() -> list[str]: - return [ - "check_type", - "more_info", - "last_run", - "fail_count", - "outage_history", - "extra_details", - "status", - "stdout", - "stderr", - "retcode", - "execution_time", - "history", - "readable_desc", - "history_info", - "parent_check", - "managed_by_policy", - "overriden_by_policy", - "created_by", - "created_time", - "modified_by", - "modified_time", - ] + return CHECKS_NON_EDITABLE_FIELDS - @property - def policy_fields_to_copy(self) -> list[str]: - return [ - "warning_threshold", - "error_threshold", - "alert_severity", - "name", - "run_interval", - "disk", - "fails_b4_alert", - "ip", - "script", - "script_args", - "info_return_codes", - "warning_return_codes", - "timeout", - "svc_name", - "svc_display_name", - "svc_policy_mode", - "pass_if_start_pending", - "pass_if_svc_not_exist", - "restart_if_stopped", - "log_name", - "event_id", - "event_id_is_wildcard", - "event_type", - "event_source", - "event_message", - "fail_when", - "search_last_days", - "number_of_events_b4_alert", - "email_alert", - "text_alert", - "dashboard_alert", - ] + def create_policy_check(self, policy: "Policy") -> None: + check = Check.objects.create( + policy=policy, + ) - def should_create_alert(self, alert_template=None): + for task in self.assignedtasks.all(): # type: ignore + task.create_policy_task(policy=policy, assigned_check=check) + for field in POLICY_CHECK_FIELDS_TO_COPY: + setattr(check, field, getattr(self, field)) + + check.save() + + def should_create_alert(self, alert_template=None): return ( self.dashboard_alert or self.email_alert @@ -310,85 +250,195 @@ def should_create_alert(self, alert_template=None): ) ) - def add_check_history(self, value: int, more_info: Any = None) -> None: - CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info) + def add_check_history( + self, value: int, agent_id: str, more_info: Any = None + ) -> None: + CheckHistory.objects.create( + check_id=self.pk, y=value, results=more_info, agent_id=agent_id + ) + + @staticmethod + def serialize(check): + # serializes the check and returns json + from .serializers import CheckAuditSerializer + + return CheckAuditSerializer(check).data + + def is_duplicate(self, check): + if self.check_type == CheckType.DISK_SPACE: + return self.disk == check.disk + + elif self.check_type == CheckType.SCRIPT: + return self.script == check.script + + elif self.check_type == CheckType.PING: + return self.ip == check.ip + + elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): + return True + + elif self.check_type == CheckType.WINSVC: + return self.svc_name == check.svc_name + + elif self.check_type == CheckType.EVENT_LOG: + return [self.log_name, self.event_id] == [check.log_name, check.event_id] + + +class CheckResult(models.Model): + objects = PermissionQuerySet.as_manager() + + class Meta: + unique_together = (("agent", "assigned_check"),) - def handle_check(self, data): + agent = models.ForeignKey( + "agents.Agent", + related_name="checkresults", + on_delete=models.CASCADE, + ) + + assigned_check = models.ForeignKey( + "checks.Check", + related_name="checkresults", + on_delete=models.CASCADE, + ) + status = models.CharField( + max_length=100, choices=CheckStatus.choices, default=CheckStatus.PENDING + ) + # for memory, diskspace, script, and cpu checks where severity changes + alert_severity = models.CharField( + max_length=15, + choices=AlertSeverity.choices, + null=True, + blank=True, + ) + more_info = models.TextField(null=True, blank=True) + last_run = models.DateTimeField(null=True, blank=True) + fail_count = models.PositiveIntegerField(default=0) + outage_history = models.JSONField(null=True, blank=True) # store + extra_details = models.JSONField(null=True, blank=True) + stdout = models.TextField(null=True, blank=True) + stderr = models.TextField(null=True, blank=True) + retcode = models.BigIntegerField(null=True, blank=True) + execution_time = models.CharField(max_length=100, null=True, blank=True) + # cpu and mem check history + history = ArrayField( + models.IntegerField(blank=True), null=True, blank=True, default=list + ) + + def __str__(self): + return f"{self.agent.hostname} - {self.assigned_check}" + + def save(self, *args, **kwargs): + # if check is a policy check clear cache on everything + if not self.alert_severity and self.assigned_check.check_type in ( + CheckType.MEMORY, + CheckType.CPU_LOAD, + CheckType.DISK_SPACE, + CheckType.SCRIPT, + ): + self.alert_severity = AlertSeverity.WARNING + + super(CheckResult, self).save( + *args, + **kwargs, + ) + + @property + def history_info(self): + 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( + self, alert_template: "Optional[AlertTemplate]" + ) -> "Optional[Alert]": from alerts.models import Alert - # cpuload or mem checks - if self.check_type == "cpuload" or self.check_type == "memory": + return Alert.create_or_return_check_alert( + self.assigned_check, + agent=self.agent, + alert_severity=self.alert_severity, + skip_create=not self.assigned_check.should_create_alert(alert_template), + ) + def handle_check(self, data, check: "Check", agent: "Agent"): + from alerts.models import Alert + + update_fields = [] + # cpuload or mem checks + 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 self.error_threshold and avg > self.error_threshold: - self.status = "failing" - self.alert_severity = "error" - elif self.warning_threshold and avg > self.warning_threshold: - self.status = "failing" - self.alert_severity = "warning" + if check.error_threshold and avg > check.error_threshold: + self.status = CheckStatus.FAILING + self.alert_severity = AlertSeverity.ERROR + elif check.warning_threshold and avg > check.warning_threshold: + self.status = CheckStatus.FAILING + self.alert_severity = AlertSeverity.WARNING else: - self.status = "passing" + self.status = CheckStatus.PASSING # add check history - self.add_check_history(data["percent"]) + check.add_check_history(data["percent"], agent.agent_id) # diskspace checks - elif self.check_type == "diskspace": + elif check.check_type == CheckType.DISK_SPACE: if data["exists"]: percent_used = round(data["percent_used"]) - if self.error_threshold and (100 - percent_used) < self.error_threshold: - self.status = "failing" - self.alert_severity = "error" + if ( + check.error_threshold + and (100 - percent_used) < check.error_threshold + ): + self.status = CheckStatus.FAILING + self.alert_severity = AlertSeverity.ERROR elif ( - self.warning_threshold - and (100 - percent_used) < self.warning_threshold + check.warning_threshold + and (100 - percent_used) < check.warning_threshold ): - self.status = "failing" - self.alert_severity = "warning" + self.status = CheckStatus.FAILING + self.alert_severity = AlertSeverity.WARNING else: - self.status = "passing" + self.status = CheckStatus.PASSING self.more_info = data["more_info"] # add check history - self.add_check_history(100 - percent_used) + check.add_check_history(100 - percent_used, agent.agent_id) else: - self.status = "failing" - self.alert_severity = "error" - self.more_info = f"Disk {self.disk} does not exist" + self.status = CheckStatus.FAILING + self.alert_severity = AlertSeverity.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 self.check_type == "script": + elif check.check_type == CheckType.SCRIPT: self.stdout = data["stdout"] self.stderr = data["stderr"] self.retcode = data["retcode"] self.execution_time = "{:.4f}".format(data["runtime"]) - if data["retcode"] in self.info_return_codes: - self.alert_severity = "info" - self.status = "failing" - elif data["retcode"] in self.warning_return_codes: - self.alert_severity = "warning" - self.status = "failing" + if data["retcode"] in check.info_return_codes: + self.alert_severity = AlertSeverity.INFO + self.status = CheckStatus.FAILING + elif data["retcode"] in check.warning_return_codes: + self.alert_severity = AlertSeverity.WARNING + self.status = CheckStatus.FAILING elif data["retcode"] != 0: - self.status = "failing" - self.alert_severity = "error" + self.status = CheckStatus.FAILING + self.alert_severity = AlertSeverity.ERROR else: - self.status = "passing" + self.status = CheckStatus.PASSING - self.save( - update_fields=[ + update_fields.extend( + [ "stdout", "stderr", "retcode", @@ -397,8 +447,9 @@ def handle_check(self, data): ) # add check history - self.add_check_history( - 1 if self.status == "failing" else 0, + check.add_check_history( + 1 if self.status == CheckStatus.FAILING else 0, + agent.agent_id, { "retcode": data["retcode"], "stdout": data["stdout"][:60], @@ -408,127 +459,78 @@ def handle_check(self, data): ) # ping checks - elif self.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"]) - self.add_check_history( - 1 if self.status == "failing" else 0, self.more_info[:60] + check.add_check_history( + 1 if self.status == CheckStatus.FAILING else 0, + agent.agent_id, + self.more_info[:60], ) # windows service checks - elif self.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"]) - self.add_check_history( - 1 if self.status == "failing" else 0, self.more_info[:60] + check.add_check_history( + 1 if self.status == CheckStatus.FAILING else 0, + agent.agent_id, + self.more_info[:60], ) - elif self.check_type == "eventlog": + elif check.check_type == CheckType.EVENT_LOG: log = data["log"] - if self.fail_when == "contains": - if log and len(log) >= self.number_of_events_b4_alert: - self.status = "failing" + if check.fail_when == EvtLogFailWhen.CONTAINS: + if log and len(log) >= check.number_of_events_b4_alert: + self.status = CheckStatus.FAILING else: - self.status = "passing" + self.status = CheckStatus.PASSING - elif self.fail_when == "not_contains": - if log and len(log) >= self.number_of_events_b4_alert: - self.status = "passing" + elif check.fail_when == EvtLogFailWhen.NOT_CONTAINS: + if log and len(log) >= check.number_of_events_b4_alert: + 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"]) - self.add_check_history( - 1 if self.status == "failing" else 0, + check.add_check_history( + 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 >= self.fails_b4_alert: + 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() - if Alert.objects.filter(assigned_check=self, resolved=False).exists(): + update_fields.extend(["status", "fail_count", "alert_severity", "last_run"]) + self.save(update_fields=update_fields) + if Alert.objects.filter( + 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 - def handle_assigned_task(self) -> None: - for task in self.assignedtask.all(): # type: ignore - if task.enabled: - task.run_win_task() - - @staticmethod - def serialize(check): - # serializes the check and returns json - from .serializers import CheckAuditSerializer - - return CheckAuditSerializer(check).data - - def create_policy_check(self, agent=None, policy=None): - - if (not agent and not policy) or (agent and policy): - return - - check = Check.objects.create( - agent=agent, - policy=policy, - managed_by_policy=bool(agent), - parent_check=(self.pk if agent else None), - check_type=self.check_type, - script=self.script, - ) - - for task in self.assignedtask.all(): # type: ignore - if policy or ( - agent and not agent.autotasks.filter(parent_task=task.pk).exists() - ): - task.create_policy_task( - agent=agent, policy=policy, assigned_check=check - ) - - for field in self.policy_fields_to_copy: - setattr(check, field, getattr(self, field)) - - check.save() - - def is_duplicate(self, check): - if self.check_type == "diskspace": - return self.disk == check.disk - - elif self.check_type == "script": - return self.script == check.script - - elif self.check_type == "ping": - return self.ip == check.ip - - elif self.check_type == "cpuload": - return True - - elif self.check_type == "memory": - return True - - elif self.check_type == "winsvc": - return self.svc_name == check.svc_name - - elif self.check_type == "eventlog": - return [self.log_name, self.event_id] == [check.log_name, check.event_id] - def send_email(self): - - CORE = CoreSettings.objects.first() + CORE = get_core_settings() body: str = "" if self.agent: @@ -536,66 +538,63 @@ def send_email(self): else: subject = f"{self} Failed" - if self.check_type == "diskspace": + if self.assigned_check.check_type == CheckType.DISK_SPACE: text = "" - if self.warning_threshold: - text += f" Warning Threshold: {self.warning_threshold}%" - if self.error_threshold: - text += f" Error Threshold: {self.error_threshold}%" + if self.assigned_check.warning_threshold: + text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" + if self.assigned_check.error_threshold: + text += f" Error Threshold: {self.assigned_check.error_threshold}%" try: percent_used = [ - d["percent"] for d in self.agent.disks if d["device"] == self.disk + d["percent"] + for d in self.agent.disks + if d["device"] == self.assigned_check.disk ][0] percent_free = 100 - percent_used body = subject + f" - Free: {percent_free}%, {text}" except: - body = subject + f" - Disk {self.disk} does not exist" - - elif self.check_type == "script": + body = subject + f" - Disk {self.assigned_check.disk} does not exist" + elif self.assigned_check.check_type == CheckType.SCRIPT: body = ( subject + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" ) - elif self.check_type == "ping": - + elif self.assigned_check.check_type == CheckType.PING: body = self.more_info - elif self.check_type == "cpuload" or self.check_type == "memory": + elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): text = "" - if self.warning_threshold: - text += f" Warning Threshold: {self.warning_threshold}%" - if self.error_threshold: - text += f" Error Threshold: {self.error_threshold}%" + if self.assigned_check.warning_threshold: + text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" + if self.assigned_check.error_threshold: + text += f" Error Threshold: {self.assigned_check.error_threshold}%" avg = int(mean(self.history)) - if self.check_type == "cpuload": + if self.assigned_check.check_type == CheckType.CPU_LOAD: body = subject + f" - Average CPU utilization: {avg}%, {text}" - elif self.check_type == "memory": + elif self.assigned_check.check_type == CheckType.MEMORY: body = subject + f" - Average memory usage: {avg}%, {text}" - elif self.check_type == "winsvc": + elif self.assigned_check.check_type == CheckType.WINSVC: body = subject + f" - Status: {self.more_info}" - elif self.check_type == "eventlog": - - if self.event_source and self.event_message: - start = f"Event ID {self.event_id}, source {self.event_source}, containing string {self.event_message} " - elif self.event_source: - start = f"Event ID {self.event_id}, source {self.event_source} " - elif self.event_message: - start = ( - f"Event ID {self.event_id}, containing string {self.event_message} " - ) + 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} " + elif self.assigned_check.event_source: + start = f"Event ID {self.assigned_check.event_id}, source {self.assigned_check.event_source} " + elif self.assigned_check.event_message: + start = f"Event ID {self.assigned_check.event_id}, containing string {self.assigned_check.event_message} " else: - start = f"Event ID {self.event_id} " + start = f"Event ID {self.assigned_check.event_id} " - body = start + f"was found in the {self.log_name} log\n\n" + body = start + f"was found in the {self.assigned_check.log_name} log\n\n" for i in self.extra_details["log"]: try: @@ -607,8 +606,7 @@ def send_email(self): CORE.send_mail(subject, body, alert_template=self.agent.alert_template) def send_sms(self): - - CORE = CoreSettings.objects.first() + CORE = get_core_settings() body: str = "" if self.agent: @@ -616,47 +614,49 @@ def send_sms(self): else: subject = f"{self} Failed" - if self.check_type == "diskspace": + if self.assigned_check.check_type == CheckType.DISK_SPACE: text = "" - if self.warning_threshold: - text += f" Warning Threshold: {self.warning_threshold}%" - if self.error_threshold: - text += f" Error Threshold: {self.error_threshold}%" + if self.assigned_check.warning_threshold: + text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" + if self.assigned_check.error_threshold: + text += f" Error Threshold: {self.assigned_check.error_threshold}%" try: percent_used = [ - d["percent"] for d in self.agent.disks if d["device"] == self.disk + d["percent"] + for d in self.agent.disks + if d["device"] == self.assigned_check.disk ][0] percent_free = 100 - percent_used body = subject + f" - Free: {percent_free}%, {text}" except: - body = subject + f" - Disk {self.disk} does not exist" + body = subject + f" - Disk {self.assigned_check.disk} does not exist" - elif self.check_type == "script": + elif self.assigned_check.check_type == CheckType.SCRIPT: body = subject + f" - Return code: {self.retcode}" - elif self.check_type == "ping": + elif self.assigned_check.check_type == CheckType.PING: body = subject - elif self.check_type == "cpuload" or self.check_type == "memory": + elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): text = "" - if self.warning_threshold: - text += f" Warning Threshold: {self.warning_threshold}%" - if self.error_threshold: - text += f" Error Threshold: {self.error_threshold}%" + if self.assigned_check.warning_threshold: + text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" + if self.assigned_check.error_threshold: + text += f" Error Threshold: {self.assigned_check.error_threshold}%" avg = int(mean(self.history)) - if self.check_type == "cpuload": + if self.assigned_check.check_type == CheckType.CPU_LOAD: body = subject + f" - Average CPU utilization: {avg}%, {text}" - elif self.check_type == "memory": + elif self.assigned_check.check_type == CheckType.MEMORY: body = subject + f" - Average memory usage: {avg}%, {text}" - elif self.check_type == "winsvc": + elif self.assigned_check.check_type == CheckType.WINSVC: body = subject + f" - Status: {self.more_info}" - elif self.check_type == "eventlog": + elif self.assigned_check.check_type == CheckType.EVENT_LOG: body = subject CORE.send_sms(body, alert_template=self.agent.alert_template) def send_resolved_email(self): - CORE = CoreSettings.objects.first() + CORE = get_core_settings() subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" body = f"{self} is now back to normal" @@ -664,7 +664,7 @@ def send_resolved_email(self): CORE.send_mail(subject, body, alert_template=self.agent.alert_template) def send_resolved_sms(self): - CORE = CoreSettings.objects.first() + CORE = get_core_settings() subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved" CORE.send_sms(subject, alert_template=self.agent.alert_template) @@ -674,6 +674,7 @@ class CheckHistory(models.Model): objects = PermissionQuerySet.as_manager() check_id = models.PositiveIntegerField(default=0) + agent_id = models.CharField(max_length=200, null=True, blank=True) x = models.DateTimeField(auto_now_add=True) y = models.PositiveIntegerField(null=True, blank=True, default=None) results = models.JSONField(null=True, blank=True) diff --git a/api/tacticalrmm/checks/permissions.py b/api/tacticalrmm/checks/permissions.py index 999a1a0696..8a49e199d8 100644 --- a/api/tacticalrmm/checks/permissions.py +++ b/api/tacticalrmm/checks/permissions.py @@ -1,11 +1,16 @@ from rest_framework import permissions -from tacticalrmm.permissions import _has_perm, _has_perm_on_agent +from tacticalrmm.permissions import ( + _has_perm, + _has_perm_on_agent, + _has_perm_on_client, + _has_perm_on_site, +) class ChecksPerms(permissions.BasePermission): - def has_permission(self, r, view): - if r.method == "GET" or r.method == "PATCH": + def has_permission(self, r, view) -> bool: + if r.method in ("GET", "PATCH"): if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_list_checks") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] @@ -17,7 +22,21 @@ def has_permission(self, r, view): class RunChecksPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_run_checks") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) + + +class BulkRunChecksPerms(permissions.BasePermission): + def has_permission(self, r, view) -> bool: + if not _has_perm(r, "can_run_checks"): + return False + + if view.kwargs["target"] == "client": + return _has_perm_on_client(user=r.user, client_id=view.kwargs["pk"]) + + elif view.kwargs["target"] == "site": + return _has_perm_on_site(user=r.user, site_id=view.kwargs["pk"]) + + return False diff --git a/api/tacticalrmm/checks/serializers.py b/api/tacticalrmm/checks/serializers.py index 5bc7e10840..5a516285bf 100644 --- a/api/tacticalrmm/checks/serializers.py +++ b/api/tacticalrmm/checks/serializers.py @@ -1,12 +1,12 @@ -import pytz import validators as _v 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 -from scripts.models import Script +from .models import Check, CheckHistory, CheckResult class AssignedTaskField(serializers.ModelSerializer): @@ -15,13 +15,24 @@ class Meta: fields = "__all__" -class CheckSerializer(serializers.ModelSerializer): +class CheckResultSerializer(serializers.ModelSerializer): + class Meta: + model = CheckResult + fields = "__all__" + +class CheckSerializer(serializers.ModelSerializer): readable_desc = serializers.ReadOnlyField() - assigned_task = serializers.SerializerMethodField() - last_run = serializers.ReadOnlyField(source="last_run_as_timezone") - history_info = serializers.ReadOnlyField() + assignedtasks = AssignedTaskField(many=True, read_only=True) alert_template = serializers.SerializerMethodField() + check_result = serializers.SerializerMethodField() + + def get_check_result(self, obj): + return ( + CheckResultSerializer(obj.check_result).data + if isinstance(obj.check_result, CheckResult) + else {} + ) def get_alert_template(self, obj): if obj.agent: @@ -31,22 +42,13 @@ def get_alert_template(self, obj): if not alert_template: return None - else: - return { - "name": alert_template.name, - "always_email": alert_template.check_always_email, - "always_text": alert_template.check_always_text, - "always_alert": alert_template.check_always_alert, - } - - ## Change to return only array of tasks after 9/25/2020 - def get_assigned_task(self, obj): - if obj.assignedtask.exists(): - tasks = obj.assignedtask.all() - if len(tasks) == 1: - return AssignedTaskField(tasks[0]).data - else: - return AssignedTaskField(tasks, many=True).data + + return { + "name": alert_template.name, + "always_email": alert_template.check_always_email, + "always_text": alert_template.check_always_text, + "always_alert": alert_template.check_always_alert, + } class Meta: model = Check @@ -66,12 +68,10 @@ def validate(self, val): # 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") - .exclude(managed_by_policy=True) + checks = Check.objects.filter(**filter).filter( + check_type=CheckType.DISK_SPACE ) for check in checks: if val["disk"] in check.disk: @@ -81,7 +81,7 @@ def validate(self, val): if not val["warning_threshold"] and not val["error_threshold"]: raise serializers.ValidationError( - f"Warning threshold or Error Threshold must be set" + "Warning threshold or Error Threshold must be set" ) if ( @@ -90,11 +90,11 @@ def validate(self, val): and val["error_threshold"] > 0 ): raise serializers.ValidationError( - f"Warning threshold must be greater than Error Threshold" + "Warning threshold must be greater than Error Threshold" ) # ping checks - if check_type == "ping": + if check_type == CheckType.PING: if ( not _v.ipv4(val["ip"]) and not _v.ipv6(val["ip"]) @@ -104,19 +104,15 @@ def validate(self, val): "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") - .exclude(managed_by_policy=True) - .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" ) if not val["warning_threshold"] and not val["error_threshold"]: raise serializers.ValidationError( - f"Warning threshold or Error Threshold must be set" + "Warning threshold or Error Threshold must be set" ) if ( @@ -125,22 +121,18 @@ def validate(self, val): and val["error_threshold"] > 0 ): raise serializers.ValidationError( - f"Warning threshold must be less than Error Threshold" + "Warning threshold must be less than Error Threshold" ) - if check_type == "memory" and not self.instance: - if ( - Check.objects.filter(**filter, check_type="memory") - .exclude(managed_by_policy=True) - .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" ) if not val["warning_threshold"] and not val["error_threshold"]: raise serializers.ValidationError( - f"Warning threshold or Error Threshold must be set" + "Warning threshold or Error Threshold must be set" ) if ( @@ -149,7 +141,7 @@ def validate(self, val): and val["error_threshold"] > 0 ): raise serializers.ValidationError( - f"Warning threshold must be less than Error Threshold" + "Warning threshold must be less than Error Threshold" ) return val @@ -165,61 +157,44 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer): # only send data needed for agent to run a check script = ScriptCheckSerializer(read_only=True) script_args = serializers.SerializerMethodField() + env_vars = 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 ) + def get_env_vars(self, obj): + if obj.check_type != CheckType.SCRIPT: + return [] + + # check's env_vars override the script's env vars + return obj.env_vars or obj.script.env_vars + class Meta: model = Check exclude = [ "policy", - "managed_by_policy", - "overriden_by_policy", - "parent_check", + "overridden_by_policy", "name", - "more_info", - "last_run", "email_alert", "text_alert", "fails_b4_alert", - "fail_count", - "outage_history", - "extra_details", - "stdout", - "stderr", - "retcode", - "execution_time", "svc_display_name", "svc_policy_mode", "created_by", "created_time", "modified_by", "modified_time", - "history", "dashboard_alert", ] -class CheckResultsSerializer(serializers.ModelSerializer): - # used when patching results from the windows agent - # no validation needed - - class Meta: - model = Check - fields = "__all__" - - class CheckHistorySerializer(serializers.ModelSerializer): - x = serializers.SerializerMethodField() - - def get_x(self, obj): - return obj.x.astimezone(pytz.timezone(self.context["timezone"])).isoformat() - # used for return large amounts of graph data class Meta: model = CheckHistory diff --git a/api/tacticalrmm/checks/tasks.py b/api/tacticalrmm/checks/tasks.py index 1b5fa1f753..3e520f63a8 100644 --- a/api/tacticalrmm/checks/tasks.py +++ b/api/tacticalrmm/checks/tasks.py @@ -1,23 +1,31 @@ import datetime as dt -import random from time import sleep -from typing import Union +from typing import Optional from django.utils import timezone as djangotime +from alerts.models import Alert +from checks.models import CheckResult from tacticalrmm.celery import app +from tacticalrmm.helpers import rand_range @app.task -def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) +def handle_check_email_alert_task( + pk: int, alert_interval: Optional[float] = None +) -> str: + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending email if not alert.email_sent: - sleep(random.randint(1, 10)) - alert.assigned_check.send_email() + check_result = CheckResult.objects.get( + assigned_check=alert.assigned_check, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + check_result.send_email() alert.email_sent = djangotime.now() alert.save(update_fields=["email_sent"]) else: @@ -25,8 +33,11 @@ def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) # send an email only if the last email sent is older than alert interval delta = djangotime.now() - dt.timedelta(days=alert_interval) if alert.email_sent < delta: - sleep(random.randint(1, 10)) - alert.assigned_check.send_email() + check_result = CheckResult.objects.get( + assigned_check=alert.assigned_check, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + check_result.send_email() alert.email_sent = djangotime.now() alert.save(update_fields=["email_sent"]) @@ -34,15 +45,19 @@ def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) @app.task -def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) +def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None) -> str: + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending text if not alert.sms_sent: - sleep(random.randint(1, 3)) - alert.assigned_check.send_sms() + check_result = CheckResult.objects.get( + assigned_check=alert.assigned_check, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + check_result.send_sms() alert.sms_sent = djangotime.now() alert.save(update_fields=["sms_sent"]) else: @@ -50,8 +65,11 @@ def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) - # send a text only if the last text sent is older than 24 hours delta = djangotime.now() - dt.timedelta(days=alert_interval) if alert.sms_sent < delta: - sleep(random.randint(1, 3)) - alert.assigned_check.send_sms() + check_result = CheckResult.objects.get( + assigned_check=alert.assigned_check, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + check_result.send_sms() alert.sms_sent = djangotime.now() alert.save(update_fields=["sms_sent"]) @@ -60,14 +78,18 @@ def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) - @app.task def handle_resolved_check_sms_alert_task(pk: int) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending text if not alert.resolved_sms_sent: - sleep(random.randint(1, 3)) - alert.assigned_check.send_resolved_sms() + check_result = CheckResult.objects.get( + assigned_check=alert.assigned_check, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + check_result.send_resolved_sms() alert.resolved_sms_sent = djangotime.now() alert.save(update_fields=["resolved_sms_sent"]) @@ -76,14 +98,18 @@ def handle_resolved_check_sms_alert_task(pk: int) -> str: @app.task def handle_resolved_check_email_alert_task(pk: int) -> str: - from alerts.models import Alert - - alert = Alert.objects.get(pk=pk) + try: + alert = Alert.objects.get(pk=pk) + except Alert.DoesNotExist: + return "alert not found" # first time sending email if not alert.resolved_email_sent: - sleep(random.randint(1, 10)) - alert.assigned_check.send_resolved_email() + check_result = CheckResult.objects.get( + assigned_check=alert.assigned_check, agent=alert.agent + ) + sleep(rand_range(100, 1500)) + check_result.send_resolved_email() alert.resolved_email_sent = djangotime.now() alert.save(update_fields=["resolved_email_sent"]) diff --git a/api/tacticalrmm/checks/tests.py b/api/tacticalrmm/checks/tests.py index c015c8509b..e4efea2857 100644 --- a/api/tacticalrmm/checks/tests.py +++ b/api/tacticalrmm/checks/tests.py @@ -1,9 +1,17 @@ from unittest.mock import patch +from django.conf import settings from django.utils import timezone as djangotime from model_bakery import baker -from checks.models import CheckHistory +from checks.models import CheckHistory, CheckResult +from tacticalrmm.constants import ( + AlertSeverity, + CheckStatus, + CheckType, + EvtLogFailWhen, + EvtLogTypes, +) from tacticalrmm.test import TacticalTestCase from .serializers import CheckSerializer @@ -24,16 +32,16 @@ def test_get_checks(self): resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 8) # type: ignore + self.assertEqual(len(resp.data), 8) # test checks agent url url = f"/agents/{agent.agent_id}/checks/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 4) # type: ignore + self.assertEqual(len(resp.data), 4) # test agent doesn't exist - url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/" + url = "/agents/jh3498uf8fkh4ro8hfd8df98/checks/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 404) @@ -65,7 +73,7 @@ def test_get_check(self): serializer = CheckSerializer(disk_check) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) def test_add_disk_check(self): @@ -77,7 +85,7 @@ def test_add_disk_check(self): agent_payload = { "agent": agent.agent_id, - "check_type": "diskspace", + "check_type": CheckType.DISK_SPACE, "disk": "C:", "error_threshold": 55, "warning_threshold": 0, @@ -86,15 +94,14 @@ def test_add_disk_check(self): policy_payload = { "policy": policy.id, - "check_type": "diskspace", + "check_type": CheckType.DISK_SPACE, "disk": "C:", "error_threshold": 55, "warning_threshold": 0, "fails_b4_alert": 3, } - for payload in [agent_payload, policy_payload]: - + for payload in (agent_payload, policy_payload): # add valid check resp = self.client.post(url, payload, format="json") self.assertEqual(resp.status_code, 200) @@ -126,7 +133,7 @@ def test_add_cpuload_check(self): agent_payload = { "agent": agent.agent_id, - "check_type": "cpuload", + "check_type": CheckType.CPU_LOAD, "error_threshold": 66, "warning_threshold": 0, "fails_b4_alert": 9, @@ -134,14 +141,13 @@ def test_add_cpuload_check(self): policy_payload = { "policy": policy.id, - "check_type": "cpuload", + "check_type": CheckType.CPU_LOAD, "error_threshold": 66, "warning_threshold": 0, "fails_b4_alert": 9, } - for payload in [agent_payload, policy_payload]: - + for payload in (agent_payload, policy_payload): # add cpu check resp = self.client.post(url, payload, format="json") self.assertEqual(resp.status_code, 200) @@ -166,6 +172,31 @@ def test_add_cpuload_check(self): self.check_not_authenticated("post", url) + def test_reset_all_checks_status(self): + # setup data + agent = baker.make_recipe("agents.agent") + check = baker.make_recipe("checks.diskspace_check", agent=agent) + baker.make("checks.CheckResult", assigned_check=check, agent=agent) + baker.make( + "checks.CheckHistory", + check_id=check.id, + agent_id=agent.agent_id, + _quantity=30, + ) + baker.make( + "checks.CheckHistory", + check_id=check.id, + agent_id=agent.agent_id, + _quantity=30, + ) + + url = f"{base_url}/{agent.agent_id}/resetall/" + + resp = self.client.post(url) + self.assertEqual(resp.status_code, 200) + + self.check_not_authenticated("post", url) + def test_add_memory_check(self): url = f"{base_url}/" agent = baker.make_recipe("agents.agent") @@ -173,7 +204,7 @@ def test_add_memory_check(self): agent_payload = { "agent": agent.agent_id, - "check_type": "memory", + "check_type": CheckType.MEMORY, "error_threshold": 78, "warning_threshold": 0, "fails_b4_alert": 1, @@ -181,14 +212,13 @@ def test_add_memory_check(self): policy_payload = { "policy": policy.id, - "check_type": "memory", + "check_type": CheckType.MEMORY, "error_threshold": 78, "warning_threshold": 0, "fails_b4_alert": 1, } - for payload in [agent_payload, policy_payload]: - + for payload in (agent_payload, policy_payload): # add memory check resp = self.client.post(url, payload, format="json") self.assertEqual(resp.status_code, 200) @@ -215,18 +245,12 @@ def test_add_memory_check(self): @patch("agents.models.Agent.nats_cmd") def test_run_checks(self, nats_cmd): - agent = baker.make_recipe("agents.agent", version="1.4.1") - agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0") - - url = f"{base_url}/{agent_b4_141.agent_id}/run/" - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - nats_cmd.assert_called_with({"func": "runchecks"}, wait=False) + agent = baker.make_recipe("agents.agent", version=settings.LATEST_AGENT_VER) nats_cmd.reset_mock() nats_cmd.return_value = "busy" url = f"{base_url}/{agent.agent_id}/run/" - r = self.client.get(url) + r = self.client.post(url) self.assertEqual(r.status_code, 400) nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}") @@ -234,54 +258,62 @@ def test_run_checks(self, nats_cmd): nats_cmd.reset_mock() nats_cmd.return_value = "ok" url = f"{base_url}/{agent.agent_id}/run/" - r = self.client.get(url) + r = self.client.post(url) self.assertEqual(r.status_code, 200) nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) - self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}") nats_cmd.reset_mock() nats_cmd.return_value = "timeout" url = f"{base_url}/{agent.agent_id}/run/" - r = self.client.get(url) + r = self.client.post(url) self.assertEqual(r.status_code, 400) nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15) self.assertEqual(r.json(), "Unable to contact the agent") - self.check_not_authenticated("get", url) + self.check_not_authenticated("post", url) def test_get_check_history(self): # setup data agent = baker.make_recipe("agents.agent") check = baker.make_recipe("checks.diskspace_check", agent=agent) - baker.make("checks.CheckHistory", check_id=check.id, _quantity=30) + check_result = baker.make( + "checks.CheckResult", assigned_check=check, agent=agent + ) + baker.make( + "checks.CheckHistory", + check_id=check.id, + agent_id=agent.agent_id, + _quantity=30, + ) check_history_data = baker.make( "checks.CheckHistory", check_id=check.id, + agent_id=agent.agent_id, _quantity=30, ) # need to manually set the date back 35 days - for check_history in check_history_data: # type: ignore - check_history.x = djangotime.now() - djangotime.timedelta(days=35) # type: ignore + for check_history in check_history_data: + check_history.x = djangotime.now() - djangotime.timedelta(days=35) check_history.save() # test invalid check pk - resp = self.client.patch("/checks/history/500/", format="json") + resp = self.client.patch("/checks/500/history/", format="json") self.assertEqual(resp.status_code, 404) - url = f"/checks/{check.id}/history/" + url = f"/checks/{check_result.id}/history/" # test with timeFilter last 30 days data = {"timeFilter": 30} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 30) # type: ignore + self.assertEqual(len(resp.data), 30) # test with timeFilter equal to 0 data = {"timeFilter": 0} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 60) # type: ignore + self.assertEqual(len(resp.data), 60) self.check_not_authenticated("patch", url) @@ -305,8 +337,8 @@ def test_prune_check_history(self): ) # need to manually set the date back 35 days - for check_history in check_history_data: # type: ignore - check_history.x = djangotime.now() - djangotime.timedelta(days=35) # type: ignore + for check_history in check_history_data: + check_history.x = djangotime.now() - djangotime.timedelta(days=35) check_history.save() # prune data 30 days old @@ -318,15 +350,14 @@ def test_prune_check_history(self): self.assertEqual(CheckHistory.objects.count(), 0) def test_handle_script_check(self): - from checks.models import Check - url = "/api/v3/checkrunner/" - script = baker.make_recipe("checks.script_check", agent=self.agent) + check = baker.make_recipe("checks.script_check", agent=self.agent) # test failing data = { - "id": script.id, + "id": check.id, + "agent_id": self.agent.agent_id, "retcode": 500, "stderr": "error", "stdout": "message", @@ -336,14 +367,15 @@ def test_handle_script_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=script.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test passing data = { - "id": script.id, + "id": check.id, + "agent_id": self.agent.agent_id, "retcode": 0, "stderr": "error", "stdout": "message", @@ -353,16 +385,17 @@ def test_handle_script_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=script.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "passing") + self.assertEqual(check_result.status, CheckStatus.PASSING) # test failing info - script.info_return_codes = [20, 30, 50] - script.save() + check.info_return_codes = [20, 30, 50] + check.save() data = { - "id": script.id, + "id": check.id, + "agent_id": self.agent.agent_id, "retcode": 30, "stderr": "error", "stdout": "message", @@ -372,17 +405,18 @@ def test_handle_script_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=script.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "info") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.INFO) # test failing warning - script.warning_return_codes = [80, 100, 1040] - script.save() + check.warning_return_codes = [80, 100, 1040] + check.save() data = { - "id": script.id, + "id": check.id, + "agent_id": self.agent.agent_id, "retcode": 1040, "stderr": "error", "stdout": "message", @@ -392,17 +426,15 @@ def test_handle_script_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=script.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) def test_handle_diskspace_check(self): - from checks.models import Check - url = "/api/v3/checkrunner/" - diskspace = baker.make_recipe( + check = baker.make_recipe( "checks.diskspace_check", warning_threshold=20, error_threshold=10, @@ -411,7 +443,8 @@ def test_handle_diskspace_check(self): # test warning threshold failure data = { - "id": diskspace.id, + "id": check.id, + "agent_id": self.agent.agent_id, "exists": True, "percent_used": 85, "total": 500, @@ -422,14 +455,15 @@ def test_handle_diskspace_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=diskspace.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) # test error failure data = { - "id": diskspace.id, + "id": check.id, + "agent_id": self.agent.agent_id, "exists": True, "percent_used": 95, "total": 500, @@ -440,27 +474,29 @@ def test_handle_diskspace_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=diskspace.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test disk not exist - data = {"id": diskspace.id, "exists": False} + data = {"id": check.id, "agent_id": self.agent.agent_id, "exists": False} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=diskspace.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test warning threshold 0 - diskspace.warning_threshold = 0 - diskspace.save() + check.warning_threshold = 0 + check.save() + data = { - "id": diskspace.id, + "id": check.id, + "agent_id": self.agent.agent_id, "exists": True, "percent_used": 95, "total": 500, @@ -471,16 +507,17 @@ def test_handle_diskspace_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=diskspace.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test error threshold 0 - diskspace.warning_threshold = 50 - diskspace.error_threshold = 0 - diskspace.save() + check.warning_threshold = 50 + check.error_threshold = 0 + check.save() data = { - "id": diskspace.id, + "id": check.id, + "agent_id": self.agent.agent_id, "exists": True, "percent_used": 95, "total": 500, @@ -491,13 +528,14 @@ def test_handle_diskspace_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=diskspace.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) # test passing data = { - "id": diskspace.id, + "id": check.id, + "agent_id": self.agent.agent_id, "exists": True, "percent_used": 50, "total": 500, @@ -508,16 +546,14 @@ def test_handle_diskspace_check(self): resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=diskspace.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEqual(new_check.status, "passing") + self.assertEqual(check_result.status, CheckStatus.PASSING) def test_handle_cpuload_check(self): - from checks.models import Check - url = "/api/v3/checkrunner/" - cpuload = baker.make_recipe( + check = baker.make_recipe( "checks.cpuload_check", warning_threshold=70, error_threshold=90, @@ -525,81 +561,79 @@ def test_handle_cpuload_check(self): ) # test failing warning - data = {"id": cpuload.id, "percent": 80} + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 80} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=cpuload.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) # test failing error - data = {"id": cpuload.id, "percent": 95} + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95} # reset check history - cpuload.history = [] - cpuload.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=cpuload.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test passing - data = {"id": cpuload.id, "percent": 50} + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 50} # reset check history - cpuload.history = [] - cpuload.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=cpuload.id) - self.assertEqual(new_check.status, "passing") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.PASSING) # test warning threshold 0 - cpuload.warning_threshold = 0 - cpuload.save() - data = {"id": cpuload.id, "percent": 95} + check.warning_threshold = 0 + check.save() + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95} # reset check history - cpuload.history = [] - cpuload.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=cpuload.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test error threshold 0 - cpuload.warning_threshold = 50 - cpuload.error_threshold = 0 - cpuload.save() - data = {"id": cpuload.id, "percent": 95} + check.warning_threshold = 50 + check.error_threshold = 0 + check.save() + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95} # reset check history - cpuload.history = [] - cpuload.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=cpuload.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) def test_handle_memory_check(self): - from checks.models import Check - url = "/api/v3/checkrunner/" - memory = baker.make_recipe( + check = baker.make_recipe( "checks.memory_check", warning_threshold=70, error_threshold=90, @@ -607,178 +641,193 @@ def test_handle_memory_check(self): ) # test failing warning - data = {"id": memory.id, "percent": 80} + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 80} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=memory.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) # test failing error - data = {"id": memory.id, "percent": 95} + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95} # reset check history - memory.history = [] - memory.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=memory.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test passing - data = {"id": memory.id, "percent": 50} + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 50} # reset check history - memory.history = [] - memory.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=memory.id) - self.assertEqual(new_check.status, "passing") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.PASSING) # test warning threshold 0 - memory.warning_threshold = 0 - memory.save() - data = {"id": memory.id, "percent": 95} + check.warning_threshold = 0 + check.save() + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95} # reset check history - memory.history = [] - memory.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=memory.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.ERROR) # test error threshold 0 - memory.warning_threshold = 50 - memory.error_threshold = 0 - memory.save() - data = {"id": memory.id, "percent": 95} + check.warning_threshold = 50 + check.error_threshold = 0 + check.save() + data = {"id": check.id, "agent_id": self.agent.agent_id, "percent": 95} # reset check history - memory.history = [] - memory.save() + check_result.history = [] + check_result.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=memory.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check_result.alert_severity, AlertSeverity.WARNING) def test_handle_ping_check(self): - from checks.models import Check - url = "/api/v3/checkrunner/" - ping = baker.make_recipe( - "checks.ping_check", agent=self.agent, alert_severity="info" + check = baker.make_recipe( + "checks.ping_check", agent=self.agent, alert_severity=AlertSeverity.INFO ) # test failing info - data = {"id": ping.id, "status": "failing", "output": "reply from a.com"} + data = { + "id": check.id, + "agent_id": self.agent.agent_id, + "status": CheckStatus.FAILING, + "output": "reply from a.com", + } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=ping.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "info") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check.alert_severity, AlertSeverity.INFO) # test failing warning - ping.alert_severity = "warning" - ping.save() + check.alert_severity = AlertSeverity.WARNING + check.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=ping.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "warning") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check.alert_severity, AlertSeverity.WARNING) # test failing error - ping.alert_severity = "error" - ping.save() + check.alert_severity = AlertSeverity.ERROR + check.save() resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=ping.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check.alert_severity, AlertSeverity.ERROR) # test failing error resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=ping.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "error") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check.alert_severity, AlertSeverity.ERROR) # test passing - data = {"id": ping.id, "status": "passing", "output": "reply from a.com"} + data = { + "id": check.id, + "agent_id": self.agent.agent_id, + "status": CheckStatus.PASSING, + "output": "reply from a.com", + } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=ping.id) - self.assertEqual(new_check.status, "passing") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.PASSING) @patch("agents.models.Agent.nats_cmd") def test_handle_winsvc_check(self, nats_cmd): - from checks.models import Check - url = "/api/v3/checkrunner/" - winsvc = baker.make_recipe( - "checks.winsvc_check", agent=self.agent, alert_severity="info" + check = baker.make_recipe( + "checks.winsvc_check", agent=self.agent, alert_severity=AlertSeverity.INFO ) # test passing running - data = {"id": winsvc.id, "status": "passing", "more_info": "ok"} + data = { + "id": check.id, + "agent_id": self.agent.agent_id, + "status": CheckStatus.PASSING, + "more_info": "ok", + } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "passing") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.PASSING) # test failing - data = {"id": winsvc.id, "status": "failing", "more_info": "ok"} + data = { + "id": check.id, + "agent_id": self.agent.agent_id, + "status": CheckStatus.FAILING, + "more_info": "ok", + } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=winsvc.id) - self.assertEqual(new_check.status, "failing") - self.assertEqual(new_check.alert_severity, "info") + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check.alert_severity, AlertSeverity.INFO) def test_handle_eventlog_check(self): - from checks.models import Check - url = "/api/v3/checkrunner/" - eventlog = baker.make_recipe( + 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", + alert_severity=AlertSeverity.WARNING, agent=self.agent, ) data = { - "id": eventlog.id, + "id": check.id, + "agent_id": self.agent.agent_id, "log": [ { "eventType": "warning", @@ -807,62 +856,62 @@ def test_handle_eventlog_check(self): ], } - no_logs_data = {"id": eventlog.id, "log": []} + no_logs_data = {"id": check.id, "agent_id": self.agent.agent_id, "log": []} # test failing when contains resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=eventlog.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEquals(new_check.alert_severity, "warning") - self.assertEquals(new_check.status, "failing") + self.assertEqual(check.alert_severity, AlertSeverity.WARNING) + self.assertEqual(check_result.status, CheckStatus.FAILING) # test passing when contains resp = self.client.patch(url, no_logs_data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=eventlog.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEquals(new_check.status, "passing") + self.assertEqual(check_result.status, CheckStatus.PASSING) # test failing when not contains and message and source - eventlog.fail_when = "not_contains" - eventlog.alert_severity = "error" - eventlog.save() + check.fail_when = EvtLogFailWhen.NOT_CONTAINS + check.alert_severity = AlertSeverity.ERROR + check.save() resp = self.client.patch(url, no_logs_data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=eventlog.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEquals(new_check.status, "failing") - self.assertEquals(new_check.alert_severity, "error") + self.assertEqual(check_result.status, CheckStatus.FAILING) + self.assertEqual(check.alert_severity, AlertSeverity.ERROR) # test passing when contains with source and message resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - new_check = Check.objects.get(pk=eventlog.id) + check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) - self.assertEquals(new_check.status, "passing") + self.assertEqual(check_result.status, CheckStatus.PASSING) class TestCheckPermissions(TacticalTestCase): def setUp(self): self.setup_coresettings() - self.client_setup() + self.setup_client() def test_get_checks_permissions(self): agent = baker.make_recipe("agents.agent") policy = baker.make("automation.Policy") unauthorized_agent = baker.make_recipe("agents.agent") - check = baker.make("checks.Check", agent=agent, _quantity=5) - unauthorized_check = baker.make( + check = baker.make("checks.Check", agent=agent, _quantity=5) # noqa + unauthorized_check = baker.make( # noqa "checks.Check", agent=unauthorized_agent, _quantity=7 ) - policy_checks = baker.make("checks.Check", policy=policy, _quantity=2) + policy_checks = baker.make("checks.Check", policy=policy, _quantity=2) # noqa # test super user access self.check_authorized_superuser("get", f"{base_url}/") @@ -875,7 +924,7 @@ def test_get_checks_permissions(self): ) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) self.check_not_authorized("get", f"{base_url}/") self.check_not_authorized("get", f"/agents/{agent.agent_id}/checks/") @@ -889,15 +938,15 @@ def test_get_checks_permissions(self): user.role.save() r = self.check_authorized("get", f"{base_url}/") - self.assertEqual(len(r.data), 14) # type: ignore + self.assertEqual(len(r.data), 14) r = self.check_authorized("get", f"/agents/{agent.agent_id}/checks/") - self.assertEqual(len(r.data), 5) # type: ignore + self.assertEqual(len(r.data), 5) r = self.check_authorized( "get", f"/agents/{unauthorized_agent.agent_id}/checks/" ) - self.assertEqual(len(r.data), 7) # type: ignore + self.assertEqual(len(r.data), 7) r = self.check_authorized("get", f"/automation/policies/{policy.id}/checks/") - self.assertEqual(len(r.data), 2) # type: ignore + self.assertEqual(len(r.data), 2) # test limiting to client user.role.can_view_clients.set([agent.client]) @@ -909,7 +958,7 @@ def test_get_checks_permissions(self): # make sure queryset is limited too r = self.client.get(f"{base_url}/") - self.assertEqual(len(r.data), 7) # type: ignore + self.assertEqual(len(r.data), 7) def test_add_check_permissions(self): agent = baker.make_recipe("agents.agent") @@ -918,7 +967,7 @@ def test_add_check_permissions(self): policy_data = { "policy": policy.id, - "check_type": "diskspace", + "check_type": CheckType.DISK_SPACE, "disk": "C:", "error_threshold": 55, "warning_threshold": 0, @@ -927,7 +976,7 @@ def test_add_check_permissions(self): agent_data = { "agent": agent.agent_id, - "check_type": "diskspace", + "check_type": CheckType.DISK_SPACE, "disk": "C:", "error_threshold": 55, "warning_threshold": 0, @@ -936,7 +985,7 @@ def test_add_check_permissions(self): unauthorized_agent_data = { "agent": unauthorized_agent.agent_id, - "check_type": "diskspace", + "check_type": CheckType.DISK_SPACE, "disk": "C:", "error_threshold": 55, "warning_threshold": 0, @@ -945,12 +994,12 @@ def test_add_check_permissions(self): url = f"{base_url}/" - for data in [policy_data, agent_data]: + for data in (policy_data, agent_data): # test superuser access self.check_authorized_superuser("post", url, data) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("post", url, data) @@ -979,8 +1028,7 @@ def test_check_get_edit_delete_permissions(self, delete_check): unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent) policy_check = baker.make("checks.Check", policy=policy) - for method in ["get", "put", "delete"]: - + for method in ("get", "put", "delete"): url = f"{base_url}/{check.id}/" unauthorized_url = f"{base_url}/{unauthorized_check.id}/" policy_url = f"{base_url}/{policy_check.id}/" @@ -991,7 +1039,7 @@ def test_check_get_edit_delete_permissions(self, delete_check): self.check_authorized_superuser(method, policy_url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized(method, url) @@ -1017,17 +1065,27 @@ def test_check_get_edit_delete_permissions(self, delete_check): self.check_not_authorized(method, unauthorized_url) self.check_authorized(method, policy_url) - def test_check_action_permissions(self): - + @patch("agents.models.Agent.nats_cmd") + def test_check_action_permissions(self, nats_cmd): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") check = baker.make("checks.Check", agent=agent) + check_result = baker.make( + "checks.CheckResult", agent=agent, assigned_check=check + ) unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent) + unauthorized_check_result = baker.make( + "checks.CheckResult", + agent=unauthorized_agent, + assigned_check=unauthorized_check, + ) - for action in ["reset", "run"]: + for action in ("reset", "run"): if action == "reset": - url = f"{base_url}/{check.id}/{action}/" - unauthorized_url = f"{base_url}/{unauthorized_check.id}/{action}/" + url = f"{base_url}/{check_result.id}/{action}/" + unauthorized_url = ( + f"{base_url}/{unauthorized_check_result.id}/{action}/" + ) else: url = f"{base_url}/{agent.agent_id}/{action}/" unauthorized_url = f"{base_url}/{unauthorized_agent.agent_id}/{action}/" @@ -1037,7 +1095,7 @@ def test_check_action_permissions(self): self.check_authorized_superuser("post", unauthorized_url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("post", url) @@ -1064,17 +1122,25 @@ def test_check_history_permissions(self): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") check = baker.make("checks.Check", agent=agent) + check_result = baker.make( + "checks.CheckResult", agent=agent, assigned_check=check + ) unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent) + unauthorized_check_result = baker.make( + "checks.CheckResult", + agent=unauthorized_agent, + assigned_check=unauthorized_check, + ) - url = f"{base_url}/{check.id}/history/" - unauthorized_url = f"{base_url}/{unauthorized_check.id}/history/" + url = f"{base_url}/{check_result.id}/history/" + unauthorized_url = f"{base_url}/{unauthorized_check_result.id}/history/" # test superuser access self.check_authorized_superuser("patch", url) self.check_authorized_superuser("patch", unauthorized_url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("patch", url) @@ -1096,12 +1162,3 @@ def test_check_history_permissions(self): self.check_authorized("patch", url) self.check_not_authorized("patch", unauthorized_url) - - def test_policy_fields_to_copy_exists(self): - from .models import Check - - fields = [i.name for i in Check._meta.get_fields()] - check = baker.make("checks.Check") - - for i in check.policy_fields_to_copy: # type: ignore - self.assertIn(i, fields) diff --git a/api/tacticalrmm/checks/urls.py b/api/tacticalrmm/checks/urls.py index 7765290632..e6734210e0 100644 --- a/api/tacticalrmm/checks/urls.py +++ b/api/tacticalrmm/checks/urls.py @@ -6,6 +6,8 @@ path("", views.GetAddChecks.as_view()), path("/", views.GetUpdateDeleteCheck.as_view()), path("/reset/", views.ResetCheck.as_view()), + path("/resetall/", views.ResetAllChecksStatus.as_view()), path("/run/", views.run_checks), path("/history/", views.GetCheckHistory.as_view()), + path("//csbulkrun/", views.bulk_run_checks), ] diff --git a/api/tacticalrmm/checks/utils.py b/api/tacticalrmm/checks/utils.py index e0b0d6abdb..baa60d6420 100644 --- a/api/tacticalrmm/checks/utils.py +++ b/api/tacticalrmm/checks/utils.py @@ -1,4 +1,4 @@ -def bytes2human(n): +def bytes2human(n: int) -> str: # http://code.activestate.com/recipes/578019 symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") prefix = {} diff --git a/api/tacticalrmm/checks/views.py b/api/tacticalrmm/checks/views.py index 7248b5e61e..994d7711fa 100644 --- a/api/tacticalrmm/checks/views.py +++ b/api/tacticalrmm/checks/views.py @@ -1,23 +1,26 @@ import asyncio from datetime import datetime as dt -from django.db.models import Q +from django.db.models import Prefetch, Q from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime -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 rest_framework.exceptions import PermissionDenied from agents.models import Agent +from alerts.models import Alert from automation.models import Policy -from tacticalrmm.utils import notify_error +from tacticalrmm.constants import AGENT_DEFER, CheckStatus, CheckType +from tacticalrmm.exceptions import NatsDown +from tacticalrmm.helpers import notify_error +from tacticalrmm.nats_utils import abulk_nats_command from tacticalrmm.permissions import _has_perm_on_agent -from .models import Check, CheckHistory -from .permissions import ChecksPerms, RunChecksPerms +from .models import Check, CheckHistory, CheckResult +from .permissions import BulkRunChecksPerms, ChecksPerms, RunChecksPerms from .serializers import CheckHistorySerializer, CheckSerializer @@ -27,17 +30,15 @@ class GetAddChecks(APIView): def get(self, request, agent_id=None, policy=None): if agent_id: agent = get_object_or_404(Agent, agent_id=agent_id) - checks = Check.objects.filter(agent=agent) + checks = agent.get_checks_with_policies() elif policy: policy = get_object_or_404(Policy, id=policy) checks = Check.objects.filter(policy=policy) else: - checks = Check.objects.filter_by_role(request.user) + checks = Check.objects.filter_by_role(request.user) # type: ignore return Response(CheckSerializer(checks, many=True).data) def post(self, request): - from automation.tasks import generate_agent_checks_task - data = request.data.copy() # Determine if adding check to Agent and replace agent_id with pk if "agent" in data.keys(): @@ -49,34 +50,13 @@ def post(self, request): # 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) serializer.is_valid(raise_exception=True) new_check = serializer.save() - # Generate policy Checks - if "policy" in data.keys(): - generate_agent_checks_task.delay(policy=data["policy"]) - elif "agent" in data.keys(): - checks = agent.agentchecks.filter( # type: ignore - check_type=new_check.check_type, managed_by_policy=True - ) - - # Should only be one - duplicate_check = [ - check for check in checks if check.is_duplicate(new_check) - ] - - if duplicate_check: - policy = Check.objects.get(pk=duplicate_check[0].parent_check).policy - if policy.enforced: - new_check.overriden_by_policy = True - new_check.save() - else: - duplicate_check[0].delete() - return Response(f"{new_check.readable_desc} was added!") @@ -91,8 +71,6 @@ def get(self, request, pk): return Response(CheckSerializer(check).data) def put(self, request, pk): - from automation.tasks import update_policy_check_fields_task - check = get_object_or_404(Check, pk=pk) data = request.data.copy() @@ -105,7 +83,7 @@ def put(self, request, pk): # 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: @@ -118,14 +96,9 @@ def put(self, request, pk): serializer.is_valid(raise_exception=True) check = serializer.save() - if check.policy: - update_policy_check_fields_task.delay(check=check.pk) - return Response(f"{check.readable_desc} was edited!") def delete(self, request, pk): - from automation.tasks import generate_agent_checks_task - check = get_object_or_404(Check, pk=pk) if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): @@ -133,18 +106,6 @@ def delete(self, request, pk): check.delete() - # Policy check deleted - if check.policy: - Check.objects.filter(managed_by_policy=True, parent_check=pk).delete() - - # Re-evaluate agent checks is policy was enforced - if check.policy.enforced: - generate_agent_checks_task.delay(policy=check.policy.pk) - - # Agent check deleted - elif check.agent: - generate_agent_checks_task.delay(agents=[check.agent.pk]) - return Response(f"{check.readable_desc} was deleted!") @@ -152,28 +113,70 @@ class ResetCheck(APIView): permission_classes = [IsAuthenticated, ChecksPerms] def post(self, request, pk): - check = get_object_or_404(Check, pk=pk) + result = get_object_or_404(CheckResult, pk=pk) - if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + if result.agent and not _has_perm_on_agent(request.user, result.agent.agent_id): raise PermissionDenied() - check.status = "passing" - check.save() + result.status = CheckStatus.PASSING + result.save() # resolve any alerts that are open - if check.alert.filter(resolved=False).exists(): - check.alert.get(resolved=False).resolve() + if alert := Alert.create_or_return_check_alert( + result.assigned_check, agent=result.agent, skip_create=True + ): + alert.resolve() return Response("The check status was reset") +class ResetAllChecksStatus(APIView): + permission_classes = [IsAuthenticated, ChecksPerms] + + def post(self, request, agent_id): + agent = get_object_or_404( + Agent.objects.defer(*AGENT_DEFER) + .select_related( + "policy", + "policy__alert_template", + "alert_template", + ) + .prefetch_related( + Prefetch( + "checkresults", + queryset=CheckResult.objects.select_related("assigned_check"), + ), + "agentchecks", + ), + agent_id=agent_id, + ) + + if not _has_perm_on_agent(request.user, agent.agent_id): + raise PermissionDenied() + + for check in agent.get_checks_with_policies(): + try: + result = check.check_result + result.status = CheckStatus.PASSING + result.save() + if alert := Alert.create_or_return_check_alert( + result.assigned_check, agent=agent, skip_create=True + ): + alert.resolve() + except: + # check hasn't run yet, no check result entry + continue + + return Response("All checks status were reset") + + class GetCheckHistory(APIView): permission_classes = [IsAuthenticated, ChecksPerms] def patch(self, request, pk): - check = get_object_or_404(Check, pk=pk) + result = get_object_or_404(CheckResult, pk=pk) - if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id): + if result.agent and not _has_perm_on_agent(request.user, result.agent.agent_id): raise PermissionDenied() timeFilter = Q() @@ -186,28 +189,57 @@ def patch(self, request, pk): - djangotime.timedelta(days=request.data["timeFilter"]), ) - check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x") # type: ignore - - return Response( - CheckHistorySerializer( - check_history, context={"timezone": check.agent.timezone}, many=True - ).data + check_history = ( + CheckHistory.objects.filter( + check_id=result.assigned_check.id, agent_id=result.agent.agent_id + ) + .filter(timeFilter) + .order_by("-x") ) + return Response(CheckHistorySerializer(check_history, many=True).data) -@api_view() + +@api_view(["POST"]) @permission_classes([IsAuthenticated, RunChecksPerms]) def run_checks(request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - if pyver.parse(agent.version) >= pyver.parse("1.4.1"): - r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15)) - if r == "busy": - return notify_error(f"Checks are already running on {agent.hostname}") - elif r == "ok": - return Response(f"Checks will now be re-run on {agent.hostname}") - else: - return notify_error("Unable to contact the agent") - else: - asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) - return Response(f"Checks will now be re-run on {agent.hostname}") + r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15)) + if r == "busy": + return notify_error(f"Checks are already running on {agent.hostname}") + elif r == "ok": + return Response(f"Checks will now be run on {agent.hostname}") + + return notify_error("Unable to contact the agent") + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated, BulkRunChecksPerms]) +def bulk_run_checks(request, target, pk): + q = Q() + match target: + case "client": + q = Q(site__client__id=pk) + case "site": + q = Q(site__id=pk) + + agent_ids = list( + Agent.objects.only("agent_id", "site") + .filter(q) + .values_list("agent_id", flat=True) + ) + + if not agent_ids: + return notify_error("No agents matched query") + + payload = {"func": "runchecks"} + items = [(agent_id, payload) for agent_id in agent_ids] + + try: + asyncio.run(abulk_nats_command(items=items)) + except NatsDown as e: + return notify_error(str(e)) + + ret = f"Checks will now be run on {len(agent_ids)} agents" + return Response(ret) diff --git a/api/tacticalrmm/clients/migrations/0020_auto_20211226_0547.py b/api/tacticalrmm/clients/migrations/0020_auto_20211226_0547.py new file mode 100644 index 0000000000..15371a55ba --- /dev/null +++ b/api/tacticalrmm/clients/migrations/0020_auto_20211226_0547.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.10 on 2021-12-26 05:47 + +from django.db import migrations, models + +import clients.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clients', '0019_remove_deployment_client'), + ] + + operations = [ + migrations.AddField( + model_name='client', + name='agent_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='client', + name='failing_checks', + field=models.JSONField(default=clients.models._default_failing_checks_data), + ), + migrations.AddField( + model_name='site', + name='agent_count', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='site', + name='failing_checks', + field=models.JSONField(default=clients.models._default_failing_checks_data), + ), + ] diff --git a/api/tacticalrmm/clients/migrations/0021_remove_client_agent_count_remove_site_agent_count.py b/api/tacticalrmm/clients/migrations/0021_remove_client_agent_count_remove_site_agent_count.py new file mode 100644 index 0000000000..65041af12c --- /dev/null +++ b/api/tacticalrmm/clients/migrations/0021_remove_client_agent_count_remove_site_agent_count.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.3 on 2022-04-15 01:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('clients', '0020_auto_20211226_0547'), + ] + + operations = [ + migrations.RemoveField( + model_name='client', + name='agent_count', + ), + migrations.RemoveField( + model_name='site', + name='agent_count', + ), + ] diff --git a/api/tacticalrmm/clients/migrations/0022_change_arch_to_goarch.py b/api/tacticalrmm/clients/migrations/0022_change_arch_to_goarch.py new file mode 100644 index 0000000000..70c1cabdce --- /dev/null +++ b/api/tacticalrmm/clients/migrations/0022_change_arch_to_goarch.py @@ -0,0 +1,25 @@ +from django.db import migrations + +from tacticalrmm.constants import GoArch + + +def change_arch(apps, schema_editor): + Deployment = apps.get_model("clients", "Deployment") + for d in Deployment.objects.all(): + if d.arch == "64": + d.arch = GoArch.AMD64 + else: + d.arch = GoArch.i386 + + d.save(update_fields=["arch"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("clients", "0021_remove_client_agent_count_remove_site_agent_count"), + ] + + operations = [ + migrations.RunPython(change_arch), + ] diff --git a/api/tacticalrmm/clients/migrations/0023_alter_deployment_arch_deployment_goarch.py b/api/tacticalrmm/clients/migrations/0023_alter_deployment_arch_deployment_goarch.py new file mode 100644 index 0000000000..f8b9beb411 --- /dev/null +++ b/api/tacticalrmm/clients/migrations/0023_alter_deployment_arch_deployment_goarch.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.5 on 2022-06-19 07:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("clients", "0022_change_arch_to_goarch"), + ] + + operations = [ + migrations.RenameField( + model_name="deployment", + old_name="arch", + new_name="goarch", + ), + migrations.AlterField( + model_name="deployment", + name="goarch", + field=models.CharField( + blank=True, + choices=[ + ("amd64", "amd64"), + ("386", "386"), + ("arm64", "arm64"), + ("arm", "arm"), + ], + default="amd64", + max_length=255, + ), + ), + ] diff --git a/api/tacticalrmm/clients/migrations/0024_alter_deployment_goarch.py b/api/tacticalrmm/clients/migrations/0024_alter_deployment_goarch.py new file mode 100644 index 0000000000..52c2c5c636 --- /dev/null +++ b/api/tacticalrmm/clients/migrations/0024_alter_deployment_goarch.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2022-06-23 05:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clients', '0023_alter_deployment_arch_deployment_goarch'), + ] + + operations = [ + migrations.AlterField( + model_name='deployment', + name='goarch', + field=models.CharField(choices=[('amd64', 'amd64'), ('386', '386'), ('arm64', 'arm64'), ('arm', 'arm')], default='amd64', max_length=255), + ), + ] diff --git a/api/tacticalrmm/clients/models.py b/api/tacticalrmm/clients/models.py index 17c0b7b707..2f04040cc1 100644 --- a/api/tacticalrmm/clients/models.py +++ b/api/tacticalrmm/clients/models.py @@ -1,12 +1,18 @@ 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 logs.models import BaseAuditModel +from tacticalrmm.constants import AGENT_DEFER, AgentMonType, CustomFieldType, GoArch from tacticalrmm.models import PermissionQuerySet -from tacticalrmm.utils import AGENT_DEFER + + +def _default_failing_checks_data() -> Dict[str, bool]: + return {"error": False, "warning": False} class Client(BaseAuditModel): @@ -14,6 +20,7 @@ class Client(BaseAuditModel): name = models.CharField(max_length=255, unique=True) block_policy_inheritance = models.BooleanField(default=False) + failing_checks = models.JSONField(default=_default_failing_checks_data) workstation_policy = models.ForeignKey( "automation.Policy", related_name="workstation_clients", @@ -39,7 +46,6 @@ class Client(BaseAuditModel): def save(self, *args, **kwargs): from alerts.tasks import cache_agents_alert_template - from automation.tasks import generate_agent_checks_task # get old client if exists old_client = Client.objects.get(pk=self.pk) if self.pk else None @@ -50,21 +56,25 @@ def save(self, *args, **kwargs): ) # check if polcies have changed and initiate task to reapply policies if so - if old_client: - if ( - (old_client.server_policy != self.server_policy) - or (old_client.workstation_policy != self.workstation_policy) - or ( - old_client.block_policy_inheritance != self.block_policy_inheritance - ) - ): - generate_agent_checks_task.delay( - client=self.pk, - create_tasks=True, - ) - - if old_client.alert_template != self.alert_template: - cache_agents_alert_template.delay() + if old_client and ( + old_client.alert_template != self.alert_template + or old_client.workstation_policy != self.workstation_policy + or old_client.server_policy != self.server_policy + ): + cache_agents_alert_template.delay() + + if old_client and ( + old_client.workstation_policy != self.workstation_policy + or old_client.server_policy != self.server_policy + ): + sites = self.sites.all() + if old_client.workstation_policy != self.workstation_policy: + for site in sites: + cache.delete_many_pattern(f"site_workstation_*{site.pk}_*") + + if old_client.server_policy != self.server_policy: + for site in sites: + cache.delete_many_pattern(f"site_server_*{site.pk}_*") class Meta: ordering = ("name",) @@ -73,49 +83,9 @@ def __str__(self): return self.name @property - def agent_count(self) -> int: + def live_agent_count(self) -> int: return Agent.objects.defer(*AGENT_DEFER).filter(site__client=self).count() - @property - def has_maintenanace_mode_agents(self): - return ( - Agent.objects.defer(*AGENT_DEFER) - .filter(site__client=self, maintenance_mode=True) - .count() - > 0 - ) - - @property - def has_failing_checks(self): - agents = Agent.objects.defer(*AGENT_DEFER).filter(site__client=self) - data = {"error": False, "warning": False} - - for agent in agents: - if agent.maintenance_mode: - break - - if agent.overdue_email_alert or agent.overdue_text_alert: - if agent.status == "overdue": - data["error"] = True - break - - if agent.checks["has_failing_checks"]: - - if agent.checks["warning"]: - data["warning"] = True - - if agent.checks["failing"]: - data["error"] = True - break - - if agent.autotasks.exists(): # type: ignore - for i in agent.autotasks.all(): # type: ignore - if i.status == "failing" and i.alert_severity == "error": - data["error"] = True - break - - return data - @staticmethod def serialize(client): from .serializers import ClientAuditSerializer @@ -130,6 +100,7 @@ class Site(BaseAuditModel): client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE) name = models.CharField(max_length=255) block_policy_inheritance = models.BooleanField(default=False) + failing_checks = models.JSONField(default=_default_failing_checks_data) workstation_policy = models.ForeignKey( "automation.Policy", related_name="workstation_sites", @@ -155,7 +126,6 @@ class Site(BaseAuditModel): def save(self, *args, **kwargs): from alerts.tasks import cache_agents_alert_template - from automation.tasks import generate_agent_checks_task # get old client if exists old_site = Site.objects.get(pk=self.pk) if self.pk else None @@ -168,15 +138,18 @@ def save(self, *args, **kwargs): # check if polcies have changed and initiate task to reapply policies if so if old_site: if ( - (old_site.server_policy != self.server_policy) - or (old_site.workstation_policy != self.workstation_policy) - or (old_site.block_policy_inheritance != self.block_policy_inheritance) + old_site.alert_template != self.alert_template + or old_site.workstation_policy != self.workstation_policy + or old_site.server_policy != self.server_policy ): - generate_agent_checks_task.delay(site=self.pk, create_tasks=True) - - if old_site.alert_template != self.alert_template: cache_agents_alert_template.delay() + if old_site.workstation_policy != self.workstation_policy: + cache.delete_many_pattern(f"site_workstation_*{self.pk}_*") + + if old_site.server_policy != self.server_policy: + cache.delete_many_pattern(f"site_server_*{self.pk}_*") + class Meta: ordering = ("name",) unique_together = (("client", "name"),) @@ -185,52 +158,8 @@ def __str__(self): return self.name @property - def agent_count(self) -> int: - return Agent.objects.defer(*AGENT_DEFER).filter(site=self).count() - - @property - def has_maintenanace_mode_agents(self): - return ( - Agent.objects.defer(*AGENT_DEFER) - .filter(site=self, maintenance_mode=True) - .count() - > 0 - ) - - @property - def has_failing_checks(self): - agents = ( - Agent.objects.defer(*AGENT_DEFER) - .filter(site=self) - .prefetch_related("agentchecks", "autotasks") - ) - - data = {"error": False, "warning": False} - - for agent in agents: - if agent.maintenance_mode: - break - - if agent.overdue_email_alert or agent.overdue_text_alert: - if agent.status == "overdue": - data["error"] = True - break - - if agent.checks["has_failing_checks"]: - if agent.checks["warning"]: - data["warning"] = True - - if agent.checks["failing"]: - data["error"] = True - break - - if agent.autotasks.exists(): # type: ignore - for i in agent.autotasks.all(): # type: ignore - if i.status == "failing" and i.alert_severity == "error": - data["error"] = True - break - - return data + def live_agent_count(self) -> int: + return self.agents.defer(*AGENT_DEFER).count() # type: ignore @staticmethod def serialize(site): @@ -240,17 +169,6 @@ def serialize(site): return SiteAuditSerializer(site).data -MON_TYPE_CHOICES = [ - ("server", "Server"), - ("workstation", "Workstation"), -] - -ARCH_CHOICES = [ - ("64", "64 bit"), - ("32", "32 bit"), -] - - class Deployment(models.Model): objects = PermissionQuerySet.as_manager() @@ -259,9 +177,11 @@ class Deployment(models.Model): "clients.Site", related_name="deploysites", on_delete=models.CASCADE ) mon_type = models.CharField( - max_length=255, choices=MON_TYPE_CHOICES, default="server" + max_length=255, choices=AgentMonType.choices, default=AgentMonType.SERVER + ) + goarch = models.CharField( + max_length=255, choices=GoArch.choices, default=GoArch.AMD64 ) - arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64") expiry = models.DateTimeField(null=True, blank=True) created = models.DateTimeField(auto_now_add=True, null=True, blank=True) auth_token = models.ForeignKey( @@ -305,26 +225,26 @@ def __str__(self): @property def value(self): - if self.field.type == "multiple": + if self.field.type == CustomFieldType.MULTIPLE: return self.multiple_value - elif self.field.type == "checkbox": + elif self.field.type == CustomFieldType.CHECKBOX: return self.bool_value - else: - return self.string_value + + return self.string_value def save_to_field(self, value): - if self.field.type in [ - "text", - "number", - "single", - "datetime", - ]: + if self.field.type in ( + CustomFieldType.TEXT, + CustomFieldType.NUMBER, + CustomFieldType.SINGLE, + CustomFieldType.DATETIME, + ): self.string_value = value self.save() - elif type == "multiple": + elif self.field.type == CustomFieldType.MULTIPLE: self.multiple_value = value.split(",") self.save() - elif type == "checkbox": + elif self.field.type == CustomFieldType.CHECKBOX: self.bool_value = bool(value) self.save() @@ -356,25 +276,25 @@ def __str__(self): @property def value(self): - if self.field.type == "multiple": + if self.field.type == CustomFieldType.MULTIPLE: return self.multiple_value - elif self.field.type == "checkbox": + elif self.field.type == CustomFieldType.CHECKBOX: return self.bool_value - else: - return self.string_value + + return self.string_value def save_to_field(self, value): - if self.field.type in [ - "text", - "number", - "single", - "datetime", - ]: + if self.field.type in ( + CustomFieldType.TEXT, + CustomFieldType.NUMBER, + CustomFieldType.SINGLE, + CustomFieldType.DATETIME, + ): self.string_value = value self.save() - elif type == "multiple": + elif self.field.type == CustomFieldType.MULTIPLE: self.multiple_value = value.split(",") self.save() - elif type == "checkbox": + elif self.field.type == CustomFieldType.CHECKBOX: self.bool_value = bool(value) self.save() diff --git a/api/tacticalrmm/clients/permissions.py b/api/tacticalrmm/clients/permissions.py index 95e1a67f4e..a483719a45 100644 --- a/api/tacticalrmm/clients/permissions.py +++ b/api/tacticalrmm/clients/permissions.py @@ -4,7 +4,7 @@ class ClientsPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": if "pk" in view.kwargs.keys(): return _has_perm(r, "can_list_clients") and _has_perm_on_client( @@ -12,7 +12,7 @@ def has_permission(self, r, view): ) else: return _has_perm(r, "can_list_clients") - elif r.method == "PUT" or r.method == "DELETE": + elif r.method in ("PUT", "DELETE"): return _has_perm(r, "can_manage_clients") and _has_perm_on_client( r.user, view.kwargs["pk"] ) @@ -21,7 +21,7 @@ def has_permission(self, r, view): class SitesPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": if "pk" in view.kwargs.keys(): return _has_perm(r, "can_list_sites") and _has_perm_on_site( @@ -29,7 +29,7 @@ def has_permission(self, r, view): ) else: return _has_perm(r, "can_list_sites") - elif r.method == "PUT" or r.method == "DELETE": + elif r.method in ("PUT", "DELETE"): return _has_perm(r, "can_manage_sites") and _has_perm_on_site( r.user, view.kwargs["pk"] ) @@ -38,8 +38,8 @@ def has_permission(self, r, view): class DeploymentPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_deployments") - else: - return _has_perm(r, "can_manage_deployments") + + return _has_perm(r, "can_manage_deployments") diff --git a/api/tacticalrmm/clients/serializers.py b/api/tacticalrmm/clients/serializers.py index 8336157785..e92b478ae9 100644 --- a/api/tacticalrmm/clients/serializers.py +++ b/api/tacticalrmm/clients/serializers.py @@ -1,8 +1,8 @@ from rest_framework.serializers import ( ModelSerializer, ReadOnlyField, - ValidationError, SerializerMethodField, + ValidationError, ) from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField @@ -30,9 +30,8 @@ class Meta: class SiteSerializer(ModelSerializer): client_name = ReadOnlyField(source="client.name") custom_fields = SiteCustomFieldSerializer(many=True, read_only=True) + maintenance_mode = ReadOnlyField() agent_count = ReadOnlyField() - maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents") - failing_checks = ReadOnlyField(source="has_failing_checks") class Meta: model = Site @@ -94,13 +93,12 @@ class Meta: class ClientSerializer(ModelSerializer): sites = SerializerMethodField() custom_fields = ClientCustomFieldSerializer(many=True, read_only=True) + maintenance_mode = ReadOnlyField() agent_count = ReadOnlyField() - maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents") - failing_checks = ReadOnlyField(source="has_failing_checks") def get_sites(self, obj): return SiteSerializer( - obj.sites.select_related("client").filter_by_role(self.context["user"]), + obj.filtered_sites, many=True, ).data @@ -143,7 +141,7 @@ class Meta: "client_name", "site_name", "mon_type", - "arch", + "goarch", "expiry", "install_flags", "created", diff --git a/api/tacticalrmm/clients/tests.py b/api/tacticalrmm/clients/tests.py index cb8534c7b2..cb538f4042 100644 --- a/api/tacticalrmm/clients/tests.py +++ b/api/tacticalrmm/clients/tests.py @@ -1,19 +1,16 @@ import uuid -from unittest.mock import patch from itertools import cycle +from unittest.mock import patch from model_bakery import baker -from rest_framework.serializers import ValidationError from rest_framework.response import Response +from rest_framework.serializers import ValidationError +from tacticalrmm.constants import CustomFieldModel, CustomFieldType from tacticalrmm.test import TacticalTestCase from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField -from .serializers import ( - ClientSerializer, - DeploymentSerializer, - SiteSerializer, -) +from .serializers import ClientSerializer, DeploymentSerializer, SiteSerializer base_url = "/clients" @@ -26,7 +23,7 @@ def setUp(self): def test_get_clients(self): # setup data baker.make("clients.Client", _quantity=5) - clients = Client.objects.all() + clients = Client.objects.all() # noqa url = f"{base_url}/" r = self.client.get(url, format="json") @@ -97,11 +94,13 @@ def test_add_client(self): self.assertEqual(r.status_code, 200) # test add with custom fields - field = baker.make("core.CustomField", model="client", type="text") + field = baker.make( + "core.CustomField", model=CustomFieldModel.CLIENT, type=CustomFieldType.TEXT + ) payload = { "client": {"name": "Custom Field Client"}, "site": {"name": "Setup Site"}, - "custom_fields": [{"field": field.id, "string_value": "new Value"}], # type: ignore + "custom_fields": [{"field": field.id, "string_value": "new Value"}], } r = self.client.post(url, payload, format="json") self.assertEqual(r.status_code, 200) @@ -117,7 +116,7 @@ def test_get_client(self): # setup data client = baker.make("clients.Client") - url = f"{base_url}/{client.id}/" # type: ignore + url = f"{base_url}/{client.id}/" r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) @@ -133,7 +132,7 @@ def test_edit_client(self): # test successfull edit client data = {"client": {"name": "NewClientName"}, "custom_fields": []} - url = f"{base_url}/{client.id}/" # type: ignore + url = f"{base_url}/{client.id}/" r = self.client.put(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertTrue(Client.objects.filter(name="NewClientName").exists()) @@ -145,13 +144,17 @@ def test_edit_client(self): self.assertEqual(r.status_code, 400) # test add with custom fields new value - field = baker.make("core.CustomField", model="client", type="checkbox") + field = baker.make( + "core.CustomField", + model=CustomFieldModel.CLIENT, + type=CustomFieldType.CHECKBOX, + ) payload = { "client": { - "id": client.id, # type: ignore + "id": client.id, "name": "Custom Field Client", }, - "custom_fields": [{"field": field.id, "bool_value": True}], # type: ignore + "custom_fields": [{"field": field.id, "bool_value": True}], } r = self.client.put(url, payload, format="json") self.assertEqual(r.status_code, 200) @@ -164,10 +167,10 @@ def test_edit_client(self): # edit custom field value payload = { "client": { - "id": client.id, # type: ignore + "id": client.id, "name": "Custom Field Client", }, - "custom_fields": [{"field": field.id, "bool_value": False}], # type: ignore + "custom_fields": [{"field": field.id, "bool_value": False}], } r = self.client.put(url, payload, format="json") self.assertEqual(r.status_code, 200) @@ -191,14 +194,14 @@ def test_delete_client(self): r = self.client.delete(f"{base_url}/334/", format="json") self.assertEqual(r.status_code, 404) - url = f"/clients/{client_to_delete.id}/?site_to_move={site_to_move.id}" # type: ignore + url = f"/clients/{client_to_delete.id}/?site_to_move={site_to_move.id}" # test successful deletion r = self.client.delete(url, format="json") self.assertEqual(r.status_code, 200) agent_moved = Agent.objects.get(pk=agent.pk) - self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore - self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists()) # type: ignore + self.assertEqual(agent_moved.site.id, site_to_move.id) + self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists()) self.check_not_authenticated("delete", url) @@ -211,7 +214,7 @@ def test_get_sites(self): r = self.client.get(url, format="json") serializer = SiteSerializer(sites, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -224,7 +227,7 @@ def test_add_site(self): # test success add payload = { - "site": {"client": client.id, "name": "LA Office"}, # type: ignore + "site": {"client": client.id, "name": "LA Office"}, "custom_fields": [], } r = self.client.post(url, payload, format="json") @@ -232,7 +235,7 @@ def test_add_site(self): # test with | symbol payload = { - "site": {"client": client.id, "name": "LA Office |*&@#$"}, # type: ignore + "site": {"client": client.id, "name": "LA Office |*&@#$"}, "custom_fields": [], } serializer = SiteSerializer(data=payload["site"]) @@ -246,7 +249,7 @@ def test_add_site(self): # test site already exists payload = { - "site": {"client": site.client.id, "name": "LA Office"}, # type: ignore + "site": {"client": site.client.id, "name": "LA Office"}, "custom_fields": [], } serializer = SiteSerializer(data=payload["site"]) @@ -258,13 +261,13 @@ def test_add_site(self): # test add with custom fields field = baker.make( "core.CustomField", - model="site", - type="single", + model=CustomFieldModel.SITE, + type=CustomFieldType.SINGLE, options=["one", "two", "three"], ) payload = { - "site": {"client": client.id, "name": "Custom Field Site"}, # type: ignore - "custom_fields": [{"field": field.id, "string_value": "one"}], # type: ignore + "site": {"client": client.id, "name": "Custom Field Site"}, + "custom_fields": [{"field": field.id, "string_value": "one"}], } r = self.client.post(url, payload, format="json") self.assertEqual(r.status_code, 200) @@ -278,11 +281,11 @@ def test_get_site(self): # setup data site = baker.make("clients.Site") - url = f"{base_url}/sites/{site.id}/" # type: ignore + url = f"{base_url}/sites/{site.id}/" r = self.client.get(url, format="json") serializer = SiteSerializer(site) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -296,11 +299,11 @@ def test_edit_site(self): self.assertEqual(r.status_code, 404) data = { - "site": {"client": client.id, "name": "New Site Name"}, # type: ignore + "site": {"client": client.id, "name": "New Site Name"}, "custom_fields": [], } - url = f"{base_url}/sites/{site.id}/" # type: ignore + url = f"{base_url}/sites/{site.id}/" r = self.client.put(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertTrue( @@ -310,17 +313,17 @@ def test_edit_site(self): # test add with custom fields new value field = baker.make( "core.CustomField", - model="site", - type="multiple", + model=CustomFieldModel.SITE, + type=CustomFieldType.MULTIPLE, options=["one", "two", "three"], ) payload = { "site": { - "id": site.id, # type: ignore - "client": site.client.id, # type: ignore + "id": site.id, + "client": site.client.id, "name": "Custom Field Site", }, - "custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}], # type: ignore + "custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}], } r = self.client.put(url, payload, format="json") self.assertEqual(r.status_code, 200) @@ -331,11 +334,11 @@ def test_edit_site(self): # edit custom field value payload = { "site": { - "id": site.id, # type: ignore - "client": client.id, # type: ignore + "id": site.id, + "client": client.id, "name": "Custom Field Site", }, - "custom_fields": [{"field": field.id, "multiple_value": ["one"]}], # type: ignore + "custom_fields": [{"field": field.id, "multiple_value": ["one"]}], } r = self.client.put(url, payload, format="json") self.assertEqual(r.status_code, 200) @@ -360,7 +363,7 @@ def test_delete_site(self): r = self.client.delete("{base_url}/500/", format="json") self.assertEqual(r.status_code, 404) - url = f"/clients/sites/{site_to_delete.id}/?move_to_site={site_to_move.id}" # type: ignore + url = f"/clients/sites/{site_to_delete.id}/?move_to_site={site_to_move.id}" # test deleting with last site under client r = self.client.delete(url, format="json") @@ -368,12 +371,12 @@ def test_delete_site(self): self.assertEqual(r.json(), "A client must have at least 1 site.") # test successful deletion - site_to_move.client = client # type: ignore - site_to_move.save(update_fields=["client"]) # type: ignore + site_to_move.client = client + site_to_move.save(update_fields=["client"]) r = self.client.delete(url, format="json") self.assertEqual(r.status_code, 200) agent_moved = Agent.objects.get(pk=agent.pk) - self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore + self.assertEqual(agent_moved.site.id, site_to_move.id) self.check_not_authenticated("delete", url) @@ -385,7 +388,7 @@ def test_get_deployments(self): r = self.client.get(url) serializer = DeploymentSerializer(deployments, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -395,14 +398,14 @@ def test_add_deployment(self): url = f"{base_url}/deployments/" payload = { - "client": site.client.id, # type: ignore - "site": site.id, # type: ignore - "expires": "2037-11-23 18:53", + "client": site.client.id, + "site": site.id, + "expires": "2037-11-23T18:53:04-04:00", "power": 1, "ping": 0, "rdp": 1, "agenttype": "server", - "arch": "64", + "goarch": "amd64", } r = self.client.post(url, payload, format="json") @@ -422,10 +425,10 @@ def test_delete_deployment(self): # setup data deployment = baker.make("clients.Deployment") - url = f"{base_url}/deployments/{deployment.id}/" # type: ignore + url = f"{base_url}/deployments/{deployment.id}/" r = self.client.delete(url) self.assertEqual(r.status_code, 200) - self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists()) # type: ignore + self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists()) url = f"{base_url}/deployments/32348/" r = self.client.delete(url) @@ -439,7 +442,7 @@ def test_generate_deployment(self, post): r = self.client.get(url) self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "invalid") # type: ignore + self.assertEqual(r.data, "invalid") uid = uuid.uuid4() url = f"/clients/{uid}/deploy/" @@ -460,13 +463,13 @@ def test_generate_deployment(self, post): class TestClientPermissions(TacticalTestCase): def setUp(self): - self.client_setup() + self.setup_client() self.setup_coresettings() def test_get_clients_permissions(self): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) url = f"{base_url}/" @@ -483,17 +486,17 @@ def test_get_clients_permissions(self): # all agents should be returned response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 5) # type: ignore + self.assertEqual(len(response.data), 5) # limit user to specific client. only 1 client should be returned user.role.can_view_clients.set([clients[3]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 1) # type: ignore + self.assertEqual(len(response.data), 1) # 2 should be returned now user.role.can_view_clients.set([clients[0], clients[1]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 2) # type: ignore + self.assertEqual(len(response.data), 2) # limit to a specific site. The site shouldn't be in client returned sites sites = baker.make("clients.Site", client=clients[4], _quantity=3) @@ -502,8 +505,8 @@ def test_get_clients_permissions(self): user.role.can_view_sites.set([sites[0]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 3) # type: ignore - for client in response.data: # type: ignore + self.assertEqual(len(response.data), 3) + for client in response.data: if client["id"] == clients[0].id: self.assertEqual(len(client["sites"]), 4) elif client["id"] == clients[1].id: @@ -517,7 +520,6 @@ def test_get_clients_permissions(self): @patch("clients.models.Client.save") @patch("clients.models.Client.delete") def test_add_clients_permissions(self, save, delete): - data = {"client": {"name": "Client Name"}, "site": {"name": "Site Name"}} url = f"{base_url}/" @@ -526,7 +528,7 @@ def test_add_clients_permissions(self, save, delete): self.check_authorized_superuser("post", url, data) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("post", url, data) @@ -541,7 +543,7 @@ def test_add_clients_permissions(self, save, delete): def test_get_edit_delete_clients_permissions(self, delete): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) client = baker.make("clients.Client") unauthorized_client = baker.make("clients.Client") @@ -579,7 +581,7 @@ def test_get_edit_delete_clients_permissions(self, delete): def test_get_sites_permissions(self): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) url = f"{base_url}/sites/" @@ -597,28 +599,28 @@ def test_get_sites_permissions(self): # all sites should be returned response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 10) # type: ignore + self.assertEqual(len(response.data), 10) # limit user to specific site. only 1 site should be returned user.role.can_view_sites.set([sites[3]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 1) # type: ignore + self.assertEqual(len(response.data), 1) # 2 should be returned now user.role.can_view_sites.set([sites[0], sites[1]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 2) # type: ignore + self.assertEqual(len(response.data), 2) # check if limiting user to client works user.role.can_view_sites.clear() user.role.can_view_clients.set([clients[0]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 4) # type: ignore + self.assertEqual(len(response.data), 4) # add a site to see if the results still work user.role.can_view_sites.set([sites[1], sites[0]]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 5) # type: ignore + self.assertEqual(len(response.data), 5) # make sure superusers work self.check_authorized_superuser("get", url) @@ -636,7 +638,7 @@ def test_add_sites_permissions(self, delete, save): self.check_authorized_superuser("post", url, data) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("post", url, data) @@ -659,7 +661,7 @@ def test_add_sites_permissions(self, delete, save): def test_get_edit_delete_sites_permissions(self, delete): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) site = baker.make("clients.Site") unauthorized_site = baker.make("clients.Site") @@ -707,8 +709,8 @@ def test_get_pendingactions_permissions(self): site = baker.make("clients.Site") other_site = baker.make("clients.Site") - deployments = baker.make("clients.Deployment", site=site, _quantity=5) - other_deployments = baker.make( + deployments = baker.make("clients.Deployment", site=site, _quantity=5) # noqa + other_deployments = baker.make( # noqa "clients.Deployment", site=other_site, _quantity=7 ) @@ -718,7 +720,7 @@ def test_get_pendingactions_permissions(self): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # user with empty role should fail self.check_not_authorized("get", url) @@ -729,23 +731,23 @@ def test_get_pendingactions_permissions(self): # all sites should be returned response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 12) # type: ignore + self.assertEqual(len(response.data), 12) # limit user to specific site. only 1 site should be returned user.role.can_view_sites.set([site]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 5) # type: ignore + self.assertEqual(len(response.data), 5) # all should be returned now user.role.can_view_clients.set([other_site.client]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 12) # type: ignore + self.assertEqual(len(response.data), 12) # check if limiting user to client works user.role.can_view_sites.clear() user.role.can_view_clients.set([other_site.client]) response = self.check_authorized("get", url) - self.assertEqual(len(response.data), 7) # type: ignore + self.assertEqual(len(response.data), 7) @patch("clients.models.Deployment.save") def test_add_deployments_permissions(self, save): @@ -766,7 +768,7 @@ def test_add_deployments_permissions(self, save): self.check_authorized_superuser("post", url, data) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("post", url, data) @@ -806,7 +808,7 @@ def test_delete_deployments_permissions(self, delete): # create user with empty role user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # make sure user with empty role is unauthorized self.check_not_authorized("delete", url) @@ -846,7 +848,7 @@ def test_restricted_user_creating_clients(self): # when a user that is limited to a specific subset of clients creates a client. It should allow access to that client client = baker.make("clients.Client") user = self.create_user_with_roles(["can_manage_clients"]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) user.role.can_view_clients.set([client]) data = {"client": {"name": "New Client"}, "site": {"name": "New Site"}} @@ -854,7 +856,7 @@ def test_restricted_user_creating_clients(self): self.client.post(f"{base_url}/", data, format="json") # make sure two clients are allowed now - self.assertEqual(User.objects.get(id=user.id).role.can_view_clients.count(), 2) + self.assertEqual(User.objects.get(id=user.pk).role.can_view_clients.count(), 2) def test_restricted_user_creating_sites(self): from accounts.models import User @@ -862,7 +864,7 @@ def test_restricted_user_creating_sites(self): # when a user that is limited to a specific subset of clients creates a client. It should allow access to that client site = baker.make("clients.Site") user = self.create_user_with_roles(["can_manage_sites"]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) user.role.can_view_sites.set([site]) data = {"site": {"client": site.client.id, "name": "New Site"}} @@ -870,4 +872,4 @@ def test_restricted_user_creating_sites(self): self.client.post(f"{base_url}/sites/", data, format="json") # make sure two sites are allowed now - self.assertEqual(User.objects.get(id=user.id).role.can_view_sites.count(), 2) + self.assertEqual(User.objects.get(id=user.pk).role.can_view_sites.count(), 2) diff --git a/api/tacticalrmm/clients/views.py b/api/tacticalrmm/clients/views.py index 6bec2a4a09..e82ec76901 100644 --- a/api/tacticalrmm/clients/views.py +++ b/api/tacticalrmm/clients/views.py @@ -1,26 +1,24 @@ import datetime as dt import re import uuid +from contextlib import suppress -import pytz +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 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 rest_framework.exceptions import PermissionDenied from agents.models import Agent -from core.models import CoreSettings -from tacticalrmm.utils import notify_error +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 .models import Client, ClientCustomField, Deployment, Site, SiteCustomField -from .permissions import ( - ClientsPerms, - DeploymentPerms, - SitesPerms, -) +from .permissions import ClientsPerms, DeploymentPerms, SitesPerms from .serializers import ( ClientCustomFieldSerializer, ClientSerializer, @@ -34,12 +32,42 @@ class GetAddClients(APIView): permission_classes = [IsAuthenticated, ClientsPerms] def get(self, request): - clients = Client.objects.select_related( - "workstation_policy", "server_policy", "alert_template" - ).filter_by_role(request.user) - return Response( - ClientSerializer(clients, context={"user": request.user}, many=True).data + clients = ( + Client.objects.order_by("name") + .select_related("workstation_policy", "server_policy", "alert_template") + .filter_by_role(request.user) # type: ignore + .prefetch_related( + Prefetch( + "custom_fields", + queryset=ClientCustomField.objects.select_related("field"), + ), + Prefetch( + "sites", + queryset=Site.objects.order_by("name") + .select_related("client") + .filter_by_role(request.user) + .prefetch_related("custom_fields__field") + .annotate( + maintenance_mode=Exists( + Agent.objects.filter( + site=OuterRef("pk"), maintenance_mode=True + ) + ) + ) + .annotate(agent_count=Count("agents")), + to_attr="filtered_sites", + ), + ) + .annotate( + maintenance_mode=Exists( + Agent.objects.filter( + site__client=OuterRef("pk"), maintenance_mode=True + ) + ) + ) + .annotate(agent_count=Count("sites__agents")) ) + return Response(ClientSerializer(clients, many=True).data) def post(self, request): # create client @@ -61,14 +89,13 @@ def post(self, request): site_serializer.is_valid(raise_exception=True) if "initialsetup" in request.data.keys(): - core = CoreSettings.objects.first() + core = get_core_settings() core.default_time_zone = request.data["timezone"] core.save(update_fields=["default_time_zone"]) # save custom fields if "custom_fields" in request.data.keys(): for field in request.data["custom_fields"]: - custom_field = field custom_field["client"] = client.id @@ -88,7 +115,25 @@ class GetUpdateDeleteClient(APIView): def get(self, request, pk): client = get_object_or_404(Client, pk=pk) - return Response(ClientSerializer(client, context={"user": request.user}).data) + + prefetch_related_objects( + [client], + Prefetch( + "sites", + queryset=Site.objects.order_by("name") + .select_related("client") + .filter_by_role(request.user) + .prefetch_related("custom_fields__field") + .annotate( + maintenance_mode=Exists( + Agent.objects.filter(site=OuterRef("pk"), maintenance_mode=True) + ) + ) + .annotate(agent_count=Count("agents")), + to_attr="filtered_sites", + ), + ) + return Response(ClientSerializer(client).data) def put(self, request, pk): client = get_object_or_404(Client, pk=pk) @@ -102,7 +147,6 @@ def put(self, request, pk): # update custom fields if "custom_fields" in request.data.keys(): for field in request.data["custom_fields"]: - custom_field = field custom_field["client"] = pk @@ -123,18 +167,16 @@ def put(self, request, pk): return Response("{client} was updated") def delete(self, request, pk): - from automation.tasks import generate_agent_checks_task - client = get_object_or_404(Client, pk=pk) + agent_count = client.live_agent_count # only run tasks if it affects clients - if client.agent_count > 0 and "move_to_site" in request.query_params.keys(): + if agent_count > 0 and "move_to_site" in request.query_params.keys(): agents = Agent.objects.filter(site__client=client) site = get_object_or_404(Site, pk=request.query_params["move_to_site"]) agents.update(site=site) - generate_agent_checks_task.delay(all=True, create_tasks=True) - elif client.agent_count > 0: + elif agent_count > 0: return notify_error( "Agents exist under this client. There needs to be a site specified to move existing agents to" ) @@ -147,11 +189,10 @@ class GetAddSites(APIView): permission_classes = [IsAuthenticated, SitesPerms] def get(self, request): - sites = Site.objects.filter_by_role(request.user) + sites = Site.objects.filter_by_role(request.user) # type: ignore return Response(SiteSerializer(sites, many=True).data) def post(self, request): - if not _has_perm_on_client(request.user, request.data["site"]["client"]): raise PermissionDenied() @@ -161,9 +202,7 @@ def post(self, request): # save custom fields if "custom_fields" in request.data.keys(): - for field in request.data["custom_fields"]: - custom_field = field custom_field["site"] = site.id @@ -202,9 +241,7 @@ def put(self, request, pk): # update custom field if "custom_fields" in request.data.keys(): - for field in request.data["custom_fields"]: - custom_field = field custom_field["site"] = pk @@ -223,20 +260,18 @@ def put(self, request, pk): return Response("Site was edited") def delete(self, request, pk): - from automation.tasks import generate_agent_checks_task - site = get_object_or_404(Site, pk=pk) if site.client.sites.count() == 1: return notify_error("A client must have at least 1 site.") # only run tasks if it affects clients - if site.agent_count > 0 and "move_to_site" in request.query_params.keys(): + agent_count = site.live_agent_count + if agent_count > 0 and "move_to_site" in request.query_params.keys(): agents = Agent.objects.filter(site=site) new_site = get_object_or_404(Site, pk=request.query_params["move_to_site"]) agents.update(site=new_site) - generate_agent_checks_task.delay(all=True, create_tasks=True) - elif site.agent_count > 0: + elif agent_count > 0: return notify_error( "There needs to be a site specified to move the agents to" ) @@ -249,11 +284,10 @@ class AgentDeployment(APIView): permission_classes = [IsAuthenticated, DeploymentPerms] def get(self, request): - deps = Deployment.objects.filter_by_role(request.user) + deps = Deployment.objects.filter_by_role(request.user) # type: ignore return Response(DeploymentSerializer(deps, many=True).data) def post(self, request): - from knox.models import AuthToken from accounts.models import User site = get_object_or_404(Site, pk=request.data["site"]) @@ -263,12 +297,17 @@ def post(self, request): installer_user = User.objects.filter(is_installer_user=True).first() - expires = dt.datetime.strptime( - request.data["expires"], "%Y-%m-%d %H:%M" - ).astimezone(pytz.timezone("UTC")) - now = djangotime.now() - delta = expires - now - obj, token = AuthToken.objects.create(user=installer_user, expiry=delta) + try: + expires = dt.datetime.strptime( + request.data["expires"], "%Y-%m-%dT%H:%M:%S%z" + ) + + except Exception: + return notify_error("expire date is invalid") + + obj, token = AuthToken.objects.create( + user=installer_user, expiry=expires - djangotime.now() + ) flags = { "power": request.data["power"], @@ -280,7 +319,7 @@ def post(self, request): site=site, expiry=expires, mon_type=request.data["agenttype"], - arch=request.data["arch"], + goarch=request.data["goarch"], auth_token=obj, token_key=token, install_flags=flags, @@ -293,17 +332,14 @@ def delete(self, request, pk): if not _has_perm_on_site(request.user, d.site.pk): raise PermissionDenied() - try: + with suppress(Exception): d.auth_token.delete() - except: - pass d.delete() return Response("The deployment was deleted") class GenerateAgent(APIView): - permission_classes = (AllowAny,) def get(self, request, uid): @@ -320,8 +356,7 @@ def get(self, request, uid): site = d.site.name.replace(" ", "").lower() client = re.sub(r"([^a-zA-Z0-9]+)", "", client) site = re.sub(r"([^a-zA-Z0-9]+)", "", site) - ext = ".exe" if d.arch == "64" else "-x86.exe" - file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}" + file_name = f"trmm-{client}-{site}-{d.mon_type}-{d.goarch}.exe" return generate_winagent_exe( client=d.client.pk, @@ -330,7 +365,7 @@ def get(self, request, uid): rdp=d.install_flags["rdp"], ping=d.install_flags["ping"], power=d.install_flags["power"], - arch=d.arch, + goarch=d.goarch, token=d.token_key, api=f"https://{request.get_host()}", file_name=file_name, diff --git a/api/tacticalrmm/core/agent_linux.sh b/api/tacticalrmm/core/agent_linux.sh new file mode 100755 index 0000000000..b5fbcb04d7 --- /dev/null +++ b/api/tacticalrmm/core/agent_linux.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash + +if [ $EUID -ne 0 ]; then + echo "ERROR: Must be run as root" + exit 1 +fi + +HAS_SYSTEMD=$(ps --no-headers -o comm 1) +if [ "${HAS_SYSTEMD}" != 'systemd' ]; then + echo "This install script only supports systemd" + echo "Please install systemd or manually create the service using your systems's service manager" + exit 1 +fi + +if [[ $DISPLAY ]]; then + echo "ERROR: Display detected. Installer only supports running headless, i.e from ssh." + echo "If you cannot ssh in then please run 'sudo systemctl isolate multi-user.target' to switch to a non-graphical user session and run the installer again." + exit 1 +fi + +DEBUG=0 +INSECURE=0 +NOMESH=0 + +agentDL='agentDLChange' +meshDL='meshDLChange' + +apiURL='apiURLChange' +token='tokenChange' +clientID='clientIDChange' +siteID='siteIDChange' +agentType='agentTypeChange' +proxy='' + +agentBinPath='/usr/local/bin' +binName='tacticalagent' +agentBin="${agentBinPath}/${binName}" +agentConf='/etc/tacticalagent' +agentSvcName='tacticalagent.service' +agentSysD="/etc/systemd/system/${agentSvcName}" +meshDir='/opt/tacticalmesh' +meshSystemBin="${meshDir}/meshagent" +meshSvcName='meshagent.service' +meshSysD="/lib/systemd/system/${meshSvcName}" + +deb=(ubuntu debian raspbian kali linuxmint) +rhe=(fedora rocky centos rhel amzn arch opensuse) + +set_locale_deb() { + locale-gen "en_US.UTF-8" + localectl set-locale LANG=en_US.UTF-8 + . /etc/default/locale +} + +set_locale_rhel() { + localedef -c -i en_US -f UTF-8 en_US.UTF-8 >/dev/null 2>&1 + localectl set-locale LANG=en_US.UTF-8 + . /etc/locale.conf +} + +RemoveOldAgent() { + if [ -f "${agentSysD}" ]; then + systemctl disable ${agentSvcName} + systemctl stop ${agentSvcName} + rm -f ${agentSysD} + systemctl daemon-reload + fi + + if [ -f "${agentConf}" ]; then + rm -f ${agentConf} + fi + + if [ -f "${agentBin}" ]; then + rm -f ${agentBin} + fi +} + +InstallMesh() { + if [ -f /etc/os-release ]; then + distroID=$( + . /etc/os-release + echo $ID + ) + distroIDLIKE=$( + . /etc/os-release + echo $ID_LIKE + ) + if [[ " ${deb[*]} " =~ " ${distroID} " ]]; then + set_locale_deb + elif [[ " ${deb[*]} " =~ " ${distroIDLIKE} " ]]; then + set_locale_deb + elif [[ " ${rhe[*]} " =~ " ${distroID} " ]]; then + set_locale_rhel + else + set_locale_rhel + fi + fi + + meshTmpDir='/root/meshtemp' + mkdir -p $meshTmpDir + + meshTmpBin="${meshTmpDir}/meshagent" + wget --no-check-certificate -q -O ${meshTmpBin} ${meshDL} + chmod +x ${meshTmpBin} + mkdir -p ${meshDir} + env LC_ALL=en_US.UTF-8 LANGUAGE=en_US XAUTHORITY=foo DISPLAY=bar ${meshTmpBin} -install --installPath=${meshDir} + sleep 1 + rm -rf ${meshTmpDir} +} + +RemoveMesh() { + if [ -f "${meshSystemBin}" ]; then + env XAUTHORITY=foo DISPLAY=bar ${meshSystemBin} -uninstall + sleep 1 + fi + + if [ -f "${meshSysD}" ]; then + systemctl stop ${meshSvcName} >/dev/null 2>&1 + systemctl disable ${meshSvcName} >/dev/null 2>&1 + rm -f ${meshSysD} + fi + + rm -rf ${meshDir} + systemctl daemon-reload +} + +Uninstall() { + RemoveMesh + RemoveOldAgent +} + +if [ $# -ne 0 ] && [ $1 == 'uninstall' ]; then + Uninstall + exit 0 +fi + +while [[ "$#" -gt 0 ]]; do + case $1 in + --debug) DEBUG=1 ;; + --insecure) INSECURE=1 ;; + --nomesh) NOMESH=1 ;; + *) + echo "ERROR: Unknown parameter: $1" + exit 1 + ;; + esac + shift +done + +RemoveOldAgent + +echo "Downloading tactical agent..." +wget -q -O ${agentBin} "${agentDL}" +if [ $? -ne 0 ]; then + echo "ERROR: Unable to download tactical agent" + exit 1 +fi +chmod +x ${agentBin} + +MESH_NODE_ID="" + +if [[ $NOMESH -eq 1 ]]; then + echo "Skipping mesh install" +else + if [ -f "${meshSystemBin}" ]; then + RemoveMesh + fi + echo "Downloading and installing mesh agent..." + InstallMesh + sleep 2 + echo "Getting mesh node id..." + MESH_NODE_ID=$(env XAUTHORITY=foo DISPLAY=bar ${agentBin} -m nixmeshnodeid) +fi + +if [ ! -d "${agentBinPath}" ]; then + echo "Creating ${agentBinPath}" + mkdir -p ${agentBinPath} +fi + +INSTALL_CMD="${agentBin} -m install -api ${apiURL} -client-id ${clientID} -site-id ${siteID} -agent-type ${agentType} -auth ${token}" + +if [ "${MESH_NODE_ID}" != '' ]; then + INSTALL_CMD+=" --meshnodeid ${MESH_NODE_ID}" +fi + +if [[ $DEBUG -eq 1 ]]; then + INSTALL_CMD+=" --log debug" +fi + +if [[ $INSECURE -eq 1 ]]; then + INSTALL_CMD+=" --insecure" +fi + +if [ "${proxy}" != '' ]; then + INSTALL_CMD+=" --proxy ${proxy}" +fi + +eval ${INSTALL_CMD} + +tacticalsvc="$( + cat </dev/null + +systemctl daemon-reload +systemctl enable ${agentSvcName} +systemctl start ${agentSvcName} diff --git a/api/tacticalrmm/core/consumers.py b/api/tacticalrmm/core/consumers.py index 01d41b24e6..853bc647fd 100644 --- a/api/tacticalrmm/core/consumers.py +++ b/api/tacticalrmm/core/consumers.py @@ -1,15 +1,19 @@ import asyncio +from contextlib import suppress from channels.db import database_sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.contrib.auth.models import AnonymousUser +from django.db.models import F +from django.utils import timezone as djangotime from agents.models import Agent +from tacticalrmm.constants import AgentMonType +from tacticalrmm.helpers import days_until_cert_expires class DashInfo(AsyncJsonWebsocketConsumer): async def connect(self): - self.user = self.scope["user"] if isinstance(self.user, AnonymousUser): @@ -20,57 +24,52 @@ async def connect(self): self.dash_info = asyncio.create_task(self.send_dash_info()) async def disconnect(self, close_code): - - try: + with suppress(Exception): self.dash_info.cancel() - except: - pass self.connected = False - await self.close() - async def receive(self, json_data=None): + async def receive_json(self, payload, **kwargs): pass @database_sync_to_async def get_dashboard_info(self): - server_offline_count = len( - [ - agent - for agent in Agent.objects.filter(monitoring_type="server").only( - "pk", - "last_seen", - "overdue_time", - "offline_time", - ) - if not agent.status == "online" - ] + total_server_agents_count = ( + Agent.objects.filter_by_role(self.user) + .filter(monitoring_type=AgentMonType.SERVER) + .count() ) - - workstation_offline_count = len( - [ - agent - for agent in Agent.objects.filter(monitoring_type="workstation").only( - "pk", - "last_seen", - "overdue_time", - "offline_time", - ) - if not agent.status == "online" - ] + offline_server_agents_count = ( + Agent.objects.filter_by_role(self.user) + .filter(monitoring_type=AgentMonType.SERVER) + .filter( + last_seen__lt=djangotime.now() + - (djangotime.timedelta(minutes=1) * F("offline_time")) + ) + .count() + ) + total_workstation_agents_count = ( + Agent.objects.filter_by_role(self.user) + .filter(monitoring_type=AgentMonType.WORKSTATION) + .count() + ) + offline_workstation_agents_count = ( + Agent.objects.filter_by_role(self.user) + .filter(monitoring_type=AgentMonType.WORKSTATION) + .filter( + last_seen__lt=djangotime.now() + - (djangotime.timedelta(minutes=1) * F("offline_time")) + ) + .count() ) - ret = { - "total_server_offline_count": server_offline_count, - "total_workstation_offline_count": workstation_offline_count, - "total_server_count": Agent.objects.filter( - monitoring_type="server" - ).count(), - "total_workstation_count": Agent.objects.filter( - monitoring_type="workstation" - ).count(), + return { + "total_server_offline_count": offline_server_agents_count, + "total_workstation_offline_count": offline_workstation_agents_count, + "total_server_count": total_server_agents_count, + "total_workstation_count": total_workstation_agents_count, + "days_until_cert_expires": days_until_cert_expires(), } - return ret async def send_dash_info(self): while self.connected: diff --git a/api/tacticalrmm/core/decorators.py b/api/tacticalrmm/core/decorators.py new file mode 100644 index 0000000000..c77dde37fa --- /dev/null +++ b/api/tacticalrmm/core/decorators.py @@ -0,0 +1,31 @@ +import json + +from django.conf import settings +from django.http import HttpResponse + + +def monitoring_view(function): + def wrap(request, *args, **kwargs): + if request.method != "POST": + return HttpResponse("Invalid request type\n", status=400) + + try: + data = json.loads(request.body) + except: + return HttpResponse("Invalid json\n", status=400) + + if "auth" not in data.keys(): + return HttpResponse("Invalid payload\n", status=400) + + token = getattr(settings, "MON_TOKEN", "") + if not token: + return HttpResponse("Missing token\n", status=401) + + if data.get("auth") != token: + return HttpResponse("Not authenticated\n", status=401) + + return function(request, *args, **kwargs) + + wrap.__doc__ = function.__doc__ + wrap.__name__ = function.__name__ + return wrap diff --git a/api/tacticalrmm/core/installer.ps1 b/api/tacticalrmm/core/installer.ps1 index 462573ab43..4d01675b10 100644 --- a/api/tacticalrmm/core/installer.ps1 +++ b/api/tacticalrmm/core/installer.ps1 @@ -13,7 +13,7 @@ $apilink = $downloadlink.split('/') [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$serviceName = 'tacticalagent' +$serviceName = 'tacticalrmm' If (Get-Service $serviceName -ErrorAction SilentlyContinue) { write-host ('Tactical RMM Is Already Installed') } Else { @@ -39,9 +39,8 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) { $DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled if ($DefenderStatus -match "True") { Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*' - Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe' Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*' - Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*' + Add-MpPreference -ExclusionPath 'C:\ProgramData\TacticalRMM\*' } } Catch { diff --git a/api/tacticalrmm/core/mac_uninstall.sh b/api/tacticalrmm/core/mac_uninstall.sh new file mode 100755 index 0000000000..2cb29347af --- /dev/null +++ b/api/tacticalrmm/core/mac_uninstall.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if [ -f /usr/local/mesh_services/meshagent/meshagent ]; then + /usr/local/mesh_services/meshagent/meshagent -fulluninstall +fi + +if [ -f /opt/tacticalmesh/meshagent ]; then + /opt/tacticalmesh/meshagent -fulluninstall +fi + +launchctl bootout system /Library/LaunchDaemons/tacticalagent.plist +rm -rf /usr/local/mesh_services +rm -rf /opt/tacticalmesh +rm -f /etc/tacticalagent +rm -rf /opt/tacticalagent +rm -f /Library/LaunchDaemons/tacticalagent.plist \ No newline at end of file diff --git a/api/tacticalrmm/core/management/commands/bulk_restart_agents.py b/api/tacticalrmm/core/management/commands/bulk_restart_agents.py new file mode 100644 index 0000000000..1635169046 --- /dev/null +++ b/api/tacticalrmm/core/management/commands/bulk_restart_agents.py @@ -0,0 +1,32 @@ +from time import sleep + +from django.core.management.base import BaseCommand + +from agents.models import Agent +from core.utils import get_mesh_ws_url +from tacticalrmm.constants import AGENT_DEFER + + +class Command(BaseCommand): + help = "Restarts the tactical and meshagent services" + + def handle(self, *args, **kwargs) -> None: + agents = Agent.objects.defer(*AGENT_DEFER) + uri = get_mesh_ws_url() + + for agent in agents: + self.stdout.write( + self.style.SUCCESS( + f"Restarting Tactical Agent Service on {agent.hostname}" + ) + ) + agent.recover("tacagent", uri, wait=False) + + self.stdout.write(self.style.WARNING("Waiting 10 seconds...")) + sleep(10) + + for agent in agents: + self.stdout.write( + self.style.SUCCESS(f"Restarting MeshAgent Service on {agent.hostname}") + ) + agent.recover("mesh", "", wait=False) diff --git a/api/tacticalrmm/core/management/commands/check_mesh.py b/api/tacticalrmm/core/management/commands/check_mesh.py new file mode 100644 index 0000000000..2efeae0510 --- /dev/null +++ b/api/tacticalrmm/core/management/commands/check_mesh.py @@ -0,0 +1,61 @@ +import asyncio + +from django.core.management.base import BaseCommand +from meshctrl.utils import get_auth_token + +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) -> None: + self.stdout.write(self.style.SUCCESS(" ".join(args))) + + def _error(self, *args) -> None: + self.stdout.write(self.style.ERROR(" ".join(args))) + + def _warning(self, *args) -> None: + self.stdout.write(self.style.WARNING(" ".join(args))) + + def handle(self, *args, **kwargs) -> None: + core = get_core_settings() + + self._warning("Mesh site:", core.mesh_site) + self._warning("Mesh username:", core.mesh_username) + self._warning("Mesh token:", core.mesh_token) + self._warning("Mesh device group:", core.mesh_device_group) + + try: + token = get_auth_token(core.mesh_username, core.mesh_token) + except Exception as e: + self._error("Error getting auth token:") + self._error(str(e)) + return + else: + self._success("Auth token ok:") + self._success(token) + + try: + uri = get_mesh_ws_url() + except Exception as e: + self._error("Error getting mesh url:") + self._error(str(e)) + return + else: + self._success("Mesh url ok:") + self._success(uri) + + try: + mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) + except IndexError: + self._error( + "Error: you are using a custom mesh device group name. The name in TRMMs Global Settings > MeshCentral must match a MeshCentral group exactly." + ) + return + except Exception as e: + self._error("Error getting mesh device id:") + self._error(str(e)) + return + else: + self._success("Mesh device id ok:", mesh_id) diff --git a/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py b/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py new file mode 100644 index 0000000000..ece7fc9137 --- /dev/null +++ b/api/tacticalrmm/core/management/commands/clear_redis_celery_locks.py @@ -0,0 +1,22 @@ +from django.core.cache import cache +from django.core.management.base import BaseCommand + +from tacticalrmm.constants import ( + AGENT_OUTAGES_LOCK, + ORPHANED_WIN_TASK_LOCK, + RESOLVE_ALERTS_LOCK, + SYNC_SCHED_TASK_LOCK, +) + + +class Command(BaseCommand): + help = "Clear redis celery locks. Should only be ran while celery/beat is stopped." + + def handle(self, *args, **kwargs): + for key in ( + AGENT_OUTAGES_LOCK, + ORPHANED_WIN_TASK_LOCK, + RESOLVE_ALERTS_LOCK, + SYNC_SCHED_TASK_LOCK, + ): + cache.delete(key) diff --git a/api/tacticalrmm/core/management/commands/create_natsapi_conf.py b/api/tacticalrmm/core/management/commands/create_natsapi_conf.py index 86fb735fd2..9d74632f45 100644 --- a/api/tacticalrmm/core/management/commands/create_natsapi_conf.py +++ b/api/tacticalrmm/core/management/commands/create_natsapi_conf.py @@ -1,24 +1,39 @@ -import os import json +import os -from django.core.management.base import BaseCommand from django.conf import settings +from django.core.management.base import BaseCommand + +from tacticalrmm.helpers import get_nats_internal_protocol, get_nats_ports class Command(BaseCommand): help = "Generate conf for nats-api" def handle(self, *args, **kwargs): + self.stdout.write("Creating configuration for nats-api...") db = settings.DATABASES["default"] + if hasattr(settings, "DB_SSL"): + ssl = settings.DB_SSL + elif "DB_SSL" in os.environ: + ssl = os.getenv("DB_SSL") + else: + ssl = "disable" + + nats_std_port, _ = get_nats_ports() + proto = get_nats_internal_protocol() config = { "key": settings.SECRET_KEY, - "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", + "natsurl": f"{proto}://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}", "user": db["USER"], "pass": db["PASSWORD"], "host": db["HOST"], "port": int(db["PORT"]), "dbname": db["NAME"], + "sslmode": ssl, } conf = os.path.join(settings.BASE_DIR, "nats-api.conf") with open(conf, "w") as f: json.dump(config, f) + + self.stdout.write("Configuration for nats-api created successfully") diff --git a/api/tacticalrmm/core/management/commands/create_uwsgi_conf.py b/api/tacticalrmm/core/management/commands/create_uwsgi_conf.py new file mode 100644 index 0000000000..10446caae3 --- /dev/null +++ b/api/tacticalrmm/core/management/commands/create_uwsgi_conf.py @@ -0,0 +1,59 @@ +import configparser +import os +from pathlib import Path + +from django.conf import settings +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "Generate conf for uwsgi" + + def handle(self, *args, **kwargs): + self.stdout.write("Creating uwsgi conf...") + + config = configparser.ConfigParser() + + if getattr(settings, "DOCKER_BUILD", False): + home = str(Path(os.getenv("VIRTUAL_ENV"))) # type: ignore + socket = "0.0.0.0:8080" + else: + home = str(settings.BASE_DIR.parents[0] / "env") + socket = str(settings.BASE_DIR / "tacticalrmm.sock") + + config["uwsgi"] = { + "chdir": str(settings.BASE_DIR), + "module": "tacticalrmm.wsgi", + "home": home, + "master": str(getattr(settings, "UWSGI_MASTER", True)).lower(), + "enable-threads": str( + getattr(settings, "UWSGI_ENABLE_THREADS", True) + ).lower(), + "socket": socket, + "harakiri": str(getattr(settings, "UWSGI_HARAKIRI", 300)), + "chmod-socket": str(getattr(settings, "UWSGI_CHMOD_SOCKET", 660)), + "buffer-size": str(getattr(settings, "UWSGI_BUFFER_SIZE", 65535)), + "vacuum": str(getattr(settings, "UWSGI_VACUUM", True)).lower(), + "die-on-term": str(getattr(settings, "UWSGI_DIE_ON_TERM", True)).lower(), + "max-requests": str(getattr(settings, "UWSGI_MAX_REQUESTS", 500)), + "disable-logging": str( + getattr(settings, "UWSGI_DISABLE_LOGGING", True) + ).lower(), + "cheaper-algo": "busyness", + "cheaper": str(getattr(settings, "UWSGI_CHEAPER", 4)), + "cheaper-initial": str(getattr(settings, "UWSGI_CHEAPER_INITIAL", 4)), + "workers": str(getattr(settings, "UWSGI_MAX_WORKERS", 40)), + "cheaper-step": str(getattr(settings, "UWSGI_CHEAPER_STEP", 2)), + "cheaper-overload": str(getattr(settings, "UWSGI_CHEAPER_OVERLOAD", 3)), + "cheaper-busyness-min": str(getattr(settings, "UWSGI_BUSYNESS_MIN", 5)), + "cheaper-busyness-max": str(getattr(settings, "UWSGI_BUSYNESS_MAX", 10)), + } + + if getattr(settings, "UWSGI_DEBUG", False): + config["uwsgi"]["stats"] = "/tmp/stats.socket" + config["uwsgi"]["cheaper-busyness-verbose"] = str(True).lower() + + with open(settings.BASE_DIR / "app.ini", "w") as fp: + config.write(fp) + + self.stdout.write("Created uwsgi conf") diff --git a/api/tacticalrmm/core/management/commands/get_config.py b/api/tacticalrmm/core/management/commands/get_config.py new file mode 100644 index 0000000000..4374228d96 --- /dev/null +++ b/api/tacticalrmm/core/management/commands/get_config.py @@ -0,0 +1,61 @@ +from urllib.parse import urlparse + +from django.conf import settings +from django.core.management.base import BaseCommand + +from tacticalrmm.helpers import get_webdomain + + +class Command(BaseCommand): + help = "Get config vars to be used in shell scripts" + + def add_arguments(self, parser): + parser.add_argument("name", type=str, help="The name of the config") + + def handle(self, *args, **kwargs): + match kwargs["name"]: + case "api": + self.stdout.write(settings.ALLOWED_HOSTS[0]) + case "version": + self.stdout.write(settings.TRMM_VERSION) + case "webversion": + self.stdout.write(settings.WEB_VERSION) + case "meshver": + self.stdout.write(settings.MESH_VER) + case "natsver": + self.stdout.write(settings.NATS_SERVER_VER) + case "frontend": + self.stdout.write(settings.CORS_ORIGIN_WHITELIST[0]) + case "webdomain": + self.stdout.write(get_webdomain()) + case "djangoadmin": + url = f"https://{settings.ALLOWED_HOSTS[0]}/{settings.ADMIN_URL}" + self.stdout.write(url) + case "setuptoolsver": + self.stdout.write(settings.SETUPTOOLS_VER) + case "wheelver": + self.stdout.write(settings.WHEEL_VER) + case "dbname": + self.stdout.write(settings.DATABASES["default"]["NAME"]) + case "dbuser": + self.stdout.write(settings.DATABASES["default"]["USER"]) + case "dbpw": + self.stdout.write(settings.DATABASES["default"]["PASSWORD"]) + case "dbhost": + self.stdout.write(settings.DATABASES["default"]["HOST"]) + case "dbport": + self.stdout.write(settings.DATABASES["default"]["PORT"]) + case "meshsite" | "meshuser" | "meshtoken" | "meshdomain": + from core.models import CoreSettings + + core: "CoreSettings" = CoreSettings.objects.first() + if kwargs["name"] == "meshsite": + obj = core.mesh_site + elif kwargs["name"] == "meshuser": + obj = core.mesh_username + elif kwargs["name"] == "meshdomain": + obj = urlparse(core.mesh_site).netloc + else: + obj = core.mesh_token + + self.stdout.write(obj) diff --git a/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py b/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py index 6058863566..07a548bf60 100644 --- a/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py +++ b/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py @@ -2,29 +2,16 @@ import json import websockets -from django.conf import settings from django.core.management.base import BaseCommand -from core.models import CoreSettings - -from .helpers import get_auth_token +from core.utils import get_mesh_ws_url class Command(BaseCommand): help = "Sets up initial mesh central configuration" - async def websocket_call(self, mesh_settings): - token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) - - if settings.DOCKER_BUILD: - site = mesh_settings.mesh_site.replace("https", "ws") - uri = f"{site}:443/control.ashx?auth={token}" - else: - site = mesh_settings.mesh_site.replace("https", "wss") - uri = f"{site}/control.ashx?auth={token}" - + async def websocket_call(self, uri): async with websockets.connect(uri) as websocket: - # Get Invitation Link await websocket.send( json.dumps( @@ -42,9 +29,9 @@ async def websocket_call(self, mesh_settings): response = json.loads(message) if response["action"] == "createInviteLink": - print(response["url"]) + self.stdout.write(response["url"].replace(":4443", ":443")) break def handle(self, *args, **kwargs): - mesh_settings = CoreSettings.objects.first() - asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings)) + uri = get_mesh_ws_url() + asyncio.run(self.websocket_call(uri)) diff --git a/api/tacticalrmm/core/management/commands/helpers.py b/api/tacticalrmm/core/management/commands/helpers.py deleted file mode 100644 index da9026013e..0000000000 --- a/api/tacticalrmm/core/management/commands/helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -import time -from base64 import b64encode - -from Crypto.Cipher import AES -from Crypto.Random import get_random_bytes - - -def get_auth_token(user, key): - key = bytes.fromhex(key) - key1 = key[0:32] - msg = '{{"userid":"{}", "domainid":"{}", "time":{}}}'.format( - f"user//{user}", "", int(time.time()) - ) - iv = get_random_bytes(12) - - a = AES.new(key1, AES.MODE_GCM, iv) - msg, tag = a.encrypt_and_digest(bytes(msg, "utf-8")) - - return b64encode(iv + tag + msg, altchars=b"@$").decode("utf-8") diff --git a/api/tacticalrmm/core/management/commands/initial_db_setup.py b/api/tacticalrmm/core/management/commands/initial_db_setup.py index a1281c5a30..ac1611f43d 100644 --- a/api/tacticalrmm/core/management/commands/initial_db_setup.py +++ b/api/tacticalrmm/core/management/commands/initial_db_setup.py @@ -1,3 +1,5 @@ +from contextlib import suppress + from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand @@ -8,9 +10,7 @@ class Command(BaseCommand): help = "Populates the global site settings on first install" def handle(self, *args, **kwargs): - try: + # can only be 1 instance of this. Prevents error when rebuilding docker container + with suppress(ValidationError): CoreSettings().save() self.stdout.write("Core db populated") - except ValidationError: - # can only be 1 instance of this. Prevents error when rebuilding docker container - pass diff --git a/api/tacticalrmm/core/management/commands/initial_mesh_setup.py b/api/tacticalrmm/core/management/commands/initial_mesh_setup.py index 30f045e728..3ca363575d 100644 --- a/api/tacticalrmm/core/management/commands/initial_mesh_setup.py +++ b/api/tacticalrmm/core/management/commands/initial_mesh_setup.py @@ -5,34 +5,20 @@ from django.conf import settings from django.core.management.base import BaseCommand -from core.models import CoreSettings - -from .helpers import get_auth_token +from core.utils import get_core_settings, get_mesh_ws_url class Command(BaseCommand): help = "Sets up initial mesh central configuration" - async def websocket_call(self, mesh_settings): - - token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) - - if settings.DOCKER_BUILD: - site = mesh_settings.mesh_site.replace("https", "ws") - uri = f"{site}:443/control.ashx?auth={token}" - else: - site = mesh_settings.mesh_site.replace("https", "wss") - uri = f"{site}/control.ashx?auth={token}" - + async def websocket_call(self, uri): async with websockets.connect(uri) as websocket: - # Get Device groups to see if it exists await websocket.send(json.dumps({"action": "meshes"})) async for message in websocket: response = json.loads(message) if response["action"] == "meshes": - # If no meshes are present if not response["meshes"]: await websocket.send( @@ -50,15 +36,15 @@ async def websocket_call(self, mesh_settings): break def handle(self, *args, **kwargs): - mesh_settings = CoreSettings.objects.first() + mesh_settings = get_core_settings() try: # Check for Mesh Username if ( not mesh_settings.mesh_username - or settings.MESH_USERNAME != mesh_settings.mesh_username + or settings.MESH_USERNAME.lower() != mesh_settings.mesh_username ): - mesh_settings.mesh_username = settings.MESH_USERNAME + mesh_settings.mesh_username = settings.MESH_USERNAME.lower() # Check for Mesh Site if ( @@ -83,9 +69,8 @@ def handle(self, *args, **kwargs): return try: - asyncio.get_event_loop().run_until_complete( - self.websocket_call(mesh_settings) - ) + uri = get_mesh_ws_url() + asyncio.run(self.websocket_call(uri)) self.stdout.write("Initial Mesh Central setup complete") except websockets.exceptions.ConnectionClosedError: self.stdout.write( diff --git a/api/tacticalrmm/core/management/commands/post_update_tasks.py b/api/tacticalrmm/core/management/commands/post_update_tasks.py index b7e3a53111..3a7d259a87 100644 --- a/api/tacticalrmm/core/management/commands/post_update_tasks.py +++ b/api/tacticalrmm/core/management/commands/post_update_tasks.py @@ -1,16 +1,20 @@ +import base64 + from django.core.management.base import BaseCommand -from logs.models import PendingAction -from scripts.models import Script from accounts.models import User +from agents.models import Agent +from autotasks.models import AutomatedTask +from checks.models import Check, CheckHistory +from scripts.models import Script +from tacticalrmm.constants import AGENT_DEFER, ScriptType class Command(BaseCommand): help = "Collection of tasks to run after updating the rmm, after migrations" - def handle(self, *args, **kwargs): - # remove task pending actions. deprecated 4/20/2021 - PendingAction.objects.filter(action_type="taskaction").delete() + def handle(self, *args, **kwargs) -> None: + self.stdout.write("Running post update tasks") # load community scripts into the db Script.load_community_scripts() @@ -20,3 +24,34 @@ def handle(self, *args, **kwargs): for user in User.objects.filter(is_installer_user=True): user.block_dashboard_login = True user.save() + + # convert script base64 field to text field + user_scripts = Script.objects.exclude(script_type=ScriptType.BUILT_IN).filter( + script_body="" + ) + for script in user_scripts: + # decode base64 string + script.script_body = base64.b64decode( + script.code_base64.encode("ascii", "ignore") + ).decode("ascii", "ignore") + # script.hash_script_body() # also saves script + script.save(update_fields=["script_body"]) + + # Remove policy checks and tasks on agents and check + AutomatedTask.objects.filter(managed_by_policy=True).delete() + Check.objects.filter(managed_by_policy=True).delete() + CheckHistory.objects.filter(agent_id=None).delete() + + # set goarch for older windows agents + for agent in Agent.objects.defer(*AGENT_DEFER): + if not agent.goarch: + if agent.arch == "64": + agent.goarch = "amd64" + elif agent.arch == "32": + agent.goarch = "386" + else: + agent.goarch = "amd64" + + agent.save(update_fields=["goarch"]) + + self.stdout.write("Post update tasks finished") diff --git a/api/tacticalrmm/core/management/commands/pre_update_tasks.py b/api/tacticalrmm/core/management/commands/pre_update_tasks.py index 89f61eabb6..7e689beb6b 100644 --- a/api/tacticalrmm/core/management/commands/pre_update_tasks.py +++ b/api/tacticalrmm/core/management/commands/pre_update_tasks.py @@ -1,9 +1,14 @@ +from django.core.management import call_command from django.core.management.base import BaseCommand +from core.utils import clear_entire_cache + class Command(BaseCommand): help = "Collection of tasks to run after updating the rmm, before migrations" def handle(self, *args, **kwargs): - # adding this now for future updates - pass + self.stdout.write(self.style.WARNING("Cleaning the cache")) + clear_entire_cache() + self.stdout.write(self.style.SUCCESS("Cache was cleared!")) + call_command("fix_dupe_agent_customfields") diff --git a/api/tacticalrmm/core/management/commands/reload_nats.py b/api/tacticalrmm/core/management/commands/reload_nats.py index d7da8f1d73..4c83027417 100644 --- a/api/tacticalrmm/core/management/commands/reload_nats.py +++ b/api/tacticalrmm/core/management/commands/reload_nats.py @@ -7,4 +7,6 @@ class Command(BaseCommand): help = "Reload Nats" def handle(self, *args, **kwargs): + self.stdout.write("Reloading NATs configuration...") reload_nats() + self.stdout.write("NATs configuration reloaded") diff --git a/api/tacticalrmm/core/management/commands/run_all_tasks.py b/api/tacticalrmm/core/management/commands/run_all_tasks.py new file mode 100644 index 0000000000..006058ed07 --- /dev/null +++ b/api/tacticalrmm/core/management/commands/run_all_tasks.py @@ -0,0 +1,30 @@ +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, + resolve_alerts_task, + resolve_pending_actions, + sync_scheduled_tasks, +) +from winupdate.tasks import auto_approve_updates_task, check_agent_update_schedule_task + + +class Command(BaseCommand): + help = "Run all celery tasks" + + def handle(self, *args, **kwargs): + auto_self_agent_update_task.delay() + agent_outages_task.delay() + unsnooze_alerts.delay() + cache_db_fields_task.delay() + core_maintenance_tasks.delay() + resolve_pending_actions.delay() + resolve_alerts_task.delay() + sync_scheduled_tasks.delay() + remove_orphaned_win_tasks.delay() + auto_approve_updates_task.delay() + check_agent_update_schedule_task.delay() diff --git a/api/tacticalrmm/core/migrations/0030_coresettings_mesh_device_group.py b/api/tacticalrmm/core/migrations/0030_coresettings_mesh_device_group.py new file mode 100644 index 0000000000..83f8269243 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0030_coresettings_mesh_device_group.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-02-16 21:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0029_alter_coresettings_default_time_zone'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='mesh_device_group', + field=models.CharField(blank=True, default='TacticalRMM', max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0031_coresettings_date_format.py b/api/tacticalrmm/core/migrations/0031_coresettings_date_format.py new file mode 100644 index 0000000000..e422c2ce43 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0031_coresettings_date_format.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-03-17 17:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0030_coresettings_mesh_device_group'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='date_format', + field=models.CharField(blank=True, default='MMM-DD-YYYY - HH:mm', max_length=30), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0032_alter_coresettings_email_alert_recipients_and_more.py b/api/tacticalrmm/core/migrations/0032_alter_coresettings_email_alert_recipients_and_more.py new file mode 100644 index 0000000000..fd9fb9297f --- /dev/null +++ b/api/tacticalrmm/core/migrations/0032_alter_coresettings_email_alert_recipients_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.0.3 on 2022-04-08 03:16 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0031_coresettings_date_format'), + ] + + operations = [ + migrations.AlterField( + model_name='coresettings', + name='email_alert_recipients', + field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(blank=True, max_length=254, null=True), blank=True, default=list, size=None), + ), + migrations.AlterField( + model_name='coresettings', + name='sms_alert_recipients', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, size=None), + ), + migrations.AlterField( + model_name='coresettings', + name='smtp_from_email', + field=models.CharField(blank=True, default='from@example.com', max_length=255), + ), + migrations.AlterField( + model_name='coresettings', + name='smtp_host', + field=models.CharField(blank=True, default='smtp.gmail.com', max_length=255), + ), + migrations.AlterField( + model_name='coresettings', + name='smtp_host_password', + field=models.CharField(blank=True, default='changeme', max_length=255), + ), + migrations.AlterField( + model_name='coresettings', + name='smtp_host_user', + field=models.CharField(blank=True, default='admin@example.com', max_length=255), + ), + migrations.AlterField( + model_name='coresettings', + name='smtp_port', + field=models.PositiveIntegerField(blank=True, default=587), + ), + migrations.AlterField( + model_name='customfield', + name='name', + field=models.CharField(max_length=30), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0033_coresettings_mesh_disable_auto_login.py b/api/tacticalrmm/core/migrations/0033_coresettings_mesh_disable_auto_login.py new file mode 100644 index 0000000000..4a18973d24 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0033_coresettings_mesh_disable_auto_login.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.3 on 2022-04-12 18:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0032_alter_coresettings_email_alert_recipients_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='mesh_disable_auto_login', + field=models.BooleanField(default=False), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0034_alter_customfield_name.py b/api/tacticalrmm/core/migrations/0034_alter_customfield_name.py new file mode 100644 index 0000000000..62e5033db6 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0034_alter_customfield_name.py @@ -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), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0035_alter_coresettings_default_time_zone.py b/api/tacticalrmm/core/migrations/0035_alter_coresettings_default_time_zone.py new file mode 100644 index 0000000000..863249b958 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0035_alter_coresettings_default_time_zone.py @@ -0,0 +1,630 @@ +# Generated by Django 4.1 on 2022-08-24 07:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0034_alter_customfield_name"), + ] + + operations = [ + migrations.AlterField( + model_name="coresettings", + name="default_time_zone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "America/North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="America/Los_Angeles", + max_length=255, + ), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0036_alter_coresettings_default_time_zone.py b/api/tacticalrmm/core/migrations/0036_alter_coresettings_default_time_zone.py new file mode 100644 index 0000000000..9bc67ca553 --- /dev/null +++ b/api/tacticalrmm/core/migrations/0036_alter_coresettings_default_time_zone.py @@ -0,0 +1,630 @@ +# Generated by Django 4.1.7 on 2023-02-28 22:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0035_alter_coresettings_default_time_zone"), + ] + + operations = [ + migrations.AlterField( + model_name="coresettings", + name="default_time_zone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "America/North_Dakota/Center"), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="America/Los_Angeles", + max_length=255, + ), + ), + ] diff --git a/api/tacticalrmm/core/migrations/0037_coresettings_open_ai_model_and_more.py b/api/tacticalrmm/core/migrations/0037_coresettings_open_ai_model_and_more.py new file mode 100644 index 0000000000..f502dd49fc --- /dev/null +++ b/api/tacticalrmm/core/migrations/0037_coresettings_open_ai_model_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2 on 2023-04-09 15:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0036_alter_coresettings_default_time_zone'), + ] + + operations = [ + migrations.AddField( + model_name='coresettings', + name='open_ai_model', + field=models.CharField(blank=True, default='gpt-3.5-turbo', max_length=255), + ), + migrations.AddField( + model_name='coresettings', + name='open_ai_token', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/core/models.py b/api/tacticalrmm/core/models.py index fa0a220499..e0d75faca1 100644 --- a/api/tacticalrmm/core/models.py +++ b/api/tacticalrmm/core/models.py @@ -1,16 +1,28 @@ -import requests import smtplib +from contextlib import suppress from email.message import EmailMessage +from typing import TYPE_CHECKING, List, Optional, cast import pytz +import requests 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 twilio.rest import Client as TwClient from twilio.base.exceptions import TwilioRestException +from twilio.rest import Client as TwClient + +from logs.models import BaseAuditModel, DebugLog +from tacticalrmm.constants import ( + CORESETTINGS_CACHE_KEY, + CustomFieldModel, + CustomFieldType, + DebugLogLevel, +) -from logs.models import BaseAuditModel, DebugLog, LOG_LEVEL_CHOICES +if TYPE_CHECKING: + from alerts.models import AlertTemplate TZ_CHOICES = [(_, _) for _ in pytz.all_timezones] @@ -18,13 +30,11 @@ class CoreSettings(BaseAuditModel): email_alert_recipients = ArrayField( models.EmailField(null=True, blank=True), - null=True, blank=True, default=list, ) sms_alert_recipients = ArrayField( models.CharField(max_length=255, null=True, blank=True), - null=True, blank=True, default=list, ) @@ -32,18 +42,16 @@ class CoreSettings(BaseAuditModel): twilio_account_sid = models.CharField(max_length=255, null=True, blank=True) twilio_auth_token = models.CharField(max_length=255, null=True, blank=True) smtp_from_email = models.CharField( - max_length=255, null=True, blank=True, default="from@example.com" - ) - smtp_host = models.CharField( - max_length=255, null=True, blank=True, default="smtp.gmail.com" + max_length=255, blank=True, default="from@example.com" ) + smtp_host = models.CharField(max_length=255, blank=True, default="smtp.gmail.com") smtp_host_user = models.CharField( - max_length=255, null=True, blank=True, default="admin@example.com" + max_length=255, blank=True, default="admin@example.com" ) smtp_host_password = models.CharField( - max_length=255, null=True, blank=True, default="changeme" + max_length=255, blank=True, default="changeme" ) - smtp_port = models.PositiveIntegerField(default=587, null=True, blank=True) + smtp_port = models.PositiveIntegerField(default=587, blank=True) smtp_requires_auth = models.BooleanField(default=True) default_time_zone = models.CharField( max_length=255, choices=TZ_CHOICES, default="America/Los_Angeles" @@ -55,12 +63,16 @@ 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="") mesh_username = models.CharField(max_length=255, null=True, blank=True, default="") mesh_site = models.CharField(max_length=255, null=True, blank=True, default="") + mesh_device_group = models.CharField( + max_length=255, null=True, blank=True, default="TacticalRMM" + ) + mesh_disable_auto_login = models.BooleanField(default=False) agent_auto_update = models.BooleanField(default=True) workstation_policy = models.ForeignKey( "automation.Policy", @@ -83,40 +95,57 @@ class CoreSettings(BaseAuditModel): null=True, blank=True, ) + date_format = models.CharField( + max_length=30, blank=True, default="MMM-DD-YYYY - HH:mm" + ) + open_ai_token = models.CharField(max_length=255, null=True, blank=True) + open_ai_model = models.CharField( + max_length=255, blank=True, default="gpt-3.5-turbo" + ) - def save(self, *args, **kwargs): + def save(self, *args, **kwargs) -> None: from alerts.tasks import cache_agents_alert_template - from automation.tasks import generate_agent_checks_task + + cache.delete(CORESETTINGS_CACHE_KEY) if not self.pk and CoreSettings.objects.exists(): raise ValidationError("There can only be one CoreSettings instance") # for install script if not self.pk: - try: + with suppress(Exception): self.mesh_site = settings.MESH_SITE - self.mesh_username = settings.MESH_USERNAME + self.mesh_username = settings.MESH_USERNAME.lower() self.mesh_token = settings.MESH_TOKEN_KEY - except: - pass old_settings = type(self).objects.get(pk=self.pk) if self.pk else None super(BaseAuditModel, self).save(*args, **kwargs) - # check if server polcies have changed and initiate task to reapply policies if so - if (old_settings and old_settings.server_policy != self.server_policy) or ( - old_settings and old_settings.workstation_policy != self.workstation_policy - ): - generate_agent_checks_task.delay(all=True, create_tasks=True) + if old_settings: + if ( + old_settings.alert_template != self.alert_template + or old_settings.server_policy != self.server_policy + or old_settings.workstation_policy != self.workstation_policy + ): + cache_agents_alert_template.delay() - if old_settings and old_settings.alert_template != self.alert_template: - cache_agents_alert_template.delay() + if old_settings.workstation_policy != self.workstation_policy: + cache.delete_many_pattern("site_workstation_*") - def __str__(self): + if old_settings.server_policy != self.server_policy: + cache.delete_many_pattern("site_server_*") + + if ( + old_settings.server_policy != self.server_policy + or old_settings.workstation_policy != self.workstation_policy + ): + cache.delete_many_pattern("agent_*") + + def __str__(self) -> str: return "Global Site Settings" @property - def sms_is_configured(self): + def sms_is_configured(self) -> bool: return all( [ self.twilio_auth_token, @@ -126,7 +155,7 @@ def sms_is_configured(self): ) @property - def email_is_configured(self): + def email_is_configured(self) -> bool: # smtp with username/password authentication if ( self.smtp_requires_auth @@ -148,12 +177,18 @@ def email_is_configured(self): return False - def send_mail(self, subject, body, alert_template=None, test=False): + def send_mail( + self, + subject: str, + body: str, + alert_template: "Optional[AlertTemplate]" = None, + test: bool = False, + ) -> 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 False + 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: @@ -164,11 +199,10 @@ def send_mail(self, subject, body, alert_template=None, test=False): # override email recipients if alert_template is passed and is set if alert_template and alert_template.email_recipients: email_recipients = ", ".join(alert_template.email_recipients) + elif self.email_alert_recipients: + email_recipients = ", ".join(cast(List[str], self.email_alert_recipients)) else: - email_recipients = ", ".join(self.email_alert_recipients) - - if not email_recipients: - 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() @@ -181,7 +215,10 @@ def send_mail(self, subject, body, alert_template=None, test=False): if self.smtp_requires_auth: server.ehlo() server.starttls() - server.login(self.smtp_host_user, self.smtp_host_password) + server.login( + self.smtp_host_user, + self.smtp_host_password, + ) server.send_message(msg) server.quit() else: @@ -192,22 +229,29 @@ def send_mail(self, subject, body, alert_template=None, test=False): except Exception as e: DebugLog.error(message=f"Sending email failed with error: {e}") if test: - return str(e) - else: - return True + return str(e), False + + if test: + return "Email test ok!", True - def send_sms(self, body, alert_template=None, test=False): + return "ok", True + + def send_sms( + self, + body: str, + alert_template: "Optional[AlertTemplate]" = None, + test: bool = False, + ) -> 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: text_recipients = alert_template.text_recipients + elif self.sms_alert_recipients: + text_recipients = cast(List[str], self.sms_alert_recipients) else: - text_recipients = self.sms_alert_recipients - - if not text_recipients: - 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: @@ -216,9 +260,12 @@ def send_sms(self, body, alert_template=None, test=False): except TwilioRestException as e: DebugLog.error(message=f"SMS failed to send: {e}") if test: - return str(e) + return str(e), False + + if test: + return "SMS Test sent successfully!", True - return True + return "ok", True @staticmethod def serialize(core): @@ -228,30 +275,19 @@ def serialize(core): return CoreSerializer(core).data -FIELD_TYPE_CHOICES = ( - ("text", "Text"), - ("number", "Number"), - ("single", "Single"), - ("multiple", "Multiple"), - ("checkbox", "Checkbox"), - ("datetime", "DateTime"), -) - -MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent")) - - class CustomField(BaseAuditModel): - order = models.PositiveIntegerField(default=0) - model = models.CharField(max_length=25, choices=MODEL_CHOICES) - type = models.CharField(max_length=25, choices=FIELD_TYPE_CHOICES, default="text") + model = models.CharField(max_length=25, choices=CustomFieldModel.choices) + type = models.CharField( + max_length=25, choices=CustomFieldType.choices, default=CustomFieldType.TEXT + ) options = ArrayField( models.CharField(max_length=255, null=True, blank=True), null=True, blank=True, default=list, ) - name = models.TextField(null=True, blank=True) + 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) @@ -266,7 +302,7 @@ class CustomField(BaseAuditModel): class Meta: unique_together = (("model", "name"),) - def __str__(self): + def __str__(self) -> str: return self.name @staticmethod @@ -277,12 +313,12 @@ def serialize(field): @property def default_value(self): - if self.type == "multiple": + if self.type == CustomFieldType.MULTIPLE: return self.default_values_multiple - elif self.type == "checkbox": + elif self.type == CustomFieldType.CHECKBOX: return self.default_value_bool - else: - return self.default_value_string + + return self.default_value_string def get_or_create_field_value(self, instance): from agents.models import Agent, AgentCustomField @@ -306,7 +342,7 @@ def get_or_create_field_value(self, instance): class CodeSignToken(models.Model): - token = models.CharField(max_length=255, null=True, blank=True) + token: str = models.CharField(max_length=255, null=True, blank=True) def save(self, *args, **kwargs): if not self.pk and CodeSignToken.objects.exists(): @@ -319,26 +355,35 @@ def is_valid(self) -> bool: if not self.token: return False - errors = [] - for url in settings.EXE_GEN_URLS: - try: - r = requests.post( - f"{url}/api/v1/checktoken", - json={"token": self.token}, - headers={"Content-type": "application/json"}, - timeout=15, - ) - except Exception as e: - errors.append(str(e)) - else: - errors = [] - break - - if errors: + try: + r = requests.post( + settings.CHECK_TOKEN_URL, + json={"token": self.token, "api": settings.ALLOWED_HOSTS[0]}, + headers={"Content-type": "application/json"}, + timeout=15, + ) + except: return False return r.status_code == 200 + @property + def is_expired(self) -> bool: + if not self.token: + return False + + try: + r = requests.post( + settings.CHECK_TOKEN_URL, + json={"token": self.token, "api": settings.ALLOWED_HOSTS[0]}, + headers={"Content-type": "application/json"}, + timeout=15, + ) + except: + return False + + return r.status_code == 401 + def __str__(self): return "Code signing token" @@ -405,7 +450,6 @@ def serialize(action): ) timeout = models.PositiveIntegerField(default=120) retcode = models.IntegerField(null=True, blank=True) - retvalue = models.TextField(null=True, blank=True) stdout = models.TextField(null=True, blank=True) stderr = models.TextField(null=True, blank=True) execution_time = models.CharField(max_length=100, default="0.0000") diff --git a/api/tacticalrmm/core/permissions.py b/api/tacticalrmm/core/permissions.py index 86293c5ddf..19bd294a85 100644 --- a/api/tacticalrmm/core/permissions.py +++ b/api/tacticalrmm/core/permissions.py @@ -4,31 +4,31 @@ class CoreSettingsPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_view_core_settings") - else: - return _has_perm(r, "can_edit_core_settings") + + return _has_perm(r, "can_edit_core_settings") class URLActionPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_run_urlactions") class ServerMaintPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_do_server_maint") class CodeSignPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: return _has_perm(r, "can_code_sign") class CustomFieldPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_view_customfields") - else: - return _has_perm(r, "can_manage_customfields") + + return _has_perm(r, "can_manage_customfields") diff --git a/api/tacticalrmm/core/serializers.py b/api/tacticalrmm/core/serializers.py index 32034fc8ab..f9001146aa 100644 --- a/api/tacticalrmm/core/serializers.py +++ b/api/tacticalrmm/core/serializers.py @@ -5,7 +5,6 @@ class CoreSettingsSerializer(serializers.ModelSerializer): - all_timezones = serializers.SerializerMethodField("all_time_zones") def all_time_zones(self, obj): diff --git a/api/tacticalrmm/core/tasks.py b/api/tacticalrmm/core/tasks.py index 6d1781b088..6760c2114c 100644 --- a/api/tacticalrmm/core/tasks.py +++ b/api/tacticalrmm/core/tasks.py @@ -1,69 +1,254 @@ -import pytz +import time +from typing import TYPE_CHECKING, Any + +from django.conf import settings +from django.db.models import Prefetch from django.utils import timezone as djangotime +from packaging import version as pyver -from autotasks.models import AutomatedTask -from autotasks.tasks import delete_win_task_schedule -from checks.tasks import prune_check_history +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 core.models import CoreSettings -from logs.tasks import prune_debug_log, prune_audit_log +from autotasks.models import AutomatedTask, TaskResult +from checks.models import Check, CheckResult +from checks.tasks import prune_check_history +from clients.models import Client, Site +from core.utils import get_core_settings +from logs.models import PendingAction +from logs.tasks import prune_audit_log, prune_debug_log from tacticalrmm.celery import app -from tacticalrmm.utils import AGENT_DEFER +from tacticalrmm.constants import ( + AGENT_DEFER, + AGENT_STATUS_ONLINE, + AGENT_STATUS_OVERDUE, + RESOLVE_ALERTS_LOCK, + SYNC_SCHED_TASK_LOCK, + AlertSeverity, + AlertType, + PAAction, + PAStatus, + TaskStatus, + TaskSyncStatus, +) +from tacticalrmm.helpers import rand_range +from tacticalrmm.utils import DjangoConnectionThreadPoolExecutor, redis_lock +if TYPE_CHECKING: + from django.db.models import QuerySet -@app.task -def core_maintenance_tasks(): - # cleanup expired runonce tasks - tasks = AutomatedTask.objects.filter( - task_type="runonce", - remove_if_not_scheduled=True, - ).exclude(last_run=None) - - for task in tasks: - agent_tz = pytz.timezone(task.agent.timezone) - task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc) - now = djangotime.now() - if now > task_time_utc: - delete_win_task_schedule.delay(task.pk) +@app.task +def core_maintenance_tasks() -> None: + AutomatedTask.objects.filter( + remove_if_not_scheduled=True, expire_date__lt=djangotime.now() + ).delete() - core = CoreSettings.objects.first() + core = get_core_settings() # remove old CheckHistory data - if core.check_history_prune_days > 0: # type: ignore - prune_check_history.delay(core.check_history_prune_days) # type: ignore + if core.check_history_prune_days > 0: + prune_check_history.delay(core.check_history_prune_days) # remove old resolved alerts - if core.resolved_alerts_prune_days > 0: # type: ignore - prune_resolved_alerts.delay(core.resolved_alerts_prune_days) # type: ignore + if core.resolved_alerts_prune_days > 0: + prune_resolved_alerts.delay(core.resolved_alerts_prune_days) # remove old agent history - if core.agent_history_prune_days > 0: # type: ignore - prune_agent_history.delay(core.agent_history_prune_days) # type: ignore + if core.agent_history_prune_days > 0: + prune_agent_history.delay(core.agent_history_prune_days) # remove old debug logs - if core.debug_log_prune_days > 0: # type: ignore - prune_debug_log.delay(core.debug_log_prune_days) # type: ignore + if core.debug_log_prune_days > 0: + prune_debug_log.delay(core.debug_log_prune_days) # remove old audit logs - if core.audit_log_prune_days > 0: # type: ignore - prune_audit_log.delay(core.audit_log_prune_days) # type: ignore + if core.audit_log_prune_days > 0: + prune_audit_log.delay(core.audit_log_prune_days) # clear faults - if core.clear_faults_days > 0: # type: ignore - clear_faults_task.delay(core.clear_faults_days) # type: ignore + if core.clear_faults_days > 0: + clear_faults_task.delay(core.clear_faults_days) @app.task -def cache_db_fields_task(): - from agents.models import Agent - - for agent in Agent.objects.defer(*AGENT_DEFER): - agent.pending_actions_count = agent.pendingactions.filter( - status="pending" - ).count() - agent.has_patches_pending = ( - agent.winupdates.filter(action="approve").filter(installed=False).exists() +def resolve_pending_actions() -> None: + # change agent update pending status to completed if agent has just updated + actions: "QuerySet[PendingAction]" = ( + PendingAction.objects.select_related("agent") + .defer("agent__services", "agent__wmi_detail") + .filter(action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING) + ) + + to_update: list[int] = [ + action.id + for action in actions + if pyver.parse(action.agent.version) == pyver.parse(settings.LATEST_AGENT_VER) + and action.agent.status == AGENT_STATUS_ONLINE + ] + + PendingAction.objects.filter(pk__in=to_update).update(status=PAStatus.COMPLETED) + + +def _get_agent_qs() -> "QuerySet[Agent]": + qs: "QuerySet[Agent]" = ( + Agent.objects.defer(*AGENT_DEFER) + .select_related( + "site__server_policy", + "site__workstation_policy", + "site__client__server_policy", + "site__client__workstation_policy", + "policy", + "policy__alert_template", + "alert_template", + ) + .prefetch_related( + Prefetch( + "agentchecks", + queryset=Check.objects.select_related("script"), + ), + Prefetch( + "checkresults", + queryset=CheckResult.objects.select_related("assigned_check"), + ), + Prefetch( + "taskresults", + queryset=TaskResult.objects.select_related("task"), + ), + "autotasks", ) - agent.save(update_fields=["pending_actions_count", "has_patches_pending"]) + ) + return qs + + +@app.task(bind=True) +def resolve_alerts_task(self) -> str: + with redis_lock(RESOLVE_ALERTS_LOCK, self.app.oid) as acquired: + if not acquired: + return f"{self.app.oid} still running" + + # TODO rework this to not use an agent queryset, use Alerts + for agent in _get_agent_qs(): + if ( + pyver.parse(agent.version) >= pyver.parse("1.6.0") + and agent.status == AGENT_STATUS_ONLINE + ): + # handles any alerting actions + if Alert.objects.filter( + alert_type=AlertType.AVAILABILITY, agent=agent, resolved=False + ).exists(): + Alert.handle_alert_resolve(agent) + + return "completed" + + +@app.task(bind=True) +def sync_scheduled_tasks(self) -> str: + with redis_lock(SYNC_SCHED_TASK_LOCK, self.app.oid) as acquired: + if not acquired: + return f"{self.app.oid} still running" + + task_actions = [] # list of tuples + for agent in _get_agent_qs(): + if ( + pyver.parse(agent.version) >= pyver.parse("1.6.0") + and agent.status == AGENT_STATUS_ONLINE + ): + # create a list of tasks to be synced so we can run them in parallel later with thread pool executor + for task in agent.get_tasks_with_policies(): + agent_obj = agent if task.policy else None + + # policy tasks will be an empty dict on initial + if (not task.task_result) or ( + isinstance(task.task_result, TaskResult) + and task.task_result.sync_status == TaskSyncStatus.INITIAL + ): + task_actions.append(("create", task.id, agent_obj)) + elif ( + isinstance(task.task_result, TaskResult) + and task.task_result.sync_status + == TaskSyncStatus.PENDING_DELETION + ): + task_actions.append(("delete", task.id, agent_obj)) + elif ( + isinstance(task.task_result, TaskResult) + and task.task_result.sync_status == TaskSyncStatus.NOT_SYNCED + ): + task_actions.append(("modify", task.id, agent_obj)) + + def _handle_task(actions: tuple[str, int, Any]) -> None: + time.sleep(rand_range(50, 600)) + task: "AutomatedTask" = AutomatedTask.objects.get(id=actions[1]) + if actions[0] == "create": + task.create_task_on_agent(agent=actions[2]) + elif actions[0] == "modify": + task.modify_task_on_agent(agent=actions[2]) + elif actions[0] == "delete": + task.delete_task_on_agent(agent=actions[2]) + + # TODO this is a janky hack + # Rework this with asyncio. Need to rewrite all sync db operations with django's new async api + with DjangoConnectionThreadPoolExecutor(max_workers=50) as executor: + executor.map(_handle_task, task_actions) + + return "completed" + + +def _get_failing_data(agents: "QuerySet[Agent]") -> dict[str, bool]: + data = {"error": False, "warning": False} + for agent in agents: + if agent.maintenance_mode: + break + + if ( + agent.overdue_email_alert + or agent.overdue_text_alert + or agent.overdue_dashboard_alert + ): + if agent.status == AGENT_STATUS_OVERDUE: + data["error"] = True + break + + if agent.checks["has_failing_checks"]: + if agent.checks["warning"]: + data["warning"] = True + + if agent.checks["failing"]: + data["error"] = True + break + + if not data["error"] and not data["warning"]: + for task in agent.get_tasks_with_policies(): + if data["error"] and data["warning"]: + break + elif not isinstance(task.task_result, TaskResult): + continue + elif ( + not data["error"] + and task.task_result.status == TaskStatus.FAILING + and task.alert_severity == AlertSeverity.ERROR + ): + data["error"] = True + elif ( + not data["warning"] + and task.task_result.status == TaskStatus.FAILING + and task.alert_severity == AlertSeverity.WARNING + ): + data["warning"] = True + + return data + + +@app.task +def cache_db_fields_task() -> None: + qs = _get_agent_qs() + # update client/site failing check fields and agent counts + for site in Site.objects.all(): + agents = qs.filter(site=site) + site.failing_checks = _get_failing_data(agents) + site.save(update_fields=["failing_checks"]) + + for client in Client.objects.all(): + agents = qs.filter(site__client=client) + client.failing_checks = _get_failing_data(agents) + client.save(update_fields=["failing_checks"]) diff --git a/api/tacticalrmm/core/tests.py b/api/tacticalrmm/core/tests.py index f42a619f88..ac664eb9aa 100644 --- a/api/tacticalrmm/core/tests.py +++ b/api/tacticalrmm/core/tests.py @@ -3,14 +3,30 @@ import requests from channels.db import database_sync_to_async from channels.testing import WebsocketCommunicator -from model_bakery import baker +# from django.conf import settings +from django.core.management import call_command +from django.test import override_settings +from model_bakery import baker +from rest_framework.authtoken.models import Token + +# from agents.models import Agent +from core.utils import get_core_settings, get_meshagent_url + +# from logs.models import PendingAction +from tacticalrmm.constants import ( + CONFIG_MGMT_CMDS, + CustomFieldModel, + MeshAgentIdent, + # PAAction, + # PAStatus, +) from tacticalrmm.test import TacticalTestCase from .consumers import DashInfo -from .models import CoreSettings, CustomField, GlobalKVStore, URLAction +from .models import CustomField, GlobalKVStore, URLAction from .serializers import CustomFieldSerializer, KeyStoreSerializer, URLActionSerializer -from .tasks import core_maintenance_tasks +from .tasks import core_maintenance_tasks # , resolve_pending_actions class TestCodeSign(TacticalTestCase): @@ -34,6 +50,12 @@ def test_edit_codesign_timeout(self, mock_post): self.check_not_authenticated("patch", self.url) + def test_delete_codesign(self): + r = self.client.delete(self.url) + self.assertEqual(r.status_code, 200) + + self.check_not_authenticated("delete", self.url) + class TestConsumers(TacticalTestCase): def setUp(self): @@ -42,8 +64,6 @@ def setUp(self): @database_sync_to_async def get_token(self): - from rest_framework.authtoken.models import Token - token = Token.objects.create(user=self.john) return token.key @@ -64,8 +84,8 @@ def setUp(self): self.authenticate() def test_core_maintenance_tasks(self): - task = core_maintenance_tasks.s().apply() - self.assertEqual(task.state, "SUCCESS") + core_maintenance_tasks() + self.assertTrue(True) def test_dashboard_info(self): url = "/core/dashinfo/" @@ -88,12 +108,11 @@ def test_get_core_settings(self): self.check_not_authenticated("get", url) - @patch("automation.tasks.generate_agent_checks_task.delay") - def test_edit_coresettings(self, generate_agent_checks_task): + def test_edit_coresettings(self): url = "/core/settings/" # setup - policies = baker.make("automation.Policy", _quantity=2) + baker.make("automation.Policy", _quantity=2) # test normal request data = { "smtp_from_email": "newexample@example.com", @@ -101,38 +120,8 @@ def test_edit_coresettings(self, generate_agent_checks_task): } r = self.client.put(url, data) self.assertEqual(r.status_code, 200) - self.assertEqual( - CoreSettings.objects.first().smtp_from_email, data["smtp_from_email"] - ) - self.assertEqual(CoreSettings.objects.first().mesh_token, data["mesh_token"]) - - generate_agent_checks_task.assert_not_called() - - # test adding policy - data = { - "workstation_policy": policies[0].id, # type: ignore - "server_policy": policies[1].id, # type: ignore - } - r = self.client.put(url, data) - self.assertEqual(r.status_code, 200) - self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id) # type: ignore - self.assertEqual( - CoreSettings.objects.first().workstation_policy.id, policies[0].id # type: ignore - ) - - generate_agent_checks_task.assert_called_once() - - generate_agent_checks_task.reset_mock() - - # test remove policy - data = { - "workstation_policy": "", - } - r = self.client.put(url, data) - self.assertEqual(r.status_code, 200) - self.assertEqual(CoreSettings.objects.first().workstation_policy, None) - - self.assertEqual(generate_agent_checks_task.call_count, 1) + self.assertEqual(get_core_settings().smtp_from_email, data["smtp_from_email"]) + self.assertEqual(get_core_settings().mesh_token, data["mesh_token"]) self.check_not_authenticated("put", url) @@ -141,7 +130,7 @@ def test_edit_coresettings(self, generate_agent_checks_task): def test_ui_maintenance_actions(self, remove_orphaned_win_tasks, reload_nats): url = "/core/servermaintenance/" - agents = baker.make_recipe("agents.online_agent", _quantity=3) + baker.make_recipe("agents.online_agent", _quantity=3) # test with empty data r = self.client.post(url, {}) @@ -189,8 +178,8 @@ def test_get_custom_fields(self): r = self.client.get(url) serializer = CustomFieldSerializer(custom_fields, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 2) # type: ignore - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(len(r.data), 2) + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -198,7 +187,7 @@ def test_get_custom_fields_by_model(self): url = "/core/customfields/" # setup - custom_fields = baker.make("core.CustomField", model="agent", _quantity=5) + baker.make("core.CustomField", model=CustomFieldModel.AGENT, _quantity=5) baker.make("core.CustomField", model="client", _quantity=5) # will error if request invalid @@ -207,10 +196,8 @@ def test_get_custom_fields_by_model(self): data = {"model": "agent"} r = self.client.patch(url, data) - serializer = CustomFieldSerializer(custom_fields, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 5) # type: ignore - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(len(r.data), 5) self.check_not_authenticated("patch", url) @@ -231,11 +218,11 @@ def test_get_custom_field(self): r = self.client.get("/core/customfields/500/") self.assertEqual(r.status_code, 404) - url = f"/core/customfields/{custom_field.id}/" # type: ignore + url = f"/core/customfields/{custom_field.id}/" r = self.client.get(url) serializer = CustomFieldSerializer(custom_field) self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -247,12 +234,12 @@ def test_update_custom_field(self): r = self.client.put("/core/customfields/500/") self.assertEqual(r.status_code, 404) - url = f"/core/customfields/{custom_field.id}/" # type: ignore + url = f"/core/customfields/{custom_field.id}/" data = {"type": "single", "options": ["ione", "two", "three"]} r = self.client.put(url, data) self.assertEqual(r.status_code, 200) - new_field = CustomField.objects.get(pk=custom_field.id) # type: ignore + new_field = CustomField.objects.get(pk=custom_field.id) self.assertEqual(new_field.type, data["type"]) self.assertEqual(new_field.options, data["options"]) @@ -266,11 +253,11 @@ def test_delete_custom_field(self): r = self.client.delete("/core/customfields/500/") self.assertEqual(r.status_code, 404) - url = f"/core/customfields/{custom_field.id}/" # type: ignore + url = f"/core/customfields/{custom_field.id}/" r = self.client.delete(url) self.assertEqual(r.status_code, 200) - self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) # type: ignore + self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) self.check_not_authenticated("delete", url) @@ -283,8 +270,8 @@ def test_get_keystore(self): r = self.client.get(url) serializer = KeyStoreSerializer(keys, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 2) # type: ignore - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(len(r.data), 2) + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -305,12 +292,12 @@ def test_update_keystore(self): r = self.client.put("/core/keystore/500/") self.assertEqual(r.status_code, 404) - url = f"/core/keystore/{key.id}/" # type: ignore + url = f"/core/keystore/{key.id}/" data = {"name": "test", "value": "text"} r = self.client.put(url, data) self.assertEqual(r.status_code, 200) - new_key = GlobalKVStore.objects.get(pk=key.id) # type: ignore + new_key = GlobalKVStore.objects.get(pk=key.id) self.assertEqual(new_key.name, data["name"]) self.assertEqual(new_key.value, data["value"]) @@ -324,11 +311,11 @@ def test_delete_keystore(self): r = self.client.delete("/core/keystore/500/") self.assertEqual(r.status_code, 404) - url = f"/core/keystore/{key.id}/" # type: ignore + url = f"/core/keystore/{key.id}/" r = self.client.delete(url) self.assertEqual(r.status_code, 200) - self.assertFalse(GlobalKVStore.objects.filter(pk=key.id).exists()) # type: ignore + self.assertFalse(GlobalKVStore.objects.filter(pk=key.id).exists()) self.check_not_authenticated("delete", url) @@ -341,8 +328,8 @@ def test_get_urlaction(self): r = self.client.get(url) serializer = URLActionSerializer(action, many=True) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 2) # type: ignore - self.assertEqual(r.data, serializer.data) # type: ignore + self.assertEqual(len(r.data), 2) + self.assertEqual(r.data, serializer.data) self.check_not_authenticated("get", url) @@ -363,12 +350,12 @@ def test_update_urlaction(self): r = self.client.put("/core/urlaction/500/") self.assertEqual(r.status_code, 404) - url = f"/core/urlaction/{action.id}/" # type: ignore + url = f"/core/urlaction/{action.id}/" data = {"name": "test", "pattern": "text"} r = self.client.put(url, data) self.assertEqual(r.status_code, 200) - new_action = URLAction.objects.get(pk=action.id) # type: ignore + new_action = URLAction.objects.get(pk=action.id) self.assertEqual(new_action.name, data["name"]) self.assertEqual(new_action.pattern, data["pattern"]) @@ -382,11 +369,11 @@ def test_delete_urlaction(self): r = self.client.delete("/core/urlaction/500/") self.assertEqual(r.status_code, 404) - url = f"/core/urlaction/{action.id}/" # type: ignore + url = f"/core/urlaction/{action.id}/" r = self.client.delete(url) self.assertEqual(r.status_code, 200) - self.assertFalse(URLAction.objects.filter(pk=action.id).exists()) # type: ignore + self.assertFalse(URLAction.objects.filter(pk=action.id).exists()) self.check_not_authenticated("delete", url) @@ -407,19 +394,135 @@ def test_run_url_action(self): r = self.client.patch(url, {"agent_id": 500, "action": 500}) self.assertEqual(r.status_code, 404) - data = {"agent_id": agent.agent_id, "action": action.id} # type: ignore + data = {"agent_id": agent.agent_id, "action": action.id} r = self.client.patch(url, data) self.assertEqual(r.status_code, 200) self.assertEqual( - r.data, # type: ignore + r.data, f"https://remote.example.com/connect?globalstore=value%20with%20space&client_name={agent.client.name}&site%20id={agent.site.id}&agent_id=123123-assdss4s-343-sds545-45dfdf%7CDESKTOP", ) self.check_not_authenticated("patch", url) + def test_clear_cache(self): + url = "/core/clearcache/" + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + + 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) + + # resolve_pending_actions() + + # 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 TestCoreMgmtCommands(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + + def test_get_config(self): + for cmd in CONFIG_MGMT_CMDS: + call_command("get_config", cmd) + class TestCorePermissions(TacticalTestCase): def setUp(self): - self.client_setup() + self.setup_client() self.setup_coresettings() + + +class TestCoreUtils(TacticalTestCase): + def setUp(self): + self.setup_coresettings() + + def test_get_meshagent_url_standard(self): + r = get_meshagent_url( + ident=MeshAgentIdent.DARWIN_UNIVERSAL, + plat="darwin", + mesh_site="https://mesh.example.com", + mesh_device_id="abc123", + ) + self.assertEqual( + r, + "https://mesh.example.com/meshagents?id=abc123&installflags=2&meshinstall=10005", + ) + + r = get_meshagent_url( + ident=MeshAgentIdent.WIN64, + plat="windows", + mesh_site="https://mesh.example.com", + mesh_device_id="abc123", + ) + self.assertEqual( + r, + "https://mesh.example.com/meshagents?id=4&meshid=abc123&installflags=0", + ) + + @override_settings(DOCKER_BUILD=True) + @override_settings(MESH_WS_URL="ws://tactical-meshcentral:4443") + def test_get_meshagent_url_docker(self): + r = get_meshagent_url( + ident=MeshAgentIdent.DARWIN_UNIVERSAL, + plat="darwin", + mesh_site="https://mesh.example.com", + mesh_device_id="abc123", + ) + self.assertEqual( + r, + "http://tactical-meshcentral:4443/meshagents?id=abc123&installflags=2&meshinstall=10005", + ) + + r = get_meshagent_url( + ident=MeshAgentIdent.WIN64, + plat="windows", + mesh_site="https://mesh.example.com", + mesh_device_id="abc123", + ) + self.assertEqual( + r, + "http://tactical-meshcentral:4443/meshagents?id=4&meshid=abc123&installflags=0", + ) + + @override_settings(TRMM_INSECURE=True) + def test_get_meshagent_url_insecure(self): + r = get_meshagent_url( + ident=MeshAgentIdent.DARWIN_UNIVERSAL, + plat="darwin", + mesh_site="https://mesh.example.com", + mesh_device_id="abc123", + ) + self.assertEqual( + r, + "http://mesh.example.com:4430/meshagents?id=abc123&installflags=2&meshinstall=10005", + ) + + r = get_meshagent_url( + ident=MeshAgentIdent.WIN64, + plat="windows", + mesh_site="https://mesh.example.com", + mesh_device_id="abc123", + ) + self.assertEqual( + r, + "http://mesh.example.com:4430/meshagents?id=4&meshid=abc123&installflags=0", + ) diff --git a/api/tacticalrmm/core/urls.py b/api/tacticalrmm/core/urls.py index 6ca270f649..fcc4274cac 100644 --- a/api/tacticalrmm/core/urls.py +++ b/api/tacticalrmm/core/urls.py @@ -3,7 +3,6 @@ from . import views urlpatterns = [ - path("uploadmesh/", views.UploadMeshAgent.as_view()), path("settings/", views.GetEditCoreSettings.as_view()), path("version/", views.version), path("emailtest/", views.email_test), @@ -18,4 +17,7 @@ path("urlaction//", views.UpdateDeleteURLAction.as_view()), path("urlaction/run/", views.RunURLAction.as_view()), path("smstest/", views.TwilioSMSTest.as_view()), + path("clearcache/", views.clear_cache), + path("status/", views.status), + path("openai/generate/", views.OpenAICodeCompletion.as_view()), ] diff --git a/api/tacticalrmm/core/utils.py b/api/tacticalrmm/core/utils.py new file mode 100644 index 0000000000..aeb06e61df --- /dev/null +++ b/api/tacticalrmm/core/utils.py @@ -0,0 +1,206 @@ +import json +import subprocess +import tempfile +import urllib.parse +from base64 import b64encode +from typing import TYPE_CHECKING, Optional, cast + +import requests +import websockets +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 ( + AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX, + CORESETTINGS_CACHE_KEY, + ROLE_CACHE_PREFIX, + AgentPlat, + MeshAgentIdent, +) + +if TYPE_CHECKING: + from core.models import CoreSettings + + +class CoreSettingsNotFound(Exception): + pass + + +def clear_entire_cache() -> None: + cache.delete_many_pattern(f"{ROLE_CACHE_PREFIX}*") + cache.delete_many_pattern(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}*") + cache.delete(CORESETTINGS_CACHE_KEY) + cache.delete_many_pattern("site_*") + cache.delete_many_pattern("agent_*") + + +def token_is_valid() -> tuple[str, bool]: + """ + Return type: token: str, is_valid: bool. Token wil be an empty string is not valid. + """ + from core.models import CodeSignToken + + t: "Optional[CodeSignToken]" = CodeSignToken.objects.first() + if not t: + return "", False + + if not t.token: + return "", False + + if t.is_valid: + return t.token, True + + return "", False + + +def token_is_expired() -> bool: + from core.models import CodeSignToken + + t: "CodeSignToken" = CodeSignToken.objects.first() + if not t or not t.token: + return False + + return t.is_expired + + +def get_core_settings() -> "CoreSettings": + from core.models import CORESETTINGS_CACHE_KEY, CoreSettings + + coresettings = cache.get(CORESETTINGS_CACHE_KEY) + + if coresettings and isinstance(coresettings, CoreSettings): + return coresettings + else: + coresettings = CoreSettings.objects.first() + if not coresettings: + raise CoreSettingsNotFound("CoreSettings not found.") + + cache.set(CORESETTINGS_CACHE_KEY, coresettings, 600) + return cast(CoreSettings, coresettings) + + +def get_mesh_ws_url() -> str: + core = get_core_settings() + token = get_auth_token(core.mesh_username, core.mesh_token) + + if settings.DOCKER_BUILD: + uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" + else: + if getattr(settings, "TRMM_INSECURE", False): + site = core.mesh_site.replace("https", "ws") + uri = f"{site}:4430/control.ashx?auth={token}" + else: + site = core.mesh_site.replace("https", "wss") + uri = f"{site}/control.ashx?auth={token}" + + return uri + + +async def get_mesh_device_id(uri: str, device_group: str) -> None: + async with websockets.connect(uri) as ws: + payload = {"action": "meshes", "responseid": "meshctrl"} + await ws.send(json.dumps(payload)) + + async for message in ws: + r = json.loads(message) + if r["action"] == "meshes": + return list(filter(lambda x: x["name"] == device_group, r["meshes"]))[ + 0 + ]["_id"].split("mesh//")[1] + + +def download_mesh_agent(dl_url: str) -> FileResponse: + with tempfile.NamedTemporaryFile(prefix="mesh-", dir=settings.EXE_DIR) as fp: + r = requests.get(dl_url, stream=True, timeout=15) + with open(fp.name, "wb") as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + del r + + return FileResponse(open(fp.name, "rb"), as_attachment=True, filename=fp.name) + + +def _b64_to_hex(h: str) -> str: + return b64encode(bytes.fromhex(h)).decode().replace(r"/", "$").replace(r"+", "@") + + +async def send_command_with_mesh( + cmd: str, uri: str, mesh_node_id: str, shell: int, run_as_user: int +) -> None: + node_id = _b64_to_hex(mesh_node_id) + async with websockets.connect(uri) as ws: + await ws.send( + json.dumps( + { + "action": "runcommands", + "cmds": cmd, + "nodeids": [f"node//{node_id}"], + "runAsUser": run_as_user, + "type": shell, + "responseid": "trmm", + } + ) + ) + + +async def wake_on_lan(*, uri: str, mesh_node_id: str) -> None: + node_id = _b64_to_hex(mesh_node_id) + async with websockets.connect(uri) as ws: + await ws.send( + json.dumps( + { + "action": "wakedevices", + "nodeids": [f"node//{node_id}"], + "responseid": "trmm", + } + ) + ) + + +async def remove_mesh_agent(uri: str, mesh_node_id: str) -> None: + node_id = _b64_to_hex(mesh_node_id) + async with websockets.connect(uri) as ws: + await ws.send( + json.dumps( + { + "action": "removedevices", + "nodeids": [f"node//{node_id}"], + "responseid": "trmm", + } + ) + ) + + +def sysd_svc_is_running(svc: str) -> bool: + cmd = ["systemctl", "is-active", "--quiet", svc] + r = subprocess.run(cmd, capture_output=True) + return not r.returncode + + +def get_meshagent_url( + *, ident: "MeshAgentIdent", plat: str, mesh_site: str, mesh_device_id: str +) -> str: + if settings.DOCKER_BUILD: + base = settings.MESH_WS_URL.replace("ws://", "http://") + elif getattr(settings, "TRMM_INSECURE", False): + base = mesh_site.replace("https", "http") + ":4430" + else: + base = mesh_site + + if plat == AgentPlat.WINDOWS: + params = { + "id": ident, + "meshid": mesh_device_id, + "installflags": 0, + } + else: + params = { + "id": mesh_device_id, + "installflags": 2, + "meshinstall": ident, + } + + return base + "/meshagents?" + urllib.parse.urlencode(params) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index fa68fb6c54..e8e5aa22b9 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -1,21 +1,31 @@ -import os +import json import re +from pathlib import Path +from zoneinfo import ZoneInfo +import psutil +import requests +from cryptography import x509 from django.conf import settings +from django.http import JsonResponse from django.shortcuts import get_object_or_404 -from logs.models import AuditLog -from rest_framework import status +from django.utils import timezone as djangotime +from django.views.decorators.csrf import csrf_exempt from rest_framework.decorators import api_view, permission_classes -from rest_framework.exceptions import ParseError, PermissionDenied -from rest_framework.parsers import FileUploadParser +from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from tacticalrmm.utils import notify_error +from core.decorators import monitoring_view +from core.utils import get_core_settings, sysd_svc_is_running, token_is_valid +from logs.models import AuditLog +from tacticalrmm.constants import AuditActionType, PAStatus +from tacticalrmm.helpers import get_certs, notify_error from tacticalrmm.permissions import ( - _has_perm_on_client, _has_perm_on_agent, + _has_perm_on_client, _has_perm_on_site, ) @@ -23,9 +33,9 @@ from .permissions import ( CodeSignPerms, CoreSettingsPerms, + CustomFieldPerms, ServerMaintPerms, URLActionPerms, - CustomFieldPerms, ) from .serializers import ( CodeSignTokenSerializer, @@ -36,28 +46,6 @@ ) -class UploadMeshAgent(APIView): - permission_classes = [IsAuthenticated, CoreSettingsPerms] - parser_class = (FileUploadParser,) - - def put(self, request, format=None): - if "meshagent" not in request.data and "arch" not in request.data: - raise ParseError("Empty content") - - arch = request.data["arch"] - f = request.data["meshagent"] - mesh_exe = os.path.join( - settings.EXE_DIR, "meshagent.exe" if arch == "64" else "meshagent-x86.exe" - ) - with open(mesh_exe, "wb+") as j: - for chunk in f.chunks(): - j.write(chunk) - - return Response( - "Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED - ) - - class GetEditCoreSettings(APIView): permission_classes = [IsAuthenticated, CoreSettingsPerms] @@ -79,10 +67,20 @@ def version(request): return Response(settings.APP_VER) +@api_view() +def clear_cache(request): + from core.utils import clear_entire_cache + + clear_entire_cache() + return Response("Cache was cleared!") + + @api_view() def dashboard_info(request): - from tacticalrmm.utils import get_latest_trmm_ver + from core.utils import token_is_expired + from tacticalrmm.utils import get_latest_trmm_ver, runcmd_placeholder_text + core_settings = get_core_settings() return Response( { "trmm_version": settings.TRMM_VERSION, @@ -98,7 +96,16 @@ def dashboard_info(request): "client_tree_splitter": request.user.client_tree_splitter, "loading_bar_color": request.user.loading_bar_color, "clear_search_when_switching": request.user.clear_search_when_switching, - "hosted": hasattr(settings, "HOSTED") and settings.HOSTED, + "hosted": getattr(settings, "HOSTED", False), + "date_format": request.user.date_format, + "default_date_format": core_settings.date_format, + "token_is_expired": token_is_expired(), + "open_ai_integration_enabled": bool(core_settings.open_ai_token), + "dash_info_color": request.user.dash_info_color, + "dash_positive_color": request.user.dash_positive_color, + "dash_negative_color": request.user.dash_negative_color, + "dash_warning_color": request.user.dash_warning_color, + "run_cmd_placeholder_text": runcmd_placeholder_text(), } ) @@ -106,15 +113,15 @@ def dashboard_info(request): @api_view(["POST"]) @permission_classes([IsAuthenticated, CoreSettingsPerms]) def email_test(request): - core = CoreSettings.objects.first() - r = core.send_mail( + core = get_core_settings() + + 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"]) @@ -130,17 +137,10 @@ def server_maintenance(request): return Response("Nats configuration was reloaded successfully.") if request.data["action"] == "rm_orphaned_tasks": - from agents.models import Agent from autotasks.tasks import remove_orphaned_win_tasks - agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time") - online = [i for i in agents if i.status == "online"] - for agent in online: - remove_orphaned_win_tasks.delay(agent.pk) - - return Response( - "The task has been initiated. Check the Debug Log in the UI for progress." - ) + remove_orphaned_win_tasks.delay() + return Response("The task has been initiated.") if request.data["action"] == "prune_db": from logs.models import AuditLog, PendingAction @@ -151,12 +151,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() @@ -186,8 +186,8 @@ def patch(self, request): if "model" in request.data.keys(): fields = CustomField.objects.filter(model=request.data["model"]) return Response(CustomFieldSerializer(fields, many=True).data) - else: - return notify_error("The request was invalid") + + return notify_error("The request was invalid") def post(self, request): serializer = CustomFieldSerializer(data=request.data, partial=True) @@ -232,27 +232,19 @@ def get(self, request): def patch(self, request): import requests - errors = [] - for url in settings.EXE_GEN_URLS: - try: - r = requests.post( - f"{url}/api/v1/checktoken", - json={"token": request.data["token"]}, - headers={"Content-type": "application/json"}, - timeout=15, - ) - except Exception as e: - errors.append(str(e)) - else: - errors = [] - break - - if errors: - return notify_error(", ".join(errors)) + try: + r = requests.post( + settings.CHECK_TOKEN_URL, + json={"token": request.data["token"], "api": settings.ALLOWED_HOSTS[0]}, + headers={"Content-type": "application/json"}, + timeout=15, + ) + except Exception as e: + return notify_error(str(e)) - if r.status_code == 400 or r.status_code == 401: # type: ignore - return notify_error(r.json()["ret"]) # type: ignore - elif r.status_code == 200: # type: ignore + if r.status_code in (400, 401): + return notify_error(r.json()["ret"]) + elif r.status_code == 200: t = CodeSignToken.objects.first() if t is None: CodeSignToken.objects.create(token=request.data["token"]) @@ -263,30 +255,29 @@ def patch(self, request): return Response("Token was saved") try: - ret = r.json()["ret"] # type: ignore + ret = r.json()["ret"] except: ret = "Something went wrong" return notify_error(ret) def post(self, request): from agents.models import Agent - from agents.tasks import force_code_sign + from agents.tasks import send_agent_update_task - err = "A valid token must be saved first" - try: - t = CodeSignToken.objects.first().token - except: - return notify_error(err) - - if t is None or t == "": - return notify_error(err) + token, is_valid = token_is_valid() + if not is_valid: + return notify_error("Invalid token") agent_ids: list[str] = list( Agent.objects.only("pk", "agent_id").values_list("agent_id", flat=True) ) - force_code_sign.delay(agent_ids=agent_ids) + send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=True) return Response("Agents will be code signed shortly") + def delete(self, request): + CodeSignToken.objects.all().delete() + return Response("ok") + class GetAddKeyStore(APIView): permission_classes = [IsAuthenticated, CoreSettingsPerms] @@ -409,16 +400,114 @@ class TwilioSMSTest(APIView): permission_classes = [IsAuthenticated, CoreSettingsPerms] def post(self, request): - - core = CoreSettings.objects.first() + core = get_core_settings() if not core.sms_is_configured: return notify_error( "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) + + return Response(msg) + + +@csrf_exempt +@monitoring_view +def status(request): + from agents.models import Agent + from clients.models import Client, Site + + disk_usage: int = round(psutil.disk_usage("/").percent) + mem_usage: int = round(psutil.virtual_memory().percent) + + cert_file, _ = get_certs() + cert_bytes = Path(cert_file).read_bytes() + + cert = x509.load_pem_x509_certificate(cert_bytes) + expires = cert.not_valid_after.replace(tzinfo=ZoneInfo("UTC")) + now = djangotime.now() + delta = expires - now + + ret = { + "version": settings.TRMM_VERSION, + "latest_agent_version": settings.LATEST_AGENT_VER, + "agent_count": Agent.objects.count(), + "client_count": Client.objects.count(), + "site_count": Site.objects.count(), + "disk_usage_percent": disk_usage, + "mem_usage_percent": mem_usage, + "days_until_cert_expires": delta.days, + "cert_expired": delta.days < 0, + } + + if settings.DOCKER_BUILD: + ret["services_running"] = "not available in docker" + else: + ret["services_running"] = { + "django": sysd_svc_is_running("rmm.service"), + "mesh": sysd_svc_is_running("meshcentral.service"), + "daphne": sysd_svc_is_running("daphne.service"), + "celery": sysd_svc_is_running("celery.service"), + "celerybeat": sysd_svc_is_running("celerybeat.service"), + "redis": sysd_svc_is_running("redis-server.service"), + "postgres": sysd_svc_is_running("postgresql.service"), + "mongo": sysd_svc_is_running("mongod.service"), + "nats": sysd_svc_is_running("nats.service"), + "nats-api": sysd_svc_is_running("nats-api.service"), + "nginx": sysd_svc_is_running("nginx.service"), + } + return JsonResponse(ret, json_dumps_params={"indent": 2}) + + +class OpenAICodeCompletion(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request: Request) -> Response: + settings = get_core_settings() + + if not settings.open_ai_token: + return notify_error( + "Open AI API Key not found. Open Global Settings > Open AI." + ) + + if not request.data["prompt"]: + return notify_error("Not prompt field found") + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {settings.open_ai_token}", + } + + data = { + "messages": [ + { + "role": "user", + "content": request.data["prompt"], + }, + ], + "model": settings.open_ai_model, + "temperature": 0.5, + "max_tokens": 1000, + "n": 1, + "stop": None, + } - if not isinstance(r, bool) and isinstance(r, str): - return notify_error(r) + try: + response = requests.post( + "https://api.openai.com/v1/chat/completions", + headers=headers, + data=json.dumps(data), + ) + except Exception as e: + return notify_error(str(e)) + + response_data = json.loads(response.text) + + if "error" in response_data: + return notify_error( + f"The Open AI API returned an error: {response_data['error']['message']}" + ) - return Response("SMS Test sent successfully!") + return Response(response_data["choices"][0]["message"]["content"]) diff --git a/api/tacticalrmm/logs/__init__.py b/api/tacticalrmm/logs/__init__.py index 9a7deba39d..e69de29bb2 100644 --- a/api/tacticalrmm/logs/__init__.py +++ b/api/tacticalrmm/logs/__init__.py @@ -1 +0,0 @@ -default_app_config = "logs.apps.LogsConfig" diff --git a/api/tacticalrmm/logs/admin.py b/api/tacticalrmm/logs/admin.py index 00320fa20f..ecfd455244 100644 --- a/api/tacticalrmm/logs/admin.py +++ b/api/tacticalrmm/logs/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import AuditLog, PendingAction, DebugLog +from .models import AuditLog, DebugLog, PendingAction admin.site.register(PendingAction) admin.site.register(AuditLog) diff --git a/api/tacticalrmm/logs/apps.py b/api/tacticalrmm/logs/apps.py index ac75a42914..9e148fad27 100644 --- a/api/tacticalrmm/logs/apps.py +++ b/api/tacticalrmm/logs/apps.py @@ -5,4 +5,4 @@ class LogsConfig(AppConfig): name = "logs" def ready(self): - from . import signals + from . import signals # noqa diff --git a/api/tacticalrmm/logs/baker_recipes.py b/api/tacticalrmm/logs/baker_recipes.py index 06a55f9d08..3dbf6947cc 100644 --- a/api/tacticalrmm/logs/baker_recipes.py +++ b/api/tacticalrmm/logs/baker_recipes.py @@ -2,27 +2,48 @@ 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, +) diff --git a/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py b/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py index 62f2f7f2ee..69dbcf335b 100644 --- a/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py +++ b/api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py @@ -1,8 +1,8 @@ # Generated by Django 3.2.1 on 2021-06-14 18:35 -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/api/tacticalrmm/logs/migrations/0022_auto_20211105_0158.py b/api/tacticalrmm/logs/migrations/0022_auto_20211105_0158.py index 5a7c0448e1..f954356bc2 100644 --- a/api/tacticalrmm/logs/migrations/0022_auto_20211105_0158.py +++ b/api/tacticalrmm/logs/migrations/0022_auto_20211105_0158.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.6 on 2021-11-05 01:58 -from django.db import migrations from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations def update_agent_field(apps, schema_editor): diff --git a/api/tacticalrmm/logs/migrations/0023_alter_pendingaction_action_type.py b/api/tacticalrmm/logs/migrations/0023_alter_pendingaction_action_type.py new file mode 100644 index 0000000000..3d39fa51d1 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0023_alter_pendingaction_action_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-02-27 05:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('logs', '0022_auto_20211105_0158'), + ] + + operations = [ + migrations.AlterField( + model_name='pendingaction', + name='action_type', + field=models.CharField(blank=True, 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')], max_length=255, null=True), + ), + ] diff --git a/api/tacticalrmm/logs/migrations/0024_remove_pendingaction_cancelable_and_more.py b/api/tacticalrmm/logs/migrations/0024_remove_pendingaction_cancelable_and_more.py new file mode 100644 index 0000000000..a43e71eb36 --- /dev/null +++ b/api/tacticalrmm/logs/migrations/0024_remove_pendingaction_cancelable_and_more.py @@ -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', + ), + ] diff --git a/api/tacticalrmm/logs/models.py b/api/tacticalrmm/logs/models.py index 9bb35b03d1..07aa8d2a40 100644 --- a/api/tacticalrmm/logs/models.py +++ b/api/tacticalrmm/logs/models.py @@ -1,68 +1,28 @@ -import datetime as dt 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 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 + -def get_debug_level(): - from core.models import CoreSettings - - return CoreSettings.objects.first().agent_debug_level # type: ignore - - -ACTION_TYPE_CHOICES = [ - ("schedreboot", "Scheduled Reboot"), - ("taskaction", "Scheduled Task Action"), # deprecated - ("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"), -] +def get_debug_level() -> str: + return get_core_settings().agent_debug_level class AuditLog(models.Model): @@ -70,18 +30,17 @@ class AuditLog(models.Model): 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) debug_info = models.JSONField(null=True, blank=True) - def __str__(self): + def __str__(self) -> str: return f"{self.username} {self.action} {self.object_type}" - def save(self, *args, **kwargs): - + def save(self, *args: Any, **kwargs: Any) -> None: if not self.pk and self.message: # truncate message field if longer than 255 characters self.message = ( @@ -91,25 +50,33 @@ def save(self, *args, **kwargs): return super(AuditLog, self).save(*args, **kwargs) @staticmethod - def audit_mesh_session(username, agent, debug_info={}): + def audit_mesh_session( + username: str, agent: "Agent", debug_info: Dict[Any, Any] = {} + ) -> None: AuditLog.objects.create( 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, ) @staticmethod - def audit_raw_command(username, agent, cmd, shell, debug_info={}): + def audit_raw_command( + username: str, + agent: "Agent", + cmd: str, + shell: str, + debug_info: Dict[Any, Any] = {}, + ) -> None: AuditLog.objects.create( 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, @@ -117,14 +84,19 @@ def audit_raw_command(username, agent, cmd, shell, debug_info={}): @staticmethod def audit_object_changed( - username, object_type, before, after, name="", debug_info={} - ): + username: str, + object_type: str, + before: Dict[Any, Any], + after: Dict[Any, Any], + name: str = "", + debug_info: Dict[Any, Any] = {}, + ) -> None: 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, @@ -132,90 +104,119 @@ def audit_object_changed( ) @staticmethod - def audit_object_add(username, object_type, after, name="", debug_info={}): + def audit_object_add( + username: str, + object_type: str, + after: Dict[Any, Any], + name: str = "", + debug_info: Dict[Any, Any] = {}, + ) -> None: 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, ) @staticmethod - def audit_object_delete(username, object_type, before, name="", debug_info={}): + def audit_object_delete( + username: str, + object_type: str, + before: Dict[Any, Any], + name: str = "", + debug_info: Dict[Any, Any] = {}, + ) -> None: 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, ) @staticmethod - def audit_script_run(username, agent, script, debug_info={}): + def audit_script_run( + username: str, agent: "Agent", script: str, debug_info: Dict[Any, Any] = {} + ) -> None: AuditLog.objects.create( 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, ) @staticmethod - def audit_user_failed_login(username, debug_info={}): + 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, ) @staticmethod - def audit_user_failed_twofactor(username, debug_info={}): + def audit_user_failed_twofactor( + 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: Two Factor token rejected", debug_info=debug_info, ) @staticmethod - def audit_user_login_successful(username, debug_info={}): + def audit_user_login_successful( + username: str, debug_info: Dict[Any, Any] = {} + ) -> 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, ) @staticmethod - def audit_url_action(username, urlaction, instance, debug_info={}): + def audit_url_action( + username: str, + urlaction: "URLAction", + instance: "Union[Agent, Client, Site]", + debug_info: Dict[Any, Any] = {}, + ) -> None: + from agents.models import Agent - name = instance.hostname if hasattr(instance, "hostname") else instance.name + name = instance.hostname if isinstance(instance, Agent) else instance.name classname = type(instance).__name__ AuditLog.objects.create( username=username, - agent=instance.hostname if classname == "Agent" else None, - agent_id=instance.agent_id if classname == "Agent" else None, + 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, ) @staticmethod - def audit_bulk_action(username, action, affected, debug_info={}): + def audit_bulk_action( + username: str, + action: str, + affected: Dict[str, Any], + debug_info: Dict[Any, Any] = {}, + ) -> None: from agents.models import Agent from clients.models import Client, Site from scripts.models import Script @@ -246,30 +247,14 @@ def audit_bulk_action(username, action, affected, debug_info={}): 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() @@ -282,44 +267,80 @@ 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) @classmethod def info( cls, - message, - agent=None, - log_type="system_issues", - ): - if get_debug_level() in ["info"]: + message: str, + agent: "Optional[Agent]" = None, + log_type: str = DebugLogType.SYSTEM_ISSUES, + ) -> None: + if get_debug_level() == 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 - def warning(cls, message, agent=None, log_type="system_issues"): - if get_debug_level() in ["info", "warning"]: + def warning( + cls, + message: str, + agent: "Optional[Agent]" = None, + log_type: str = DebugLogType.SYSTEM_ISSUES, + ) -> None: + 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 - def error(cls, message, agent=None, log_type="system_issues"): - if get_debug_level() in ["info", "warning", "error"]: + def error( + cls, + message: str, + agent: "Optional[Agent]" = None, + log_type: str = DebugLogType.SYSTEM_ISSUES, + ) -> None: + 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 - def critical(cls, message, agent=None, log_type="system_issues"): - if get_debug_level() in ["info", "warning", "error", "critical"]: + def critical( + cls, + message: str, + agent: "Optional[Agent]" = None, + log_type: str = DebugLogType.SYSTEM_ISSUES, + ) -> None: + 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, ) @@ -333,51 +354,50 @@ 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): + def __str__(self) -> str: return f"{self.agent.hostname} - {self.action_type}" @property - def due(self): - if self.action_type == "schedreboot": - obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S") - return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") - elif self.action_type == "agentupdate": + def due(self) -> str: + if self.action_type == PAAction.SCHED_REBOOT: + return cast(str, self.details["time"]) + 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" + + return "On next checkin" @property - def description(self): - if self.action_type == "schedreboot": + def description(self) -> Optional[str]: + 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", - ]: + elif self.action_type in ( + PAAction.RUN_CMD, + PAAction.RUN_SCRIPT, + PAAction.RUN_PATCH_SCAN, + PAAction.RUN_PATCH_INSTALL, + ): return f"{self.action_type}" + return None + class BaseAuditModel(models.Model): # abstract base class for auditing models @@ -391,17 +411,15 @@ class Meta: modified_time = models.DateTimeField(auto_now=True, null=True, blank=True) @abstractmethod - def serialize(): + def serialize(class_name: models.Model) -> Dict[str, Any]: pass - def save(self, old_model=None, *args, **kwargs): - - if get_username(): - + def save(self, old_model: Optional[models.Model] = None, *args, **kwargs) -> None: + username = get_username() + if username: object_class = type(self) object_name = object_class.__name__.lower() - username = get_username() - after_value = object_class.serialize(self) # type: ignore + after_value = object_class.serialize(self) # populate created_by and modified_by fields on instance if not getattr(self, "created_by", None): @@ -410,44 +428,44 @@ def save(self, old_model=None, *args, **kwargs): self.modified_by = username # dont create entry for agent add since that is done in view - if not self.pk: + if not self.pk and username: AuditLog.audit_object_add( username, object_name, - after_value, # type: ignore + after_value, self.__str__(), debug_info=get_debug_info(), ) else: - if old_model: - before_value = object_class.serialize(old_model) # type: ignore + before_value = object_class.serialize(old_model) else: - before_value = object_class.serialize(object_class.objects.get(pk=self.pk)) # type: ignore + before_value = object_class.serialize( + object_class.objects.get(pk=self.pk) + ) # only create an audit entry if the values have changed - if before_value != after_value: # type: ignore - + if before_value != after_value and username: AuditLog.audit_object_changed( username, object_class.__name__.lower(), before_value, - after_value, # type: ignore + after_value, self.__str__(), debug_info=get_debug_info(), ) super(BaseAuditModel, self).save(*args, **kwargs) - def delete(self, *args, **kwargs): + def delete(self, *args, **kwargs) -> Tuple[int, Dict[str, int]]: super(BaseAuditModel, self).delete(*args, **kwargs) - if get_username(): - + username = get_username() + if username: object_class = type(self) AuditLog.audit_object_delete( - get_username(), + username, object_class.__name__.lower(), - object_class.serialize(self), # type: ignore + object_class.serialize(self), self.__str__(), debug_info=get_debug_info(), ) diff --git a/api/tacticalrmm/logs/serializers.py b/api/tacticalrmm/logs/serializers.py index 89faabcd27..ca5abeccee 100644 --- a/api/tacticalrmm/logs/serializers.py +++ b/api/tacticalrmm/logs/serializers.py @@ -1,25 +1,31 @@ from rest_framework import serializers - from .models import AuditLog, DebugLog, PendingAction class AuditLogSerializer(serializers.ModelSerializer): - entry_time = serializers.SerializerMethodField(read_only=True) + entry_time = serializers.ReadOnlyField() ip_address = serializers.ReadOnlyField(source="debug_info.ip") + site = serializers.SerializerMethodField() + + def get_site(self, obj): + from agents.models import Agent + from clients.serializers import SiteMinimumSerializer + + if obj.agent_id and Agent.objects.filter(agent_id=obj.agent_id).exists(): + return SiteMinimumSerializer( + Agent.objects.get(agent_id=obj.agent_id).site + ).data + + return None class Meta: model = AuditLog fields = "__all__" - def get_entry_time(self, log): - tz = self.context["default_tz"] - return log.entry_time.astimezone(tz).strftime("%m %d %Y %H:%M:%S") - class PendingActionSerializer(serializers.ModelSerializer): hostname = serializers.ReadOnlyField(source="agent.hostname") - salt_id = serializers.ReadOnlyField(source="agent.salt_id") client = serializers.ReadOnlyField(source="agent.client.name") site = serializers.ReadOnlyField(source="agent.site.name") due = serializers.ReadOnlyField() @@ -32,12 +38,8 @@ class Meta: class DebugLogSerializer(serializers.ModelSerializer): agent = serializers.ReadOnlyField(source="agent.hostname") - entry_time = serializers.SerializerMethodField(read_only=True) + entry_time = serializers.ReadOnlyField() class Meta: model = DebugLog fields = "__all__" - - def get_entry_time(self, log): - tz = self.context["default_tz"] - return log.entry_time.astimezone(tz).strftime("%m %d %Y %H:%M:%S") diff --git a/api/tacticalrmm/logs/signals.py b/api/tacticalrmm/logs/signals.py index c71c1ded38..41f01eac54 100644 --- a/api/tacticalrmm/logs/signals.py +++ b/api/tacticalrmm/logs/signals.py @@ -1,30 +1,27 @@ import datetime as dt -import pytz 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 tacticalrmm.helpers import date_is_in_past from .models import PendingAction @receiver(post_init, sender=PendingAction) def handle_status(sender, instance: PendingAction, **kwargs): - if instance.id is not None: + 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" ) - - # need to convert agent tz to UTC in order to compare - agent_tz = pytz.timezone(instance.agent.timezone) - localized = agent_tz.localize(reboot_time) - - now = djangotime.now() - reboot_time_utc = localized.astimezone(pytz.utc) - - if now > reboot_time_utc: - instance.status = "completed" + if date_is_in_past( + datetime_obj=reboot_time, agent_tz=instance.agent.timezone + ): + instance.status = PAStatus.COMPLETED instance.save(update_fields=["status"]) diff --git a/api/tacticalrmm/logs/tests.py b/api/tacticalrmm/logs/tests.py index 5929bd399a..ef1f766688 100644 --- a/api/tacticalrmm/logs/tests.py +++ b/api/tacticalrmm/logs/tests.py @@ -3,6 +3,8 @@ 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" @@ -14,7 +16,6 @@ def setUp(self): self.setup_coresettings() def create_audit_records(self): - # create clients for client filter site = baker.make("clients.Site") agent1 = baker.make_recipe("agents.agent", site=site, hostname="AgentHostname1") @@ -167,22 +168,22 @@ def test_get_pending_actions(self): 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, ) r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.data), 26) # type: ignore + self.assertEqual(len(r.data), 26) self.check_not_authenticated("get", url) @@ -193,7 +194,7 @@ def test_cancel_pending_action(self, nats_cmd): 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", @@ -219,7 +220,7 @@ def test_cancel_pending_action(self, nats_cmd): 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", @@ -231,7 +232,7 @@ def test_cancel_pending_action(self, nats_cmd): f"{base_url}/pendingactions/{action2.id}/", format="json" ) self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "error deleting sched task") # type: ignore + self.assertEqual(r.data, "error deleting sched task") self.check_not_authenticated("delete", url) @@ -242,16 +243,16 @@ def test_get_debug_log(self): 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 = baker.make( # noqa "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, ) @@ -259,19 +260,22 @@ def test_get_debug_log(self): data = {"agentFilter": agent.agent_id} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 4) # type: ignore + self.assertEqual(len(resp.data), 4) # test log type filter and agent data = {"agentFilter": agent.agent_id, "logLevelFilter": "warning"} resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 1) # type: ignore + 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) # type: ignore + self.assertEqual(len(resp.data), 4) self.check_not_authenticated("patch", url) @@ -293,7 +297,7 @@ def test_auditlog_permissions(self): self.check_authorized_superuser("patch", url, data) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("patch", url, data) @@ -303,44 +307,43 @@ def test_auditlog_permissions(self): user.role.save() response = self.check_authorized("patch", url, data) - self.assertEqual(len(response.data["audit_logs"]), 86) # type: ignore + self.assertEqual(len(response.data["audit_logs"]), 86) # limit user to client if agent check user.role.can_view_sites.set([site]) response = self.check_authorized("patch", url, data) - self.assertEqual(len(response.data["audit_logs"]), 63) # type: ignore + self.assertEqual(len(response.data["audit_logs"]), 63) # limit user to client if agent check user.role.can_view_clients.set([site.client]) response = self.check_authorized("patch", url, data) - self.assertEqual(len(response.data["audit_logs"]), 63) # type: ignore + self.assertEqual(len(response.data["audit_logs"]), 63) def test_debuglog_permissions(self): - # create data agent = baker.make_recipe("agents.agent") 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, ) @@ -353,7 +356,7 @@ def test_debuglog_permissions(self): ) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("patch", url) @@ -363,31 +366,31 @@ def test_debuglog_permissions(self): user.role.save() response = self.check_authorized("patch", url) - self.assertEqual(len(response.data), 27) # type: ignore + self.assertEqual(len(response.data), 27) # limit user to site user.role.can_view_sites.set([agent.site]) response = self.check_authorized("patch", url) - self.assertEqual(len(response.data), 19) # type: ignore + self.assertEqual(len(response.data), 19) # limit user to client user.role.can_view_sites.clear() user.role.can_view_clients.set([agent2.site.client]) response = self.check_authorized("patch", url) - self.assertEqual(len(response.data), 23) # type: ignore + self.assertEqual(len(response.data), 23) # limit user to client and site user.role.can_view_sites.set([agent.site]) user.role.can_view_clients.set([agent2.site.client]) response = self.check_authorized("patch", url) - self.assertEqual(len(response.data), 27) # type: ignore + self.assertEqual(len(response.data), 27) def test_get_pendingaction_permissions(self): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") - actions = baker.make("logs.PendingAction", agent=agent, _quantity=5) - unauthorized_actions = baker.make( + actions = baker.make("logs.PendingAction", agent=agent, _quantity=5) # noqa + unauthorized_actions = baker.make( # noqa "logs.PendingAction", agent=unauthorized_agent, _quantity=7 ) @@ -401,7 +404,7 @@ def test_get_pendingaction_permissions(self): ) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) self.check_not_authorized("get", f"{base_url}/pendingactions/") self.check_not_authorized("get", f"/agents/{agent.agent_id}/pendingactions/") @@ -414,13 +417,13 @@ def test_get_pendingaction_permissions(self): user.role.save() r = self.check_authorized("get", f"{base_url}/pendingactions/") - self.assertEqual(len(r.data), 12) # type: ignore + self.assertEqual(len(r.data), 12) r = self.check_authorized("get", f"/agents/{agent.agent_id}/pendingactions/") - self.assertEqual(len(r.data), 5) # type: ignore + self.assertEqual(len(r.data), 5) r = self.check_authorized( "get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/" ) - self.assertEqual(len(r.data), 7) # type: ignore + self.assertEqual(len(r.data), 7) # test limiting to client user.role.can_view_clients.set([agent.client]) @@ -431,7 +434,7 @@ def test_get_pendingaction_permissions(self): # make sure queryset is limited too r = self.client.get(f"{base_url}/pendingactions/") - self.assertEqual(len(r.data), 5) # type: ignore + self.assertEqual(len(r.data), 5) @patch("agents.models.Agent.nats_cmd", return_value="ok") @patch("logs.models.PendingAction.delete") @@ -453,7 +456,7 @@ def test_delete_pendingaction_permissions(self, delete, nats_cmd): self.check_authorized_superuser("delete", unauthorized_url) user = self.create_user_with_roles([]) - self.client.force_authenticate(user=user) # type: ignore + self.client.force_authenticate(user=user) # test user without role self.check_not_authorized("delete", url) diff --git a/api/tacticalrmm/logs/views.py b/api/tacticalrmm/logs/views.py index 8c6bc32104..bf8a599a90 100644 --- a/api/tacticalrmm/logs/views.py +++ b/api/tacticalrmm/logs/views.py @@ -5,15 +5,18 @@ from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime +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 rest_framework.exceptions import PermissionDenied -from tacticalrmm.utils import notify_error, get_default_timezone, AGENT_DEFER -from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent -from .models import AuditLog, PendingAction, DebugLog 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 + +from .models import AuditLog, DebugLog, PendingAction from .permissions import AuditLogPerms, DebugLogPerms, PendingActionPerms from .serializers import AuditLogSerializer, DebugLogSerializer, PendingActionSerializer @@ -94,14 +97,22 @@ 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.select_related("agent") + PendingAction.objects.filter_by_role(request.user) # type: ignore + .select_related( + "agent__site", + "agent__site__client", + ) .defer("agent__services", "agent__wmi_detail") - .filter_by_role(request.user) # type: ignore ) return Response(PendingActionSerializer(actions, many=True).data) @@ -112,13 +123,14 @@ def delete(self, request, pk): if not _has_perm_on_agent(request.user, action.agent.agent_id): raise PermissionDenied() - nats_data = { - "func": "delschedtask", - "schedtaskpayload": {"name": action.details["taskname"]}, - } - r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10)) - if r != "ok": - return notify_error(r) + if action.action_type == PAAction.SCHED_REBOOT: + nats_data = { + "func": "delschedtask", + "schedtaskpayload": {"name": action.details["taskname"]}, + } + r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10)) + if r != "ok": + return notify_error(r) action.delete() return Response(f"{action.agent.hostname}: {action.description} was cancelled") @@ -143,7 +155,7 @@ def patch(self, request): debug_logs = ( DebugLog.objects.prefetch_related("agent") - .filter_by_role(request.user) + .filter_by_role(request.user) # type: ignore .filter(logLevelFilter) .filter(agentFilter) .filter(logTypeFilter) diff --git a/api/tacticalrmm/pytest.ini b/api/tacticalrmm/pytest.ini new file mode 100644 index 0000000000..480bfae5dc --- /dev/null +++ b/api/tacticalrmm/pytest.ini @@ -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 diff --git a/api/tacticalrmm/requirements-dev.txt b/api/tacticalrmm/requirements-dev.txt index d2cef4c9f3..6dd1b04267 100644 --- a/api/tacticalrmm/requirements-dev.txt +++ b/api/tacticalrmm/requirements-dev.txt @@ -1,11 +1,17 @@ black Werkzeug django-extensions -mkdocs -mkdocs-material -pymdown-extensions -Pygments isort -mypy types-pytz -types-pytz \ No newline at end of file +django-silk +mypy +django-stubs +djangorestframework-stubs +django-types +djangorestframework-types +celery-types +msgpack-types +types-Markdown +types-requests +types-PyYAML +types-urllib3 diff --git a/api/tacticalrmm/requirements-test.txt b/api/tacticalrmm/requirements-test.txt index d3375aa86b..62d7da4f82 100644 --- a/api/tacticalrmm/requirements-test.txt +++ b/api/tacticalrmm/requirements-test.txt @@ -1,2 +1,9 @@ -coveralls==3.2.0 -model_bakery \ No newline at end of file +coverage +model_bakery +black +pytest +pytest-django +pytest-xdist +pytest-cov +refurb +flake8 \ No newline at end of file diff --git a/api/tacticalrmm/requirements.txt b/api/tacticalrmm/requirements.txt index 4387e74b9d..bb791425a4 100644 --- a/api/tacticalrmm/requirements.txt +++ b/api/tacticalrmm/requirements.txt @@ -1,38 +1,39 @@ -asgiref==3.4.1 -asyncio-nats-client==0.11.4 -celery==5.2.1 -certifi==2021.10.8 -cffi==1.15.0 -channels==3.0.4 -channels_redis==3.3.1 -chardet==4.0.0 -cryptography==35.0.0 -daphne==3.0.2 -Django==3.2.9 -django-cors-headers==3.10.0 -django-ipware==4.0.0 -django-rest-knox==4.1.0 -djangorestframework==3.12.4 -future==0.18.2 -loguru==0.5.3 -msgpack==1.0.2 -packaging==21.3 -psycopg2-binary==2.9.2 +adrf==0.1.1 +asgiref==3.7.2 +celery==5.3.1 +certifi==2023.7.22 +cffi==1.15.1 +channels==4.0.0 +channels_redis==4.1.0 +cryptography==41.0.3 +daphne==4.0.0 +Django==4.2.4 +django-cors-headers==4.2.0 +django-ipware==5.0.0 +django-rest-knox==4.2.0 +djangorestframework==3.14.0 +drf-spectacular==0.26.4 +hiredis==2.2.3 +meshctrl==0.1.15 +msgpack==1.0.5 +nats-py==2.3.1 +packaging==23.1 +psutil==5.9.5 +psycopg[binary]==3.1.10 pycparser==2.21 -pycryptodome==3.11.0 -pyotp==2.6.0 -pyparsing==2.4.7 -pytz==2021.3 -qrcode==6.1 -redis==3.5.3 -requests==2.26.0 +pycryptodome==3.18.0 +pyotp==2.9.0 +pyparsing==3.1.1 +pytz==2023.3 +qrcode==7.4.2 +redis==4.5.5 +requests==2.31.0 six==1.16.0 -sqlparse==0.4.2 -twilio==7.3.1 -urllib3==1.26.7 -uWSGI==2.0.20 -validators==0.18.2 +sqlparse==0.4.4 +twilio==8.5.0 +urllib3==2.0.4 +uWSGI==2.0.22 +validators==0.20.0 vine==5.0.0 -websockets==9.1 -zipp==3.6.0 -drf_spectacular==0.21.0 \ No newline at end of file +websockets==11.0.3 +zipp==3.16.2 diff --git a/api/tacticalrmm/scripts/baker_recipes.py b/api/tacticalrmm/scripts/baker_recipes.py index fa1acd1731..6dc9e9e373 100644 --- a/api/tacticalrmm/scripts/baker_recipes.py +++ b/api/tacticalrmm/scripts/baker_recipes.py @@ -1,9 +1,61 @@ from model_bakery.recipe import Recipe +from tacticalrmm.constants import AgentPlat, 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=[AgentPlat.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=[AgentPlat.WINDOWS, AgentPlat.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=[AgentPlat.LINUX], + category="RHSA", ) diff --git a/api/tacticalrmm/scripts/community_scripts.json b/api/tacticalrmm/scripts/community_scripts.json deleted file mode 100644 index 025254fcc8..0000000000 --- a/api/tacticalrmm/scripts/community_scripts.json +++ /dev/null @@ -1,854 +0,0 @@ -[ - { - "guid": "6820cb5e-5a7f-4d9b-8c22-d54677e3cc04", - "filename": "Win_Firefox_Clear_Cache.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Firefox - Clean Cache", - "description": "This script will clean up Mozilla Firefox for all users.", - "shell": "powershell", - "category": "TRMM (Win):Browsers", - "default_timeout": "300" - }, - { - "guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443", - "filename": "Win_Google_Chrome_Clear_Cache.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Chrome - Clear Cache for All Users", - "description": "This script will clean up Google Chrome for all users.", - "shell": "powershell", - "category": "TRMM (Win):Browsers", - "default_timeout": "300" - }, - { - "guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc", - "filename": "Win_Install_Adobe_Reader.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Adobe Reader DC - Install", - "description": "Installs Adobe Reader DC.", - "shell": "powershell", - "category": "TRMM (Win):3rd Party Software>Chocolatey", - "default_timeout": "300" - }, - { - "guid": "7b1d90a1-3eda-48ab-9c49-20e714c9e82a", - "filename": "Win_Duplicati_Install.bat", - "submittedBy": "https://github.com/dinger1986", - "name": "Duplicati - Install 2.0.6.100 to work with Community Check Status", - "description": "This script installs Duplicati 2.0.6.100 as a service and creates status files to be used with commuity check", - "shell": "cmd", - "category": "TRMM (Win):3rd Party Software", - "default_timeout": "300" - }, - { - "guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080", - "filename": "Win_Duplicati_Status.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Duplicati - Check Status", - "description": "Checks Duplicati Backup is running properly over the last 24 hours", - "shell": "powershell", - "category": "TRMM (Win):3rd Party Software>Monitoring" - }, - { - "guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04", - "filename": "Win_Windows_Update_Reset.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Windows Update - Reset", - "description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.", - "shell": "powershell", - "category": "TRMM (Win):Updates", - "default_timeout": "300" - }, - { - "guid": "8db87ff0-a9b4-4d9d-bc55-377bbcb85b6d", - "filename": "Win_Start_Cleanup.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Disk - Cleanup C: drive", - "description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.", - "shell": "powershell", - "category": "TRMM (Win):Maintenance", - "default_timeout": "25000" - }, - { - "guid": "2f28e8c1-ae0f-4b46-a826-f513974526a3", - "filename": "Win_Defender_FullScan_Background.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Defender - Full Scan", - "description": "Runs a Windows Defender Full background scan.", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "adf81ddb-3b77-415c-a89b-2ccc826b5aa7", - "filename": "Win_Defender_QuickScan_Background.ps1", - "submittedBy": "https://github.com/Omnicef", - "name": "Defender - Quick Scan", - "description": "Runs a Quick Scan using Windows Defender in the Background.", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "3c46290b-85db-4cd2-93a2-943c8c93b3b1", - "filename": "Speedtest.py", - "submittedBy": "https://github.com/wh1te909", - "name": "Speed Test - Python", - "description": "Runs a Speed Test using Python", - "shell": "python", - "category": "TRMM (Win):Network", - "default_timeout": "120" - }, - { - "guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f", - "filename": "Win_TRMM_Rename_Installed_App.ps1", - "submittedBy": "https://github.com/bradhawkins85", - "name": "TacticalRMM - Agent Rename", - "description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.", - "syntax": "", - "shell": "powershell", - "category": "TRMM (Win):TacticalRMM Related" - }, - { - "guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5", - "filename": "Win_Bitlocker_Drive_Check_Status.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Bitlocker - Check Drive for Status", - "description": "Runs a check on drive for Bitlocker status. Returns 0 if Bitlocker is not enabled, 1 if Bitlocker is enabled", - "syntax": "[Drive ]", - "shell": "powershell", - "category": "TRMM (Win):Storage" - }, - { - "guid": "2ea35fa2-c227-4d17-a40e-4d39f252e27a", - "filename": "Win_Bitlocker_Create_Status_Report.ps1", - "submittedBy": "https://github.com/ThatsNASt", - "name": "Bitlocker - Create Status Report", - "description": "Creates a Bitlocker status report.", - "shell": "powershell", - "category": "TRMM (Win):Storage" - }, - { - "guid": "9e5769c1-3873-4941-bf70-e851e0afbd6d", - "filename": "Win_Bitlocker_Retrieve_Status_Report.ps1", - "submittedBy": "https://github.com/ThatsNASt", - "name": "Bitlocker - Retrieve Status Report", - "description": "Retreives a Bitlocker status report.", - "shell": "powershell", - "category": "TRMM (Win):Storage" - }, - { - "guid": "72b93487-0266-43f0-97cc-03d4c7ee0b44", - "filename": "Win_Bitlocker_Get_Recovery_Keys.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Bitlocker - Get Recovery Keys", - "description": "Retreives a Bitlocker Recovery Keys", - "shell": "powershell", - "category": "TRMM (Win):Storage" - }, - { - "guid": "cfa14c28-4dfc-4d4e-95ee-a380652e058d", - "filename": "Win_Bios_Check.ps1", - "submittedBy": "https://github.com/ThatsNASt", - "name": "BIOS - Check Information", - "description": "Retreives and reports on BIOS make, version, and date.", - "shell": "powershell", - "category": "TRMM (Win):Hardware" - }, - { - "guid": "e1c27982-b955-4766-85b6-d92527a177cf", - "filename": "Win_Hardware_Monitor_Get_Info.ps1", - "submittedBy": "https://github.com/MaxAnderson95/", - "name": "Monitor - Get Info", - "description": "Retreives and reports on Monitor info: Manufacturer, Model, Serial", - "shell": "powershell", - "category": "TRMM (Win):Hardware" - }, - { - "guid": "ae231ac4-b01f-4a39-a9d2-3d817af75260", - "filename": "Win_Hardware_RAM_Status.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "RAM - Check Information", - "description": "Retreives and reports on RAM info: DIMM's, total memory, slots total and used", - "shell": "powershell", - "category": "TRMM (Win):Hardware" - }, - { - "guid": "72c56717-28ed-4cc6-b30f-b362d30fb4b6", - "filename": "Win_Hardware_SN.ps1", - "submittedBy": "https://github.com/subzdev", - "name": "Hardware - Get Serial Number", - "description": "Returns BIOS Serial Number - Use with Custom Fields for later use", - "shell": "powershell", - "category": "TRMM (Win):Collectors" - }, - { - "guid": "973c34d7-cab0-4fda-999c-b4933655f946", - "filename": "Win_Screenconnect_GetGUID.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Screenconnect - Get GUID for client", - "description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use.", - "args": [ - "{{client.ScreenConnectService}}" - ], - "shell": "powershell", - "category": "TRMM (Win):Collectors" - }, - { - "guid": "9cfdfe8f-82bf-4081-a59f-576d694f4649", - "filename": "Win_Teamviewer_Get_ID.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "TeamViewer - Get ClientID for client", - "description": "Returns Teamviwer ClientID for client - Use with Custom Fields for later use.", - "shell": "powershell", - "category": "TRMM (Win):Collectors" - }, - { - "guid": "e43081d4-6f71-4ce3-881a-22da749f7a57", - "filename": "Win_AnyDesk_Get_Anynet_ID.ps1", - "submittedBy": "https://github.com/meuchels", - "name": "AnyDesk - Get AnyNetID for client", - "description": "Returns AnyNetID for client - Use with Custom Fields for later use.", - "shell": "powershell", - "category": "TRMM (Win):Collectors" - }, - { - "guid": "95a2ee6f-b89b-4551-856e-3081b041caa7", - "filename": "Win_Power_Profile_Reset_High_Performance_to_Defaults.ps1", - "submittedBy": "https://github.com/azulskyknight", - "name": "Power Profile - Reset High Perf Power Profile to defaults", - "description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.", - "shell": "powershell", - "category": "TRMM (Win):Power" - }, - { - "guid": "2cbd30b0-84dd-4388-a36d-2e2e980f1a3e", - "filename": "Win_Power_Profile_Set_High_Performance.ps1", - "submittedBy": "https://github.com/azulskyknight", - "name": "Power Profile - Set High Performance", - "description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.", - "shell": "powershell", - "category": "TRMM (Win):Power" - }, - { - "guid": "553236d3-81bc-49f4-af8a-0cff925a7f6d", - "filename": "Win_10_Upgrade.ps1", - "submittedBy": "https://github.com/RVL-Solutions and https://github.com/darimm", - "name": "Windows 10 Upgrade", - "description": "Forces an upgrade to the latest release of Windows 10.", - "shell": "powershell", - "category": "TRMM (Win):Updates", - "default_timeout": "25000" - }, - { - "guid": "4d0ba685-2259-44be-9010-8ed2fa48bf74", - "filename": "Win_Win11_Ready.ps1", - "submittedBy": "https://github.com/adamjrberry/", - "name": "Windows 11 Upgrade capable check", - "description": "Checks to see if machine is Win11 capable", - "shell": "powershell", - "category": "TRMM (Win):Updates", - "default_timeout": "3600" - }, - { - "guid": "375323e5-cac6-4f35-a304-bb7cef35902d", - "filename": "Win_Disk_Volume_Status.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Disk Drive Volume Health Check (using Event Viewer errors)", - "description": "Checks Drive Volumes for errors reported in event viewer within the last 24 hours", - "shell": "powershell", - "category": "TRMM (Win):Hardware" - }, - { - "guid": "907652a5-9ec1-4759-9871-a7743f805ff2", - "filename": "Win_Software_Uninstall.ps1", - "submittedBy": "https://github.com/subzdev", - "name": "Software Uninstaller - list, find, and uninstall most software", - "description": "Allows listing, finding and uninstalling most software on Windows. There will be a best effort to uninstall silently if the silent uninstall string is not provided.", - "syntax": "-list \n[-u ]\n[-u quiet ]", - "shell": "powershell", - "category": "TRMM (Win):3rd Party Software", - "default_timeout": "600" - }, - { - "guid": "64c3b1a8-c85f-4800-85a3-485f78a2d9ad", - "filename": "Win_Bitdefender_GravityZone_Install.ps1", - "submittedBy": "https://github.com/jhtechIL/", - "name": "BitDefender Gravity Zone Install", - "description": "Installs BitDefender Gravity Zone, requires client custom field setup. See script comments for details", - "syntax": "[-log]", - "args": [ - "-url {{client.bdurl}}", - "-exe {{client.bdexe}}" - ], - "default_timeout": "2500", - "shell": "powershell", - "category": "TRMM (Win):3rd Party Software" - }, - { - "guid": "da51111c-aff6-4d87-9d76-0608e1f67fe5", - "filename": "Win_Defender_Enable.ps1", - "submittedBy": "https://github.com/dinger1986", - "syntax": "[-NoControlledFolders]", - "name": "Defender - Enable", - "description": "Enables Windows Defender and sets preferences", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "a223d03a-e22e-40e0-94f2-92dd8c481d14", - "filename": "Win_Open_SSH_Server_Install.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "SSH - Install Feature and Enable", - "description": "Installs and enabled OpenSSH Server Feature in Win10", - "shell": "powershell", - "category": "TRMM (Win):Windows Features", - "default_timeout": "300" - }, - { - "guid": "2435297a-6263-4e90-8688-1847400d0e22", - "filename": "Win_RDP_enable.bat", - "submittedBy": "https://github.com/dinger1986", - "name": "RDP - Enable", - "description": "Enables RDP", - "shell": "cmd", - "category": "TRMM (Win):Windows Features" - }, - { - "guid": "0afd8d00-b95b-4318-8d07-0b9bc4424287", - "filename": "Win_Feature_NET35_Enable.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Windows Feature - Enable .NET 3.5", - "description": "Enables the Windows .NET 3.5 Framework in Turn Features on and off", - "shell": "powershell", - "default_timeout": "300", - "category": "TRMM (Win):Windows Features" - }, - { - "guid": "24f19ead-fdfe-46b4-9dcb-4cd0e12a3940", - "filename": "Win_Speedtest.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Speed Test - Powershell", - "description": "Speed Test with Powershell(win 10 or server2016+)", - "shell": "powershell", - "category": "TRMM (Win):Network" - }, - { - "guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0", - "filename": "Win_Time_Sync.bat", - "submittedBy": "https://github.com/dinger1986", - "name": "ADDC - Sync DC Time", - "description": "Syncs time with domain controller", - "shell": "cmd", - "category": "TRMM (Win):Active Directory" - }, - { - "guid": "5320dfc8-022a-41e7-9e39-11c493545ec9", - "filename": "Win_AD_Hudu_ADDS_Documentation.ps1", - "submittedBy": "https://github.com/unplugged216", - "name": "ADDS - Directory documentation in Hudu", - "description": "Auto generates ADDS documentation and submits it to your Hudu instance.", - "args": [ - "-ClientName {{client.name}}", - "-HuduBaseDomain {{global.HuduBaseDomain}}", - "-HuduApiKey {{global.HuduApiKey}}" - ], - "shell": "powershell", - "category": "TRMM (Win):Active Directory" - }, - { - "guid": "b6b9912f-4274-4162-99cc-9fd47fbcb292", - "filename": "Win_ADDC_Sync_Start.bat", - "submittedBy": "https://github.com/silversword411", - "name": "ADDC - Sync AD", - "description": "Trigger AD Sync on domain controller", - "shell": "cmd", - "category": "TRMM (Win):Active Directory" - }, - { - "guid": "b720e320-7755-4c89-9992-e1a6c43699ed", - "filename": "Win_Defender_Clear_Logs.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Defender - Clear Logs", - "description": "Clears Windows Defender Logs", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "d980fda3-a068-47eb-8495-1aab07a24e64", - "filename": "Win_Defender_Status_Report.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Defender - Status Report", - "description": "This will check for Malware and Antispyware within the last 24 hours and display, otherwise will report as Healthy. Command Parameter: (number) if provided will check that number of days back in the log.", - "syntax": "[]", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "9956e936-6fdb-4488-a9d8-8b274658037f", - "filename": "Win_Disable_Fast_Startup.bat", - "submittedBy": "https://github.com/dinger1986", - "name": "Power - Fast Startup Disable", - "description": "Disables Faststartup on Windows 10", - "shell": "cmd", - "category": "TRMM (Win):Power" - }, - { - "guid": "f628a02b-16c3-4ab5-b788-dec5bc2af1d9", - "filename": "Win_Power_Disable_Hibernation.bat", - "submittedBy": "https://github.com/silversword411", - "name": "Power - Hibernate Disable", - "description": "Disables Hibernation", - "shell": "cmd", - "category": "TRMM (Win):Power" - }, - { - "guid": "2472bbaf-1941-4722-8a58-d1dd0f528801", - "filename": "Win_TRMM_AV_Update_Exclusion.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "TRMM Defender Exclusions", - "description": "Windows Defender Exclusions for Tactical RMM", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "b253dc76-41a0-48ca-9cea-bee4277402c4", - "filename": "Win_Display_Message_To_User.ps1", - "submittedBy": "https://github.com/bradhawkins85", - "name": "Message Popup To User", - "syntax": "", - "description": "Displays a popup message to the currently logged on user", - "shell": "powershell", - "category": "TRMM (Win):Other" - }, - { - "guid": "19224d21-bd39-44bc-b9cf-8f1ba3ca9c11", - "filename": "Win_Antivirus_Verify.ps1", - "submittedBy": "https://github.com/beejayzed", - "name": "Antivirus - Verify Status", - "syntax": "[-antivirusName ]", - "description": "Verify and display status for all installed Antiviruses", - "shell": "powershell", - "category": "TRMM (Win):Security>Antivirus" - }, - { - "guid": "f88c5c52-c6fe-44db-b727-b7912a4279ed", - "filename": "Win_Create_All_User_Logon_Script.ps1", - "submittedBy": "https://github.com/nr-plaxon", - "name": "Template Example - Create User Logon Script", - "description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.", - "shell": "powershell", - "category": "TRMM (Win):Other" - }, - { - "guid": "6c78eb04-57ae-43b0-98ed-cbd3ef9e2f80", - "filename": "Win_Chocolatey_Manage_Apps_Bulk.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Chocolatey - Install, Uninstall and Upgrade Software", - "description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x", - "syntax": "-$PackageName \n[-Hosts ]\n[-mode {(install) | update | uninstall}]", - "shell": "powershell", - "category": "TRMM (Win):3rd Party Software>Chocolatey", - "default_timeout": "600" - }, - { - "guid": "fff8024d-d72e-4457-84fa-6c780f69a16f", - "filename": "Win_AD_Check_And_Enable_AD_Recycle_Bin.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "ADDC - Check and Enable AD Recycle Bin", - "description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled", - "shell": "powershell", - "category": "TRMM (Win):Active Directory" - }, - { - "guid": "3afd07c0-04fd-4b23-b5f2-88205c0744d4", - "filename": "Win_User_Admins_Local_Disable.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Local Administrators - Disables all local admins if joined to domain or AzureAD", - "description": "Checks to see if computer is either joined to a AD domain or Azure AD. If it is, it disables all local admin accounts. If not joined to domain/AzureAD, leaves admin accounts in place", - "shell": "powershell", - "category": "TRMM (Win):User Management" - }, - { - "guid": "71090fc4-faa6-460b-adb0-95d7863544e1", - "filename": "Win_Check_Events_for_Bluescreens.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Event Viewer - Bluescreen Notification", - "description": "Event Viewer Monitor - Notify Bluescreen events on your system", - "shell": "powershell", - "category": "TRMM (Win):Monitoring" - }, - { - "guid": "8373846f-facc-49b9-9891-3a780a394c89", - "filename": "Win_Local_User_Created_Monitor.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Event Viewer - New User Notification", - "description": "Event Viewer Monitor - Notify when new Local user is created", - "shell": "powershell", - "category": "TRMM (Win):Monitoring" - }, - { - "guid": "65e5cef1-8338-4180-a0bc-cd54e62de690", - "filename": "Win_Task_Scheduler_New_Items_Monitor.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Event Viewer - Task Scheduler New Item Notification", - "description": "Event Viewer Monitor - Notify when new Task Scheduler item is created", - "shell": "powershell", - "category": "TRMM (Win):Monitoring" - }, - { - "guid": "08ca81f2-f044-4dfc-ad47-090b19b19d76", - "filename": "Win_User_Logged_in_with_Temp_Profile.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "User Check - See if user logged in with temp profile", - "description": "Check if users are logged in with a temp profile", - "shell": "powershell", - "category": "TRMM (Win):Other" - }, - { - "guid": "5d905886-9eb1-4129-8b81-a013f842eb24", - "filename": "Win_Computer_Rename.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Rename Computer", - "description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine", - "syntax": "-NewName \n[-Username ]\n[-Password ]\n[-Restart]", - "shell": "powershell", - "category": "TRMM (Win):Other", - "default_timeout": 30 - }, - { - "guid": "f396dae2-c768-45c5-bd6c-176e56ed3614", - "filename": "Win_Power_RestartorShutdown.ps1", - "submittedBy": "https://github.com/tremor021", - "name": "Power - Restart or Shutdown PC", - "description": "Restart PC. Add parameter: shutdown if you want to shutdown computer", - "syntax": "[shutdown]", - "shell": "powershell", - "category": "TRMM (Win):Updates" - }, - { - "guid": "93038ae0-58ce-433e-a3b9-bc99ad1ea79a", - "filename": "Win_Services_AutomaticStartup_Running.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Ensure all services with startup type Automatic are running", - "description": "Gets a list of all service with startup type of Automatic but aren't running and tries to start them", - "shell": "powershell", - "default_timeout": "300", - "category": "TRMM (Win):Updates" - }, - { - "guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8", - "filename": "Win_ScreenConnectAIO.ps1", - "submittedBy": "https://github.com/bradhawkins85", - "name": "ScreenConnect AIO", - "description": "Install, Uninstall, Start and Stop ScreenConnect Access Agent", - "args": [ - "-serviceName {{client.ScreenConnectService}}", - "-url {{client.ScreenConnectInstaller}}", - "-clientname {{client.name}}", - "-sitename {{site.name}}", - "-action {(install) | uninstall | start | stop}" - ], - "default_timeout": "90", - "shell": "powershell", - "category": "TRMM (Win):3rd Party Software" - }, - { - "guid": "3abbb62a-3757-492c-8979-b4fc6174845d", - "filename": "Win_AutoRun_Disable.bat", - "submittedBy": "https://github.com/silversword411", - "name": "Autorun - Disable", - "description": "Disable Autorun System Wide", - "shell": "cmd", - "category": "TRMM (Win):Other", - "default_timeout": "30" - }, - { - "guid": "4a11877a-7555-494c-ac74-29d6df3c1989", - "filename": "Win_Cortana_Disable.bat", - "submittedBy": "https://github.com/silversword411", - "name": "Cortana - Disable", - "description": "Disable Cortana System Wide", - "shell": "cmd", - "category": "TRMM (Win):Other", - "default_timeout": "30" - }, - { - "guid": "28ef1387-dd4f-4bab-b042-26250914e370", - "filename": "Win_WOL_Enable_Status.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "BROKEN Network WoL - Enable function", - "description": "Wake on Lan enable on Dell, HP, Lenovo", - "shell": "powershell", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "685d5432-0b84-46d5-98e8-3ec2054150fe", - "filename": "Win_WOL_Test_State.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "BROKEN Network WoL - Test State", - "description": "Wake on Lan test status", - "shell": "powershell", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "7c0c7e37-60ff-462f-9c34-b5cd4c4796a7", - "filename": "Win_Wifi_SSID_and_Password_Retrieval.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Network Wireless - Retrieve Saved WiFi passwords", - "description": "Returns all saved wifi passwords stored on the computer", - "shell": "powershell", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "abe78170-7cf9-435b-9666-c5ef6c11a106", - "filename": "Win_Network_IPv6_Disable.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Network IPv6 - Disable", - "description": "Disable IPv6 on all adapters", - "shell": "powershell", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "745bb7cd-b71a-4f2e-b6f2-c579b1828162", - "filename": "Win_Network_DHCP_Set.bat", - "submittedBy": "https://github.com/silversword411", - "name": "Network - Set Primary NIC to DHCP", - "description": "Enable DHCP on primary adapter", - "shell": "cmd", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "5676acca-44e5-46c8-af61-ae795ecb3ef1", - "filename": "Win_Network_IP_DHCP_Renew.bat", - "submittedBy": "https://github.com/silversword411", - "name": "Network - Release and Renew IP", - "description": "Trigger and release and renew of IP address on all network adapters", - "shell": "cmd", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "83aa4d51-63ce-41e7-829f-3c16e6115bbf", - "filename": "Win_Network_DNS_Set_to_1.1.1.2.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Network - Set all NICs to use DNS of 1.1.1.2", - "description": "Domain computers skipped. Sets all NICs to have primary DNS server of 1.1.1.2, backup of 1.0.0.2 (Cloudflare malware blocking)", - "shell": "powershell", - "category": "TRMM (Win):Network", - "default_timeout": "90" - }, - { - "guid": "0caa33bc-89ca-47e0-ad4a-04626ae6384d", - "filename": "Win_Network_TCP_Reset_Stack.bat", - "submittedBy": "https://github.com/silversword411", - "name": "Network - Reset tcp using netsh", - "description": "Resets TCP stack using netsh", - "shell": "cmd", - "category": "TRMM (Win):Network", - "default_timeout": "120" - }, - { - "guid": "6ce5682a-49db-4c0b-9417-609cf905ac43", - "filename": "Win_Win10_Change_Key_and_Activate.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "Product Key in Win10 - Change and Activate", - "description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use", - "shell": "powershell", - "category": "TRMM (Win):Other", - "default_timeout": "90" - }, - { - "guid": "43e65e5f-717a-4b6d-a724-1a86229fcd42", - "filename": "Win_Activation_Check.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Windows Activation check", - "description": "Checks to see if windows is activated and returns status", - "shell": "powershell", - "category": "TRMM (Win):Other", - "default_timeout": "120" - }, - { - "guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf", - "filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "TacticalRMM - Delete Start Menu Shortcut for App", - "description": "Delete its application shortcut that's installed in the start menu by default", - "shell": "powershell", - "category": "TRMM (Win):TacticalRMM Related", - "default_timeout": "10" - }, - { - "guid": "60130fca-7636-446e-acd7-cc5d29d609c2", - "filename": "Win_Firewall_Check_Status.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Windows Firewall - Check Status", - "description": "Windows Firewall - Check state, report status", - "shell": "powershell", - "category": "TRMM (Win):Network" - }, - { - "guid": "93379675-c01c-433f-87df-a11597c959f0", - "filename": "Win_UAC_Check_Status.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Windows UAC - Check Status", - "description": "Windows UAC - Report status", - "shell": "powershell", - "category": "TRMM (Win):Security" - }, - { - "guid": "43a3206d-f1cb-44ef-8405-aae4d33a0bad", - "filename": "Win_Security_Audit.ps1", - "submittedBy": "theinterwebs", - "name": "Windows Security - Security Audit", - "description": "Runs an Audit on many components of windows to check for security issues", - "shell": "powershell", - "category": "TRMM (Win):Security" - }, - { - "guid": "7ea6a11a-05c0-4151-b5c1-cb8af029299f", - "filename": "Win_AzureAD_Check_Connection_Status.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Azure AD - Check Status", - "description": "Azure AD - Check if joined or not", - "shell": "powershell", - "category": "TRMM (Win):Azure>AD" - }, - { - "guid": "7d81859a-1ba3-42b0-8664-29844f0dd765", - "filename": "Win_Azure_Mars_Cloud_Backup_Status.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Azure - Mars Cloud backup Status", - "description": "Azure - Mars Cloud backup Check Status", - "shell": "powershell", - "category": "TRMM (Win):Azure>Backup" - }, - { - "guid": "e18c64d0-b783-4b52-b44b-9bb7592b439b", - "filename": "Win_FileSystem_Enable_Long_Paths.bat", - "submittedBy": "https://github.com/silversword411", - "name": "File System - Enable Long Paths", - "description": "Enables NTFS Long paths greater than 260 characters", - "shell": "cmd", - "category": "TRMM (Win):Storage" - }, - { - "guid": "c6252ca8-5172-42ea-9114-e447f80868f5", - "filename": "Win_USB_Disable_Access.bat", - "submittedBy": "https://github.com/silversword411", - "name": "USB - Disable Access", - "description": "USB - Disable Plugged in USB devices", - "shell": "cmd", - "category": "TRMM (Win):Storage" - }, - { - "guid": "3785952f-69fb-4bda-b2fe-5e3e8642738a", - "filename": "Win_USB_Enable_Access.bat", - "submittedBy": "https://github.com/silversword411", - "name": "USB - Enable Access", - "description": "USB - Enable Plugged in USB devices", - "shell": "cmd", - "category": "TRMM (Win):Storage" - }, - { - "guid": "c6014da2-b188-4e1b-b96a-e3440ade3a6a", - "filename": "Win_RecycleBin_Empty.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "File System - Empty Recycle Bin", - "description": "Empty the recycle bin", - "shell": "powershell", - "category": "TRMM (Win):Storage" - }, - { - "guid": "6a52f495-d43e-40f4-91a9-bbe4f578e6d1", - "filename": "Win_User_Create.ps1", - "submittedBy": "https://github.com/brodur", - "name": "User - Create Local", - "description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)", - "syntax": "-username \n-password \n[-description ]\n[-fullname ]\n[-group ]", - "shell": "powershell", - "category": "TRMM (Win):User Management" - }, - { - "guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90", - "filename": "Win_Users_List.ps1", - "submittedBy": "https://github.com/tremor021", - "name": "Users - List Local Users and Enabled/Disabled Status", - "description": "Get list of computer users and show which one is enabled", - "shell": "powershell", - "category": "TRMM (Win):User Management" - }, - { - "guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d", - "filename": "Win_Network_Set_To_Private.ps1", - "submittedBy": "https://github.com/tremor021", - "name": "Network Category - Set Network To Private", - "description": "Sets current network type to Private", - "shell": "powershell", - "category": "TRMM (Win):Network" - }, - { - "guid": "768f42d5-7b45-45ed-8233-254ae537aaa2", - "filename": "Win_TaskScheduler_Add_Task.ps1", - "submittedBy": "https://github.com/tremor021", - "name": "Task Scheduler - Add a task", - "description": "Add a task to Task Scheduler, needs editing", - "shell": "powershell", - "category": "TRMM (Win):Other" - }, - { - "guid": "e371f1c6-0dd9-44de-824c-a17e1ca4c4ab", - "filename": "Win_Outlook_SentItems_To_Delegated_Folders.ps1", - "submittedBy": "https://github.com/dinger1986", - "name": "Outlook - Delegated folders set for all profiles", - "description": "Uses RunAsUser to setup sent items for the currently logged on user on delegated folders to go into the delegated folders sent for all.", - "shell": "powershell", - "category": "TRMM (Win):Office", - "default_timeout": "90" - }, - { - "guid": "17040742-184a-4251-8f7b-4a1b0a1f02d1", - "filename": "Win_File_Copy_Misc.ps1", - "submittedBy": "https://github.com/tremor021", - "name": "EXAMPLE File Copying using powershell", - "description": "Reference Script: Will need manual tweaking, for copying files/folders from paths/websites to local", - "syntax": "-source \n-destination \n[-recursive {True | False}]", - "shell": "powershell", - "category": "TRMM (Win):Misc>Reference", - "default_timeout": "1" - }, - { - "guid": "168037d8-78e6-4a6a-a9a9-8ec2c1dbe949", - "filename": "Win_MSI_Install.ps1", - "submittedBy": "https://github.com/silversword411", - "name": "EXAMPLE Function for running MSI install via powershell", - "description": "Reference Script: Will need manual tweaking, for running MSI from powershell", - "shell": "powershell", - "category": "TRMM (Win):Misc>Reference", - "default_timeout": "1" - }, - { - "guid": "453c6d22-84b7-4767-8b5f-b825f233cf55", - "filename": "Win_AD_Join_Computer.ps1", - "submittedBy": "https://github.com/rfost52", - "name": "AD - Join Computer to Domain", - "syntax": "-domain \n-password \n-UserAccount ADMINaccount\n[-OUPath ]", - "description": "Join computer to a domain in Active Directory", - "shell": "powershell", - "category": "TRMM (Win):Active Directory", - "default_timeout": "300" - }, - { - "guid": "962d3cce-49a2-4f3e-a790-36f62a6799a0", - "filename": "Win_Collect_System_Report_And_Email.ps1", - "submittedBy": "https://github.com/rfost52", - "name": "Collect System Report and Email", - "syntax": "-agentname \n-file \n-fromaddress \n-toaddress \n-smtpserver \n-password \n-port ", - "description": "Generates a system report in HTML format, then emails it", - "shell": "powershell", - "category": "TRMM (Win):Other", - "default_timeout": "300" - } -] \ No newline at end of file diff --git a/api/tacticalrmm/scripts/management/commands/load_community_scripts.py b/api/tacticalrmm/scripts/management/commands/load_community_scripts.py index 0ba2308ca3..ae1de70355 100644 --- a/api/tacticalrmm/scripts/management/commands/load_community_scripts.py +++ b/api/tacticalrmm/scripts/management/commands/load_community_scripts.py @@ -7,4 +7,6 @@ class Command(BaseCommand): help = "Loads community scripts into the database" def handle(self, *args, **kwargs): + self.stdout.write("Syncing community scripts...") Script.load_community_scripts() + self.stdout.write("Community scripts synced successfully") diff --git a/api/tacticalrmm/scripts/migrations/0015_auto_20211128_1637.py b/api/tacticalrmm/scripts/migrations/0015_auto_20211128_1637.py new file mode 100644 index 0000000000..50c6b69862 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0015_auto_20211128_1637.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.9 on 2021-11-28 16:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0014_alter_script_filename'), + ] + + operations = [ + migrations.AddField( + model_name='script', + name='script_body', + field=models.TextField(blank=True, default=''), + ), + migrations.AddField( + model_name='script', + name='script_hash', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='script', + name='code_base64', + field=models.TextField(blank=True, default=''), + ), + ] diff --git a/api/tacticalrmm/scripts/migrations/0016_auto_20220217_1446.py b/api/tacticalrmm/scripts/migrations/0016_auto_20220217_1446.py new file mode 100644 index 0000000000..aafa82f1f3 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0016_auto_20220217_1446.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2022-02-17 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0015_auto_20211128_1637'), + ] + + operations = [ + migrations.AlterField( + model_name='script', + name='shell', + field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python'), ('shell', 'Shell')], default='powershell', max_length=100), + ), + migrations.AlterField( + model_name='scriptsnippet', + name='shell', + field=models.CharField(choices=[('powershell', 'Powershell'), ('cmd', 'Batch (CMD)'), ('python', 'Python'), ('shell', 'Shell')], default='powershell', max_length=15), + ), + ] diff --git a/api/tacticalrmm/scripts/migrations/0017_auto_20220311_0100.py b/api/tacticalrmm/scripts/migrations/0017_auto_20220311_0100.py new file mode 100644 index 0000000000..a663ddda3a --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0017_auto_20220311_0100.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.12 on 2022-03-11 01:00 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0016_auto_20220217_1446'), + ] + + operations = [ + migrations.AddField( + model_name='script', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='script', + name='supported_platforms', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), blank=True, default=list, null=True, size=None), + ), + ] diff --git a/api/tacticalrmm/scripts/migrations/0018_script_run_as_user.py b/api/tacticalrmm/scripts/migrations/0018_script_run_as_user.py new file mode 100644 index 0000000000..37720768df --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0018_script_run_as_user.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.6 on 2022-07-30 21:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scripts', '0017_auto_20220311_0100'), + ] + + operations = [ + migrations.AddField( + model_name='script', + name='run_as_user', + field=models.BooleanField(default=False), + ), + ] diff --git a/api/tacticalrmm/scripts/migrations/0019_script_env_vars.py b/api/tacticalrmm/scripts/migrations/0019_script_env_vars.py new file mode 100644 index 0000000000..1ea9edc956 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0019_script_env_vars.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.3 on 2022-11-26 01:38 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("scripts", "0018_script_run_as_user"), + ] + + operations = [ + migrations.AddField( + model_name="script", + name="env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/api/tacticalrmm/scripts/models.py b/api/tacticalrmm/scripts/models.py index 2ac608adf1..7886a3397b 100644 --- a/api/tacticalrmm/scripts/models.py +++ b/api/tacticalrmm/scripts/models.py @@ -1,24 +1,16 @@ -import base64 +import hashlib +import hmac import re 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"), -] - -SCRIPT_TYPES = [ - ("userdefined", "User Defined"), - ("builtin", "Built In"), -] - class Script(BaseAuditModel): guid = models.CharField(max_length=64, null=True, blank=True) @@ -26,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), @@ -37,23 +29,31 @@ class Script(BaseAuditModel): blank=True, default=list, ) + env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) syntax = TextField(null=True, blank=True) favorite = models.BooleanField(default=False) category = models.CharField(max_length=100, null=True, blank=True) - code_base64 = models.TextField(null=True, blank=True, default="") + script_body = models.TextField(blank=True, default="") + script_hash = models.CharField(max_length=100, null=True, blank=True) + code_base64 = models.TextField(blank=True, default="") # deprecated default_timeout = models.PositiveIntegerField(default=90) + hidden = models.BooleanField(default=False) + supported_platforms = ArrayField( + models.CharField(max_length=20), null=True, blank=True, default=list + ) + run_as_user = models.BooleanField(default=False) def __str__(self): return self.name @property def code_no_snippets(self): - if self.code_base64: - return base64.b64decode(self.code_base64.encode("ascii", "ignore")).decode( - "ascii", "ignore" - ) - else: - return "" + return self.script_body or "" @property def code(self): @@ -72,37 +72,42 @@ def replace_with_snippets(cls, code): else: value = "" - replaced_code = re.sub(snippet.group(), value, replaced_code) - + replaced_code = re.sub( + snippet.group(), value.replace("\\", "\\\\"), replaced_code + ) return replaced_code - else: - return code + + return code + + def hash_script_body(self): + from django.conf import settings + + msg = self.code.encode(errors="ignore") + return hmac.new(settings.SECRET_KEY.encode(), msg, hashlib.sha256).hexdigest() @classmethod def load_community_scripts(cls): import json import os - from pathlib import Path from django.conf import settings # load community uploaded scripts into the database # skip ones that already exist, only updating name / desc in case it changes # for install script - if not settings.DOCKER_BUILD: - scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts") - # for docker - else: - scripts_dir = settings.SCRIPTS_DIR - - with open( - os.path.join(settings.BASE_DIR, "scripts/community_scripts.json") - ) as f: + scripts_dir = os.path.join(settings.SCRIPTS_DIR, "scripts") + + with open(os.path.join(settings.SCRIPTS_DIR, "community_scripts.json")) as f: info = json.load(f) + # used to remove scripts from DB that are removed from the json file and file system + community_scripts_processed = [] # list of script guids + 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" @@ -118,105 +123,68 @@ def load_community_scripts(cls): syntax = script["syntax"] if "syntax" in script.keys() else "" + supported_platforms = ( + script["supported_platforms"] + if "supported_platforms" in script.keys() + else [] + ) + + # if community script exists update it if s.exists(): - i = s.first() - i.name = script["name"] # type: ignore - i.description = script["description"] # type: ignore - i.category = category # type: ignore - i.shell = script["shell"] # type: ignore - i.default_timeout = default_timeout # type: ignore - i.args = args # type: ignore - i.syntax = syntax # type: ignore - i.filename = script["filename"] # type: ignore + i: Script = s.get() + i.name = script["name"] + i.description = script["description"] + i.category = category + i.shell = script["shell"] + i.default_timeout = default_timeout + i.args = args + i.syntax = syntax + i.filename = script["filename"] + i.supported_platforms = supported_platforms with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: - script_bytes = ( - f.read().decode("utf-8").encode("ascii", "ignore") - ) - i.code_base64 = base64.b64encode(script_bytes).decode("ascii") # type: ignore - - i.save( # type: ignore - update_fields=[ - "name", - "description", - "category", - "default_timeout", - "code_base64", - "shell", - "args", - "filename", - "syntax", - ] - ) - - # check if script was added without a guid - elif cls.objects.filter( - script_type="builtin", name=script["name"] - ).exists(): - s = cls.objects.get(script_type="builtin", name=script["name"]) - - if not s.guid: - print(f"Updating GUID for: {script['name']}") - s.guid = script["guid"] - s.name = script["name"] - s.description = script["description"] - s.category = category - s.shell = script["shell"] - s.default_timeout = default_timeout - s.args = args - s.filename = script["filename"] - s.syntax = syntax - - with open( - os.path.join(scripts_dir, script["filename"]), "rb" - ) as f: - script_bytes = ( - f.read().decode("utf-8").encode("ascii", "ignore") - ) - s.code_base64 = base64.b64encode(script_bytes).decode( - "ascii" - ) - - s.save( - update_fields=[ - "guid", - "name", - "description", - "category", - "default_timeout", - "code_base64", - "shell", - "args", - "filename", - "syntax", - ] - ) + i.script_body = f.read().decode("utf-8") + # i.hash_script_body() + i.save() + + community_scripts_processed.append(i.guid) + # doesn't exist in database so create it else: print(f"Adding new community script: {script['name']}") with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: - script_bytes = ( - f.read().decode("utf-8").encode("ascii", "ignore") - ) - code_base64 = base64.b64encode(script_bytes).decode("ascii") + script_body = f.read().decode("utf-8") - cls( - code_base64=code_base64, + new_script: Script = cls( + script_body=script_body, guid=script["guid"], 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, filename=script["filename"], syntax=syntax, - ).save() - - # delete community scripts that had their name changed - cls.objects.filter(script_type="builtin", guid=None).delete() + supported_platforms=supported_platforms, + ) + # new_script.hash_script_body() # also saves script + new_script.save() + + community_scripts_processed.append(new_script.guid) + + # check for community scripts that were deleted from json and scripts folder + count, _ = ( + Script.objects.filter(script_type=ScriptType.BUILT_IN) + .exclude(guid__in=community_scripts_processed) + .delete() + ) + if count: + print( + f"Removing {count} community scripts that was removed from source repo" + ) @staticmethod def serialize(script): @@ -226,12 +194,11 @@ def serialize(script): return ScriptSerializer(script).data @classmethod - def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list: - + def parse_script_args(cls, agent, shell: str, args: List[str] = []) -> list: if not args: return [] - temp_args = list() + temp_args = [] # pattern to match for injection pattern = re.compile(".*\\{\\{(.*)\\}\\}.*") @@ -241,10 +208,20 @@ def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list: 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=shell != ScriptShell.CMD, + ) if value: - temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) + try: + temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) + except re.error: + temp_args.append( + re.sub("\\{\\{.*\\}\\}", re.escape(value), arg) + ) else: # pass parameter unaltered temp_args.append(arg) @@ -259,7 +236,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 diff --git a/api/tacticalrmm/scripts/permissions.py b/api/tacticalrmm/scripts/permissions.py index 94f1827788..5e4ffcd81f 100644 --- a/api/tacticalrmm/scripts/permissions.py +++ b/api/tacticalrmm/scripts/permissions.py @@ -4,8 +4,8 @@ class ScriptsPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": return _has_perm(r, "can_list_scripts") - else: - return _has_perm(r, "can_manage_scripts") + + return _has_perm(r, "can_manage_scripts") diff --git a/api/tacticalrmm/scripts/serializers.py b/api/tacticalrmm/scripts/serializers.py index d40e4a832e..6c7d80e026 100644 --- a/api/tacticalrmm/scripts/serializers.py +++ b/api/tacticalrmm/scripts/serializers.py @@ -18,10 +18,16 @@ class Meta: "default_timeout", "syntax", "filename", + "hidden", + "supported_platforms", + "run_as_user", + "env_vars", ] class ScriptSerializer(ModelSerializer): + script_hash = ReadOnlyField() + class Meta: model = Script fields = [ @@ -32,19 +38,25 @@ class Meta: "args", "category", "favorite", - "code_base64", + "script_body", + "script_hash", "default_timeout", "syntax", "filename", + "hidden", + "supported_platforms", + "run_as_user", + "env_vars", ] class ScriptCheckSerializer(ModelSerializer): code = ReadOnlyField() + script_hash = ReadOnlyField() class Meta: model = Script - fields = ["code", "shell"] + fields = ["code", "shell", "run_as_user", "env_vars", "script_hash"] class ScriptSnippetSerializer(ModelSerializer): diff --git a/api/tacticalrmm/scripts/tasks.py b/api/tacticalrmm/scripts/tasks.py index 74b83bdb83..d5b8b18682 100644 --- a/api/tacticalrmm/scripts/tasks.py +++ b/api/tacticalrmm/scripts/tasks.py @@ -1,16 +1,23 @@ import asyncio -from packaging import version as pyver - from agents.models import Agent, AgentHistory from scripts.models import Script from tacticalrmm.celery import app +from tacticalrmm.constants import AgentHistoryType +from tacticalrmm.nats_utils import abulk_nats_command @app.task -def handle_bulk_command_task( - agentpks, cmd, shell, timeout, username, run_on_offline=False +def bulk_command_task( + *, + agent_pks: list[int], + cmd: str, + shell: str, + timeout: int, + username: str, + run_as_user: bool = False, ) -> None: + items = [] nats_data = { "func": "rawcmd", "timeout": timeout, @@ -18,33 +25,61 @@ def handle_bulk_command_task( "command": cmd, "shell": shell, }, + "run_as_user": run_as_user, } - for agent in Agent.objects.filter(pk__in=agentpks): - if pyver.parse(agent.version) >= pyver.parse("1.6.0"): - hist = AgentHistory.objects.create( - agent=agent, - type="cmd_run", - command=cmd, - username=username, - ) - nats_data["id"] = hist.pk + agent: "Agent" + for agent in Agent.objects.filter(pk__in=agent_pks): + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.CMD_RUN, + command=cmd, + username=username, + ) + tmp = {**nats_data} + tmp["id"] = hist.pk + items.append((agent.agent_id, tmp)) - asyncio.run(agent.nats_cmd(nats_data, wait=False)) + asyncio.run(abulk_nats_command(items=items)) @app.task -def handle_bulk_script_task(scriptpk, agentpks, args, timeout, username) -> None: - script = Script.objects.get(pk=scriptpk) - for agent in Agent.objects.filter(pk__in=agentpks): - history_pk = 0 - if pyver.parse(agent.version) >= pyver.parse("1.6.0"): - hist = AgentHistory.objects.create( - agent=agent, - type="script_run", - script=script, - username=username, - ) - history_pk = hist.pk - agent.run_script( - scriptpk=script.pk, args=args, timeout=timeout, history_pk=history_pk +def bulk_script_task( + *, + script_pk: int, + agent_pks: list[int], + args: list[str] = [], + timeout: int, + username: str, + run_as_user: bool = False, + env_vars: list[str] = [], +) -> None: + script = Script.objects.get(pk=script_pk) + # always override if set on script model + if script.run_as_user: + run_as_user = True + + items = [] + agent: "Agent" + for agent in Agent.objects.filter(pk__in=agent_pks): + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.SCRIPT_RUN, + script=script, + username=username, ) + data = { + "func": "runscriptfull", + "id": hist.pk, + "timeout": timeout, + "script_args": script.parse_script_args(agent, script.shell, args), + "payload": { + "code": script.code, + "shell": script.shell, + }, + "run_as_user": run_as_user, + "env_vars": env_vars, + } + tup = (agent.agent_id, data) + items.append(tup) + + asyncio.run(abulk_nats_command(items=items)) diff --git a/api/tacticalrmm/scripts/tests.py b/api/tacticalrmm/scripts/tests.py index 39305cab67..8bb1b1b91a 100644 --- a/api/tacticalrmm/scripts/tests.py +++ b/api/tacticalrmm/scripts/tests.py @@ -1,17 +1,21 @@ -import json -import os -from pathlib import Path from unittest.mock import patch -from django.conf import settings +from django.test import override_settings from model_bakery import baker + +from tacticalrmm.constants import ( + CustomFieldModel, + CustomFieldType, + ScriptShell, + ScriptType, +) from tacticalrmm.test import TacticalTestCase from .models import Script, ScriptSnippet from .serializers import ( ScriptSerializer, - ScriptTableSerializer, ScriptSnippetSerializer, + ScriptTableSerializer, ) @@ -27,19 +31,20 @@ def test_get_scripts(self): serializer = ScriptTableSerializer(scripts, many=True) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(serializer.data, resp.data) # type: ignore + self.assertEqual(serializer.data, resp.data) self.check_not_authenticated("get", url) + @override_settings(SECRET_KEY="Test Secret Key") def test_add_script(self): - url = f"/scripts/" + url = "/scripts/" data = { "name": "Name", "description": "Description", - "shell": "powershell", + "shell": ScriptShell.POWERSHELL, "category": "New", - "code_base64": "VGVzdA==", # Test + "script_body": "Test Script", "default_timeout": 99, "args": ["hello", "world", r"{{agent.public_ip}}"], "favorite": False, @@ -48,11 +53,18 @@ def test_add_script(self): # test without file upload resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertTrue(Script.objects.filter(name="Name").exists()) - self.assertEqual(Script.objects.get(name="Name").code, "Test") + + new_script = Script.objects.filter(name="Name").get() + self.assertTrue(new_script) + + # correct_hash = hmac.new( + # settings.SECRET_KEY.encode(), data["script_body"].encode(), hashlib.sha256 + # ).hexdigest() + # self.assertEqual(new_script.script_hash, correct_hash) self.check_not_authenticated("post", url) + @override_settings(SECRET_KEY="Test Secret Key") def test_modify_script(self): # test a call where script doesn't exist resp = self.client.put("/scripts/500/", format="json") @@ -66,7 +78,7 @@ def test_modify_script(self): "name": script.name, "description": "Description Change", "shell": script.shell, - "code_base64": "VGVzdA==", # Test + "script_body": "Test Script Body", # Test "default_timeout": 13344556, } @@ -74,17 +86,22 @@ def test_modify_script(self): 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.assertEquals(script.code, "Test") + self.assertEqual(script.description, "Description Change") - # test edit a builtin script + # correct_hash = hmac.new( + # settings.SECRET_KEY.encode(), data["script_body"].encode(), hashlib.sha256 + # ).hexdigest() + # self.assertEqual(script.script_hash, correct_hash) + # test edit a builtin script data = { "name": "New Name", "description": "New Desc", - "code_base64": "VGVzdA==", + "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) @@ -94,7 +111,7 @@ def test_modify_script(self): "description": "Description Change", "shell": script.shell, "favorite": True, - "code_base64": "VGVzdA==", # Test + "script_body": "Test Script Body", # Test "default_timeout": 54345, } # test marking a builtin script as favorite @@ -110,11 +127,11 @@ def test_get_script(self): self.assertEqual(resp.status_code, 404) script = baker.make("scripts.Script") - url = f"/scripts/{script.pk}/" # type: ignore + url = f"/scripts/{script.pk}/" serializer = ScriptSerializer(script) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(serializer.data, resp.data) # type: ignore + self.assertEqual(serializer.data, resp.data) self.check_not_authenticated("get", url) @@ -127,12 +144,14 @@ def test_test_script(self, run_script): "code": "some_code", "timeout": 90, "args": [], - "shell": "powershell", + "shell": ScriptShell.POWERSHELL, + "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, "return value") # type: ignore + self.assertEqual(resp.data, "return value") self.check_not_authenticated("post", url) @@ -150,7 +169,7 @@ def test_delete_script(self): 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) @@ -166,129 +185,45 @@ def test_download_script(self): # test powershell file script = baker.make( - "scripts.Script", code_base64="VGVzdA==", shell="powershell" + "scripts.Script", + script_body="Test Script Body", + shell=ScriptShell.POWERSHELL, ) - url = f"/scripts/{script.pk}/download/" # type: ignore + url = f"/scripts/{script.pk}/download/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, {"filename": f"{script.name}.ps1", "code": "Test"}) # type: ignore + self.assertEqual( + resp.data, {"filename": f"{script.name}.ps1", "code": "Test Script Body"} + ) # test batch file - script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd") - url = f"/scripts/{script.pk}/download/" # type: ignore + script = baker.make( + "scripts.Script", script_body="Test Script Body", shell=ScriptShell.CMD + ) + url = f"/scripts/{script.pk}/download/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, {"filename": f"{script.name}.bat", "code": "Test"}) # type: ignore + self.assertEqual( + resp.data, {"filename": f"{script.name}.bat", "code": "Test Script Body"} + ) # test python file - script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python") - url = f"/scripts/{script.pk}/download/" # type: ignore + script = baker.make( + "scripts.Script", script_body="Test Script Body", shell=ScriptShell.PYTHON + ) + url = f"/scripts/{script.pk}/download/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, {"filename": f"{script.name}.py", "code": "Test"}) # type: ignore + self.assertEqual( + resp.data, {"filename": f"{script.name}.py", "code": "Test Script Body"} + ) self.check_not_authenticated("get", url) - def test_community_script_json_file(self): - valid_shells = ["powershell", "python", "cmd"] - - if not settings.DOCKER_BUILD: - scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts") - else: - scripts_dir = settings.SCRIPTS_DIR - - with open( - os.path.join(settings.BASE_DIR, "scripts/community_scripts.json") - ) as f: - info = json.load(f) - - guids = [] - for script in info: - fn: str = script["filename"] - self.assertTrue(os.path.exists(os.path.join(scripts_dir, fn))) - self.assertTrue(script["filename"]) - self.assertTrue(script["name"]) - self.assertTrue(script["description"]) - self.assertTrue(script["shell"]) - self.assertIn(script["shell"], valid_shells) - - if fn.endswith(".ps1"): - self.assertEqual(script["shell"], "powershell") - elif fn.endswith(".bat"): - self.assertEqual(script["shell"], "cmd") - elif fn.endswith(".py"): - self.assertEqual(script["shell"], "python") - - if "args" in script.keys(): - self.assertIsInstance(script["args"], list) - - # allows strings as long as they can be type casted to int - if "default_timeout" in script.keys(): - self.assertIsInstance(int(script["default_timeout"]), int) - - self.assertIn("guid", script.keys()) - guids.append(script["guid"]) - - # check guids are unique - self.assertEqual(len(guids), len(set(guids))) - - def test_load_community_scripts(self): - with open( - os.path.join(settings.BASE_DIR, "scripts/community_scripts.json") - ) as f: - info = json.load(f) - - Script.load_community_scripts() - - community_scripts_count = Script.objects.filter(script_type="builtin").count() - if len(info) != community_scripts_count: - raise Exception( - f"There are {len(info)} scripts in json file but only {community_scripts_count} in database" - ) - - # test updating already added community scripts - Script.load_community_scripts() - community_scripts_count2 = Script.objects.filter(script_type="builtin").count() - self.assertEqual(len(info), community_scripts_count2) - - def test_community_script_has_jsonfile_entry(self): - with open( - os.path.join(settings.BASE_DIR, "scripts/community_scripts.json") - ) as f: - info = json.load(f) - - filenames = [i["filename"] for i in info] - - # normal - if not settings.DOCKER_BUILD: - scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts") - # docker - else: - scripts_dir = settings.SCRIPTS_DIR - - with os.scandir(scripts_dir) as it: - for f in it: - if not f.name.startswith(".") and f.is_file(): - if f.name not in filenames: - raise Exception( - f"{f.name} is missing an entry in community_scripts.json" - ) - - def test_script_filenames_do_not_contain_spaces(self): - with open( - os.path.join(settings.BASE_DIR, "scripts/community_scripts.json") - ) as f: - info = json.load(f) - for script in info: - fn: str = script["filename"] - if " " in fn: - raise Exception(f"{fn} must not contain spaces in filename") - def test_script_arg_variable_replacement(self): - agent = baker.make_recipe("agents.agent", public_ip="12.12.12.12") args = [ "-Parameter", @@ -304,7 +239,7 @@ def test_script_arg_variable_replacement(self): 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): @@ -312,8 +247,8 @@ def test_script_arg_replacement_custom_field(self): field = baker.make( "core.CustomField", name="Test Field", - model="agent", - type="text", + model=CustomFieldModel.AGENT, + type=CustomFieldType.TEXT, default_value_string="DEFAULT", ) @@ -322,7 +257,7 @@ def test_script_arg_replacement_custom_field(self): # 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 @@ -334,7 +269,7 @@ def test_script_arg_replacement_custom_field(self): ) 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): @@ -342,8 +277,8 @@ def test_script_arg_replacement_client_custom_fields(self): field = baker.make( "core.CustomField", name="Test Field", - model="client", - type="text", + model=CustomFieldModel.CLIENT, + type=CustomFieldType.TEXT, default_value_string="DEFAULT", ) @@ -352,7 +287,7 @@ def test_script_arg_replacement_client_custom_fields(self): # 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 @@ -364,7 +299,7 @@ def test_script_arg_replacement_client_custom_fields(self): ) 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): @@ -372,8 +307,8 @@ def test_script_arg_replacement_site_custom_fields(self): field = baker.make( "core.CustomField", name="Test Field", - model="site", - type="text", + model=CustomFieldModel.SITE, + type=CustomFieldType.TEXT, default_value_string="DEFAULT", ) @@ -382,7 +317,7 @@ def test_script_arg_replacement_site_custom_fields(self): # 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 @@ -394,25 +329,25 @@ def test_script_arg_replacement_site_custom_fields(self): ) 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 - value.string_value = "" # type: ignore - value.save() # type: ignore + value.string_value = "" + value.save() 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 - field.default_value_string = "" # type: ignore - field.save() # type: ignore + field.default_value_string = "" + field.save() 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): @@ -420,8 +355,8 @@ def test_script_arg_replacement_array_fields(self): field = baker.make( "core.CustomField", name="Test Field", - model="agent", - type="multiple", + model=CustomFieldModel.AGENT, + type=CustomFieldType.MULTIPLE, default_values_multiple=["this", "is", "an", "array"], ) @@ -430,7 +365,7 @@ def test_script_arg_replacement_array_fields(self): # 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 @@ -442,7 +377,7 @@ def test_script_arg_replacement_array_fields(self): ) 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): @@ -450,8 +385,8 @@ def test_script_arg_replacement_boolean_fields(self): field = baker.make( "core.CustomField", name="Test Field", - model="agent", - type="checkbox", + model=CustomFieldModel.AGENT, + type=CustomFieldType.CHECKBOX, default_value_bool=True, ) @@ -460,7 +395,7 @@ def test_script_arg_replacement_boolean_fields(self): # 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 @@ -472,28 +407,32 @@ def test_script_arg_replacement_boolean_fields(self): ) 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 - custom.bool_value = True # type: ignore - custom.save() # type: ignore + custom.bool_value = True + custom.save() 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 + ), ) @@ -509,17 +448,17 @@ def test_get_script_snippets(self): serializer = ScriptSnippetSerializer(snippets, many=True) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(serializer.data, resp.data) # type: ignore + self.assertEqual(serializer.data, resp.data) self.check_not_authenticated("get", url) def test_add_script_snippet(self): - url = f"/scripts/snippets/" + url = "/scripts/snippets/" data = { "name": "Name", "description": "Description", - "shell": "powershell", + "shell": ScriptShell.POWERSHELL, "code": "Test", } @@ -536,14 +475,14 @@ def test_modify_script_snippet(self): # make a userdefined script snippet = baker.make("scripts.ScriptSnippet", name="Test") - url = f"/scripts/snippets/{snippet.pk}/" # type: ignore + url = f"/scripts/snippets/{snippet.pk}/" - data = {"name": "New Name"} # type: ignore + data = {"name": "New Name"} resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) - snippet = ScriptSnippet.objects.get(pk=snippet.pk) # type: ignore - self.assertEquals(snippet.name, "New Name") + snippet = ScriptSnippet.objects.get(pk=snippet.pk) + self.assertEqual(snippet.name, "New Name") self.check_not_authenticated("put", url) @@ -553,11 +492,11 @@ def test_get_script_snippet(self): self.assertEqual(resp.status_code, 404) snippet = baker.make("scripts.ScriptSnippet") - url = f"/scripts/snippets/{snippet.pk}/" # type: ignore + url = f"/scripts/snippets/{snippet.pk}/" serializer = ScriptSnippetSerializer(snippet) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(serializer.data, resp.data) # type: ignore + self.assertEqual(serializer.data, resp.data) self.check_not_authenticated("get", url) @@ -568,16 +507,15 @@ def test_delete_script_snippet(self): # test delete script snippet snippet = baker.make("scripts.ScriptSnippet") - url = f"/scripts/snippets/{snippet.pk}/" # type: ignore + url = f"/scripts/snippets/{snippet.pk}/" resp = self.client.delete(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertFalse(ScriptSnippet.objects.filter(pk=snippet.pk).exists()) # type: ignore + self.assertFalse(ScriptSnippet.objects.filter(pk=snippet.pk).exists()) self.check_not_authenticated("delete", url) def test_snippet_replacement(self): - snippet1 = baker.make( "scripts.ScriptSnippet", name="snippet1", code="Snippet 1 Code" ) diff --git a/api/tacticalrmm/scripts/views.py b/api/tacticalrmm/scripts/views.py index a4cc65ce78..08ae826ee0 100644 --- a/api/tacticalrmm/scripts/views.py +++ b/api/tacticalrmm/scripts/views.py @@ -1,4 +1,3 @@ -import base64 import asyncio from django.shortcuts import get_object_or_404 @@ -6,37 +5,47 @@ 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 -from agents.permissions import RunScriptPerms from .serializers import ( ScriptSerializer, - ScriptTableSerializer, ScriptSnippetSerializer, + ScriptTableSerializer, ) +from core.utils import clear_entire_cache class GetAddScripts(APIView): permission_classes = [IsAuthenticated, ScriptsPerms] def get(self, request): - showCommunityScripts = request.GET.get("showCommunityScripts", True) + 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() - return Response(ScriptTableSerializer(scripts, many=True).data) + if not showHiddenScripts or showHiddenScripts != "true": + scripts = scripts.filter(hidden=False) - def post(self, request): + return Response( + ScriptTableSerializer(scripts.order_by("category"), many=True).data + ) + def post(self, request): serializer = ScriptSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) obj = serializer.save() + # obj.hash_script_body() + return Response(f"{obj.name} was added!") @@ -48,15 +57,17 @@ def get(self, request, pk): return Response(ScriptSerializer(script).data) def put(self, request, pk): - script = get_object_or_404(Script, pk=pk) + script = get_object_or_404(Script.objects.prefetch_related("script"), pk=pk) 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 data = {"favorite": data["favorite"]} + elif "hidden" in data: + data = {"hidden": data["hidden"]} else: return notify_error("Community scripts cannot be edited.") @@ -64,13 +75,20 @@ def put(self, request, pk): serializer.is_valid(raise_exception=True) obj = serializer.save() + # TODO rename the related field from 'script' to 'scriptchecks' so it's not so confusing + if script.script.exists(): + for script_check in script.script.all(): + if script_check.policy: + clear_entire_cache() + break + return Response(f"{obj.name} was edited!") def delete(self, request, pk): 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() @@ -85,7 +103,6 @@ def get(self, request): return Response(ScriptSnippetSerializer(snippets, many=True).data) def post(self, request): - serializer = ScriptSnippetSerializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() @@ -122,9 +139,10 @@ class TestScript(APIView): permission_classes = [IsAuthenticated, RunScriptPerms] def post(self, request, agent_id): - from .models import Script from agents.models import Agent + from .models import Script + agent = get_object_or_404(Agent, agent_id=agent_id) parsed_args = Script.parse_script_args( @@ -139,6 +157,8 @@ def post(self, request, agent_id): "code": Script.replace_with_snippets(request.data["code"]), "shell": request.data["shell"], }, + "run_as_user": request.data["run_as_user"], + "env_vars": request.data["env_vars"], } r = asyncio.run( @@ -158,16 +178,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, } ) diff --git a/api/tacticalrmm/services/permissions.py b/api/tacticalrmm/services/permissions.py index ae9c5a97ac..a29e8ea3b2 100644 --- a/api/tacticalrmm/services/permissions.py +++ b/api/tacticalrmm/services/permissions.py @@ -4,10 +4,10 @@ class WinSvcsPerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) - else: - return _has_perm(r, "can_manage_winsvcs") + + return _has_perm(r, "can_manage_winsvcs") diff --git a/api/tacticalrmm/services/tests.py b/api/tacticalrmm/services/tests.py index 7fbf5ee16c..ee7717ce94 100644 --- a/api/tacticalrmm/services/tests.py +++ b/api/tacticalrmm/services/tests.py @@ -57,13 +57,12 @@ def test_get_services(self, nats_cmd): resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) nats_cmd.assert_called_with(data={"func": "winservices"}, timeout=10) - self.assertEquals(Agent.objects.get(pk=agent.pk).services, nats_return) + self.assertEqual(Agent.objects.get(pk=agent.pk).services, nats_return) self.check_not_authenticated("get", url) @patch("agents.models.Agent.nats_cmd") def test_service_action(self, nats_cmd): - data = {"sv_action": "restart"} # test a call where agent doesn't exist resp = self.client.post( @@ -135,7 +134,7 @@ def test_service_detail(self, nats_cmd): nats_cmd.assert_called_with( {"func": "winsvcdetail", "payload": {"name": "alg"}}, timeout=10 ) - self.assertEquals(resp.data, nats_return) + self.assertEqual(resp.data, nats_return) self.check_not_authenticated("get", url) @@ -195,7 +194,7 @@ def test_edit_service(self, nats_cmd): class TestServicePermissions(TacticalTestCase): def setUp(self): self.setup_coresettings() - self.client_setup() + self.setup_client() @patch("agents.models.Agent.nats_cmd", return_value="ok") def test_services_permissions(self, nats_cmd): diff --git a/api/tacticalrmm/services/views.py b/api/tacticalrmm/services/views.py index 60e8b81806..15b800077f 100644 --- a/api/tacticalrmm/services/views.py +++ b/api/tacticalrmm/services/views.py @@ -1,24 +1,47 @@ import asyncio +from typing import Dict, Tuple, Union -from agents.models import Agent -from checks.models import Check +from django.conf import settings from django.shortcuts import get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from tacticalrmm.utils import notify_error + +from agents.models import Agent +from tacticalrmm.helpers import notify_error from .permissions import WinSvcsPerms +def process_nats_response(data: Union[str, Dict]) -> Tuple[bool, bool, str]: + natserror = isinstance(data, str) + success = ( + data["success"] + if isinstance(data, dict) and isinstance(data["success"], bool) + else False + ) + errormsg = ( + data["errormsg"] + if isinstance(data, dict) and isinstance(data["errormsg"], str) + else "timeout" + ) + + return success, natserror, errormsg + + class GetServices(APIView): permission_classes = [IsAuthenticated, WinSvcsPerms] def get(self, request, agent_id): + if getattr(settings, "DEMO", False): + from tacticalrmm.demo_views import demo_get_services + + return demo_get_services() + agent = get_object_or_404(Agent, agent_id=agent_id) r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10)) - if r == "timeout" or r == "natsdown": + if r in ("timeout", "natsdown"): return notify_error("Unable to contact the agent") agent.services = r @@ -42,6 +65,8 @@ def get(self, request, agent_id, svcname): # win service action def post(self, request, agent_id, svcname): agent = get_object_or_404(Agent, agent_id=agent_id) + if agent.is_posix: + return notify_error("Please use 'Recover Connection' instead.") action = request.data["sv_action"] data = { "func": "winsvcaction", @@ -53,27 +78,33 @@ def post(self, request, agent_id, svcname): if action == "restart": data["payload"]["action"] = "stop" r = asyncio.run(agent.nats_cmd(data, timeout=32)) - if r == "timeout" or r == "natsdown": + success, natserror, errormsg = process_nats_response(r) + + if errormsg == "timeout" or natserror: return notify_error("Unable to contact the agent") - elif not r["success"] and r["errormsg"]: - return notify_error(r["errormsg"]) - elif r["success"]: + elif not success and errormsg: + return notify_error(errormsg) + elif success: data["payload"]["action"] = "start" r = asyncio.run(agent.nats_cmd(data, timeout=32)) - if r == "timeout": + success, natserror, errormsg = process_nats_response(r) + + if errormsg == "timeout" or natserror: return notify_error("Unable to contact the agent") - elif not r["success"] and r["errormsg"]: - return notify_error(r["errormsg"]) - elif r["success"]: + elif not success and errormsg: + return notify_error(errormsg) + elif success: return Response("The service was restarted successfully") else: data["payload"]["action"] = action r = asyncio.run(agent.nats_cmd(data, timeout=32)) - if r == "timeout" or r == "natsdown": + success, natserror, errormsg = process_nats_response(r) + + if errormsg == "timeout" or natserror: return notify_error("Unable to contact the agent") - elif not r["success"] and r["errormsg"]: - return notify_error(r["errormsg"]) - elif r["success"]: + elif not success and errormsg: + return notify_error(errormsg) + elif success: return Response( f"The service was {'started' if action == 'start' else 'stopped'} successfully" ) @@ -92,12 +123,13 @@ def put(self, request, agent_id, svcname): } r = asyncio.run(agent.nats_cmd(data, timeout=10)) + success, natserror, errormsg = process_nats_response(r) # response struct from agent: {success: bool, errormsg: string} - if r == "timeout" or r == "natsdown": + if r == "timeout" or natserror: return notify_error("Unable to contact the agent") - elif not r["success"] and r["errormsg"]: - return notify_error(r["errormsg"]) - elif r["success"]: + elif not success and errormsg: + return notify_error(errormsg) + elif success: return Response("The service start type was updated successfully") return notify_error("Something went wrong") diff --git a/api/tacticalrmm/software/management/commands/find_software.py b/api/tacticalrmm/software/management/commands/find_software.py index bd5ce84e92..9b37bf897b 100644 --- a/api/tacticalrmm/software/management/commands/find_software.py +++ b/api/tacticalrmm/software/management/commands/find_software.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from agents.models import Agent +from software.models import InstalledSoftware class Command(BaseCommand): @@ -12,22 +12,15 @@ def add_arguments(self, parser): def handle(self, *args, **kwargs): search = kwargs["name"].lower() - agents = Agent.objects.all() - for agent in agents: - try: - sw = agent.installedsoftware_set.first().software - except: - self.stdout.write( - self.style.ERROR( - f"Agent {agent.hostname} missing software list. Try manually refreshing it from the web UI from the software tab." - ) - ) - continue - for i in sw: - if search in i["name"].lower(): + all_sw = InstalledSoftware.objects.select_related( + "agent", "agent__site", "agent__site__client" + ) + for instance in all_sw.iterator(chunk_size=20): + for sw in instance.software: + if search in sw["name"].lower(): self.stdout.write( self.style.SUCCESS( - f"Found {i['name']} installed on {agent.hostname}" + f"Found {sw['name']} installed on: {instance.agent.client.name}\\{instance.agent.site.name}\\{instance.agent.hostname}" ) ) break diff --git a/api/tacticalrmm/software/models.py b/api/tacticalrmm/software/models.py index be3d80371b..f28e0362dd 100644 --- a/api/tacticalrmm/software/models.py +++ b/api/tacticalrmm/software/models.py @@ -1,7 +1,7 @@ from django.db import models -from tacticalrmm.models import PermissionQuerySet from agents.models import Agent +from tacticalrmm.models import PermissionQuerySet class ChocoSoftware(models.Model): diff --git a/api/tacticalrmm/software/permissions.py b/api/tacticalrmm/software/permissions.py index 74553b6745..cec1c99a60 100644 --- a/api/tacticalrmm/software/permissions.py +++ b/api/tacticalrmm/software/permissions.py @@ -4,7 +4,7 @@ class SoftwarePerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if r.method == "GET": if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_list_software") and _has_perm_on_agent( @@ -13,7 +13,6 @@ def has_permission(self, r, view): return _has_perm(r, "can_list_software") - else: - return _has_perm(r, "can_manage_software") and _has_perm_on_agent( - r.user, view.kwargs["agent_id"] - ) + return _has_perm(r, "can_manage_software") and _has_perm_on_agent( + r.user, view.kwargs["agent_id"] + ) diff --git a/api/tacticalrmm/software/tests.py b/api/tacticalrmm/software/tests.py index 2c01bbb4e5..bfe99bc4df 100644 --- a/api/tacticalrmm/software/tests.py +++ b/api/tacticalrmm/software/tests.py @@ -42,7 +42,7 @@ def test_get_installed_software(self): # test without agent software resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEquals(resp.data, []) # type: ignore + self.assertEqual(resp.data, []) # make some software software = baker.make( @@ -54,13 +54,13 @@ def test_get_installed_software(self): serializer = InstalledSoftwareSerializer(software) resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) # test checking all software (multiple agents) serializer = InstalledSoftwareSerializer([software], many=True) resp = self.client.get(f"{base_url}/", format="json") self.assertEqual(resp.status_code, 200) - self.assertEquals(resp.data, serializer.data) # type: ignore + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) @@ -139,13 +139,13 @@ def test_refresh_installed_software(self, nats_cmd): class TestSoftwarePermissions(TacticalTestCase): def setUp(self): self.setup_coresettings() - self.client_setup() + self.setup_client() def test_list_software_permissions(self): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") software = baker.make("software.InstalledSoftware", software={}, agent=agent) - unauthorized_software = baker.make( + unauthorized_software = baker.make( # noqa "software.InstalledSoftware", software={}, agent=unauthorized_agent ) @@ -180,8 +180,10 @@ def test_list_software_permissions(self): def test_install_refresh_software_permissions(self, nats_cmd): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") - software = baker.make("software.InstalledSoftware", software={}, agent=agent) - unauthorized_software = baker.make( + software = baker.make( # noqa + "software.InstalledSoftware", software={}, agent=agent + ) + unauthorized_software = baker.make( # noqa "software.InstalledSoftware", software={}, agent=unauthorized_agent ) diff --git a/api/tacticalrmm/software/views.py b/api/tacticalrmm/software/views.py index 6e36f0b9d7..4b1a6068d5 100644 --- a/api/tacticalrmm/software/views.py +++ b/api/tacticalrmm/software/views.py @@ -2,7 +2,6 @@ from typing import Any from django.shortcuts import get_object_or_404 -from packaging import version as pyver from rest_framework.decorators import api_view from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -10,7 +9,8 @@ from agents.models import Agent from logs.models import PendingAction -from tacticalrmm.utils import notify_error +from tacticalrmm.constants import PAAction +from tacticalrmm.helpers import notify_error from .models import ChocoSoftware, InstalledSoftware from .permissions import SoftwarePerms @@ -19,7 +19,11 @@ @api_view(["GET"]) def chocos(request): - return Response(ChocoSoftware.objects.last().chocos) + chocos = ChocoSoftware.objects.last() + if not chocos: + return Response({}) + + return Response(chocos.chocos) class GetSoftware(APIView): @@ -36,20 +40,20 @@ def get(self, request, agent_id=None): except Exception: return Response([]) else: - software = InstalledSoftware.objects.filter_by_role(request.user) + software = InstalledSoftware.objects.filter_by_role(request.user) # type: ignore return Response(InstalledSoftwareSerializer(software, many=True).data) # software install def post(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) - if pyver.parse(agent.version) < pyver.parse("1.4.8"): - return notify_error("Requires agent v1.4.8") + if agent.is_posix: + return notify_error(f"Not available for {agent.plat}") name = request.data["name"] action = PendingAction.objects.create( agent=agent, - action_type="chocoinstall", + action_type=PAAction.CHOCO_INSTALL, details={"name": name, "output": None, "installed": False}, ) @@ -71,9 +75,11 @@ def post(self, request, agent_id): # refresh software list def put(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) + if agent.is_posix: + return notify_error(f"Not available for {agent.plat}") r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) - if r == "timeout" or r == "natsdown": + if r in ("timeout", "natsdown"): return notify_error("Unable to contact the agent") if not InstalledSoftware.objects.filter(agent=agent).exists(): diff --git a/api/tacticalrmm/tacticalrmm/__init__.py b/api/tacticalrmm/tacticalrmm/__init__.py index 8a891ca190..0da1386dff 100644 --- a/api/tacticalrmm/tacticalrmm/__init__.py +++ b/api/tacticalrmm/tacticalrmm/__init__.py @@ -2,4 +2,7 @@ from .celery import app as celery_app +# drf auto-registers this as an authentication method when imported +from .schema import APIAuthenticationScheme # noqa + __all__ = ("celery_app",) diff --git a/api/tacticalrmm/tacticalrmm/asgi.py b/api/tacticalrmm/tacticalrmm/asgi.py index e76f13506c..7fd597e8e7 100644 --- a/api/tacticalrmm/tacticalrmm/asgi.py +++ b/api/tacticalrmm/tacticalrmm/asgi.py @@ -1,20 +1,18 @@ import os -import django - from channels.routing import ProtocolTypeRouter, URLRouter # isort:skip from django.core.asgi import get_asgi_application # isort:skip -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings") -django.setup() +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings") # isort:skip +django_asgi_app = get_asgi_application() # isort:skip -from tacticalrmm.utils import KnoxAuthMiddlewareStack # isort:skip -from .urls import ws_urlpatterns # isort:skip +from tacticalrmm.utils import KnoxAuthMiddlewareStack # isort:skip # noqa +from .urls import ws_urlpatterns # isort:skip # noqa application = ProtocolTypeRouter( { - "http": get_asgi_application(), + "http": django_asgi_app, "websocket": KnoxAuthMiddlewareStack(URLRouter(ws_urlpatterns)), } ) diff --git a/api/tacticalrmm/tacticalrmm/auth.py b/api/tacticalrmm/tacticalrmm/auth.py index 6d373b96ff..0a8995dd8b 100644 --- a/api/tacticalrmm/tacticalrmm/auth.py +++ b/api/tacticalrmm/tacticalrmm/auth.py @@ -1,12 +1,12 @@ from django.utils import timezone as djangotime -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions -from rest_framework.authentication import BaseAuthentication, HTTP_HEADER_ENCODING +from rest_framework.authentication import HTTP_HEADER_ENCODING, BaseAuthentication from accounts.models import APIKey -def get_authorization_header(request): +def get_authorization_header(request) -> str: """ Return request's 'Authorization:' header, as a bytestring. @@ -39,7 +39,7 @@ def authenticate(self, request): return None try: - apikey = auth.decode() + apikey = auth.decode() # type: ignore except UnicodeError: msg = _( "Invalid token header. Token string should not contain invalid characters." @@ -59,6 +59,6 @@ def authenticate_credentials(self, key): # check if token is expired if apikey.expiration and apikey.expiration < djangotime.now(): - raise exceptions.AuthenticationFailed(_("The token as expired.")) + raise exceptions.AuthenticationFailed(_("The token has expired.")) - return (apikey.user, apikey.key) + return apikey.user, apikey.key diff --git a/api/tacticalrmm/tacticalrmm/cache.py b/api/tacticalrmm/tacticalrmm/cache.py new file mode 100644 index 0000000000..077054278d --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/cache.py @@ -0,0 +1,21 @@ +from typing import Optional + +from django.core.cache.backends.dummy import DummyCache +from django.core.cache.backends.redis import RedisCache + + +class TacticalRedisCache(RedisCache): + def delete_many_pattern(self, pattern: str, version: Optional[int] = None) -> None: + keys = self._cache.get_client().keys(f":{version or 1}:{pattern}") + + if keys: + self._cache.delete_many(keys) + + # just for debugging + def show_everything(self, version: Optional[int] = None) -> list[bytes]: + return self._cache.get_client().keys(f":{version or 1}:*") + + +class TacticalDummyCache(DummyCache): + def delete_many_pattern(self, pattern: str, version: Optional[int] = None) -> None: + return None diff --git a/api/tacticalrmm/tacticalrmm/celery.py b/api/tacticalrmm/tacticalrmm/celery.py index cfee60b874..0abcc0fe1a 100644 --- a/api/tacticalrmm/tacticalrmm/celery.py +++ b/api/tacticalrmm/tacticalrmm/celery.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals import os +from datetime import timedelta from celery import Celery from celery.schedules import crontab @@ -8,17 +9,10 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings") -app = Celery( - "tacticalrmm", - backend="redis://" + settings.REDIS_HOST, - broker="redis://" + settings.REDIS_HOST, -) -# app.config_from_object('django.conf:settings', namespace='CELERY') -app.broker_url = "redis://" + settings.REDIS_HOST + ":6379" # type: ignore -app.result_backend = "redis://" + settings.REDIS_HOST + ":6379" # type: ignore -app.accept_content = ["application/json"] # type: ignore -app.result_serializer = "json" # type: ignore -app.task_serializer = "json" # type: ignore +app = Celery("tacticalrmm", backend="redis://" + settings.REDIS_HOST, broker="redis://" + settings.REDIS_HOST) # type: ignore +app.accept_content = ["application/json"] +app.result_serializer = "json" +app.task_serializer = "json" app.conf.task_track_started = True app.conf.worker_proc_alive_timeout = 30 app.conf.worker_max_tasks_per_child = 2 @@ -37,9 +31,37 @@ "task": "agents.tasks.auto_self_agent_update_task", "schedule": crontab(minute=35, hour="*"), }, - "handle-agents": { - "task": "agents.tasks.handle_agents_task", - "schedule": crontab(minute="*/3"), + "remove-orphaned-tasks": { + "task": "autotasks.tasks.remove_orphaned_win_tasks", + "schedule": crontab(minute=50, hour="12"), + }, + "agent-outages-task": { + "task": "agents.tasks.agent_outages_task", + "schedule": timedelta(seconds=150.0), + }, + "unsnooze-alerts": { + "task": "alerts.tasks.unsnooze_alerts", + "schedule": crontab(minute=10, hour="*"), + }, + "core-maintenance-tasks": { + "task": "core.tasks.core_maintenance_tasks", + "schedule": crontab(minute=15, hour="*"), + }, + "cache-db-fields-task": { + "task": "core.tasks.cache_db_fields_task", + "schedule": crontab(minute="*/3", hour="*"), + }, + "sync-scheduled-tasks": { + "task": "core.tasks.sync_scheduled_tasks", + "schedule": crontab(minute="*/2", hour="*"), + }, + "resolve-pending-actions": { + "task": "core.tasks.resolve_pending_actions", + "schedule": timedelta(seconds=100.0), + }, + "resolve-alerts-task": { + "task": "core.tasks.resolve_alerts_task", + "schedule": timedelta(seconds=80.0), }, } @@ -47,16 +69,3 @@ @app.task(bind=True) def debug_task(self): print("Request: {0!r}".format(self.request)) - - -@app.on_after_finalize.connect -def setup_periodic_tasks(sender, **kwargs): - - from agents.tasks import agent_outages_task - from alerts.tasks import unsnooze_alerts - from core.tasks import core_maintenance_tasks, cache_db_fields_task - - sender.add_periodic_task(60.0, agent_outages_task.s()) - sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s()) - sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s()) - sender.add_periodic_task(90.0, cache_db_fields_task.s()) diff --git a/api/tacticalrmm/tacticalrmm/constants.py b/api/tacticalrmm/tacticalrmm/constants.py new file mode 100644 index 0000000000..568287e22c --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/constants.py @@ -0,0 +1,453 @@ +from enum import Enum + +from django.db import models + + +class MeshAgentIdent(Enum): + WIN32 = 3 + WIN64 = 4 + LINUX32 = 5 + LINUX64 = 6 + LINUX_ARM_64 = 26 + LINUX_ARM_HF = 25 + DARWIN_UNIVERSAL = 10005 + + def __str__(self): + return str(self.value) + + +CORESETTINGS_CACHE_KEY = "core_settings" +ROLE_CACHE_PREFIX = "role_" +AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX = "agent_tbl_pendingactions_" + +AGENT_STATUS_ONLINE = "online" +AGENT_STATUS_OFFLINE = "offline" +AGENT_STATUS_OVERDUE = "overdue" + +REDIS_LOCK_EXPIRE = 60 * 60 * 2 # Lock expires in 2 hours +RESOLVE_ALERTS_LOCK = "resolve-alerts-lock-key" +SYNC_SCHED_TASK_LOCK = "sync-sched-tasks-lock-key" +AGENT_OUTAGES_LOCK = "agent-outages-task-lock-key" +ORPHANED_WIN_TASK_LOCK = "orphaned-win-task-lock-key" + + +class GoArch(models.TextChoices): + AMD64 = "amd64", "amd64" + i386 = "386", "386" + ARM64 = "arm64", "arm64" + ARM32 = "arm", "arm" + + +class CustomFieldModel(models.TextChoices): + CLIENT = "client", "Client" + SITE = "site", "Site" + AGENT = "agent", "Agent" + + +class CustomFieldType(models.TextChoices): + TEXT = "text", "Text" + NUMBER = "number", "Number" + SINGLE = "single", "Single" + MULTIPLE = "multiple", "Multiple" + CHECKBOX = "checkbox", "Checkbox" + DATETIME = "datetime", "DateTime" + + +class TaskSyncStatus(models.TextChoices): + SYNCED = "synced", "Synced With Agent" + NOT_SYNCED = "notsynced", "Waiting On Agent Checkin" + PENDING_DELETION = "pendingdeletion", "Pending Deletion on Agent" + INITIAL = "initial", "Initial Task Sync" + + +class TaskStatus(models.TextChoices): + PASSING = "passing", "Passing" + FAILING = "failing", "Failing" + PENDING = "pending", "Pending" + + +class TaskType(models.TextChoices): + DAILY = "daily", "Daily" + WEEKLY = "weekly", "Weekly" + MONTHLY = "monthly", "Monthly" + MONTHLY_DOW = "monthlydow", "Monthly Day of Week" + CHECK_FAILURE = "checkfailure", "On Check Failure" + MANUAL = "manual", "Manual" + RUN_ONCE = "runonce", "Run Once" + SCHEDULED = "scheduled", "Scheduled" # deprecated + + +class AlertSeverity(models.TextChoices): + INFO = "info", "Informational" + WARNING = "warning", "Warning" + ERROR = "error", "Error" + + +class AlertType(models.TextChoices): + AVAILABILITY = "availability", "Availability" + CHECK = "check", "Check" + TASK = "task", "Task" + CUSTOM = "custom", "Custom" + + +class AgentHistoryType(models.TextChoices): + TASK_RUN = "task_run", "Task Run" + SCRIPT_RUN = "script_run", "Script Run" + CMD_RUN = "cmd_run", "CMD Run" + + +class AgentMonType(models.TextChoices): + SERVER = "server", "Server" + WORKSTATION = "workstation", "Workstation" + + +class AgentPlat(models.TextChoices): + WINDOWS = "windows", "Windows" + LINUX = "linux", "Linux" + DARWIN = "darwin", "macOS" + + +class ClientTreeSort(models.TextChoices): + ALPHA_FAIL = "alphafail", "Move failing clients to the top" + ALPHA = "alpha", "Sort alphabetically" + + +class AgentTableTabs(models.TextChoices): + SERVER = "server", "Servers" + WORKSTATION = "workstation", "Workstations" + MIXED = "mixed", "Mixed" + + +class AgentDblClick(models.TextChoices): + EDIT_AGENT = "editagent", "Edit Agent" + TAKE_CONTROL = "takecontrol", "Take Control" + REMOTE_BG = "remotebg", "Remote Background" + URL_ACTION = "urlaction", "URL Action" + + +class ScriptShell(models.TextChoices): + POWERSHELL = "powershell", "Powershell" + CMD = "cmd", "Batch (CMD)" + PYTHON = "python", "Python" + SHELL = "shell", "Shell" + + +class ScriptType(models.TextChoices): + USER_DEFINED = "userdefined", "User Defined" + BUILT_IN = "builtin", "Built In" + + +class EvtLogNames(models.TextChoices): + APPLICATION = "Application", "Application" + SYSTEM = "System", "System" + SECURITY = "Security", "Security" + + +class EvtLogTypes(models.TextChoices): + INFO = "INFO", "Information" + WARNING = "WARNING", "Warning" + ERROR = "ERROR", "Error" + AUDIT_SUCCESS = "AUDIT_SUCCESS", "Success Audit" + AUDIT_FAILURE = "AUDIT_FAILURE", "Failure Audit" + + +class EvtLogFailWhen(models.TextChoices): + CONTAINS = "contains", "Log contains" + NOT_CONTAINS = "not_contains", "Log does not contain" + + +class CheckStatus(models.TextChoices): + PASSING = "passing", "Passing" + FAILING = "failing", "Failing" + PENDING = "pending", "Pending" + + +class PAStatus(models.TextChoices): + PENDING = "pending", "Pending" + COMPLETED = "completed", "Completed" + + +class PAAction(models.TextChoices): + SCHED_REBOOT = "schedreboot", "Scheduled Reboot" + AGENT_UPDATE = "agentupdate", "Agent Update" + CHOCO_INSTALL = "chocoinstall", "Chocolatey Software Install" + RUN_CMD = "runcmd", "Run Command" + RUN_SCRIPT = "runscript", "Run Script" + RUN_PATCH_SCAN = "runpatchscan", "Run Patch Scan" + RUN_PATCH_INSTALL = "runpatchinstall", "Run Patch Install" + + +class CheckType(models.TextChoices): + DISK_SPACE = "diskspace", "Disk Space Check" + PING = "ping", "Ping Check" + CPU_LOAD = "cpuload", "CPU Load Check" + MEMORY = "memory", "Memory Check" + WINSVC = "winsvc", "Service Check" + SCRIPT = "script", "Script Check" + EVENT_LOG = "eventlog", "Event Log Check" + + +class AuditActionType(models.TextChoices): + LOGIN = "login", "User Login" + FAILED_LOGIN = "failed_login", "Failed User Login" + DELETE = "delete", "Delete Object" + MODIFY = "modify", "Modify Object" + ADD = "add", "Add Object" + VIEW = "view", "View Object" + CHECK_RUN = "check_run", "Check Run" + TASK_RUN = "task_run", "Task Run" + AGENT_INSTALL = "agent_install", "Agent Install" + REMOTE_SESSION = "remote_session", "Remote Session" + EXEC_SCRIPT = "execute_script", "Execute Script" + EXEC_COMMAND = "execute_command", "Execute Command" + BULK_ACTION = "bulk_action", "Bulk Action" + URL_ACTION = "url_action", "URL Action" + + +class AuditObjType(models.TextChoices): + USER = "user", "User" + SCRIPT = "script", "Script" + AGENT = "agent", "Agent" + POLICY = "policy", "Policy" + WINUPDATE = "winupdatepolicy", "Patch Policy" + CLIENT = "client", "Client" + SITE = "site", "Site" + CHECK = "check", "Check" + AUTOTASK = "automatedtask", "Automated Task" + CORE = "coresettings", "Core Settings" + BULK = "bulk", "Bulk" + ALERT_TEMPLATE = "alerttemplate", "Alert Template" + ROLE = "role", "Role" + URL_ACTION = "urlaction", "URL Action" + KEYSTORE = "keystore", "Global Key Store" + CUSTOM_FIELD = "customfield", "Custom Field" + + +class DebugLogLevel(models.TextChoices): + INFO = "info", "Info" + WARN = "warning", "Warning" + ERROR = "error", "Error" + CRITICAL = "critical", "Critical" + + +class DebugLogType(models.TextChoices): + AGENT_UPDATE = "agent_update", "Agent Update" + AGENT_ISSUES = "agent_issues", "Agent Issues" + WIN_UPDATES = "win_updates", "Windows Updates" + SYSTEM_ISSUES = "system_issues", "System Issues" + SCRIPTING = "scripting", "Scripting" + + +# Agent db fields that are not needed for most queries, speeds up query +AGENT_DEFER = ( + "wmi_detail", + "services", + "created_by", + "created_time", + "modified_by", + "modified_time", +) + +AGENT_TABLE_DEFER = ( + "services", + "created_by", + "created_time", + "modified_by", + "modified_time", +) + +ONLINE_AGENTS = ( + "pk", + "agent_id", + "last_seen", + "overdue_time", + "offline_time", + "version", +) + +FIELDS_TRIGGER_TASK_UPDATE_AGENT = [ + "run_time_bit_weekdays", + "run_time_date", + "expire_date", + "daily_interval", + "weekly_interval", + "enabled", + "remove_if_not_scheduled", + "run_asap_after_missed", + "monthly_days_of_month", + "monthly_months_of_year", + "monthly_weeks_of_month", + "task_repetition_duration", + "task_repetition_interval", + "stop_task_at_duration_end", + "random_task_delay", + "run_asap_after_missed", + "task_instance_policy", +] + +POLICY_TASK_FIELDS_TO_COPY = [ + "alert_severity", + "email_alert", + "text_alert", + "dashboard_alert", + "name", + "actions", + "run_time_bit_weekdays", + "run_time_date", + "expire_date", + "daily_interval", + "weekly_interval", + "task_type", + "enabled", + "remove_if_not_scheduled", + "run_asap_after_missed", + "custom_field", + "collector_all_output", + "monthly_days_of_month", + "monthly_months_of_year", + "monthly_weeks_of_month", + "task_repetition_duration", + "task_repetition_interval", + "stop_task_at_duration_end", + "random_task_delay", + "run_asap_after_missed", + "task_instance_policy", + "continue_on_error", +] + +CHECKS_NON_EDITABLE_FIELDS = [ + "check_type", + "overridden_by_policy", + "created_by", + "created_time", + "modified_by", + "modified_time", +] + +POLICY_CHECK_FIELDS_TO_COPY = [ + "check_type", + "warning_threshold", + "error_threshold", + "alert_severity", + "name", + "run_interval", + "disk", + "fails_b4_alert", + "ip", + "script", + "script_args", + "info_return_codes", + "warning_return_codes", + "timeout", + "svc_name", + "svc_display_name", + "svc_policy_mode", + "pass_if_start_pending", + "pass_if_svc_not_exist", + "restart_if_stopped", + "log_name", + "event_id", + "event_id_is_wildcard", + "event_type", + "event_source", + "event_message", + "fail_when", + "search_last_days", + "number_of_events_b4_alert", + "email_alert", + "text_alert", + "dashboard_alert", +] + + +WEEK_DAYS = { + "Sunday": 0x1, + "Monday": 0x2, + "Tuesday": 0x4, + "Wednesday": 0x8, + "Thursday": 0x10, + "Friday": 0x20, + "Saturday": 0x40, +} + +MONTHS = { + "January": 0x1, + "February": 0x2, + "March": 0x4, + "April": 0x8, + "May": 0x10, + "June": 0x20, + "July": 0x40, + "August": 0x80, + "September": 0x100, + "October": 0x200, + "November": 0x400, + "December": 0x800, +} + +WEEKS = { + "First Week": 0x1, + "Second Week": 0x2, + "Third Week": 0x4, + "Fourth Week": 0x8, + "Last Week": 0x10, +} + +MONTH_DAYS = {f"{b}": 0x1 << a for a, b in enumerate(range(1, 32))} +MONTH_DAYS["Last Day"] = 0x80000000 + +DEMO_NOT_ALLOWED = [ + {"name": "AgentProcesses", "methods": ["DELETE"]}, + {"name": "AgentMeshCentral", "methods": ["GET", "POST"]}, + {"name": "update_agents", "methods": ["POST"]}, + {"name": "send_raw_cmd", "methods": ["POST"]}, + {"name": "install_agent", "methods": ["POST"]}, + {"name": "GenerateAgent", "methods": ["GET"]}, + {"name": "email_test", "methods": ["POST"]}, + {"name": "server_maintenance", "methods": ["POST"]}, + {"name": "CodeSign", "methods": ["PATCH", "POST"]}, + {"name": "TwilioSMSTest", "methods": ["POST"]}, + {"name": "GetEditActionService", "methods": ["PUT", "POST"]}, + {"name": "TestScript", "methods": ["POST"]}, + {"name": "GetUpdateDeleteAgent", "methods": ["DELETE"]}, + {"name": "Reboot", "methods": ["POST", "PATCH"]}, + {"name": "recover", "methods": ["POST"]}, + {"name": "run_script", "methods": ["POST"]}, + {"name": "bulk", "methods": ["POST"]}, + {"name": "WMI", "methods": ["POST"]}, + {"name": "PolicyAutoTask", "methods": ["POST"]}, + {"name": "RunAutoTask", "methods": ["POST"]}, + {"name": "run_checks", "methods": ["POST"]}, + {"name": "GetSoftware", "methods": ["POST", "PUT"]}, + {"name": "ScanWindowsUpdates", "methods": ["POST"]}, + {"name": "InstallWindowsUpdates", "methods": ["POST"]}, + {"name": "PendingActions", "methods": ["DELETE"]}, + {"name": "clear_cache", "methods": ["GET"]}, + {"name": "ResetPass", "methods": ["PUT"]}, + {"name": "Reset2FA", "methods": ["PUT"]}, + {"name": "bulk_run_checks", "methods": ["GET"]}, + {"name": "OpenAICodeCompletion", "methods": ["POST"]}, + {"name": "wol", "methods": ["POST"]}, +] + +CONFIG_MGMT_CMDS = ( + "api", + "version", + "webversion", + "meshver", + "natsver", + "frontend", + "webdomain", + "djangoadmin", + "setuptoolsver", + "wheelver", + "dbname", + "dbuser", + "dbhost", + "dbpw", + "dbport", + "meshsite", + "meshuser", + "meshtoken", + "meshdomain", +) diff --git a/api/tacticalrmm/tacticalrmm/demo_data.py b/api/tacticalrmm/tacticalrmm/demo_data.py new file mode 100644 index 0000000000..fa82446372 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/demo_data.py @@ -0,0 +1,754 @@ +disks = [ + [ + { + "free": "343.3G", + "used": "121.9G", + "total": "465.3G", + "device": "C:", + "fstype": "NTFS", + "percent": 26, + }, + { + "free": "745.2G", + "used": "1.1T", + "total": "1.8T", + "device": "D:", + "fstype": "NTFS", + "percent": 59, + }, + { + "free": "1.2T", + "used": "669.7G", + "total": "1.8T", + "device": "F:", + "fstype": "NTFS", + "percent": 36, + }, + ], + [ + { + "free": "516.7G", + "used": "413.5G", + "total": "930.2G", + "device": "C:", + "fstype": "NTFS", + "percent": 44, + } + ], + [ + { + "free": "346.5G", + "used": "129.1G", + "total": "475.6G", + "device": "C:", + "fstype": "NTFS", + "percent": 27, + } + ], + [ + { + "free": "84.2G", + "used": "34.4G", + "total": "118.6G", + "device": "C:", + "fstype": "NTFS", + "percent": 29, + } + ], +] + +disks_linux_pi = [ + { + "free": "109.0 GB", + "used": "3.4 GB", + "total": "117.2 GB", + "device": "/dev/mmcblk0p2", + "fstype": "ext4", + "percent": 3, + }, + { + "free": "203.8 MB", + "used": "48.3 MB", + "total": "252.0 MB", + "device": "/dev/mmcblk0p1", + "fstype": "vfat", + "percent": 19, + }, +] + +disks_linux_deb = [ + { + "free": "9.8 GB", + "used": "9.0 GB", + "total": "19.8 GB", + "device": "/dev/vda1", + "fstype": "ext4", + "percent": 47, + }, + { + "free": "62.6 GB", + "used": "414.7 GB", + "total": "503.0 GB", + "device": "/dev/sda1", + "fstype": "ext4", + "percent": 86, + }, +] + +disks_mac = [ + { + "free": "94.2 GB", + "used": "134.1 GB", + "total": "228.3 GB", + "device": "/dev/disk3s1s1", + "fstype": "apfs", + "percent": 58, + }, + { + "free": "481.6 MB", + "used": "18.4 MB", + "total": "500.0 MB", + "device": "/dev/disk1s3", + "fstype": "apfs", + "percent": 3, + }, + { + "free": "3.4 GB", + "used": "1.6 GB", + "total": "5.0 GB", + "device": "/dev/disk2s1", + "fstype": "apfs", + "percent": 32, + }, + { + "free": "94.2 GB", + "used": "134.1 GB", + "total": "228.3 GB", + "device": "/dev/disk3s1", + "fstype": "apfs", + "percent": 58, + }, +] + +wmi_deb = { + "cpus": ["AMD Ryzen 9 3900X 12-Core Processor"], + "gpus": ["Cirrus Logic GD 5446"], + "disks": ["BUYVM SLAB SCSI HDD sda 512.0 GB", "0x1af4 virtio HDD vda 20.0 GB"], + "local_ips": ["203.121.23.54/24", "fd70::253:70dc:fe65:143/64"], + "make_model": "QEMU pc-i440fx-3.1", +} + +wmi_pi = { + "cpus": ["ARMv7 Processor rev 5 (v7l)"], + "gpus": [], + "disks": ["MMC SSD mmcblk0 119.4 GB"], + "local_ips": ["192.168.33.10/24", "fe10::3332:4hgr:9634:1097/64"], + "make_model": "Raspberry Pi 2 Model B Rev 1.1", +} + +wmi_mac = { + "cpus": ["Apple M1"], + "gpus": [], + "disks": [ + "Apple APPLE SSD AP0256Q SCSI SSD disk0 233.8 GB", + "Apple APPLE SSD AP0256Q SCSI SSD disk1 500.0 MB", + "Apple APPLE SSD AP0256Q SCSI SSD disk2 5.0 GB", + "Apple APPLE SSD AP0256Q SCSI SSD disk3 228.3 GB", + ], + "local_ips": [ + "192.168.45.113/24", + "fe80::476:c390:c8dc:11af/64", + ], + "make_model": "MacBookAir10,1", +} + +check_network_loc_aware_ps1 = r""" +$networkstatus = Get-NetConnectionProfile | Select NetworkCategory | Out-String + +if ($networkstatus.Contains("DomainAuthenticated")) { + exit 0 +} else { + exit 1 +} +""" + +check_storage_pool_health_ps1 = r""" +$pools = Get-VirtualDisk | select -ExpandProperty HealthStatus + +$err = $False + +ForEach ($pool in $pools) { + if ($pool -ne "Healthy") { + $err = $True + } +} + +if ($err) { + exit 1 +} else { + exit 0 +} +""" + +clear_print_spool_bat = r""" +@echo off + +net stop spooler + +del C:\Windows\System32\spool\printers\* /Q /F /S + +net start spooler +""" + +restart_nla_ps1 = r""" +Restart-Service NlaSvc -Force +""" + +show_temp_dir_py = r""" +#!/usr/bin/python3 + +import os + +temp_dir = "C:\\Windows\\Temp" +files = [] +total = 0 + +with os.scandir(temp_dir) as it: + for f in it: + file = {} + if not f.name.startswith(".") and f.is_file(): + + total += 1 + stats = f.stat() + + file["name"] = f.name + file["size"] = stats.st_size + file["mtime"] = stats.st_mtime + + files.append(file) + + print(f"Total files: {total}\n") + + for file in files: + print(file) + +""" + +redhat_insights = r""" +#!/bin/bash + +# poor man’s red hat insights + +# this script mimics what ansible does with red hat insights +# pass it a file containing all RHSA’s you want to patch, one per line +# it concatenates the advisories into a single yum command + +if [ $# -eq 0 ] +then + echo "Usage: $0 " + exit 1 +fi + +DT=$(date '+%m%d%Y%H%M') + +SRCFILE=$1 + +for i in $(cat $SRCFILE) +do + ARGS+=" --advisory $i" +done + +CHECK="yum check-update -q" +CMD_CHECK="${CHECK}${ARGS}" + +eval ${CMD_CHECK} >> /var/tmp/patch-$(hostname)-${DT}.output 2>&1 + +if [ $? -eq 100 ]; then + UPDATE="yum update -d 2 -y" + CMD_UPDATE="${UPDATE}${ARGS}" + eval ${CMD_UPDATE} >> /var/tmp/patch-$(hostname)-${DT}.output 2>&1 +else + echo "error: exit code must be 100. fix yum errors and try again" +fi +""" + +ping_success_output = """ +Pinging 8.8.8.8 with 32 bytes of data: +Reply from 8.8.8.8: bytes=32 time=28ms TTL=116 +Reply from 8.8.8.8: bytes=32 time=26ms TTL=116 +Reply from 8.8.8.8: bytes=32 time=29ms TTL=116 +Reply from 8.8.8.8: bytes=32 time=23ms TTL=116 + +Ping statistics for 8.8.8.8: + Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), +Approximate round trip times in milli-seconds: + Minimum = 23ms, Maximum = 29ms, Average = 26ms +""" + +ping_fail_output = """ +Pinging 10.42.33.2 with 32 bytes of data: +Request timed out. +Request timed out. +Request timed out. +Request timed out. + +Ping statistics for 10.42.33.2: +Packets: Sent = 4, Received = 0, Lost = 4 (100% loss), +""" + +spooler_stdout = """ +SERVICE_NAME: spooler + TYPE : 110 WIN32_OWN_PROCESS (interactive) + STATE : 3 STOP_PENDING + (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN) + WIN32_EXIT_CODE : 0 (0x0) + SERVICE_EXIT_CODE : 0 (0x0) + CHECKPOINT : 0x0 + WAIT_HINT : 0x0 +Deleted file - C:\Windows\System32\spool\printers\FP00004.SHD +Deleted file - C:\Windows\System32\spool\printers\FP00004.SPL + +SERVICE_NAME: spooler + TYPE : 110 WIN32_OWN_PROCESS (interactive) + STATE : 2 START_PENDING + (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN) + WIN32_EXIT_CODE : 0 (0x0) + SERVICE_EXIT_CODE : 0 (0x0) + CHECKPOINT : 0x0 + WAIT_HINT : 0x7d0 + PID : 10536 + FLAGS : +""" + + +temp_dir_stdout = """ +Total files: 427 + +{'name': '2E71.tmp', 'size': 7430272, 'mtime': 1581925416.2497344} +{'name': 'AdobeARM.log', 'size': 29451, 'mtime': 1594655619.9011872} +{'name': 'adobegc.log', 'size': 10231328, 'mtime': 1595040481.91346} +{'name': 'adobegc_a00168', 'size': 827, 'mtime': 1587681946.9771478} +{'name': 'adobegc_a00736', 'size': 827, 'mtime': 1588706044.6594567} +{'name': 'adobegc_a01612', 'size': 827, 'mtime': 1580168032.7042644} +{'name': 'adobegc_a01872', 'size': 827, 'mtime': 1588695409.1667633} +{'name': 'adobegc_a02040', 'size': 827, 'mtime': 1586472391.868406} +{'name': 'adobegc_a02076', 'size': 827, 'mtime': 1580250789.654343} +{'name': 'adobegc_a02316', 'size': 827, 'mtime': 1584469722.280189} +{'name': 'adobegc_a02840', 'size': 827, 'mtime': 1580168195.0954776} +{'name': 'adobegc_a02844', 'size': 827, 'mtime': 1588704553.4443035} +{'name': 'adobegc_a02940', 'size': 827, 'mtime': 1588705125.4622736} +{'name': 'adobegc_a03388', 'size': 827, 'mtime': 1588694931.7341642} +{'name': 'adobegc_a03444', 'size': 827, 'mtime': 1588694575.377482} +{'name': 'adobegc_a03468', 'size': 827, 'mtime': 1588705816.5495117} +{'name': 'adobegc_a03516', 'size': 827, 'mtime': 1588695236.1638494} +{'name': 'adobegc_a03660', 'size': 827, 'mtime': 1588694714.0769584} +{'name': 'adobegc_a03668', 'size': 827, 'mtime': 1588791976.2615259} +{'name': 'adobegc_a03984', 'size': 827, 'mtime': 1588708060.4916122} +{'name': 'adobegc_a04244', 'size': 827, 'mtime': 1588882348.195425} +{'name': 'adobegc_a04296', 'size': 827, 'mtime': 1587595547.000954} +{'name': 'adobegc_a04400', 'size': 827, 'mtime': 1588698785.5022683} +{'name': 'adobegc_a04476', 'size': 827, 'mtime': 1588696181.497377} +{'name': 'adobegc_a04672', 'size': 827, 'mtime': 1588707309.2342112} +{'name': 'adobegc_a05072', 'size': 827, 'mtime': 1588718744.760823} +{'name': 'adobegc_a05308', 'size': 827, 'mtime': 1588884352.0702107} +{'name': 'adobegc_a05372', 'size': 827, 'mtime': 1587571313.9485312} +{'name': 'adobegc_a06196', 'size': 826, 'mtime': 1594654959.318391} +{'name': 'adobegc_a07432', 'size': 827, 'mtime': 1588887412.235366} +{'name': 'adobegc_a07592', 'size': 827, 'mtime': 1587768346.7856867} +{'name': 'adobegc_a08336', 'size': 827, 'mtime': 1580251587.8173583} +{'name': 'adobegc_a08540', 'size': 826, 'mtime': 1590517886.4135766} +{'name': 'adobegc_a08676', 'size': 827, 'mtime': 1588796865.261678} +{'name': 'adobegc_a08788', 'size': 827, 'mtime': 1586385998.4148164} +{'name': 'adobegc_a09164', 'size': 827, 'mtime': 1588882638.920801} +{'name': 'adobegc_a10672', 'size': 827, 'mtime': 1580142397.240663} +{'name': 'adobegc_a11260', 'size': 827, 'mtime': 1588791820.5066414} +{'name': 'adobegc_a12180', 'size': 827, 'mtime': 1580146831.0441327} +{'name': 'adobegc_a14468', 'size': 827, 'mtime': 1585674106.878755} +{'name': 'adobegc_a14596', 'size': 827, 'mtime': 1580510788.5562158} +{'name': 'adobegc_a15124', 'size': 826, 'mtime': 1590523889.367007} +{'name': 'adobegc_a15936', 'size': 827, 'mtime': 1580256796.572934} +{'name': 'adobegc_a15992', 'size': 826, 'mtime': 1594664396.6619377} +{'name': 'adobegc_a16976', 'size': 827, 'mtime': 1585674384.4933422} +{'name': 'adobegc_a18972', 'size': 826, 'mtime': 1594748694.4924471} +{'name': 'adobegc_a19836', 'size': 827, 'mtime': 1588880974.3856514} +{'name': 'adobegc_a20168', 'size': 827, 'mtime': 1580256300.931633} +{'name': 'adobegc_a20424', 'size': 826, 'mtime': 1590619548.096738} +{'name': 'adobegc_a20476', 'size': 827, 'mtime': 1580241090.30506} +{'name': 'adobegc_a20696', 'size': 827, 'mtime': 1588883054.266526} +{'name': 'adobegc_a21160', 'size': 827, 'mtime': 1585867545.8835862} +{'name': 'adobegc_a21600', 'size': 827, 'mtime': 1584546053.6350517} +{'name': 'adobegc_a21604', 'size': 827, 'mtime': 1585781145.016732} +{'name': 'adobegc_a23208', 'size': 826, 'mtime': 1594766767.8597474} +{'name': 'adobegc_a23792', 'size': 827, 'mtime': 1587748006.602304} +{'name': 'adobegc_a24996', 'size': 827, 'mtime': 1580337748.2107458} +{'name': 'adobegc_a25280', 'size': 827, 'mtime': 1589561457.17108} +{'name': 'adobegc_a25716', 'size': 827, 'mtime': 1586558746.818827} +{'name': 'adobegc_a26788', 'size': 827, 'mtime': 1589317959.1261017} +{'name': 'adobegc_a29788', 'size': 826, 'mtime': 1594853168.0936923} +{'name': 'adobegc_a30772', 'size': 827, 'mtime': 1586645146.8381186} +{'name': 'adobegc_a30868', 'size': 826, 'mtime': 1590705947.4294834} +{'name': 'adobegc_a33340', 'size': 827, 'mtime': 1580424388.6278617} +{'name': 'adobegc_a34072', 'size': 826, 'mtime': 1591036884.930815} +{'name': 'adobegc_a34916', 'size': 827, 'mtime': 1589063225.3951604} +{'name': 'adobegc_a36312', 'size': 826, 'mtime': 1590792347.1066074} +{'name': 'adobegc_a36732', 'size': 827, 'mtime': 1587941146.7667546} +{'name': 'adobegc_a37684', 'size': 826, 'mtime': 1591051545.0491257} +{'name': 'adobegc_a38820', 'size': 826, 'mtime': 1594939568.850206} +{'name': 'adobegc_a39800', 'size': 827, 'mtime': 1586731547.200965} +{'name': 'adobegc_a39968', 'size': 827, 'mtime': 1585953945.0148494} +{'name': 'adobegc_a40276', 'size': 827, 'mtime': 1580774658.3211977} +{'name': 'adobegc_a40312', 'size': 827, 'mtime': 1589237146.946895} +{'name': 'adobegc_a40988', 'size': 827, 'mtime': 1586817946.949773} +{'name': 'adobegc_a40992', 'size': 827, 'mtime': 1580770793.4948478} +{'name': 'adobegc_a41180', 'size': 827, 'mtime': 1588027546.7357743} +{'name': 'adobegc_a41188', 'size': 826, 'mtime': 1595009475.295114} +{'name': 'adobegc_a41640', 'size': 826, 'mtime': 1590878747.5881732} +{'name': 'adobegc_a42100', 'size': 827, 'mtime': 1589150748.9527063} +{'name': 'adobegc_a43012', 'size': 827, 'mtime': 1580683588.5658195} +{'name': 'adobegc_a44676', 'size': 827, 'mtime': 1580597188.6451297} +{'name': 'adobegc_a45184', 'size': 827, 'mtime': 1586904346.5828853} +{'name': 'adobegc_a45308', 'size': 826, 'mtime': 1595025969.4777381} +{'name': 'adobegc_a46804', 'size': 827, 'mtime': 1580772007.5569854} +{'name': 'adobegc_a47368', 'size': 827, 'mtime': 1588100330.3886814} +{'name': 'adobegc_a47428', 'size': 827, 'mtime': 1589307202.4241476} +{'name': 'adobegc_a48120', 'size': 827, 'mtime': 1587061429.3050117} +{'name': 'adobegc_a48264', 'size': 827, 'mtime': 1586040345.9605994} +{'name': 'adobegc_a49348', 'size': 827, 'mtime': 1589308572.5917764} +{'name': 'adobegc_a50068', 'size': 827, 'mtime': 1589823616.2651317} +{'name': 'adobegc_a50512', 'size': 827, 'mtime': 1588113946.9230535} +{'name': 'adobegc_a54396', 'size': 826, 'mtime': 1590965147.3472395} +{'name': 'adobegc_a55764', 'size': 827, 'mtime': 1586126745.5002806} +{'name': 'adobegc_a56868', 'size': 827, 'mtime': 1584988994.5648835} +{'name': 'adobegc_a56920', 'size': 827, 'mtime': 1589826940.2840052} +{'name': 'adobegc_a58060', 'size': 827, 'mtime': 1588200346.9590664} +{'name': 'adobegc_a58664', 'size': 827, 'mtime': 1580772553.408082} +{'name': 'adobegc_a58836', 'size': 827, 'mtime': 1586213145.9856122} +{'name': 'adobegc_a58952', 'size': 827, 'mtime': 1580769957.2542257} +{'name': 'adobegc_a59448', 'size': 827, 'mtime': 1586191309.3788278} +{'name': 'adobegc_a59920', 'size': 827, 'mtime': 1580837382.8384278} +{'name': 'adobegc_a60092', 'size': 827, 'mtime': 1589820894.3119876} +{'name': 'adobegc_a60188', 'size': 827, 'mtime': 1580773319.7630682} +{'name': 'adobegc_a60376', 'size': 827, 'mtime': 1584984234.995152} +{'name': 'adobegc_a60836', 'size': 827, 'mtime': 1586990747.0581498} +{'name': 'adobegc_a61768', 'size': 826, 'mtime': 1591035116.5898964} +{'name': 'adobegc_a62200', 'size': 827, 'mtime': 1586195417.8275757} +{'name': 'adobegc_a62432', 'size': 827, 'mtime': 1580942790.5409286} +{'name': 'adobegc_a65288', 'size': 827, 'mtime': 1588373147.0190327} +{'name': 'adobegc_a65332', 'size': 827, 'mtime': 1580838023.0994027} +{'name': 'adobegc_a65672', 'size': 826, 'mtime': 1591133604.032657} +{'name': 'adobegc_a66164', 'size': 827, 'mtime': 1580837511.0639248} +{'name': 'adobegc_a66532', 'size': 827, 'mtime': 1587077146.9995973} +{'name': 'adobegc_a66744', 'size': 827, 'mtime': 1587061281.624075} +{'name': 'adobegc_a68000', 'size': 826, 'mtime': 1591137947.4248047} +{'name': 'adobegc_a68072', 'size': 827, 'mtime': 1589928347.070936} +{'name': 'adobegc_a68720', 'size': 827, 'mtime': 1581029189.2618403} +{'name': 'adobegc_a68848', 'size': 827, 'mtime': 1587163546.6515636} +{'name': 'adobegc_a69732', 'size': 827, 'mtime': 1580930519.3353608} +{'name': 'adobegc_a70528', 'size': 827, 'mtime': 1589841947.731212} +{'name': 'adobegc_a71096', 'size': 827, 'mtime': 1580920745.8008296} +{'name': 'adobegc_a71132', 'size': 827, 'mtime': 1586299545.3437998} +{'name': 'adobegc_a71648', 'size': 827, 'mtime': 1581031075.2702963} +{'name': 'adobegc_a72972', 'size': 827, 'mtime': 1588626359.7385614} +{'name': 'adobegc_a75840', 'size': 826, 'mtime': 1591373471.8618608} +{'name': 'adobegc_a76096', 'size': 827, 'mtime': 1581030280.123038} +{'name': 'adobegc_a76636', 'size': 827, 'mtime': 1581010194.8814292} +{'name': 'adobegc_a76928', 'size': 827, 'mtime': 1586368563.2366085} +{'name': 'adobegc_a78272', 'size': 827, 'mtime': 1588459547.364358} +{'name': 'adobegc_a78448', 'size': 827, 'mtime': 1589755547.709028} +{'name': 'adobegc_a78868', 'size': 827, 'mtime': 1587249947.0512784} +{'name': 'adobegc_a79232', 'size': 827, 'mtime': 1590014747.2459671} +{'name': 'adobegc_a80708', 'size': 827, 'mtime': 1587854746.995757} +{'name': 'adobegc_a81928', 'size': 826, 'mtime': 1591387037.7909682} +{'name': 'adobegc_a82640', 'size': 827, 'mtime': 1588620563.6037686} +{'name': 'adobegc_a84680', 'size': 827, 'mtime': 1588622988.5522242} +{'name': 'adobegc_a86668', 'size': 827, 'mtime': 1588632347.3290584} +{'name': 'adobegc_a87760', 'size': 826, 'mtime': 1591389738.9057505} +{'name': 'adobegc_a87796', 'size': 827, 'mtime': 1588620113.9931662} +{'name': 'adobegc_a88000', 'size': 827, 'mtime': 1587336346.5873897} +{'name': 'adobegc_a88772', 'size': 827, 'mtime': 1588545946.2672083} +{'name': 'adobegc_a88864', 'size': 826, 'mtime': 1591388571.4809685} +{'name': 'adobegc_a90492', 'size': 826, 'mtime': 1591569945.3237975} +{'name': 'adobegc_a90536', 'size': 827, 'mtime': 1588615525.34365} +{'name': 'adobegc_a90696', 'size': 827, 'mtime': 1588618638.6518161} +{'name': 'adobegc_a92020', 'size': 827, 'mtime': 1588626079.888435} +{'name': 'adobegc_a92036', 'size': 827, 'mtime': 1588883650.2998574} +{'name': 'adobegc_a92060', 'size': 827, 'mtime': 1587422746.7982752} +{'name': 'adobegc_a92332', 'size': 827, 'mtime': 1588617429.6228204} +{'name': 'adobegc_a92708', 'size': 827, 'mtime': 1588621683.480289} +{'name': 'adobegc_a93576', 'size': 827, 'mtime': 1588611949.1138964} +{'name': 'adobegc_a93952', 'size': 826, 'mtime': 1591483547.0099566} +{'name': 'adobegc_a93968', 'size': 827, 'mtime': 1588619947.3429031} +{'name': 'adobegc_a94188', 'size': 827, 'mtime': 1588625869.6090748} +{'name': 'adobegc_a94428', 'size': 827, 'mtime': 1588625083.0555425} +{'name': 'adobegc_a94564', 'size': 827, 'mtime': 1587400776.9892576} +{'name': 'adobegc_a94620', 'size': 827, 'mtime': 1588616005.2649503} +{'name': 'adobegc_a94672', 'size': 827, 'mtime': 1588608305.686614} +{'name': 'adobegc_a95104', 'size': 827, 'mtime': 1588619862.1936185} +{'name': 'adobegc_a95268', 'size': 827, 'mtime': 1588618316.1273627} +{'name': 'adobegc_a95992', 'size': 827, 'mtime': 1588625699.327125} +{'name': 'adobegc_a96116', 'size': 827, 'mtime': 1588625465.4000483} +{'name': 'adobegc_a96140', 'size': 827, 'mtime': 1588707579.585134} +{'name': 'adobegc_a96196', 'size': 827, 'mtime': 1588616659.9653125} +{'name': 'adobegc_a96264', 'size': 827, 'mtime': 1588624388.424492} +{'name': 'adobegc_a96396', 'size': 827, 'mtime': 1588619230.3394928} +{'name': 'adobegc_a97428', 'size': 827, 'mtime': 1587509147.0930684} +{'name': 'adobegc_a97480', 'size': 827, 'mtime': 1589669147.1720312} +{'name': 'adobegc_a97532', 'size': 827, 'mtime': 1588623710.0535893} +{'name': 'adobegc_a97576', 'size': 827, 'mtime': 1588699829.405278} +{'name': 'adobegc_a97888', 'size': 827, 'mtime': 1588700236.4738493} +{'name': 'adobegc_a97936', 'size': 827, 'mtime': 1588705581.3051977} +{'name': 'adobegc_a98628', 'size': 827, 'mtime': 1588707770.5158248} +{'name': 'adobegc_a98676', 'size': 827, 'mtime': 1588707160.0849242} +{'name': 'adobegc_a99320', 'size': 827, 'mtime': 1588286747.3271153} +{'name': 'adobegc_a99416', 'size': 827, 'mtime': 1588705913.0032701} +{'name': 'adobegc_a99776', 'size': 827, 'mtime': 1588695055.6383822} +{'name': 'adobegc_a99944', 'size': 827, 'mtime': 1588700090.9956398} +{'name': 'adobegc_b00736', 'size': 827, 'mtime': 1588706066.725238} +{'name': 'adobegc_b01872', 'size': 827, 'mtime': 1588695416.625433} +{'name': 'adobegc_b02844', 'size': 827, 'mtime': 1588704612.7520032} +{'name': 'adobegc_b02940', 'size': 827, 'mtime': 1588705218.2862568} +{'name': 'adobegc_b03516', 'size': 827, 'mtime': 1588695279.1507645} +{'name': 'adobegc_b03668', 'size': 827, 'mtime': 1588791984.8225732} +{'name': 'adobegc_b03984', 'size': 827, 'mtime': 1588708170.4855063} +{'name': 'adobegc_b04400', 'size': 827, 'mtime': 1588698790.8114717} +{'name': 'adobegc_b06196', 'size': 826, 'mtime': 1594655070.3379285} +{'name': 'adobegc_b08540', 'size': 826, 'mtime': 1590517989.972172} +{'name': 'adobegc_b08676', 'size': 827, 'mtime': 1588796952.7518158} +{'name': 'adobegc_b11260', 'size': 827, 'mtime': 1588791830.28458} +{'name': 'adobegc_b12180', 'size': 827, 'mtime': 1580146854.104489} +{'name': 'adobegc_b14468', 'size': 827, 'mtime': 1585674135.6150348} +{'name': 'adobegc_b15992', 'size': 826, 'mtime': 1594664406.76352} +{'name': 'adobegc_b18972', 'size': 826, 'mtime': 1594748752.0301268} +{'name': 'adobegc_b20424', 'size': 826, 'mtime': 1590619550.6114154} +{'name': 'adobegc_b20696', 'size': 827, 'mtime': 1588883091.2836785} +{'name': 'adobegc_b25280', 'size': 827, 'mtime': 1589561471.058807} +{'name': 'adobegc_b26788', 'size': 827, 'mtime': 1589318049.2721062} +{'name': 'adobegc_b30868', 'size': 826, 'mtime': 1590705949.9086082} +{'name': 'adobegc_b34072', 'size': 826, 'mtime': 1591036916.1677504} +{'name': 'adobegc_b36312', 'size': 826, 'mtime': 1590792349.6286027} +{'name': 'adobegc_b37684', 'size': 826, 'mtime': 1591051547.7088954} +{'name': 'adobegc_b41188', 'size': 826, 'mtime': 1595009499.2530031} +{'name': 'adobegc_b41640', 'size': 826, 'mtime': 1590878750.2055979} +{'name': 'adobegc_b48120', 'size': 827, 'mtime': 1587061437.18547} +{'name': 'adobegc_b49348', 'size': 827, 'mtime': 1589308608.9336922} +{'name': 'adobegc_b50068', 'size': 827, 'mtime': 1589823624.2151668} +{'name': 'adobegc_b54396', 'size': 826, 'mtime': 1590965149.8471487} +{'name': 'adobegc_b56868', 'size': 827, 'mtime': 1584989020.8257363} +{'name': 'adobegc_b56920', 'size': 827, 'mtime': 1589826973.5304308} +{'name': 'adobegc_b58952', 'size': 827, 'mtime': 1580770043.2167466} +{'name': 'adobegc_b59448', 'size': 827, 'mtime': 1586191317.2202032} +{'name': 'adobegc_b60376', 'size': 827, 'mtime': 1584984269.807791} +{'name': 'adobegc_b68000', 'size': 826, 'mtime': 1591137949.8555748} +{'name': 'adobegc_b68072', 'size': 827, 'mtime': 1589928349.6981187} +{'name': 'adobegc_b70528', 'size': 827, 'mtime': 1589841950.8458745} +{'name': 'adobegc_b71096', 'size': 827, 'mtime': 1580920761.6914532} +{'name': 'adobegc_b72972', 'size': 827, 'mtime': 1588626390.183644} +{'name': 'adobegc_b76636', 'size': 827, 'mtime': 1581010200.9350817} +{'name': 'adobegc_b78448', 'size': 827, 'mtime': 1589755550.4021} +{'name': 'adobegc_b79232', 'size': 827, 'mtime': 1590014749.9412005} +{'name': 'adobegc_b82640', 'size': 827, 'mtime': 1588620586.923453} +{'name': 'adobegc_b84680', 'size': 827, 'mtime': 1588623002.5390074} +{'name': 'adobegc_b87796', 'size': 827, 'mtime': 1588620149.2323031} +{'name': 'adobegc_b90536', 'size': 827, 'mtime': 1588615561.6454446} +{'name': 'adobegc_b90696', 'size': 827, 'mtime': 1588618646.516128} +{'name': 'adobegc_b92020', 'size': 827, 'mtime': 1588626116.4113202} +{'name': 'adobegc_b92332', 'size': 827, 'mtime': 1588617466.6833763} +{'name': 'adobegc_b92708', 'size': 827, 'mtime': 1588621723.2322977} +{'name': 'adobegc_b93968', 'size': 827, 'mtime': 1588619970.3566632} +{'name': 'adobegc_b94188', 'size': 827, 'mtime': 1588625878.801097} +{'name': 'adobegc_b94428', 'size': 827, 'mtime': 1588625091.057683} +{'name': 'adobegc_b94564', 'size': 827, 'mtime': 1587400800.9059412} +{'name': 'adobegc_b95268', 'size': 827, 'mtime': 1588618334.0967414} +{'name': 'adobegc_b95992', 'size': 827, 'mtime': 1588625737.972303} +{'name': 'adobegc_b96116', 'size': 827, 'mtime': 1588625472.4204888} +{'name': 'adobegc_b96196', 'size': 827, 'mtime': 1588616768.8672354} +{'name': 'adobegc_b96396', 'size': 827, 'mtime': 1588619236.3330257} +{'name': 'adobegc_b97480', 'size': 827, 'mtime': 1589669149.7252228} +{'name': 'adobegc_b97532', 'size': 827, 'mtime': 1588623738.1396592} +{'name': 'adobegc_b97576', 'size': 827, 'mtime': 1588699862.141512} +{'name': 'adobegc_b97888', 'size': 827, 'mtime': 1588700318.3893816} +{'name': 'adobegc_b97936', 'size': 827, 'mtime': 1588705599.7656307} +{'name': 'adobegc_b98628', 'size': 827, 'mtime': 1588707795.8756163} +{'name': 'adobegc_b99416', 'size': 827, 'mtime': 1588705935.8479679} +{'name': 'adobegc_b99776', 'size': 827, 'mtime': 1588695083.277253} +{'name': 'adobegc_b99944', 'size': 827, 'mtime': 1588700116.4428499} +{'name': 'adobegc_c00736', 'size': 827, 'mtime': 1588706144.523482} +{'name': 'adobegc_c01872', 'size': 827, 'mtime': 1588695424.6709175} +{'name': 'adobegc_c02844', 'size': 827, 'mtime': 1588704655.3452854} +{'name': 'adobegc_c02940', 'size': 827, 'mtime': 1588705301.4180279} +{'name': 'adobegc_c03984', 'size': 827, 'mtime': 1588708227.6767087} +{'name': 'adobegc_c04400', 'size': 827, 'mtime': 1588698805.7789137} +{'name': 'adobegc_c08676', 'size': 827, 'mtime': 1588796987.8076794} +{'name': 'adobegc_c11260', 'size': 827, 'mtime': 1588791857.2477975} +{'name': 'adobegc_c12180', 'size': 827, 'mtime': 1580146876.464384} +{'name': 'adobegc_c15992', 'size': 826, 'mtime': 1594664430.9030519} +{'name': 'adobegc_c20696', 'size': 827, 'mtime': 1588883097.26129} +{'name': 'adobegc_c25280', 'size': 827, 'mtime': 1589561487.9573958} +{'name': 'adobegc_c26788', 'size': 827, 'mtime': 1589318109.375684} +{'name': 'adobegc_c34072', 'size': 826, 'mtime': 1591036933.363417} +{'name': 'adobegc_c48120', 'size': 827, 'mtime': 1587061454.0755453} +{'name': 'adobegc_c56920', 'size': 827, 'mtime': 1589826993.0616467} +{'name': 'adobegc_c59448', 'size': 827, 'mtime': 1586191349.8506114} +{'name': 'adobegc_c60376', 'size': 827, 'mtime': 1584984292.1612866} +{'name': 'adobegc_c72972', 'size': 827, 'mtime': 1588626413.0896137} +{'name': 'adobegc_c76636', 'size': 827, 'mtime': 1581010218.0554078} +{'name': 'adobegc_c82640', 'size': 827, 'mtime': 1588620613.321756} +{'name': 'adobegc_c84680', 'size': 827, 'mtime': 1588623117.9436429} +{'name': 'adobegc_c87796', 'size': 827, 'mtime': 1588620230.1520216} +{'name': 'adobegc_c92020', 'size': 827, 'mtime': 1588626141.4125187} +{'name': 'adobegc_c92332', 'size': 827, 'mtime': 1588617496.3456864} +{'name': 'adobegc_c93968', 'size': 827, 'mtime': 1588619998.5936964} +{'name': 'adobegc_c94428', 'size': 827, 'mtime': 1588625116.0481493} +{'name': 'adobegc_c94564', 'size': 827, 'mtime': 1587400814.941493} +{'name': 'adobegc_c95268', 'size': 827, 'mtime': 1588618430.4614644} +{'name': 'adobegc_c95992', 'size': 827, 'mtime': 1588625744.1483426} +{'name': 'adobegc_c97532', 'size': 827, 'mtime': 1588623768.123971} +{'name': 'adobegc_c97576', 'size': 827, 'mtime': 1588699912.811693} +{'name': 'adobegc_c98628', 'size': 827, 'mtime': 1588707823.850915} +{'name': 'adobegc_c99416', 'size': 827, 'mtime': 1588705942.7441413} +{'name': 'adobegc_c99944', 'size': 827, 'mtime': 1588700140.0327764} +{'name': 'adobegc_d00736', 'size': 827, 'mtime': 1588706212.1906126} +{'name': 'adobegc_d02844', 'size': 827, 'mtime': 1588704712.9487145} +{'name': 'adobegc_d02940', 'size': 827, 'mtime': 1588705320.1099153} +{'name': 'adobegc_d03984', 'size': 827, 'mtime': 1588708248.2397952} +{'name': 'adobegc_d04400', 'size': 827, 'mtime': 1588698820.0670853} +{'name': 'adobegc_d12180', 'size': 827, 'mtime': 1580146895.6547296} +{'name': 'adobegc_d15992', 'size': 826, 'mtime': 1594664447.5050478} +{'name': 'adobegc_d20696', 'size': 827, 'mtime': 1588883151.742091} +{'name': 'adobegc_d34072', 'size': 826, 'mtime': 1591036946.3382795} +{'name': 'adobegc_d56920', 'size': 827, 'mtime': 1589827011.6453788} +{'name': 'adobegc_d59448', 'size': 827, 'mtime': 1586191396.4112055} +{'name': 'adobegc_d60376', 'size': 827, 'mtime': 1584984310.4665244} +{'name': 'adobegc_d72972', 'size': 827, 'mtime': 1588626429.153277} +{'name': 'adobegc_d76636', 'size': 827, 'mtime': 1581010315.7584887} +{'name': 'adobegc_d82640', 'size': 827, 'mtime': 1588620653.094543} +{'name': 'adobegc_d84680', 'size': 827, 'mtime': 1588623140.4772713} +{'name': 'adobegc_d87796', 'size': 827, 'mtime': 1588620294.8475337} +{'name': 'adobegc_d92020', 'size': 827, 'mtime': 1588626228.1945815} +{'name': 'adobegc_d94428', 'size': 827, 'mtime': 1588625122.2906866} +{'name': 'adobegc_d94564', 'size': 827, 'mtime': 1587400828.0741277} +{'name': 'adobegc_d95268', 'size': 827, 'mtime': 1588618440.307652} +{'name': 'adobegc_d97532', 'size': 827, 'mtime': 1588623787.4921527} +{'name': 'adobegc_d97576', 'size': 827, 'mtime': 1588699931.81901} +{'name': 'adobegc_d98628', 'size': 827, 'mtime': 1588707855.1049612} +{'name': 'adobegc_e00736', 'size': 827, 'mtime': 1588706245.611989} +{'name': 'adobegc_e02844', 'size': 827, 'mtime': 1588704734.7796671} +{'name': 'adobegc_e02940', 'size': 827, 'mtime': 1588705346.8015952} +{'name': 'adobegc_e03984', 'size': 827, 'mtime': 1588708267.3839262} +{'name': 'adobegc_e04400', 'size': 827, 'mtime': 1588698844.0438626} +{'name': 'adobegc_e12180', 'size': 827, 'mtime': 1580146918.2748847} +{'name': 'adobegc_e15992', 'size': 826, 'mtime': 1594664462.674065} +{'name': 'adobegc_e34072', 'size': 826, 'mtime': 1591036960.5743244} +{'name': 'adobegc_e56920', 'size': 827, 'mtime': 1589827029.9772768} +{'name': 'adobegc_e59448', 'size': 827, 'mtime': 1586191423.5797856} +{'name': 'adobegc_e60376', 'size': 827, 'mtime': 1584984320.550245} +{'name': 'adobegc_e72972', 'size': 827, 'mtime': 1588626449.11985} +{'name': 'adobegc_e82640', 'size': 827, 'mtime': 1588620658.7476456} +{'name': 'adobegc_e84680', 'size': 827, 'mtime': 1588623162.9596686} +{'name': 'adobegc_e87796', 'size': 827, 'mtime': 1588620363.3213055} +{'name': 'adobegc_e92020', 'size': 827, 'mtime': 1588626236.2562673} +{'name': 'adobegc_e94428', 'size': 827, 'mtime': 1588625177.8788607} +{'name': 'adobegc_e94564', 'size': 827, 'mtime': 1587400848.3485818} +{'name': 'adobegc_e97532', 'size': 827, 'mtime': 1588623800.5197835} +{'name': 'adobegc_e97576', 'size': 827, 'mtime': 1588699954.884931} +{'name': 'adobegc_e98628', 'size': 827, 'mtime': 1588707930.3610473} +{'name': 'adobegc_f00736', 'size': 827, 'mtime': 1588706262.6876884} +{'name': 'adobegc_f02844', 'size': 827, 'mtime': 1588704857.8128686} +{'name': 'adobegc_f02940', 'size': 827, 'mtime': 1588705386.8754816} +{'name': 'adobegc_f03984', 'size': 827, 'mtime': 1588708377.0388029} +{'name': 'adobegc_f04400', 'size': 827, 'mtime': 1588698865.876907} +{'name': 'adobegc_f12180', 'size': 827, 'mtime': 1580146941.4048574} +{'name': 'adobegc_f15992', 'size': 826, 'mtime': 1594664480.5364697} +{'name': 'adobegc_f59448', 'size': 827, 'mtime': 1586191468.308414} +{'name': 'adobegc_f60376', 'size': 827, 'mtime': 1584984342.4760692} +{'name': 'adobegc_f72972', 'size': 827, 'mtime': 1588626520.413051} +{'name': 'adobegc_f82640', 'size': 827, 'mtime': 1588620707.6957185} +{'name': 'adobegc_f84680', 'size': 827, 'mtime': 1588623185.9664042} +{'name': 'adobegc_f87796', 'size': 827, 'mtime': 1588620372.2095447} +{'name': 'adobegc_f94428', 'size': 827, 'mtime': 1588625198.4473124} +{'name': 'adobegc_f98628', 'size': 827, 'mtime': 1588707956.3923628} +{'name': 'adobegc_g00736', 'size': 827, 'mtime': 1588706340.7434888} +{'name': 'adobegc_g02844', 'size': 827, 'mtime': 1588704879.0104535} +{'name': 'adobegc_g02940', 'size': 827, 'mtime': 1588705417.8788993} +{'name': 'adobegc_g03984', 'size': 827, 'mtime': 1588708394.9106903} +{'name': 'adobegc_g04400', 'size': 827, 'mtime': 1588698895.7362301} +{'name': 'adobegc_g12180', 'size': 827, 'mtime': 1580146949.484896} +{'name': 'adobegc_g72972', 'size': 827, 'mtime': 1588626624.4677527} +{'name': 'adobegc_g82640', 'size': 827, 'mtime': 1588620723.5959775} +{'name': 'adobegc_g84680', 'size': 827, 'mtime': 1588623225.1320856} +{'name': 'adobegc_g87796', 'size': 827, 'mtime': 1588620425.5512018} +{'name': 'adobegc_g94428', 'size': 827, 'mtime': 1588625228.557094} +{'name': 'adobegc_h00736', 'size': 827, 'mtime': 1588706456.0406094} +{'name': 'adobegc_h02844', 'size': 827, 'mtime': 1588704948.776196} +{'name': 'adobegc_h02940', 'size': 827, 'mtime': 1588705450.0687082} +{'name': 'adobegc_h03984', 'size': 827, 'mtime': 1588708415.418625} +{'name': 'adobegc_h04400', 'size': 827, 'mtime': 1588698929.891593} +{'name': 'adobegc_h12180', 'size': 827, 'mtime': 1580146955.5651238} +{'name': 'adobegc_h82640', 'size': 827, 'mtime': 1588620743.5954738} +{'name': 'adobegc_h84680', 'size': 827, 'mtime': 1588623352.3280022} +{'name': 'adobegc_h87796', 'size': 827, 'mtime': 1588620447.1586652} +{'name': 'adobegc_h94428', 'size': 827, 'mtime': 1588625239.4658115} +{'name': 'adobegc_i00736', 'size': 827, 'mtime': 1588706484.0562284} +{'name': 'adobegc_i02940', 'size': 827, 'mtime': 1588705465.7495365} +{'name': 'adobegc_i03984', 'size': 827, 'mtime': 1588708539.8739815} +{'name': 'adobegc_i04400', 'size': 827, 'mtime': 1588698952.9581492} +{'name': 'adobegc_i12180', 'size': 827, 'mtime': 1580147014.8754144} +{'name': 'adobegc_i82640', 'size': 827, 'mtime': 1588620751.6867297} +{'name': 'adobegc_i84680', 'size': 827, 'mtime': 1588623400.7245765} +{'name': 'adobegc_i87796', 'size': 827, 'mtime': 1588620470.659986} +{'name': 'adobegc_i94428', 'size': 827, 'mtime': 1588625266.8207235} +{'name': 'adobegc_j00736', 'size': 827, 'mtime': 1588706506.187664} +{'name': 'adobegc_j03984', 'size': 827, 'mtime': 1588708569.6812017} +{'name': 'adobegc_j04400', 'size': 827, 'mtime': 1588698970.8107784} +{'name': 'adobegc_j12180', 'size': 827, 'mtime': 1580147035.305319} +{'name': 'adobegc_j82640', 'size': 827, 'mtime': 1588620768.686572} +{'name': 'adobegc_j87796', 'size': 827, 'mtime': 1588620476.2220924} +{'name': 'adobegc_j94428', 'size': 827, 'mtime': 1588625305.749532} +{'name': 'adobegc_k00736', 'size': 827, 'mtime': 1588706597.5977101} +{'name': 'adobegc_k03984', 'size': 827, 'mtime': 1588708585.727807} +{'name': 'adobegc_k04400', 'size': 827, 'mtime': 1588699002.9317427} +{'name': 'adobegc_k12180', 'size': 827, 'mtime': 1580147056.48849} +{'name': 'adobegc_k94428', 'size': 827, 'mtime': 1588625326.7249243} +{'name': 'adobegc_l00736', 'size': 827, 'mtime': 1588706650.0458724} +{'name': 'adobegc_l04400', 'size': 827, 'mtime': 1588699173.7167861} +{'name': 'adobegc_l12180', 'size': 827, 'mtime': 1580147075.7756407} +{'name': 'adobegc_m00736', 'size': 827, 'mtime': 1588706696.6210747} +{'name': 'adobegc_m04400', 'size': 827, 'mtime': 1588699299.9061432} +{'name': 'adobegc_n00736', 'size': 827, 'mtime': 1588706702.6324935} +{'name': 'adobegc_n04400', 'size': 827, 'mtime': 1588699322.7834435} +{'name': 'adobegc_o04400', 'size': 827, 'mtime': 1588699343.7964466} +{'name': 'adobegc_p04400', 'size': 827, 'mtime': 1588699361.8530748} +{'name': 'adobegc_q04400', 'size': 827, 'mtime': 1588699435.7401783} +{'name': 'adobegc_r04400', 'size': 827, 'mtime': 1588699497.8403273} +{'name': 'adobegc_s04400', 'size': 827, 'mtime': 1588699564.148772} +{'name': 'adobegc_t04400', 'size': 827, 'mtime': 1588699581.2896767} +{'name': 'adobegc_u04400', 'size': 827, 'mtime': 1588699598.6942072} +{'name': 'adobegc_v04400', 'size': 827, 'mtime': 1588699628.5083873} +{'name': 'adobegc_w04400', 'size': 827, 'mtime': 1588699651.7972827} +{'name': 'AdobeIPCBrokerCustomHook.log', 'size': 110, 'mtime': 1594148255.931315} +{'name': 'ArmUI.ini', 'size': 257928, 'mtime': 1594655604.2703094} +{'name': 'bep_ie_tmp.log', 'size': 5750, 'mtime': 1594630046.8321078} +{'name': 'BROMJ6945DW.INI', 'size': 164, 'mtime': 1594932054.8597217} +{'name': 'CCSF_DebugLog.log', 'size': 22720, 'mtime': 1594619167.7750485} +{'name': 'chrome_installer.log', 'size': 215231, 'mtime': 1593199121.0920432} +{'name': 'dd_vcredist_amd64_20200710192056.log', 'size': 9218, 'mtime': 1594434073.4356828} +{'name': 'dd_vcredist_amd64_20200710192056_000_vcRuntimeMinimum_x64.log', 'size': 340038, 'mtime': 1594434071.8020437} +{'name': 'dd_vcredist_amd64_20200710192056_001_vcRuntimeAdditional_x64.log', 'size': 195928, 'mtime': 1594434073.3878088} +{'name': 'FXSAPIDebugLogFile.txt', 'size': 0, 'mtime': 1580005774.2871478} +{'name': 'FXSTIFFDebugLogFile.txt', 'size': 0, 'mtime': 1580005774.2402809} +{'name': 'install.ps1', 'size': 22662, 'mtime': 1594434168.2012112} +{'name': 'logserver.exe', 'size': 360392, 'mtime': 1591966026.0} +{'name': 'MpCmdRun.log', 'size': 414950, 'mtime': 1595033174.3764453} +{'name': 'Ofcdebug.ini', 'size': 2208, 'mtime': 1594619167.7125623} +{'name': 'ofcpipc.dll', 'size': 439232, 'mtime': 1591380338.0} +{'name': 'PDApp.log', 'size': 450550, 'mtime': 1594148263.081737} +{'name': 'show_temp_dir.py', 'size': 505, 'mtime': 1595040826.2968051} +{'name': 'tem33F0.tmp', 'size': 68, 'mtime': 1580005493.3622465} +{'name': 'temE0A2.tmp', 'size': 206, 'mtime': 1580005825.1382103} +{'name': 'tmdbg20.dll', 'size': 264648, 'mtime': 1591966082.0} +{'name': 'tm_icrcL_A606D985_38CA_41ab_BCD9_60F771CF800D', 'size': 0, 'mtime': 1594629977.2000608} +{'name': 'TS_3AD6.tmp', 'size': 262144, 'mtime': 1594629969.3296628} +{'name': 'TS_A4CE.tmp', 'size': 327680, 'mtime': 1594629996.4481225} +{'name': 'winagent-v0.9.4.exe', 'size': 13265088, 'mtime': 1594615216.1575873} +{'name': 'wuredist.cab', 'size': 6295, 'mtime': 1594458610.4993813} +""" diff --git a/api/tacticalrmm/tacticalrmm/demo_views.py b/api/tacticalrmm/tacticalrmm/demo_views.py new file mode 100644 index 0000000000..32ff994b92 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/demo_views.py @@ -0,0 +1,43 @@ +import json +import random + +from django.conf import settings +from rest_framework.response import Response + +SVC_FILE = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") +PROCS_FILE = settings.BASE_DIR.joinpath("tacticalrmm/test_data/procs.json") +EVT_LOG_FILE = settings.BASE_DIR.joinpath("tacticalrmm/test_data/appeventlog.json") + + +def demo_get_services(): + with open(SVC_FILE, "r") as f: + svcs = json.load(f) + + return Response(svcs) + + +# simulate realtime process monitor +def demo_get_procs(): + with open(PROCS_FILE, "r") as f: + procs = json.load(f) + + ret = [] + for proc in procs: + tmp = {} + for _, _ in proc.items(): + tmp["name"] = proc["name"] + tmp["pid"] = proc["pid"] + tmp["membytes"] = random.randrange(423424, 938921325) + tmp["username"] = proc["username"] + tmp["id"] = proc["id"] + tmp["cpu_percent"] = "{:.2f}".format(random.uniform(0.1, 99.4)) + ret.append(tmp) + + return Response(ret) + + +def demo_get_eventlog(): + with open(EVT_LOG_FILE, "r") as f: + logs = json.load(f) + + return Response(logs) diff --git a/api/tacticalrmm/tacticalrmm/exceptions.py b/api/tacticalrmm/tacticalrmm/exceptions.py new file mode 100644 index 0000000000..bdd20315cb --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/exceptions.py @@ -0,0 +1,7 @@ +class NatsDown(Exception): + """ + Raised when a connection to NATS cannot be established. + """ + + def __str__(self) -> str: + return "Unable to connect to NATS" diff --git a/api/tacticalrmm/tacticalrmm/helpers.py b/api/tacticalrmm/tacticalrmm/helpers.py new file mode 100644 index 0000000000..e8aec60303 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/helpers.py @@ -0,0 +1,101 @@ +import random +import secrets +import string +from pathlib import Path +from typing import TYPE_CHECKING, Any +from urllib.parse import urlparse +from zoneinfo import ZoneInfo + +from cryptography import x509 +from django.conf import settings +from django.utils import timezone as djangotime +from rest_framework import status +from rest_framework.response import Response + +if TYPE_CHECKING: + from datetime import datetime + + +def get_certs() -> tuple[str, str]: + domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] + cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" + + if hasattr(settings, "CERT_FILE") and hasattr(settings, "KEY_FILE"): + cert_file = settings.CERT_FILE + key_file = settings.KEY_FILE + + return cert_file, key_file + + +def notify_error(msg: str) -> Response: + return Response(msg, status=status.HTTP_400_BAD_REQUEST) + + +def get_nats_ports() -> tuple[int, int]: + """ + Returns: tuple[nats_standard_port: int, nats_websocket_port: int] + """ + nats_standard_port = getattr(settings, "NATS_STANDARD_PORT", 4222) + nats_websocket_port = getattr(settings, "NATS_WEBSOCKET_PORT", 9235) + + return nats_standard_port, nats_websocket_port + + +def get_nats_internal_protocol() -> str: + if getattr(settings, "TRMM_INSECURE", False): + return "nats" + + return "tls" + + +def date_is_in_past(*, datetime_obj: "datetime", agent_tz: str) -> bool: + """ + datetime_obj must be a naive datetime + """ + # convert agent tz to UTC to compare + localized = datetime_obj.replace(tzinfo=ZoneInfo(agent_tz)) + utc_time = localized.astimezone(ZoneInfo("UTC")) + return djangotime.now() > utc_time + + +def get_webdomain() -> str: + return urlparse(settings.CORS_ORIGIN_WHITELIST[0]).netloc + + +def rand_range(min: int, max: int) -> float: + """ + Input is milliseconds. + Returns float truncated to 2 decimals. + """ + return round(random.uniform(min, max) / 1000, 2) + + +def setup_nats_options() -> dict[str, Any]: + nats_std_port, _ = get_nats_ports() + proto = get_nats_internal_protocol() + opts = { + "servers": f"{proto}://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}", + "user": "tacticalrmm", + "name": "trmm-django", + "password": settings.SECRET_KEY, + "connect_timeout": 3, + "max_reconnect_attempts": 2, + } + return opts + + +def make_random_password(*, len: int) -> str: + alphabet = string.ascii_letters + string.digits + return "".join(secrets.choice(alphabet) for i in range(len)) + + +def days_until_cert_expires() -> int: + cert_file, _ = get_certs() + cert_bytes = Path(cert_file).read_bytes() + + cert = x509.load_pem_x509_certificate(cert_bytes) + expires = cert.not_valid_after.replace(tzinfo=ZoneInfo("UTC")) + delta = expires - djangotime.now() + + return delta.days diff --git a/api/tacticalrmm/tacticalrmm/middleware.py b/api/tacticalrmm/tacticalrmm/middleware.py index 90e41791d2..63dbd97391 100644 --- a/api/tacticalrmm/tacticalrmm/middleware.py +++ b/api/tacticalrmm/tacticalrmm/middleware.py @@ -1,17 +1,22 @@ import threading +from contextlib import suppress +from typing import Any, Dict, Optional from django.conf import settings -from rest_framework.exceptions import AuthenticationFailed from ipware import get_client_ip +from rest_framework.exceptions import AuthenticationFailed + +from tacticalrmm.constants import DEMO_NOT_ALLOWED +from tacticalrmm.helpers import notify_error request_local = threading.local() -def get_username(): +def get_username() -> Optional[str]: return getattr(request_local, "username", None) -def get_debug_info(): +def get_debug_info() -> Dict[str, Any]: return getattr(request_local, "debug_info", {}) @@ -24,13 +29,17 @@ def get_debug_info(): "/api/schema", ) +DEMO_EXCLUDE_PATHS = ( + "/api/v3", + "/api/schema", +) + class AuditMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - response = self.get_response(request) return response @@ -53,15 +62,18 @@ def process_view(self, request, view_func, view_args, view_kwargs): request = APIView().initialize_request(request) # check if user is authenticated - try: + with suppress(AuthenticationFailed): if hasattr(request, "user") and request.user.is_authenticated: - + try: + view_Name = view_func.__dict__["view_class"].__name__ + except: + view_Name = view_func.__name__ debug_info = {} # gather and save debug info debug_info["url"] = request.path debug_info["method"] = request.method debug_info["view_class"] = view_func.cls.__name__ - debug_info["view_func"] = view_func.__name__ + debug_info["view_func"] = view_Name debug_info["view_args"] = view_args debug_info["view_kwargs"] = view_kwargs debug_info["ip"] = request._client_ip @@ -70,8 +82,6 @@ def process_view(self, request, view_func, view_args, view_kwargs): # get authenticated user after request request_local.username = request.user.username - except AuthenticationFailed: - pass def process_exception(self, request, exception): request_local.debug_info = None @@ -88,8 +98,38 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - client_ip, is_routable = get_client_ip(request) + client_ip, _ = get_client_ip(request) request._client_ip = client_ip response = self.get_response(request) return response + + +class DemoMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + self.not_allowed = DEMO_NOT_ALLOWED + + def __call__(self, request): + return self.get_response(request) + + def drf_mock_response(self, request, resp): + from rest_framework.views import APIView + + view = APIView() + view.headers = view.default_response_headers + return view.finalize_response(request, resp).render() + + def process_view(self, request, view_func, view_args, view_kwargs): + err = "Not available in demo" + if request.path.startswith(DEMO_EXCLUDE_PATHS): + return self.drf_mock_response(request, notify_error(err)) + + try: + view_Name = view_func.__dict__["view_class"].__name__ + except: + return + for i in self.not_allowed: + if view_Name == i["name"] and request.method in i["methods"]: + return self.drf_mock_response(request, notify_error(err)) diff --git a/api/tacticalrmm/tacticalrmm/models.py b/api/tacticalrmm/tacticalrmm/models.py index 4c51881711..4f6aa174b5 100644 --- a/api/tacticalrmm/tacticalrmm/models.py +++ b/api/tacticalrmm/tacticalrmm/models.py @@ -1,11 +1,14 @@ +from typing import TYPE_CHECKING + from django.db import models +if TYPE_CHECKING: + from accounts.models import User -class PermissionQuerySet(models.QuerySet): +class PermissionQuerySet(models.QuerySet): # filters queryset based on permissions. Works different for Agent, Client, and Site - def filter_by_role(self, user): - + def filter_by_role(self, user: "User") -> "models.QuerySet": role = user.role # returns normal queryset if user is superuser @@ -21,7 +24,7 @@ def filter_by_role(self, user): model_name = self.model._meta.label.split(".")[1] # checks which sites and clients the user has access to and filters agents - if model_name in ["Agent", "Deployment"]: + if model_name in ("Agent", "Deployment"): if can_view_clients: clients_queryset = models.Q(site__client__in=can_view_clients) @@ -55,20 +58,11 @@ def filter_by_role(self, user): return self.filter(clients_queryset | sites_queryset) elif model_name == "Alert": - custom_alert_queryset = models.Q() if can_view_clients: - clients_queryset = ( - models.Q(agent__site__client__in=can_view_clients) - | models.Q(assigned_check__agent__site__client__in=can_view_clients) - | models.Q(assigned_task__agent__site__client__in=can_view_clients) - ) + clients_queryset = models.Q(agent__site__client__in=can_view_clients) if can_view_sites: - sites_queryset = ( - models.Q(agent__site__in=can_view_sites) - | models.Q(assigned_check__agent__site__in=can_view_sites) - | models.Q(assigned_task__agent__site__in=can_view_sites) - ) + sites_queryset = models.Q(agent__site__in=can_view_sites) if can_view_clients or can_view_sites: custom_alert_queryset = models.Q( agent=None, assigned_check=None, assigned_task=None @@ -84,7 +78,7 @@ def filter_by_role(self, user): return self # if model that is being filtered is a Check or Automated task we need to allow checks/tasks that are associated with policies - if model_name in ["Check", "AutomatedTask", "DebugLog"] and ( + if model_name in ("Check", "AutomatedTask", "DebugLog") and ( can_view_clients or can_view_sites ): agent_queryset = models.Q(agent=None) # dont filter if agent is None diff --git a/api/tacticalrmm/tacticalrmm/nats_utils.py b/api/tacticalrmm/tacticalrmm/nats_utils.py new file mode 100644 index 0000000000..0ccb41e463 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/nats_utils.py @@ -0,0 +1,38 @@ +import asyncio +from typing import TYPE_CHECKING, Any + +import msgpack +import nats + +from tacticalrmm.exceptions import NatsDown +from tacticalrmm.helpers import setup_nats_options + +if TYPE_CHECKING: + from nats.aio.client import Client as NClient + +NATS_DATA = dict[str, Any] + +BULK_NATS_TASKS = list[tuple[str, Any]] + + +async def _anats_message(*, nc: "NClient", subject: str, data: "NATS_DATA") -> None: + try: + payload = msgpack.dumps(data) + except: + return + + await nc.publish(subject=subject, payload=payload) + + +async def abulk_nats_command(*, items: "BULK_NATS_TASKS") -> None: + """Fire and forget""" + opts = setup_nats_options() + try: + nc = await nats.connect(**opts) + except Exception: + raise NatsDown + + tasks = [_anats_message(nc=nc, subject=item[0], data=item[1]) for item in items] + await asyncio.gather(*tasks) + await nc.flush() + await nc.close() diff --git a/api/tacticalrmm/tacticalrmm/permissions.py b/api/tacticalrmm/tacticalrmm/permissions.py index 350e009f06..9c1399b1c9 100644 --- a/api/tacticalrmm/tacticalrmm/permissions.py +++ b/api/tacticalrmm/tacticalrmm/permissions.py @@ -1,10 +1,15 @@ -from django.shortcuts import get_object_or_404 +from typing import TYPE_CHECKING + from django.db.models import Q +from django.shortcuts import get_object_or_404 from agents.models import Agent +if TYPE_CHECKING: + from accounts.models import User + -def _has_perm(request, perm): +def _has_perm(request, perm: str) -> bool: if request.user.is_superuser or ( request.user.role and getattr(request.user.role, "is_superuser") ): @@ -17,10 +22,10 @@ def _has_perm(request, perm): return request.user.role and getattr(request.user.role, perm) -def _has_perm_on_agent(user, agent_id: str): +def _has_perm_on_agent(user: "User", agent_id: str) -> bool: from agents.models import Agent - role = user.role + role = user.get_and_set_role_cache() if user.is_superuser or (role and getattr(role, "is_superuser")): return True @@ -44,10 +49,10 @@ def _has_perm_on_agent(user, agent_id: str): return False -def _has_perm_on_client(user, client_id: int): +def _has_perm_on_client(user: "User", client_id: int) -> bool: from clients.models import Client - role = user.role + role = user.get_and_set_role_cache() if user.is_superuser or (role and getattr(role, "is_superuser")): return True @@ -67,10 +72,10 @@ def _has_perm_on_client(user, client_id: int): return False -def _has_perm_on_site(user, site_id: int): +def _has_perm_on_site(user: "User", site_id: int) -> bool: from clients.models import Site - role = user.role + role = user.get_and_set_role_cache() if user.is_superuser or (role and getattr(role, "is_superuser")): return True @@ -94,8 +99,8 @@ def _has_perm_on_site(user, site_id: int): return False -def _audit_log_filter(user) -> Q: - role = user.role +def _audit_log_filter(user: "User") -> Q: + role = user.get_and_set_role_cache() if user.is_superuser or (role and getattr(role, "is_superuser")): return Q() diff --git a/api/tacticalrmm/tacticalrmm/schema.py b/api/tacticalrmm/tacticalrmm/schema.py new file mode 100644 index 0000000000..69f2161c28 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/schema.py @@ -0,0 +1,14 @@ +from drf_spectacular.extensions import OpenApiAuthenticationExtension + + +# custom api key auth for swagger-ui +class APIAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = "tacticalrmm.auth.APIAuthentication" # full import path OR class ref + name = "API Key Auth" # name used in the schem + + def get_security_definition(self, auto_schema): + return { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY", + } diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index a4f97a3ae0..4c879742b3 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -1,10 +1,11 @@ import os +from contextlib import suppress from datetime import timedelta from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent -SCRIPTS_DIR = "/srv/salt/scripts" +SCRIPTS_DIR = "/opt/trmm-community-scripts" DOCKER_BUILD = False @@ -12,54 +13,77 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe") +LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh" + +MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh" + AUTH_USER_MODEL = "accounts.User" # latest release -TRMM_VERSION = "0.10.2" +TRMM_VERSION = "0.16.4-dev" + +# https://github.com/amidaware/tacticalrmm-web +WEB_VERSION = "0.101.29" # bump this version everytime vue code is changed # to alert user they need to manually refresh their browser -APP_VER = "0.0.152" +APP_VER = "0.0.184" -# https://github.com/wh1te909/rmmagent -LATEST_AGENT_VER = "1.7.0" +# https://github.com/amidaware/rmmagent +LATEST_AGENT_VER = "2.4.12-dev" -MESH_VER = "0.9.51" +MESH_VER = "1.1.9" -NATS_SERVER_VER = "2.3.3" +NATS_SERVER_VER = "2.9.21" -# for the update script, bump when need to recreate venv or npm install -PIP_VER = "24" -NPM_VER = "25" +# for the update script, bump when need to recreate venv +PIP_VER = "38" -SETUPTOOLS_VER = "58.5.3" -WHEEL_VER = "0.37.0" +SETUPTOOLS_VER = "68.0.0" +WHEEL_VER = "0.41.1" -DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe" -DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe" - -EXE_GEN_URLS = [ - "https://exe2.tacticalrmm.io", - "https://exe.tacticalrmm.io", -] +AGENT_BASE_URL = "https://agents.tacticalrmm.com" DEFAULT_AUTO_FIELD = "django.db.models.AutoField" ASGI_APPLICATION = "tacticalrmm.asgi.application" +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = False # disabled for performance, enable when we add translation support +USE_TZ = True + +STATIC_URL = "/static/" + +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATICFILES_DIRS = [os.path.join(BASE_DIR, "tacticalrmm/static/")] + REST_KNOX = { "TOKEN_TTL": timedelta(hours=5), "AUTO_REFRESH": True, "MIN_REFRESH_INTERVAL": 600, } -try: - from .local_settings import * -except ImportError: - pass +DEMO = False +DEBUG = False +ADMIN_ENABLED = False +HOSTED = False +SWAGGER_ENABLED = False +REDIS_HOST = "127.0.0.1" + +with suppress(ImportError): + from .local_settings import * # noqa + +CHECK_TOKEN_URL = f"{AGENT_BASE_URL}/api/v2/checktoken" +AGENTS_URL = f"{AGENT_BASE_URL}/api/v2/agents/?" +EXE_GEN_URL = f"{AGENT_BASE_URL}/api/v2/exe" + +if "GHACTIONS" in os.environ: + DEBUG = False + ADMIN_ENABLED = False + DEMO = False REST_FRAMEWORK = { - "DATETIME_FORMAT": "%b-%d-%Y - %H:%M", "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": ( "knox.auth.TokenAuthentication", @@ -72,15 +96,17 @@ "TITLE": "Tactical RMM API", "DESCRIPTION": "Simple and Fast remote monitoring and management tool", "VERSION": TRMM_VERSION, + "AUTHENTICATION_WHITELIST": ["tacticalrmm.auth.APIAuthentication"], } -if not "AZPIPELINE" in os.environ: - if not DEBUG: # type: ignore - REST_FRAMEWORK.update( - {"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",)} - ) + +if not DEBUG: + REST_FRAMEWORK.update( + {"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",)} + ) INSTALLED_APPS = [ + "daphne", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", @@ -104,46 +130,66 @@ "logs", "scripts", "alerts", - "drf_spectacular", ] -if not "AZPIPELINE" in os.environ: - if DEBUG: # type: ignore - INSTALLED_APPS += ("django_extensions",) - - CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [(REDIS_HOST, 6379)], # type: ignore - }, +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [(REDIS_HOST, 6379)], }, - } + }, +} -if "AZPIPELINE" in os.environ: - ADMIN_ENABLED = False +# silence cache key length warnings +import warnings # noqa -if ADMIN_ENABLED: # type: ignore - INSTALLED_APPS += ( - "django.contrib.admin", - "django.contrib.messages", - ) +from django.core.cache import CacheKeyWarning # noqa + +warnings.simplefilter("ignore", CacheKeyWarning) +CACHES = { + "default": { + "BACKEND": "tacticalrmm.cache.TacticalRedisCache", + "LOCATION": f"redis://{REDIS_HOST}:6379", + "OPTIONS": { + "parser_class": "redis.connection.HiredisParser", + "pool_class": "redis.BlockingConnectionPool", + "db": "10", + }, + } +} MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", - "corsheaders.middleware.CorsMiddleware", ## + "corsheaders.middleware.CorsMiddleware", "tacticalrmm.middleware.LogIPMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "tacticalrmm.middleware.AuditMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -if ADMIN_ENABLED: # type: ignore +if SWAGGER_ENABLED: + INSTALLED_APPS += ("drf_spectacular",) + +if DEBUG and not DEMO: + INSTALLED_APPS += ( + "django_extensions", + "silk", + ) + + MIDDLEWARE.insert(0, "silk.middleware.SilkyMiddleware") + +if ADMIN_ENABLED: MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",) + INSTALLED_APPS += ( + "django.contrib.admin", + "django.contrib.messages", + ) + +if DEMO: + MIDDLEWARE += ("tacticalrmm.middleware.DemoMiddleware",) ROOT_URLCONF = "tacticalrmm.urls" @@ -182,31 +228,21 @@ }, ] - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -STATIC_URL = "/static/" - -STATIC_ROOT = os.path.join(BASE_DIR, "static") -STATICFILES_DIRS = [os.path.join(BASE_DIR, "tacticalrmm/static/")] - LOGGING = { "version": 1, "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + "datefmt": "%d/%b/%Y %H:%M:%S", + }, + }, "handlers": { "file": { "level": "ERROR", "class": "logging.FileHandler", "filename": os.path.join(LOG_DIR, "django_debug.log"), + "formatter": "verbose", } }, "loggers": { @@ -214,8 +250,9 @@ }, } -if "AZPIPELINE" in os.environ: - print("PIPELINE") + +if "GHACTIONS" in os.environ: + print("-----------------------GHACTIONS----------------------------") DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", @@ -226,24 +263,10 @@ "PORT": "", } } - - REST_FRAMEWORK = { - "DATETIME_FORMAT": "%b-%d-%Y - %H:%M", - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), - "DEFAULT_AUTHENTICATION_CLASSES": ( - "knox.auth.TokenAuthentication", - "tacticalrmm.auth.APIAuthentication", - ), - "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), - } - - ALLOWED_HOSTS = ["api.example.com"] - DEBUG = True SECRET_KEY = "abcdefghijklmnoptravis123456789" - + ALLOWED_HOSTS = ["api.example.com"] ADMIN_URL = "abc123456/" - - SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts") + CORS_ORIGIN_WHITELIST = ["https://rmm.example.com"] MESH_USERNAME = "pipeline" MESH_SITE = "https://example.com" MESH_TOKEN_KEY = "bd65e957a1e70c622d32523f61508400d6cd0937001a7ac12042227eba0b9ed625233851a316d4f489f02994145f74537a331415d00047dbbf13d940f556806dffe7a8ce1de216dc49edbad0c1a7399c" diff --git a/api/tacticalrmm/tacticalrmm/structs.py b/api/tacticalrmm/tacticalrmm/structs.py new file mode 100644 index 0000000000..481f180317 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/structs.py @@ -0,0 +1,20 @@ +import dataclasses +from typing import Any + + +class TRMMStruct: + def _to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + +@dataclasses.dataclass +class AgentCheckInConfig(TRMMStruct): + checkin_hello: int + checkin_agentinfo: int + checkin_winsvc: int + checkin_pubip: int + checkin_disks: int + checkin_sw: int + checkin_wmi: int + checkin_syncmesh: int + limit_data: bool diff --git a/api/tacticalrmm/tacticalrmm/test.py b/api/tacticalrmm/tacticalrmm/test.py index 8a82808fcb..8223edeae6 100644 --- a/api/tacticalrmm/tacticalrmm/test.py +++ b/api/tacticalrmm/tacticalrmm/test.py @@ -1,15 +1,56 @@ import uuid -from django.test import TestCase, override_settings -from model_bakery import baker, seq +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from django.test import TestCase, modify_settings, override_settings +from model_bakery import baker from rest_framework.authtoken.models import Token from rest_framework.test import APIClient from accounts.models import User +from agents.models import Agent +from automation.models import Policy from core.models import CoreSettings - - +from tacticalrmm.constants import CustomFieldModel, CustomFieldType +from tacticalrmm.helpers import make_random_password + +if TYPE_CHECKING: + from checks.models import Check + from clients.models import Client, Site + from core.models import CustomField + from scripts.models import Script + +TEST_CACHE = { + "default": { + "BACKEND": "tacticalrmm.cache.TacticalDummyCache", + } +} + + +@override_settings( + CACHES=TEST_CACHE, + DEBUG=False, + ADMIN_ENABLED=False, +) +@modify_settings( + INSTALLED_APPS={ + "remove": [ + "django.contrib.admin", + "django.contrib.messages", + "django_extensions", + "silk", + ] + }, + MIDDLEWARE={ + "remove": [ + "silk.middleware.SilkyMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + ] + }, +) class TacticalTestCase(TestCase): - def authenticate(self): + client: APIClient + + def authenticate(self) -> None: self.john = User(username="john") self.john.is_superuser = True self.john.set_password("hunter2") @@ -18,43 +59,41 @@ def authenticate(self): self.alice.is_superuser = True self.alice.set_password("hunter2") self.alice.save() - self.client_setup() + self.setup_client() self.client.force_authenticate(user=self.john) User.objects.create_user( # type: ignore username=uuid.uuid4().hex, is_installer_user=True, - password=User.objects.make_random_password(60), # type: ignore + password=make_random_password(len=60), # type: ignore ) - def setup_agent_auth(self, agent): - agent_user = User.objects.create_user( + def setup_client(self) -> None: + self.client = APIClient() + + def setup_agent_auth(self, agent: "Agent") -> None: + agent_user = User.objects.create_user( # type: ignore username=agent.agent_id, - password=User.objects.make_random_password(60), + password=make_random_password(len=60), # type: ignore ) Token.objects.create(user=agent_user) - def client_setup(self): - self.client = APIClient() - # fixes tests waiting 2 minutes for mesh token to appear @override_settings( - MESH_TOKEN_KEY="41410834b8bb4481446027f87d88ec6f119eb9aa97860366440b778540c7399613f7cabfef4f1aa5c0bd9beae03757e17b2e990e5876b0d9924da59bdf24d3437b3ed1a8593b78d65a72a76c794160d9" + MESH_TOKEN_KEY="41410834b8bb4481446027f87d88ec6f119eb9aa97860366440b778540c7399613f7cabfef4f1aa5c0bd9beae03757e17b2e990e5876b0d9924da59bdf24d3437b3ed1a8593b78d65a72a76c794160d9", ) - def setup_coresettings(self): + def setup_coresettings(self) -> None: self.coresettings = CoreSettings.objects.create() - def check_not_authenticated(self, method, url): + def check_not_authenticated(self, method: str, url: str) -> None: self.client.logout() r = getattr(self.client, method)(url) self.assertEqual(r.status_code, 401) - def create_checks(self, policy=None, agent=None, script=None): - - if not policy and not agent: - return - + def create_checks( + self, parent: "Union[Policy, Agent]", script: "Optional[Script]" = None + ) -> "List[Check]": # will create 1 of every check and associate it with the policy object passed check_recipes = [ "checks.diskspace_check", @@ -66,24 +105,31 @@ def create_checks(self, policy=None, agent=None, script=None): "checks.eventlog_check", ] - checks = list() + parent_obj = {} + if isinstance(parent, Policy): + parent_obj["policy"] = parent + else: + parent_obj["agent"] = parent + checks = [] for recipe in check_recipes: if not script: - checks.append(baker.make_recipe(recipe, policy=policy, agent=agent)) + checks.append(baker.make_recipe(recipe, **parent_obj)) else: - checks.append( - baker.make_recipe(recipe, policy=policy, agent=agent, script=script) - ) + checks.append(baker.make_recipe(recipe, **parent_obj, script=script)) return checks - def check_not_authorized(self, method: str, url: str, data: dict = {}): + def check_not_authorized( + self, method: str, url: str, data: Optional[Dict[Any, Any]] = {} + ) -> None: try: r = getattr(self.client, method)(url, data, format="json") self.assertEqual(r.status_code, 403) except KeyError: pass - def check_authorized(self, method: str, url: str, data: dict = {}): + def check_authorized( + self, method: str, url: str, data: Optional[Dict[Any, Any]] = {} + ) -> Any: try: r = getattr(self.client, method)(url, data, format="json") self.assertNotEqual(r.status_code, 403) @@ -91,8 +137,9 @@ def check_authorized(self, method: str, url: str, data: dict = {}): except KeyError: pass - def check_authorized_superuser(self, method: str, url: str, data: dict = {}): - + def check_authorized_superuser( + self, method: str, url: str, data: Optional[Dict[Any, Any]] = {} + ) -> Any: try: # create django superuser and test authorized user = baker.make("accounts.User", is_active=True, is_superuser=True) @@ -114,10 +161,35 @@ def check_authorized_superuser(self, method: str, url: str, data: dict = {}): except KeyError: pass - def create_user_with_roles(self, roles: list[str]) -> User: + def create_user_with_roles(self, roles: List[str]) -> User: new_role = baker.make("accounts.Role") for role in roles: setattr(new_role, role, True) new_role.save() return baker.make("accounts.User", role=new_role, is_active=True) + + def setup_base_instance(self): + self.company1: "Client" = baker.make("clients.Client") + self.company2: "Client" = baker.make("clients.Client") + self.site1: "Site" = baker.make("clients.Site", client=self.company1) + self.site2: "Site" = baker.make("clients.Site", client=self.company1) + self.site3: "Site" = baker.make("clients.Site", client=self.company2) + self.client_customfield: "CustomField" = baker.make( + "core.CustomField", + model=CustomFieldModel.CLIENT, + type=CustomFieldType.TEXT, + name="clientCustomField", + ) + self.site_customfield: "CustomField" = baker.make( + "core.CustomField", + model=CustomFieldModel.SITE, + type=CustomFieldType.TEXT, + name="siteCustomField", + ) + self.agent_customfield: "CustomField" = baker.make( + "core.CustomField", + model=CustomFieldModel.AGENT, + type=CustomFieldType.TEXT, + name="agentCustomField", + ) diff --git a/api/tacticalrmm/tacticalrmm/test_data/eventlog_check_fail.json b/api/tacticalrmm/tacticalrmm/test_data/eventlog_check_fail.json new file mode 100644 index 0000000000..69bf386403 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/eventlog_check_fail.json @@ -0,0 +1,132 @@ +{ + "log": [ + { + "uid": 2006, + "time": "2021-01-13 15:08:05 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 1573205062969647577, type 5\nEvent Name: StoreAgentSearchUpdatePackagesFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80240024\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WER7055.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_91ddac622de2aa181226f1833763a645a6701e_00000000_a030f5e8-b201-4b3b-b9c9-2f90448b63db\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: a030f5e8-b201-4b3b-b9c9-2f90448b63db\nReport Status: 268435456\nHashed bucket: 2ec09064b27edf5bb5d525ab6926e1d9\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2007, + "time": "2021-01-13 15:08:04 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentSearchUpdatePackagesFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80240024\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_91ddac622de2aa181226f1833763a645a6701e_00000000_a030f5e8-b201-4b3b-b9c9-2f90448b63db\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: a030f5e8-b201-4b3b-b9c9-2f90448b63db\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2008, + "time": "2021-01-13 15:08:02 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 2051817844803413223, type 5\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WER66BF.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_2c3439b6a8021ac4ff7a4e7f23e5b6f1d9e0_00000000_4c8d0432-6af0-4a6b-b153-428f6d0310c9\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: 4c8d0432-6af0-4a6b-b153-428f6d0310c9\nReport Status: 268435456\nHashed bucket: 04f95e1eb7585bb17c7985757750a4e7\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2009, + "time": "2021-01-13 15:08:02 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_2c3439b6a8021ac4ff7a4e7f23e5b6f1d9e0_00000000_4c8d0432-6af0-4a6b-b153-428f6d0310c9\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: 4c8d0432-6af0-4a6b-b153-428f6d0310c9\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2264, + "time": "2021-01-12 06:28:04 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 1693358677999346984, type 5\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: 2\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WERFEF3.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_c7a6adc885884a9aed88424cfb7d9d742f573c1_00000000_32d7534b-30da-4a6f-aa5a-7f65a48d2d47\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: 32d7534b-30da-4a6f-aa5a-7f65a48d2d47\nReport Status: 268435456\nHashed bucket: cdec799a6cf0bbef378004beef79e528\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2265, + "time": "2021-01-12 06:28:03 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: 2\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_c7a6adc885884a9aed88424cfb7d9d742f573c1_00000000_32d7534b-30da-4a6f-aa5a-7f65a48d2d47\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: 32d7534b-30da-4a6f-aa5a-7f65a48d2d47\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2636, + "time": "2021-01-10 07:13:43 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 1573205062969647577, type 5\nEvent Name: StoreAgentSearchUpdatePackagesFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80240024\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WER118F.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_91ddac622de2aa181226f1833763a645a6701e_00000000_d75f7cf4-1ba2-4a8f-a4fb-f0336137aeb9\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: d75f7cf4-1ba2-4a8f-a4fb-f0336137aeb9\nReport Status: 268435456\nHashed bucket: 2ec09064b27edf5bb5d525ab6926e1d9\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2637, + "time": "2021-01-10 07:13:42 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentSearchUpdatePackagesFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80240024\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_91ddac622de2aa181226f1833763a645a6701e_00000000_d75f7cf4-1ba2-4a8f-a4fb-f0336137aeb9\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: d75f7cf4-1ba2-4a8f-a4fb-f0336137aeb9\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2638, + "time": "2021-01-10 07:13:40 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 2051817844803413223, type 5\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WER867.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_2c3439b6a8021ac4ff7a4e7f23e5b6f1d9e0_00000000_e423f369-52f4-4cf1-98b7-6e519dad9836\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: e423f369-52f4-4cf1-98b7-6e519dad9836\nReport Status: 268435456\nHashed bucket: 04f95e1eb7585bb17c7985757750a4e7\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 2639, + "time": "2021-01-10 07:13:40 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: S\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_2c3439b6a8021ac4ff7a4e7f23e5b6f1d9e0_00000000_e423f369-52f4-4cf1-98b7-6e519dad9836\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: e423f369-52f4-4cf1-98b7-6e519dad9836\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 5553, + "time": "2020-12-25 15:45:02 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 1988976340961221197, type 5\nEvent Name: StoreAgentSearchUpdatePackagesFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80240024\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: G\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WER784D.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_2ae3e9964efbef3189977773239bfa7f29278_00000000_a20518d3-26b9-4e16-9223-924bc99df211\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: a20518d3-26b9-4e16-9223-924bc99df211\nReport Status: 268435456\nHashed bucket: d6c816dde23870028b9a4371ada65a4d\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 5554, + "time": "2020-12-25 15:45:02 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentSearchUpdatePackagesFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80240024\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: G\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_2ae3e9964efbef3189977773239bfa7f29278_00000000_a20518d3-26b9-4e16-9223-924bc99df211\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: a20518d3-26b9-4e16-9223-924bc99df211\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 5555, + "time": "2020-12-25 15:45:00 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 1198513334388591380, type 5\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: G\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WER6F15.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_11349d50384236752c9ebf31943a81bc9c22_00000000_d7901c46-3ff4-4e71-8d09-cb4511c1f790\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: d7901c46-3ff4-4e71-8d09-cb4511c1f790\nReport Status: 268435456\nHashed bucket: ca587589ef5ddc16c0a1f98712cd0b14\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 5556, + "time": "2020-12-25 15:44:59 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80246016\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: G\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_11349d50384236752c9ebf31943a81bc9c22_00000000_d7901c46-3ff4-4e71-8d09-cb4511c1f790\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: d7901c46-3ff4-4e71-8d09-cb4511c1f790\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 7058, + "time": "2020-12-17 10:59:07 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket 1324913600230033009, type 5\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80073cf9\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: 8\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\Temp\\WERD7E0.tmp.WERInternalMetadata.xml\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportArchive\\NonCritical_Update;ScanForUp_f5657215ea2368b3b590dfaee6ea708ec7f61_00000000_979cc6e7-b0ab-42ac-8127-1fec5a11c639\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: 979cc6e7-b0ab-42ac-8127-1fec5a11c639\nReport Status: 268435456\nHashed bucket: 39ea3113ccdc8abbd26309e653cb8671\nCab Guid: 0", + "eventType": "INFO" + }, + { + "uid": 7059, + "time": "2020-12-17 10:59:06 -0800 PST", + "source": "Windows Error Reporting", + "eventID": 1001, + "message": "Fault bucket , type 0\nEvent Name: StoreAgentDownloadFailure1\nResponse: Not available\nCab Id: 0\n\nProblem signature:\nP1: Update;ScanForUpdates\nP2: 80073cf9\nP3: 19041\nP4: 508\nP5: Windows.Desktop\nP6: 8\nP7: \nP8: \nP9: \nP10: \n\nAttached files:\n\nThese files may be available here:\n\\\\?\\C:\\ProgramData\\Microsoft\\Windows\\WER\\ReportQueue\\NonCritical_Update;ScanForUp_f5657215ea2368b3b590dfaee6ea708ec7f61_00000000_979cc6e7-b0ab-42ac-8127-1fec5a11c639\n\nAnalysis symbol: \nRechecking for solution: 0\nReport Id: 979cc6e7-b0ab-42ac-8127-1fec5a11c639\nReport Status: 4\nHashed bucket: \nCab Guid: 0", + "eventType": "INFO" + } + ] +} \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/procs.json b/api/tacticalrmm/tacticalrmm/test_data/procs.json index 34ae026f4b..0e8fa9f4c5 100644 --- a/api/tacticalrmm/tacticalrmm/test_data/procs.json +++ b/api/tacticalrmm/tacticalrmm/test_data/procs.json @@ -1,1102 +1,850 @@ [ - { - "name": "System", - "cpu_percent": 0.0, - "membytes": 434655234324, - "pid": 4, - "ppid": 0, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 2 - }, { "name": "Registry", - "cpu_percent": 0.0, - "membytes": 0.009720362333912082, - "pid": 280, - "ppid": 4, - "status": "running", + "pid": 368, + "membytes": 110456832, "username": "NT AUTHORITY\\SYSTEM", - "id": 4 + "id": 1, + "cpu_percent": "0.0" }, { "name": "smss.exe", - "cpu_percent": 0.0, - "membytes": 0.0006223099632878874, - "pid": 976, - "ppid": 4, - "status": "running", + "pid": 836, + "membytes": 1302528, "username": "NT AUTHORITY\\SYSTEM", - "id": 5 - }, - { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005682306310149464, - "pid": 1160, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 6 - }, - { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004793576106987529, - "pid": 1172, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 7 + "id": 2, + "cpu_percent": "0.0" }, { "name": "csrss.exe", - "cpu_percent": 0.0, - "membytes": 0.002459416691971619, - "pid": 1240, - "ppid": 1220, - "status": "running", + "pid": 1804, + "membytes": 5038080, "username": "NT AUTHORITY\\SYSTEM", - "id": 8 + "id": 3, + "cpu_percent": "0.0" }, { "name": "wininit.exe", - "cpu_percent": 0.0, - "membytes": 0.0031970428784885716, - "pid": 1316, - "ppid": 1220, - "status": "running", + "pid": 1920, + "membytes": 6553600, "username": "NT AUTHORITY\\SYSTEM", - "id": 9 - }, - { - "name": "csrss.exe", - "cpu_percent": 0.0, - "membytes": 0.0023719354191771556, - "pid": 1324, - "ppid": 1308, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 10 + "id": 4, + "cpu_percent": "0.0" }, { "name": "services.exe", - "cpu_percent": 0.0, - "membytes": 0.00596662044673147, - "pid": 1388, - "ppid": 1316, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 11 - }, - { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006052113508780605, - "pid": 1396, - "ppid": 1388, - "status": "running", + "pid": 264, + "membytes": 27267072, "username": "NT AUTHORITY\\SYSTEM", - "id": 12 + "id": 5, + "cpu_percent": "0.3" }, { "name": "LsaIso.exe", - "cpu_percent": 0.0, - "membytes": 0.0016124389144615866, - "pid": 1408, - "ppid": 1316, - "status": "running", + "pid": 256, + "membytes": 3620864, "username": "NT AUTHORITY\\SYSTEM", - "id": 13 + "id": 6, + "cpu_percent": "0.0" }, { "name": "lsass.exe", - "cpu_percent": 0.0, - "membytes": 0.012698702030414497, - "pid": 1416, - "ppid": 1316, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 14 - }, - { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.007129723732748768, - "pid": 1444, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 15 - }, - { - "name": "winlogon.exe", - "cpu_percent": 0.0, - "membytes": 0.005396003962822129, - "pid": 1492, - "ppid": 1308, - "status": "running", + "pid": 292, + "membytes": 49418240, "username": "NT AUTHORITY\\SYSTEM", - "id": 16 + "id": 7, + "cpu_percent": "0.1" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0027815068327148706, - "pid": 1568, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 17 - }, - { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.001936517265950167, - "pid": 1604, - "ppid": 1388, - "status": "running", + "pid": 1764, + "membytes": 4108288, "username": "NT AUTHORITY\\SYSTEM", - "id": 18 + "id": 8, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.011187661863964672, - "pid": 1628, - "ppid": 1388, - "status": "running", + "pid": 1932, + "membytes": 25579520, "username": "NT AUTHORITY\\SYSTEM", - "id": 19 + "id": 9, + "cpu_percent": "0.0" }, { "name": "fontdrvhost.exe", - "cpu_percent": 0.0, - "membytes": 0.002765601146752241, - "pid": 1652, - "ppid": 1492, - "status": "running", - "username": "Font Driver Host\\UMFD-1", - "id": 20 - }, - { - "name": "fontdrvhost.exe", - "cpu_percent": 0.0, - "membytes": 0.0017794486170691988, - "pid": 1660, - "ppid": 1316, - "status": "running", + "pid": 8, + "membytes": 4362240, "username": "Font Driver Host\\UMFD-0", - "id": 21 + "id": 10, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006676411682813821, - "pid": 1752, - "ppid": 1388, - "status": "running", + "pid": 2112, + "membytes": 20176896, "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 22 + "id": 11, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004892986644253965, - "pid": 1796, - "ppid": 1388, - "status": "running", + "pid": 2164, + "membytes": 9703424, "username": "NT AUTHORITY\\SYSTEM", - "id": 23 + "id": 12, + "cpu_percent": "0.0" }, { - "name": "dwm.exe", - "cpu_percent": 0.0, - "membytes": 0.02493216274642207, - "pid": 1868, - "ppid": 1492, - "status": "running", - "username": "Window Manager\\DWM-1", - "id": 24 + "name": "svchost.exe", + "pid": 2352, + "membytes": 14819328, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 13, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.011945170157934911, - "pid": 1972, - "ppid": 1388, - "status": "running", + "pid": 2384, + "membytes": 9363456, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 25 + "id": 14, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006616765360453959, - "pid": 1980, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 26 + "pid": 2392, + "membytes": 8581120, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 15, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0034435810109093323, - "pid": 2008, - "ppid": 1388, - "status": "running", + "pid": 2400, + "membytes": 12750848, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 27 + "id": 16, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004722000520155695, - "pid": 2160, - "ppid": 1388, - "status": "running", + "pid": 2516, + "membytes": 12111872, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 28 + "id": 17, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004264712048730091, - "pid": 2196, - "ppid": 1388, - "status": "running", + "pid": 2524, + "membytes": 10764288, "username": "NT AUTHORITY\\SYSTEM", - "id": 29 + "id": 18, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005493426289343236, - "pid": 2200, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 30 + "pid": 2536, + "membytes": 15400960, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 19, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.002757648303770926, - "pid": 2212, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 31 + "pid": 2624, + "membytes": 15835136, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 20, + "cpu_percent": "0.1" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0038113999987951447, - "pid": 2224, - "ppid": 1388, - "status": "running", + "pid": 2632, + "membytes": 5963776, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 32 + "id": 21, + "cpu_percent": "0.0" }, { - "name": "mmc.exe", - "cpu_percent": 0.084375, - "membytes": 0.027600341566653204, - "pid": 2272, - "ppid": 4664, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 33 + "name": "svchost.exe", + "pid": 2804, + "membytes": 22790144, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 22, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004185183618916942, - "pid": 2312, - "ppid": 1388, - "status": "running", + "pid": 2848, + "membytes": 24428544, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 34 + "id": 23, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.003334229419916253, - "pid": 2352, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 35 + "pid": 2996, + "membytes": 18673664, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 24, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.003841223159975075, - "pid": 2400, - "ppid": 1388, - "status": "running", + "pid": 3044, + "membytes": 25432064, "username": "NT AUTHORITY\\SYSTEM", - "id": 36 + "id": 25, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.00720527574107126, - "pid": 2440, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 37 + "pid": 2480, + "membytes": 12210176, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 26, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.008088041311997208, - "pid": 2512, - "ppid": 1388, - "status": "running", + "pid": 2840, + "membytes": 7012352, "username": "NT AUTHORITY\\SYSTEM", - "id": 38 + "id": 27, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005859257066483719, - "pid": 2600, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 39 + "pid": 2768, + "membytes": 18317312, + "username": "NT AUTHORITY\\SYSTEM", + "id": 28, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004566920082020056, - "pid": 2724, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 40 + "name": "atiesrxx.exe", + "pid": 1788, + "membytes": 6561792, + "username": "NT AUTHORITY\\SYSTEM", + "id": 29, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004475462387734934, - "pid": 2732, - "ppid": 1388, - "status": "running", + "name": "amdfendrsr.exe", + "pid": 3076, + "membytes": 8232960, + "username": "NT AUTHORITY\\SYSTEM", + "id": 30, + "cpu_percent": "0.0" + }, + { + "name": "WUDFHost.exe", + "pid": 3144, + "membytes": 8294400, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 41 + "id": 31, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004006244651837358, - "pid": 2748, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 42 + "pid": 3612, + "membytes": 6955008, + "username": "NT AUTHORITY\\SYSTEM", + "id": 32, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.003240783514885803, - "pid": 2796, - "ppid": 1388, - "status": "running", + "pid": 3640, + "membytes": 14684160, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 33, + "cpu_percent": "0.0" + }, + { + "name": "spaceman.exe", + "pid": 3700, + "membytes": 1089536, "username": "NT AUTHORITY\\SYSTEM", - "id": 43 + "id": 34, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0036404138746968747, - "pid": 2852, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 44 + "pid": 3824, + "membytes": 18538496, + "username": "NT AUTHORITY\\SYSTEM", + "id": 35, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005932820864060882, - "pid": 2936, - "ppid": 1388, - "status": "running", + "pid": 3844, + "membytes": 48369664, "username": "NT AUTHORITY\\SYSTEM", - "id": 45 + "id": 36, + "cpu_percent": "0.5" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004240853519786147, - "pid": 2944, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 46 + "pid": 3884, + "membytes": 9940992, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 37, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.009068229209444265, - "pid": 2952, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 47 + "pid": 3920, + "membytes": 14012416, + "username": "NT AUTHORITY\\SYSTEM", + "id": 38, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.008205345745971602, - "pid": 3036, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 48 + "pid": 3936, + "membytes": 9342976, + "username": "NT AUTHORITY\\SYSTEM", + "id": 39, + "cpu_percent": "0.0" }, { - "name": "spaceman.exe", - "cpu_percent": 0.0, - "membytes": 0.0003360076159605526, - "pid": 3112, - "ppid": 2440, - "status": "running", + "name": "svchost.exe", + "pid": 4016, + "membytes": 10964992, "username": "NT AUTHORITY\\SYSTEM", - "id": 49 + "id": 40, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.00409571413537715, - "pid": 3216, - "ppid": 1388, - "status": "running", + "pid": 4180, + "membytes": 8028160, "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 50 + "id": 41, + "cpu_percent": "0.0" }, { - "name": "ShellExperienceHost.exe", - "cpu_percent": 0.0, - "membytes": 0.030085604998314096, - "pid": 3228, - "ppid": 1628, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 51 + "name": "svchost.exe", + "pid": 4352, + "membytes": 7245824, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 42, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004664342408541163, - "pid": 3244, - "ppid": 1388, - "status": "running", + "pid": 4392, + "membytes": 5763072, "username": "NT AUTHORITY\\SYSTEM", - "id": 52 + "id": 43, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004843281375620747, - "pid": 3268, - "ppid": 1388, - "status": "running", + "pid": 4504, + "membytes": 5754880, "username": "NT AUTHORITY\\SYSTEM", - "id": 53 + "id": 44, + "cpu_percent": "0.0" }, { - "name": "python.exe", - "cpu_percent": 0.559375, - "membytes": 0.029455342192044896, - "pid": 3288, - "ppid": 4708, - "status": "running", + "name": "svchost.exe", + "pid": 4512, + "membytes": 8081408, "username": "NT AUTHORITY\\SYSTEM", - "id": 54 + "id": 45, + "cpu_percent": "0.0" }, { - "name": "RuntimeBroker.exe", - "cpu_percent": 0.0, - "membytes": 0.010283025974840107, - "pid": 3296, - "ppid": 1628, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 55 + "name": "svchost.exe", + "pid": 4604, + "membytes": 10006528, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 46, + "cpu_percent": "0.0" }, { - "name": "RuntimeBroker.exe", - "cpu_percent": 0.0, - "membytes": 0.006596883253000673, - "pid": 3308, - "ppid": 1628, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 56 + "name": "svchost.exe", + "pid": 4640, + "membytes": 12521472, + "username": "NT AUTHORITY\\SYSTEM", + "id": 47, + "cpu_percent": "0.0" }, { "name": "spoolsv.exe", - "cpu_percent": 0.0, - "membytes": 0.008095994154978522, - "pid": 3708, - "ppid": 1388, - "status": "running", + "pid": 5036, + "membytes": 17305600, "username": "NT AUTHORITY\\SYSTEM", - "id": 57 + "id": 48, + "cpu_percent": "0.0" }, { - "name": "conhost.exe", - "cpu_percent": 0.0, - "membytes": 0.011507763793962596, - "pid": 3752, - "ppid": 6620, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 58 + "name": "CagService.exe", + "pid": 3216, + "membytes": 161157120, + "username": "NT AUTHORITY\\SYSTEM", + "id": 49, + "cpu_percent": "0.4" }, { - "name": "LogMeInSystray.exe", - "cpu_percent": 0.0, - "membytes": 0.010300919871548067, - "pid": 3780, - "ppid": 4664, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 59 + "name": "svchost.exe", + "pid": 3588, + "membytes": 6782976, + "username": "NT AUTHORITY\\SYSTEM", + "id": 50, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005767799372198599, - "pid": 3808, - "ppid": 1388, - "status": "running", + "pid": 4228, + "membytes": 71839744, "username": "NT AUTHORITY\\SYSTEM", - "id": 60 + "id": 51, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.007070077410388906, - "pid": 3816, - "ppid": 1388, - "status": "running", + "pid": 4452, + "membytes": 32083968, "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 61 + "id": 52, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.014217695039845633, - "pid": 3824, - "ppid": 1388, - "status": "running", + "pid": 4804, + "membytes": 5677056, "username": "NT AUTHORITY\\SYSTEM", - "id": 62 + "id": 53, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.022611920806623463, - "pid": 3832, - "ppid": 1388, - "status": "running", + "pid": 5124, + "membytes": 13717504, "username": "NT AUTHORITY\\SYSTEM", - "id": 63 + "id": 54, + "cpu_percent": "0.0" }, { - "name": "nssm.exe", - "cpu_percent": 0.0, - "membytes": 0.003163243295817984, - "pid": 3840, - "ppid": 1388, - "status": "running", + "name": "svchost.exe", + "pid": 5132, + "membytes": 12251136, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 55, + "cpu_percent": "0.0" + }, + { + "name": "DesktopVideoHelper.exe", + "pid": 5140, + "membytes": 8085504, "username": "NT AUTHORITY\\SYSTEM", - "id": 64 + "id": 56, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0030717856015328626, - "pid": 3856, - "ppid": 1388, - "status": "running", + "name": "vmms.exe", + "pid": 5148, + "membytes": 63819776, "username": "NT AUTHORITY\\SYSTEM", - "id": 65 + "id": 57, + "cpu_percent": "0.3" }, { - "name": "LMIGuardianSvc.exe", - "cpu_percent": 0.0, - "membytes": 0.004441662805064347, - "pid": 3868, - "ppid": 1388, - "status": "running", + "name": "IPROSetMonitor.exe", + "pid": 5156, + "membytes": 10190848, "username": "NT AUTHORITY\\SYSTEM", - "id": 66 + "id": 58, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0026781198739577773, - "pid": 3876, - "ppid": 1388, - "status": "running", + "name": "tomcat7.exe", + "pid": 5260, + "membytes": 381120512, "username": "NT AUTHORITY\\SYSTEM", - "id": 67 + "id": 59, + "cpu_percent": "0.0" }, { - "name": "ramaint.exe", - "cpu_percent": 0.0, - "membytes": 0.0038471877922110613, - "pid": 3884, - "ppid": 1388, - "status": "running", + "name": "winvnc.exe", + "pid": 5284, + "membytes": 7983104, "username": "NT AUTHORITY\\SYSTEM", - "id": 68 + "id": 60, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005374133644623514, - "pid": 3892, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 69 + "name": "mongod.exe", + "pid": 5376, + "membytes": 135512064, + "username": "NT AUTHORITY\\SYSTEM", + "id": 61, + "cpu_percent": "0.4" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006421920707411746, - "pid": 3900, - "ppid": 1388, - "status": "running", + "pid": 5468, + "membytes": 8581120, "username": "NT AUTHORITY\\SYSTEM", - "id": 70 + "id": 62, + "cpu_percent": "0.0" }, { - "name": "ssm.exe", - "cpu_percent": 0.0, - "membytes": 0.0031612550850726546, - "pid": 3908, - "ppid": 1388, - "status": "running", + "name": "conhost.exe", + "pid": 5516, + "membytes": 13221888, "username": "NT AUTHORITY\\SYSTEM", - "id": 71 + "id": 63, + "cpu_percent": "0.0" }, { - "name": "MeshAgent.exe", - "cpu_percent": 0.0, - "membytes": 0.01894963661372797, - "pid": 3920, - "ppid": 1388, - "status": "running", + "name": "conhost.exe", + "pid": 6444, + "membytes": 14430208, "username": "NT AUTHORITY\\SYSTEM", - "id": 72 + "id": 64, + "cpu_percent": "0.0" + }, + { + "name": "WmiPrvSE.exe", + "pid": 7160, + "membytes": 37707776, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 65, + "cpu_percent": "0.9" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006905055918526623, - "pid": 4076, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\LOCAL SERVICE", - "id": 73 + "pid": 7648, + "membytes": 10403840, + "username": "NT AUTHORITY\\SYSTEM", + "id": 66, + "cpu_percent": "0.4" }, { - "name": "sihost.exe", - "cpu_percent": 0.0, - "membytes": 0.012527715906316225, - "pid": 4136, - "ppid": 3268, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 74 + "name": "vmcompute.exe", + "pid": 7988, + "membytes": 14204928, + "username": "NT AUTHORITY\\SYSTEM", + "id": 67, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.004169277932954313, - "pid": 4160, - "ppid": 1388, - "status": "running", + "pid": 3776, + "membytes": 9170944, "username": "NT AUTHORITY\\SYSTEM", - "id": 75 + "id": 68, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006851374228402747, - "pid": 4192, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 76 + "name": "vmwp.exe", + "pid": 8260, + "membytes": 35860480, + "username": "NT VIRTUAL MACHINE\\F7F40AEE-CE4B-4F3B-AEF1-96224F686899", + "id": 69, + "cpu_percent": "0.0" + }, + { + "name": "vmwp.exe", + "pid": 8748, + "membytes": 35844096, + "username": "NT VIRTUAL MACHINE\\E5A6878B-20B8-4D85-90CF-5AF97BF260FD", + "id": 70, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.006024278558346003, - "pid": 4208, - "ppid": 1388, - "status": "running", + "pid": 3632, + "membytes": 27947008, "username": "NT AUTHORITY\\SYSTEM", - "id": 77 + "id": 71, + "cpu_percent": "0.0" }, { - "name": "LogMeIn.exe", - "cpu_percent": 0.0, - "membytes": 0.017691099211934895, - "pid": 4232, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 78 + "name": "vmwp.exe", + "pid": 2348, + "membytes": 35233792, + "username": "NT VIRTUAL MACHINE\\B0FFC451-6B1D-40D4-AD8C-63918DCD894A", + "id": 72, + "cpu_percent": "0.0" }, { - "name": "vmms.exe", - "cpu_percent": 0.0, - "membytes": 0.017331233067030397, - "pid": 4292, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 79 + "name": "vmwp.exe", + "pid": 2844, + "membytes": 37187584, + "username": "NT VIRTUAL MACHINE\\E937A2B4-63F8-4D36-B1F8-4A7806022B26", + "id": 73, + "cpu_percent": "0.0" }, { - "name": "TabTip32.exe", - "cpu_percent": 0.0, - "membytes": 0.0023441004687425535, - "pid": 4304, - "ppid": 5916, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 80 + "name": "svchost.exe", + "pid": 5444, + "membytes": 14479360, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 74, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.022273924979917578, - "pid": 4436, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 81 + "pid": 9088, + "membytes": 30052352, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 75, + "cpu_percent": "0.1" }, { - "name": "explorer.exe", - "cpu_percent": 0.0, - "membytes": 0.040491900039364585, - "pid": 4664, - "ppid": 2804, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 82 + "name": "svchost.exe", + "pid": 5592, + "membytes": 6184960, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 76, + "cpu_percent": "0.0" }, { - "name": "tacticalrmm.exe", - "cpu_percent": 0.0, - "membytes": 0.019854272502852533, - "pid": 4696, - "ppid": 3840, - "status": "running", + "name": "svchost.exe", + "pid": 2072, + "membytes": 10117120, "username": "NT AUTHORITY\\SYSTEM", - "id": 83 + "id": 77, + "cpu_percent": "0.0" }, { - "name": "python.exe", - "cpu_percent": 0.0, - "membytes": 0.03651547854870715, - "pid": 4708, - "ppid": 3908, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 84 + "name": "msdtc.exe", + "pid": 8200, + "membytes": 10334208, + "username": "NT AUTHORITY\\NETWORK SERVICE", + "id": 78, + "cpu_percent": "0.0" }, { - "name": "conhost.exe", - "cpu_percent": 0.0, - "membytes": 0.0060938659344325075, - "pid": 4728, - "ppid": 4708, - "status": "running", + "name": "svchost.exe", + "pid": 5184, + "membytes": 19677184, "username": "NT AUTHORITY\\SYSTEM", - "id": 85 + "id": 79, + "cpu_percent": "0.0" }, { - "name": "conhost.exe", - "cpu_percent": 0.0, - "membytes": 0.006127665517103096, - "pid": 4736, - "ppid": 4696, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 86 + "name": "vmwp.exe", + "pid": 5188, + "membytes": 34082816, + "username": "NT VIRTUAL MACHINE\\99C21B25-4F9C-4DEF-9C2E-866E6C963460", + "id": 80, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0035111801762505086, - "pid": 4752, - "ppid": 1388, - "status": "running", + "name": "vmwp.exe", + "pid": 2684, + "membytes": 36429824, + "username": "NT VIRTUAL MACHINE\\2085B1D9-DFB8-482E-9454-39152BC9AA16", + "id": 81, + "cpu_percent": "0.1" + }, + { + "name": "SecurityHealthService.exe", + "pid": 8440, + "membytes": 11591680, "username": "NT AUTHORITY\\SYSTEM", - "id": 87 + "id": 82, + "cpu_percent": "0.0" }, { - "name": "vmcompute.exe", - "cpu_percent": 0.0, - "membytes": 0.005598801458845658, - "pid": 5020, - "ppid": 1388, - "status": "running", + "name": "svchost.exe", + "pid": 3816, + "membytes": 12738560, "username": "NT AUTHORITY\\SYSTEM", - "id": 88 + "id": 83, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005260805632139777, - "pid": 5088, - "ppid": 1388, - "status": "running", + "pid": 8700, + "membytes": 13672448, "username": "NT AUTHORITY\\SYSTEM", - "id": 89 + "id": 84, + "cpu_percent": "0.0" }, { - "name": "vmwp.exe", - "cpu_percent": 0.0, - "membytes": 0.011384494727752215, - "pid": 5276, - "ppid": 5020, - "status": "running", - "username": "NT VIRTUAL MACHINE\\A501B6E4-9443-49AA-99BD-E52CCF508915", - "id": 90 + "name": "AEMAgent.exe", + "pid": 9036, + "membytes": 189452288, + "username": "NT AUTHORITY\\SYSTEM", + "id": 85, + "cpu_percent": "0.4" }, { - "name": "python.exe", - "cpu_percent": 0.0, - "membytes": 0.020685344594399937, - "pid": 5472, - "ppid": 4708, - "status": "running", + "name": "aria2c.exe", + "pid": 6788, + "membytes": 9117696, "username": "NT AUTHORITY\\SYSTEM", - "id": 91 + "id": 86, + "cpu_percent": "0.0" }, { - "name": "WmiPrvSE.exe", - "cpu_percent": 0.0, - "membytes": 0.010167709751611041, - "pid": 5712, - "ppid": 1628, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 92 + "name": "RMM.WebRemote.exe", + "pid": 5368, + "membytes": 62095360, + "username": "NT AUTHORITY\\SYSTEM", + "id": 87, + "cpu_percent": "0.0" }, { - "name": "TabTip.exe", - "cpu_percent": 0.0, - "membytes": 0.008543341572677483, - "pid": 5916, - "ppid": 4752, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 93 + "name": "MsMpEng.exe", + "pid": 576, + "membytes": 238776320, + "username": "NT AUTHORITY\\SYSTEM", + "id": 88, + "cpu_percent": "0.3" }, { - "name": "vmwp.exe", - "cpu_percent": 0.0, - "membytes": 0.011780148666072628, - "pid": 5924, - "ppid": 5020, - "status": "running", - "username": "NT VIRTUAL MACHINE\\0C89E8DE-7E85-4955-832E-36F786714D55", - "id": 94 + "name": "WmiPrvSE.exe", + "pid": 7656, + "membytes": 13287424, + "username": "NT AUTHORITY\\LOCAL SERVICE", + "id": 89, + "cpu_percent": "0.1" }, { - "name": "msdtc.exe", - "cpu_percent": 0.0, - "membytes": 0.004956609388104484, - "pid": 6016, - "ppid": 1388, - "status": "running", - "username": "NT AUTHORITY\\NETWORK SERVICE", - "id": 95 + "name": "svchost.exe", + "pid": 10056, + "membytes": 12800000, + "username": "NT AUTHORITY\\SYSTEM", + "id": 90, + "cpu_percent": "0.0" }, { "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0025468979647660824, - "pid": 6056, - "ppid": 1388, - "status": "running", + "pid": 9400, + "membytes": 7532544, "username": "NT AUTHORITY\\SYSTEM", - "id": 96 + "id": 91, + "cpu_percent": "0.0" }, { - "name": "vmwp.exe", - "cpu_percent": 0.06875, - "membytes": 0.01141034146744149, - "pid": 6092, - "ppid": 5020, - "status": "running", - "username": "NT VIRTUAL MACHINE\\21F47B3B-DD44-40D4-8E12-FAC50D80B841", - "id": 97 + "name": "svchost.exe", + "pid": 11368, + "membytes": 7905280, + "username": "NT AUTHORITY\\SYSTEM", + "id": 92, + "cpu_percent": "0.0" }, { - "name": "vmwp.exe", - "cpu_percent": 0.0, - "membytes": 0.011595245066757059, - "pid": 6296, - "ppid": 5020, - "status": "running", - "username": "NT VIRTUAL MACHINE\\6F192157-71F5-4F33-AEAF-F1CB4478EDFD", - "id": 98 - }, - { - "name": "cmd.exe", - "cpu_percent": 0.0, - "membytes": 0.00203990422470726, - "pid": 6620, - "ppid": 4664, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 99 - }, - { - "name": "ctfmon.exe", - "cpu_percent": 0.0, - "membytes": 0.007632741051316932, - "pid": 6648, - "ppid": 4752, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 100 + "name": "MeshAgent.exe", + "pid": 8484, + "membytes": 39383040, + "username": "NT AUTHORITY\\SYSTEM", + "id": 93, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.007199311108835272, - "pid": 6716, - "ppid": 1388, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 101 + "name": "nssm.exe", + "pid": 7392, + "membytes": 6860800, + "username": "NT AUTHORITY\\SYSTEM", + "id": 94, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.0038054353665591583, - "pid": 6760, - "ppid": 1388, - "status": "running", + "name": "tacticalrmm.exe", + "pid": 7176, + "membytes": 24236032, "username": "NT AUTHORITY\\SYSTEM", - "id": 102 + "id": 95, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.013456210324384736, - "pid": 6868, - "ppid": 1388, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 103 + "name": "conhost.exe", + "pid": 2232, + "membytes": 13496320, + "username": "NT AUTHORITY\\SYSTEM", + "id": 96, + "cpu_percent": "0.0" }, { - "name": "SearchUI.exe", - "cpu_percent": 0.0, - "membytes": 0.04596743243199986, - "pid": 6904, - "ppid": 1628, - "status": "stopped", - "username": "VMHOST4\\Administrator", - "id": 104 + "name": "nssm.exe", + "pid": 10752, + "membytes": 6860800, + "username": "NT AUTHORITY\\SYSTEM", + "id": 97, + "cpu_percent": "0.0" }, { "name": "tacticalrmm.exe", - "cpu_percent": 0.0, - "membytes": 0.023025468641651836, - "pid": 6908, - "ppid": 7592, - "status": "running", + "pid": 4100, + "membytes": 29609984, "username": "NT AUTHORITY\\SYSTEM", - "id": 105 + "id": 98, + "cpu_percent": "0.8" }, { - "name": "taskhostw.exe", - "cpu_percent": 0.0, - "membytes": 0.006147547624556384, - "pid": 6984, - "ppid": 2440, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 106 + "name": "conhost.exe", + "pid": 6260, + "membytes": 13504512, + "username": "NT AUTHORITY\\SYSTEM", + "id": 99, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.017520113087836627, - "pid": 7092, - "ppid": 1388, - "status": "running", + "name": "csrss.exe", + "pid": 2316, + "membytes": 5185536, "username": "NT AUTHORITY\\SYSTEM", - "id": 107 + "id": 100, + "cpu_percent": "0.0" }, { - "name": "RuntimeBroker.exe", - "cpu_percent": 0.0, - "membytes": 0.011543551587378511, - "pid": 7148, - "ppid": 1628, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 108 + "name": "winlogon.exe", + "pid": 11396, + "membytes": 8990720, + "username": "NT AUTHORITY\\SYSTEM", + "id": 101, + "cpu_percent": "0.0" }, { - "name": "dllhost.exe", - "cpu_percent": 0.0, - "membytes": 0.006175382574990985, - "pid": 7232, - "ppid": 1628, - "status": "running", - "username": "VMHOST4\\Administrator", - "id": 109 + "name": "LogonUI.exe", + "pid": 2512, + "membytes": 46878720, + "username": "NT AUTHORITY\\SYSTEM", + "id": 102, + "cpu_percent": "0.0" }, { - "name": "conhost.exe", - "cpu_percent": 0.0, - "membytes": 0.006191288260953614, - "pid": 7288, - "ppid": 6908, - "status": "running", - "username": "NT AUTHORITY\\SYSTEM", - "id": 110 + "name": "fontdrvhost.exe", + "pid": 10616, + "membytes": 4440064, + "username": "Font Driver Host\\UMFD-2", + "id": 103, + "cpu_percent": "0.0" }, { - "name": "nssm.exe", - "cpu_percent": 0.0, - "membytes": 0.003252712779357776, - "pid": 7592, - "ppid": 1388, - "status": "running", + "name": "dwm.exe", + "pid": 9220, + "membytes": 48586752, + "username": "Window Manager\\DWM-2", + "id": 104, + "cpu_percent": "0.0" + }, + { + "name": "atieclxx.exe", + "pid": 8624, + "membytes": 11436032, "username": "NT AUTHORITY\\SYSTEM", - "id": 111 + "id": 105, + "cpu_percent": "0.0" }, { - "name": "svchost.exe", - "cpu_percent": 0.0, - "membytes": 0.005972585078967456, - "pid": 8012, - "ppid": 1388, - "status": "running", + "name": "winvnc.exe", + "pid": 11888, + "membytes": 10010624, "username": "NT AUTHORITY\\SYSTEM", - "id": 112 + "id": 106, + "cpu_percent": "0.0" } ] \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/software2.json b/api/tacticalrmm/tacticalrmm/test_data/software2.json new file mode 100644 index 0000000000..ce088fbd3f --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/software2.json @@ -0,0 +1,138 @@ +[ + { + "name": "7-Zip 19.00 (x64)", + "version": "19.00" + }, + { + "name": "Mesh Agent background service", + "version": "Not Found" + }, + { + "name": "MeshCentral Agent - Remote Control Software", + "version": "1.0.0" + }, + { + "name": "Microsoft Visual Studio 2010 Tools for Office Runtime (x64)", + "version": "10.0.50903,10.0.50908" + }, + { + "name": "Microsoft 365 Apps for enterprise - en-us", + "version": "16.0.13001.20266" + }, + { + "name": "Microsoft Visual C++ 2013 x64 Additional Runtime - 12.0.40664", + "version": "12.0.40664" + }, + { + "name": "Microsoft Visual C++ 2010 x64 Redistributable - 10.0.40219", + "version": "10.0.40219" + }, + { + "name": "Microsoft Visual C++ 2017 x64 Minimum Runtime - 14.13.26020", + "version": "14.13.26020" + }, + { + "name": "{4CEC2908-5CE4-48F0-A717-8FC833D8017A}", + "version": "0.1.247" + }, + { + "name": "Microsoft Visual C++ 2013 x64 Minimum Runtime - 12.0.40664", + "version": "12.0.40664" + }, + { + "name": "Google Chrome", + "version": "83.0.4103.116" + }, + { + "name": "Office 16 Click-to-Run Licensing Component", + "version": "16.0.13001.20266" + }, + { + "name": "Office 16 Click-to-Run Extensibility Component", + "version": "16.0.13001.20144" + }, + { + "name": "Office 16 Click-to-Run Localization Component", + "version": "16.0.13001.20144" + }, + { + "name": "Microsoft Visual C++ 2017 x64 Additional Runtime - 14.13.26020", + "version": "14.13.26020" + }, + { + "name": "Trend Micro Security Agent", + "version": "6.7.1364" + }, + { + "name": "Salt Minion 1.0.3 (Python 3)", + "version": "1.0.3" + }, + { + "name": "Microsoft Visual C++ 2013 Redistributable (x64) - 12.0.40664", + "version": "12.0.40664.0" + }, + { + "name": "Tactical RMM Agent", + "version": "0.9.4" + }, + { + "name": "LogMeIn Client", + "version": "1.3.4952" + }, + { + "name": "Microsoft Visual C++ 2017 Redistributable (x86) - 14.13.26020", + "version": "14.13.26020.0" + }, + { + "name": "Google Update Helper", + "version": "1.3.35.451" + }, + { + "name": "Teams Machine-Wide Installer", + "version": "1.3.0.4461" + }, + { + "name": "LogMeIn", + "version": "4.1.13508" + }, + { + "name": "Microsoft Visual C++ 2017 Redistributable (x64) - 14.13.26020", + "version": "14.13.26020.0" + }, + { + "name": "Microsoft Visual C++ 2017 x86 Additional Runtime - 14.13.26020", + "version": "14.13.26020" + }, + { + "name": "Microsoft Visual C++ 2017 x86 Minimum Runtime - 14.13.26020", + "version": "14.13.26020" + }, + { + "name": "Printer Installer Client", + "version": "25.0.0.266" + }, + { + "name": "Adobe Acrobat X Pro - English, Fran\u00e7ais, Deutsch", + "version": "10.1.1" + }, + { + "name": "Microsoft Visual C++ 2010 x86 Redistributable - 10.0.40219", + "version": "10.0.40219" + }, + { + "name": "Intel(R) Processor Graphics", + "version": "20.19.15.4835" + }, + { + "name": "Realtek High Definition Audio Driver", + "version": "6.0.1.7548" + }, + { + "name": "Microsoft OneDrive", + "version": "20.114.0607.0002" + }, + { + "name": "Microsoft Teams", + "version": "1.3.00.13565" + } +] \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/winsvcs.json b/api/tacticalrmm/tacticalrmm/test_data/winsvcs.json new file mode 100644 index 0000000000..5154db9a9b --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/winsvcs.json @@ -0,0 +1,2444 @@ +[ + { + "pid": 4340, + "name": "AdobeARMservice", + "status": "running", + "binpath": "\"C:\\Program Files (x86)\\Common Files\\Adobe\\ARM\\1.0\\armsvc.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Adobe Acrobat Updater keeps your Adobe software up to date.", + "display_name": "Adobe Acrobat Update Service" + }, + { + "pid": 0, + "name": "AJRouter", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Routes AllJoyn messages for the local AllJoyn clients. If this service is stopped the AllJoyn clients that do not have their own bundled routers will be unable to run.", + "display_name": "AllJoyn Router Service" + }, + { + "pid": 0, + "name": "ALG", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\alg.exe", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides support for 3rd party protocol plug-ins for Internet Connection Sharing", + "display_name": "Application Layer Gateway Service" + }, + { + "pid": 0, + "name": "AppIDSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Determines and verifies the identity of an application. Disabling this service will prevent AppLocker from being enforced.", + "display_name": "Application Identity" + }, + { + "pid": 0, + "name": "Appinfo", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Facilitates the running of interactive applications with additional administrative privileges. If this service is stopped, users will be unable to launch applications with the additional administrative privileges they may require to perform desired user tasks.", + "display_name": "Application Information" + }, + { + "pid": 0, + "name": "AppMgmt", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Processes installation, removal, and enumeration requests for software deployed through Group Policy. If the service is disabled, users will be unable to install, remove, or enumerate software deployed through Group Policy. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Application Management" + }, + { + "pid": 0, + "name": "AppReadiness", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k AppReadiness -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Gets apps ready for use the first time a user signs in to this PC and when adding new apps.", + "display_name": "App Readiness" + }, + { + "pid": 0, + "name": "AppVClient", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\AppVClient.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Disabled", + "description": "Manages App-V users and virtual applications", + "display_name": "Microsoft App-V Client" + }, + { + "pid": 0, + "name": "AppXSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k wsappx -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for deploying Store applications. This service is started on demand and if disabled Store applications will not be deployed to the system, and may not function properly.", + "display_name": "AppX Deployment Service (AppXSVC)" + }, + { + "pid": 0, + "name": "AssignedAccessManagerSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k AssignedAccessManagerSvc", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "AssignedAccessManager Service supports kiosk experience in Windows.", + "display_name": "AssignedAccessManager Service" + }, + { + "pid": 3148, + "name": "AudioEndpointBuilder", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Manages audio devices for the Windows Audio service. If this service is stopped, audio devices and effects will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start", + "display_name": "Windows Audio Endpoint Builder" + }, + { + "pid": 3640, + "name": "Audiosrv", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "Manages audio for Windows-based programs. If this service is stopped, audio devices and effects will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start", + "display_name": "Windows Audio" + }, + { + "pid": 0, + "name": "autotimesvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k autoTimeSvc", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "This service sets time based on NITZ messages from a Mobile Network", + "display_name": "Cellular Time" + }, + { + "pid": 0, + "name": "AxInstSV", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k AxInstSVGroup", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides User Account Control validation for the installation of ActiveX controls from the Internet and enables management of ActiveX control installation based on Group Policy settings. This service is started on demand and if disabled the installation of ActiveX controls will behave according to default browser settings.", + "display_name": "ActiveX Installer (AxInstSV)" + }, + { + "pid": 0, + "name": "BITS", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Manual", + "description": "Transfers files in the background using idle network bandwidth. If the service is disabled, then any applications that depend on BITS, such as Windows Update or MSN Explorer, will be unable to automatically download programs and other information.", + "display_name": "Background Intelligent Transfer Service" + }, + { + "pid": 1484, + "name": "BTAGService", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Service supporting the audio gateway role of the Bluetooth Handsfree Profile.", + "display_name": "Bluetooth Audio Gateway Service" + }, + { + "pid": 1500, + "name": "BthAvctpSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "This is Audio Video Control Transport Protocol service", + "display_name": "AVCTP service" + }, + { + "pid": 1492, + "name": "bthserv", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "The Bluetooth service supports discovery and association of remote Bluetooth devices. Stopping or disabling this service may cause already installed Bluetooth devices to fail to operate properly and prevent new devices from being discovered or associated.", + "display_name": "Bluetooth Support Service" + }, + { + "pid": 0, + "name": "camsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k appmodel -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides facilities for managing UWP apps access to app capabilities as well as checking an app's access to specific app capabilities", + "display_name": "Capability Access Manager Service" + }, + { + "pid": 2724, + "name": "CDPSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": true, + "start_type": "Automatic", + "description": "This service is used for Connected Devices Platform scenarios", + "display_name": "Connected Devices Platform Service" + }, + { + "pid": 3004, + "name": "CertPropSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Copies user certificates and root certificates from smart cards into the current user's certificate store, detects when a smart card is inserted into a smart card reader, and, if needed, installs the smart card Plug and Play minidriver.", + "display_name": "Certificate Propagation" + }, + { + "pid": 0, + "name": "ClipSVC", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k wsappx -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for the Microsoft Store. This service is started on demand and if disabled applications bought using Windows Store will not behave correctly.", + "display_name": "Client License Service (ClipSVC)" + }, + { + "pid": 0, + "name": "COMSysApp", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\dllhost.exe /Processid:{02D4B3F1-FD88-11D1-960D-00805FC79235}", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Manages the configuration and tracking of Component Object Model (COM)+-based components. If the service is stopped, most COM+-based components will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "COM+ System Application" + }, + { + "pid": 4912, + "name": "cphs", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_38bfcb542ef4272e\\IntelCpHeciSvc.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Intel(R) Content Protection HECI Service - enables communication with the Content Protection FW", + "display_name": "Intel(R) Content Protection HECI Service" + }, + { + "pid": 4332, + "name": "cplspcon", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_38bfcb542ef4272e\\IntelCpHDCPSvc.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Intel(R) Content Protection HDCP Service - enables communication with Content Protection HDCP HW", + "display_name": "Intel(R) Content Protection HDCP Service" + }, + { + "pid": 4280, + "name": "CryptSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k NetworkService -p", + "username": "NT Authority\\NetworkService", + "autodelay": false, + "start_type": "Automatic", + "description": "Provides three management services: Catalog Database Service, which confirms the signatures of Windows files and allows new programs to be installed; Protected Root Service, which adds and removes Trusted Root Certification Authority certificates from this computer; and Automatic Root Certificate Update Service, which retrieves root certificates from Windows Update and enable scenarios such as SSL. If this service is stopped, these management services will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Cryptographic Services" + }, + { + "pid": 2616, + "name": "CscService", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "The Offline Files service performs maintenance activities on the Offline Files cache, responds to user logon and logoff events, implements the internals of the public API, and dispatches interesting events to those interested in Offline Files activities and changes in cache state.", + "display_name": "Offline Files" + }, + { + "pid": 0, + "name": "defragsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k defragsvc", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Helps the computer run more efficiently by optimizing files on storage drives.", + "display_name": "Optimize drives" + }, + { + "pid": 2860, + "name": "DeviceAssociationService", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables pairing between the system and wired or wireless devices.", + "display_name": "Device Association Service" + }, + { + "pid": 0, + "name": "DeviceInstall", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k DcomLaunch -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables a computer to recognize and adapt to hardware changes with little or no user input. Stopping or disabling this service will result in system instability.", + "display_name": "Device Install Service" + }, + { + "pid": 0, + "name": "DevQueryBroker", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables apps to discover devices with a backgroud task", + "display_name": "DevQuery Background Discovery Broker" + }, + { + "pid": 1684, + "name": "Dhcp", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "Registers and updates IP addresses and DNS records for this computer. If this service is stopped, this computer will not receive dynamic IP addresses and DNS updates. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "DHCP Client" + }, + { + "pid": 0, + "name": "diagnosticshub.standardcollector.service", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\DiagSvcs\\DiagnosticsHub.StandardCollector.Service.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Diagnostics Hub Standard Collector Service. When running, this service collects real time ETW events and processes them.", + "display_name": "Microsoft (R) Diagnostics Hub Standard Collector Service" + }, + { + "pid": 0, + "name": "diagsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k diagnostics", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Executes diagnostic actions for troubleshooting support", + "display_name": "Diagnostic Execution Service" + }, + { + "pid": 4288, + "name": "DiagTrack", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k utcsvc -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "The Connected User Experiences and Telemetry service enables features that support in-application and connected user experiences. Additionally, this service manages the event driven collection and transmission of diagnostic and usage information (used to improve the experience and quality of the Windows Platform) when the diagnostics and usage privacy option settings are enabled under Feedback and Diagnostics.", + "display_name": "Connected User Experiences and Telemetry" + }, + { + "pid": 3132, + "name": "DispBrokerDesktopSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": true, + "start_type": "Automatic", + "description": "Manages the connection and configuration of local and remote displays", + "display_name": "Display Policy Service" + }, + { + "pid": 1848, + "name": "DisplayEnhancementService", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "A service for managing display enhancement such as brightness control.", + "display_name": "Display Enhancement Service" + }, + { + "pid": 0, + "name": "DmEnrollmentSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Performs Device Enrollment Activities for Device Management", + "display_name": "Device Management Enrollment Service" + }, + { + "pid": 0, + "name": "dmwappushservice", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Manual", + "description": "Routes Wireless Application Protocol (WAP) Push messages received by the device and synchronizes Device Management sessions", + "display_name": "Device Management Wireless Application Protocol (WAP) Push message Routing Service" + }, + { + "pid": 7640, + "name": "DoSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService -p", + "username": "NT Authority\\NetworkService", + "autodelay": true, + "start_type": "Automatic", + "description": "Performs content delivery optimization tasks", + "display_name": "Delivery Optimization" + }, + { + "pid": 0, + "name": "dot3svc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Wired AutoConfig (DOT3SVC) service is responsible for performing IEEE 802.1X authentication on Ethernet interfaces. If your current wired network deployment enforces 802.1X authentication, the DOT3SVC service should be configured to run for establishing Layer 2 connectivity and/or providing access to network resources. Wired networks that do not enforce 802.1X authentication are unaffected by the DOT3SVC service.", + "display_name": "Wired AutoConfig" + }, + { + "pid": 4320, + "name": "DPS", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNoNetwork -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "The Diagnostic Policy Service enables problem detection, troubleshooting and resolution for Windows components. If this service is stopped, diagnostics will no longer function.", + "display_name": "Diagnostic Policy Service" + }, + { + "pid": 25736, + "name": "DsmSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables the detection, download and installation of device-related software. If this service is disabled, devices may be configured with outdated software, and may not work correctly.", + "display_name": "Device Setup Manager" + }, + { + "pid": 2744, + "name": "DsSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides data brokering between applications.", + "display_name": "Data Sharing Service" + }, + { + "pid": 3836, + "name": "DusmSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "Network data usage, data limit, restrict background data, metered networks.", + "display_name": "Data Usage" + }, + { + "pid": 0, + "name": "Eaphost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Extensible Authentication Protocol (EAP) service provides network authentication in such scenarios as 802.1x wired and wireless, VPN, and Network Access Protection (NAP). EAP also provides application programming interfaces (APIs) that are used by network access clients, including wireless and VPN clients, during the authentication process. If you disable this service, this computer is prevented from accessing networks that require EAP authentication.", + "display_name": "Extensible Authentication Protocol" + }, + { + "pid": 0, + "name": "EFS", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\lsass.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides the core file encryption technology used to store encrypted files on NTFS file system volumes. If this service is stopped or disabled, applications will be unable to access encrypted files.", + "display_name": "Encrypting File System (EFS)" + }, + { + "pid": 0, + "name": "embeddedmode", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Embedded Mode service enables scenarios related to Background Applications. Disabling this service will prevent Background Applications from being activated.", + "display_name": "Embedded Mode" + }, + { + "pid": 0, + "name": "EntAppSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k appmodel -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables enterprise application management.", + "display_name": "Enterprise App Management Service" + }, + { + "pid": 1252, + "name": "EventLog", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "This service manages events and event logs. It supports logging events, querying events, subscribing to events, archiving event logs, and managing event metadata. It can display events in both XML and plain text format. Stopping this service may compromise security and reliability of the system.", + "display_name": "Windows Event Log" + }, + { + "pid": 2644, + "name": "EventSystem", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "Supports System Event Notification Service (SENS), which provides automatic distribution of events to subscribing Component Object Model (COM) components. If the service is stopped, SENS will close and will not be able to provide logon and logoff notifications. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "COM+ Event System" + }, + { + "pid": 0, + "name": "Fax", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\fxssvc.exe", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Enables you to send and receive faxes, utilizing fax resources available on this computer or on the network.", + "display_name": "Fax" + }, + { + "pid": 0, + "name": "fdPHost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "The FDPHOST service hosts the Function Discovery (FD) network discovery providers. These FD providers supply network discovery services for the Simple Services Discovery Protocol (SSDP) and Web Services – Discovery (WS-D) protocol. Stopping or disabling the FDPHOST service will disable network discovery for these protocols when using FD. When this service is unavailable, network services using FD and relying on these discovery protocols will be unable to find network devices or resources.", + "display_name": "Function Discovery Provider Host" + }, + { + "pid": 0, + "name": "FDResPub", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Publishes this computer and resources attached to this computer so they can be discovered over the network. If this service is stopped, network resources will no longer be published and they will not be discovered by other computers on the network.", + "display_name": "Function Discovery Resource Publication" + }, + { + "pid": 0, + "name": "fhsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Protects user files from accidental loss by copying them to a backup location", + "display_name": "File History Service" + }, + { + "pid": 3156, + "name": "FontCache", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "Optimizes performance of applications by caching commonly used font data. Applications will start this service if it is not already running. It can be disabled, though doing so will degrade application performance.", + "display_name": "Windows Font Cache Service" + }, + { + "pid": 0, + "name": "FrameServer", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k Camera", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables multiple clients to access video frames from camera devices.", + "display_name": "Windows Camera Frame Server" + }, + { + "pid": 0, + "name": "GoogleChromeElevationService", + "status": "stopped", + "binpath": "\"C:\\Program Files (x86)\\Google\\Chrome\\Application\\87.0.4280.141\\elevation_service.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "", + "display_name": "Google Chrome Elevation Service" + }, + { + "pid": 0, + "name": "gpsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "The service is responsible for applying settings configured by administrators for the computer and users through the Group Policy component. If the service is disabled, the settings will not be applied and applications and components will not be manageable through Group Policy. Any components or applications that depend on the Group Policy component might not be functional if the service is disabled.", + "display_name": "Group Policy Client" + }, + { + "pid": 0, + "name": "GraphicsPerfSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k GraphicsPerfSvcGroup", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Graphics performance monitor service", + "display_name": "GraphicsPerfSvc" + }, + { + "pid": 0, + "name": "gupdate", + "status": "stopped", + "binpath": "\"C:\\Program Files (x86)\\Google\\Update\\GoogleUpdate.exe\" /svc", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Automatic", + "description": "Keeps your Google software up to date. If this service is disabled or stopped, your Google software will not be kept up to date, meaning security vulnerabilities that may arise cannot be fixed and features may not work. This service uninstalls itself when there is no Google software using it.", + "display_name": "Google Update Service (gupdate)" + }, + { + "pid": 0, + "name": "gupdatem", + "status": "stopped", + "binpath": "\"C:\\Program Files (x86)\\Google\\Update\\GoogleUpdate.exe\" /medsvc", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Manual", + "description": "Keeps your Google software up to date. If this service is disabled or stopped, your Google software will not be kept up to date, meaning security vulnerabilities that may arise cannot be fixed and features may not work. This service uninstalls itself when there is no Google software using it.", + "display_name": "Google Update Service (gupdatem)" + }, + { + "pid": 1908, + "name": "hidserv", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Activates and maintains the use of hot buttons on keyboards, remote controls, and other multimedia devices. It is recommended that you keep this service running.", + "display_name": "Human Interface Device Service" + }, + { + "pid": 0, + "name": "HvHost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides an interface for the Hyper-V hypervisor to provide per-partition performance counters to the host operating system.", + "display_name": "HV Host Service" + }, + { + "pid": 0, + "name": "icssvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides the ability to share a cellular data connection with another device.", + "display_name": "Windows Mobile Hotspot Service" + }, + { + "pid": 2088, + "name": "igfxCUIService2.0.0.0", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\DriverStore\\FileRepository\\cui_dch.inf_amd64_f3a64c75ee4defb7\\igfxCUIService.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Service for Intel(R) HD Graphics Control Panel", + "display_name": "Intel(R) HD Graphics Control Panel Service" + }, + { + "pid": 0, + "name": "IKEEXT", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The IKEEXT service hosts the Internet Key Exchange (IKE) and Authenticated Internet Protocol (AuthIP) keying modules. These keying modules are used for authentication and key exchange in Internet Protocol security (IPsec). Stopping or disabling the IKEEXT service will disable IKE and AuthIP key exchange with peer computers. IPsec is typically configured to use IKE or AuthIP; therefore, stopping or disabling the IKEEXT service might result in an IPsec failure and might compromise the security of the system. It is strongly recommended that you have the IKEEXT service running.", + "display_name": "IKE and AuthIP IPsec Keying Modules" + }, + { + "pid": 4724, + "name": "InstallService", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for the Microsoft Store. This service is started on demand and if disabled then installations will not function properly.", + "display_name": "Microsoft Store Install Service" + }, + { + "pid": 3496, + "name": "iphlpsvc", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetSvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Provides tunnel connectivity using IPv6 transition technologies (6to4, ISATAP, Port Proxy, and Teredo), and IP-HTTPS. If this service is stopped, the computer will not have the enhanced connectivity benefits that these technologies offer.", + "display_name": "IP Helper" + }, + { + "pid": 0, + "name": "IpxlatCfgSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Configures and enables translation from v4 to v6 and vice versa", + "display_name": "IP Translation Configuration Service" + }, + { + "pid": 992, + "name": "KeyIso", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\lsass.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The CNG key isolation service is hosted in the LSA process. The service provides key process isolation to private keys and associated cryptographic operations as required by the Common Criteria. The service stores and uses long-lived keys in a secure process complying with Common Criteria requirements.", + "display_name": "CNG Key Isolation" + }, + { + "pid": 0, + "name": "KtmRm", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Coordinates transactions between the Distributed Transaction Coordinator (MSDTC) and the Kernel Transaction Manager (KTM). If it is not needed, it is recommended that this service remain stopped. If it is needed, both MSDTC and KTM will start this service automatically. If this service is disabled, any MSDTC transaction interacting with a Kernel Resource Manager will fail and any services that explicitly depend on it will fail to start.", + "display_name": "KtmRm for Distributed Transaction Coordinator" + }, + { + "pid": 4596, + "name": "LanmanServer", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Supports file, print, and named-pipe sharing over the network for this computer. If this service is stopped, these functions will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Server" + }, + { + "pid": 3104, + "name": "LanmanWorkstation", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Automatic", + "description": "Creates and maintains client network connections to remote servers using the SMB protocol. If this service is stopped, these connections will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Workstation" + }, + { + "pid": 0, + "name": "lfsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service monitors the current location of the system and manages geofences (a geographical location with associated events). If you turn off this service, applications will be unable to use or receive notifications for geolocation or geofences.", + "display_name": "Geolocation Service" + }, + { + "pid": 1088, + "name": "LicenseManager", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalService -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for the Microsoft Store. This service is started on demand and if disabled then content acquired through the Microsoft Store will not function properly.", + "display_name": "Windows License Manager Service" + }, + { + "pid": 0, + "name": "lltdsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Creates a Network Map, consisting of PC and device topology (connectivity) information, and metadata describing each PC and device. If this service is disabled, the Network Map will not function properly.", + "display_name": "Link-Layer Topology Discovery Mapper" + }, + { + "pid": 5392, + "name": "lmhosts", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides support for the NetBIOS over TCP/IP (NetBT) service and NetBIOS name resolution for clients on the network, therefore enabling users to share files, print, and log on to the network. If this service is stopped, these functions might be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "TCP/IP NetBIOS Helper" + }, + { + "pid": 18872, + "name": "LogMeIn", + "status": "running", + "binpath": "\"C:\\Program Files (x86)\\LogMeIn\\x64\\LogMeIn.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "", + "display_name": "LogMeIn" + }, + { + "pid": 0, + "name": "LxpSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for deploying and configuring localized Windows resources. This service is started on demand and, if disabled, additional Windows languages will not be deployed to the system, and Windows may not function properly.", + "display_name": "Language Experience Service" + }, + { + "pid": 0, + "name": "MapsBroker", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": true, + "start_type": "Automatic", + "description": "Windows service for application access to downloaded maps. This service is started on-demand by application accessing downloaded maps. Disabling this service will prevent apps from accessing maps.", + "display_name": "Downloaded Maps Manager" + }, + { + "pid": 0, + "name": "MixedRealityOpenXRSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables Mixed Reality OpenXR runtime functionality", + "display_name": "Windows Mixed Reality OpenXR Service" + }, + { + "pid": 0, + "name": "MSiSCSI", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Manages Internet SCSI (iSCSI) sessions from this computer to remote iSCSI target devices. If this service is stopped, this computer will not be able to login or access iSCSI targets. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Microsoft iSCSI Initiator Service" + }, + { + "pid": 0, + "name": "msiserver", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\msiexec.exe /V", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Adds, modifies, and removes applications provided as a Windows Installer (*.msi, *.msp) package. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Windows Installer" + }, + { + "pid": 0, + "name": "NaturalAuthentication", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Signal aggregator service, that evaluates signals based on time, network, geolocation, bluetooth and cdf factors. Supported features are Device Unlock, Dynamic Lock and Dynamo MDM policies", + "display_name": "Natural Authentication" + }, + { + "pid": 0, + "name": "NcaSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetSvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides DirectAccess status notification for UI components", + "display_name": "Network Connectivity Assistant" + }, + { + "pid": 1612, + "name": "NcbService", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Brokers connections that allow Windows Store Apps to receive notifications from the internet.", + "display_name": "Network Connection Broker" + }, + { + "pid": 0, + "name": "NcdAutoSetup", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNoNetwork -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Network Connected Devices Auto-Setup service monitors and installs qualified devices that connect to a qualified network. Stopping or disabling this service will prevent Windows from discovering and installing qualified network connected devices automatically. Users can still manually add network connected devices to a PC through the user interface.", + "display_name": "Network Connected Devices Auto-Setup" + }, + { + "pid": 992, + "name": "Netlogon", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\lsass.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Maintains a secure channel between this computer and the domain controller for authenticating users and services. If this service is stopped, the computer may not authenticate users and services and the domain controller cannot register DNS records. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Netlogon" + }, + { + "pid": 0, + "name": "Netman", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Manages objects in the Network and Dial-Up Connections folder, in which you can view both local area network and remote connections.", + "display_name": "Network Connections" + }, + { + "pid": 2824, + "name": "netprofm", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Identifies the networks to which the computer has connected, collects and stores properties for these networks, and notifies applications when these properties change.", + "display_name": "Network List Service" + }, + { + "pid": 0, + "name": "NetSetupSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Network Setup Service manages the installation of network drivers and permits the configuration of low-level network settings. If this service is stopped, any driver installations that are in-progress may be cancelled.", + "display_name": "Network Setup Service" + }, + { + "pid": 0, + "name": "NetTcpPortSharing", + "status": "stopped", + "binpath": "C:\\WINDOWS\\Microsoft.NET\\Framework64\\v4.0.30319\\SMSvcHost.exe", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Disabled", + "description": "Provides ability to share TCP ports over the net.tcp protocol.", + "display_name": "Net.Tcp Port Sharing Service" + }, + { + "pid": 2340, + "name": "NlaSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Automatic", + "description": "Collects and stores configuration information for the network and notifies programs when this information is modified. If this service is stopped, configuration information might be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Network Location Awareness" + }, + { + "pid": 1476, + "name": "nsi", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "This service delivers network notifications (e.g. interface addition/deleting etc) to user mode clients. Stopping this service will cause loss of network connectivity. If this service is disabled, any other services that explicitly depend on this service will fail to start.", + "display_name": "Network Store Interface Service" + }, + { + "pid": 0, + "name": "ose64", + "status": "stopped", + "binpath": "\"C:\\Program Files\\Common Files\\Microsoft Shared\\Source Engine\\OSE.EXE\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Saves installation files used for updates and repairs and is required for the downloading of Setup updates and Watson error reports.", + "display_name": "Office 64 Source Engine" + }, + { + "pid": 0, + "name": "p2pimsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServicePeerNet", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides identity services for the Peer Name Resolution Protocol (PNRP) and Peer-to-Peer Grouping services. If disabled, the Peer Name Resolution Protocol (PNRP) and Peer-to-Peer Grouping services may not function, and some applications, such as HomeGroup and Remote Assistance, may not function correctly.", + "display_name": "Peer Networking Identity Manager" + }, + { + "pid": 0, + "name": "p2psvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServicePeerNet", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Enables multi-party communication using Peer-to-Peer Grouping. If disabled, some applications, such as HomeGroup, may not function.", + "display_name": "Peer Networking Grouping" + }, + { + "pid": 2580, + "name": "PcaSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service provides support for the Program Compatibility Assistant (PCA). PCA monitors programs installed and run by the user and detects known compatibility problems. If this service is stopped, PCA will not function properly.", + "display_name": "Program Compatibility Assistant Service" + }, + { + "pid": 0, + "name": "PeerDistSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k PeerDist", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "This service caches network content from peers on the local subnet.", + "display_name": "BranchCache" + }, + { + "pid": 0, + "name": "perceptionsimulation", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\PerceptionSimulation\\PerceptionSimulationService.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables spatial perception simulation, virtual camera management and spatial input simulation.", + "display_name": "Windows Perception Simulation Service" + }, + { + "pid": 0, + "name": "PerfHost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\SysWow64\\perfhost.exe", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Enables remote users and 64-bit processes to query performance counters provided by 32-bit DLLs. If this service is stopped, only local users and 32-bit processes will be able to query performance counters provided by 32-bit DLLs.", + "display_name": "Performance Counter DLL Host" + }, + { + "pid": 0, + "name": "PhoneSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Manages the telephony state on the device", + "display_name": "Phone Service" + }, + { + "pid": 0, + "name": "pla", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNoNetwork -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Performance Logs and Alerts Collects performance data from local or remote computers based on preconfigured schedule parameters, then writes the data to a log or triggers an alert. If this service is stopped, performance information will not be collected. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Performance Logs & Alerts" + }, + { + "pid": 556, + "name": "PlugPlay", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k DcomLaunch -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables a computer to recognize and adapt to hardware changes with little or no user input. Stopping or disabling this service will result in system instability.", + "display_name": "Plug and Play" + }, + { + "pid": 0, + "name": "PNRPAutoReg", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServicePeerNet", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "This service publishes a machine name using the Peer Name Resolution Protocol. Configuration is managed via the netsh context 'p2p pnrp peer' ", + "display_name": "PNRP Machine Name Publication Service" + }, + { + "pid": 0, + "name": "PNRPsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServicePeerNet", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Enables serverless peer name resolution over the Internet using the Peer Name Resolution Protocol (PNRP). If disabled, some peer-to-peer and collaborative applications, such as Remote Assistance, may not function.", + "display_name": "Peer Name Resolution Protocol" + }, + { + "pid": 0, + "name": "PolicyAgent", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k NetworkServiceNetworkRestricted -p", + "username": "NT Authority\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Internet Protocol security (IPsec) supports network-level peer authentication, data origin authentication, data integrity, data confidentiality (encryption), and replay protection. This service enforces IPsec policies created through the IP Security Policies snap-in or the command-line tool \"netsh ipsec\". If you stop this service, you may experience network connectivity issues if your policy requires that connections use IPsec. Also,remote management of Windows Defender Firewall is not available when this service is stopped.", + "display_name": "IPsec Policy Agent" + }, + { + "pid": 1032, + "name": "Power", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k DcomLaunch -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Manages power policy and power policy notification delivery.", + "display_name": "Power" + }, + { + "pid": 0, + "name": "PrintNotify", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k print", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service opens custom printer dialog boxes and handles notifications from a remote print server or a printer. If you turn off this service, you won’t be able to see printer extensions or notifications.", + "display_name": "Printer Extensions and Notifications" + }, + { + "pid": 1960, + "name": "ProfSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "This service is responsible for loading and unloading user profiles. If this service is stopped or disabled, users will no longer be able to successfully sign in or sign out, apps might have problems getting to users' data, and components registered to receive profile event notifications won't receive them.", + "display_name": "User Profile Service" + }, + { + "pid": 0, + "name": "PushToInstall", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for the Microsoft Store. This service is started automatically and if disabled then remote installations will not function properly.", + "display_name": "Windows PushToInstall Service" + }, + { + "pid": 0, + "name": "QWAVE", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Quality Windows Audio Video Experience (qWave) is a networking platform for Audio Video (AV) streaming applications on IP home networks. qWave enhances AV streaming performance and reliability by ensuring network quality-of-service (QoS) for AV applications. It provides mechanisms for admission control, run time monitoring and enforcement, application feedback, and traffic prioritization.", + "display_name": "Quality Windows Audio Video Experience" + }, + { + "pid": 0, + "name": "RasAuto", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Creates a connection to a remote network whenever a program references a remote DNS or NetBIOS name or address.", + "display_name": "Remote Access Auto Connection Manager" + }, + { + "pid": 5048, + "name": "RasMan", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs", + "username": "localSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Manages dial-up and virtual private network (VPN) connections from this computer to the Internet or other remote networks. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Remote Access Connection Manager" + }, + { + "pid": 0, + "name": "RemoteAccess", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs", + "username": "localSystem", + "autodelay": false, + "start_type": "Disabled", + "description": "Offers routing services to businesses in local area and wide area network environments.", + "display_name": "Routing and Remote Access" + }, + { + "pid": 0, + "name": "RemoteRegistry", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k localService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Disabled", + "description": "Enables remote users to modify registry settings on this computer. If this service is stopped, the registry can be modified only by users on this computer. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Remote Registry" + }, + { + "pid": 0, + "name": "RetailDemo", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k rdxgroup", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Retail Demo service controls device activity while the device is in retail demo mode.", + "display_name": "Retail Demo Service" + }, + { + "pid": 0, + "name": "RpcLocator", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\locator.exe", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "In Windows 2003 and earlier versions of Windows, the Remote Procedure Call (RPC) Locator service manages the RPC name service database. In Windows Vista and later versions of Windows, this service does not provide any functionality and is present for application compatibility.", + "display_name": "Remote Procedure Call (RPC) Locator" + }, + { + "pid": 4436, + "name": "RtkAudioUniversalService", + "status": "running", + "binpath": "\"C:\\WINDOWS\\System32\\RtkAudUService64.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Realtek Audio Universal Service", + "display_name": "Realtek Audio Universal Service" + }, + { + "pid": 992, + "name": "SamSs", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\lsass.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "The startup of this service signals other services that the Security Accounts Manager (SAM) is ready to accept requests. Disabling this service will prevent other services in the system from being notified when the SAM is ready, which may in turn cause those services to fail to start correctly. This service should not be disabled.", + "display_name": "Security Accounts Manager" + }, + { + "pid": 0, + "name": "SCardSvr", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceAndNoImpersonation", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Manages access to smart cards read by this computer. If this service is stopped, this computer will be unable to read smart cards. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Smart Card" + }, + { + "pid": 0, + "name": "ScDeviceEnum", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Creates software device nodes for all smart card readers accessible to a given session. If this service is disabled, WinRT APIs will not be able to enumerate smart card readers.", + "display_name": "Smart Card Device Enumeration Service" + }, + { + "pid": 1748, + "name": "Schedule", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Enables a user to configure and schedule automated tasks on this computer. The service also hosts multiple Windows system-critical tasks. If this service is stopped or disabled, these tasks will not be run at their scheduled times. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Task Scheduler" + }, + { + "pid": 0, + "name": "SCPolicySvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Allows the system to be configured to lock the user desktop upon smart card removal.", + "display_name": "Smart Card Removal Policy" + }, + { + "pid": 0, + "name": "SDRSVC", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k SDRSVC", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides Windows Backup and Restore capabilities.", + "display_name": "Windows Backup" + }, + { + "pid": 0, + "name": "seclogon", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables starting processes under alternate credentials. If this service is stopped, this type of logon access will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Secondary Logon" + }, + { + "pid": 0, + "name": "SEMgrSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Manages payments and Near Field Communication (NFC) based secure elements.", + "display_name": "Payments and NFC/SE Manager" + }, + { + "pid": 2928, + "name": "SENS", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Monitors system events and notifies subscribers to COM+ Event System of these events.", + "display_name": "System Event Notification Service" + }, + { + "pid": 0, + "name": "SensorDataService", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\SensorDataService.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Delivers data from a variety of sensors", + "display_name": "Sensor Data Service" + }, + { + "pid": 0, + "name": "SensorService", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "A service for sensors that manages different sensors' functionality. Manages Simple Device Orientation (SDO) and History for sensors. Loads the SDO sensor that reports device orientation changes. If this service is stopped or disabled, the SDO sensor will not be loaded and so auto-rotation will not occur. History collection from Sensors will also be stopped.", + "display_name": "Sensor Service" + }, + { + "pid": 0, + "name": "SensrSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Monitors various sensors in order to expose data and adapt to system and user state. If this service is stopped or disabled, the display brightness will not adapt to lighting conditions. Stopping this service may affect other system functionality and features as well.", + "display_name": "Sensor Monitoring Service" + }, + { + "pid": 3296, + "name": "SessionEnv", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Remote Desktop Configuration service (RDCS) is responsible for all Remote Desktop Services and Remote Desktop related configuration and session maintenance activities that require SYSTEM context. These include per-session temporary folders, RD themes, and RD certificates.", + "display_name": "Remote Desktop Configuration" + }, + { + "pid": 7520, + "name": "SgrmBroker", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\SgrmBroker.exe", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Automatic", + "description": "Monitors and attests to the integrity of the Windows platform.", + "display_name": "System Guard Runtime Monitor Broker" + }, + { + "pid": 0, + "name": "SharedAccess", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides network address translation, addressing, name resolution and/or intrusion prevention services for a home or small office network.", + "display_name": "Internet Connection Sharing (ICS)" + }, + { + "pid": 0, + "name": "SharedRealitySvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "This service is used for Spatial Perception scenarios", + "display_name": "Spatial Data Service" + }, + { + "pid": 3632, + "name": "ShellHWDetection", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Provides notifications for AutoPlay hardware events.", + "display_name": "Shell Hardware Detection" + }, + { + "pid": 0, + "name": "shpamsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Disabled", + "description": "Manages profiles and accounts on a SharedPC configured device", + "display_name": "Shared PC Account Manager" + }, + { + "pid": 0, + "name": "smphost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k smphost", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Host service for the Microsoft Storage Spaces management provider. If this service is stopped or disabled, Storage Spaces cannot be managed.", + "display_name": "Microsoft Storage Spaces SMP" + }, + { + "pid": 0, + "name": "SNMPTRAP", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\snmptrap.exe", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Receives trap messages generated by local or remote Simple Network Management Protocol (SNMP) agents and forwards the messages to SNMP management programs running on this computer. If this service is stopped, SNMP-based programs on this computer will not receive SNMP trap messages. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "SNMP Trap" + }, + { + "pid": 0, + "name": "spectrum", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\spectrum.exe", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Enables spatial perception, spatial input, and holographic rendering.", + "display_name": "Windows Perception Service" + }, + { + "pid": 4104, + "name": "Spooler", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\spoolsv.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "This service spools print jobs and handles interaction with the printer. If you turn off this service, you won’t be able to print or see your printers.", + "display_name": "Print Spooler" + }, + { + "pid": 52504, + "name": "sppsvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\sppsvc.exe", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": true, + "start_type": "Automatic", + "description": "Enables the download, installation and enforcement of digital licenses for Windows and Windows applications. If the service is disabled, the operating system and licensed applications may run in a notification mode. It is strongly recommended that you not disable the Software Protection service.", + "display_name": "Software Protection" + }, + { + "pid": 3808, + "name": "SSDPSRV", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Discovers networked devices and services that use the SSDP discovery protocol, such as UPnP devices. Also announces SSDP devices and services running on the local computer. If this service is stopped, SSDP-based devices will not be discovered. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "SSDP Discovery" + }, + { + "pid": 0, + "name": "ssh-agent", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\OpenSSH\\ssh-agent.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Disabled", + "description": "Agent to hold private keys used for public key authentication.", + "display_name": "OpenSSH Authentication Agent" + }, + { + "pid": 29064, + "name": "sshd", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\OpenSSH\\sshd.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "SSH protocol based service to provide secure encrypted communications between two untrusted hosts over an insecure network.", + "display_name": "OpenSSH SSH Server" + }, + { + "pid": 4412, + "name": "SstpSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides support for the Secure Socket Tunneling Protocol (SSTP) to connect to remote computers using VPN. If this service is disabled, users will not be able to use SSTP to access remote servers.", + "display_name": "Secure Socket Tunneling Protocol Service" + }, + { + "pid": 3020, + "name": "StateRepository", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k appmodel -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides required infrastructure support for the application model.", + "display_name": "State Repository Service" + }, + { + "pid": 0, + "name": "stisvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k imgsvc", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides image acquisition services for scanners and cameras", + "display_name": "Windows Image Acquisition (WIA)" + }, + { + "pid": 1196, + "name": "StorSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Automatic", + "description": "Provides enabling services for storage settings and external storage expansion", + "display_name": "Storage Service" + }, + { + "pid": 0, + "name": "svsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Verifies potential file system corruptions.", + "display_name": "Spot Verifier" + }, + { + "pid": 0, + "name": "swprv", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k swprv", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Manages software-based volume shadow copies taken by the Volume Shadow Copy service. If this service is stopped, software-based volume shadow copies cannot be managed. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Microsoft Software Shadow Copy Provider" + }, + { + "pid": 2680, + "name": "SysMain", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Maintains and improves system performance over time.", + "display_name": "SysMain" + }, + { + "pid": 3744, + "name": "TabletInputService", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables Touch Keyboard and Handwriting Panel pen and ink functionality", + "display_name": "Touch Keyboard and Handwriting Panel Service" + }, + { + "pid": 0, + "name": "TapiSrv", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides Telephony API (TAPI) support for programs that control telephony devices on the local computer and, through the LAN, on servers that are also running the service.", + "display_name": "Telephony" + }, + { + "pid": 1364, + "name": "TermService", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService", + "username": "NT Authority\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Allows users to connect interactively to a remote computer. Remote Desktop and Remote Desktop Session Host Server depend on this service. To prevent remote use of this computer, clear the checkboxes on the Remote tab of the System properties control panel item.", + "display_name": "Remote Desktop Services" + }, + { + "pid": 2624, + "name": "Themes", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Provides user experience theme management.", + "display_name": "Themes" + }, + { + "pid": 0, + "name": "TieringEngineService", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\TieringEngineService.exe", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Optimizes the placement of data in storage tiers on all tiered storage spaces in the system.", + "display_name": "Storage Tiers Management" + }, + { + "pid": 984, + "name": "TokenBroker", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service is used by Web Account Manager to provide single-sign-on to apps and services.", + "display_name": "Web Account Manager" + }, + { + "pid": 4476, + "name": "TrkWks", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Maintains links between NTFS files within a computer or across computers in a network.", + "display_name": "Distributed Link Tracking Client" + }, + { + "pid": 0, + "name": "TroubleshootingSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables automatic mitigation for known problems by applying recommended troubleshooting. If stopped, your device will not get recommended troubleshooting for problems on your device.", + "display_name": "Recommended Troubleshooting Service" + }, + { + "pid": 0, + "name": "TrustedInstaller", + "status": "stopped", + "binpath": "C:\\WINDOWS\\servicing\\TrustedInstaller.exe", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables installation, modification, and removal of Windows updates and optional components. If this service is disabled, install or uninstall of Windows updates might fail for this computer.", + "display_name": "Windows Modules Installer" + }, + { + "pid": 0, + "name": "tzautoupdate", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Disabled", + "description": "Automatically sets the system time zone.", + "display_name": "Auto Time Zone Updater" + }, + { + "pid": 0, + "name": "UevAgentService", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\AgentService.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Disabled", + "description": "Provides support for application and OS settings roaming", + "display_name": "User Experience Virtualization Service" + }, + { + "pid": 2396, + "name": "UmRdpService", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Allows the redirection of Printers/Drives/Ports for RDP connections", + "display_name": "Remote Desktop Services UserMode Port Redirector" + }, + { + "pid": 0, + "name": "upnphost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Allows UPnP devices to be hosted on this computer. If this service is stopped, any hosted UPnP devices will stop functioning and no additional hosted devices can be added. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "UPnP Device Host" + }, + { + "pid": 2196, + "name": "UserManager", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "User Manager provides the runtime components required for multi-user interaction. If this service is stopped, some applications may not operate correctly.", + "display_name": "User Manager" + }, + { + "pid": 7208, + "name": "UsoSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Automatic", + "description": "Manages Windows Updates. If stopped, your devices will not be able to download and install the latest updates.", + "display_name": "Update Orchestrator Service" + }, + { + "pid": 0, + "name": "VacSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Hosts spatial analysis for Mixed Reality audio simulation.", + "display_name": "Volumetric Audio Compositor Service" + }, + { + "pid": 0, + "name": "VaultSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\lsass.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides secure storage and retrieval of credentials to users, applications and security service packages.", + "display_name": "Credential Manager" + }, + { + "pid": 0, + "name": "vds", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\vds.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides management services for disks, volumes, file systems, and storage arrays.", + "display_name": "Virtual Disk" + }, + { + "pid": 0, + "name": "vmicguestinterface", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides an interface for the Hyper-V host to interact with specific services running inside the virtual machine.", + "display_name": "Hyper-V Guest Service Interface" + }, + { + "pid": 0, + "name": "vmicheartbeat", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k ICService -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Monitors the state of this virtual machine by reporting a heartbeat at regular intervals. This service helps you identify running virtual machines that have stopped responding.", + "display_name": "Hyper-V Heartbeat Service" + }, + { + "pid": 0, + "name": "vmickvpexchange", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides a mechanism to exchange data between the virtual machine and the operating system running on the physical computer.", + "display_name": "Hyper-V Data Exchange Service" + }, + { + "pid": 0, + "name": "vmicrdv", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k ICService -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides a platform for communication between the virtual machine and the operating system running on the physical computer.", + "display_name": "Hyper-V Remote Desktop Virtualization Service" + }, + { + "pid": 0, + "name": "vmicshutdown", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides a mechanism to shut down the operating system of this virtual machine from the management interfaces on the physical computer.", + "display_name": "Hyper-V Guest Shutdown Service" + }, + { + "pid": 0, + "name": "vmictimesync", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Synchronizes the system time of this virtual machine with the system time of the physical computer.", + "display_name": "Hyper-V Time Synchronization Service" + }, + { + "pid": 0, + "name": "vmicvmsession", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides a mechanism to manage virtual machine with PowerShell via VM session without a virtual network.", + "display_name": "Hyper-V PowerShell Direct Service" + }, + { + "pid": 0, + "name": "vmicvss", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Coordinates the communications that are required to use Volume Shadow Copy Service to back up applications and data on this virtual machine from the operating system on the physical computer.", + "display_name": "Hyper-V Volume Shadow Copy Requestor" + }, + { + "pid": 0, + "name": "VSS", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\vssvc.exe", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Manages and implements Volume Shadow Copies used for backup and other purposes. If this service is stopped, shadow copies will be unavailable for backup and the backup may fail. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Volume Shadow Copy" + }, + { + "pid": 1516, + "name": "W32Time", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Maintains date and time synchronization on all clients and servers in the network. If this service is stopped, date and time synchronization will be unavailable. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Windows Time" + }, + { + "pid": 0, + "name": "WaaSMedicSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k wusvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables remediation and protection of Windows Update components.", + "display_name": "Windows Update Medic Service" + }, + { + "pid": 0, + "name": "WalletService", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k appmodel -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Hosts objects used by clients of the wallet", + "display_name": "WalletService" + }, + { + "pid": 0, + "name": "WarpJITSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceNetworkRestricted", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Provides a JIT out of process service for WARP when running with ACG enabled.", + "display_name": "WarpJITSvc" + }, + { + "pid": 0, + "name": "wbengine", + "status": "stopped", + "binpath": "\"C:\\WINDOWS\\system32\\wbengine.exe\"", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The WBENGINE service is used by Windows Backup to perform backup and recovery operations. If this service is stopped by a user, it may cause the currently running backup or recovery operation to fail. Disabling this service may disable backup and recovery operations using Windows Backup on this computer.", + "display_name": "Block Level Backup Engine Service" + }, + { + "pid": 23184, + "name": "WbioSrvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k WbioSvcGroup", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Windows biometric service gives client applications the ability to capture, compare, manipulate, and store biometric data without gaining direct access to any biometric hardware or samples. The service is hosted in a privileged SVCHOST process.", + "display_name": "Windows Biometric Service" + }, + { + "pid": 3828, + "name": "Wcmsvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Automatic", + "description": "Makes automatic connect/disconnect decisions based on the network connectivity options currently available to the PC and enables management of network connectivity based on Group Policy settings.", + "display_name": "Windows Connection Manager" + }, + { + "pid": 0, + "name": "wcncsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalServiceAndNoImpersonation -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "WCNCSVC hosts the Windows Connect Now Configuration which is Microsoft's Implementation of Wireless Protected Setup (WPS) protocol. This is used to configure Wireless LAN settings for an Access Point (AP) or a Wireless Device. The service is started programmatically as needed.", + "display_name": "Windows Connect Now - Config Registrar" + }, + { + "pid": 4824, + "name": "WdiServiceHost", + "status": "running", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "The Diagnostic Service Host is used by the Diagnostic Policy Service to host diagnostics that need to run in a Local Service context. If this service is stopped, any diagnostics that depend on it will no longer function.", + "display_name": "Diagnostic Service Host" + }, + { + "pid": 0, + "name": "WdiSystemHost", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "The Diagnostic System Host is used by the Diagnostic Policy Service to host diagnostics that need to run in a Local System context. If this service is stopped, any diagnostics that depend on it will no longer function.", + "display_name": "Diagnostic System Host" + }, + { + "pid": 0, + "name": "WebClient", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Enables Windows-based programs to create, access, and modify Internet-based files. If this service is stopped, these functions will not be available. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "WebClient" + }, + { + "pid": 0, + "name": "Wecsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k NetworkService -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "This service manages persistent subscriptions to events from remote sources that support WS-Management protocol. This includes Windows Vista event logs, hardware and IPMI-enabled event sources. The service stores forwarded events in a local Event Log. If this service is stopped or disabled event subscriptions cannot be created and forwarded events cannot be accepted.", + "display_name": "Windows Event Collector" + }, + { + "pid": 0, + "name": "WEPHOSTSVC", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k WepHostSvcGroup", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Windows Encryption Provider Host Service brokers encryption related functionalities from 3rd Party Encryption Providers to processes that need to evaluate and apply EAS policies. Stopping this will compromise EAS compliancy checks that have been established by the connected Mail Accounts", + "display_name": "Windows Encryption Provider Host Service" + }, + { + "pid": 0, + "name": "wercplsupport", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service provides support for viewing, sending and deletion of system-level problem reports for the Problem Reports control panel.", + "display_name": "Problem Reports Control Panel Support" + }, + { + "pid": 0, + "name": "WerSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k WerSvcGroup", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Allows errors to be reported when programs stop working or responding and allows existing solutions to be delivered. Also allows logs to be generated for diagnostic and repair services. If this service is stopped, error reporting might not work correctly and results of diagnostic services and repairs might not be displayed.", + "display_name": "Windows Error Reporting Service" + }, + { + "pid": 0, + "name": "WFDSConMgrSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "Manages connections to wireless services, including wireless display and docking.", + "display_name": "Wi-Fi Direct Services Connection Manager Service" + }, + { + "pid": 0, + "name": "WiaRpc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Launches applications associated with still image acquisition events.", + "display_name": "Still Image Acquisition Events" + }, + { + "pid": 3408, + "name": "Winmgmt", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Provides a common interface and object model to access management information about operating system, devices, applications and services. If this service is stopped, most Windows-based software will not function properly. If this service is disabled, any services that explicitly depend on it will fail to start.", + "display_name": "Windows Management Instrumentation" + }, + { + "pid": 0, + "name": "WinRM", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k NetworkService -p", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": true, + "start_type": "Manual", + "description": "Windows Remote Management (WinRM) service implements the WS-Management protocol for remote management. WS-Management is a standard web services protocol used for remote software and hardware management. The WinRM service listens on the network for WS-Management requests and processes them. The WinRM Service needs to be configured with a listener using winrm.cmd command line tool or through Group Policy in order for it to listen over the network. The WinRM service provides access to WMI data and enables event collection. Event collection and subscription to events require that the service is running. WinRM messages use HTTP and HTTPS as transports. The WinRM service does not depend on IIS but is preconfigured to share a port with IIS on the same machine. The WinRM service reserves the /wsman URL prefix. To prevent conflicts with IIS, administrators should ensure that any websites hosted on IIS do not use the /wsman URL prefix.", + "display_name": "Windows Remote Management (WS-Management)" + }, + { + "pid": 0, + "name": "wisvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides infrastructure support for the Windows Insider Program. This service must remain enabled for the Windows Insider Program to work.", + "display_name": "Windows Insider Service" + }, + { + "pid": 4032, + "name": "WlanSvc", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "The WLANSVC service provides the logic required to configure, discover, connect to, and disconnect from a wireless local area network (WLAN) as defined by IEEE 802.11 standards. It also contains the logic to turn your computer into a software access point so that other devices or computers can connect to your computer wirelessly using a WLAN adapter that can support this. Stopping or disabling the WLANSVC service will make all WLAN adapters on your computer inaccessible from the Windows networking UI. It is strongly recommended that you have the WLANSVC service running if your computer has a WLAN adapter.", + "display_name": "WLAN AutoConfig" + }, + { + "pid": 0, + "name": "wlidsvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables user sign-in through Microsoft account identity services. If this service is stopped, users will not be able to logon to the computer with their Microsoft account.", + "display_name": "Microsoft Account Sign-in Assistant" + }, + { + "pid": 0, + "name": "wlpasvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalServiceNetworkRestricted -p", + "username": "NT Authority\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "This service provides profile management for subscriber identity modules", + "display_name": "Local Profile Assistant Service" + }, + { + "pid": 0, + "name": "WManSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Performs management including Provisioning and Enrollment activities", + "display_name": "Windows Management Service" + }, + { + "pid": 0, + "name": "wmiApSrv", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\wbem\\WmiApSrv.exe", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides performance library information from Windows Management Instrumentation (WMI) providers to clients on the network. This service only runs when Performance Data Helper is activated.", + "display_name": "WMI Performance Adapter" + }, + { + "pid": 0, + "name": "WMPNetworkSvc", + "status": "stopped", + "binpath": "\"C:\\Program Files\\Windows Media Player\\wmpnetwk.exe\"", + "username": "NT AUTHORITY\\NetworkService", + "autodelay": false, + "start_type": "Manual", + "description": "Shares Windows Media Player libraries to other networked players and media devices using Universal Plug and Play", + "display_name": "Windows Media Player Network Sharing Service" + }, + { + "pid": 0, + "name": "workfolderssvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\System32\\svchost.exe -k LocalService -p", + "username": "NT AUTHORITY\\LocalService", + "autodelay": false, + "start_type": "Manual", + "description": "This service syncs files with the Work Folders server, enabling you to use the files on any of the PCs and devices on which you've set up Work Folders.", + "display_name": "Work Folders" + }, + { + "pid": 0, + "name": "WpcMonSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalService", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enforces parental controls for child accounts in Windows. If this service is stopped or disabled, parental controls may not be enforced.", + "display_name": "Parental Controls" + }, + { + "pid": 0, + "name": "WPDBusEnum", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enforces group policy for removable mass-storage devices. Enables applications such as Windows Media Player and Image Import Wizard to transfer and synchronize content using removable mass-storage devices.", + "display_name": "Portable Device Enumerator Service" + }, + { + "pid": 4532, + "name": "WpnService", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "This service runs in session 0 and hosts the notification platform and connection provider which handles the connection between the device and WNS server.", + "display_name": "Windows Push Notifications System Service" + }, + { + "pid": 5980, + "name": "WSearch", + "status": "running", + "binpath": "C:\\WINDOWS\\system32\\SearchIndexer.exe /Embedding", + "username": "LocalSystem", + "autodelay": true, + "start_type": "Automatic", + "description": "Provides content indexing, property caching, and search results for files, e-mail, and other content.", + "display_name": "Windows Search" + }, + { + "pid": 0, + "name": "wuauserv", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Enables the detection, download, and installation of updates for Windows and other programs. If this service is disabled, users of this computer will not be able to use Windows Update or its automatic updating feature, and programs will not be able to use the Windows Update Agent (WUA) API.", + "display_name": "Windows Update" + }, + { + "pid": 0, + "name": "WwanSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k LocalSystemNetworkRestricted -p", + "username": "localSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service manages mobile broadband (GSM & CDMA) data card/embedded module adapters and connections by auto-configuring the networks. It is strongly recommended that this service be kept running for best user experience of mobile broadband devices.", + "display_name": "WWAN AutoConfig" + }, + { + "pid": 0, + "name": "XblAuthManager", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "Provides authentication and authorization services for interacting with Xbox Live. If this service is stopped, some applications may not operate correctly.", + "display_name": "Xbox Live Auth Manager" + }, + { + "pid": 0, + "name": "XblGameSave", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service syncs save data for Xbox Live save enabled games. If this service is stopped, game save data will not upload to or download from Xbox Live.", + "display_name": "Xbox Live Game Save" + }, + { + "pid": 0, + "name": "XboxGipSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service manages connected Xbox Accessories.", + "display_name": "Xbox Accessory Management Service" + }, + { + "pid": 0, + "name": "XboxNetApiSvc", + "status": "stopped", + "binpath": "C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Manual", + "description": "This service supports the Windows.Networking.XboxLive application programming interface.", + "display_name": "Xbox Live Networking Service" + }, + { + "pid": 46520, + "name": "LMIGuardianSvc", + "status": "running", + "binpath": "\"C:\\Program Files (x86)\\LogMeIn\\x64\\LMIGuardianSvc.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Support LogMeIn processes with quality assurance feedback", + "display_name": "LMIGuardianSvc" + }, + { + "pid": 33952, + "name": "LMIMaint", + "status": "running", + "binpath": "\"C:\\Program Files (x86)\\LogMeIn\\x64\\RaMaint.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "", + "display_name": "LogMeIn Maintenance Service" + }, + { + "pid": 50408, + "name": "Mesh Agent", + "status": "running", + "binpath": "\"C:\\Program Files\\Mesh Agent\\MeshAgent.exe\" ", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Mesh Agent background service", + "display_name": "Mesh Agent" + }, + { + "pid": 49248, + "name": "tacticalrpc", + "status": "running", + "binpath": "\"C:\\Program Files\\TacticalAgent\\nssm.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Tactical RMM RPC Service", + "display_name": "Tactical RMM RPC Service" + }, + { + "pid": 53168, + "name": "tacticalagent", + "status": "running", + "binpath": "\"C:\\Program Files\\TacticalAgent\\nssm.exe\"", + "username": "LocalSystem", + "autodelay": false, + "start_type": "Automatic", + "description": "Tactical RMM Agent", + "display_name": "Tactical RMM Agent" + } +] \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/winupdates.json b/api/tacticalrmm/tacticalrmm/test_data/winupdates.json new file mode 100644 index 0000000000..1a4b7e7dbc --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/winupdates.json @@ -0,0 +1,319 @@ +{ + "samplecomputer": { + "07609d43-d518-4e77-856e-d1b316d1b8a8": { + "guid": "07609d43-d518-4e77-856e-d1b316d1b8a8", + "Title": "MSXML 6.0 RTM Security Update (925673)", + "Type": "Software", + "Description": "A vulnerability exists in Microsoft XML Core Services that could allow for information disclosure because the XMLHTTP ActiveX control incorrectly interprets an HTTP server-side redirect.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "Critical", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB925673" + ], + "Categories": [ + "Security Updates", + "SQL Server Feature Pack" + ] + }, + "729a0dcb-df9e-4d02-b603-ed1aee074428": { + "guid": "729a0dcb-df9e-4d02-b603-ed1aee074428", + "Title": "Security Update for Microsoft Visual C++ 2008 Service Pack 1 Redistributable Package (KB2538243)", + "Type": "Software", + "Description": "A security issue has been identified leading to MFC application vulnerability in DLL planting due to MFC not specifying the full path to system/localization DLLs. You can protect your computer by installing this update from Microsoft. After you install this item, you may have to restart your computer.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "Important", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB2538243" + ], + "Categories": [ + "Security Updates", + "Visual Studio 2008" + ] + }, + "527b2c0c-b10b-433d-9e35-4be03c28768a": { + "guid": "527b2c0c-b10b-433d-9e35-4be03c28768a", + "Title": "Update for Microsoft Office 2010 (KB2553347) 64-Bit Edition", + "Type": "Software", + "Description": "Microsoft has released an update for Microsoft Office 2010 64-Bit Edition. This update provides the latest fixes to Microsoft Office 2010 64-Bit Edition. Additionally, this update contains stability and performance improvements.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB2553347" + ], + "Categories": [ + "Critical Updates", + "Office 2010" + ] + }, + "7a7f49fc-15e8-4760-b750-e7c57d1bdb02": { + "guid": "7a7f49fc-15e8-4760-b750-e7c57d1bdb02", + "Title": "Security Update for Microsoft Office 2010 (KB4022206) 64-Bit Edition", + "Type": "Software", + "Description": "A security vulnerability exists in Microsoft Office 2010 64-Bit Edition that could allow arbitrary code to run when a maliciously modified file is opened. This update resolves that vulnerability.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4022206" + ], + "Categories": [ + "Office 2010", + "Security Updates" + ] + }, + "c168fa28-799f-4643-a45b-23d9d50875a9": { + "guid": "c168fa28-799f-4643-a45b-23d9d50875a9", + "Title": "Update for Microsoft Office 2010 (KB4461579) 64-Bit Edition", + "Type": "Software", + "Description": "Microsoft has released an update for Microsoft Office 2010 64-Bit Edition. This update provides the latest fixes to Microsoft Office 2010 64-Bit Edition. Additionally, this update contains stability and performance improvements.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4461579" + ], + "Categories": [ + "Critical Updates", + "Office 2010" + ] + }, + "ca3bb521-a8ea-4e26-a563-2ad6e3108b9a": { + "guid": "ca3bb521-a8ea-4e26-a563-2ad6e3108b9a", + "Title": "Microsoft Silverlight (KB4481252)", + "Type": "Software", + "Description": "Microsoft Silverlight is a Web browser plug-in for Windows and Mac OS X that delivers high quality video/audio, animation, and richer Website experiences in popular Web browsers.", + "Downloaded": false, + "Installed": false, + "Mandatory": false, + "EULAAccepted": false, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Never Requires Reboot", + "KBs": [ + "KB4481252" + ], + "Categories": [ + "Feature Packs", + "Silverlight" + ] + }, + "1edff8d4-bc5c-44e3-93ee-d123b6fd5c05": { + "guid": "1edff8d4-bc5c-44e3-93ee-d123b6fd5c05", + "Title": "Update for Microsoft Office 2010 (KB2589339) 64-Bit Edition", + "Type": "Software", + "Description": "Microsoft has released an update for Microsoft Office 2010 64-Bit Edition. This update provides the latest fixes to Microsoft Office 2010 64-Bit Edition. Additionally, this update contains stability and performance improvements.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB2589339" + ], + "Categories": [ + "Critical Updates", + "Office 2010" + ] + }, + "c9805b1b-e4e4-4179-91c5-fb3a57aa3368": { + "guid": "c9805b1b-e4e4-4179-91c5-fb3a57aa3368", + "Title": "Security Update for Microsoft Office 2010 (KB4484238) 64-Bit Edition", + "Type": "Software", + "Description": "A security vulnerability exists in Microsoft Office 2010 64-Bit Edition that could allow arbitrary code to run when a maliciously modified file is opened. This update resolves that vulnerability.", + "Downloaded": false, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "Important", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4484238" + ], + "Categories": [ + "Office 2010", + "Security Updates" + ] + }, + "2221dd34-39bb-4f16-b320-be49fe4a6b95": { + "guid": "2221dd34-39bb-4f16-b320-be49fe4a6b95", + "Title": "Windows Malicious Software Removal Tool x64 - v5.82 (KB890830)", + "Type": "Software", + "Description": "After the download, this tool runs one time to check your computer for infection by specific, prevalent malicious software (including Blaster, Sasser, and Mydoom) and helps remove any infection that is found. If an infection is found, the tool will display a status report the next time that you start your computer. A new version of the tool will be offered every month. If you want to manually run the tool on your computer, you can download a copy from the Microsoft Download Center, or you can run an online version from microsoft.com. This tool is not a replacement for an antivirus product. To help protect your computer, you should use an antivirus product.", + "Downloaded": true, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB890830" + ], + "Categories": [ + "Update Rollups", + "Windows Server 2016", + "Windows Server 2019" + ] + }, + "884a6101-3b1a-4b53-bede-2f8b6bf14772": { + "guid": "884a6101-3b1a-4b53-bede-2f8b6bf14772", + "Title": "2020-01 Update for Windows Server 2019 for x64-based Systems (KB4494174)", + "Type": "Software", + "Description": "Install this update to resolve issues in Windows. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article for more information. After you install this item, you may have to restart your computer.", + "Downloaded": true, + "Installed": true, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4494174" + ], + "Categories": [ + "Updates", + "Windows Server 2019" + ] + }, + "df5a6ec0-890e-4fb4-9a91-df999a4a5c46": { + "guid": "df5a6ec0-890e-4fb4-9a91-df999a4a5c46", + "Title": "Security Update for Microsoft Office 2010 (KB4484373) 64-Bit Edition", + "Type": "Software", + "Description": "A security vulnerability exists in Microsoft Office 2010 64-Bit Edition that could allow arbitrary code to run when a maliciously modified file is opened. This update resolves that vulnerability.", + "Downloaded": true, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "Important", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4484373" + ], + "Categories": [ + "Office 2010", + "Security Updates" + ] + }, + "14b1604e-c818-4fae-b1df-2bd789ec173a": { + "guid": "14b1604e-c818-4fae-b1df-2bd789ec173a", + "Title": "2020-06 Security Update for Adobe Flash Player for Windows Server 2019 for x64-based Systems (KB4561600)", + "Type": "Software", + "Description": "A security issue has been identified in a Microsoft software product that could affect your system. You can help protect your system by installing this update from Microsoft. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article. After you install this update, you may have to restart your system.", + "Downloaded": true, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "Critical", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4561600" + ], + "Categories": [ + "Security Updates", + "Windows Server 2019" + ] + }, + "9aab9121-0766-4f06-8204-61da23cc34b9": { + "guid": "9aab9121-0766-4f06-8204-61da23cc34b9", + "Title": "SQL Server 2019 RTM Cumulative Update (CU) 5 KB4552255", + "Type": "Software", + "Description": "CU5 for SQL Server 2019 RTM upgraded all SQL Server 2019 RTM instances and components installed through the SQL Server setup. CU5 can upgrade all editions and servicing levels of SQL Server 2019 RTM to the CU5 level. For customers in need of additional installation options, please visit the Microsoft Download Center to download the latest Cumulative Update (https://support.microsoft.com/en-us/kb/957826). To learn more about SQL Server 2019 RTM CU5, please visit the Microsoft Support (http://support.microsoft.com) Knowledge Base article KB4552255.", + "Downloaded": false, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4552255" + ], + "Categories": [ + "Microsoft SQL Server 2019", + "Updates" + ] + }, + "0d506775-e391-41bd-b932-d79df9147c9b": { + "guid": "0d506775-e391-41bd-b932-d79df9147c9b", + "Title": "2020-07 Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64 (KB4566516)", + "Type": "Software", + "Description": "A security issue has been identified in a Microsoft software product that could affect your system. You can help protect your system by installing this update from Microsoft. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article. After you install this update, you may have to restart your system.", + "Downloaded": true, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "Critical", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4566516" + ], + "Categories": [ + "Security Updates", + "Windows Server 2019" + ] + }, + "0641752f-29fb-48d7-a3cf-f93dde26b82b": { + "guid": "0641752f-29fb-48d7-a3cf-f93dde26b82b", + "Title": "2020-07 Cumulative Update for Windows Server 2019 (1809) for x64-based Systems (KB4558998)", + "Type": "Software", + "Description": "Install this update to resolve issues in Windows. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article for more information. After you install this item, you may have to restart your computer.", + "Downloaded": true, + "Installed": false, + "Mandatory": false, + "EULAAccepted": true, + "NeedsReboot": false, + "Severity": "", + "UserInput": false, + "RebootBehavior": "Can Require Reboot", + "KBs": [ + "KB4558998" + ], + "Categories": [ + "Security Updates" + ] + } + } +} \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/wmi1.json b/api/tacticalrmm/tacticalrmm/test_data/wmi1.json new file mode 100644 index 0000000000..4714d8dc66 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/wmi1.json @@ -0,0 +1,2397 @@ +{ + "graphics": [ + [ + { + "Name": "NVIDIA Quadro P520", + "Status": "OK", + "Caption": "NVIDIA Quadro P520", + "DeviceID": "VideoController1", + "AdapterRAM": 2147483648, + "DriverDate": "2020-11-20T00:00:00Z", + "SystemName": "VNVMHOSTSRV4", + "Description": "NVIDIA Quadro P520", + "InstallDate": "0001-01-01T00:00:00Z", + "Availability": 3, + "DriverVersion": "27.21.14.5266", + "AdapterDACType": "Integrated RAMDAC", + "MaxRefreshRate": 0, + "MinRefreshRate": 0, + "VideoProcessor": "Quadro P520", + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "CurrentRefreshRate": 60, + "MaxMemorySupported": 0, + "AdapterCompatibility": "NVIDIA", + "VideoModeDescription": "2560 x 1440 x 4294967296 colors", + "CapabilityDescriptions": null, + "AcceleratorCapabilities": null, + "InstalledDisplayDrivers": "C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll", + "SystemCreationClassName": "Win32_ComputerSystem", + "CurrentVerticalResolution": 1440 + } + ], + [ + { + "Name": "Intel(R) UHD Graphics 620", + "Status": "OK", + "Caption": "Intel(R) UHD Graphics 620", + "DeviceID": "VideoController2", + "AdapterRAM": 1073741824, + "DriverDate": "2020-10-28T00:00:00Z", + "SystemName": "VNVMHOSTSRV4", + "Description": "Intel(R) UHD Graphics 620", + "InstallDate": "0001-01-01T00:00:00Z", + "Availability": 3, + "DriverVersion": "27.20.100.8935", + "AdapterDACType": "Internal", + "MaxRefreshRate": 60, + "MinRefreshRate": 29, + "VideoProcessor": "Intel(R) UHD Graphics Family", + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "CurrentRefreshRate": 59, + "MaxMemorySupported": 0, + "AdapterCompatibility": "Intel Corporation", + "VideoModeDescription": "3840 x 1600 x 4294967296 colors", + "CapabilityDescriptions": null, + "AcceleratorCapabilities": null, + "InstalledDisplayDrivers": "C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igdumdim64.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igd10iumd64.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igd10iumd64.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igd12umd64.dll", + "SystemCreationClassName": "Win32_ComputerSystem", + "CurrentVerticalResolution": 1600 + } + ] + ], + "os": [ + [ + { + "Name": "Microsoft Windows Server 2019 Standard|C:\\Windows|\\Device\\Harddisk2\\Partition4", + "Debug": false, + "CSName": "VNVMHOSTSRV4", + "Locale": "0409", + "OSType": 18, + "Status": "OK", + "Caption": "Microsoft Windows Server 2019 Standard", + "CodeSet": "1252", + "Primary": true, + "Version": "10.0.17763", + "BuildType": "Multiprocessor Free", + "SuiteMask": 272, + "BootDevice": "\\Device\\HarddiskVolume4", + "CSDVersion": "", + "OSLanguage": 1033, + "PAEEnabled": false, + "BuildNumber": "17763", + "CountryCode": "1", + "Description": "", + "Distributed": false, + "InstallDate": "2020-02-21T12:32:21-08:00", + "ProductType": 3, + "SystemDrive": "C:", + "MUILanguages": [ + "en-US" + ], + "Manufacturer": "Microsoft Corporation", + "Organization": "", + "SerialNumber": "00429-80127-85875-AA456", + "SystemDevice": "\\Device\\HarddiskVolume6", + "LocalDateTime": "2020-11-26T00:15:02.743-08:00", + "NumberOfUsers": 1, + "PlusProductID": "", + "LastBootUpTime": "2020-11-12T08:09:38.56763-08:00", + "OSArchitecture": "64-bit", + "OSProductSuite": 272, + "RegisteredUser": "Windows User", + "CurrentTimeZone": -480, + "EncryptionLevel": 256, + "SystemDirectory": "C:\\Windows\\system32", + "WindowsDirectory": "C:\\Windows", + "CreationClassName": "Win32_OperatingSystem", + "FreeVirtualMemory": 124707648, + "NumberOfProcesses": 119, + "PlusVersionNumber": "", + "FreePhysicalMemory": 95233388, + "OperatingSystemSKU": 7, + "TotalSwapSpaceSize": 0, + "CSCreationClassName": "Win32_ComputerSystem", + "MaxNumberOfProcesses": 4294967295, + "MaxProcessMemorySize": 137438953344, + "OtherTypeDescription": "", + "NumberOfLicensedUsers": 0, + "FreeSpaceInPagingFiles": 29360128, + "TotalVirtualMemorySize": 230546044, + "TotalVisibleMemorySize": 201185916, + "ServicePackMajorVersion": 0, + "ServicePackMinorVersion": 0, + "SizeStoredInPagingFiles": 29360128, + "ForegroundApplicationBoost": 2 + } + ] + ], + "cpu": [ + [ + { + "Name": "Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz", + "Role": "CPU", + "Level": 6, + "Family": 179, + "Status": "OK", + "Caption": "Intel64 Family 6 Model 79 Stepping 1", + "Version": "", + "AssetTag": "UNKNOWN", + "DeviceID": "CPU0", + "ExtClock": 100, + "Revision": 20225, + "Stepping": "", + "UniqueId": "", + "CpuStatus": 1, + "DataWidth": 64, + "PartNumber": "", + "StatusInfo": 3, + "SystemName": "VNVMHOSTSRV4", + "Description": "Intel64 Family 6 Model 79 Stepping 1", + "InstallDate": "0001-01-01T00:00:00Z", + "L2CacheSize": 2048, + "L3CacheSize": 20480, + "PNPDeviceID": "", + "ProcessorId": "BFEBFBFF000406F1", + "ThreadCount": 16, + "VoltageCaps": 0, + "AddressWidth": 64, + "Architecture": 9, + "Availability": 3, + "ErrorCleared": false, + "L2CacheSpeed": 0, + "L3CacheSpeed": 0, + "Manufacturer": "GenuineIntel", + "SerialNumber": "", + "LastErrorCode": 0, + "MaxClockSpeed": 2098, + "NumberOfCores": 8, + "ProcessorType": 3, + "UpgradeMethod": 43, + "CurrentVoltage": 18, + "LoadPercentage": 1, + "Characteristics": 252, + "ErrorDescription": "", + "CreationClassName": "Win32_Processor", + "CurrentClockSpeed": 2098, + "SocketDesignation": "Proc 1", + "NumberOfEnabledCore": 8, + "ConfigManagerErrorCode": 0, + "OtherFamilyDescription": "", + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "VMMonitorModeExtensions": false, + "PowerManagementSupported": false, + "NumberOfLogicalProcessors": 16, + "VirtualizationFirmwareEnabled": false, + "SecondLevelAddressTranslationExtensions": false + } + ], + [ + { + "Name": "Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz", + "Role": "CPU", + "Level": 6, + "Family": 179, + "Status": "OK", + "Caption": "Intel64 Family 6 Model 79 Stepping 1", + "Version": "", + "AssetTag": "UNKNOWN", + "DeviceID": "CPU1", + "ExtClock": 100, + "Revision": 20225, + "Stepping": "", + "UniqueId": "", + "CpuStatus": 1, + "DataWidth": 64, + "PartNumber": "", + "StatusInfo": 3, + "SystemName": "VNVMHOSTSRV4", + "Description": "Intel64 Family 6 Model 79 Stepping 1", + "InstallDate": "0001-01-01T00:00:00Z", + "L2CacheSize": 2048, + "L3CacheSize": 20480, + "PNPDeviceID": "", + "ProcessorId": "BFEBFBFF000406F1", + "ThreadCount": 16, + "VoltageCaps": 0, + "AddressWidth": 64, + "Architecture": 9, + "Availability": 3, + "ErrorCleared": false, + "L2CacheSpeed": 0, + "L3CacheSpeed": 0, + "Manufacturer": "GenuineIntel", + "SerialNumber": "", + "LastErrorCode": 0, + "MaxClockSpeed": 2098, + "NumberOfCores": 8, + "ProcessorType": 3, + "UpgradeMethod": 43, + "CurrentVoltage": 18, + "LoadPercentage": 0, + "Characteristics": 252, + "ErrorDescription": "", + "CreationClassName": "Win32_Processor", + "CurrentClockSpeed": 2098, + "SocketDesignation": "Proc 2", + "NumberOfEnabledCore": 8, + "ConfigManagerErrorCode": 0, + "OtherFamilyDescription": "", + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "VMMonitorModeExtensions": false, + "PowerManagementSupported": false, + "NumberOfLogicalProcessors": 16, + "VirtualizationFirmwareEnabled": false, + "SecondLevelAddressTranslationExtensions": false + } + ] + ], + "mem": [ + [ + { + "SKU": "", + "Tag": "Physical Memory 0", + "Name": "Physical Memory", + "Model": "", + "Speed": 2400, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 17179869184, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 1, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "809082-091", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "HP ", + "SerialNumber": "", + "DeviceLocator": "PROC 1 DIMM 1", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 3", + "Name": "Physical Memory", + "Model": "", + "Speed": 2400, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 17179869184, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 1, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "809082-091", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "HP ", + "SerialNumber": "", + "DeviceLocator": "PROC 1 DIMM 4", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 8", + "Name": "Physical Memory", + "Model": "", + "Speed": 2400, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 17179869184, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 1, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "809082-091", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "HP ", + "SerialNumber": "", + "DeviceLocator": "PROC 1 DIMM 9", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 11", + "Name": "Physical Memory", + "Model": "", + "Speed": 2400, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 17179869184, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 1, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "809082-091", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "HP ", + "SerialNumber": "", + "DeviceLocator": "PROC 1 DIMM 12", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 12", + "Name": "Physical Memory", + "Model": "", + "Speed": 2600, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 34359738368, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 2, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "NOT AVAILABLE", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "UNKNOWN", + "SerialNumber": "", + "DeviceLocator": "PROC 2 DIMM 1", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 15", + "Name": "Physical Memory", + "Model": "", + "Speed": 2600, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 34359738368, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 2, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "NOT AVAILABLE", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "UNKNOWN", + "SerialNumber": "", + "DeviceLocator": "PROC 2 DIMM 4", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 20", + "Name": "Physical Memory", + "Model": "", + "Speed": 2600, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 34359738368, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 2, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "NOT AVAILABLE", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "UNKNOWN", + "SerialNumber": "", + "DeviceLocator": "PROC 2 DIMM 9", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ], + [ + { + "SKU": "", + "Tag": "Physical Memory 23", + "Name": "Physical Memory", + "Model": "", + "Speed": 2600, + "Status": "", + "Caption": "Physical Memory", + "Version": "", + "Capacity": 34359738368, + "BankLabel": "", + "DataWidth": 64, + "PoweredOn": false, + "Removable": false, + "Attributes": 2, + "FormFactor": 8, + "MaxVoltage": 1200, + "MemoryType": 0, + "MinVoltage": 1200, + "PartNumber": "NOT AVAILABLE", + "TotalWidth": 72, + "TypeDetail": 8320, + "Description": "Physical Memory", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": false, + "HotSwappable": false, + "Manufacturer": "UNKNOWN", + "SerialNumber": "", + "DeviceLocator": "PROC 2 DIMM 12", + "PositionInRow": 0, + "SMBIOSMemoryType": 26, + "ConfiguredVoltage": 1200, + "CreationClassName": "Win32_PhysicalMemory", + "InterleavePosition": 0, + "InterleaveDataDepth": 0, + "ConfiguredClockSpeed": 2133, + "OtherIdentifyingInfo": "" + } + ] + ], + "usb": [ + [ + { + "Name": "Standard Universal PCI to USB Host Controller", + "Status": "OK", + "Caption": "Standard Universal PCI to USB Host Controller", + "DeviceID": "PCI\\VEN_103C&DEV_3300&SUBSYS_3381103C&REV_03\\4&2383FE5D&0&04E2", + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "Description": "Standard Universal PCI to USB Host Controller", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_103C&DEV_3300&SUBSYS_3381103C&REV_03\\4&2383FE5D&0&04E2", + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "(Standard USB Host Controller)", + "LastErrorCode": 0, + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "ErrorDescription": "", + "CreationClassName": "Win32_USBController", + "ProtocolSupported": 16, + "MaxNumberControlled": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "Name": "Standard Enhanced PCI to USB Host Controller", + "Status": "OK", + "Caption": "Standard Enhanced PCI to USB Host Controller", + "DeviceID": "PCI\\VEN_8086&DEV_8D2D&SUBSYS_8030103C&REV_05\\3&11583659&0&D0", + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "Description": "Standard Enhanced PCI to USB Host Controller", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_8086&DEV_8D2D&SUBSYS_8030103C&REV_05\\3&11583659&0&D0", + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "(Standard USB Host Controller)", + "LastErrorCode": 0, + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "ErrorDescription": "", + "CreationClassName": "Win32_USBController", + "ProtocolSupported": 16, + "MaxNumberControlled": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "Name": "Standard Enhanced PCI to USB Host Controller", + "Status": "OK", + "Caption": "Standard Enhanced PCI to USB Host Controller", + "DeviceID": "PCI\\VEN_8086&DEV_8D26&SUBSYS_8030103C&REV_05\\3&11583659&0&E8", + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "Description": "Standard Enhanced PCI to USB Host Controller", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_8086&DEV_8D26&SUBSYS_8030103C&REV_05\\3&11583659&0&E8", + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "(Standard USB Host Controller)", + "LastErrorCode": 0, + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "ErrorDescription": "", + "CreationClassName": "Win32_USBController", + "ProtocolSupported": 16, + "MaxNumberControlled": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "Name": "Intel(R) USB 3.0 eXtensible Host Controller - 1.0 (Microsoft)", + "Status": "OK", + "Caption": "Intel(R) USB 3.0 eXtensible Host Controller - 1.0 (Microsoft)", + "DeviceID": "PCI\\VEN_8086&DEV_8D31&SUBSYS_8030103C&REV_05\\3&11583659&0&A0", + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "Description": "USB xHCI Compliant Host Controller", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_8086&DEV_8D31&SUBSYS_8030103C&REV_05\\3&11583659&0&A0", + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "Generic USB xHCI Host Controller", + "LastErrorCode": 0, + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "ErrorDescription": "", + "CreationClassName": "Win32_USBController", + "ProtocolSupported": 16, + "MaxNumberControlled": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ] + ], + "bios": [ + [ + { + "Name": "P89", + "Status": "OK", + "Caption": "P89", + "CodeSet": "", + "Version": "HP - 1", + "BIOSVersion": [ + "HP - 1", + "P89", + "HP - 21E00" + ], + "BuildNumber": "", + "Description": "P89", + "InstallDate": "0001-01-01T00:00:00Z", + "PrimaryBIOS": true, + "ReleaseDate": "2016-09-13T00:00:00Z", + "Manufacturer": "HP", + "SerialNumber": "ABCD123456", + "OtherTargetOS": "", + "SMBIOSPresent": true, + "CurrentLanguage": "", + "LanguageEdition": "", + "ListOfLanguages": null, + "SMBIOSBIOSVersion": "P89", + "SoftwareElementID": "P89", + "IdentificationCode": "", + "SMBIOSMajorVersion": 2, + "SMBIOSMinorVersion": 8, + "InstallableLanguages": 0, + "SoftwareElementState": 3, + "TargetOperatingSystem": 0, + "SystemBiosMajorVersion": 2, + "SystemBiosMinorVersion": 30, + "EmbeddedControllerMajorVersion": 2, + "EmbeddedControllerMinorVersion": 50 + } + ] + ], + "disk": [ + [ + { + "Name": "\\\\.\\PHYSICALDRIVE5", + "Size": 3962100925440, + "Index": 5, + "Model": "Microsoft Storage Space Device", + "Status": "OK", + "Caption": "Microsoft Storage Space Device", + "SCSIBus": 0, + "DeviceID": "\\\\.\\PHYSICALDRIVE5", + "SCSIPort": 0, + "MediaType": "Fixed hard disk media", + "Signature": 0, + "Partitions": 1, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "TotalHeads": 255, + "Description": "Disk drive", + "InstallDate": "0001-01-01T00:00:00Z", + "MediaLoaded": true, + "PNPDeviceID": "STORAGE\\DISK\\{C905ECA4-6609-4829-8510-DB4D3AFD36E6}", + "TotalTracks": 122832990, + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "(Standard disk drives)", + "MaxBlockSize": 0, + "MaxMediaSize": 0, + "MinBlockSize": 0, + "SCSITargetId": 0, + "SerialNumber": "{c905eca4-6609-4829-8510-db4d3afd36e6}", + "TotalSectors": 7738478370, + "InterfaceType": "SCSI", + "LastErrorCode": 0, + "NeedsCleaning": false, + "BytesPerSector": 512, + "TotalCylinders": 481698, + "SCSILogicalUnit": 1, + "SectorsPerTrack": 63, + "DefaultBlockSize": 0, + "ErrorDescription": "", + "ErrorMethodology": "", + "FirmwareRevision": "0.1", + "CompressionMethod": "", + "CreationClassName": "Win32_DiskDrive", + "TracksPerCylinder": 255, + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ], + "ConfigManagerErrorCode": 0, + "NumberOfMediaSupported": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "Name": "\\\\.\\PHYSICALDRIVE3", + "Size": 500105249280, + "Index": 3, + "Model": "ATA CT500MX500SSD1 SCSI Disk Device", + "Status": "OK", + "Caption": "ATA CT500MX500SSD1 SCSI Disk Device", + "SCSIBus": 0, + "DeviceID": "\\\\.\\PHYSICALDRIVE3", + "SCSIPort": 0, + "MediaType": "Fixed hard disk media", + "Signature": 0, + "Partitions": 0, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "TotalHeads": 255, + "Description": "Disk drive", + "InstallDate": "0001-01-01T00:00:00Z", + "MediaLoaded": true, + "PNPDeviceID": "SCSI\\DISK&VEN_ATA&PROD_CT500MX500SSD1\\5&2F1553C3&0&000700", + "TotalTracks": 15504255, + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "(Standard disk drives)", + "MaxBlockSize": 0, + "MaxMediaSize": 0, + "MinBlockSize": 0, + "SCSITargetId": 7, + "SerialNumber": "1922E2071D37 ", + "TotalSectors": 976768065, + "InterfaceType": "SCSI", + "LastErrorCode": 0, + "NeedsCleaning": false, + "BytesPerSector": 512, + "TotalCylinders": 60801, + "SCSILogicalUnit": 0, + "SectorsPerTrack": 63, + "DefaultBlockSize": 0, + "ErrorDescription": "", + "ErrorMethodology": "", + "FirmwareRevision": "023 ", + "CompressionMethod": "", + "CreationClassName": "Win32_DiskDrive", + "TracksPerCylinder": 255, + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ], + "ConfigManagerErrorCode": 0, + "NumberOfMediaSupported": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "Name": "\\\\.\\PHYSICALDRIVE2", + "Size": 500105249280, + "Index": 2, + "Model": "ATA Crucial_CT500MX2 SCSI Disk Device", + "Status": "OK", + "Caption": "ATA Crucial_CT500MX2 SCSI Disk Device", + "SCSIBus": 0, + "DeviceID": "\\\\.\\PHYSICALDRIVE2", + "SCSIPort": 0, + "MediaType": "Fixed hard disk media", + "Signature": 0, + "Partitions": 3, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "TotalHeads": 255, + "Description": "Disk drive", + "InstallDate": "0001-01-01T00:00:00Z", + "MediaLoaded": true, + "PNPDeviceID": "SCSI\\DISK&VEN_ATA&PROD_CRUCIAL_CT500MX2\\5&2F1553C3&0&000600", + "TotalTracks": 15504255, + "Availability": 0, + "ErrorCleared": false, + "Manufacturer": "(Standard disk drives)", + "MaxBlockSize": 0, + "MaxMediaSize": 0, + "MinBlockSize": 0, + "SCSITargetId": 6, + "SerialNumber": " 153810985FA4", + "TotalSectors": 976768065, + "InterfaceType": "SCSI", + "LastErrorCode": 0, + "NeedsCleaning": false, + "BytesPerSector": 512, + "TotalCylinders": 60801, + "SCSILogicalUnit": 0, + "SectorsPerTrack": 63, + "DefaultBlockSize": 0, + "ErrorDescription": "", + "ErrorMethodology": "", + "FirmwareRevision": "MU02", + "CompressionMethod": "", + "CreationClassName": "Win32_DiskDrive", + "TracksPerCylinder": 255, + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ], + "ConfigManagerErrorCode": 0, + "NumberOfMediaSupported": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ] + ], + "comp_sys": [ + [ + { + "Name": "VNVMHOSTSRV4", + "Model": "ProLiant DL380 Gen9", + "Roles": [ + "LM_Workstation", + "LM_Server", + "NT", + "Server_NT" + ], + "Domain": "WORKGROUP", + "Status": "OK", + "Caption": "VNVMHOSTSRV4", + "UserName": "", + "Workgroup": "WORKGROUP", + "DomainRole": 2, + "NameFormat": "", + "PowerState": 0, + "ResetCount": -1, + "ResetLimit": -1, + "SystemType": "x64-based PC", + "WakeUpType": 6, + "BootupState": "Normal boot", + "DNSHostName": "VNVMHOSTSRV4", + "Description": "AT/AT COMPATIBLE", + "InstallDate": "0001-01-01T00:00:00Z", + "Manufacturer": "HP", + "PCSystemType": 4, + "PartOfDomain": false, + "SystemFamily": "ProLiant", + "ThermalState": 3, + "OEMStringArray": [ + "PSF: ", + "Product ID: 859084-S01", + "OEM String: " + ], + "PCSystemTypeEx": 4, + "CurrentTimeZone": -480, + "PauseAfterReset": -1, + "ResetCapability": 1, + "SystemSKUNumber": "859084-S01", + "BootROMSupported": true, + "ChassisSKUNumber": "", + "DaylightInEffect": false, + "PowerSupplyState": 3, + "PrimaryOwnerName": "Windows User", + "BootOptionOnLimit": 0, + "CreationClassName": "Win32_ComputerSystem", + "HypervisorPresent": true, + "InfraredSupported": false, + "ChassisBootupState": 3, + "NumberOfProcessors": 2, + "AdminPasswordStatus": 3, + "PrimaryOwnerContact": "", + "TotalPhysicalMemory": 206014377984, + "BootOptionOnWatchDog": 0, + "FrontPanelResetStatus": 3, + "PowerOnPasswordStatus": 3, + "KeyboardPasswordStatus": 3, + "AutomaticManagedPagefile": true, + "AutomaticResetBootOption": true, + "AutomaticResetCapability": true, + "NetworkServerModeEnabled": true, + "PowerManagementSupported": false, + "EnableDaylightSavingsTime": true, + "NumberOfLogicalProcessors": 32, + "SupportContactDescription": null + } + ] + ], + "base_board": [ + [ + { + "SKU": "", + "Tag": "Base Board", + "Name": "Base Board", + "Depth": 0, + "Model": "", + "Width": 0, + "Height": 0, + "Status": "OK", + "Weight": 0, + "Caption": "Base Board", + "Product": "ProLiant DL380 Gen9", + "Version": "", + "PoweredOn": true, + "Removable": true, + "PartNumber": "", + "SlotLayout": "", + "Description": "Base Board", + "InstallDate": "0001-01-01T00:00:00Z", + "Replaceable": true, + "HostingBoard": true, + "HotSwappable": false, + "Manufacturer": "HP", + "SerialNumber": "ABCD123456", + "ConfigOptions": null, + "CreationClassName": "Win32_BaseBoard", + "SpecialRequirements": false, + "OtherIdentifyingInfo": "", + "RequiresDaughterBoard": false, + "RequirementsDescription": "" + } + ] + ], + "comp_sys_prod": [ + [ + { + "Name": "ProLiant DL380 Gen9", + "UUID": "30393538-3438-584D-5136-353130325031", + "Vendor": "HP", + "Caption": "Computer System Product", + "Version": "", + "SKUNumber": "", + "Description": "Computer System Product", + "IdentifyingNumber": "ABCD123456" + } + ] + ], + "network_config": [ + [ + { + "MTU": 0, + "Index": 0, + "Caption": "[00000000] Microsoft Kernel Debug Network Adapter", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{C8813F21-FC58-4EF5-B6DE-F10DF9AFF459}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Microsoft Kernel Debug Network Adapter", + "ServiceName": "kdnic", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 14, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 1, + "Caption": "[00000001] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{07BCD0FA-68CB-43DA-A95C-9F5761C070E4}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "3C:A8:2A:E6:27:C9", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 3, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 2, + "Caption": "[00000002] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{EB82D1C8-1AE1-436F-92C9-CAAC93E72E95}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "94:18:82:6E:16:2C", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 17, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 3, + "Caption": "[00000003] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{77E8A4E5-31CE-4618-A6A8-084310C616E2}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "3C:A8:2A:E6:27:CA", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 10, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 4, + "Caption": "[00000004] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": [ + "255.255.240.0", + "64" + ], + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": [ + "172.17.9.121", + "fe80::45d7:5980:c39a:b6b2" + ], + "IPEnabled": true, + "SettingID": "{7128A11A-718E-430E-98C7-99DE8AE13020}", + "DHCPServer": "172.17.0.1", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "3C:A8:2A:E6:27:C8", + "DHCPEnabled": true, + "DNSHostName": "VNVMHOSTSRV4", + "Description": "Broadcom NetXtreme Gigabit Ethernet #4", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 8, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "2020-11-26T20:09:51-08:00", + "DefaultIPGateway": [ + "172.17.0.1" + ], + "DHCPLeaseObtained": "2020-11-25T20:09:51-08:00", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 25, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": [], + "IPSecPermitUDPPorts": [], + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": [ + "172.17.4.11", + "172.17.4.22", + "172.17.4.86" + ], + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": [], + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": true, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": [], + "FullDNSRegistrationEnabled": true, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 5, + "Caption": "[00000005] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{1A157B66-2890-4416-B43F-883CD05B8B0B}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "94:18:82:6E:16:2D", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 4, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 6, + "Caption": "[00000006] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{C2DD58DC-8153-4492-8A4A-A3C6804BF2D7}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "3C:A8:2A:E6:27:CB", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 13, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 7, + "Caption": "[00000007] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{D670A8E4-1D5B-4E7C-90EF-7B95ED0F9A1B}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "94:18:82:6E:16:2E", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 15, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 8, + "Caption": "[00000008] Broadcom NetXtreme Gigabit Ethernet", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{3E4991C7-0C2C-42BB-A40F-133AAD11FB34}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "94:18:82:6E:16:2F", + "DHCPEnabled": true, + "DNSHostName": "", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 6, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 9, + "Caption": "[00000009] Hyper-V Virtual Switch Extension Adapter", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{DA1BE60D-9764-4A53-A05A-BAFD7D6D8622}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "", + "DHCPEnabled": false, + "DNSHostName": "", + "Description": "Hyper-V Virtual Switch Extension Adapter", + "ServiceName": "VMSMP", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 16, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 10, + "Caption": "[00000010] Hyper-V Virtual Ethernet Adapter", + "IPSubnet": [ + "255.255.240.0", + "64" + ], + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": [ + "172.17.9.6", + "fe80::6017:bb9f:9609:aa1" + ], + "IPEnabled": true, + "SettingID": "{76543B78-0150-491F-9CF7-5195A55FFDD5}", + "DHCPServer": "172.17.0.1", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "94:18:82:6E:16:2D", + "DHCPEnabled": true, + "DNSHostName": "VNVMHOSTSRV4", + "Description": "Hyper-V Virtual Ethernet Adapter", + "ServiceName": "VMSNPXYMP", + "WINSScopeID": "", + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 9, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "2020-11-26T20:09:55-08:00", + "DefaultIPGateway": [ + "172.17.0.1" + ], + "DHCPLeaseObtained": "2020-11-25T20:09:55-08:00", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 25, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": [], + "IPSecPermitUDPPorts": [], + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": [ + "8.8.8.8", + "208.67.222.222" + ], + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": [], + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": true, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": [], + "FullDNSRegistrationEnabled": true, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 11, + "Caption": "[00000011] Hyper-V Virtual Switch Extension Adapter", + "IPSubnet": null, + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": null, + "IPEnabled": false, + "SettingID": "{78CB2FB1-3139-40D7-A3F2-63B3A63943D6}", + "DHCPServer": "", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "", + "DHCPEnabled": false, + "DNSHostName": "", + "Description": "Hyper-V Virtual Switch Extension Adapter", + "ServiceName": "VMSMP", + "WINSScopeID": "", + "DatabasePath": "", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 11, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "0001-01-01T00:00:00Z", + "DefaultIPGateway": null, + "DHCPLeaseObtained": "0001-01-01T00:00:00Z", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 0, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": null, + "IPSecPermitUDPPorts": null, + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": null, + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": null, + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": false, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": null, + "FullDNSRegistrationEnabled": false, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ], + [ + { + "MTU": 0, + "Index": 12, + "Caption": "[00000012] Hyper-V Virtual Ethernet Adapter", + "IPSubnet": [ + "255.255.240.0", + "64" + ], + "DNSDomain": "", + "IGMPLevel": 0, + "IPAddress": [ + "172.17.9.122", + "fe80::f8a1:3a0c:e2c7:d174" + ], + "IPEnabled": true, + "SettingID": "{549B2925-331C-4865-9148-8D5AFCB1B054}", + "DHCPServer": "172.17.0.1", + "DefaultTOS": 0, + "DefaultTTL": 0, + "MACAddress": "94:18:82:6E:16:2C", + "DHCPEnabled": true, + "DNSHostName": "VNVMHOSTSRV4", + "Description": "Hyper-V Virtual Ethernet Adapter #2", + "ServiceName": "VMSNPXYMP", + "WINSScopeID": "", + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc", + "KeepAliveTime": 0, + "TcpWindowSize": 0, + "InterfaceIndex": 7, + "ArpUseEtherSNAP": false, + "DHCPLeaseExpires": "2020-11-26T20:09:55-08:00", + "DefaultIPGateway": [ + "172.17.0.1" + ], + "DHCPLeaseObtained": "2020-11-25T20:09:55-08:00", + "KeepAliveInterval": 0, + "NumForwardPackets": 0, + "TcpNumConnections": 0, + "WINSPrimaryServer": "", + "IPConnectionMetric": 25, + "IPUseZeroBroadcast": false, + "WINSHostLookupFile": "", + "DeadGWDetectEnabled": false, + "ForwardBufferMemory": 0, + "IPSecPermitTCPPorts": [], + "IPSecPermitUDPPorts": [], + "PMTUBHDetectEnabled": false, + "TcpipNetbiosOptions": 0, + "WINSSecondaryServer": "", + "ArpAlwaysSourceRoute": false, + "DNSServerSearchOrder": [ + "8.8.8.8", + "208.67.222.222" + ], + "PMTUDiscoveryEnabled": false, + "IPSecPermitIPProtocols": [], + "IPFilterSecurityEnabled": false, + "WINSEnableLMHostsLookup": true, + "TcpMaxDataRetransmissions": 0, + "DNSDomainSuffixSearchOrder": [], + "FullDNSRegistrationEnabled": true, + "TcpUseRFC1122UrgentPointer": false, + "DNSEnabledForWINSResolution": false, + "DomainDNSRegistrationEnabled": false, + "TcpMaxConnectRetransmissions": 0 + } + ] + ], + "desktop_monitor": [ + [ + { + "Name": "Default Monitor", + "Status": "OK", + "Caption": "Default Monitor", + "DeviceID": "DesktopMonitor1", + "IsLocked": false, + "Bandwidth": 0, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "Description": "Default Monitor", + "DisplayType": 0, + "InstallDate": "0001-01-01T00:00:00Z", + "MonitorType": "Default Monitor", + "PNPDeviceID": "", + "ScreenWidth": 0, + "Availability": 8, + "ErrorCleared": false, + "ScreenHeight": 0, + "LastErrorCode": 0, + "ErrorDescription": "", + "CreationClassName": "Win32_DesktopMonitor", + "MonitorManufacturer": "", + "PixelsPerXLogicalInch": 96, + "PixelsPerYLogicalInch": 96, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "Name": "Generic PnP Monitor", + "Status": "OK", + "Caption": "Generic PnP Monitor", + "DeviceID": "DesktopMonitor2", + "IsLocked": false, + "Bandwidth": 0, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "Description": "Generic PnP Monitor", + "DisplayType": 0, + "InstallDate": "0001-01-01T00:00:00Z", + "MonitorType": "Generic PnP Monitor", + "PNPDeviceID": "DISPLAY\\AQ_0000\\5&116A715B&0&UID0", + "ScreenWidth": 1024, + "Availability": 3, + "ErrorCleared": false, + "ScreenHeight": 768, + "LastErrorCode": 0, + "ErrorDescription": "", + "CreationClassName": "Win32_DesktopMonitor", + "MonitorManufacturer": "(Standard monitor types)", + "PixelsPerXLogicalInch": 96, + "PixelsPerYLogicalInch": 96, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ] + ], + "network_adapter": [ + [ + { + "GUID": "", + "Name": "Microsoft Kernel Debug Network Adapter", + "Index": 0, + "Speed": 0, + "Status": "", + "Caption": "[00000000] Microsoft Kernel Debug Network Adapter", + "DeviceID": "0", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "", + "Description": "Microsoft Kernel Debug Network Adapter", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "ROOT\\KDNIC\\0000", + "ProductName": "Microsoft Kernel Debug Network Adapter", + "ServiceName": "kdnic", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Microsoft", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 14, + "NetConnectionID": "", + "PhysicalAdapter": false, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{07BCD0FA-68CB-43DA-A95C-9F5761C070E4}", + "Name": "Broadcom NetXtreme Gigabit Ethernet", + "Index": 1, + "Speed": 9223372036854776000, + "Status": "", + "Caption": "[00000001] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "1", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "3C:A8:2A:E6:27:C9", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627C901", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 3, + "NetConnectionID": "Ethernet", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 7, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{EB82D1C8-1AE1-436F-92C9-CAAC93E72E95}", + "Name": "Broadcom NetXtreme Gigabit Ethernet", + "Index": 2, + "Speed": 0, + "Status": "", + "Caption": "[00000002] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "2", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "94:18:82:6E:16:2C", + "NetEnabled": true, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162C00", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 17, + "NetConnectionID": "Embedded LOM 1 Port 1", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 2, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{77E8A4E5-31CE-4618-A6A8-084310C616E2}", + "Name": "Broadcom NetXtreme Gigabit Ethernet #3", + "Index": 3, + "Speed": 9223372036854776000, + "Status": "", + "Caption": "[00000003] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "3", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "3C:A8:2A:E6:27:CA", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627CA02", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 10, + "NetConnectionID": "Ethernet 2", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 7, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{7128A11A-718E-430E-98C7-99DE8AE13020}", + "Name": "Broadcom NetXtreme Gigabit Ethernet #4", + "Index": 4, + "Speed": 1000000000, + "Status": "", + "Caption": "[00000004] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "4", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "3C:A8:2A:E6:27:C8", + "NetEnabled": true, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627C800", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 8, + "NetConnectionID": "HOST NIC", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 2, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{1A157B66-2890-4416-B43F-883CD05B8B0B}", + "Name": "Broadcom NetXtreme Gigabit Ethernet", + "Index": 5, + "Speed": 0, + "Status": "", + "Caption": "[00000005] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "5", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "94:18:82:6E:16:2D", + "NetEnabled": true, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162D01", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 4, + "NetConnectionID": "Embedded LOM 1 Port 2", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 2, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{C2DD58DC-8153-4492-8A4A-A3C6804BF2D7}", + "Name": "Broadcom NetXtreme Gigabit Ethernet #6", + "Index": 6, + "Speed": 9223372036854776000, + "Status": "", + "Caption": "[00000006] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "6", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "3C:A8:2A:E6:27:CB", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627CB03", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 13, + "NetConnectionID": "Ethernet 4", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 7, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{D670A8E4-1D5B-4E7C-90EF-7B95ED0F9A1B}", + "Name": "Broadcom NetXtreme Gigabit Ethernet #7", + "Index": 7, + "Speed": 9223372036854776000, + "Status": "", + "Caption": "[00000007] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "7", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "94:18:82:6E:16:2E", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162E02", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 15, + "NetConnectionID": "Embedded LOM 1 Port 3", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 7, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{3E4991C7-0C2C-42BB-A40F-133AAD11FB34}", + "Name": "Broadcom NetXtreme Gigabit Ethernet #8", + "Index": 8, + "Speed": 9223372036854776000, + "Status": "", + "Caption": "[00000008] Broadcom NetXtreme Gigabit Ethernet", + "DeviceID": "8", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "94:18:82:6E:16:2F", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Broadcom NetXtreme Gigabit Ethernet", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162F03", + "ProductName": "Broadcom NetXtreme Gigabit Ethernet", + "ServiceName": "b57nd60a", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Broadcom Corporation", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 6, + "NetConnectionID": "Embedded LOM 1 Port 4", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 7, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "", + "Name": "Hyper-V Virtual Switch Extension Adapter", + "Index": 9, + "Speed": 0, + "Status": "", + "Caption": "[00000009] Hyper-V Virtual Switch Extension Adapter", + "DeviceID": "9", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Hyper-V Virtual Switch Extension Adapter", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "ROOT\\VMS_VSMP\\0000", + "ProductName": "Hyper-V Virtual Switch Extension Adapter", + "ServiceName": "VMSMP", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Microsoft", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 16, + "NetConnectionID": "", + "PhysicalAdapter": false, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{76543B78-0150-491F-9CF7-5195A55FFDD5}", + "Name": "Hyper-V Virtual Ethernet Adapter", + "Index": 10, + "Speed": 1000000000, + "Status": "", + "Caption": "[00000010] Hyper-V Virtual Ethernet Adapter", + "DeviceID": "10", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "94:18:82:6E:16:2D", + "NetEnabled": true, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Hyper-V Virtual Ethernet Adapter", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "ROOT\\VMS_MP\\0000", + "ProductName": "Hyper-V Virtual Ethernet Adapter", + "ServiceName": "VMSNPXYMP", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Microsoft", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 9, + "NetConnectionID": "vEthernet (SHARED1)", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 2, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "", + "Name": "Hyper-V Virtual Switch Extension Adapter", + "Index": 11, + "Speed": 0, + "Status": "", + "Caption": "[00000011] Hyper-V Virtual Switch Extension Adapter", + "DeviceID": "11", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "", + "NetEnabled": false, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Hyper-V Virtual Switch Extension Adapter", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "ROOT\\VMS_VSMP\\0001", + "ProductName": "Hyper-V Virtual Switch Extension Adapter", + "ServiceName": "VMSMP", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Microsoft", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 11, + "NetConnectionID": "", + "PhysicalAdapter": false, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 0, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ], + [ + { + "GUID": "{549B2925-331C-4865-9148-8D5AFCB1B054}", + "Name": "Hyper-V Virtual Ethernet Adapter #2", + "Index": 12, + "Speed": 1000000000, + "Status": "", + "Caption": "[00000012] Hyper-V Virtual Ethernet Adapter", + "DeviceID": "12", + "MaxSpeed": 0, + "AutoSense": false, + "MACAddress": "94:18:82:6E:16:2C", + "NetEnabled": true, + "StatusInfo": 0, + "SystemName": "VNVMHOSTSRV4", + "AdapterType": "Ethernet 802.3", + "Description": "Hyper-V Virtual Ethernet Adapter", + "InstallDate": "0001-01-01T00:00:00Z", + "PNPDeviceID": "ROOT\\VMS_MP\\0001", + "ProductName": "Hyper-V Virtual Ethernet Adapter", + "ServiceName": "VMSNPXYMP", + "Availability": 3, + "ErrorCleared": false, + "Manufacturer": "Microsoft", + "AdapterTypeID": 0, + "LastErrorCode": 0, + "InterfaceIndex": 7, + "NetConnectionID": "vEthernet (SHARED2)", + "PhysicalAdapter": true, + "TimeOfLastReset": "2020-11-12T08:09:38.56763-08:00", + "ErrorDescription": "", + "NetworkAddresses": null, + "PermanentAddress": "", + "CreationClassName": "Win32_NetworkAdapter", + "MaxNumberControlled": 0, + "NetConnectionStatus": 2, + "ConfigManagerErrorCode": 0, + "ConfigManagerUserConfig": false, + "SystemCreationClassName": "Win32_ComputerSystem", + "PowerManagementSupported": false + } + ] + ] +} \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/wmi2.json b/api/tacticalrmm/tacticalrmm/test_data/wmi2.json new file mode 100644 index 0000000000..0de8f8b6d8 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/wmi2.json @@ -0,0 +1,3613 @@ +{ + "graphics": [ + [ + { + "Name": "NVIDIA Quadro P520", + "Status": "OK", + "Caption": "NVIDIA Quadro P520", + "DeviceID": "VideoController1", + "AdapterRAM": 2147483648, + "DriverDate": "2020-11-20T00:00:00Z", + "SystemName": "SERVER123456", + "Description": "NVIDIA Quadro P520", + "InstallDate": "0001-01-01T00:00:00Z", + "Availability": 3, + "DriverVersion": "27.21.14.5266", + "AdapterDACType": "Integrated RAMDAC", + "MaxRefreshRate": 0, + "MinRefreshRate": 0, + "VideoProcessor": "Quadro P520", + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "CurrentRefreshRate": 60, + "MaxMemorySupported": 0, + "AdapterCompatibility": "NVIDIA", + "VideoModeDescription": "2560 x 1440 x 4294967296 colors", + "CapabilityDescriptions": null, + "AcceleratorCapabilities": null, + "InstalledDisplayDrivers": "C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll", + "SystemCreationClassName": "Win32_ComputerSystem", + "CurrentVerticalResolution": 1440 + } + ] + ], + "os": [ + [ + { + "BootDevice": "\\Device\\HarddiskVolume4" + }, + { + "BuildNumber": "17763" + }, + { + "BuildType": "Multiprocessor Free" + }, + { + "Caption": "Microsoft Windows Server 2019 Standard" + }, + { + "CodeSet": "1252" + }, + { + "CountryCode": "1" + }, + { + "CreationClassName": "Win32_OperatingSystem" + }, + { + "CSCreationClassName": "Win32_ComputerSystem" + }, + { + "CSName": "SERVER123456" + }, + { + "CurrentTimeZone": -420 + }, + { + "DataExecutionPrevention_32BitApplications": true + }, + { + "DataExecutionPrevention_Available": true + }, + { + "DataExecutionPrevention_Drivers": true + }, + { + "DataExecutionPrevention_SupportPolicy": 3 + }, + { + "Debug": false + }, + { + "Description": "" + }, + { + "Distributed": false + }, + { + "EncryptionLevel": 256 + }, + { + "ForegroundApplicationBoost": 2 + }, + { + "FreePhysicalMemory": "101909320" + }, + { + "FreeSpaceInPagingFiles": "29360128" + }, + { + "FreeVirtualMemory": "131687780" + }, + { + "InstallDate": "20200221133221.000000-420" + }, + { + "LastBootUpTime": "20200221153223.097418-420" + }, + { + "LocalDateTime": "20200712234402.775000-420" + }, + { + "Locale": "0409" + }, + { + "Manufacturer": "Microsoft Corporation" + }, + { + "MaxNumberOfProcesses": -1 + }, + { + "MaxProcessMemorySize": "137438953344" + }, + { + "MUILanguages": [ + "en-US" + ] + }, + { + "Name": "Microsoft Windows Server 2019 Standard|C:\\Windows|\\Device\\Harddisk2\\Partition4" + }, + { + "NumberOfLicensedUsers": 0 + }, + { + "NumberOfProcesses": 133 + }, + { + "NumberOfUsers": 1 + }, + { + "OperatingSystemSKU": 7 + }, + { + "Organization": "" + }, + { + "OSArchitecture": "64-bit" + }, + { + "OSLanguage": 1033 + }, + { + "OSProductSuite": 272 + }, + { + "OSType": 18 + }, + { + "PortableOperatingSystem": false + }, + { + "Primary": true + }, + { + "ProductType": 3 + }, + { + "RegisteredUser": "Windows User" + }, + { + "SerialNumber": "123456" + }, + { + "ServicePackMajorVersion": 0 + }, + { + "ServicePackMinorVersion": 0 + }, + { + "SizeStoredInPagingFiles": "29360128" + }, + { + "Status": "OK" + }, + { + "SuiteMask": 272 + }, + { + "SystemDevice": "\\Device\\HarddiskVolume6" + }, + { + "SystemDirectory": "C:\\Windows\\system32" + }, + { + "SystemDrive": "C:" + }, + { + "TotalVirtualMemorySize": "230546044" + }, + { + "TotalVisibleMemorySize": "201185916" + }, + { + "Version": "10.0.17763" + }, + { + "WindowsDirectory": "C:\\Windows" + } + ] + ], + "cpu": [ + [ + { + "AddressWidth": 64 + }, + { + "Architecture": 9 + }, + { + "AssetTag": "UNKNOWN" + }, + { + "Availability": 3 + }, + { + "Caption": "Intel64 Family 6 Model 79 Stepping 1" + }, + { + "Characteristics": 252 + }, + { + "CpuStatus": 1 + }, + { + "CreationClassName": "Win32_Processor" + }, + { + "CurrentClockSpeed": 2098 + }, + { + "CurrentVoltage": 18 + }, + { + "DataWidth": 64 + }, + { + "Description": "Intel64 Family 6 Model 79 Stepping 1" + }, + { + "DeviceID": "CPU0" + }, + { + "ExtClock": 100 + }, + { + "Family": 179 + }, + { + "L2CacheSize": 2048 + }, + { + "L3CacheSize": 20480 + }, + { + "L3CacheSpeed": 0 + }, + { + "Level": 6 + }, + { + "LoadPercentage": 2 + }, + { + "Manufacturer": "GenuineIntel" + }, + { + "MaxClockSpeed": 2098 + }, + { + "Name": "Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz" + }, + { + "NumberOfCores": 8 + }, + { + "NumberOfEnabledCore": 8 + }, + { + "NumberOfLogicalProcessors": 16 + }, + { + "PartNumber": "" + }, + { + "PowerManagementSupported": false + }, + { + "ProcessorId": "BFEBFBFF000406F1" + }, + { + "ProcessorType": 3 + }, + { + "Revision": 20225 + }, + { + "Role": "CPU" + }, + { + "SecondLevelAddressTranslationExtensions": false + }, + { + "SerialNumber": "123456" + }, + { + "SocketDesignation": "Proc 1" + }, + { + "Status": "OK" + }, + { + "StatusInfo": 3 + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "ThreadCount": 16 + }, + { + "UpgradeMethod": 43 + }, + { + "Version": "" + }, + { + "VirtualizationFirmwareEnabled": false + }, + { + "VMMonitorModeExtensions": false + } + ], + [ + { + "AddressWidth": 64 + }, + { + "Architecture": 9 + }, + { + "AssetTag": "UNKNOWN" + }, + { + "Availability": 3 + }, + { + "Caption": "Intel64 Family 6 Model 79 Stepping 1" + }, + { + "Characteristics": 252 + }, + { + "CpuStatus": 1 + }, + { + "CreationClassName": "Win32_Processor" + }, + { + "CurrentClockSpeed": 2098 + }, + { + "CurrentVoltage": 18 + }, + { + "DataWidth": 64 + }, + { + "Description": "Intel64 Family 6 Model 79 Stepping 1" + }, + { + "DeviceID": "CPU1" + }, + { + "ExtClock": 100 + }, + { + "Family": 179 + }, + { + "L2CacheSize": 2048 + }, + { + "L3CacheSize": 20480 + }, + { + "L3CacheSpeed": 0 + }, + { + "Level": 6 + }, + { + "Manufacturer": "GenuineIntel" + }, + { + "MaxClockSpeed": 2098 + }, + { + "Name": "Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz" + }, + { + "NumberOfCores": 8 + }, + { + "NumberOfEnabledCore": 8 + }, + { + "NumberOfLogicalProcessors": 16 + }, + { + "PartNumber": "" + }, + { + "PowerManagementSupported": false + }, + { + "ProcessorId": "BFEBFBFF000406F1" + }, + { + "ProcessorType": 3 + }, + { + "Revision": 20225 + }, + { + "Role": "CPU" + }, + { + "SecondLevelAddressTranslationExtensions": false + }, + { + "SerialNumber": "123456" + }, + { + "SocketDesignation": "Proc 2" + }, + { + "Status": "OK" + }, + { + "StatusInfo": 3 + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "ThreadCount": 16 + }, + { + "UpgradeMethod": 43 + }, + { + "Version": "" + }, + { + "VirtualizationFirmwareEnabled": false + }, + { + "VMMonitorModeExtensions": false + } + ] + ], + "mem": [ + [ + { + "Attributes": 1 + }, + { + "BankLabel": "" + }, + { + "Capacity": "17179869184" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 1 DIMM 1" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "HP " + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "809082-091" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2400 + }, + { + "Tag": "Physical Memory 0" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 1 + }, + { + "BankLabel": "" + }, + { + "Capacity": "17179869184" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 1 DIMM 4" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "HP " + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "809082-091" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2400 + }, + { + "Tag": "Physical Memory 3" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 1 + }, + { + "BankLabel": "" + }, + { + "Capacity": "17179869184" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 1 DIMM 9" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "HP " + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "809082-091" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2400 + }, + { + "Tag": "Physical Memory 8" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 1 + }, + { + "BankLabel": "" + }, + { + "Capacity": "17179869184" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 1 DIMM 12" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "HP " + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "809082-091" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2400 + }, + { + "Tag": "Physical Memory 11" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 2 DIMM 1" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "UNKNOWN" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "NOT AVAILABLE" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2600 + }, + { + "Tag": "Physical Memory 12" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 2 DIMM 4" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "UNKNOWN" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "NOT AVAILABLE" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2600 + }, + { + "Tag": "Physical Memory 15" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 2 DIMM 9" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "UNKNOWN" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "NOT AVAILABLE" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2600 + }, + { + "Tag": "Physical Memory 20" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "PROC 2 DIMM 12" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "UNKNOWN" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "NOT AVAILABLE" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2600 + }, + { + "Tag": "Physical Memory 23" + }, + { + "TotalWidth": 72 + }, + { + "TypeDetail": 8320 + } + ] + ], + "usb": [ + [ + { + "Caption": "Standard Universal PCI to USB Host Controller" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "Standard Universal PCI to USB Host Controller" + }, + { + "DeviceID": "PCI\\VEN_103C&DEV_3300&SUBSYS_3381103C&REV_03\\4&2383FE5D&0&04E2" + }, + { + "Manufacturer": "(Standard USB Host Controller)" + }, + { + "Name": "Standard Universal PCI to USB Host Controller" + }, + { + "PNPDeviceID": "PCI\\VEN_103C&DEV_3300&SUBSYS_3381103C&REV_03\\4&2383FE5D&0&04E2" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + } + ], + [ + { + "Caption": "Standard Enhanced PCI to USB Host Controller" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "Standard Enhanced PCI to USB Host Controller" + }, + { + "DeviceID": "PCI\\VEN_8086&DEV_8D2D&SUBSYS_8030103C&REV_05\\3&11583659&0&D0" + }, + { + "Manufacturer": "(Standard USB Host Controller)" + }, + { + "Name": "Standard Enhanced PCI to USB Host Controller" + }, + { + "PNPDeviceID": "PCI\\VEN_8086&DEV_8D2D&SUBSYS_8030103C&REV_05\\3&11583659&0&D0" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + } + ], + [ + { + "Caption": "Standard Enhanced PCI to USB Host Controller" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "Standard Enhanced PCI to USB Host Controller" + }, + { + "DeviceID": "PCI\\VEN_8086&DEV_8D26&SUBSYS_8030103C&REV_05\\3&11583659&0&E8" + }, + { + "Manufacturer": "(Standard USB Host Controller)" + }, + { + "Name": "Standard Enhanced PCI to USB Host Controller" + }, + { + "PNPDeviceID": "PCI\\VEN_8086&DEV_8D26&SUBSYS_8030103C&REV_05\\3&11583659&0&E8" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + } + ], + [ + { + "Caption": "Intel(R) USB 3.0 eXtensible Host Controller - 1.0 (Microsoft)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "USB xHCI Compliant Host Controller" + }, + { + "DeviceID": "PCI\\VEN_8086&DEV_8D31&SUBSYS_8030103C&REV_05\\3&11583659&0&A0" + }, + { + "Manufacturer": "Generic USB xHCI Host Controller" + }, + { + "Name": "Intel(R) USB 3.0 eXtensible Host Controller - 1.0 (Microsoft)" + }, + { + "PNPDeviceID": "PCI\\VEN_8086&DEV_8D31&SUBSYS_8030103C&REV_05\\3&11583659&0&A0" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + } + ] + ], + "bios": [ + [ + { + "BiosCharacteristics": [ + 7, + 9, + 11, + 12, + 14, + 15, + 16, + 19, + 22, + 23, + 24, + 26, + 27, + 28, + 29, + 30, + 32, + 33, + 40, + 41, + 42, + 43 + ] + }, + { + "BIOSVersion": [ + "HP - 1", + "P89", + "HP - 21E00" + ] + }, + { + "Caption": "P89" + }, + { + "Description": "P89" + }, + { + "EmbeddedControllerMajorVersion": 2 + }, + { + "EmbeddedControllerMinorVersion": 50 + }, + { + "Manufacturer": "HP" + }, + { + "Name": "P89" + }, + { + "PrimaryBIOS": true + }, + { + "ReleaseDate": "20160913000000.000000+000" + }, + { + "SerialNumber": "123456" + }, + { + "SMBIOSBIOSVersion": "P89" + }, + { + "SMBIOSMajorVersion": 2 + }, + { + "SMBIOSMinorVersion": 8 + }, + { + "SMBIOSPresent": true + }, + { + "SoftwareElementID": "P89" + }, + { + "SoftwareElementState": 3 + }, + { + "Status": "OK" + }, + { + "SystemBiosMajorVersion": 2 + }, + { + "SystemBiosMinorVersion": 30 + }, + { + "TargetOperatingSystem": 0 + }, + { + "Version": "HP - 1" + } + ] + ], + "disk": [ + [ + { + "BytesPerSector": 512 + }, + { + "Capabilities": [ + 3, + 4 + ] + }, + { + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ] + }, + { + "Caption": "Microsoft Storage Space Device" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DiskDrive" + }, + { + "Description": "Disk drive" + }, + { + "DeviceID": "\\\\.\\PHYSICALDRIVE5" + }, + { + "FirmwareRevision": "0.1" + }, + { + "Index": 5 + }, + { + "InterfaceType": "SCSI" + }, + { + "Manufacturer": "(Standard disk drives)" + }, + { + "MediaLoaded": true + }, + { + "MediaType": "Fixed hard disk media" + }, + { + "Model": "Microsoft Storage Space Device" + }, + { + "Name": "\\\\.\\PHYSICALDRIVE5" + }, + { + "Partitions": 1 + }, + { + "PNPDeviceID": "STORAGE\\DISK\\{C905ECA4-6609-4829-8510-DB4D3AFD36E6}" + }, + { + "SCSIBus": 0 + }, + { + "SCSILogicalUnit": 1 + }, + { + "SCSIPort": 0 + }, + { + "SCSITargetId": 0 + }, + { + "SectorsPerTrack": 63 + }, + { + "SerialNumber": "123456" + }, + { + "Size": "3962100925440" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TotalCylinders": "481698" + }, + { + "TotalHeads": 255 + }, + { + "TotalSectors": "7738478370" + }, + { + "TotalTracks": "122832990" + }, + { + "TracksPerCylinder": 255 + } + ], + [ + { + "BytesPerSector": 512 + }, + { + "Capabilities": [ + 3, + 4 + ] + }, + { + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ] + }, + { + "Caption": "ATA CT500MX500SSD1 SCSI Disk Device" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DiskDrive" + }, + { + "Description": "Disk drive" + }, + { + "DeviceID": "\\\\.\\PHYSICALDRIVE3" + }, + { + "FirmwareRevision": "023 " + }, + { + "Index": 3 + }, + { + "InterfaceType": "SCSI" + }, + { + "Manufacturer": "(Standard disk drives)" + }, + { + "MediaLoaded": true + }, + { + "MediaType": "Fixed hard disk media" + }, + { + "Model": "ATA CT500MX500SSD1 SCSI Disk Device" + }, + { + "Name": "\\\\.\\PHYSICALDRIVE3" + }, + { + "Partitions": 0 + }, + { + "PNPDeviceID": "SCSI\\DISK&VEN_ATA&PROD_CT500MX500SSD1\\5&2F1553C3&0&000700" + }, + { + "SCSIBus": 0 + }, + { + "SCSILogicalUnit": 0 + }, + { + "SCSIPort": 0 + }, + { + "SCSITargetId": 7 + }, + { + "SectorsPerTrack": 63 + }, + { + "SerialNumber": "123456" + }, + { + "Size": "500105249280" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TotalCylinders": "60801" + }, + { + "TotalHeads": 255 + }, + { + "TotalSectors": "976768065" + }, + { + "TotalTracks": "15504255" + }, + { + "TracksPerCylinder": 255 + } + ], + [ + { + "BytesPerSector": 512 + }, + { + "Capabilities": [ + 3, + 4 + ] + }, + { + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ] + }, + { + "Caption": "ATA Crucial_CT500MX2 SCSI Disk Device" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DiskDrive" + }, + { + "Description": "Disk drive" + }, + { + "DeviceID": "\\\\.\\PHYSICALDRIVE2" + }, + { + "FirmwareRevision": "MU02" + }, + { + "Index": 2 + }, + { + "InterfaceType": "SCSI" + }, + { + "Manufacturer": "(Standard disk drives)" + }, + { + "MediaLoaded": true + }, + { + "MediaType": "Fixed hard disk media" + }, + { + "Model": "ATA Crucial_CT500MX2 SCSI Disk Device" + }, + { + "Name": "\\\\.\\PHYSICALDRIVE2" + }, + { + "Partitions": 3 + }, + { + "PNPDeviceID": "SCSI\\DISK&VEN_ATA&PROD_CRUCIAL_CT500MX2\\5&2F1553C3&0&000600" + }, + { + "SCSIBus": 0 + }, + { + "SCSILogicalUnit": 0 + }, + { + "SCSIPort": 0 + }, + { + "SCSITargetId": 6 + }, + { + "SectorsPerTrack": 63 + }, + { + "SerialNumber": "123456" + }, + { + "Size": "500105249280" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TotalCylinders": "60801" + }, + { + "TotalHeads": 255 + }, + { + "TotalSectors": "976768065" + }, + { + "TotalTracks": "15504255" + }, + { + "TracksPerCylinder": 255 + } + ] + ], + "comp_sys": [ + [ + { + "AdminPasswordStatus": 3 + }, + { + "AutomaticManagedPagefile": true + }, + { + "AutomaticResetBootOption": true + }, + { + "AutomaticResetCapability": true + }, + { + "BootROMSupported": true + }, + { + "BootStatus": [ + 0, + 0, + 0, + 196, + 15, + 194, + 0, + 0, + 0, + 0 + ] + }, + { + "BootupState": "Normal boot" + }, + { + "Caption": "SERVER123456" + }, + { + "ChassisBootupState": 3 + }, + { + "CreationClassName": "Win32_ComputerSystem" + }, + { + "CurrentTimeZone": -420 + }, + { + "DaylightInEffect": true + }, + { + "Description": "AT/AT COMPATIBLE" + }, + { + "DNSHostName": "SERVER123456" + }, + { + "Domain": "WORKGROUP" + }, + { + "DomainRole": 2 + }, + { + "EnableDaylightSavingsTime": true + }, + { + "FrontPanelResetStatus": 3 + }, + { + "HypervisorPresent": true + }, + { + "InfraredSupported": false + }, + { + "KeyboardPasswordStatus": 3 + }, + { + "Manufacturer": "HP" + }, + { + "Model": "ProLiant DL380 Gen9" + }, + { + "Name": "SERVER123456" + }, + { + "NetworkServerModeEnabled": true + }, + { + "NumberOfLogicalProcessors": 32 + }, + { + "NumberOfProcessors": 2 + }, + { + "OEMStringArray": [ + "PSF: ", + "Product ID: 859084-S01", + "OEM String: " + ] + }, + { + "PartOfDomain": false + }, + { + "PauseAfterReset": "-1" + }, + { + "PCSystemType": 4 + }, + { + "PCSystemTypeEx": 4 + }, + { + "PowerOnPasswordStatus": 3 + }, + { + "PowerState": 0 + }, + { + "PowerSupplyState": 3 + }, + { + "PrimaryOwnerName": "Windows User" + }, + { + "ResetCapability": 1 + }, + { + "ResetCount": -1 + }, + { + "ResetLimit": -1 + }, + { + "Roles": [ + "LM_Workstation", + "LM_Server", + "NT", + "Server_NT" + ] + }, + { + "Status": "OK" + }, + { + "SystemFamily": "ProLiant" + }, + { + "SystemSKUNumber": "859084-S01" + }, + { + "SystemType": "x64-based PC" + }, + { + "ThermalState": 3 + }, + { + "TotalPhysicalMemory": "206014377984" + }, + { + "WakeUpType": 6 + }, + { + "Workgroup": "WORKGROUP" + } + ] + ], + "base_board": [ + [ + { + "Caption": "Base Board" + }, + { + "CreationClassName": "Win32_BaseBoard" + }, + { + "Description": "Base Board" + }, + { + "HostingBoard": true + }, + { + "HotSwappable": false + }, + { + "Manufacturer": "HP" + }, + { + "Name": "Base Board" + }, + { + "PoweredOn": true + }, + { + "Product": "ProLiant DL380 Gen9" + }, + { + "Removable": true + }, + { + "Replaceable": true + }, + { + "RequiresDaughterBoard": false + }, + { + "SerialNumber": "123456" + }, + { + "Status": "OK" + }, + { + "Tag": "Base Board" + } + ] + ], + "comp_sys_prod": [ + [ + { + "Caption": "Computer System Product" + }, + { + "Description": "Computer System Product" + }, + { + "IdentifyingNumber": "123456" + }, + { + "Name": "ProLiant DL380 Gen9" + }, + { + "UUID": "123456" + }, + { + "Vendor": "HP" + }, + { + "Version": "" + } + ] + ], + "network_config": [ + [ + { + "Caption": "[00000000] Microsoft Kernel Debug Network Adapter" + }, + { + "Description": "Microsoft Kernel Debug Network Adapter" + }, + { + "DHCPEnabled": true + }, + { + "Index": 0 + }, + { + "InterfaceIndex": 11 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "kdnic" + }, + { + "SettingID": "{C8813F21-FC58-4EF5-B6DE-F10DF9AFF459}" + } + ], + [ + { + "Caption": "[00000001] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 1 + }, + { + "InterfaceIndex": 3 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "3C:A8:2A:E6:27:C9" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{07BCD0FA-68CB-43DA-A95C-9F5761C070E4}" + } + ], + [ + { + "Caption": "[00000002] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 2 + }, + { + "InterfaceIndex": 13 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "94:18:82:6E:16:2C" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{EB82D1C8-1AE1-436F-92C9-CAAC93E72E95}" + } + ], + [ + { + "Caption": "[00000003] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 3 + }, + { + "InterfaceIndex": 8 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "3C:A8:2A:E6:27:CA" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{77E8A4E5-31CE-4618-A6A8-084310C616E2}" + } + ], + [ + { + "Caption": "[00000004] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc" + }, + { + "DefaultIPGateway": [ + "172.17.0.1" + ] + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet #4" + }, + { + "DHCPEnabled": true + }, + { + "DHCPLeaseExpires": "20200713203019.000000-420" + }, + { + "DHCPLeaseObtained": "20200712203019.000000-420" + }, + { + "DHCPServer": "172.17.0.1" + }, + { + "DNSDomainSuffixSearchOrder": [] + }, + { + "DNSEnabledForWINSResolution": false + }, + { + "DNSHostName": "SERVER123456" + }, + { + "DNSServerSearchOrder": [ + "172.17.4.11", + "172.17.4.22", + "172.17.4.86" + ] + }, + { + "DomainDNSRegistrationEnabled": false + }, + { + "FullDNSRegistrationEnabled": true + }, + { + "GatewayCostMetric": [ + 0 + ] + }, + { + "Index": 4 + }, + { + "InterfaceIndex": 7 + }, + { + "IPAddress": [ + "172.17.9.121", + "fe80::45d7:5980:c39a:b6b2" + ] + }, + { + "IPConnectionMetric": 25 + }, + { + "IPEnabled": true + }, + { + "IPFilterSecurityEnabled": false + }, + { + "IPSecPermitIPProtocols": [] + }, + { + "IPSecPermitTCPPorts": [] + }, + { + "IPSecPermitUDPPorts": [] + }, + { + "IPSubnet": [ + "255.255.240.0", + "64" + ] + }, + { + "MACAddress": "3C:A8:2A:E6:27:C8" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{7128A11A-718E-430E-98C7-99DE8AE13020}" + }, + { + "TcpipNetbiosOptions": 0 + }, + { + "WINSEnableLMHostsLookup": true + }, + { + "WINSScopeID": "" + } + ], + [ + { + "Caption": "[00000005] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 5 + }, + { + "InterfaceIndex": 4 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "94:18:82:6E:16:2D" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{1A157B66-2890-4416-B43F-883CD05B8B0B}" + } + ], + [ + { + "Caption": "[00000006] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 6 + }, + { + "InterfaceIndex": 10 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "3C:A8:2A:E6:27:CB" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{C2DD58DC-8153-4492-8A4A-A3C6804BF2D7}" + } + ], + [ + { + "Caption": "[00000007] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 7 + }, + { + "InterfaceIndex": 12 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "94:18:82:6E:16:2E" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{D670A8E4-1D5B-4E7C-90EF-7B95ED0F9A1B}" + } + ], + [ + { + "Caption": "[00000008] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DHCPEnabled": true + }, + { + "Index": 8 + }, + { + "InterfaceIndex": 6 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "94:18:82:6E:16:2F" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SettingID": "{3E4991C7-0C2C-42BB-A40F-133AAD11FB34}" + } + ], + [ + { + "Caption": "[00000009] Hyper-V Virtual Switch Extension Adapter" + }, + { + "Description": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "DHCPEnabled": false + }, + { + "Index": 9 + }, + { + "InterfaceIndex": 38 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "VMSMP" + }, + { + "SettingID": "{DA1BE60D-9764-4A53-A05A-BAFD7D6D8622}" + } + ], + [ + { + "Caption": "[00000010] Hyper-V Virtual Ethernet Adapter" + }, + { + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc" + }, + { + "DefaultIPGateway": [ + "172.17.0.1" + ] + }, + { + "Description": "Hyper-V Virtual Ethernet Adapter" + }, + { + "DHCPEnabled": true + }, + { + "DHCPLeaseExpires": "20200713203024.000000-420" + }, + { + "DHCPLeaseObtained": "20200712203024.000000-420" + }, + { + "DHCPServer": "172.17.0.1" + }, + { + "DNSDomainSuffixSearchOrder": [] + }, + { + "DNSEnabledForWINSResolution": false + }, + { + "DNSHostName": "SERVER123456" + }, + { + "DNSServerSearchOrder": [ + "8.8.8.8", + "208.67.222.222" + ] + }, + { + "DomainDNSRegistrationEnabled": false + }, + { + "FullDNSRegistrationEnabled": true + }, + { + "GatewayCostMetric": [ + 0 + ] + }, + { + "Index": 10 + }, + { + "InterfaceIndex": 30 + }, + { + "IPAddress": [ + "172.17.9.6", + "fe80::6017:bb9f:9609:aa1" + ] + }, + { + "IPConnectionMetric": 25 + }, + { + "IPEnabled": true + }, + { + "IPFilterSecurityEnabled": false + }, + { + "IPSecPermitIPProtocols": [] + }, + { + "IPSecPermitTCPPorts": [] + }, + { + "IPSecPermitUDPPorts": [] + }, + { + "IPSubnet": [ + "255.255.240.0", + "64" + ] + }, + { + "MACAddress": "94:18:82:6E:16:2D" + }, + { + "ServiceName": "VMSNPXYMP" + }, + { + "SettingID": "{76543B78-0150-491F-9CF7-5195A55FFDD5}" + }, + { + "TcpipNetbiosOptions": 0 + }, + { + "WINSEnableLMHostsLookup": true + }, + { + "WINSScopeID": "" + } + ], + [ + { + "Caption": "[00000011] Hyper-V Virtual Switch Extension Adapter" + }, + { + "Description": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "DHCPEnabled": false + }, + { + "Index": 11 + }, + { + "InterfaceIndex": 42 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "VMSMP" + }, + { + "SettingID": "{78CB2FB1-3139-40D7-A3F2-63B3A63943D6}" + } + ], + [ + { + "Caption": "[00000012] Hyper-V Virtual Ethernet Adapter" + }, + { + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc" + }, + { + "DefaultIPGateway": [ + "172.17.0.1" + ] + }, + { + "Description": "Hyper-V Virtual Ethernet Adapter #2" + }, + { + "DHCPEnabled": true + }, + { + "DHCPLeaseExpires": "20200713203024.000000-420" + }, + { + "DHCPLeaseObtained": "20200712203024.000000-420" + }, + { + "DHCPServer": "172.17.0.1" + }, + { + "DNSDomainSuffixSearchOrder": [] + }, + { + "DNSEnabledForWINSResolution": false + }, + { + "DNSHostName": "SERVER123456" + }, + { + "DNSServerSearchOrder": [ + "8.8.8.8", + "208.67.222.222" + ] + }, + { + "DomainDNSRegistrationEnabled": false + }, + { + "FullDNSRegistrationEnabled": true + }, + { + "GatewayCostMetric": [ + 0 + ] + }, + { + "Index": 12 + }, + { + "InterfaceIndex": 27 + }, + { + "IPAddress": [ + "172.17.9.122", + "fe80::f8a1:3a0c:e2c7:d174" + ] + }, + { + "IPConnectionMetric": 25 + }, + { + "IPEnabled": true + }, + { + "IPFilterSecurityEnabled": false + }, + { + "IPSecPermitIPProtocols": [] + }, + { + "IPSecPermitTCPPorts": [] + }, + { + "IPSecPermitUDPPorts": [] + }, + { + "IPSubnet": [ + "255.255.240.0", + "64" + ] + }, + { + "MACAddress": "94:18:82:6E:16:2C" + }, + { + "ServiceName": "VMSNPXYMP" + }, + { + "SettingID": "{549B2925-331C-4865-9148-8D5AFCB1B054}" + }, + { + "TcpipNetbiosOptions": 0 + }, + { + "WINSEnableLMHostsLookup": true + }, + { + "WINSScopeID": "" + } + ] + ], + "desktop_monitor": [ + [ + { + "Availability": 8 + }, + { + "Caption": "Generic PnP Monitor" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DesktopMonitor" + }, + { + "Description": "Generic PnP Monitor" + }, + { + "DeviceID": "DesktopMonitor1" + }, + { + "MonitorManufacturer": "(Standard monitor types)" + }, + { + "MonitorType": "Generic PnP Monitor" + }, + { + "Name": "Generic PnP Monitor" + }, + { + "PixelsPerXLogicalInch": 96 + }, + { + "PixelsPerYLogicalInch": 96 + }, + { + "PNPDeviceID": "DISPLAY\\AQ_0000\\5&116A715B&0&UID0" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + } + ] + ], + "network_adapter": [ + [ + { + "Availability": 3 + }, + { + "Caption": "[00000000] Microsoft Kernel Debug Network Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Microsoft Kernel Debug Network Adapter" + }, + { + "DeviceID": "0" + }, + { + "Index": 0 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 11 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Microsoft Kernel Debug Network Adapter" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "ROOT\\KDNIC\\0000" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Microsoft Kernel Debug Network Adapter" + }, + { + "ServiceName": "kdnic" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000001] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "1" + }, + { + "GUID": "{07BCD0FA-68CB-43DA-A95C-9F5761C070E4}" + }, + { + "Index": 1 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 3 + }, + { + "MACAddress": "3C:A8:2A:E6:27:C9" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "NetConnectionID": "Ethernet" + }, + { + "NetConnectionStatus": 7 + }, + { + "NetEnabled": false + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627C901" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "Speed": "9223372036854775807" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000002] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "2" + }, + { + "GUID": "{EB82D1C8-1AE1-436F-92C9-CAAC93E72E95}" + }, + { + "Index": 2 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 13 + }, + { + "MACAddress": "94:18:82:6E:16:2C" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "NetConnectionID": "Embedded LOM 1 Port 1" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162C00" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000003] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "3" + }, + { + "GUID": "{77E8A4E5-31CE-4618-A6A8-084310C616E2}" + }, + { + "Index": 3 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 8 + }, + { + "MACAddress": "3C:A8:2A:E6:27:CA" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet #3" + }, + { + "NetConnectionID": "Ethernet 2" + }, + { + "NetConnectionStatus": 7 + }, + { + "NetEnabled": false + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627CA02" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "Speed": "9223372036854775807" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000004] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "4" + }, + { + "GUID": "{7128A11A-718E-430E-98C7-99DE8AE13020}" + }, + { + "Index": 4 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 7 + }, + { + "MACAddress": "3C:A8:2A:E6:27:C8" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet #4" + }, + { + "NetConnectionID": "HOST NIC" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627C800" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "Speed": "1000000000" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000005] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "5" + }, + { + "GUID": "{1A157B66-2890-4416-B43F-883CD05B8B0B}" + }, + { + "Index": 5 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 4 + }, + { + "MACAddress": "94:18:82:6E:16:2D" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "NetConnectionID": "Embedded LOM 1 Port 2" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162D01" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000006] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "6" + }, + { + "GUID": "{C2DD58DC-8153-4492-8A4A-A3C6804BF2D7}" + }, + { + "Index": 6 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 10 + }, + { + "MACAddress": "3C:A8:2A:E6:27:CB" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet #6" + }, + { + "NetConnectionID": "Ethernet 4" + }, + { + "NetConnectionStatus": 7 + }, + { + "NetEnabled": false + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_3383103C&REV_01\\00003CA82AE627CB03" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "Speed": "9223372036854775807" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000007] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "7" + }, + { + "GUID": "{D670A8E4-1D5B-4E7C-90EF-7B95ED0F9A1B}" + }, + { + "Index": 7 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 12 + }, + { + "MACAddress": "94:18:82:6E:16:2E" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet #7" + }, + { + "NetConnectionID": "Embedded LOM 1 Port 3" + }, + { + "NetConnectionStatus": 7 + }, + { + "NetEnabled": false + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162E02" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "Speed": "9223372036854775807" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000008] Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "DeviceID": "8" + }, + { + "GUID": "{3E4991C7-0C2C-42BB-A40F-133AAD11FB34}" + }, + { + "Index": 8 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 6 + }, + { + "MACAddress": "94:18:82:6E:16:2F" + }, + { + "Manufacturer": "Broadcom Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Broadcom NetXtreme Gigabit Ethernet #8" + }, + { + "NetConnectionID": "Embedded LOM 1 Port 4" + }, + { + "NetConnectionStatus": 7 + }, + { + "NetEnabled": false + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_14E4&DEV_1657&SUBSYS_22BE103C&REV_01\\00009418826E162F03" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Broadcom NetXtreme Gigabit Ethernet" + }, + { + "ServiceName": "b57nd60a" + }, + { + "Speed": "9223372036854775807" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000009] Hyper-V Virtual Switch Extension Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "DeviceID": "9" + }, + { + "Index": 9 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 38 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "ROOT\\VMS_VSMP\\0000" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "ServiceName": "VMSMP" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000010] Hyper-V Virtual Ethernet Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Hyper-V Virtual Ethernet Adapter" + }, + { + "DeviceID": "10" + }, + { + "GUID": "{76543B78-0150-491F-9CF7-5195A55FFDD5}" + }, + { + "Index": 10 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 30 + }, + { + "MACAddress": "94:18:82:6E:16:2D" + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Hyper-V Virtual Ethernet Adapter" + }, + { + "NetConnectionID": "vEthernet (SHARED1)" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "ROOT\\VMS_MP\\0000" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Hyper-V Virtual Ethernet Adapter" + }, + { + "ServiceName": "VMSNPXYMP" + }, + { + "Speed": "1000000000" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000011] Hyper-V Virtual Switch Extension Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "DeviceID": "11" + }, + { + "Index": 11 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 42 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "ROOT\\VMS_VSMP\\0001" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "ServiceName": "VMSMP" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000012] Hyper-V Virtual Ethernet Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Hyper-V Virtual Ethernet Adapter" + }, + { + "DeviceID": "12" + }, + { + "GUID": "{549B2925-331C-4865-9148-8D5AFCB1B054}" + }, + { + "Index": 12 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 27 + }, + { + "MACAddress": "94:18:82:6E:16:2C" + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Hyper-V Virtual Ethernet Adapter #2" + }, + { + "NetConnectionID": "vEthernet (SHARED2)" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "ROOT\\VMS_MP\\0001" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Hyper-V Virtual Ethernet Adapter" + }, + { + "ServiceName": "VMSNPXYMP" + }, + { + "Speed": "1000000000" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "SERVER123456" + }, + { + "TimeOfLastReset": "20200221153223.097418-420" + } + ] + ] +} \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/test_data/wmi3.json b/api/tacticalrmm/tacticalrmm/test_data/wmi3.json new file mode 100644 index 0000000000..02df1d018e --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/test_data/wmi3.json @@ -0,0 +1,3102 @@ +{ + "graphics": [ + [ + { + "Name": "NVIDIA Quadro P520", + "Status": "OK", + "Caption": "NVIDIA Quadro P520", + "DeviceID": "VideoController1", + "AdapterRAM": 2147483648, + "DriverDate": "2020-11-20T00:00:00Z", + "SystemName": "VMHOST-123456", + "Description": "NVIDIA Quadro P520", + "InstallDate": "0001-01-01T00:00:00Z", + "Availability": 3, + "DriverVersion": "27.21.14.5266", + "AdapterDACType": "Integrated RAMDAC", + "MaxRefreshRate": 0, + "MinRefreshRate": 0, + "VideoProcessor": "Quadro P520", + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "CurrentRefreshRate": 60, + "MaxMemorySupported": 0, + "AdapterCompatibility": "NVIDIA", + "VideoModeDescription": "2560 x 1440 x 4294967296 colors", + "CapabilityDescriptions": null, + "AcceleratorCapabilities": null, + "InstalledDisplayDrivers": "C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\nvlt.inf_amd64_3156bc34fe7846db\\nvldumdx.dll", + "SystemCreationClassName": "Win32_ComputerSystem", + "CurrentVerticalResolution": 1440 + } + ], + [ + { + "Name": "Intel(R) UHD Graphics 620", + "Status": "OK", + "Caption": "Intel(R) UHD Graphics 620", + "DeviceID": "VideoController2", + "AdapterRAM": 1073741824, + "DriverDate": "2020-10-28T00:00:00Z", + "SystemName": "VMHOST-123456", + "Description": "Intel(R) UHD Graphics 620", + "InstallDate": "0001-01-01T00:00:00Z", + "Availability": 3, + "DriverVersion": "27.20.100.8935", + "AdapterDACType": "Internal", + "MaxRefreshRate": 60, + "MinRefreshRate": 29, + "VideoProcessor": "Intel(R) UHD Graphics Family", + "TimeOfLastReset": "0001-01-01T00:00:00Z", + "CurrentRefreshRate": 59, + "MaxMemorySupported": 0, + "AdapterCompatibility": "Intel Corporation", + "VideoModeDescription": "3840 x 1600 x 4294967296 colors", + "CapabilityDescriptions": null, + "AcceleratorCapabilities": null, + "InstalledDisplayDrivers": "C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igdumdim64.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igd10iumd64.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igd10iumd64.dll,C:\\Windows\\System32\\DriverStore\\FileRepository\\iigd_dch.inf_amd64_997a69017605b77c\\igd12umd64.dll", + "SystemCreationClassName": "Win32_ComputerSystem", + "CurrentVerticalResolution": 1600 + } + ] + ], + "os": [ + [ + { + "BootDevice": "\\Device\\HarddiskVolume5" + }, + { + "BuildNumber": "17763" + }, + { + "BuildType": "Multiprocessor Free" + }, + { + "Caption": "Microsoft Windows Server 2019 Standard" + }, + { + "CodeSet": "1252" + }, + { + "CountryCode": "1" + }, + { + "CreationClassName": "Win32_OperatingSystem" + }, + { + "CSCreationClassName": "Win32_ComputerSystem" + }, + { + "CSName": "VMHOST-123456" + }, + { + "CurrentTimeZone": -420 + }, + { + "DataExecutionPrevention_32BitApplications": true + }, + { + "DataExecutionPrevention_Available": true + }, + { + "DataExecutionPrevention_Drivers": true + }, + { + "DataExecutionPrevention_SupportPolicy": 3 + }, + { + "Debug": false + }, + { + "Description": "" + }, + { + "Distributed": false + }, + { + "EncryptionLevel": 256 + }, + { + "ForegroundApplicationBoost": 2 + }, + { + "FreePhysicalMemory": "68727684" + }, + { + "FreeSpaceInPagingFiles": "19922944" + }, + { + "FreeVirtualMemory": "88989360" + }, + { + "InstallDate": "20200313074752.000000-420" + }, + { + "LastBootUpTime": "20200511200945.500000-420" + }, + { + "LocalDateTime": "20200712214050.120000-420" + }, + { + "Locale": "0409" + }, + { + "Manufacturer": "Microsoft Corporation" + }, + { + "MaxNumberOfProcesses": -1 + }, + { + "MaxProcessMemorySize": "137438953344" + }, + { + "MUILanguages": [ + "en-US" + ] + }, + { + "Name": "Microsoft Windows Server 2019 Standard|C:\\Windows|\\Device\\Harddisk1\\Partition4" + }, + { + "NumberOfLicensedUsers": 0 + }, + { + "NumberOfProcesses": 115 + }, + { + "NumberOfUsers": 0 + }, + { + "OperatingSystemSKU": 7 + }, + { + "Organization": "" + }, + { + "OSArchitecture": "64-bit" + }, + { + "OSLanguage": 1033 + }, + { + "OSProductSuite": 272 + }, + { + "OSType": 18 + }, + { + "PortableOperatingSystem": false + }, + { + "Primary": true + }, + { + "ProductType": 3 + }, + { + "RegisteredUser": "Windows User" + }, + { + "SerialNumber": "123456" + }, + { + "ServicePackMajorVersion": 0 + }, + { + "ServicePackMinorVersion": 0 + }, + { + "SizeStoredInPagingFiles": "19922944" + }, + { + "Status": "OK" + }, + { + "SuiteMask": 272 + }, + { + "SystemDevice": "\\Device\\HarddiskVolume7" + }, + { + "SystemDirectory": "C:\\Windows\\system32" + }, + { + "SystemDrive": "C:" + }, + { + "TotalVirtualMemorySize": "154088928" + }, + { + "TotalVisibleMemorySize": "134165984" + }, + { + "Version": "10.0.17763" + }, + { + "WindowsDirectory": "C:\\Windows" + } + ] + ], + "cpu": [ + [ + { + "AddressWidth": 64 + }, + { + "Architecture": 9 + }, + { + "AssetTag": "Unknown" + }, + { + "Availability": 3 + }, + { + "Caption": "AMD64 Family 23 Model 113 Stepping 0" + }, + { + "Characteristics": 252 + }, + { + "CpuStatus": 1 + }, + { + "CreationClassName": "Win32_Processor" + }, + { + "CurrentClockSpeed": 3793 + }, + { + "CurrentVoltage": 11 + }, + { + "DataWidth": 64 + }, + { + "Description": "AMD64 Family 23 Model 113 Stepping 0" + }, + { + "DeviceID": "CPU0" + }, + { + "ExtClock": 100 + }, + { + "Family": 107 + }, + { + "L2CacheSize": 6144 + }, + { + "L3CacheSize": 65536 + }, + { + "L3CacheSpeed": 0 + }, + { + "Level": 23 + }, + { + "LoadPercentage": 1 + }, + { + "Manufacturer": "AuthenticAMD" + }, + { + "MaxClockSpeed": 3793 + }, + { + "Name": "AMD Ryzen 9 3900X 12-Core Processor " + }, + { + "NumberOfCores": 12 + }, + { + "NumberOfEnabledCore": 12 + }, + { + "NumberOfLogicalProcessors": 24 + }, + { + "PartNumber": "Unknown" + }, + { + "PowerManagementSupported": false + }, + { + "ProcessorId": "178BFBFF00870F10" + }, + { + "ProcessorType": 3 + }, + { + "Revision": 28928 + }, + { + "Role": "CPU" + }, + { + "SecondLevelAddressTranslationExtensions": false + }, + { + "SerialNumber": "Unknown" + }, + { + "SocketDesignation": "AM4" + }, + { + "Status": "OK" + }, + { + "StatusInfo": 3 + }, + { + "Stepping": "0" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "ThreadCount": 24 + }, + { + "UpgradeMethod": 49 + }, + { + "Version": "Model 1, Stepping 0" + }, + { + "VirtualizationFirmwareEnabled": true + }, + { + "VMMonitorModeExtensions": false + } + ] + ], + "mem": [ + [ + { + "Attributes": 2 + }, + { + "BankLabel": "P0 CHANNEL A" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "DIMM 0" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "Unknown" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "CMK64GX4M2D3000C16" + }, + { + "SerialNumber": "00000000" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2133 + }, + { + "Tag": "Physical Memory 0" + }, + { + "TotalWidth": 64 + }, + { + "TypeDetail": 16512 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "P0 CHANNEL A" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "DIMM 1" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "Unknown" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "CMK64GX4M2D3000C16" + }, + { + "SerialNumber": "00000000" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2133 + }, + { + "Tag": "Physical Memory 1" + }, + { + "TotalWidth": 64 + }, + { + "TypeDetail": 16512 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "P0 CHANNEL B" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "DIMM 0" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "Unknown" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "CMK64GX4M2D3000C16" + }, + { + "SerialNumber": "00000000" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2133 + }, + { + "Tag": "Physical Memory 2" + }, + { + "TotalWidth": 64 + }, + { + "TypeDetail": 16512 + } + ], + [ + { + "Attributes": 2 + }, + { + "BankLabel": "P0 CHANNEL B" + }, + { + "Capacity": "34359738368" + }, + { + "Caption": "Physical Memory" + }, + { + "ConfiguredClockSpeed": 2133 + }, + { + "ConfiguredVoltage": 1200 + }, + { + "CreationClassName": "Win32_PhysicalMemory" + }, + { + "DataWidth": 64 + }, + { + "Description": "Physical Memory" + }, + { + "DeviceLocator": "DIMM 1" + }, + { + "FormFactor": 8 + }, + { + "Manufacturer": "Unknown" + }, + { + "MaxVoltage": 1200 + }, + { + "MemoryType": 0 + }, + { + "MinVoltage": 1200 + }, + { + "Name": "Physical Memory" + }, + { + "PartNumber": "CMK64GX4M2D3000C16" + }, + { + "SerialNumber": "00000000" + }, + { + "SMBIOSMemoryType": 26 + }, + { + "Speed": 2133 + }, + { + "Tag": "Physical Memory 3" + }, + { + "TotalWidth": 64 + }, + { + "TypeDetail": 16512 + } + ] + ], + "usb": [ + [ + { + "Caption": "AMD USB 3.10 eXtensible Host Controller - 1.10 (Microsoft)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "USB xHCI Compliant Host Controller" + }, + { + "DeviceID": "PCI\\VEN_1022&DEV_149C&SUBSYS_14861022&REV_00\\6&1EA5F98D&0&0140000A" + }, + { + "Manufacturer": "Generic USB xHCI Host Controller" + }, + { + "Name": "AMD USB 3.10 eXtensible Host Controller - 1.10 (Microsoft)" + }, + { + "PNPDeviceID": "PCI\\VEN_1022&DEV_149C&SUBSYS_14861022&REV_00\\6&1EA5F98D&0&0140000A" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + } + ], + [ + { + "Caption": "AMD USB 3.10 eXtensible Host Controller - 1.10 (Microsoft)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "USB xHCI Compliant Host Controller" + }, + { + "DeviceID": "PCI\\VEN_1022&DEV_149C&SUBSYS_50071458&REV_00\\4&1FDE7688&0&0341" + }, + { + "Manufacturer": "Generic USB xHCI Host Controller" + }, + { + "Name": "AMD USB 3.10 eXtensible Host Controller - 1.10 (Microsoft)" + }, + { + "PNPDeviceID": "PCI\\VEN_1022&DEV_149C&SUBSYS_50071458&REV_00\\4&1FDE7688&0&0341" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + } + ], + [ + { + "Caption": "AMD USB 3.10 eXtensible Host Controller - 1.10 (Microsoft)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_USBController" + }, + { + "Description": "USB xHCI Compliant Host Controller" + }, + { + "DeviceID": "PCI\\VEN_1022&DEV_149C&SUBSYS_148C1022&REV_00\\6&1EA5F98D&0&0340000A" + }, + { + "Manufacturer": "Generic USB xHCI Host Controller" + }, + { + "Name": "AMD USB 3.10 eXtensible Host Controller - 1.10 (Microsoft)" + }, + { + "PNPDeviceID": "PCI\\VEN_1022&DEV_149C&SUBSYS_148C1022&REV_00\\6&1EA5F98D&0&0340000A" + }, + { + "ProtocolSupported": 16 + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + } + ] + ], + "bios": [ + [ + { + "BiosCharacteristics": [ + 7, + 11, + 12, + 15, + 16, + 17, + 19, + 23, + 24, + 25, + 26, + 28, + 29, + 32, + 33, + 40, + 42, + 43 + ] + }, + { + "BIOSVersion": [ + "ALASKA - 1072009", + "F11", + "American Megatrends - 5000E" + ] + }, + { + "Caption": "F11" + }, + { + "CurrentLanguage": "en|US|iso8859-1" + }, + { + "Description": "F11" + }, + { + "EmbeddedControllerMajorVersion": 255 + }, + { + "EmbeddedControllerMinorVersion": 255 + }, + { + "InstallableLanguages": 15 + }, + { + "ListOfLanguages": [ + "en|US|iso8859-1", + "zh|TW|unicode", + "zh|CN|unicode", + "ru|RU|iso8859-5", + "de|DE|iso8859-1", + "ja|JP|unicode", + "ko|KR|unicode", + "es|ES|iso8859-1", + "fr|FR|iso8859-1", + "it|IT|iso8859-1", + "pt|PT|iso8859-1", + "vi|VI|iso8859-1", + "id|ID|iso8859-1", + "tr|TR|iso8859-1", + "pl|PL|iso8859-1" + ] + }, + { + "Manufacturer": "American Megatrends Inc." + }, + { + "Name": "F11" + }, + { + "PrimaryBIOS": true + }, + { + "ReleaseDate": "20191206000000.000000+000" + }, + { + "SerialNumber": "Default string" + }, + { + "SMBIOSBIOSVersion": "F11" + }, + { + "SMBIOSMajorVersion": 3 + }, + { + "SMBIOSMinorVersion": 2 + }, + { + "SMBIOSPresent": true + }, + { + "SoftwareElementID": "F11" + }, + { + "SoftwareElementState": 3 + }, + { + "Status": "OK" + }, + { + "SystemBiosMajorVersion": 5 + }, + { + "SystemBiosMinorVersion": 14 + }, + { + "TargetOperatingSystem": 0 + }, + { + "Version": "ALASKA - 1072009" + } + ] + ], + "disk": [ + [ + { + "BytesPerSector": 512 + }, + { + "Capabilities": [ + 3, + 4 + ] + }, + { + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing" + ] + }, + { + "Caption": "Microsoft Storage Space Device" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DiskDrive" + }, + { + "Description": "Disk drive" + }, + { + "DeviceID": "\\\\.\\PHYSICALDRIVE5" + }, + { + "FirmwareRevision": "0.1" + }, + { + "Index": 5 + }, + { + "InterfaceType": "SCSI" + }, + { + "Manufacturer": "(Standard disk drives)" + }, + { + "MediaLoaded": true + }, + { + "MediaType": "Fixed hard disk media" + }, + { + "Model": "Microsoft Storage Space Device" + }, + { + "Name": "\\\\.\\PHYSICALDRIVE5" + }, + { + "Partitions": 1 + }, + { + "PNPDeviceID": "STORAGE\\DISK\\{1AD24CEE-DC2A-4A4D-A3A3-FDD0A25BFE07}" + }, + { + "SCSIBus": 0 + }, + { + "SCSILogicalUnit": 1 + }, + { + "SCSIPort": 0 + }, + { + "SCSITargetId": 0 + }, + { + "SectorsPerTrack": 63 + }, + { + "SerialNumber": "123456" + }, + { + "Size": "2040107973120" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TotalCylinders": "248029" + }, + { + "TotalHeads": 255 + }, + { + "TotalSectors": "3984585885" + }, + { + "TotalTracks": "63247395" + }, + { + "TracksPerCylinder": 255 + } + ], + [ + { + "BytesPerSector": 512 + }, + { + "Capabilities": [ + 3, + 4, + 10 + ] + }, + { + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing", + "SMART Notification" + ] + }, + { + "Caption": "WDC WD60EFRX-68MYMN1" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DiskDrive" + }, + { + "Description": "Disk drive" + }, + { + "DeviceID": "\\\\.\\PHYSICALDRIVE0" + }, + { + "FirmwareRevision": "82.00A82" + }, + { + "Index": 0 + }, + { + "InterfaceType": "IDE" + }, + { + "Manufacturer": "(Standard disk drives)" + }, + { + "MediaLoaded": true + }, + { + "MediaType": "Fixed hard disk media" + }, + { + "Model": "WDC WD60EFRX-68MYMN1" + }, + { + "Name": "\\\\.\\PHYSICALDRIVE0" + }, + { + "Partitions": 1 + }, + { + "PNPDeviceID": "SCSI\\DISK&VEN_WDC&PROD_WD60EFRX-68MYMN1\\7&2E1550D9&0&000000" + }, + { + "SCSIBus": 0 + }, + { + "SCSILogicalUnit": 0 + }, + { + "SCSIPort": 1 + }, + { + "SCSITargetId": 0 + }, + { + "SectorsPerTrack": 63 + }, + { + "SerialNumber": "WD-123456" + }, + { + "Size": "6001172513280" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TotalCylinders": "729601" + }, + { + "TotalHeads": 255 + }, + { + "TotalSectors": "11721040065" + }, + { + "TotalTracks": "186048255" + }, + { + "TracksPerCylinder": 255 + } + ], + [ + { + "BytesPerSector": 512 + }, + { + "Capabilities": [ + 3, + 4, + 10 + ] + }, + { + "CapabilityDescriptions": [ + "Random Access", + "Supports Writing", + "SMART Notification" + ] + }, + { + "Caption": "Samsung SSD 860 EVO 500GB" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DiskDrive" + }, + { + "Description": "Disk drive" + }, + { + "DeviceID": "\\\\.\\PHYSICALDRIVE1" + }, + { + "FirmwareRevision": "RVT04B6Q" + }, + { + "Index": 1 + }, + { + "InterfaceType": "IDE" + }, + { + "Manufacturer": "(Standard disk drives)" + }, + { + "MediaLoaded": true + }, + { + "MediaType": "Fixed hard disk media" + }, + { + "Model": "Samsung SSD 860 EVO 500GB" + }, + { + "Name": "\\\\.\\PHYSICALDRIVE1" + }, + { + "Partitions": 3 + }, + { + "PNPDeviceID": "SCSI\\DISK&VEN_SAMSUNG&PROD_SSD_860_EVO_500G\\7&2E1550D9&0&010000" + }, + { + "SCSIBus": 1 + }, + { + "SCSILogicalUnit": 0 + }, + { + "SCSIPort": 1 + }, + { + "SCSITargetId": 0 + }, + { + "SectorsPerTrack": 63 + }, + { + "SerialNumber": "123456" + }, + { + "Size": "500105249280" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TotalCylinders": "60801" + }, + { + "TotalHeads": 255 + }, + { + "TotalSectors": "976768065" + }, + { + "TotalTracks": "15504255" + }, + { + "TracksPerCylinder": 255 + } + ] + ], + "comp_sys": [ + [ + { + "AdminPasswordStatus": 3 + }, + { + "AutomaticManagedPagefile": true + }, + { + "AutomaticResetBootOption": true + }, + { + "AutomaticResetCapability": true + }, + { + "BootROMSupported": true + }, + { + "BootStatus": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "BootupState": "Normal boot" + }, + { + "Caption": "VMHOST-123456" + }, + { + "ChassisBootupState": 3 + }, + { + "ChassisSKUNumber": "Default string" + }, + { + "CreationClassName": "Win32_ComputerSystem" + }, + { + "CurrentTimeZone": -420 + }, + { + "DaylightInEffect": true + }, + { + "Description": "AT/AT COMPATIBLE" + }, + { + "DNSHostName": "VMHOST-123456" + }, + { + "Domain": "WORKGROUP" + }, + { + "DomainRole": 2 + }, + { + "EnableDaylightSavingsTime": true + }, + { + "FrontPanelResetStatus": 3 + }, + { + "HypervisorPresent": true + }, + { + "InfraredSupported": false + }, + { + "KeyboardPasswordStatus": 3 + }, + { + "Manufacturer": "Gigabyte Technology Co., Ltd." + }, + { + "Model": "X570 AORUS ULTRA" + }, + { + "Name": "VMHOST-123456" + }, + { + "NetworkServerModeEnabled": true + }, + { + "NumberOfLogicalProcessors": 24 + }, + { + "NumberOfProcessors": 1 + }, + { + "OEMStringArray": [ + "Default string" + ] + }, + { + "PartOfDomain": false + }, + { + "PauseAfterReset": "-1" + }, + { + "PCSystemType": 1 + }, + { + "PCSystemTypeEx": 1 + }, + { + "PowerOnPasswordStatus": 3 + }, + { + "PowerState": 0 + }, + { + "PowerSupplyState": 3 + }, + { + "PrimaryOwnerName": "Windows User" + }, + { + "ResetCapability": 1 + }, + { + "ResetCount": -1 + }, + { + "ResetLimit": -1 + }, + { + "Roles": [ + "LM_Workstation", + "LM_Server", + "NT", + "Server_NT" + ] + }, + { + "Status": "OK" + }, + { + "SystemFamily": "Default string" + }, + { + "SystemSKUNumber": "Default string" + }, + { + "SystemType": "x64-based PC" + }, + { + "ThermalState": 3 + }, + { + "TotalPhysicalMemory": "137385967616" + }, + { + "WakeUpType": 6 + }, + { + "Workgroup": "WORKGROUP" + } + ] + ], + "base_board": [ + [ + { + "Caption": "Base Board" + }, + { + "ConfigOptions": [ + "Default string" + ] + }, + { + "CreationClassName": "Win32_BaseBoard" + }, + { + "Description": "Base Board" + }, + { + "HostingBoard": true + }, + { + "HotSwappable": false + }, + { + "Manufacturer": "Gigabyte Technology Co., Ltd." + }, + { + "Name": "Base Board" + }, + { + "PoweredOn": true + }, + { + "Product": "X570 AORUS ULTRA" + }, + { + "Removable": false + }, + { + "Replaceable": true + }, + { + "RequiresDaughterBoard": false + }, + { + "SerialNumber": "Default string" + }, + { + "Status": "OK" + }, + { + "Tag": "Base Board" + }, + { + "Version": "x.x" + } + ] + ], + "comp_sys_prod": [ + [ + { + "Caption": "Computer System Product" + }, + { + "Description": "Computer System Product" + }, + { + "IdentifyingNumber": "123456" + }, + { + "Name": "X570 AORUS ULTRA" + }, + { + "UUID": "123456" + }, + { + "Vendor": "Gigabyte Technology Co., Ltd." + }, + { + "Version": "-CF" + } + ] + ], + "network_config": [ + [ + { + "Caption": "[00000000] Microsoft Kernel Debug Network Adapter" + }, + { + "Description": "Microsoft Kernel Debug Network Adapter" + }, + { + "DHCPEnabled": true + }, + { + "Index": 0 + }, + { + "InterfaceIndex": 12 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "kdnic" + }, + { + "SettingID": "{7C68344E-1DD8-4B8C-A839-714591D8E25C}" + } + ], + [ + { + "Caption": "[00000001] Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "Description": "Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "DHCPEnabled": true + }, + { + "Index": 1 + }, + { + "InterfaceIndex": 4 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "6C:B3:11:1C:A9:EA" + }, + { + "ServiceName": "e1iexpress" + }, + { + "SettingID": "{29D28E82-B624-4A0D-B71B-2355B00E7D9B}" + } + ], + [ + { + "Caption": "[00000002] Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc" + }, + { + "DefaultIPGateway": [ + "192.168.10.1" + ] + }, + { + "Description": "Intel(R) Gigabit ET Dual Port Server Adapter #2" + }, + { + "DHCPEnabled": true + }, + { + "DHCPLeaseExpires": "20200713201124.000000-420" + }, + { + "DHCPLeaseObtained": "20200712201124.000000-420" + }, + { + "DHCPServer": "192.168.10.1" + }, + { + "DNSDomainSuffixSearchOrder": [] + }, + { + "DNSEnabledForWINSResolution": false + }, + { + "DNSHostName": "VMHOST-123456" + }, + { + "DNSServerSearchOrder": [ + "192.168.10.18", + "192.168.1.18" + ] + }, + { + "DomainDNSRegistrationEnabled": false + }, + { + "FullDNSRegistrationEnabled": true + }, + { + "GatewayCostMetric": [ + 0 + ] + }, + { + "Index": 2 + }, + { + "InterfaceIndex": 6 + }, + { + "IPAddress": [ + "192.168.10.149", + "fe80::f3:2cf2:4961:8d13" + ] + }, + { + "IPConnectionMetric": 25 + }, + { + "IPEnabled": true + }, + { + "IPFilterSecurityEnabled": false + }, + { + "IPSecPermitIPProtocols": [] + }, + { + "IPSecPermitTCPPorts": [] + }, + { + "IPSecPermitUDPPorts": [] + }, + { + "IPSubnet": [ + "255.255.255.0", + "64" + ] + }, + { + "MACAddress": "6C:B3:11:1C:A9:EB" + }, + { + "ServiceName": "e1iexpress" + }, + { + "SettingID": "{3F06220B-C096-4A4A-9573-FB344A2EC698}" + }, + { + "TcpipNetbiosOptions": 0 + }, + { + "WINSEnableLMHostsLookup": true + }, + { + "WINSScopeID": "" + } + ], + [ + { + "Caption": "[00000003] WAN Miniport (SSTP)" + }, + { + "Description": "WAN Miniport (SSTP)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 3 + }, + { + "InterfaceIndex": 9 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "RasSstp" + }, + { + "SettingID": "{6CA87764-F71A-42CC-B59A-32A28EBCBEF3}" + } + ], + [ + { + "Caption": "[00000004] WAN Miniport (IKEv2)" + }, + { + "Description": "WAN Miniport (IKEv2)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 4 + }, + { + "InterfaceIndex": 14 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "RasAgileVpn" + }, + { + "SettingID": "{94FED5A7-D831-4166-BB67-00B982CE2178}" + } + ], + [ + { + "Caption": "[00000005] WAN Miniport (L2TP)" + }, + { + "Description": "WAN Miniport (L2TP)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 5 + }, + { + "InterfaceIndex": 10 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "Rasl2tp" + }, + { + "SettingID": "{76D5C158-4596-4B39-9C92-5B5F06F440BC}" + } + ], + [ + { + "Caption": "[00000006] WAN Miniport (PPTP)" + }, + { + "Description": "WAN Miniport (PPTP)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 6 + }, + { + "InterfaceIndex": 8 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "PptpMiniport" + }, + { + "SettingID": "{6B96FDEA-4293-4B11-87FF-3EBECD19D8F7}" + } + ], + [ + { + "Caption": "[00000007] WAN Miniport (PPPOE)" + }, + { + "Description": "WAN Miniport (PPPOE)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 7 + }, + { + "InterfaceIndex": 2 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "RasPppoe" + }, + { + "SettingID": "{03839347-0867-4E47-8C29-8241418B3E5D}" + } + ], + [ + { + "Caption": "[00000008] WAN Miniport (GRE)" + }, + { + "Description": "WAN Miniport (GRE)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 8 + }, + { + "InterfaceIndex": 7 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "RasGre" + }, + { + "SettingID": "{5C698A0B-6D28-4F36-9312-5AE8254526F0}" + } + ], + [ + { + "Caption": "[00000009] WAN Miniport (IP)" + }, + { + "Description": "WAN Miniport (IP)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 9 + }, + { + "InterfaceIndex": 18 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "42:9F:20:52:41:53" + }, + { + "ServiceName": "NdisWan" + }, + { + "SettingID": "{EDC2CD2D-21AE-4288-9515-B291533CA32E}" + } + ], + [ + { + "Caption": "[00000010] WAN Miniport (IPv6)" + }, + { + "Description": "WAN Miniport (IPv6)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 10 + }, + { + "InterfaceIndex": 16 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "42:9F:20:52:41:53" + }, + { + "ServiceName": "NdisWan" + }, + { + "SettingID": "{B577201A-EA0A-4415-9113-D986CADB1F4B}" + } + ], + [ + { + "Caption": "[00000011] WAN Miniport (Network Monitor)" + }, + { + "Description": "WAN Miniport (Network Monitor)" + }, + { + "DHCPEnabled": false + }, + { + "Index": 11 + }, + { + "InterfaceIndex": 11 + }, + { + "IPEnabled": false + }, + { + "MACAddress": "44:02:20:52:41:53" + }, + { + "ServiceName": "NdisWan" + }, + { + "SettingID": "{7885E3B5-1002-4B2C-AF70-9FCE946CCF8C}" + } + ], + [ + { + "Caption": "[00000012] Hyper-V Virtual Switch Extension Adapter" + }, + { + "Description": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "DHCPEnabled": false + }, + { + "Index": 12 + }, + { + "InterfaceIndex": 17 + }, + { + "IPEnabled": false + }, + { + "ServiceName": "VMSMP" + }, + { + "SettingID": "{E9114E10-1F94-4A24-A129-2D07B40CB70A}" + } + ], + [ + { + "Caption": "[00000013] Hyper-V Virtual Ethernet Adapter" + }, + { + "DatabasePath": "%SystemRoot%\\System32\\drivers\\etc" + }, + { + "DefaultIPGateway": [ + "192.168.10.1" + ] + }, + { + "Description": "Hyper-V Virtual Ethernet Adapter" + }, + { + "DHCPEnabled": true + }, + { + "DHCPLeaseExpires": "20200713201124.000000-420" + }, + { + "DHCPLeaseObtained": "20200712201124.000000-420" + }, + { + "DHCPServer": "192.168.10.1" + }, + { + "DNSDomainSuffixSearchOrder": [] + }, + { + "DNSEnabledForWINSResolution": false + }, + { + "DNSHostName": "VMHOST-123456" + }, + { + "DNSServerSearchOrder": [ + "8.8.8.8", + "208.67.222.222" + ] + }, + { + "DomainDNSRegistrationEnabled": false + }, + { + "FullDNSRegistrationEnabled": true + }, + { + "GatewayCostMetric": [ + 0 + ] + }, + { + "Index": 13 + }, + { + "InterfaceIndex": 15 + }, + { + "IPAddress": [ + "192.168.10.114", + "fe80::c5fd:fc78:8f5a:d037" + ] + }, + { + "IPConnectionMetric": 25 + }, + { + "IPEnabled": true + }, + { + "IPFilterSecurityEnabled": false + }, + { + "IPSecPermitIPProtocols": [] + }, + { + "IPSecPermitTCPPorts": [] + }, + { + "IPSecPermitUDPPorts": [] + }, + { + "IPSubnet": [ + "255.255.255.0", + "64" + ] + }, + { + "MACAddress": "6C:B3:11:1C:A9:EA" + }, + { + "ServiceName": "VMSNPXYMP" + }, + { + "SettingID": "{965FB0D4-E52D-4131-8DA3-57EDBF70EBF7}" + }, + { + "TcpipNetbiosOptions": 0 + }, + { + "WINSEnableLMHostsLookup": true + }, + { + "WINSScopeID": "" + } + ] + ], + "desktop_monitor": [ + [ + { + "Availability": 8 + }, + { + "Caption": "Generic Non-PnP Monitor" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_DesktopMonitor" + }, + { + "Description": "Generic Non-PnP Monitor" + }, + { + "DeviceID": "DesktopMonitor1" + }, + { + "MonitorManufacturer": "(Standard monitor types)" + }, + { + "MonitorType": "Generic Non-PnP Monitor" + }, + { + "Name": "Generic Non-PnP Monitor" + }, + { + "PixelsPerXLogicalInch": 96 + }, + { + "PixelsPerYLogicalInch": 96 + }, + { + "PNPDeviceID": "DISPLAY\\DEFAULT_MONITOR\\7&2BA76299&0&UID0" + }, + { + "Status": "OK" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + } + ] + ], + "network_adapter": [ + [ + { + "Availability": 3 + }, + { + "Caption": "[00000000] Microsoft Kernel Debug Network Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Microsoft Kernel Debug Network Adapter" + }, + { + "DeviceID": "0" + }, + { + "Index": 0 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 12 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Microsoft Kernel Debug Network Adapter" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "ROOT\\KDNIC\\0000" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Microsoft Kernel Debug Network Adapter" + }, + { + "ServiceName": "kdnic" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000001] Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "DeviceID": "1" + }, + { + "GUID": "{29D28E82-B624-4A0D-B71B-2355B00E7D9B}" + }, + { + "Index": 1 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 4 + }, + { + "MACAddress": "6C:B3:11:1C:A9:EA" + }, + { + "Manufacturer": "Intel Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "NetConnectionID": "Ethernet" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_8086&DEV_10C9&SUBSYS_A03C8086&REV_01\\6CB311FFFF1CA9EA00" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "ServiceName": "e1iexpress" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000002] Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "DeviceID": "2" + }, + { + "GUID": "{3F06220B-C096-4A4A-9573-FB344A2EC698}" + }, + { + "Index": 2 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 6 + }, + { + "MACAddress": "6C:B3:11:1C:A9:EB" + }, + { + "Manufacturer": "Intel Corporation" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Intel(R) Gigabit ET Dual Port Server Adapter #2" + }, + { + "NetConnectionID": "Ethernet 2" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "PCI\\VEN_8086&DEV_10C9&SUBSYS_A03C8086&REV_01\\6CB311FFFF1CA9EA01" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Intel(R) Gigabit ET Dual Port Server Adapter" + }, + { + "ServiceName": "e1iexpress" + }, + { + "Speed": "1000000000" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "Availability": 3 + }, + { + "Caption": "[00000003] WAN Miniport (SSTP)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (SSTP)" + }, + { + "DeviceID": "3" + }, + { + "Index": 3 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 9 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (SSTP)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_SSTPMINIPORT" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (SSTP)" + }, + { + "ServiceName": "RasSstp" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "Availability": 3 + }, + { + "Caption": "[00000004] WAN Miniport (IKEv2)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (IKEv2)" + }, + { + "DeviceID": "4" + }, + { + "Index": 4 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 14 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (IKEv2)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_AGILEVPNMINIPORT" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (IKEv2)" + }, + { + "ServiceName": "RasAgileVpn" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "Availability": 3 + }, + { + "Caption": "[00000005] WAN Miniport (L2TP)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (L2TP)" + }, + { + "DeviceID": "5" + }, + { + "Index": 5 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 10 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (L2TP)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_L2TPMINIPORT" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (L2TP)" + }, + { + "ServiceName": "Rasl2tp" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "Availability": 3 + }, + { + "Caption": "[00000006] WAN Miniport (PPTP)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (PPTP)" + }, + { + "DeviceID": "6" + }, + { + "Index": 6 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 8 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (PPTP)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_PPTPMINIPORT" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (PPTP)" + }, + { + "ServiceName": "PptpMiniport" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "Availability": 3 + }, + { + "Caption": "[00000007] WAN Miniport (PPPOE)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (PPPOE)" + }, + { + "DeviceID": "7" + }, + { + "Index": 7 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 2 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (PPPOE)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_PPPOEMINIPORT" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (PPPOE)" + }, + { + "ServiceName": "RasPppoe" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "Availability": 3 + }, + { + "Caption": "[00000008] WAN Miniport (GRE)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (GRE)" + }, + { + "DeviceID": "8" + }, + { + "Index": 8 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 7 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (GRE)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_GREMINIPORT" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (GRE)" + }, + { + "ServiceName": "RasGre" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000009] WAN Miniport (IP)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (IP)" + }, + { + "DeviceID": "9" + }, + { + "Index": 9 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 18 + }, + { + "MACAddress": "42:9F:20:52:41:53" + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (IP)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_NDISWANIP" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (IP)" + }, + { + "ServiceName": "NdisWan" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000010] WAN Miniport (IPv6)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (IPv6)" + }, + { + "DeviceID": "10" + }, + { + "Index": 10 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 16 + }, + { + "MACAddress": "42:9F:20:52:41:53" + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (IPv6)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_NDISWANIPV6" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (IPv6)" + }, + { + "ServiceName": "NdisWan" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000011] WAN Miniport (Network Monitor)" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "WAN Miniport (Network Monitor)" + }, + { + "DeviceID": "11" + }, + { + "Index": 11 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 11 + }, + { + "MACAddress": "44:02:20:52:41:53" + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "WAN Miniport (Network Monitor)" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "SWD\\MSRRAS\\MS_NDISWANBH" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "WAN Miniport (Network Monitor)" + }, + { + "ServiceName": "NdisWan" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000012] Hyper-V Virtual Switch Extension Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "DeviceID": "12" + }, + { + "Index": 12 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 17 + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "PhysicalAdapter": false + }, + { + "PNPDeviceID": "ROOT\\VMS_VSMP\\0000" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Hyper-V Virtual Switch Extension Adapter" + }, + { + "ServiceName": "VMSMP" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ], + [ + { + "AdapterType": "Ethernet 802.3" + }, + { + "AdapterTypeId": 0 + }, + { + "Availability": 3 + }, + { + "Caption": "[00000013] Hyper-V Virtual Ethernet Adapter" + }, + { + "ConfigManagerErrorCode": 0 + }, + { + "ConfigManagerUserConfig": false + }, + { + "CreationClassName": "Win32_NetworkAdapter" + }, + { + "Description": "Hyper-V Virtual Ethernet Adapter" + }, + { + "DeviceID": "13" + }, + { + "GUID": "{965FB0D4-E52D-4131-8DA3-57EDBF70EBF7}" + }, + { + "Index": 13 + }, + { + "Installed": true + }, + { + "InterfaceIndex": 15 + }, + { + "MACAddress": "6C:B3:11:1C:A9:EA" + }, + { + "Manufacturer": "Microsoft" + }, + { + "MaxNumberControlled": 0 + }, + { + "Name": "Hyper-V Virtual Ethernet Adapter" + }, + { + "NetConnectionID": "vEthernet (shared1)" + }, + { + "NetConnectionStatus": 2 + }, + { + "NetEnabled": true + }, + { + "PhysicalAdapter": true + }, + { + "PNPDeviceID": "ROOT\\VMS_MP\\0000" + }, + { + "PowerManagementSupported": false + }, + { + "ProductName": "Hyper-V Virtual Ethernet Adapter" + }, + { + "ServiceName": "VMSNPXYMP" + }, + { + "Speed": "1000000000" + }, + { + "SystemCreationClassName": "Win32_ComputerSystem" + }, + { + "SystemName": "VMHOST-123456" + }, + { + "TimeOfLastReset": "20200511200945.500000-420" + } + ] + ] +} \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/tests.py b/api/tacticalrmm/tacticalrmm/tests.py index 91ec8cf3dd..9497589ffe 100644 --- a/api/tacticalrmm/tacticalrmm/tests.py +++ b/api/tacticalrmm/tacticalrmm/tests.py @@ -2,15 +2,19 @@ import requests from django.test import override_settings -from tacticalrmm.test import TacticalTestCase -from .utils import ( - bitdays_to_string, - generate_winagent_exe, - get_bit_days, - reload_nats, +from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER +from tacticalrmm.constants import ( AGENT_DEFER, + CHECKS_NON_EDITABLE_FIELDS, + FIELDS_TRIGGER_TASK_UPDATE_AGENT, + ONLINE_AGENTS, + POLICY_CHECK_FIELDS_TO_COPY, + POLICY_TASK_FIELDS_TO_COPY, ) +from tacticalrmm.test import TacticalTestCase + +from .utils import bitdays_to_string, generate_winagent_exe, get_bit_days, reload_nats class TestUtils(TacticalTestCase): @@ -27,7 +31,7 @@ def test_generate_winagent_exe_success(self, m_open, mock_post): rdp=1, ping=0, power=0, - arch="64", + goarch="amd64", token="abc123", api="https://api.example.com", file_name="rmm-client-site-server.exe", @@ -45,7 +49,7 @@ def test_generate_winagent_exe_timeout(self, mock_post): rdp=1, ping=0, power=0, - arch="64", + goarch="amd64", token="abc123", api="https://api.example.com", file_name="rmm-client-site-server.exe", @@ -94,10 +98,39 @@ def test_bitdays_to_string(self): r = bitdays_to_string(bit_weekdays) self.assertEqual(r, "Every day") - def test_defer_fields_exist(self): + # for checking when removing db fields, make sure we update these tuples + def test_constants_fields_exist(self) -> None: from agents.models import Agent + from autotasks.models import AutomatedTask + from checks.models import Check, CheckResult - fields = [i.name for i in Agent._meta.get_fields()] + agent_fields = [i.name for i in Agent._meta.get_fields()] + agent_fields.append("pk") + + autotask_fields = [i.name for i in AutomatedTask._meta.get_fields()] + check_fields = [i.name for i in Check._meta.get_fields()] + check_result_fields = [i.name for i in CheckResult._meta.get_fields()] for i in AGENT_DEFER: - self.assertIn(i, fields) + self.assertIn(i, agent_fields) + + for i in ONLINE_AGENTS: + self.assertIn(i, agent_fields) + + for i in FIELDS_TRIGGER_TASK_UPDATE_AGENT: + self.assertIn(i, autotask_fields) + + for i in POLICY_TASK_FIELDS_TO_COPY: + self.assertIn(i, autotask_fields) + + for i in CHECKS_NON_EDITABLE_FIELDS: + self.assertIn(i, check_fields) + + for i in POLICY_CHECK_FIELDS_TO_COPY: + self.assertIn(i, check_fields) + + for i in CHECK_DEFER: + self.assertIn(i, check_fields) + + for i in CHECK_RESULT_DEFER: + self.assertIn(i, check_result_fields) diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index 71a93dd643..9d74598681 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -3,6 +3,7 @@ from knox import views as knox_views from accounts.views import CheckCreds, LoginView +from agents.consumers import SendCMD from core.consumers import DashInfo @@ -39,12 +40,15 @@ def to_url(self, value): path("accounts/", include("accounts.urls")), ] -if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED: +if getattr(settings, "ADMIN_ENABLED", False): from django.contrib import admin urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),) -if hasattr(settings, "SWAGGER_ENABLED") and settings.SWAGGER_ENABLED: +if getattr(settings, "DEBUG", False) and not getattr(settings, "DEMO", False): + urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))] + +if getattr(settings, "SWAGGER_ENABLED", False): from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns += ( @@ -57,5 +61,6 @@ def to_url(self, value): ) ws_urlpatterns = [ - path("ws/dashinfo/", DashInfo.as_asgi()), # type: ignore + path("ws/dashinfo/", DashInfo.as_asgi()), + path("ws/sendcmd/", SendCMD.as_asgi()), ] diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index e57b533af2..59f0b2ed2e 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -3,65 +3,67 @@ import subprocess import tempfile import time -from typing import Optional, Union +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from functools import wraps +from typing import List, Optional, Union +from zoneinfo import ZoneInfo -import pytz import requests from channels.auth import AuthMiddlewareStack from channels.db import database_sync_to_async from django.conf import settings from django.contrib.auth.models import AnonymousUser +from django.core.cache import cache +from django.db import connection from django.http import FileResponse from knox.auth import TokenAuthentication -from rest_framework import status from rest_framework.response import Response -from core.models import CodeSignToken -from logs.models import DebugLog from agents.models import Agent - -notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) - -AGENT_DEFER = ["wmi_detail", "services"] - -WEEK_DAYS = { - "Sunday": 0x1, - "Monday": 0x2, - "Tuesday": 0x4, - "Wednesday": 0x8, - "Thursday": 0x10, - "Friday": 0x20, - "Saturday": 0x40, -} +from core.utils import get_core_settings, token_is_valid +from logs.models import DebugLog +from tacticalrmm.constants import ( + MONTH_DAYS, + MONTHS, + REDIS_LOCK_EXPIRE, + WEEK_DAYS, + WEEKS, + AgentPlat, + CustomFieldType, + DebugLogType, + ScriptShell, +) +from tacticalrmm.helpers import ( + get_certs, + get_nats_internal_protocol, + get_nats_ports, + notify_error, +) def generate_winagent_exe( + *, client: int, site: int, agent_type: str, rdp: int, ping: int, power: int, - arch: str, + goarch: str, token: str, api: str, file_name: str, ) -> Union[Response, FileResponse]: - - from agents.utils import get_winagent_url + from agents.utils import get_agent_url inno = ( - f"winagent-v{settings.LATEST_AGENT_VER}.exe" - if arch == "64" - else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe" + f"tacticalagent-v{settings.LATEST_AGENT_VER}-{AgentPlat.WINDOWS}-{goarch}.exe" ) - dl_url = get_winagent_url(arch) + codetoken, _ = token_is_valid() - try: - codetoken = CodeSignToken.objects.first().token # type:ignore - except: - codetoken = "" + dl_url = get_agent_url(goarch=goarch, plat=AgentPlat.WINDOWS, token=codetoken) data = { "client": client, @@ -70,7 +72,7 @@ def generate_winagent_exe( "rdp": str(rdp), "ping": str(ping), "power": str(power), - "goarch": "amd64" if arch == "64" else "386", + "goarch": goarch, "token": token, "inno": inno, "url": dl_url, @@ -79,31 +81,23 @@ def generate_winagent_exe( } headers = {"Content-type": "application/json"} - errors = [] with tempfile.NamedTemporaryFile() as fp: - for url in settings.EXE_GEN_URLS: - try: - r = requests.post( - f"{url}/api/v1/exe", - json=data, - headers=headers, - stream=True, - timeout=900, - ) - except Exception as e: - errors.append(str(e)) - else: - errors = [] - break - - if errors: - DebugLog.error(message=errors) + try: + r = requests.post( + settings.EXE_GEN_URL, + json=data, + headers=headers, + stream=True, + timeout=900, + ) + except Exception as e: + DebugLog.error(message=str(e)) return notify_error( "Something went wrong. Check debug error log for exact error message" ) with open(fp.name, "wb") as f: - for chunk in r.iter_content(chunk_size=1024): # type: ignore + for chunk in r.iter_content(chunk_size=1024): if chunk: f.write(chunk) del r @@ -111,75 +105,133 @@ def generate_winagent_exe( def get_default_timezone(): - from core.models import CoreSettings - - return pytz.timezone(CoreSettings.objects.first().default_time_zone) # type:ignore + return ZoneInfo(get_core_settings().default_time_zone) def get_bit_days(days: list[str]) -> int: bit_days = 0 for day in days: - bit_days |= WEEK_DAYS.get(day) # type: ignore + bit_days |= WEEK_DAYS.get(day, 0) return bit_days def bitdays_to_string(day: int) -> str: - ret = [] + ret: List[str] = [] if day == 127: return "Every day" - if day & WEEK_DAYS["Sunday"]: - ret.append("Sunday") - if day & WEEK_DAYS["Monday"]: - ret.append("Monday") - if day & WEEK_DAYS["Tuesday"]: - ret.append("Tuesday") - if day & WEEK_DAYS["Wednesday"]: - ret.append("Wednesday") - if day & WEEK_DAYS["Thursday"]: - ret.append("Thursday") - if day & WEEK_DAYS["Friday"]: - ret.append("Friday") - if day & WEEK_DAYS["Saturday"]: - ret.append("Saturday") + for key, value in WEEK_DAYS.items(): + if day & value: + ret.append(key) + return ", ".join(ret) + +def bitmonths_to_string(month: int) -> str: + ret: List[str] = [] + if month == 4095: + return "Every month" + + for key, value in MONTHS.items(): + if month & value: + ret.append(key) + return ", ".join(ret) + + +def bitweeks_to_string(week: int) -> str: + ret: List[str] = [] + if week == 31: + return "Every week" + + for key, value in WEEKS.items(): + if week & value: + ret.append(key) + return ", ".join(ret) + + +def bitmonthdays_to_string(day: int) -> str: + ret: List[str] = [] + + if day == MONTH_DAYS["Last Day"]: + return "Last day" + elif day in (2147483647, 4294967295): + return "Every day" + + for key, value in MONTH_DAYS.items(): + if day & value: + ret.append(key) return ", ".join(ret) -def reload_nats(): - users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}] +def convert_to_iso_duration(string: str) -> str: + tmp = string.upper() + if "D" in tmp: + return f"P{tmp.replace('D', 'DT')}" + + return f"PT{tmp}" + + +def reload_nats() -> None: + users = [ + { + "user": "tacticalrmm", + "password": settings.SECRET_KEY, + "permissions": {"publish": ">", "subscribe": ">"}, + } + ] agents = Agent.objects.prefetch_related("user").only( "pk", "agent_id" ) # type:ignore for agent in agents: try: users.append( - {"user": agent.agent_id, "password": agent.user.auth_token.key} + { + "user": agent.agent_id, + "password": agent.user.auth_token.key, + "permissions": { + "publish": {"allow": agent.agent_id}, + "subscribe": {"allow": agent.agent_id}, + "allow_responses": { + "expires": getattr( + settings, "NATS_ALLOW_RESPONSE_EXPIRATION", "1435m" + ) + }, + }, + } ) except: DebugLog.critical( agent=agent, - log_type="agent_issues", + log_type=DebugLogType.AGENT_ISSUES, message=f"{agent.hostname} does not have a user account, NATS will not work", ) - domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] - cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" - key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" - if hasattr(settings, "CERT_FILE") and hasattr(settings, "KEY_FILE"): - if os.path.exists(settings.CERT_FILE) and os.path.exists(settings.KEY_FILE): - cert_file = settings.CERT_FILE - key_file = settings.KEY_FILE + cert_file, key_file = get_certs() + nats_std_port, nats_ws_port = get_nats_ports() config = { - "tls": { - "cert_file": cert_file, - "key_file": key_file, - }, "authorization": {"users": users}, "max_payload": 67108864, + "port": nats_std_port, # internal only + "websocket": { + "port": nats_ws_port, + "no_tls": True, # TLS is handled by nginx, so not needed here + }, } + if get_nats_internal_protocol() == "tls": + config["tls"] = { + "cert_file": cert_file, + "key_file": key_file, + } + + if "NATS_HTTP_PORT" in os.environ: + config["http_port"] = int(os.getenv("NATS_HTTP_PORT")) # type: ignore + elif hasattr(settings, "NATS_HTTP_PORT"): + config["http_port"] = settings.NATS_HTTP_PORT # type: ignore + + if "NATS_WS_COMPRESSION" in os.environ or hasattr(settings, "NATS_WS_COMPRESSION"): + config["websocket"]["compression"] = True + conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf") with open(conf, "w") as f: json.dump(config, f) @@ -213,13 +265,13 @@ async def __call__(self, scope, receive, send): return await self.app(scope, receive, send) -KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance( +KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance( # noqa AuthMiddlewareStack(inner) ) def get_latest_trmm_ver() -> str: - url = "https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py" + url = "https://raw.githubusercontent.com/amidaware/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py" try: r = requests.get(url, timeout=5) except: @@ -238,8 +290,8 @@ def get_latest_trmm_ver() -> str: def replace_db_values( string: str, instance=None, shell: str = None, quotes=True # type:ignore ) -> Union[str, None]: - from core.models import CustomField, GlobalKVStore from clients.models import Client, Site + from core.models import CustomField, GlobalKVStore # split by period if exists. First should be model and second should be property i.e {{client.name}} temp = string.split(".") @@ -257,8 +309,8 @@ def replace_db_values( return f"'{value}'" if quotes else value else: DebugLog.error( - log_type="scripting", - message=f"{agent.hostname} Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store", # type:ignore + log_type=DebugLogType.SCRIPTING, + message=f"Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store", # type:ignore ) return "" @@ -291,7 +343,7 @@ def replace_db_values( else: # ignore arg since it is invalid DebugLog.error( - log_type="scripting", + log_type=DebugLogType.SCRIPTING, message=f"{instance} Not enough information to find value for: {string}. Only agent, site, client, and global are supported.", ) return "" @@ -299,56 +351,70 @@ def replace_db_values( if not obj: return "" - if hasattr(obj, temp[1]): - value = f"'{getattr(obj, temp[1])}'" if quotes else getattr(obj, temp[1]) + # check if attr exists and isn't a function + if hasattr(obj, temp[1]) and not callable(getattr(obj, temp[1])): + temp1 = getattr(obj, temp[1]) + if shell == ScriptShell.POWERSHELL and isinstance(temp1, str) and "'" in temp1: + temp1 = temp1.replace("'", "''") - elif CustomField.objects.filter(model=model, name=temp[1]).exists(): + value = f"'{temp1}'" if quotes else temp1 + elif CustomField.objects.filter(model=model, name=temp[1]).exists(): field = CustomField.objects.get(model=model, name=temp[1]) model_fields = getattr(field, f"{model}_fields") value = None if model_fields.filter(**{model: obj}).exists(): - if field.type != "checkbox" and model_fields.get(**{model: obj}).value: + if ( + field.type != CustomFieldType.CHECKBOX + and model_fields.get(**{model: obj}).value + ): value = model_fields.get(**{model: obj}).value - elif field.type == "checkbox": + elif field.type == CustomFieldType.CHECKBOX: value = model_fields.get(**{model: obj}).value # need explicit None check since a false boolean value will pass default value - if value == None and field.default_value != None: + if value is None and field.default_value is not None: value = field.default_value # check if value exists and if not use default - if value and field.type == "multiple": + if value and field.type == CustomFieldType.MULTIPLE: value = ( f"'{format_shell_array(value)}'" if quotes else format_shell_array(value) ) - elif value != None and field.type == "checkbox": + elif value is not None and field.type == CustomFieldType.CHECKBOX: value = format_shell_bool(value, shell) else: + if ( + shell == ScriptShell.POWERSHELL + and isinstance(value, str) + and "'" in value + ): + value = value.replace("'", "''") + value = f"'{value}'" if quotes else value else: # ignore arg since property is invalid DebugLog.error( - log_type="scripting", + log_type=DebugLogType.SCRIPTING, message=f"{instance} Couldn't find property on supplied variable: {string}. Make sure it exists as a custom field or a valid agent property", ) return "" # log any unhashable type errors - if value != None: - return value # type: ignore + if value is not None: + return value else: DebugLog.error( - log_type="scripting", + log_type=DebugLogType.SCRIPTING, message=f" {instance}({instance.pk}) Couldn't lookup value for: {string}. Make sure it exists as a custom field or a valid agent property", ) return "" -def format_shell_array(value: list) -> str: +def format_shell_array(value: list[str]) -> str: temp_string = "" for item in value: temp_string += item + "," @@ -356,7 +422,77 @@ def format_shell_array(value: list) -> str: def format_shell_bool(value: bool, shell: Optional[str]) -> str: - if shell == "powershell": + if shell == ScriptShell.POWERSHELL: return "$True" if value else "$False" - else: - return "1" if value else "0" + + return "1" if value else "0" + + +# https://docs.celeryq.dev/en/latest/tutorials/task-cookbook.html#cookbook-task-serial +@contextmanager +def redis_lock(lock_id, oid): + timeout_at = time.monotonic() + REDIS_LOCK_EXPIRE - 3 + status = cache.add(lock_id, oid, REDIS_LOCK_EXPIRE) + try: + yield status + finally: + if time.monotonic() < timeout_at and status: + cache.delete(lock_id) + + +# https://stackoverflow.com/a/57794016 +class DjangoConnectionThreadPoolExecutor(ThreadPoolExecutor): + """ + When a function is passed into the ThreadPoolExecutor via either submit() or map(), + this will wrap the function, and make sure that close_django_db_connection() is called + inside the thread when it's finished so Django doesn't leak DB connections. + + Since map() calls submit(), only submit() needs to be overwritten. + """ + + def close_django_db_connection(self): + connection.close() + + def generate_thread_closing_wrapper(self, fn): + @wraps(fn) + def new_func(*args, **kwargs): + try: + return fn(*args, **kwargs) + finally: + self.close_django_db_connection() + + return new_func + + def submit(*args, **kwargs): + if len(args) >= 2: + self, fn, *args = args + fn = self.generate_thread_closing_wrapper(fn=fn) + elif not args: + raise TypeError( + "descriptor 'submit' of 'ThreadPoolExecutor' object " + "needs an argument" + ) + elif "fn" in kwargs: + fn = self.generate_thread_closing_wrapper(fn=kwargs.pop("fn")) + self, *args = args + + return super(self.__class__, self).submit(fn, *args, **kwargs) + + +def runcmd_placeholder_text() -> dict[str, str]: + ret = { + "cmd": getattr( + settings, + "CMD_PLACEHOLDER_TEXT", + "rmdir /S /Q C:\\Windows\\System32", + ), + "powershell": getattr( + settings, + "POWERSHELL_PLACEHOLDER_TEXT", + "Remove-Item -Recurse -Force C:\\Windows\\System32", + ), + "shell": getattr( + settings, "SHELL_PLACEHOLDER_TEXT", "rm -rf --no-preserve-root /" + ), + } + return ret diff --git a/api/tacticalrmm/winupdate/baker_recipes.py b/api/tacticalrmm/winupdate/baker_recipes.py index d066972913..b597d12997 100644 --- a/api/tacticalrmm/winupdate/baker_recipes.py +++ b/api/tacticalrmm/winupdate/baker_recipes.py @@ -1,12 +1,12 @@ from datetime import datetime as dt from itertools import cycle +from zoneinfo import ZoneInfo -import pytz from model_bakery.recipe import Recipe, seq from .models import WinUpdate, WinUpdatePolicy -timezone = pytz.timezone("America/Los_Angeles") +timezone = ZoneInfo("America/Los_Angeles") severity = ["Critical", "Important", "Moderate", "Low", ""] winupdate = Recipe( diff --git a/api/tacticalrmm/winupdate/migrations/0012_auto_20220227_0554.py b/api/tacticalrmm/winupdate/migrations/0012_auto_20220227_0554.py new file mode 100644 index 0000000000..689ff0d127 --- /dev/null +++ b/api/tacticalrmm/winupdate/migrations/0012_auto_20220227_0554.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.12 on 2022-02-27 05:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('winupdate', '0011_auto_20210917_1954'), + ] + + operations = [ + migrations.RemoveField( + model_name='winupdate', + name='mandatory', + ), + migrations.RemoveField( + model_name='winupdate', + name='needs_reboot', + ), + ] diff --git a/api/tacticalrmm/winupdate/models.py b/api/tacticalrmm/winupdate/models.py index 3ae2858a5f..77e0dabfd2 100644 --- a/api/tacticalrmm/winupdate/models.py +++ b/api/tacticalrmm/winupdate/models.py @@ -44,9 +44,7 @@ class WinUpdate(models.Model): ) guid = models.CharField(max_length=255, null=True, blank=True) kb = models.CharField(max_length=100, null=True, blank=True) - mandatory = models.BooleanField(default=False) # deprecated title = models.TextField(null=True, blank=True) - needs_reboot = models.BooleanField(default=False) # deprecated installed = models.BooleanField(default=False) downloaded = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) @@ -145,8 +143,8 @@ class WinUpdatePolicy(BaseAuditModel): def __str__(self): if self.agent: return self.agent.hostname - else: - return self.policy.name + + return self.policy.name @staticmethod def serialize(policy): diff --git a/api/tacticalrmm/winupdate/permissions.py b/api/tacticalrmm/winupdate/permissions.py index a99b35d6cb..86899e195a 100644 --- a/api/tacticalrmm/winupdate/permissions.py +++ b/api/tacticalrmm/winupdate/permissions.py @@ -4,10 +4,10 @@ class AgentWinUpdatePerms(permissions.BasePermission): - def has_permission(self, r, view): + def has_permission(self, r, view) -> bool: if "agent_id" in view.kwargs.keys(): return _has_perm(r, "can_manage_winupdates") and _has_perm_on_agent( r.user, view.kwargs["agent_id"] ) - else: - return _has_perm(r, "can_manage_winupdates") + + return _has_perm(r, "can_manage_winupdates") diff --git a/api/tacticalrmm/winupdate/serializers.py b/api/tacticalrmm/winupdate/serializers.py index ae0ef638f9..5fa19083e0 100644 --- a/api/tacticalrmm/winupdate/serializers.py +++ b/api/tacticalrmm/winupdate/serializers.py @@ -1,4 +1,3 @@ -import pytz from rest_framework import serializers from .models import WinUpdate, WinUpdatePolicy diff --git a/api/tacticalrmm/winupdate/tasks.py b/api/tacticalrmm/winupdate/tasks.py index 5e00434118..e9de2e0eec 100644 --- a/api/tacticalrmm/winupdate/tasks.py +++ b/api/tacticalrmm/winupdate/tasks.py @@ -1,18 +1,20 @@ import asyncio import datetime as dt import time +from contextlib import suppress +from zoneinfo import ZoneInfo -import pytz from django.utils import timezone as djangotime from packaging import version as pyver from agents.models import Agent -from tacticalrmm.celery import app from logs.models import DebugLog +from tacticalrmm.celery import app +from tacticalrmm.constants import AGENT_STATUS_ONLINE, DebugLogType @app.task -def auto_approve_updates_task(): +def auto_approve_updates_task() -> None: # scheduled task that checks and approves updates daily agents = Agent.objects.only( @@ -28,38 +30,21 @@ def auto_approve_updates_task(): online = [ i for i in agents - if i.status == "online" and pyver.parse(i.version) >= pyver.parse("1.3.0") + if i.status == AGENT_STATUS_ONLINE + and pyver.parse(i.version) >= pyver.parse("1.3.0") ] chunks = (online[i : i + 40] for i in range(0, len(online), 40)) for chunk in chunks: for agent in chunk: asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) - time.sleep(0.05) - time.sleep(15) + time.sleep(1) @app.task -def check_agent_update_schedule_task(): +def check_agent_update_schedule_task() -> None: # scheduled task that installs updates on agents if enabled - agents = Agent.objects.only( - "pk", - "agent_id", - "version", - "last_seen", - "overdue_time", - "offline_time", - "has_patches_pending", - ) - online = [ - i - for i in agents - if pyver.parse(i.version) >= pyver.parse("1.3.0") - and i.has_patches_pending - and i.status == "online" - ] - - for agent in online: + for agent in Agent.online_agents(min_version="1.3.0"): agent.delete_superseded_updates() install = False patch_policy = agent.get_patch_policy() @@ -72,9 +57,8 @@ def check_agent_update_schedule_task(): or patch_policy.low == "approve" or patch_policy.other == "approve" ): - # get current time in agent local time - timezone = pytz.timezone(agent.timezone) + timezone = ZoneInfo(agent.timezone) agent_localtime_now = dt.datetime.now(timezone) weekday = agent_localtime_now.weekday() hour = agent_localtime_now.hour @@ -88,7 +72,7 @@ def check_agent_update_schedule_task(): if last_installed.strftime("%d/%m/%Y") == agent_localtime_now.strftime( "%d/%m/%Y" ): - return + continue # check if schedule is set to daily/weekly and if now is the time to run if ( @@ -99,7 +83,6 @@ def check_agent_update_schedule_task(): install = True elif patch_policy.run_time_frequency == "monthly": - if patch_policy.run_time_day > 28: months_with_30_days = [3, 6, 9, 11] current_month = agent_localtime_now.month @@ -119,7 +102,7 @@ def check_agent_update_schedule_task(): # initiate update on agent asynchronously and don't worry about ret code DebugLog.info( agent=agent, - log_type="windows_updates", + log_type=DebugLogType.WIN_UPDATES, message=f"Installing windows updates on {agent.hostname}", ) nats_data = { @@ -139,17 +122,16 @@ def bulk_install_updates_task(pks: list[int]) -> None: for chunk in chunks: for agent in chunk: agent.delete_superseded_updates() - try: + + with suppress(Exception): agent.approve_updates() - except: - pass + nats_data = { "func": "installwinupdates", "guids": agent.get_approved_update_guids(), } asyncio.run(agent.nats_cmd(nats_data, wait=False)) - time.sleep(0.05) - time.sleep(15) + time.sleep(1) @app.task @@ -161,5 +143,4 @@ def bulk_check_for_updates_task(pks: list[int]) -> None: for agent in chunk: agent.delete_superseded_updates() asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) - time.sleep(0.05) - time.sleep(15) + time.sleep(1) diff --git a/api/tacticalrmm/winupdate/tests.py b/api/tacticalrmm/winupdate/tests.py index c708f9f4b2..7769408633 100644 --- a/api/tacticalrmm/winupdate/tests.py +++ b/api/tacticalrmm/winupdate/tests.py @@ -1,4 +1,4 @@ -from itertools import cycle +# from itertools import cycle from unittest.mock import patch from model_bakery import baker @@ -51,8 +51,8 @@ def test_get_winupdates(self): updates = WinUpdate.objects.filter(agent=agent).order_by("-id", "installed") serializer = WinUpdateSerializer(updates, many=True) self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data), 4) # type: ignore - self.assertEqual(resp.data, serializer.data) # type: ignore + self.assertEqual(len(resp.data), 4) + self.assertEqual(resp.data, serializer.data) self.check_not_authenticated("get", url) @@ -75,11 +75,10 @@ def test_edit_winupdate(self): class TestWinUpdatePermissions(TacticalTestCase): def setUp(self): self.setup_coresettings() - self.client_setup() + self.setup_client() @patch("agents.models.Agent.nats_cmd", return_value="ok") def test_get_scan_install_permissions(self, nats_cmd): - agent = baker.make_recipe("agents.agent") baker.make("winupdate.WinUpdatePolicy", agent=agent) unauthorized_agent = baker.make_recipe("agents.agent") @@ -162,34 +161,34 @@ def setUp(self): ) self.offline_agent = baker.make_recipe("agents.agent", site=site) - @patch("agents.models.Agent.nats_cmd") - @patch("time.sleep") - def test_auto_approve_task(self, mock_sleep, nats_cmd): - from .tasks import auto_approve_updates_task - - # Setup data - baker.make_recipe( - "winupdate.winupdate", - agent=cycle( - [self.online_agents[0], self.online_agents[1], self.offline_agent] - ), - _quantity=20, - ) - baker.make_recipe( - "winupdate.winupdate_approve", - agent=cycle( - [self.online_agents[0], self.online_agents[1], self.offline_agent] - ), - _quantity=3, - ) - - # run task synchronously - auto_approve_updates_task() - - # make sure the check_for_updates_task was run once for each online agent - self.assertEqual(nats_cmd.call_count, 2) - - # check if all of the created updates were approved - winupdates = WinUpdate.objects.all() - for update in winupdates: - self.assertEqual(update.action, "approve") + # @patch("agents.models.Agent.nats_cmd") + # @patch("time.sleep") + # def test_auto_approve_task(self, mock_sleep, nats_cmd): + # from .tasks import auto_approve_updates_task + + # # Setup data + # baker.make_recipe( + # "winupdate.winupdate", + # agent=cycle( + # [self.online_agents[0], self.online_agents[1], self.offline_agent] + # ), + # _quantity=20, + # ) + # baker.make_recipe( + # "winupdate.winupdate_approve", + # agent=cycle( + # [self.online_agents[0], self.online_agents[1], self.offline_agent] + # ), + # _quantity=3, + # ) + + # # run task synchronously + # auto_approve_updates_task() + + # # make sure the check_for_updates_task was run once for each online agent + # self.assertEqual(nats_cmd.call_count, 2) + + # # check if all of the created updates were approved + # winupdates = WinUpdate.objects.all() + # for update in winupdates: + # self.assertEqual(update.action, "approve") diff --git a/api/tacticalrmm/winupdate/views.py b/api/tacticalrmm/winupdate/views.py index be7e7bd983..42aa70101c 100644 --- a/api/tacticalrmm/winupdate/views.py +++ b/api/tacticalrmm/winupdate/views.py @@ -1,14 +1,15 @@ import asyncio from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView +from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.exceptions import PermissionDenied +from rest_framework.views import APIView from agents.models import Agent -from tacticalrmm.utils import get_default_timezone +from tacticalrmm.helpers import notify_error from tacticalrmm.permissions import _has_perm_on_agent +from tacticalrmm.utils import get_default_timezone from .models import WinUpdate from .permissions import AgentWinUpdatePerms @@ -29,12 +30,16 @@ def get(self, request, agent_id): class ScanWindowsUpdates(APIView): permission_classes = [IsAuthenticated, AgentWinUpdatePerms] + # scan for windows updates on agent def post(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) + if agent.is_posix: + return notify_error(f"Not available for {agent.plat}") + agent.delete_superseded_updates() asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False)) - return Response(f"A Windows update scan will performed on {agent.hostname}") + return Response(f"A Windows update scan will be performed on {agent.hostname}") class InstallWindowsUpdates(APIView): diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 84293eb8a5..0000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,65 +0,0 @@ -trigger: - - master - - develop - -jobs: - - job: setup_env - displayName: "Setup" - strategy: - matrix: - Debian10: - AGENT_NAME: "az-pipeline-fran" - - pool: - name: linux-vms - demands: - - agent.name -equals $(AGENT_NAME) - - steps: - - script: | - 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 - SETTINGS_FILE="/myagent/_work/1/s/api/tacticalrmm/tacticalrmm/settings.py" - rm -rf /myagent/_work/1/s/api/env - cd /myagent/_work/1/s/api - python3.9 -m venv env - source env/bin/activate - cd /myagent/_work/1/s/api/tacticalrmm - pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --upgrade pip - SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') - WHEEL_VER=$(grep "^WHEEL_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') - pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER} - pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org -r requirements.txt -r requirements-test.txt -r requirements-dev.txt - displayName: "Install Python Dependencies" - - - script: | - cd /myagent/_work/1/s/api - source env/bin/activate - cd /myagent/_work/1/s/api/tacticalrmm - coverage run manage.py test -v 2 - if [ $? -ne 0 ]; then - exit 1 - fi - displayName: "Run django tests" - - - script: | - cd /myagent/_work/1/s/api - source env/bin/activate - black --exclude migrations/ --check tacticalrmm - if [ $? -ne 0 ]; then - exit 1 - fi - displayName: "Codestyle black" - - - script: | - cd /myagent/_work/1/s/api - source env/bin/activate - cd /myagent/_work/1/s/api/tacticalrmm - export CIRCLE_BRANCH=$BUILD_SOURCEBRANCH - coveralls - displayName: "coveralls" - env: - CIRCLECI: 1 - CIRCLE_BUILD_NUM: $(Build.BuildNumber) diff --git a/backup.sh b/backup.sh index cb7b24f2ba..04f693ffb1 100755 --- a/backup.sh +++ b/backup.sh @@ -1,35 +1,44 @@ -#!/bin/bash +#!/usr/bin/env bash -SCRIPT_VERSION="16" -SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh' +SCRIPT_VERSION="28" GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' RED='\033[0;31m' NC='\033[0m' -THIS_SCRIPT=$(readlink -f "$0") -TMP_FILE=$(mktemp -p "" "rmmbackup_XXXXXXXXXX") -curl -s -L "${SCRIPT_URL}" > ${TMP_FILE} -NEW_VER=$(grep "^SCRIPT_VERSION" "$TMP_FILE" | awk -F'[="]' '{print $3}') - -if [ "${SCRIPT_VERSION}" -ne "${NEW_VER}" ]; then - printf >&2 "${YELLOW}Old backup script detected, downloading and replacing with the latest version...${NC}\n" - wget -q "${SCRIPT_URL}" -O backup.sh - exec ${THIS_SCRIPT} +if [ $EUID -eq 0 ]; then + echo -ne "\033[0;31mDo NOT run this script as root. Exiting.\e[0m\n" + exit 1 fi -rm -f $TMP_FILE +if [[ $* == *--schedule* ]]; then + ( + crontab -l 2>/dev/null + echo "0 0 * * * /rmm/backup.sh --auto > /dev/null 2>&1" + ) | crontab - -if [ $EUID -eq 0 ]; then - echo -ne "\033[0;31mDo NOT run this script as root. Exiting.\e[0m\n" - exit 1 -fi + if [ ! -d /rmmbackups ]; then + sudo mkdir /rmmbackups + fi + + if [ ! -d /rmmbackups/daily ]; then + sudo mkdir /rmmbackups/daily + fi -POSTGRES_USER=$(grep -w USER /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//') -POSTGRES_PW=$(grep -w PASSWORD /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//') + if [ ! -d /rmmbackups/weekly ]; then + sudo mkdir /rmmbackups/weekly + fi + if [ ! -d /rmmbackups/monthly ]; then + sudo mkdir /rmmbackups/monthly + fi + sudo chown ${USER}:${USER} -R /rmmbackups + + printf >&2 "${GREEN}Backups setup to run at midnight and rotate.${NC}\n" + exit 0 +fi if [ ! -d /rmmbackups ]; then sudo mkdir /rmmbackups @@ -40,14 +49,14 @@ if [ -d /meshcentral/meshcentral-backup ]; then rm -rf /meshcentral/meshcentral-backup/* fi +if [ -d /meshcentral/meshcentral-backups ]; then + rm -rf /meshcentral/meshcentral-backups/* +fi + if [ -d /meshcentral/meshcentral-coredumps ]; then rm -f /meshcentral/meshcentral-coredumps/* fi -printf >&2 "${GREEN}Running postgres vacuum${NC}\n" -sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_auditlog" -sudo -u postgres psql -d tacticalrmm -c "vacuum full logs_pendingaction" - dt_now=$(date '+%Y_%m_%d__%H_%M_%S') tmp_dir=$(mktemp -d -t tacticalrmm-XXXXXXXXXXXXXXXXXXXXX) sysd="/etc/systemd/system" @@ -59,34 +68,81 @@ mkdir ${tmp_dir}/nginx mkdir ${tmp_dir}/systemd mkdir ${tmp_dir}/rmm mkdir ${tmp_dir}/confd -mkdir ${tmp_dir}/redis +POSTGRES_USER=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config dbuser) +POSTGRES_PW=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config dbpw) + +pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432/tacticalrmm | gzip -9 >${tmp_dir}/postgres/db-${dt_now}.psql.gz -pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432/tacticalrmm | gzip -9 > ${tmp_dir}/postgres/db-${dt_now}.psql.gz +node /meshcentral/node_modules/meshcentral --dbexport # for import to postgres + +if grep -q postgres "/meshcentral/meshcentral-data/config.json"; then + if ! which jq >/dev/null; then + sudo apt-get install -y jq >/dev/null + fi + MESH_POSTGRES_USER=$(jq '.settings.postgres.user' /meshcentral/meshcentral-data/config.json -r) + MESH_POSTGRES_PW=$(jq '.settings.postgres.password' /meshcentral/meshcentral-data/config.json -r) + pg_dump --dbname=postgresql://"${MESH_POSTGRES_USER}":"${MESH_POSTGRES_PW}"@127.0.0.1:5432/meshcentral | gzip -9 >${tmp_dir}/postgres/mesh-db-${dt_now}.psql.gz +else + mongodump --gzip --out=${tmp_dir}/meshcentral/mongo +fi tar -czvf ${tmp_dir}/meshcentral/mesh.tar.gz --exclude=/meshcentral/node_modules /meshcentral -mongodump --gzip --out=${tmp_dir}/meshcentral/mongo -sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt . +if [ -d /etc/letsencrypt ]; then + sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt . +fi + +local_settings='/rmm/api/tacticalrmm/tacticalrmm/local_settings.py' + +if grep -q CERT_FILE "$local_settings"; then + mkdir -p ${tmp_dir}/certs/custom + CERT_FILE=$(grep "^CERT_FILE" "$local_settings" | awk -F'[= "]' '{print $5}') + KEY_FILE=$(grep "^KEY_FILE" "$local_settings" | awk -F'[= "]' '{print $5}') + cp -p $CERT_FILE ${tmp_dir}/certs/custom/cert + cp -p $KEY_FILE ${tmp_dir}/certs/custom/key +elif grep -q TRMM_INSECURE "$local_settings"; then + mkdir -p ${tmp_dir}/certs/selfsigned + certdir='/etc/ssl/tactical' + cp -p ${certdir}/key.pem ${tmp_dir}/certs/selfsigned/ + cp -p ${certdir}/cert.pem ${tmp_dir}/certs/selfsigned/ +fi -sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx . +for i in rmm frontend meshcentral; do + sudo cp /etc/nginx/sites-available/${i}.conf ${tmp_dir}/nginx/ +done sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d . -sudo gzip -9 -c /var/lib/redis/appendonly.aof > ${tmp_dir}/redis/appendonly.aof.gz +sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/daphne.service ${sysd}/nats-api.service ${tmp_dir}/systemd/ -sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/daphne.service ${tmp_dir}/systemd/ -if [ -f "${sysd}/nats-api.service" ]; then - sudo cp ${sysd}/nats-api.service ${tmp_dir}/systemd/ -fi +cp $local_settings ${tmp_dir}/rmm/ + +if [[ $* == *--auto* ]]; then + + month_day=$(date +"%d") + week_day=$(date +"%u") -cat /rmm/api/tacticalrmm/tacticalrmm/private/log/django_debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz -cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/ -cp /rmm/web/.env ${tmp_dir}/rmm/env -cp /rmm/api/tacticalrmm/tacticalrmm/private/exe/mesh*.exe ${tmp_dir}/rmm/ + if [ "$month_day" -eq 10 ]; then + tar -cf /rmmbackups/monthly/rmm-backup-${dt_now}.tar -C ${tmp_dir} . + else + if [ "$week_day" -eq 5 ]; then + tar -cf /rmmbackups/weekly/rmm-backup-${dt_now}.tar -C ${tmp_dir} . + else + tar -cf /rmmbackups/daily/rmm-backup-${dt_now}.tar -C ${tmp_dir} . + fi + fi -tar -cf /rmmbackups/rmm-backup-${dt_now}.tar -C ${tmp_dir} . + rm -rf ${tmp_dir} -rm -rf ${tmp_dir} + find /rmmbackups/daily/ -type f -mtime +14 -name '*.tar' -execdir rm -- '{}' \; + find /rmmbackups/weekly/ -type f -mtime +60 -name '*.tar' -execdir rm -- '{}' \; + find /rmmbackups/monthly/ -type f -mtime +380 -name '*.tar' -execdir rm -- '{}' \; + echo -ne "${GREEN}Backup Completed${NC}\n" + exit -echo -ne "${GREEN}Backup saved to /rmmbackups/rmm-backup-${dt_now}.tar${NC}\n" \ No newline at end of file +else + tar -cf /rmmbackups/rmm-backup-${dt_now}.tar -C ${tmp_dir} . + + echo -ne "${GREEN}Backup saved to /rmmbackups/rmm-backup-${dt_now}.tar${NC}\n" +fi diff --git a/docker/containers/tactical-frontend/dockerfile b/docker/containers/tactical-frontend/dockerfile index f4e96a8b41..bacd823823 100644 --- a/docker/containers/tactical-frontend/dockerfile +++ b/docker/containers/tactical-frontend/dockerfile @@ -1,30 +1,32 @@ -FROM node:14-alpine AS builder +FROM nginxinc/nginx-unprivileged:stable-alpine -WORKDIR /home/node/app - -COPY ./web/package.json . -RUN npm install -g npm@latest -RUN npm install +ENV PUBLIC_DIR /usr/share/nginx/html -COPY ./web . +USER root -# copy env file to set DOCKER_BUILD to true -RUN echo "DOCKER_BUILD=1" > .env +RUN deluser --remove-home nginx \ + && addgroup -S nginx -g 1000 \ + && adduser -S -G nginx -u 1000 nginx -# modify index.html template to allow injection of js variables at runtime -RUN sed -i '/<\/head>/i - - \ No newline at end of file diff --git a/web/src/api/accounts.js b/web/src/api/accounts.js deleted file mode 100644 index 3ca976a660..0000000000 --- a/web/src/api/accounts.js +++ /dev/null @@ -1,57 +0,0 @@ -import axios from "axios" - -const baseUrl = "/accounts" - -// user api functions -export async function fetchUsers(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/users/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -// role api function -export async function fetchRoles(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/roles/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function removeRole(id) { - const { data } = await axios.delete(`${baseUrl}/roles/${id}/`) - return data -} - -export async function saveRole(payload) { - const { data } = await axios.post(`${baseUrl}/roles/`, payload) - return data -} - -export async function editRole(id, payload) { - const { data } = await axios.put(`${baseUrl}/roles/${id}/`, payload) - return data -} - -// api key api functions -export async function fetchAPIKeys(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/apikeys/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function saveAPIKey(payload) { - const { data } = await axios.post(`${baseUrl}/apikeys/`, payload) - return data -} - -export async function editAPIKey(payload) { - const { data } = await axios.put(`${baseUrl}/apikeys/${payload.id}/`, payload) - return data -} - -export async function removeAPIKey(id) { - const { data } = await axios.delete(`${baseUrl}/apikeys/${id}/`) - return data -} diff --git a/web/src/api/agents.js b/web/src/api/agents.js deleted file mode 100644 index 68845b2d92..0000000000 --- a/web/src/api/agents.js +++ /dev/null @@ -1,130 +0,0 @@ -import axios from "axios" - -const baseUrl = "/agents" - -export async function fetchAgents(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function fetchAgent(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function fetchAgentHistory(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/history/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function fetchAgentChecks(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/checks/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function fetchAgentTasks(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/tasks/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function sendAgentRecovery(agent_id, payload) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/recover/`, payload) - return data -} - -export async function sendAgentCommand(agent_id, payload) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/cmd/`, payload) - return data -} - -export async function refreshAgentWMI(agent_id) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/wmi/`) - return data -} - -export async function runScript(agent_id, payload) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/runscript/`, payload) - return data -} - -export async function runBulkAction(payload) { - const { data } = await axios.post(`${baseUrl}/actions/bulk/`, payload) - return data -} - -export async function fetchAgentProcesses(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/processes/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function killAgentProcess(agent_id, pid, params = {}) { - const { data } = await axios.delete(`${baseUrl}/${agent_id}/processes/${pid}/`, { params: params }) - return data -} - -export async function fetchAgentEventLog(agent_id, logType, days, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function fetchAgentMeshCentralURLs(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/meshcentral/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function scheduleAgentReboot(agent_id, payload) { - const { data } = await axios.patch(`${baseUrl}/${agent_id}/reboot/`, payload) - return data -} - -export async function sendAgentRecoverMesh(agent_id, params = {}) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/meshcentral/recover/`, { params: params }) - return data -} - -// agent notes -export async function fetchAgentNotes(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/notes/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function saveAgentNote(payload) { - const { data } = await axios.post(`${baseUrl}/notes/`, payload) - return data -} - -export async function editAgentNote(pk, payload) { - const { data } = await axios.put(`${baseUrl}/notes/${pk}/`, payload) - return data -} - -export async function removeAgentNote(pk) { - const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`) - return data -} diff --git a/web/src/api/automation.js b/web/src/api/automation.js deleted file mode 100644 index 296d0914d5..0000000000 --- a/web/src/api/automation.js +++ /dev/null @@ -1,15 +0,0 @@ -import axios from "axios" - -const baseUrl = "/automation" - -export async function sendPatchPolicyReset(payload) { - const { data } = await axios.post(`${baseUrl}/patchpolicy/reset/`, payload) - return data -} - -export async function fetchPolicyChecks(id) { - try { - const { data } = await axios.get(`${baseUrl}/policies/${id}/checks/`) - return data - } catch (e) { console.error(e) } -} \ No newline at end of file diff --git a/web/src/api/checks.js b/web/src/api/checks.js deleted file mode 100644 index ec5436a8ab..0000000000 --- a/web/src/api/checks.js +++ /dev/null @@ -1,32 +0,0 @@ -import axios from "axios" - -const baseUrl = "/checks" - -export async function fetchChecks(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function saveCheck(payload) { - const { data } = await axios.post(`${baseUrl}/`, payload) - return data -} - -export async function updateCheck(id, payload) { - const { data } = await axios.put(`${baseUrl}/${id}/`, payload) - return data -} - -export async function removeCheck(id) { - const { data } = await axios.delete(`${baseUrl}/${id}/`) - return data -} - -export async function resetCheck(id) { - const { data } = await axios.post(`${baseUrl}/${id}/reset/`) - return data -} \ No newline at end of file diff --git a/web/src/api/clients.js b/web/src/api/clients.js deleted file mode 100644 index faa9c6ef56..0000000000 --- a/web/src/api/clients.js +++ /dev/null @@ -1,81 +0,0 @@ -import axios from "axios" - -const baseUrl = "/clients" - -// client endpoints -export async function fetchClients() { - try { - const { data } = await axios.get(`${baseUrl}/`) - return data - } catch (e) { console.error(e) } -} - -export async function fetchClient(id) { - try { - const { data } = await axios.get(`${baseUrl}/${id}/`) - return data - } catch (e) { console.error(e) } -} - -export async function saveClient(payload) { - const { data } = await axios.post(`${baseUrl}/`, payload) - return data -} - -export async function editClient(id, payload) { - const { data } = await axios.put(`${baseUrl}/${id}/`, payload) - return data -} - -export async function removeClient(id, params = {}) { - const { data } = await axios.delete(`${baseUrl}/${id}/`, { params: params }) - return data -} - -// site endpoints -export async function fetchSites() { - try { - const { data } = await axios.get(`${baseUrl}/sites/`) - return data - } catch (e) { console.error(e) } -} - -export async function fetchSite(id) { - try { - const { data } = await axios.get(`${baseUrl}/sites/${id}/`) - return data - } catch (e) { console.error(e) } -} - -export async function saveSite(payload) { - const { data } = await axios.post(`${baseUrl}/sites/`, payload) - return data -} - -export async function editSite(id, payload) { - const { data } = await axios.put(`${baseUrl}/sites/${id}/`, payload) - return data -} - -export async function removeSite(id, params = {}) { - const { data } = await axios.delete(`${baseUrl}/sites/${id}/`, { params: params }) - return data -} - -// deployment endpoints -export async function fetchDeployments() { - try { - const { data } = await axios.get(`${baseUrl}/deployments/`) - return data - } catch (e) { console.error(e) } -} - -export async function saveDeployment(payload) { - const { data } = await axios.post(`${baseUrl}/deployments/`, payload) - return data -} - -export async function removeDeployment(id, params = {}) { - const { data } = await axios.delete(`${baseUrl}/deployments/${id}/`, { params: params }) - return data -} \ No newline at end of file diff --git a/web/src/api/core.js b/web/src/api/core.js deleted file mode 100644 index 3c04358e30..0000000000 --- a/web/src/api/core.js +++ /dev/null @@ -1,20 +0,0 @@ -import axios from "axios" - -const baseUrl = "/core" - -export async function fetchCustomFields(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/customfields/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function uploadMeshAgent(payload) { - const { data } = await axios.put(`${baseUrl}/uploadmesh/`, payload) - return data -} - -export async function fetchDashboardInfo(params = {}) { - const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params }) - return data -} \ No newline at end of file diff --git a/web/src/api/logs.js b/web/src/api/logs.js deleted file mode 100644 index 0462626a59..0000000000 --- a/web/src/api/logs.js +++ /dev/null @@ -1,35 +0,0 @@ -import axios from "axios" - -const baseUrl = "/logs" - -export async function fetchDebugLog(payload) { - try { - const { data } = await axios.patch(`${baseUrl}/debug/`, payload) - return data - } catch (e) { } -} - -export async function fetchAuditLog(payload) { - const { data } = await axios.patch(`${baseUrl}/audit/`, payload) - return data -} - -// pending actions -export async function fetchPendingActions(params = {}) { - const { data } = await axios.get(`${baseUrl}/pendingactions/`, { params: params }) - return data -} - -export async function fetchAgentPendingActions(agent_id) { - try { - const { data } = await axios.get(`/agents/${agent_id}/pendingactions/`) - return data - } catch (e) { - console.error(e) - } -} - -export async function deletePendingAction(id) { - const { data } = await axios.delete(`${baseUrl}/pendingactions/${id}/`) - return data -} diff --git a/web/src/api/scripts.js b/web/src/api/scripts.js deleted file mode 100644 index e37e7b9811..0000000000 --- a/web/src/api/scripts.js +++ /dev/null @@ -1,62 +0,0 @@ -import axios from "axios" - -const baseUrl = "/scripts" - -// script operations -export async function fetchScripts(params = {}) { - const { data } = await axios.get(`${baseUrl}/`, { params: params }) - return data -} - -export async function testScript(agent_id, payload) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/test/`, payload) - return data -} - -export async function saveScript(payload) { - const { data } = await axios.post(`${baseUrl}/`, payload) - return data -} - -export async function editScript(payload) { - const { data } = await axios.put(`${baseUrl}/${payload.id}/`, payload) - return data -} - -export async function removeScript(id) { - const { data } = await axios.delete(`${baseUrl}/${id}/`) - return data -} - -export async function downloadScript(id, params = {}) { - const { data } = await axios.get(`${baseUrl}/${id}/download/`, { params: params }) - return data -} - - -// script snippet operations -export async function fetchScriptSnippets(params = {}) { - const { data } = await axios.get(`${baseUrl}/snippets/`, { params: params }) - return data - -} - -export async function saveScriptSnippet(payload) { - const { data } = await axios.post(`${baseUrl}/snippets/`, payload) - return data -} - -export async function fetchScriptSnippet(id, params = {}) { - const { data } = await axios.get(`${baseUrl}/snippets/${id}/`, { params: params }) - return data -} - -export async function editScriptSnippet(payload) { - const { data } = await axios.put(`${baseUrl}/snippets/${payload.id}/`, payload) - return data -} - -export async function removeScriptSnippet(id) { - const { data } = await axios.delete(`${baseUrl}/snippets/${id}/`) - return data -} \ No newline at end of file diff --git a/web/src/api/services.js b/web/src/api/services.js deleted file mode 100644 index ddd9a045c0..0000000000 --- a/web/src/api/services.js +++ /dev/null @@ -1,31 +0,0 @@ -import axios from "axios" - -const baseUrl = "/services" - -export async function getAgentServices(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function getAgentServiceDetails(agent_id, svcname, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/${svcname}/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function editAgentServiceStartType(agent_id, svcname, payload) { - const { data } = await axios.put(`${baseUrl}/${agent_id}/${svcname}/`, payload) - return data -} - -export async function sendAgentServiceAction(agent_id, svcname, payload) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/${svcname}/`, payload) - return data -} diff --git a/web/src/api/software.js b/web/src/api/software.js deleted file mode 100644 index 22c525e012..0000000000 --- a/web/src/api/software.js +++ /dev/null @@ -1,35 +0,0 @@ -import axios from "axios" - -const baseUrl = "/software" - -export async function fetchChocosSoftware(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/chocos/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function fetchAgentSoftware(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) - return data.software - } catch (e) { - console.error(e) - } -} - -export async function installAgentSoftware(agent_id, payload) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/`, payload) - return data -} - -export async function refreshAgentSoftware(agent_id) { - try { - const { data } = await axios.put(`${baseUrl}/${agent_id}/`) - return data - } catch (e) { - console.error(e) - } -} \ No newline at end of file diff --git a/web/src/api/tasks.js b/web/src/api/tasks.js deleted file mode 100644 index 5d7299947c..0000000000 --- a/web/src/api/tasks.js +++ /dev/null @@ -1,32 +0,0 @@ -import axios from "axios" - -const baseUrl = "/tasks" - -export async function fetchTasks(params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/`, { params: params }) - return data - } catch (e) { - console.error(e) - } -} - -export async function saveTask(payload) { - const { data } = await axios.post(`${baseUrl}/`, payload) - return data -} - -export async function updateTask(id, payload) { - const { data } = await axios.put(`${baseUrl}/${id}/`, payload) - return data -} - -export async function removeTask(id) { - const { data } = await axios.delete(`${baseUrl}/${id}/`) - return data -} - -export async function runTask(id) { - const { data } = await axios.post(`${baseUrl}/${id}/run/`) - return data -} \ No newline at end of file diff --git a/web/src/api/winupdates.js b/web/src/api/winupdates.js deleted file mode 100644 index 96515a4765..0000000000 --- a/web/src/api/winupdates.js +++ /dev/null @@ -1,26 +0,0 @@ -import axios from "axios" - -const baseUrl = "/winupdate" - -// win updates api functions -export async function fetchAgentUpdates(agent_id, params = {}) { - try { - const { data } = await axios.get(`${baseUrl}/${agent_id}/`, { params: params }) - return data - } catch (e) { console.error(e) } -} - -export async function runAgentUpdateScan(agent_id) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/scan/`) - return data -} - -export async function runAgentUpdateInstall(agent_id) { - const { data } = await axios.post(`${baseUrl}/${agent_id}/install/`) - return data -} - -export async function editAgentUpdate(id, payload) { - const { data } = await axios.put(`${baseUrl}/${id}/`, payload) - return data -} diff --git a/web/src/assets/logo.png b/web/src/assets/logo.png deleted file mode 100644 index 2a9b50b833..0000000000 Binary files a/web/src/assets/logo.png and /dev/null differ diff --git a/web/src/boot/axios.js b/web/src/boot/axios.js deleted file mode 100644 index dfe73a3cae..0000000000 --- a/web/src/boot/axios.js +++ /dev/null @@ -1,87 +0,0 @@ -import axios from 'axios'; -import { Notify } from "quasar" - -export const getBaseUrl = () => { - if (process.env.NODE_ENV === "production") { - if (process.env.DOCKER_BUILD) { - return window._env_.PROD_URL; - } else { - return process.env.PROD_API; - } - } else { - return process.env.DEV_API; - } -}; - -export default function ({ app, router, store }) { - - app.config.globalProperties.$axios = axios; - - axios.interceptors.request.use( - function (config) { - config.baseURL = getBaseUrl() - const token = store.state.token; - if (token != null) { - config.headers.Authorization = `Token ${token}`; - } - return config; - }, - function (err) { - return Promise.reject(err); - } - ); - - axios.interceptors.response.use( - function (response) { - return response; - }, - async function (error) { - let text - - if (!error.response) { - text = error.message - } - // unauthorized - else if (error.response.status === 401) { - router.push({ path: "/expired" }); - } - // perms - else if (error.response.status === 403) { - // don't notify user if method is GET - if (error.config.method === "get" || error.config.method === "patch") return Promise.reject({ ...error }); - text = error.response.data.detail; - } - // catch all for other 400 error messages - else if (error.response.status >= 400 && error.response.status < 500) { - - if (error.config.responseType === "blob") { - text = (await error.response.data.text()).replace(/^"|"$/g, '') - } - - else if (error.response.data.non_field_errors) { - text = error.response.data.non_field_errors[0] - - } else { - if (typeof error.response.data === "string") { - text = error.response.data - } else if (typeof error.response.data === "object") { - let [key, value] = Object.entries(error.response.data)[0] - text = key + ": " + value[0] - } - } - } - - if (text || error.response) { - Notify.create({ - color: "negative", - message: text ? text : "", - caption: error.response ? error.response.status + ": " + error.response.statusText : "", - timeout: 2500 - }) - } - - return Promise.reject({ ...error }); - } - ); -} - diff --git a/web/src/components/AdminManager.vue b/web/src/components/AdminManager.vue deleted file mode 100644 index 8aa9e68b4b..0000000000 --- a/web/src/components/AdminManager.vue +++ /dev/null @@ -1,270 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue deleted file mode 100644 index 1b29069e8f..0000000000 --- a/web/src/components/AgentTable.vue +++ /dev/null @@ -1,800 +0,0 @@ - - - - diff --git a/web/src/components/AlertsIcon.vue b/web/src/components/AlertsIcon.vue deleted file mode 100644 index 6d38dff31a..0000000000 --- a/web/src/components/AlertsIcon.vue +++ /dev/null @@ -1,154 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/AlertsManager.vue b/web/src/components/AlertsManager.vue deleted file mode 100644 index 54cc22a352..0000000000 --- a/web/src/components/AlertsManager.vue +++ /dev/null @@ -1,331 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/FileBar.vue b/web/src/components/FileBar.vue deleted file mode 100644 index edebfd1505..0000000000 --- a/web/src/components/FileBar.vue +++ /dev/null @@ -1,344 +0,0 @@ - - - diff --git a/web/src/components/SubTableTabs.vue b/web/src/components/SubTableTabs.vue deleted file mode 100644 index a28be3aa68..0000000000 --- a/web/src/components/SubTableTabs.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/accounts/PermissionsManager.vue b/web/src/components/accounts/PermissionsManager.vue deleted file mode 100644 index 090120b7e5..0000000000 --- a/web/src/components/accounts/PermissionsManager.vue +++ /dev/null @@ -1,156 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/accounts/RolesForm.vue b/web/src/components/accounts/RolesForm.vue deleted file mode 100644 index 22c628aa4c..0000000000 --- a/web/src/components/accounts/RolesForm.vue +++ /dev/null @@ -1,359 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/AssetsTab.vue b/web/src/components/agents/AssetsTab.vue deleted file mode 100644 index 560d131918..0000000000 --- a/web/src/components/agents/AssetsTab.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - - diff --git a/web/src/components/agents/AuditTab.vue b/web/src/components/agents/AuditTab.vue deleted file mode 100644 index d9ef08c862..0000000000 --- a/web/src/components/agents/AuditTab.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/AutomatedTasksTab.vue b/web/src/components/agents/AutomatedTasksTab.vue deleted file mode 100644 index 34ff72a28e..0000000000 --- a/web/src/components/agents/AutomatedTasksTab.vue +++ /dev/null @@ -1,449 +0,0 @@ - - - - diff --git a/web/src/components/agents/ChecksTab.vue b/web/src/components/agents/ChecksTab.vue deleted file mode 100644 index 71fc136b2a..0000000000 --- a/web/src/components/agents/ChecksTab.vue +++ /dev/null @@ -1,506 +0,0 @@ - - - - diff --git a/web/src/components/agents/DebugTab.vue b/web/src/components/agents/DebugTab.vue deleted file mode 100644 index 591d259b72..0000000000 --- a/web/src/components/agents/DebugTab.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/HistoryTab.vue b/web/src/components/agents/HistoryTab.vue deleted file mode 100644 index 105148cc5c..0000000000 --- a/web/src/components/agents/HistoryTab.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/NotesTab.vue b/web/src/components/agents/NotesTab.vue deleted file mode 100644 index 9faae702c4..0000000000 --- a/web/src/components/agents/NotesTab.vue +++ /dev/null @@ -1,225 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/agents/SoftwareTab.vue b/web/src/components/agents/SoftwareTab.vue deleted file mode 100644 index 655045b5be..0000000000 --- a/web/src/components/agents/SoftwareTab.vue +++ /dev/null @@ -1,168 +0,0 @@ - - - - diff --git a/web/src/components/agents/SummaryTab.vue b/web/src/components/agents/SummaryTab.vue deleted file mode 100644 index b42efee6d6..0000000000 --- a/web/src/components/agents/SummaryTab.vue +++ /dev/null @@ -1,205 +0,0 @@ - - - - diff --git a/web/src/components/agents/WinUpdateTab.vue b/web/src/components/agents/WinUpdateTab.vue deleted file mode 100644 index 5ebf61a532..0000000000 --- a/web/src/components/agents/WinUpdateTab.vue +++ /dev/null @@ -1,293 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/WmiDetail.vue b/web/src/components/agents/WmiDetail.vue deleted file mode 100644 index dbd2b2cd36..0000000000 --- a/web/src/components/agents/WmiDetail.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - diff --git a/web/src/components/agents/remotebg/EventLogManager.vue b/web/src/components/agents/remotebg/EventLogManager.vue deleted file mode 100644 index 4ab5770813..0000000000 --- a/web/src/components/agents/remotebg/EventLogManager.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/remotebg/ProcessManager.vue b/web/src/components/agents/remotebg/ProcessManager.vue deleted file mode 100644 index efcf61d9cd..0000000000 --- a/web/src/components/agents/remotebg/ProcessManager.vue +++ /dev/null @@ -1,233 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/agents/remotebg/ServiceDetail.vue b/web/src/components/agents/remotebg/ServiceDetail.vue deleted file mode 100644 index c2951fb9eb..0000000000 --- a/web/src/components/agents/remotebg/ServiceDetail.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - diff --git a/web/src/components/agents/remotebg/ServicesManager.vue b/web/src/components/agents/remotebg/ServicesManager.vue deleted file mode 100644 index d6be5741c6..0000000000 --- a/web/src/components/agents/remotebg/ServicesManager.vue +++ /dev/null @@ -1,215 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/automation/AutomationManager.vue b/web/src/components/automation/AutomationManager.vue deleted file mode 100644 index bbf59a0abc..0000000000 --- a/web/src/components/automation/AutomationManager.vue +++ /dev/null @@ -1,518 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/automation/PolicyAutomatedTasksTab.vue b/web/src/components/automation/PolicyAutomatedTasksTab.vue deleted file mode 100644 index 0522e3e7ef..0000000000 --- a/web/src/components/automation/PolicyAutomatedTasksTab.vue +++ /dev/null @@ -1,320 +0,0 @@ - - - - diff --git a/web/src/components/automation/PolicyChecksTab.vue b/web/src/components/automation/PolicyChecksTab.vue deleted file mode 100644 index 3c4115d487..0000000000 --- a/web/src/components/automation/PolicyChecksTab.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - diff --git a/web/src/components/automation/PolicyOverview.vue b/web/src/components/automation/PolicyOverview.vue deleted file mode 100644 index cb1720d731..0000000000 --- a/web/src/components/automation/PolicyOverview.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - diff --git a/web/src/components/automation/modals/PolicyAdd.vue b/web/src/components/automation/modals/PolicyAdd.vue deleted file mode 100644 index 27b0a7778e..0000000000 --- a/web/src/components/automation/modals/PolicyAdd.vue +++ /dev/null @@ -1,194 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/automation/modals/PolicyExclusions.vue b/web/src/components/automation/modals/PolicyExclusions.vue deleted file mode 100644 index 669f16f3cc..0000000000 --- a/web/src/components/automation/modals/PolicyExclusions.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/automation/modals/PolicyForm.vue b/web/src/components/automation/modals/PolicyForm.vue deleted file mode 100644 index 47269207ca..0000000000 --- a/web/src/components/automation/modals/PolicyForm.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/automation/modals/PolicyStatus.vue b/web/src/components/automation/modals/PolicyStatus.vue deleted file mode 100644 index d8ac156685..0000000000 --- a/web/src/components/automation/modals/PolicyStatus.vue +++ /dev/null @@ -1,233 +0,0 @@ - - - diff --git a/web/src/components/automation/modals/RelationsView.vue b/web/src/components/automation/modals/RelationsView.vue deleted file mode 100644 index 4b7945240b..0000000000 --- a/web/src/components/automation/modals/RelationsView.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/CpuLoadCheck.vue b/web/src/components/checks/CpuLoadCheck.vue deleted file mode 100644 index f4eaf0d1c8..0000000000 --- a/web/src/components/checks/CpuLoadCheck.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/DiskSpaceCheck.vue b/web/src/components/checks/DiskSpaceCheck.vue deleted file mode 100644 index 66531bbbf2..0000000000 --- a/web/src/components/checks/DiskSpaceCheck.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/EventLogCheck.vue b/web/src/components/checks/EventLogCheck.vue deleted file mode 100644 index 38b6eed9fd..0000000000 --- a/web/src/components/checks/EventLogCheck.vue +++ /dev/null @@ -1,231 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/EventLogCheckOutput.vue b/web/src/components/checks/EventLogCheckOutput.vue deleted file mode 100644 index ec69e8c276..0000000000 --- a/web/src/components/checks/EventLogCheckOutput.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/MemCheck.vue b/web/src/components/checks/MemCheck.vue deleted file mode 100644 index 2fbd126a1d..0000000000 --- a/web/src/components/checks/MemCheck.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/PingCheck.vue b/web/src/components/checks/PingCheck.vue deleted file mode 100644 index e835898f21..0000000000 --- a/web/src/components/checks/PingCheck.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/ScriptCheck.vue b/web/src/components/checks/ScriptCheck.vue deleted file mode 100644 index 32b1a8d193..0000000000 --- a/web/src/components/checks/ScriptCheck.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/ScriptOutput.vue b/web/src/components/checks/ScriptOutput.vue deleted file mode 100644 index 5ceb518010..0000000000 --- a/web/src/components/checks/ScriptOutput.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/checks/WinSvcCheck.vue b/web/src/components/checks/WinSvcCheck.vue deleted file mode 100644 index 3fff133885..0000000000 --- a/web/src/components/checks/WinSvcCheck.vue +++ /dev/null @@ -1,199 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/ClientsForm.vue b/web/src/components/clients/ClientsForm.vue deleted file mode 100644 index 21be085764..0000000000 --- a/web/src/components/clients/ClientsForm.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/ClientsManager.vue b/web/src/components/clients/ClientsManager.vue deleted file mode 100644 index 2030c10d54..0000000000 --- a/web/src/components/clients/ClientsManager.vue +++ /dev/null @@ -1,218 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/DeleteClient.vue b/web/src/components/clients/DeleteClient.vue deleted file mode 100644 index 224ae52c2d..0000000000 --- a/web/src/components/clients/DeleteClient.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/Deployment.vue b/web/src/components/clients/Deployment.vue deleted file mode 100644 index 6f5b760a6a..0000000000 --- a/web/src/components/clients/Deployment.vue +++ /dev/null @@ -1,171 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/NewDeployment.vue b/web/src/components/clients/NewDeployment.vue deleted file mode 100644 index 0545ffa662..0000000000 --- a/web/src/components/clients/NewDeployment.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/SitesForm.vue b/web/src/components/clients/SitesForm.vue deleted file mode 100644 index 0d8de9a15c..0000000000 --- a/web/src/components/clients/SitesForm.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/clients/SitesTable.vue b/web/src/components/clients/SitesTable.vue deleted file mode 100644 index ae9c27e6e7..0000000000 --- a/web/src/components/clients/SitesTable.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/core/APIKeysForm.vue b/web/src/components/core/APIKeysForm.vue deleted file mode 100644 index 90cc8e9677..0000000000 --- a/web/src/components/core/APIKeysForm.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/core/APIKeysTable.vue b/web/src/components/core/APIKeysTable.vue deleted file mode 100644 index 89fb888b6b..0000000000 --- a/web/src/components/core/APIKeysTable.vue +++ /dev/null @@ -1,212 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/core/UploadMesh.vue b/web/src/components/core/UploadMesh.vue deleted file mode 100644 index 9e9a09249b..0000000000 --- a/web/src/components/core/UploadMesh.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/graphs/CheckGraph.vue b/web/src/components/graphs/CheckGraph.vue deleted file mode 100644 index 7a4d4c08a9..0000000000 --- a/web/src/components/graphs/CheckGraph.vue +++ /dev/null @@ -1,264 +0,0 @@ - - diff --git a/web/src/components/logs/AuditLogDetailModal.vue b/web/src/components/logs/AuditLogDetailModal.vue deleted file mode 100644 index 0318369537..0000000000 --- a/web/src/components/logs/AuditLogDetailModal.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/logs/AuditManager.vue b/web/src/components/logs/AuditManager.vue deleted file mode 100644 index eb4bb0b934..0000000000 --- a/web/src/components/logs/AuditManager.vue +++ /dev/null @@ -1,386 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/logs/DebugLog.vue b/web/src/components/logs/DebugLog.vue deleted file mode 100644 index adb816fc93..0000000000 --- a/web/src/components/logs/DebugLog.vue +++ /dev/null @@ -1,199 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/logs/PendingActions.vue b/web/src/components/logs/PendingActions.vue deleted file mode 100644 index 693d7c85c8..0000000000 --- a/web/src/components/logs/PendingActions.vue +++ /dev/null @@ -1,213 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/admin/UserForm.vue b/web/src/components/modals/admin/UserForm.vue deleted file mode 100644 index f33435d281..0000000000 --- a/web/src/components/modals/admin/UserForm.vue +++ /dev/null @@ -1,204 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/admin/UserResetPasswordForm.vue b/web/src/components/modals/admin/UserResetPasswordForm.vue deleted file mode 100644 index 5b177ad0d2..0000000000 --- a/web/src/components/modals/admin/UserResetPasswordForm.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/AgentDownload.vue b/web/src/components/modals/agents/AgentDownload.vue deleted file mode 100644 index 661217d6d4..0000000000 --- a/web/src/components/modals/agents/AgentDownload.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/AgentRecovery.vue b/web/src/components/modals/agents/AgentRecovery.vue deleted file mode 100644 index bbb6f0e426..0000000000 --- a/web/src/components/modals/agents/AgentRecovery.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/BulkAction.vue b/web/src/components/modals/agents/BulkAction.vue deleted file mode 100644 index 56119d7edb..0000000000 --- a/web/src/components/modals/agents/BulkAction.vue +++ /dev/null @@ -1,297 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/EditAgent.vue b/web/src/components/modals/agents/EditAgent.vue deleted file mode 100644 index 1e0bfd3bcd..0000000000 --- a/web/src/components/modals/agents/EditAgent.vue +++ /dev/null @@ -1,264 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/InstallAgent.vue b/web/src/components/modals/agents/InstallAgent.vue deleted file mode 100644 index ccc204aa5a..0000000000 --- a/web/src/components/modals/agents/InstallAgent.vue +++ /dev/null @@ -1,242 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/PatchPolicyForm.vue b/web/src/components/modals/agents/PatchPolicyForm.vue deleted file mode 100644 index 0f40fae962..0000000000 --- a/web/src/components/modals/agents/PatchPolicyForm.vue +++ /dev/null @@ -1,307 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/RebootLater.vue b/web/src/components/modals/agents/RebootLater.vue deleted file mode 100644 index 773c196261..0000000000 --- a/web/src/components/modals/agents/RebootLater.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/RunScript.vue b/web/src/components/modals/agents/RunScript.vue deleted file mode 100644 index eab516fea1..0000000000 --- a/web/src/components/modals/agents/RunScript.vue +++ /dev/null @@ -1,202 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/SendCommand.vue b/web/src/components/modals/agents/SendCommand.vue deleted file mode 100644 index d2db2c0fa0..0000000000 --- a/web/src/components/modals/agents/SendCommand.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/agents/UpdateAgents.vue b/web/src/components/modals/agents/UpdateAgents.vue deleted file mode 100644 index c220c5e3b5..0000000000 --- a/web/src/components/modals/agents/UpdateAgents.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/web/src/components/modals/alerts/AlertExclusions.vue b/web/src/components/modals/alerts/AlertExclusions.vue deleted file mode 100644 index a20a48568c..0000000000 --- a/web/src/components/modals/alerts/AlertExclusions.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertTemplateAdd.vue b/web/src/components/modals/alerts/AlertTemplateAdd.vue deleted file mode 100644 index 9f6beab1a6..0000000000 --- a/web/src/components/modals/alerts/AlertTemplateAdd.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertTemplateForm.vue b/web/src/components/modals/alerts/AlertTemplateForm.vue deleted file mode 100644 index 6dbb625e66..0000000000 --- a/web/src/components/modals/alerts/AlertTemplateForm.vue +++ /dev/null @@ -1,720 +0,0 @@ - - - diff --git a/web/src/components/modals/alerts/AlertTemplateRelated.vue b/web/src/components/modals/alerts/AlertTemplateRelated.vue deleted file mode 100644 index 98c665ba7d..0000000000 --- a/web/src/components/modals/alerts/AlertTemplateRelated.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/alerts/AlertsOverview.vue b/web/src/components/modals/alerts/AlertsOverview.vue deleted file mode 100644 index 4625d50cd7..0000000000 --- a/web/src/components/modals/alerts/AlertsOverview.vue +++ /dev/null @@ -1,460 +0,0 @@ - - - diff --git a/web/src/components/modals/core/ServerMaintenance.vue b/web/src/components/modals/core/ServerMaintenance.vue deleted file mode 100644 index 141dcc444d..0000000000 --- a/web/src/components/modals/core/ServerMaintenance.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/CodeSign.vue b/web/src/components/modals/coresettings/CodeSign.vue deleted file mode 100644 index 5fe8cc21f1..0000000000 --- a/web/src/components/modals/coresettings/CodeSign.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/CustomFields.vue b/web/src/components/modals/coresettings/CustomFields.vue deleted file mode 100644 index bb9fbaa2b3..0000000000 --- a/web/src/components/modals/coresettings/CustomFields.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/CustomFieldsForm.vue b/web/src/components/modals/coresettings/CustomFieldsForm.vue deleted file mode 100644 index 69f290d27f..0000000000 --- a/web/src/components/modals/coresettings/CustomFieldsForm.vue +++ /dev/null @@ -1,274 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/CustomFieldsTable.vue b/web/src/components/modals/coresettings/CustomFieldsTable.vue deleted file mode 100644 index 6255794296..0000000000 --- a/web/src/components/modals/coresettings/CustomFieldsTable.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/EditCoreSettings.vue b/web/src/components/modals/coresettings/EditCoreSettings.vue deleted file mode 100644 index a86128517e..0000000000 --- a/web/src/components/modals/coresettings/EditCoreSettings.vue +++ /dev/null @@ -1,611 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/KeyStoreForm.vue b/web/src/components/modals/coresettings/KeyStoreForm.vue deleted file mode 100644 index 1c8b9936f9..0000000000 --- a/web/src/components/modals/coresettings/KeyStoreForm.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/KeyStoreTable.vue b/web/src/components/modals/coresettings/KeyStoreTable.vue deleted file mode 100644 index c64541327e..0000000000 --- a/web/src/components/modals/coresettings/KeyStoreTable.vue +++ /dev/null @@ -1,155 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/ResetPatchPolicy.vue b/web/src/components/modals/coresettings/ResetPatchPolicy.vue deleted file mode 100644 index 8f77197255..0000000000 --- a/web/src/components/modals/coresettings/ResetPatchPolicy.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/URLActionsForm.vue b/web/src/components/modals/coresettings/URLActionsForm.vue deleted file mode 100644 index 37d45ee381..0000000000 --- a/web/src/components/modals/coresettings/URLActionsForm.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/URLActionsTable.vue b/web/src/components/modals/coresettings/URLActionsTable.vue deleted file mode 100644 index d8c1480b97..0000000000 --- a/web/src/components/modals/coresettings/URLActionsTable.vue +++ /dev/null @@ -1,166 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/modals/coresettings/UserPreferences.vue b/web/src/components/modals/coresettings/UserPreferences.vue deleted file mode 100644 index 0ebe97f6f3..0000000000 --- a/web/src/components/modals/coresettings/UserPreferences.vue +++ /dev/null @@ -1,233 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/ScriptFormModal.vue b/web/src/components/scripts/ScriptFormModal.vue deleted file mode 100644 index 0e21ede9ee..0000000000 --- a/web/src/components/scripts/ScriptFormModal.vue +++ /dev/null @@ -1,271 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/ScriptManager.vue b/web/src/components/scripts/ScriptManager.vue deleted file mode 100644 index dd3204a860..0000000000 --- a/web/src/components/scripts/ScriptManager.vue +++ /dev/null @@ -1,648 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/ScriptSnippetFormModal.vue b/web/src/components/scripts/ScriptSnippetFormModal.vue deleted file mode 100644 index 5bac88dbdd..0000000000 --- a/web/src/components/scripts/ScriptSnippetFormModal.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/ScriptSnippets.vue b/web/src/components/scripts/ScriptSnippets.vue deleted file mode 100644 index 755b6cd85c..0000000000 --- a/web/src/components/scripts/ScriptSnippets.vue +++ /dev/null @@ -1,204 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/ScriptUploadModal.vue b/web/src/components/scripts/ScriptUploadModal.vue deleted file mode 100644 index abb506b6fd..0000000000 --- a/web/src/components/scripts/ScriptUploadModal.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/scripts/TestScriptModal.vue b/web/src/components/scripts/TestScriptModal.vue deleted file mode 100644 index 7976c2095d..0000000000 --- a/web/src/components/scripts/TestScriptModal.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/software/InstallSoftware.vue b/web/src/components/software/InstallSoftware.vue deleted file mode 100644 index d713bb5b24..0000000000 --- a/web/src/components/software/InstallSoftware.vue +++ /dev/null @@ -1,136 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/tasks/AddAutomatedTask.vue b/web/src/components/tasks/AddAutomatedTask.vue deleted file mode 100644 index 3abee00132..0000000000 --- a/web/src/components/tasks/AddAutomatedTask.vue +++ /dev/null @@ -1,351 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/tasks/EditAutomatedTask.vue b/web/src/components/tasks/EditAutomatedTask.vue deleted file mode 100644 index 9b65b3a332..0000000000 --- a/web/src/components/tasks/EditAutomatedTask.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/ui/CustomField.vue b/web/src/components/ui/CustomField.vue deleted file mode 100644 index f4c4951091..0000000000 --- a/web/src/components/ui/CustomField.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/ui/DialogWrapper.vue b/web/src/components/ui/DialogWrapper.vue deleted file mode 100644 index 2d731cba11..0000000000 --- a/web/src/components/ui/DialogWrapper.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/ui/ExportTableBtn.vue b/web/src/components/ui/ExportTableBtn.vue deleted file mode 100644 index 107739d34c..0000000000 --- a/web/src/components/ui/ExportTableBtn.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/components/ui/TacticalDropdown.vue b/web/src/components/ui/TacticalDropdown.vue deleted file mode 100644 index 60c97ca41f..0000000000 --- a/web/src/components/ui/TacticalDropdown.vue +++ /dev/null @@ -1,95 +0,0 @@ - - \ No newline at end of file diff --git a/web/src/composables/accounts.js b/web/src/composables/accounts.js deleted file mode 100644 index 3767c58234..0000000000 --- a/web/src/composables/accounts.js +++ /dev/null @@ -1,48 +0,0 @@ - -import { ref, onMounted } from "vue" -import { fetchUsers } from "@/api/accounts" -import { formatUserOptions } from "@/utils/format" - -export function useUserDropdown(onMount = false) { - - const userOptions = ref([]) - const userDropdownLoading = ref(false) - - async function getUserOptions(flat = false) { - userOptions.value = formatUserOptions(await fetchUsers(), flat) - } - - function getDynamicUserOptions(val, update, abort) { - if (!val || val.length < 2) { - abort() - return - } - - update(async () => { - userDropdownLoading.value = true - - const params = { - search: val.toLowerCase() - } - - const options = await fetchUsers(params) - - userOptions.value = options.map(user => user.username) - userDropdownLoading.value = false - }) - } - - if (onMount) { - onMounted(getUserOptions()) - } - - return { - //data - userOptions, - userDropdownLoading, - - //methods - getUserOptions, - getDynamicUserOptions - } -} diff --git a/web/src/composables/agents.js b/web/src/composables/agents.js deleted file mode 100644 index d380213928..0000000000 --- a/web/src/composables/agents.js +++ /dev/null @@ -1,26 +0,0 @@ - -import { ref } from "vue" -import { fetchAgents } from "@/api/agents" -import { formatAgentOptions } from "@/utils/format" - -// agent dropdown -export function useAgentDropdown() { - const agent = ref(null) - const agents = ref([]) - const agentOptions = ref([]) - - // specifing flat returns an array of hostnames versus {value:id, label: hostname} - async function getAgentOptions(flat = false) { - agentOptions.value = formatAgentOptions(await fetchAgents({ detail: false }), flat) - } - - return { - //data - agent, - agents, - agentOptions, - - //methods - getAgentOptions - } -} diff --git a/web/src/composables/checks.js b/web/src/composables/checks.js deleted file mode 100644 index fda1c7eb12..0000000000 --- a/web/src/composables/checks.js +++ /dev/null @@ -1,1197 +0,0 @@ - -import { ref, onMounted } from "vue" -import { updateCheck, saveCheck } from "@/api/checks" -import { fetchAgentChecks } from "@/api/agents"; -import { fetchPolicyChecks } from "@/api/automation"; -import { formatCheckOptions } from "@/utils/format"; -import { fetchAgent } from "@/api/agents" -import { isValidThreshold } from "@/utils/validation"; -import { notifySuccess } from "@/utils/notify" - -// for check add/edit modals -// pass as an object {editCheck: props.check, initialState: {default form values for adding check} } -export function useCheckModal({ editCheck, initialState }) { - - const check = editCheck - ? ref(Object.assign({}, editCheck)) - : ref(initialState); - - const loading = ref(false) - - // save check function - async function submit(onOk) { - if (check.value.check_type === "cpuload" || check.value.check_type === "memory") { - if (!isValidThreshold(check.value.warning_threshold, check.value.error_threshold)) return; - } - else if (check.value.check_type === "diskspace") { - if (!isValidThreshold(check.value.warning_threshold, check.value.error_threshold, true)) return; - } - - loading.value = true; - try { - const result = editCheck ? await updateCheck(check.value.id, check.value) : await saveCheck(check.value); - notifySuccess(result); - onOk(); - } catch (e) { - console.error(e); - } - loading.value = false; - } - - // dropdown options - const failOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - - const severityOptions = [ - { label: "Informational", value: "info" }, - { label: "Warning", value: "warning" }, - { label: "Error", value: "error" }, - ] - - const logNameOptions = ["Application", "System", "Security"] - const failWhenOptions = [ - { label: "Log contains", value: "contains" }, - { label: "Log does not contain", value: "not_contains" }, - ] - - const diskOptions = ref('A:,B:,C:,D:,E:,F:,G:,H:,I:,J:,K:,L:,M:,N:,O:,P:,Q:,R:,S:,T:,U:,V:,W:,X:,Y:,Z:'.split(',')) - - const serviceOptions = ref(Object.freeze(defaultServiceOptions)) - - async function getAgentDiskOptions() { - const { disks } = await fetchAgent(check.value.agent) - diskOptions.value = disks.map(disk => disk.device) - check.value.disk = diskOptions.value[0] - } - - async function getAgentServiceOptions() { - const { services } = await fetchAgent(check.value.agent) - serviceOptions.value = Object.freeze(services.map(service => ({ label: service.display_name, value: service.name }))) - check.value.svc_name = serviceOptions.value[0].value - check.value.svc_display_name = serviceOptions.value[0].label - } - - onMounted(async () => { - if (!editCheck && check.value.check_type === "diskspace" && check.value.agent) { - await getAgentDiskOptions() - } - - else if (!editCheck && check.value.check_type === "winsvc" && check.value.agent) { - await getAgentServiceOptions() - } - }) - - return { - //data - state: check, - loading, - failOptions, - diskOptions, - logNameOptions, - failWhenOptions, - severityOptions, - serviceOptions, - - // methods - submit - } -} - -export function useCheckDropdown() { - const check = ref(null) - const checks = ref([]) - const checkOptions = ref([]) - - async function getCheckOptions({ agent, policy }, flat = false) { - checkOptions.value = formatCheckOptions(agent ? await fetchAgentChecks(agent) : await fetchPolicyChecks(policy), flat) - } - - return { - //data - check, - checks, - checkOptions, - - //methods - getCheckOptions - } -} - - -export const defaultServiceOptions = [ - { - value: "AJRouter", - label: "AllJoyn Router Service" - }, - { - value: "ALG", - label: "Application Layer Gateway Service" - }, - { - value: "AppIDSvc", - label: "Application Identity" - }, - { - value: "Appinfo", - label: "Application Information" - }, - { - value: "AppMgmt", - label: "Application Management" - }, - { - value: "AppReadiness", - label: "App Readiness" - }, - { - value: "AppVClient", - label: "Microsoft App-V Client" - }, - { - value: "AppXSvc", - label: "AppX Deployment Service (AppXSVC)" - }, - { - value: "AssignedAccessManagerSvc", - label: "AssignedAccessManager Service" - }, - { - value: "atashost", - label: "WebEx Service Host for Support Center" - }, - { - value: "AudioEndpointBuilder", - label: "Windows Audio Endpoint Builder" - }, - { - value: "Audiosrv", - label: "Windows Audio" - }, - { - value: "autotimesvc", - label: "Cellular Time" - }, - { - value: "AxInstSV", - label: "ActiveX Installer (AxInstSV)" - }, - { - value: "BDESVC", - label: "BitLocker Drive Encryption Service" - }, - { - value: "BFE", - label: "Base Filtering Engine" - }, - { - value: "BITS", - label: "Background Intelligent Transfer Service" - }, - { - value: "Bonjour Service", - label: "Bonjour Service" - }, - { - value: "BrokerInfrastructure", - label: "Background Tasks Infrastructure Service" - }, - { - value: "BTAGService", - label: "Bluetooth Audio Gateway Service" - }, - { - value: "BthAvctpSvc", - label: "AVCTP service" - }, - { - value: "bthserv", - label: "Bluetooth Support Service" - }, - { - value: "camsvc", - label: "Capability Access Manager Service" - }, - { - value: "CDPSvc", - label: "Connected Devices Platform Service" - }, - { - value: "CertPropSvc", - label: "Certificate Propagation" - }, - { - value: "ClipSVC", - label: "Client License Service (ClipSVC)" - }, - { - value: "COMSysApp", - label: "COM+ System Application" - }, - { - value: "CoreMessagingRegistrar", - label: "CoreMessaging" - }, - { - value: "cphs", - label: "Intel(R) Content Protection HECI Service" - }, - { - value: "cplspcon", - label: "Intel(R) Content Protection HDCP Service" - }, - { - value: "CryptSvc", - label: "Cryptographic Services" - }, - { - value: "CscService", - label: "Offline Files" - }, - { - value: "DcomLaunch", - label: "DCOM Server Process Launcher" - }, - { - value: "defragsvc", - label: "Optimize drives" - }, - { - value: "DeviceAssociationService", - label: "Device Association Service" - }, - { - value: "DeviceInstall", - label: "Device Install Service" - }, - { - value: "DevQueryBroker", - label: "DevQuery Background Discovery Broker" - }, - { - value: "Dhcp", - label: "DHCP Client" - }, - { - value: "diagnosticshub.standardcollector.service", - label: "Microsoft (R) Diagnostics Hub Standard Collector Service" - }, - { - value: "diagsvc", - label: "Diagnostic Execution Service" - }, - { - value: "DiagTrack", - label: "Connected User Experiences and Telemetry" - }, - { - value: "DispBrokerDesktopSvc", - label: "Display Policy Service" - }, - { - value: "DisplayEnhancementService", - label: "Display Enhancement Service" - }, - { - value: "DmEnrollmentSvc", - label: "Device Management Enrollment Service" - }, - { - value: "dmwappushservice", - label: "Device Management Wireless Application Protocol (WAP) Push message Routing Service" - }, - { - value: "Dnscache", - label: "DNS Client" - }, - { - value: "DoSvc", - label: "Delivery Optimization" - }, - { - value: "dot3svc", - label: "Wired AutoConfig" - }, - { - value: "DPS", - label: "Diagnostic Policy Service" - }, - { - value: "DsmSvc", - label: "Device Setup Manager" - }, - { - value: "DsSvc", - label: "Data Sharing Service" - }, - { - value: "DusmSvc", - label: "Data Usage" - }, - { - value: "Eaphost", - label: "Extensible Authentication Protocol" - }, - { - value: "EFS", - label: "Encrypting File System (EFS)" - }, - { - value: "embeddedmode", - label: "Embedded Mode" - }, - { - value: "EntAppSvc", - label: "Enterprise App Management Service" - }, - { - value: "EventLog", - label: "Windows Event Log" - }, - { - value: "EventSystem", - label: "COM+ Event System" - }, - { - value: "Fax", - label: "Fax" - }, - { - value: "fdPHost", - label: "Function Discovery Provider Host" - }, - { - value: "FDResPub", - label: "Function Discovery Resource Publication" - }, - { - value: "fhsvc", - label: "File History Service" - }, - { - value: "FontCache", - label: "Windows Font Cache Service" - }, - { - value: "FrameServer", - label: "Windows Camera Frame Server" - }, - { - value: "GoogleChromeElevationService", - label: "Google Chrome Elevation Service" - }, - { - value: "gpsvc", - label: "Group Policy Client" - }, - { - value: "GraphicsPerfSvc", - label: "GraphicsPerfSvc" - }, - { - value: "hidserv", - label: "Human Interface Device Service" - }, - { - value: "hns", - label: "Host Network Service" - }, - { - value: "HvHost", - label: "HV Host Service" - }, - { - value: "ibtsiva", - label: "Intel Bluetooth Service" - }, - { - value: "icssvc", - label: "Windows Mobile Hotspot Service" - }, - { - value: "IKEEXT", - label: "IKE and AuthIP IPsec Keying Modules" - }, - { - value: "InstallService", - label: "Microsoft Store Install Service" - }, - { - value: "iphlpsvc", - label: "IP Helper" - }, - { - value: "IpxlatCfgSvc", - label: "IP Translation Configuration Service" - }, - { - value: "KeyIso", - label: "CNG Key Isolation" - }, - { - value: "KtmRm", - label: "KtmRm for Distributed Transaction Coordinator" - }, - { - value: "LanmanServer", - label: "Server" - }, - { - value: "LanmanWorkstation", - label: "Workstation" - }, - { - value: "lfsvc", - label: "Geolocation Service" - }, - { - value: "LicenseManager", - label: "Windows License Manager Service" - }, - { - value: "lltdsvc", - label: "Link-Layer Topology Discovery Mapper" - }, - { - value: "lmhosts", - label: "TCP/IP NetBIOS Helper" - }, - { - value: "LSM", - label: "Local Session Manager" - }, - { - value: "LxpSvc", - label: "Language Experience Service" - }, - { - value: "LxssManager", - label: "LxssManager" - }, - { - value: "MapsBroker", - label: "Downloaded Maps Manager" - }, - { - value: "mpssvc", - label: "Windows Defender Firewall" - }, - { - value: "MSDTC", - label: "Distributed Transaction Coordinator" - }, - { - value: "MSiSCSI", - label: "Microsoft iSCSI Initiator Service" - }, - { - value: "msiserver", - label: "Windows Installer" - }, - { - value: "NaturalAuthentication", - label: "Natural Authentication" - }, - { - value: "NcaSvc", - label: "Network Connectivity Assistant" - }, - { - value: "NcbService", - label: "Network Connection Broker" - }, - { - value: "NcdAutoSetup", - label: "Network Connected Devices Auto-Setup" - }, - { - value: "Net Driver HPZ12", - label: "Net Driver HPZ12" - }, - { - value: "Netlogon", - label: "Netlogon" - }, - { - value: "Netman", - label: "Network Connections" - }, - { - value: "netprofm", - label: "Network List Service" - }, - { - value: "NetSetupSvc", - label: "Network Setup Service" - }, - { - value: "NetTcpPortSharing", - label: "Net.Tcp Port Sharing Service" - }, - { - value: "NgcCtnrSvc", - label: "Microsoft Passport Container" - }, - { - value: "NgcSvc", - label: "Microsoft Passport" - }, - { - value: "NlaSvc", - label: "Network Location Awareness" - }, - { - value: "nsi", - label: "Network Store Interface Service" - }, - { - value: "nvagent", - label: "Network Virtualization Service" - }, - { - value: "OpenVPNService", - label: "OpenVPNService" - }, - { - value: "OpenVPNServiceInteractive", - label: "OpenVPN Interactive Service" - }, - { - value: "OpenVPNServiceLegacy", - label: "OpenVPN Legacy Service" - }, - { - value: "ose64", - label: "Office 64 Source Engine" - }, - { - value: "p2pimsvc", - label: "Peer Networking Identity Manager" - }, - { - value: "p2psvc", - label: "Peer Networking Grouping" - }, - { - value: "PcaSvc", - label: "Program Compatibility Assistant Service" - }, - { - value: "PeerDistSvc", - label: "BranchCache" - }, - { - value: "perceptionsimulation", - label: "Windows Perception Simulation Service" - }, - { - value: "PerfHost", - label: "Performance Counter DLL Host" - }, - { - value: "PhoneSvc", - label: "Phone Service" - }, - { - value: "pla", - label: "Performance Logs & Alerts" - }, - { - value: "PlugPlay", - label: "Plug and Play" - }, - { - value: "Pml Driver HPZ12", - label: "Pml Driver HPZ12" - }, - { - value: "PNRPAutoReg", - label: "PNRP Machine Name Publication Service" - }, - { - value: "PNRPsvc", - label: "Peer Name Resolution Protocol" - }, - { - value: "PolicyAgent", - label: "IPsec Policy Agent" - }, - { - value: "Power", - label: "Power" - }, - { - value: "PrintNotify", - label: "Printer Extensions and Notifications" - }, - { - value: "ProfSvc", - label: "User Profile Service" - }, - { - value: "PushToInstall", - label: "Windows PushToInstall Service" - }, - { - value: "QWAVE", - label: "Quality Windows Audio Video Experience" - }, - { - value: "RasAuto", - label: "Remote Access Auto Connection Manager" - }, - { - value: "RasMan", - label: "Remote Access Connection Manager" - }, - { - value: "RemoteAccess", - label: "Routing and Remote Access" - }, - { - value: "RemoteRegistry", - label: "Remote Registry" - }, - { - value: "RetailDemo", - label: "Retail Demo Service" - }, - { - value: "RmSvc", - label: "Radio Management Service" - }, - { - value: "RpcEptMapper", - label: "RPC Endpoint Mapper" - }, - { - value: "RpcLocator", - label: "Remote Procedure Call (RPC) Locator" - }, - { - value: "RpcSs", - label: "Remote Procedure Call (RPC)" - }, - { - value: "SamSs", - label: "Security Accounts Manager" - }, - { - value: "SCardSvr", - label: "Smart Card" - }, - { - value: "ScDeviceEnum", - label: "Smart Card Device Enumeration Service" - }, - { - value: "Schedule", - label: "Task Scheduler" - }, - { - value: "SCPolicySvc", - label: "Smart Card Removal Policy" - }, - { - value: "SDRSVC", - label: "Windows Backup" - }, - { - value: "seclogon", - label: "Secondary Logon" - }, - { - value: "SecurityHealthService", - label: "Windows Security Service" - }, - { - value: "SEMgrSvc", - label: "Payments and NFC/SE Manager" - }, - { - value: "SENS", - label: "System Event Notification Service" - }, - { - value: "Sense", - label: "Windows Defender Advanced Threat Protection Service" - }, - { - value: "SensorDataService", - label: "Sensor Data Service" - }, - { - value: "SensorService", - label: "Sensor Service" - }, - { - value: "SensrSvc", - label: "Sensor Monitoring Service" - }, - { - value: "SessionEnv", - label: "Remote Desktop Configuration" - }, - { - value: "SgrmBroker", - label: "System Guard Runtime Monitor Broker" - }, - { - value: "SharedAccess", - label: "Internet Connection Sharing (ICS)" - }, - { - value: "SharedRealitySvc", - label: "Spatial Data Service" - }, - { - value: "ShellHWDetection", - label: "Shell Hardware Detection" - }, - { - value: "shpamsvc", - label: "Shared PC Account Manager" - }, - { - value: "smphost", - label: "Microsoft Storage Spaces SMP" - }, - { - value: "SmsRouter", - label: "Microsoft Windows SMS Router Service." - }, - { - value: "SNMPTRAP", - label: "SNMP Trap" - }, - { - value: "spectrum", - label: "Windows Perception Service" - }, - { - value: "Spooler", - label: "Print Spooler" - }, - { - value: "sppsvc", - label: "Software Protection" - }, - { - value: "SSDPSRV", - label: "SSDP Discovery" - }, - { - value: "ssh-agent", - label: "OpenSSH Authentication Agent" - }, - { - value: "sshd", - label: "OpenSSH SSH Server" - }, - { - value: "SstpSvc", - label: "Secure Socket Tunneling Protocol Service" - }, - { - value: "StateRepository", - label: "State Repository Service" - }, - { - value: "stisvc", - label: "Windows Image Acquisition (WIA)" - }, - { - value: "StorSvc", - label: "Storage Service" - }, - { - value: "svsvc", - label: "Spot Verifier" - }, - { - value: "swprv", - label: "Microsoft Software Shadow Copy Provider" - }, - { - value: "SynTPEnhService", - label: "SynTPEnh Caller Service" - }, - { - value: "SysMain", - label: "SysMain" - }, - { - value: "SystemEventsBroker", - label: "System Events Broker" - }, - { - value: "TabletInputService", - label: "Touch Keyboard and Handwriting Panel Service" - }, - { - value: "TapiSrv", - label: "Telephony" - }, - { - value: "TermService", - label: "Remote Desktop Services" - }, - { - value: "Themes", - label: "Themes" - }, - { - value: "TieringEngineService", - label: "Storage Tiers Management" - }, - { - value: "TimeBrokerSvc", - label: "Time Broker" - }, - { - value: "TokenBroker", - label: "Web Account Manager" - }, - { - value: "TrkWks", - label: "Distributed Link Tracking Client" - }, - { - value: "TroubleshootingSvc", - label: "Recommended Troubleshooting Service" - }, - { - value: "TrustedInstaller", - label: "Windows Modules Installer" - }, - { - value: "tzautoupdate", - label: "Auto Time Zone Updater" - }, - { - value: "UevAgentService", - label: "User Experience Virtualization Service" - }, - { - value: "UmRdpService", - label: "Remote Desktop Services UserMode Port Redirector" - }, - { - value: "upnphost", - label: "UPnP Device Host" - }, - { - value: "UserManager", - label: "User Manager" - }, - { - value: "UsoSvc", - label: "Update Orchestrator Service" - }, - { - value: "VacSvc", - label: "Volumetric Audio Compositor Service" - }, - { - value: "VaultSvc", - label: "Credential Manager" - }, - { - value: "vds", - label: "Virtual Disk" - }, - { - value: "vmcompute", - label: "Hyper-V Host Compute Service" - }, - { - value: "vmicguestinterface", - label: "Hyper-V Guest Service Interface" - }, - { - value: "vmicheartbeat", - label: "Hyper-V Heartbeat Service" - }, - { - value: "vmickvpexchange", - label: "Hyper-V Data Exchange Service" - }, - { - value: "vmicrdv", - label: "Hyper-V Remote Desktop Virtualization Service" - }, - { - value: "vmicshutdown", - label: "Hyper-V Guest Shutdown Service" - }, - { - value: "vmictimesync", - label: "Hyper-V Time Synchronization Service" - }, - { - value: "vmicvmsession", - label: "Hyper-V PowerShell Direct Service" - }, - { - value: "vmicvss", - label: "Hyper-V Volume Shadow Copy Requestor" - }, - { - value: "VSS", - label: "Volume Shadow Copy" - }, - { - value: "W32Time", - label: "Windows Time" - }, - { - value: "WaaSMedicSvc", - label: "Windows Update Medic Service" - }, - { - value: "WalletService", - label: "WalletService" - }, - { - value: "WarpJITSvc", - label: "WarpJITSvc" - }, - { - value: "wbengine", - label: "Block Level Backup Engine Service" - }, - { - value: "WbioSrvc", - label: "Windows Biometric Service" - }, - { - value: "Wcmsvc", - label: "Windows Connection Manager" - }, - { - value: "wcncsvc", - label: "Windows Connect Now - Config Registrar" - }, - { - value: "WdiServiceHost", - label: "Diagnostic Service Host" - }, - { - value: "WdiSystemHost", - label: "Diagnostic System Host" - }, - { - value: "WdNisSvc", - label: "Windows Defender Antivirus Network Inspection Service" - }, - { - value: "WebClient", - label: "WebClient" - }, - { - value: "Wecsvc", - label: "Windows Event Collector" - }, - { - value: "WEPHOSTSVC", - label: "Windows Encryption Provider Host Service" - }, - { - value: "wercplsupport", - label: "Problem Reports and Solutions Control Panel Support" - }, - { - value: "WerSvc", - label: "Windows Error Reporting Service" - }, - { - value: "WFDSConMgrSvc", - label: "Wi-Fi Direct Services Connection Manager Service" - }, - { - value: "WiaRpc", - label: "Still Image Acquisition Events" - }, - { - value: "WinDefend", - label: "Windows Defender Antivirus Service" - }, - { - value: "WinHttpAutoProxySvc", - label: "WinHTTP Web Proxy Auto-Discovery Service" - }, - { - value: "Winmgmt", - label: "Windows Management Instrumentation" - }, - { - value: "WinRM", - label: "Windows Remote Management (WS-Management)" - }, - { - value: "wisvc", - label: "Windows Insider Service" - }, - { - value: "WlanSvc", - label: "WLAN AutoConfig" - }, - { - value: "wlidsvc", - label: "Microsoft Account Sign-in Assistant" - }, - { - value: "wlpasvc", - label: "Local Profile Assistant Service" - }, - { - value: "WManSvc", - label: "Windows Management Service" - }, - { - value: "wmiApSrv", - label: "WMI Performance Adapter" - }, - { - value: "WMPNetworkSvc", - label: "Windows Media Player Network Sharing Service" - }, - { - value: "workfolderssvc", - label: "Work Folders" - }, - { - value: "WpcMonSvc", - label: "Parental Controls" - }, - { - value: "WPDBusEnum", - label: "Portable Device Enumerator Service" - }, - { - value: "WpnService", - label: "Windows Push Notifications System Service" - }, - { - value: "wscsvc", - label: "Security Center" - }, - { - value: "WSearch", - label: "Windows Search" - }, - { - value: "wuauserv", - label: "Windows Update" - }, - { - value: "WwanSvc", - label: "WWAN AutoConfig" - }, - { - value: "XblAuthManager", - label: "Xbox Live Auth Manager" - }, - { - value: "XblGameSave", - label: "Xbox Live Game Save" - }, - { - value: "XboxGipSvc", - label: "Xbox Accessory Management Service" - }, - { - value: "XboxNetApiSvc", - label: "Xbox Live Networking Service" - }, - { - value: "YMC", - label: "YMC" - }, - { - value: "AarSvc_35b28a", - label: "Agent Activation Runtime_35b28a" - }, - { - value: "BcastDVRUserService_35b28a", - label: "GameDVR and Broadcast User Service_35b28a" - }, - { - value: "BluetoothUserService_35b28a", - label: "Bluetooth User Support Service_35b28a" - }, - { - value: "CaptureService_35b28a", - label: "CaptureService_35b28a" - }, - { - value: "cbdhsvc_35b28a", - label: "Clipboard User Service_35b28a" - }, - { - value: "CDPUserSvc_35b28a", - label: "Connected Devices Platform User Service_35b28a" - }, - { - value: "ConsentUxUserSvc_35b28a", - label: "ConsentUX_35b28a" - }, - { - value: "CredentialEnrollmentManagerUserSvc_35b28a", - label: "CredentialEnrollmentManagerUserSvc_35b28a" - }, - { - value: "DeviceAssociationBrokerSvc_35b28a", - label: "DeviceAssociationBroker_35b28a" - }, - { - value: "DevicePickerUserSvc_35b28a", - label: "DevicePicker_35b28a" - }, - { - value: "DevicesFlowUserSvc_35b28a", - label: "DevicesFlow_35b28a" - }, - { - value: "LxssManagerUser_35b28a", - label: "LxssManagerUser_35b28a" - }, - { - value: "MessagingService_35b28a", - label: "MessagingService_35b28a" - }, - { - value: "OneSyncSvc_35b28a", - label: "Sync Host_35b28a" - }, - { - value: "PimIndexMaintenanceSvc_35b28a", - label: "Contact Data_35b28a" - }, - { - value: "PrintWorkflowUserSvc_35b28a", - label: "PrintWorkflow_35b28a" - }, - { - value: "UnistoreSvc_35b28a", - label: "User Data Storage_35b28a" - }, - { - value: "UserDataSvc_35b28a", - label: "User Data Access_35b28a" - }, - { - value: "WpnUserService_35b28a", - label: "Windows Push Notifications User Service_35b28a" - }, - { - value: "Mesh Agent", - label: "Mesh Agent background service" - }, - { - value: "salt-minion", - label: "salt-minion" - }, - { - value: "tacticalagent", - label: "Tactical RMM Agent" - } -] \ No newline at end of file diff --git a/web/src/composables/clients.js b/web/src/composables/clients.js deleted file mode 100644 index a09ce6789a..0000000000 --- a/web/src/composables/clients.js +++ /dev/null @@ -1,48 +0,0 @@ - -import { ref, onMounted } from "vue" -import { fetchClients } from "@/api/clients" -import { formatClientOptions, formatSiteOptions } from "@/utils/format" - -export function useClientDropdown(onMount = false) { - const client = ref(null) - const clients = ref([]) - const clientOptions = ref([]) - - async function getClientOptions(flat = false) { - clientOptions.value = formatClientOptions(await fetchClients(), flat) - } - - if (onMount) onMounted(getClientOptions) - - return { - //data - client, - clients, - clientOptions, - - //methods - getClientOptions - } -} - -export function useSiteDropdown(onMount = false) { - const site = ref(null) - const sites = ref([]) - const siteOptions = ref([]) - - async function getSiteOptions() { - siteOptions.value = formatSiteOptions(await fetchClients()) - } - - if (onMount) onMounted(getSiteOptions) - - return { - //data - site, - sites, - siteOptions, - - //methods - getSiteOptions - } -} \ No newline at end of file diff --git a/web/src/composables/core.js b/web/src/composables/core.js deleted file mode 100644 index 9e082d88ba..0000000000 --- a/web/src/composables/core.js +++ /dev/null @@ -1,28 +0,0 @@ - -import { ref, onMounted } from "vue" -import { fetchCustomFields } from "@/api/core" -import { formatCustomFieldOptions } from "@/utils/format" - -export function useCustomFieldDropdown({ onMount = false }) { - - const customFieldOptions = ref([]) - - // type can be "client", "site", or "agent" - async function getCustomFieldOptions(model = null, flat = false) { - - const params = {} - - if (model) params[model] = model - customFieldOptions.value = formatCustomFieldOptions(await fetchCustomFields(params), flat) - } - - if (onMount) onMounted(getCustomFieldOptions) - - return { - //data - customFieldOptions, - - //methods - getCustomFieldOptions - } -} diff --git a/web/src/composables/scripts.js b/web/src/composables/scripts.js deleted file mode 100644 index a26a6f090b..0000000000 --- a/web/src/composables/scripts.js +++ /dev/null @@ -1,56 +0,0 @@ -import { ref, watch, computed, onMounted } from "vue" -import { useStore } from "vuex" -import { fetchScripts } from "@/api/scripts" -import { formatScriptOptions } from "@/utils/format" - -// script dropdown -export function useScriptDropdown(setScript = null, { onMount = false } = {}) { - const scriptOptions = ref([]) - const defaultTimeout = ref(30) - const defaultArgs = ref([]) - const script = ref(setScript) - const syntax = ref("") - const link = ref("") - const baseUrl = "https://github.com/wh1te909/tacticalrmm/blob/master/scripts/" - - // specifing flat returns an array of script names versus {value:id, label: hostname} - async function getScriptOptions(showCommunityScripts = false, flat = false) { - scriptOptions.value = Object.freeze(formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat)) - } - - // watch scriptPk for changes and update the default timeout and args - watch([script, scriptOptions], (newValue, oldValue) => { - if (script.value && scriptOptions.value.length > 0) { - const tmpScript = scriptOptions.value.find(i => i.value === script.value); - defaultTimeout.value = tmpScript.timeout; - defaultArgs.value = tmpScript.args; - syntax.value = tmpScript.syntax - link.value = `${baseUrl}${tmpScript.filename}` - } - }) - - // vuex show community scripts - const store = useStore() - const showCommunityScripts = computed(() => store.state.showCommunityScripts) - - if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value)) - - return { - //data - script, - scriptOptions, - defaultTimeout, - defaultArgs, - syntax, - link, - - //methods - getScriptOptions - } -} - -export const shellOptions = [ - { label: "Powershell", value: "powershell" }, - { label: "Batch", value: "cmd" }, - { label: "Python", value: "python" }, -]; \ No newline at end of file diff --git a/web/src/css/app.sass b/web/src/css/app.sass deleted file mode 100644 index 2280f2ae15..0000000000 --- a/web/src/css/app.sass +++ /dev/null @@ -1 +0,0 @@ -// app global css in Sass form diff --git a/web/src/css/quasar.variables.sass b/web/src/css/quasar.variables.sass deleted file mode 100644 index cf59b8e27d..0000000000 --- a/web/src/css/quasar.variables.sass +++ /dev/null @@ -1,24 +0,0 @@ -// Quasar Sass (& SCSS) Variables -// -------------------------------------------------- -// To customize the look and feel of this app, you can override -// the Sass/SCSS variables found in Quasar's source Sass/SCSS files. - -// Check documentation for full list of Quasar variables - -// Your own variables (that are declared here) and Quasar's own -// ones will be available out of the box in your .vue/.scss/.sass files - -// It's highly recommended to change the default colors -// to match your app's branding. -// Tip: Use the "Theme Builder" on Quasar's documentation website. - -$primary : #1976D2 -$secondary : #26A69A -$accent : #9C27B0 - -$dark : #1D1D1D - -$positive : #21BA45 -$negative : #C10015 -$info : #31CCEC -$warning : #F2C037 diff --git a/web/src/index.template.html b/web/src/index.template.html deleted file mode 100644 index 2e48282117..0000000000 --- a/web/src/index.template.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - <%= productName %> - - - - - - - - - - - -
- - - \ No newline at end of file diff --git a/web/src/mixins/data.js b/web/src/mixins/data.js deleted file mode 100644 index c0379f0907..0000000000 --- a/web/src/mixins/data.js +++ /dev/null @@ -1,64 +0,0 @@ -const scheduledTimes = [ - { label: "12 AM", value: 0 }, - { label: "1 AM", value: 1 }, - { label: "2 AM", value: 2 }, - { label: "3 AM", value: 3 }, - { label: "4 AM", value: 4 }, - { label: "5 AM", value: 5 }, - { label: "6 AM", value: 6 }, - { label: "7 AM", value: 7 }, - { label: "8 AM", value: 8 }, - { label: "9 AM", value: 9 }, - { label: "10 AM", value: 10 }, - { label: "11 AM", value: 11 }, - { label: "12 PM", value: 12 }, - { label: "1 PM", value: 13 }, - { label: "2 PM", value: 14 }, - { label: "3 PM", value: 15 }, - { label: "4 PM", value: 16 }, - { label: "5 PM", value: 17 }, - { label: "6 PM", value: 18 }, - { label: "7 PM", value: 19 }, - { label: "8 PM", value: 20 }, - { label: "9 PM", value: 21 }, - { label: "10 PM", value: 22 }, - { label: "11 PM", value: 23 } -]; - -const monthDays = [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", - "29", - "30", - "31" -] - -const loadingBarColors = ["red", "pink", "purple", "deep-purple", "indigo", "blue", "light-blue", "cyan", "teal", "green", "light-green", "lime", "yellow", "amber", "orange", "deep-orange", "brown", "grey", "blue-grey"] - -export { scheduledTimes, monthDays, loadingBarColors }; diff --git a/web/src/mixins/mixins.js b/web/src/mixins/mixins.js deleted file mode 100644 index cadc0452dc..0000000000 --- a/web/src/mixins/mixins.js +++ /dev/null @@ -1,223 +0,0 @@ -import { Notify, date } from "quasar"; -import axios from 'axios' - -import { formatAgentOptions } from "@/utils/format" - -function getTimeLapse(unixtime) { - var previous = unixtime * 1000; - var current = new Date(); - var msPerMinute = 60 * 1000; - var msPerHour = msPerMinute * 60; - var msPerDay = msPerHour * 24; - var msPerMonth = msPerDay * 30; - var msPerYear = msPerDay * 365; - var elapsed = current - previous; - if (elapsed < msPerMinute) { - return Math.round(elapsed / 1000) + " seconds ago"; - } else if (elapsed < msPerHour) { - return Math.round(elapsed / msPerMinute) + " minutes ago"; - } else if (elapsed < msPerDay) { - return Math.round(elapsed / msPerHour) + " hours ago"; - } else if (elapsed < msPerMonth) { - return Math.round(elapsed / msPerDay) + " days ago"; - } else if (elapsed < msPerYear) { - return Math.round(elapsed / msPerMonth) + " months ago"; - } else { - return Math.round(elapsed / msPerYear) + " years ago"; - } -} - -function appendLeadingZeroes(n) { - if (n <= 9) { - return "0" + n; - } - return n -} - -export default { - methods: { - bootTime(unixtime) { - return getTimeLapse(unixtime); - }, - alertTime(unixtime) { - return getTimeLapse(unixtime); - - }, - notifySuccess(msg, timeout = 2000) { - Notify.create({ - type: "positive", - message: msg, - timeout: timeout - }); - }, - notifyError(msg, timeout = 2000) { - Notify.create({ - type: "negative", - message: msg, - timeout: timeout - }); - }, - notifyWarning(msg, timeout = 2000) { - Notify.create({ - type: "warning", - message: msg, - timeout: timeout - }); - }, - notifyInfo(msg, timeout = 2000) { - Notify.create({ - type: "info", - message: msg, - timeout: timeout - }); - }, - - isValidThreshold(warning, error, diskcheck = false) { - if (warning === 0 && error === 0) { - Notify.create({ type: "negative", timeout: 2000, message: "Warning Threshold or Error Threshold need to be set" }); - return false; - } - - if (!diskcheck && warning > error && warning > 0 && error > 0) { - Notify.create({ type: "negative", timeout: 2000, message: "Warning Threshold must be less than Error Threshold" }); - return false; - } - - if (diskcheck && warning < error && warning > 0 && error > 0) { - Notify.create({ type: "negative", timeout: 2000, message: "Warning Threshold must be more than Error Threshold" }); - return false; - } - - return true; - }, - isValidEmail(val) { - const email = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/; - return email.test(val); - }, - formatDate(date, includeSeconds = false) { - if (!date) return - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - let dt = new Date(date) - let formatted = months[dt.getMonth()] + "-" + appendLeadingZeroes(dt.getDate()) + "-" + appendLeadingZeroes(dt.getFullYear()) + " - " + appendLeadingZeroes(dt.getHours()) + ":" + appendLeadingZeroes(dt.getMinutes()) - - return includeSeconds ? formatted + ":" + appendLeadingZeroes(dt.getSeconds()) : formatted - }, - unixToString(timestamp) { - if (!timestamp) return "" - - let t = new Date(timestamp * 1000) - return date.formatDate(t, 'MMM-D-YYYY - HH:mm') - }, - dateStringToUnix(drfString) { - if (!drfString) return 0; - const d = date.extractDate(drfString, "MM DD YYYY HH:mm"); - return parseInt(date.formatDate(d, "X")); - }, - formatDjangoDate(drfString) { - if (!drfString) return ""; - const d = date.extractDate(drfString, "MM DD YYYY HH:mm"); - return date.formatDate(d, "MMM-DD-YYYY - HH:mm"); - }, - formatClientOptions(clients) { - return clients.map(client => ({ label: client.name, value: client.id, sites: client.sites })) - }, - formatSiteOptions(sites) { - return sites.map(site => ({ label: site.name, value: site.id })); - }, - capitalize(string) { - return string[0].toUpperCase() + string.substring(1); - }, - getCustomFields(model) { - return axios.patch("/core/customfields/", { model: model }) - }, - getAgentCount(data, type, id) { - if (type === "client") { - return data.find(i => id === i.id).agent_count - } else { - const sites = data.map(i => i.sites) - for (let i of sites) { - for (let k of i) { - if (k.id === id) return k.agent_count; - } - } - return 0; - } - }, - formatCustomFields(fields, values) { - let tempArray = []; - - for (let field of fields) { - if (field.type === "multiple") { - tempArray.push({ multiple_value: values[field.name], field: field.id }); - } else if (field.type === "checkbox") { - tempArray.push({ bool_value: values[field.name], field: field.id }); - } else { - tempArray.push({ string_value: values[field.name], field: field.id }); - } - } - return tempArray - }, - async getScriptOptions(showCommunityScripts = false) { - let options = []; - const { data } = await axios.get("/scripts/") - let scripts; - if (showCommunityScripts) { - scripts = data; - } else { - scripts = data.filter(i => i.script_type !== "builtin"); - } - - let categories = []; - let create_unassigned = false - scripts.forEach(script => { - if (!!script.category && !categories.includes(script.category)) { - categories.push(script.category); - } else if (!script.category) { - create_unassigned = true - } - }); - - if (create_unassigned) categories.push("Unassigned") - - categories.sort().forEach(cat => { - options.push({ category: cat }); - let tmp = []; - scripts.forEach(script => { - if (script.category === cat) { - tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args }); - } else if (cat === "Unassigned" && !script.category) { - tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args }); - } - }) - const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label)); - options.push(...sorted); - }); - - return options; - }, - async getAgentOptions(value_field = "agent_id") { - - const { data } = await axios.get("/agents/?detail=false") - - return formatAgentOptions(data, false, value_field) - }, - getNextAgentUpdateTime() { - const d = new Date(); - let ret; - if (d.getMinutes() <= 35) { - ret = d.setMinutes(35); - } else { - ret = date.addToDate(d, { hours: 1 }); - ret.setMinutes(35); - } - const a = date.formatDate(ret, "MMM D, YYYY"); - const b = date.formatDate(ret, "h:mm A"); - return `${a} at ${b}`; - }, - truncateText(txt) { - if (txt) - return txt.length >= 60 ? txt.substring(0, 60) + "..." : txt; - else return "" - }, - } -} diff --git a/web/src/router/index.js b/web/src/router/index.js deleted file mode 100644 index 1d5f4d1f5b..0000000000 --- a/web/src/router/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' -import routes from './routes'; - -export default function ({ store }) { - const createHistory = process.env.SERVER - ? createMemoryHistory - : process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory - - const Router = new createRouter({ - scrollBehavior: () => ({ left: 0, top: 0 }), - routes, - history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE) - }) - - Router.beforeEach((to, from, next) => { - if (to.meta.requireAuth) { - if (!store.getters.loggedIn) { - next({ - name: "Login" - }); - } - else { - next(); - } - } else if (to.meta.requiresVisitor) { - if (store.getters.loggedIn) { - next({ - name: "Dashboard" - }); - } else { - next(); - } - } else { - next(); - } - }); - - return Router -} diff --git a/web/src/router/routes.js b/web/src/router/routes.js deleted file mode 100644 index 3d36372e96..0000000000 --- a/web/src/router/routes.js +++ /dev/null @@ -1,58 +0,0 @@ -const routes = [ - { - path: "/", - name: "Dashboard", - component: () => import("@/views/Dashboard"), - meta: { - requireAuth: true - } - }, - { - path: "/setup", - name: "InitialSetup", - component: () => import("@/views/InitialSetup"), - meta: { - requireAuth: true - } - }, - { - path: "/totp_setup", - name: "TOTPSetup", - component: () => import("@/views/TOTPSetup"), - meta: { - requireAuth: true - } - }, - { - path: "/takecontrol/:agent_id", - name: "TakeControl", - component: () => import("@/views/TakeControl"), - meta: { - requireAuth: true - } - }, - { - path: "/remotebackground/:agent_id", - name: "RemoteBackground", - component: () => import("@/views/RemoteBackground"), - meta: { - requireAuth: true - } - }, - { - path: "/login", - name: "Login", - component: () => import("@/views/Login"), - meta: { - requiresVisitor: true - } - }, - { - path: "/expired", - name: "SessionExpired", - component: () => import("@/views/SessionExpired") - }, - { path: "/:catchAll(.*)*", component: () => import("@/views/NotFound") } -] - -export default routes diff --git a/web/src/store/index.js b/web/src/store/index.js deleted file mode 100644 index dc5a0fabfd..0000000000 --- a/web/src/store/index.js +++ /dev/null @@ -1,255 +0,0 @@ -import { createStore } from 'vuex' -import { Screen } from 'quasar' -import axios from "axios"; - -export default function () { - const Store = new createStore({ - state() { - return { - username: localStorage.getItem("user_name") || null, - token: localStorage.getItem("access_token") || null, - clients: {}, - tree: [], - treeReady: false, - selectedRow: null, - agentTableLoading: false, - needrefresh: false, - tableHeight: "300px", - tabHeight: "300px", - showCommunityScripts: false, - agentDblClickAction: "", - agentUrlAction: null, - defaultAgentTblTab: "server", - clientTreeSort: "alphafail", - clientTreeSplitter: 20, - noCodeSign: false, - hosted: false - } - }, - getters: { - clientTreeSplitterModel(state) { - return state.clientTreeSplitter; - }, - loggedIn(state) { - return state.token !== null; - }, - selectedAgentId(state) { - return state.selectedRow; - }, - showCommunityScripts(state) { - return state.showCommunityScripts; - }, - needRefresh(state) { - return state.needrefresh; - }, - agentTableHeight(state) { - return state.tableHeight; - }, - tabsTableHeight(state) { - return state.tabHeight; - }, - }, - mutations: { - AGENT_TABLE_LOADING(state, visible) { - state.agentTableLoading = visible; - }, - setActiveRow(state, agent_id) { - state.selectedRow = agent_id; - }, - retrieveToken(state, { token, username }) { - state.token = token; - state.username = username; - }, - destroyCommit(state) { - state.token = null; - state.username = null; - }, - getUpdatedSites(state, clients) { - state.clients = clients; - }, - loadTree(state, treebar) { - state.tree = treebar; - state.treeReady = true; - }, - destroySubTable(state) { - state.selectedRow = null; - }, - SET_REFRESH_NEEDED(state, action) { - state.needrefresh = action; - }, - SET_SPLITTER(state, val) { - // top toolbar is 50px. Filebar is 40px and agent filter tabs are 44px - state.tableHeight = `${Screen.height - 50 - 40 - 78 - val}px`; - - // q-tabs are 37px - state.tabHeight = `${val - 37}px`; - }, - SET_CLIENT_SPLITTER(state, val) { - state.clientTreeSplitter = val; - }, - setShowCommunityScripts(state, show) { - state.showCommunityScripts = show - }, - SET_AGENT_DBLCLICK_ACTION(state, action) { - state.agentDblClickAction = action - }, - SET_URL_ACTION(state, action) { - state.agentUrlAction = action - }, - SET_DEFAULT_AGENT_TBL_TAB(state, tab) { - state.defaultAgentTblTab = tab - }, - SET_CLIENT_TREE_SORT(state, val) { - state.clientTreeSort = val - }, - SET_HOSTED(state, val) { - state.hosted = val - } - }, - actions: { - setClientTreeSplitter(context, val) { - axios.patch("/accounts/users/ui/", { client_tree_splitter: Math.trunc(val) }).then(r => { - context.commit("SET_CLIENT_SPLITTER", val) - }) - .catch(e => { }) - }, - setShowCommunityScripts(context, data) { - axios.patch("/accounts/users/ui/", { show_community_scripts: data }).then(r => { - context.commit("setShowCommunityScripts", data) - }) - .catch(e => { }) - }, - getDashInfo(context) { - return axios.get("/core/dashinfo/"); - }, - getUpdatedSites(context) { - axios.get("/clients/").then(r => { - context.commit("getUpdatedSites", r.data); - }) - .catch(e => { }); - }, - loadTree({ commit, state }) { - axios.get("/clients/").then(r => { - - if (r.data.length === 0) { - this.$router.push({ name: "InitialSetup" }); - } - - let output = []; - for (let client of r.data) { - - let childSites = []; - for (let site of client.sites) { - - let siteNode = { - label: site.name, - id: site.id, - raw: `Site|${site.id}`, - header: "generic", - icon: "apartment", - selectable: true, - site: site - } - - if (site.maintenance_mode) { siteNode["color"] = "green" } - else if (site.failing_checks.error) { siteNode["color"] = "negative" } - else if (site.failing_checks.warning) { siteNode["color"] = "warning" } - - childSites.push(siteNode); - } - - let clientNode = { - label: client.name, - id: client.id, - raw: `Client|${client.id}`, - header: "root", - icon: "business", - children: childSites, - client: client - } - - if (client.maintenance_mode) clientNode["color"] = "green" - else if (client.failing_checks.error) { clientNode["color"] = "negative" } - else if (client.failing_checks.warning) { clientNode["color"] = "warning" } - - output.push(clientNode); - } - - - if (state.clientTreeSort === "alphafail") { - // move failing clients to the top - const failing = output.filter(i => i.color === "negative" || i.color === "warning"); - const ok = output.filter(i => i.color !== "negative" && i.color !== "warning"); - const sortedByFailing = [...failing, ...ok]; - commit("loadTree", sortedByFailing); - } else { - commit("loadTree", output); - } - - }) - .catch(e => { - state.treeReady = true - }); - }, - checkVer(context) { - axios.get("/core/version/").then(r => { - const version = r.data; - - if (localStorage.getItem("rmmver")) { - if (localStorage.getItem("rmmver") === version) { - return; - } else { - localStorage.setItem("rmmver", "0.0.1"); - context.commit("SET_REFRESH_NEEDED", true); - } - } else { - localStorage.setItem("rmmver", version); - return; - } - }) - .catch(e => { }) - }, - reload() { - localStorage.removeItem("rmmver"); - location.reload(); - }, - retrieveToken(context, credentials) { - return new Promise((resolve, reject) => { - axios - .post("/login/", credentials) - .then(response => { - const token = response.data.token; - const username = credentials.username; - localStorage.setItem("access_token", token); - localStorage.setItem("user_name", username); - context.commit("retrieveToken", { token, username }); - resolve(response); - }) - .catch(e => { }) - }); - }, - destroyToken(context) { - if (context.getters.loggedIn) { - return new Promise((resolve, reject) => { - axios - .post("/logout/") - .then(response => { - localStorage.removeItem("access_token"); - localStorage.removeItem("user_name"); - context.commit("destroyCommit"); - resolve(response); - }) - .catch(error => { - localStorage.removeItem("access_token"); - localStorage.removeItem("user_name"); - context.commit("destroyCommit"); - }); - }); - } - } - } - }); - - return Store; -} - diff --git a/web/src/store/store-flag.d.ts b/web/src/store/store-flag.d.ts deleted file mode 100644 index ec274bd6d1..0000000000 --- a/web/src/store/store-flag.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// THIS FEATURE-FLAG FILE IS AUTOGENERATED, -// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING -import "quasar/dist/types/feature-flag"; - -declare module "quasar/dist/types/feature-flag" { - interface QuasarFeatureFlags { - store: true; - } -} diff --git a/web/src/utils/csv.js b/web/src/utils/csv.js deleted file mode 100644 index c5f385ccac..0000000000 --- a/web/src/utils/csv.js +++ /dev/null @@ -1,45 +0,0 @@ -import { exportFile, Notify } from "quasar"; - -function _wrapCsvValue(val, formatFn) { - let formatted = formatFn !== void 0 ? formatFn(val) : val; - - formatted = formatted === void 0 || formatted === null ? "" : String(formatted); - - formatted = formatted.split('"').join('""'); - /** - * Excel accepts \n and \r in strings, but some other CSV parsers do not - * Uncomment the next two lines to escape new lines - */ - // .split('\n').join('\\n') - // .split('\r').join('\\r') - - return `"${formatted}"`; -} - -export function exportTableToCSV(rows, columns) { - // naive encoding to csv format - const content = [columns.map(col => _wrapCsvValue(col.label))] - .concat( - rows.map(row => - columns - .map(col => - _wrapCsvValue( - typeof col.field === "function" ? col.field(row) : row[col.field === void 0 ? col.name : col.field], - col.format - ) - ) - .join(",") - ) - ) - .join("\r\n"); - - const status = exportFile("export.csv", content, "text/csv"); - - if (status !== true) { - Notify({ - message: "Browser denied file download...", - color: "negative", - icon: "warning", - }); - } -} \ No newline at end of file diff --git a/web/src/utils/format.js b/web/src/utils/format.js deleted file mode 100644 index 8541d57843..0000000000 --- a/web/src/utils/format.js +++ /dev/null @@ -1,234 +0,0 @@ -import { date } from "quasar"; - -// dropdown options formatting - -function _formatOptions(data, { label, value = "id", flat = false, allowDuplicates = true }) { - if (!flat) - // returns array of options in object format [{label: label, value: 1}] - return data.map(i => ({ label: i[label], value: i[value] })); - else - // returns options as an array of strings ["label", "label1"] - if (!allowDuplicates) - return data.map(i => i[label]); - else { - const options = [] - data.forEach(i => { - if (!options.includes(i[label])) - options.push(i[label]) - }); - return options - } -} - -export function formatScriptOptions(data, flat = false) { - if (flat) { - // returns just script names in array - return _formatOptions(data, { label: "name", value: "pk", flat: true, allowDuplicates: false }) - } else { - - let options = []; - let categories = []; - let create_unassigned = false - data.forEach(script => { - if (!!script.category && !categories.includes(script.category)) { - categories.push(script.category); - } else if (!script.category) { - create_unassigned = true - } - }); - - if (create_unassigned) categories.push("Unassigned") - - categories.sort().forEach(cat => { - options.push({ category: cat }); - let tmp = []; - data.forEach(script => { - if (script.category === cat) { - tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax }); - } else if (cat === "Unassigned" && !script.category) { - tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args, filename: script.filename, syntax: script.syntax }); - } - }) - const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label)); - options.push(...sorted); - }); - - return options; - } -} - -export function formatAgentOptions(data, flat = false, value_field = "agent_id") { - - if (flat) { - // returns just agent hostnames in array - return _formatOptions(data, { label: "hostname", value: value_field, flat: true, allowDuplicates: false }) - } else { - // returns options with categories in object format - let options = [] - const agents = data.map(agent => ({ - label: agent.hostname, - value: agent[value_field], - cat: `${agent.client} > ${agent.site}`, - })); - - let categories = []; - agents.forEach(option => { - if (!categories.includes(option.cat)) { - categories.push(option.cat); - } - }); - - categories.sort().forEach(cat => { - options.push({ category: cat }); - let tmp = [] - agents.forEach(agent => { - if (agent.cat === cat) { - tmp.push(agent); - } - }); - - const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label)); - options.push(...sorted); - }); - - return options - } -} - -export function formatCustomFieldOptions(data, flat = false) { - if (flat) { - return _formatOptions(data, { label: "name", flat: true }) - } - else { - const categories = ["Client", "Site", "Agent"] - const options = [] - - categories.forEach(cat => { - options.push({ category: cat }); - const tmp = []; - data.forEach(custom_field => { - if (custom_field.model === cat.toLowerCase()) { - tmp.push({ label: custom_field.name, value: custom_field.id }) - } - }); - - const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label)); - options.push(...sorted); - }) - - return options - } -} - -export function formatClientOptions(data, flat = false) { - return _formatOptions(data, { label: "name", flat: flat }) -} - -export function formatSiteOptions(data, flat = false) { - const options = [] - - data.forEach(client => { - options.push({ category: client.name }); - options.push(..._formatOptions(client.sites, { label: "name", flat: flat })) - }); - - return options -} - -export function formatUserOptions(data, flat = false) { - return _formatOptions(data, { label: "username", flat: flat }) -} - -export function formatCheckOptions(data, flat = false) { - return _formatOptions(data, { label: "readable_desc", flat: flat }) -} - - -export function formatCustomFields(fields, values) { - let tempArray = []; - - for (let field of fields) { - if (field.type === "multiple") { - tempArray.push({ multiple_value: values[field.name], field: field.id }); - } else if (field.type === "checkbox") { - tempArray.push({ bool_value: values[field.name], field: field.id }); - } else { - tempArray.push({ string_value: values[field.name], field: field.id }); - } - } - return tempArray -} - -export function formatScriptSyntax(syntax) { - let temp = syntax - temp = temp.replaceAll("<", "<").replaceAll(">", ">") - temp = temp.replaceAll("<", `<`).replaceAll(">", `>`) - temp = temp.replaceAll("[", `[`).replaceAll("]", `]`) - temp = temp.replaceAll("(", `(`).replaceAll(")", `)`) - temp = temp.replaceAll("{", `{`).replaceAll("}", `}`) - temp = temp.replaceAll("\n", `
`) - return temp -} - -// date formatting - -export function formatDate(dateString) { - if (!dateString) return ""; - const d = date.extractDate(dateString, "MM DD YYYY HH:mm"); - return date.formatDate(d, "MMM-DD-YYYY - HH:mm"); -} - -export function getNextAgentUpdateTime() { - const d = new Date(); - let ret; - if (d.getMinutes() <= 35) { - ret = d.setMinutes(35); - } else { - ret = date.addToDate(d, { hours: 1 }); - ret.setMinutes(35); - } - const a = date.formatDate(ret, "MMM D, YYYY"); - const b = date.formatDate(ret, "h:mm A"); - return `${a} at ${b}`; -} - -export function dateStringToUnix(drfString) { - if (!drfString) return 0; - const d = date.extractDate(drfString, "MM DD YYYY HH:mm"); - return parseInt(date.formatDate(d, "X")); -} - -// string formatting - -export function capitalize(string) { - return string[0].toUpperCase() + string.substring(1); -} - -export function formatTableColumnText(text) { - - let string = "" - // split at underscore if exists - const words = text.split("_") - words.forEach(word => string = string + " " + capitalize(word)) - - return string.trim() -} - -export function truncateText(txt, chars) { - if (!txt) return - - return txt.length >= chars ? txt.substring(0, chars) + "..." : txt; -} - -export function bytes2Human(bytes) { - if (bytes == 0) return "0B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; -} - -export function convertMemoryToPercent(percent, memory) { - const mb = memory * 1024; - return Math.ceil((percent * mb) / 100).toLocaleString(); -} diff --git a/web/src/utils/notify.js b/web/src/utils/notify.js deleted file mode 100644 index 2d114ef48e..0000000000 --- a/web/src/utils/notify.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Notify } from "quasar"; - -export function notifySuccess(msg, timeout = 2000) { - Notify.create({ - type: "positive", - message: msg, - timeout: timeout - }); -} - -export function notifyError(msg, timeout = 2000) { - Notify.create({ - type: "negative", - message: msg, - timeout: timeout - }); -} - -export function notifyWarning(msg, timeout = 2000) { - Notify.create({ - type: "warning", - message: msg, - timeout: timeout - }); -} - -export function notifyInfo(msg, timeout = 2000) { - Notify.create({ - type: "info", - message: msg, - timeout: timeout - }); -} \ No newline at end of file diff --git a/web/src/utils/validation.js b/web/src/utils/validation.js deleted file mode 100644 index 8f8ce7a6d2..0000000000 --- a/web/src/utils/validation.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Notify } from "quasar"; - -export function isValidThreshold(warning, error, diskcheck = false) { - if (warning === 0 && error === 0) { - Notify.create({ type: "negative", timeout: 2000, message: "Warning Threshold or Error Threshold need to be set" }); - return false; - } - - if (!diskcheck && warning > error && warning > 0 && error > 0) { - Notify.create({ type: "negative", timeout: 2000, message: "Warning Threshold must be less than Error Threshold" }); - return false; - } - - if (diskcheck && warning < error && warning > 0 && error > 0) { - Notify.create({ type: "negative", timeout: 2000, message: "Warning Threshold must be more than Error Threshold" }); - return false; - } - - return true; -} - -export function validateEventID(val) { - if (val === null || val.toString().replace(/\s/g, "") === "") { - return false; - } else if (val === "*") { - return true; - } else if (!isNaN(val)) { - return true; - } else { - return false; - } -} - -// validate script return code -export function validateRetcode(val, done) { - /^\d+$/.test(val) ? done(val) : done(); -} \ No newline at end of file diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue deleted file mode 100644 index 524103ed06..0000000000 --- a/web/src/views/Dashboard.vue +++ /dev/null @@ -1,1023 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/views/InitialSetup.vue b/web/src/views/InitialSetup.vue deleted file mode 100644 index 6a1f08cce5..0000000000 --- a/web/src/views/InitialSetup.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue deleted file mode 100644 index d35ffb5e8f..0000000000 --- a/web/src/views/Login.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - - - diff --git a/web/src/views/NotFound.vue b/web/src/views/NotFound.vue deleted file mode 100644 index 64883c2aea..0000000000 --- a/web/src/views/NotFound.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/views/RemoteBackground.vue b/web/src/views/RemoteBackground.vue deleted file mode 100644 index dda89e0642..0000000000 --- a/web/src/views/RemoteBackground.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/views/SessionExpired.vue b/web/src/views/SessionExpired.vue deleted file mode 100644 index d8808201fd..0000000000 --- a/web/src/views/SessionExpired.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - \ No newline at end of file diff --git a/web/src/views/TOTPSetup.vue b/web/src/views/TOTPSetup.vue deleted file mode 100644 index 2320598012..0000000000 --- a/web/src/views/TOTPSetup.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - diff --git a/web/src/views/TakeControl.vue b/web/src/views/TakeControl.vue deleted file mode 100644 index 73ccd5ae55..0000000000 --- a/web/src/views/TakeControl.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - \ No newline at end of file