Compare commits
	
		
			350 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3588bf827b | ||
|  | d66a41a8a3 | ||
|  | 90914bff14 | ||
|  | 62414848f4 | ||
|  | d4ece6ecd7 | ||
|  | d1ec60bb63 | ||
|  | 4f672c736b | ||
|  | 2e5c351d8b | ||
|  | 3562553346 | ||
|  | 4750b292a5 | ||
|  | 3eb0561e90 | ||
|  | abb118c8ca | ||
|  | 2818a229b6 | ||
|  | a9b8af3677 | ||
|  | 0354da00da | ||
|  | b179587475 | ||
|  | 3021f90bc5 | ||
|  | a14b0278c8 | ||
|  | 80070b333e | ||
|  | 3aa8dcac11 | ||
|  | e920f05611 | ||
|  | 3594afd3aa | ||
|  | 9daaee8212 | ||
|  | d022707349 | ||
|  | 3948605ae6 | ||
|  | f2ded5fdd6 | ||
|  | 00b47be181 | ||
|  | a2fac5d946 | ||
|  | a00b5bb36b | ||
|  | d4fbc34085 | ||
|  | e9e3031992 | ||
|  | c2c7553f56 | ||
|  | 4e60cb89c9 | ||
|  | ec4523240f | ||
|  | 1655ddbcaa | ||
|  | 997c677f30 | ||
|  | d5fc8a2d7e | ||
|  | 3bcd0302a8 | ||
|  | de91b7e8af | ||
|  | 7efd1d7c9e | ||
|  | b5151a2178 | ||
|  | c8432020c6 | ||
|  | 2c9d413a1a | ||
|  | cdf842e7ad | ||
|  | c917007949 | ||
|  | 64278c6b3c | ||
|  | 10a01ed14a | ||
|  | ba3bd1407b | ||
|  | 73666c9a04 | ||
|  | eae24083c9 | ||
|  | a644510c27 | ||
|  | 57859d0da2 | ||
|  | 057f0ff648 | ||
|  | 05d1c867f2 | ||
|  | a2238fa435 | ||
|  | 12b7426a7c | ||
|  | 5148d613a7 | ||
|  | f455c15882 | ||
|  | 618fdabd0e | ||
|  | 3b69e2896c | ||
|  | 7306b63ab1 | ||
|  | 7e3133caa2 | ||
|  | 560901d714 | ||
|  | 166ce9ae78 | ||
|  | d3395a685e | ||
|  | 6d5e9a8566 | ||
|  | 69ec03feb4 | ||
|  | f92982cd5a | ||
|  | 5570f2b464 | ||
|  | ad19dc0240 | ||
|  | 9b1d4faff8 | ||
|  | 76756d20e9 | ||
|  | e564500480 | ||
|  | 19c15ce58d | ||
|  | a027785098 | ||
|  | 36a9f10aae | ||
|  | 99a11a4b53 | ||
|  | 55cac4465c | ||
|  | ff395fd074 | ||
|  | 972b6e09c7 | ||
|  | e793a33b15 | ||
|  | e70d4ff3f3 | ||
|  | cd0635d3a0 | ||
|  | 81702d8595 | ||
|  | aaa4a65b04 | ||
|  | 430797e626 | ||
|  | d454001f49 | ||
|  | bd90ee1f58 | ||
|  | 196aaa5427 | ||
|  | 6e42233b33 | ||
|  | 8e44df8525 | ||
|  | a8a1536941 | ||
|  | 99d1728c70 | ||
|  | 6bbb92cdb9 | ||
|  | b80e7c06bf | ||
|  | bf467b874c | ||
|  | 43c9f6be56 | ||
|  | 6811a4f4ae | ||
|  | 1f16dd9c43 | ||
|  | 63a43ce104 | ||
|  | bd7ce5417e | ||
|  | 941ee54a97 | ||
|  | a5d4a64f47 | ||
|  | d96fcd4a98 | ||
|  | de42e2f747 | ||
|  | 822a93aeb6 | ||
|  | c31b4aaeff | ||
|  | 8c9a386054 | ||
|  | 8c90933615 | ||
|  | 6f8c242333 | ||
|  | fe8b66873a | ||
|  | 00c5f1365a | ||
|  | f7d317328a | ||
|  | 3ccd705225 | ||
|  | 9e439fffaa | ||
|  | 859dc170e7 | ||
|  | 1932d8fad9 | ||
|  | 0c814ae436 | ||
|  | 89313d8a37 | ||
|  | 2b85722222 | ||
|  | 57e5b0188c | ||
|  | 2d7c830e70 | ||
|  | ccaa1790a9 | ||
|  | f6531d905e | ||
|  | 64a31879d3 | ||
|  | 0c6a4b1ed2 | ||
|  | 67801f39fe | ||
|  | 892a0d67bf | ||
|  | 9fc0b7d5cc | ||
|  | 22a614ef54 | ||
|  | cd257b8e4d | ||
|  | fa1ee2ca14 | ||
|  | 34ea1adde6 | ||
|  | 41cf8abb1f | ||
|  | c0ffec1a4c | ||
|  | 65779b8eaf | ||
|  | c47bdb2d56 | ||
|  | d47ae642e7 | ||
|  | 39c4609cc6 | ||
|  | 3ebba02a10 | ||
|  | 4dc7a96e79 | ||
|  | 5a49a29110 | ||
|  | 983a5c2034 | ||
|  | 15829f04a3 | ||
|  | 934618bc1c | ||
|  | 2c5ec75b88 | ||
|  | df11fd744f | ||
|  | 4dba0fb43d | ||
|  | 7a0d86b8dd | ||
|  | a94cd98e0f | ||
|  | 8e95e51edc | ||
|  | 6f1b00284a | ||
|  | 58549a6cac | ||
|  | acc9a6118f | ||
|  | c7811e861c | ||
|  | 55cf766ff0 | ||
|  | a1eaf38324 | ||
|  | c6788092d3 | ||
|  | f89f74ef3f | ||
|  | 3e40f02001 | ||
|  | c169967c1b | ||
|  | 2830e7c569 | ||
|  | 415f08ba3a | ||
|  | d726bcdc19 | ||
|  | f259c25a70 | ||
|  | 4db937cf1f | ||
|  | dad9d0660c | ||
|  | 0c450a5bb2 | ||
|  | ef59819c01 | ||
|  | c651e7c84b | ||
|  | 20b8debb1c | ||
|  | dd5743f0a1 | ||
|  | 7da2b51fae | ||
|  | 0236800392 | ||
|  | 4f822878f7 | ||
|  | c2810e5fe5 | ||
|  | b89ba4b801 | ||
|  | 07c680b839 | ||
|  | fd50db4eab | ||
|  | 0ee95b36a6 | ||
|  | b8cf07149e | ||
|  | 1b699f1a87 | ||
|  | d3bfd238d3 | ||
|  | 1f43abb3c8 | ||
|  | 287c753e4a | ||
|  | 8a5374d31a | ||
|  | e219eaa934 | ||
|  | fd314480ca | ||
|  | dd45396cf3 | ||
|  | 1e2a56c5e9 | ||
|  | 8011773af4 | ||
|  | ddc69c692e | ||
|  | df925c9744 | ||
|  | 1726341aad | ||
|  | 63b1ccc7a7 | ||
|  | ee5db31518 | ||
|  | e80397c857 | ||
|  | 81aa7ca1a4 | ||
|  | f0f7695890 | ||
|  | e7e8ce2f7a | ||
|  | ba37a3f18d | ||
|  | 60b11a7a5d | ||
|  | 29461c20a7 | ||
|  | 2ff1f34543 | ||
|  | b75d7f970f | ||
|  | 204681f097 | ||
|  | e239fe95a4 | ||
|  | 0a101f061a | ||
|  | f112a17afa | ||
|  | 54658a66d2 | ||
|  | 6b8f5a76e4 | ||
|  | 623a5d338d | ||
|  | 9c5565cfd5 | ||
|  | 722f2efaee | ||
|  | 4928264204 | ||
|  | 12d62ddc2a | ||
|  | da54e97217 | ||
|  | 9c0993dac8 | ||
|  | 175486b7c4 | ||
|  | 4760a287f6 | ||
|  | 0237b48c87 | ||
|  | 95c9f22e6c | ||
|  | 9b001219d5 | ||
|  | 6ff15efc7b | ||
|  | 6fe1dccc7e | ||
|  | 1c80f6f3fa | ||
|  | 54d3177fdd | ||
|  | a24ad245d2 | ||
|  | f38cfdcadf | ||
|  | 92e4ad8ccd | ||
|  | 3f3ab088d2 | ||
|  | 2c2cbaa175 | ||
|  | 911b6bf863 | ||
|  | 31462cab64 | ||
|  | 1ee35da62d | ||
|  | edf4815595 | ||
|  | 06ccee5d18 | ||
|  | d5ad85725f | ||
|  | 4d5bddb413 | ||
|  | 2f4da7c381 | ||
|  | 8b845fce03 | ||
|  | 9fd15c38a9 | ||
|  | ec1573d01f | ||
|  | 92ec1cc9e7 | ||
|  | 8b2f9665ce | ||
|  | cb388a5a78 | ||
|  | 7f4389ae08 | ||
|  | 76d71beaa2 | ||
|  | 31bb9c2197 | ||
|  | 6a2cd5c45a | ||
|  | 520632514b | ||
|  | f998b28d0b | ||
|  | 1a6587e9e6 | ||
|  | 9b4b729d19 | ||
|  | e80345295e | ||
|  | 026c259a2e | ||
|  | 63474c2269 | ||
|  | faa1a9312f | ||
|  | 23fa0726d5 | ||
|  | 22210eaf7d | ||
|  | dcd8bee676 | ||
|  | 06f0fa8f0e | ||
|  | 6d0f9e2cd5 | ||
|  | 732afdb65d | ||
|  | 1a9e8742f7 | ||
|  | b8eda37339 | ||
|  | 5107db6169 | ||
|  | 2c8f207454 | ||
|  | 489bc9c3b3 | ||
|  | 514713e883 | ||
|  | 17cc0cd09c | ||
|  | 4475df1295 | ||
|  | fdad267cfd | ||
|  | 3684fc80f0 | ||
|  | e97a5fef94 | ||
|  | de2972631f | ||
|  | e5b8fd67c8 | ||
|  | 5fade89e2d | ||
|  | 2eefedadb3 | ||
|  | e63d7a0b8a | ||
|  | 2a1b1849fa | ||
|  | 0461cb7f19 | ||
|  | 0932e0be03 | ||
|  | 4638ac9474 | ||
|  | d8d7255029 | ||
|  | fa05276c3f | ||
|  | e50a5d51d8 | ||
|  | c03ba78587 | ||
|  | ff07c69e7d | ||
|  | 735b84b26d | ||
|  | 8dd069ad67 | ||
|  | 1857e68003 | ||
|  | ff2508382a | ||
|  | 9cb952b116 | ||
|  | 105e8089bb | ||
|  | 730f37f247 | ||
|  | 284716751f | ||
|  | 8d0db699bf | ||
|  | 53cf1cae58 | ||
|  | 307e4719e0 | ||
|  | 5effae787a | ||
|  | 6532be0b52 | ||
|  | fb225a5347 | ||
|  | b83830a45e | ||
|  | ca28288c33 | ||
|  | b6f8d9cb25 | ||
|  | 9cad0f11e5 | ||
|  | 807be08566 | ||
|  | 67f6a985f8 | ||
|  | f87d54ae8d | ||
|  | d894bf7271 | ||
|  | 56e0e5cace | ||
|  | 685084e784 | ||
|  | cbeec5a973 | ||
|  | 3fff56bcd7 | ||
|  | c504c23eec | ||
|  | 16dae5a655 | ||
|  | e512c5ae7d | ||
|  | 094078b928 | ||
|  | 34fc3ff919 | ||
|  | 4391f48e78 | ||
|  | 775608a3c0 | ||
|  | b326228901 | ||
|  | b2e98173a8 | ||
|  | 65c9b7952c | ||
|  | b9dc9e7d62 | ||
|  | ce178d0354 | ||
|  | a3ff6efebc | ||
|  | 6a9bc56723 | ||
|  | c9ac158d25 | ||
|  | 4b937a0fe8 | ||
|  | 405bf26ac5 | ||
|  | 5dcda0e0a0 | ||
|  | 83e9b60308 | ||
|  | 10b40b4730 | ||
|  | 79d6d804ef | ||
|  | e9c7b6d8f8 | ||
|  | 4fcfbfb3f4 | ||
|  | 30cde14ed3 | ||
|  | cf76e6f538 | ||
|  | d0f600ec8d | ||
|  | 675f9e956f | ||
|  | 381605a6bb | ||
|  | 0fce66062b | ||
|  | 747cc9e5da | ||
|  | 25a1b464da | ||
|  | 3b6738b547 | ||
|  | fc93e3e97f | ||
|  | 0edbb13d48 | ||
|  | 673687341c | 
| @@ -1,4 +1,4 @@ | ||||
| FROM python:3.9.6-slim | ||||
| FROM python:3.9.9-slim | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
| @@ -13,10 +13,6 @@ EXPOSE 8000 8383 8005 | ||||
| RUN groupadd -g 1000 tactical && \ | ||||
|     useradd -u 1000 -g 1000 tactical | ||||
|  | ||||
| # Copy nats-api file | ||||
| COPY natsapi/bin/nats-api /usr/local/bin/ | ||||
| RUN chmod +x /usr/local/bin/nats-api | ||||
|  | ||||
| # Copy dev python reqs | ||||
| COPY .devcontainer/requirements.txt  / | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ services: | ||||
|     build: | ||||
|       context: .. | ||||
|       dockerfile: .devcontainer/api.dockerfile | ||||
|     command: ["tactical-api"] | ||||
|     command: [ "tactical-api" ] | ||||
|     environment: | ||||
|       API_PORT: ${API_PORT} | ||||
|     ports: | ||||
| @@ -18,14 +18,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}" | ||||
|     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 | ||||
| @@ -33,7 +34,7 @@ services: | ||||
|       - "8080:${APP_PORT}" | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases:  | ||||
|         aliases: | ||||
|           - tactical-frontend | ||||
|  | ||||
|   # nats | ||||
| @@ -61,7 +62,7 @@ services: | ||||
|     container_name: trmm-meshcentral-dev | ||||
|     image: ${IMAGE_REPO}tactical-meshcentral:${VERSION} | ||||
|     restart: always | ||||
|     environment:  | ||||
|     environment: | ||||
|       MESH_HOST: ${MESH_HOST} | ||||
|       MESH_USER: ${MESH_USER} | ||||
|       MESH_PASS: ${MESH_PASS} | ||||
| @@ -117,7 +118,7 @@ services: | ||||
|     restart: always | ||||
|     command: redis-server --appendonly yes | ||||
|     image: redis:6.0-alpine | ||||
|     volumes:  | ||||
|     volumes: | ||||
|       - redis-data-dev:/data | ||||
|     networks: | ||||
|       dev: | ||||
| @@ -128,7 +129,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} | ||||
| @@ -153,7 +154,7 @@ services: | ||||
|   celery-dev: | ||||
|     container_name: trmm-celery-dev | ||||
|     image: api-dev | ||||
|     command: ["tactical-celery-dev"] | ||||
|     command: [ "tactical-celery-dev" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - dev | ||||
| @@ -168,7 +169,7 @@ services: | ||||
|   celerybeat-dev: | ||||
|     container_name: trmm-celerybeat-dev | ||||
|     image: api-dev | ||||
|     command: ["tactical-celerybeat-dev"] | ||||
|     command: [ "tactical-celerybeat-dev" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - dev | ||||
| @@ -183,7 +184,7 @@ services: | ||||
|   websockets-dev: | ||||
|     container_name: trmm-websockets-dev | ||||
|     image: api-dev | ||||
|     command: ["tactical-websockets-dev"] | ||||
|     command: [ "tactical-websockets-dev" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       dev: | ||||
| @@ -223,7 +224,7 @@ services: | ||||
|     container_name: trmm-mkdocs-dev | ||||
|     image: api-dev | ||||
|     restart: always | ||||
|     command: ["tactical-mkdocs-dev"] | ||||
|     command: [ "tactical-mkdocs-dev" ] | ||||
|     ports: | ||||
|       - "8005:8005" | ||||
|     volumes: | ||||
| @@ -232,11 +233,11 @@ services: | ||||
|       - 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: | ||||
|   | ||||
| @@ -9,7 +9,8 @@ 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}:443}" | ||||
| : "${MESH_USER:=meshcentral}" | ||||
| : "${MESH_PASS:=meshcentralpass}" | ||||
| : "${MESH_HOST:=tactical-meshcentral}" | ||||
| @@ -20,6 +21,9 @@ set -e | ||||
| : "${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 +41,7 @@ function django_setup { | ||||
|     sleep 5 | ||||
|   done | ||||
|  | ||||
|   until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do | ||||
|   until (echo > /dev/tcp/"${MESH_SERVICE}"/443) &> /dev/null; do | ||||
|     echo "waiting for meshcentral container to be ready..." | ||||
|     sleep 5 | ||||
|   done | ||||
| @@ -56,8 +60,8 @@ DEBUG = True | ||||
|  | ||||
| DOCKER_BUILD = True | ||||
|  | ||||
| CERT_FILE = '/opt/tactical/certs/fullchain.pem' | ||||
| KEY_FILE = '/opt/tactical/certs/privkey.pem' | ||||
| CERT_FILE = '${CERT_PUB_PATH}' | ||||
| KEY_FILE = '${CERT_PRIV_PATH}' | ||||
|  | ||||
| SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts' | ||||
|  | ||||
| @@ -82,6 +86,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 | ||||
| )" | ||||
| @@ -96,7 +101,10 @@ EOF | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py load_chocos | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py reload_nats | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py create_installer_user | ||||
|     "${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 | ||||
|   | ||||
| @@ -4,7 +4,7 @@ celery | ||||
| channels | ||||
| channels_redis | ||||
| django-ipware | ||||
| Django | ||||
| Django==3.2.10 | ||||
| django-cors-headers | ||||
| django-rest-knox | ||||
| djangorestframework | ||||
| @@ -35,3 +35,5 @@ Pygments | ||||
| mypy | ||||
| pysnooper | ||||
| isort | ||||
| drf_spectacular | ||||
| pandas | ||||
|   | ||||
							
								
								
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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', 'javascript', '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 | ||||
							
								
								
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| # This workflow uses actions that are not certified by GitHub. | ||||
| # They are provided by a third-party and are governed by | ||||
| # separate terms of service, privacy policy, and support | ||||
| # documentation. | ||||
|  | ||||
| name: DevSkim | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ develop ] | ||||
|   pull_request: | ||||
|     branches: [ develop ] | ||||
|   schedule: | ||||
|     - cron: '19 5 * * 0' | ||||
|  | ||||
| jobs: | ||||
|   lint: | ||||
|     name: DevSkim | ||||
|     runs-on: ubuntu-20.04 | ||||
|     permissions: | ||||
|       actions: read | ||||
|       contents: read | ||||
|       security-events: write | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Run DevSkim scanner | ||||
|         uses: microsoft/DevSkim-Action@v1 | ||||
|          | ||||
|       - name: Upload DevSkim scan results to GitHub Security tab | ||||
|         uses: github/codeql-action/upload-sarif@v1 | ||||
|         with: | ||||
|           sarif_file: devskim-results.sarif | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -49,3 +49,5 @@ nats-rmm.conf | ||||
| docs/site/ | ||||
| reset_db.sh | ||||
| run_go_cmd.py | ||||
| nats-api.conf | ||||
|  | ||||
|   | ||||
							
								
								
									
										19
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # Security Policy | ||||
|  | ||||
| ## Supported Versions | ||||
|  | ||||
| Use this section to tell people about which versions of your project are | ||||
| currently being supported with security updates. | ||||
|  | ||||
| | Version | Supported          | | ||||
| | ------- | ------------------ | | ||||
| | 0.10.4   | :white_check_mark: | | ||||
| | < 0.10.4| :x:                | | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
| Use this section to tell people how to report a vulnerability. | ||||
|  | ||||
| Tell them where to go, how often they can expect to get an update on a | ||||
| reported vulnerability, what to expect if the vulnerability is accepted or | ||||
| declined, etc. | ||||
| @@ -21,4 +21,6 @@ omit = | ||||
|     */tests.py | ||||
|     */test.py | ||||
|     checks/utils.py | ||||
|     */asgi.py | ||||
|     */demo_views.py | ||||
|      | ||||
|   | ||||
| @@ -6,7 +6,6 @@ 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 | ||||
| @@ -25,11 +24,15 @@ from .serializers import ( | ||||
|  | ||||
|  | ||||
| def _is_root_user(request, user) -> bool: | ||||
|     return ( | ||||
|     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 | ||||
|  | ||||
|  | ||||
| class CheckCreds(KnoxLoginView): | ||||
| @@ -80,6 +83,8 @@ class LoginView(KnoxLoginView): | ||||
|  | ||||
|         if settings.DEBUG and token == "sekret": | ||||
|             valid = True | ||||
|         elif getattr(settings, "DEMO", False): | ||||
|             valid = True | ||||
|         elif totp.verify(token, valid_window=10): | ||||
|             valid = True | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,81 @@ | ||||
| import asyncio | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.utils import AGENT_DEFER, reload_nats | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Delete old agents" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument( | ||||
|             "--days", | ||||
|             type=int, | ||||
|             help="Delete agents that have not checked in for this many days", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--agentver", | ||||
|             type=str, | ||||
|             help="Delete agents that equal to or less than this version", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--delete", | ||||
|             action="store_true", | ||||
|             help="This will delete agents", | ||||
|         ) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         days = kwargs["days"] | ||||
|         agentver = kwargs["agentver"] | ||||
|         delete = kwargs["delete"] | ||||
|  | ||||
|         if not days and not agentver: | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR("Must have at least one parameter: days or agentver") | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         q = Agent.objects.defer(*AGENT_DEFER) | ||||
|  | ||||
|         agents = [] | ||||
|         if days: | ||||
|             overdue = djangotime.now() - djangotime.timedelta(days=days) | ||||
|             agents = [i for i in q if i.last_seen < overdue] | ||||
|  | ||||
|         if agentver: | ||||
|             agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)] | ||||
|  | ||||
|         if not agents: | ||||
|             self.stdout.write(self.style.ERROR("No agents matched")) | ||||
|             return | ||||
|  | ||||
|         deleted_count = 0 | ||||
|         for agent in agents: | ||||
|             s = f"{agent.hostname} | Version {agent.version} | Last Seen {agent.last_seen} | {agent.client} > {agent.site}" | ||||
|             if delete: | ||||
|                 s = "Deleting " + s | ||||
|                 self.stdout.write(self.style.SUCCESS(s)) | ||||
|                 asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) | ||||
|                 try: | ||||
|                     agent.delete() | ||||
|                 except Exception as e: | ||||
|                     err = f"Failed to delete agent {agent.hostname}: {str(e)}" | ||||
|                     self.stdout.write(self.style.ERROR(err)) | ||||
|                 else: | ||||
|                     deleted_count += 1 | ||||
|             else: | ||||
|                 self.stdout.write(self.style.WARNING(s)) | ||||
|  | ||||
|         if delete: | ||||
|             reload_nats() | ||||
|             self.stdout.write(self.style.SUCCESS(f"Deleted {deleted_count} agents")) | ||||
|         else: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS( | ||||
|                     "The above agents would be deleted. Run again with --delete to actually delete them." | ||||
|                 ) | ||||
|             ) | ||||
							
								
								
									
										36
									
								
								api/tacticalrmm/agents/management/commands/demo_cron.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								api/tacticalrmm/agents/management/commands/demo_cron.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| # 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 | ||||
|  | ||||
|  | ||||
| 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) | ||||
|  | ||||
|         """ for _ in range(5): | ||||
|             rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90)) | ||||
|             random_dates.append(rand) """ | ||||
|  | ||||
|         agents = Agent.objects.only("last_seen") | ||||
|         for agent in agents: | ||||
|             agent.last_seen = random.choice(random_dates) | ||||
|             agent.save(update_fields=["last_seen"]) | ||||
							
								
								
									
										668
									
								
								api/tacticalrmm/agents/management/commands/fake_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										668
									
								
								api/tacticalrmm/agents/management/commands/fake_agents.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,668 @@ | ||||
| import json | ||||
| import random | ||||
| import string | ||||
| import datetime as dt | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from clients.models import Client, Site | ||||
| from software.models import InstalledSoftware | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
| from checks.models import Check, CheckHistory | ||||
| from scripts.models import Script | ||||
| from autotasks.models import AutomatedTask | ||||
| from automation.models import Policy | ||||
| from logs.models import PendingAction, AuditLog | ||||
|  | ||||
| from tacticalrmm.demo_data import ( | ||||
|     disks, | ||||
|     temp_dir_stdout, | ||||
|     spooler_stdout, | ||||
|     ping_fail_output, | ||||
|     ping_success_output, | ||||
| ) | ||||
|  | ||||
| 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): | ||||
|         chars = string.ascii_letters | ||||
|         return "".join(random.choice(chars) for _ in range(length)) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         user = User.objects.first() | ||||
|         user.totp_key = "ABSA234234" | ||||
|         user.save(update_fields=["totp_key"]) | ||||
|  | ||||
|         Client.objects.all().delete() | ||||
|         Agent.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() | ||||
|  | ||||
|         Script.load_community_scripts() | ||||
|  | ||||
|         # 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 2", | ||||
|             "Company 3", | ||||
|             "Company 1", | ||||
|             "Company 4", | ||||
|             "Company 5", | ||||
|             "Company 6", | ||||
|         ] | ||||
|         sites1 = ["HQ1", "LA Office 1", "NY Office 1"] | ||||
|         sites2 = ["HQ2", "LA Office 2", "NY Office 2"] | ||||
|         sites3 = ["HQ3", "LA Office 3", "NY Office 3"] | ||||
|         sites4 = ["HQ4", "LA Office 4", "NY Office 4"] | ||||
|         sites5 = ["HQ5", "LA Office 5", "NY Office 5"] | ||||
|         sites6 = ["HQ6", "LA Office 6", "NY Office 6"] | ||||
|  | ||||
|         client1 = Client(name="Company 1") | ||||
|         client2 = Client(name="Company 2") | ||||
|         client3 = Client(name="Company 3") | ||||
|         client4 = Client(name="Company 4") | ||||
|         client5 = Client(name="Company 5") | ||||
|         client6 = Client(name="Company 6") | ||||
|  | ||||
|         client1.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", | ||||
|         ] | ||||
|         descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"] | ||||
|         modes = ["server", "workstation"] | ||||
|         op_systems_servers = [ | ||||
|             "Microsoft Windows Server 2016 Standard, 64bit (build 14393)", | ||||
|             "Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)", | ||||
|             "Microsoft Windows Server 2019 Standard, 64bit (build 17763)", | ||||
|         ] | ||||
|  | ||||
|         op_systems_workstations = [ | ||||
|             "Microsoft Windows 8.1 Pro, 64bit (build 9600)", | ||||
|             "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)", | ||||
|             "Microsoft Windows 10 Pro, 64bit (build 18363)", | ||||
|         ] | ||||
|  | ||||
|         public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"] | ||||
|  | ||||
|         total_rams = [4, 8, 16, 32, 64, 128] | ||||
|         used_rams = [10, 13, 60, 25, 76, 34, 56, 34, 39] | ||||
|  | ||||
|         now = dt.datetime.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 = [] | ||||
|         wmi_details.append(wmi1) | ||||
|         wmi_details.append(wmi2) | ||||
|         wmi_details.append(wmi3) | ||||
|  | ||||
|         # software | ||||
|         with open(SW_1) as f: | ||||
|             software1 = json.load(f) | ||||
|  | ||||
|         with open(SW_2) as f: | ||||
|             software2 = json.load(f) | ||||
|  | ||||
|         softwares = [] | ||||
|         softwares.append(software1) | ||||
|         softwares.append(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 = "cmd" | ||||
|         clear_spool.save() | ||||
|  | ||||
|         check_net_aware = Script() | ||||
|         check_net_aware.name = "Check Network Location Awareness" | ||||
|         check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails." | ||||
|         check_net_aware.filename = "check_network_loc_aware.ps1" | ||||
|         check_net_aware.shell = "powershell" | ||||
|         check_net_aware.save() | ||||
|  | ||||
|         check_pool_health = Script() | ||||
|         check_pool_health.name = "Check storage spool health" | ||||
|         check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy" | ||||
|         check_pool_health.filename = "check_storage_pool_health.ps1" | ||||
|         check_pool_health.shell = "powershell" | ||||
|         check_pool_health.save() | ||||
|  | ||||
|         restart_nla = Script() | ||||
|         restart_nla.name = "Restart NLA Service" | ||||
|         restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails" | ||||
|         restart_nla.filename = "restart_nla.ps1" | ||||
|         restart_nla.shell = "powershell" | ||||
|         restart_nla.save() | ||||
|  | ||||
|         show_tmp_dir_script = Script() | ||||
|         show_tmp_dir_script.name = "Check temp dir" | ||||
|         show_tmp_dir_script.description = "shows files in temp dir using python" | ||||
|         show_tmp_dir_script.filename = "show_temp_dir.py" | ||||
|         show_tmp_dir_script.shell = "python" | ||||
|         show_tmp_dir_script.save() | ||||
|  | ||||
|         for count_agents in range(AGENTS_TO_GENERATE): | ||||
|  | ||||
|             client = random.choice(clients) | ||||
|  | ||||
|             if client == "Company 1": | ||||
|                 site = random.choice(sites1) | ||||
|             elif client == "Company 2": | ||||
|                 site = random.choice(sites2) | ||||
|             elif client == "Company 3": | ||||
|                 site = random.choice(sites3) | ||||
|             elif client == "Company 4": | ||||
|                 site = random.choice(sites4) | ||||
|             elif client == "Company 5": | ||||
|                 site = random.choice(sites5) | ||||
|             elif client == "Company 6": | ||||
|                 site = random.choice(sites6) | ||||
|  | ||||
|             agent = Agent() | ||||
|  | ||||
|             mode = random.choice(modes) | ||||
|             if mode == "server": | ||||
|                 agent.operating_system = random.choice(op_systems_servers) | ||||
|             else: | ||||
|                 agent.operating_system = random.choice(op_systems_workstations) | ||||
|  | ||||
|             agent.hostname = random.choice(hostnames) | ||||
|             agent.version = settings.LATEST_AGENT_VER | ||||
|             agent.salt_ver = "1.1.0" | ||||
|             agent.site = Site.objects.get(name=site) | ||||
|             agent.agent_id = self.rand_string(25) | ||||
|             agent.description = random.choice(descriptions) | ||||
|             agent.monitoring_type = mode | ||||
|             agent.public_ip = random.choice(public_ips) | ||||
|             agent.last_seen = djangotime.now() | ||||
|             agent.plat = "windows" | ||||
|             agent.plat_release = "windows-2019Server" | ||||
|             agent.total_ram = random.choice(total_rams) | ||||
|             agent.used_ram = random.choice(used_rams) | ||||
|             agent.boot_time = random.choice(boot_times) | ||||
|             agent.logged_in_username = random.choice(user_names) | ||||
|             agent.antivirus = "windowsdefender" | ||||
|             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.wmi_detail = random.choice(wmi_details) | ||||
|             agent.services = services | ||||
|             agent.disks = random.choice(disks) | ||||
|             agent.salt_id = "not-used" | ||||
|  | ||||
|             agent.save() | ||||
|  | ||||
|             InstalledSoftware(agent=agent, software=random.choice(softwares)).save() | ||||
|  | ||||
|             if mode == "workstation": | ||||
|                 WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() | ||||
|             else: | ||||
|                 WinUpdatePolicy(agent=agent).save() | ||||
|  | ||||
|             # windows updates load | ||||
|             guids = [] | ||||
|             for k in windows_updates.keys(): | ||||
|                 guids.append(k) | ||||
|  | ||||
|             for i in guids: | ||||
|                 WinUpdate( | ||||
|                     agent=agent, | ||||
|                     guid=i, | ||||
|                     kb=windows_updates[i]["KBs"][0], | ||||
|                     mandatory=windows_updates[i]["Mandatory"], | ||||
|                     title=windows_updates[i]["Title"], | ||||
|                     needs_reboot=windows_updates[i]["NeedsReboot"], | ||||
|                     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 = "cmd_run" | ||||
|             hist.command = "ping google.com" | ||||
|             hist.username = "demo" | ||||
|             hist.results = ping_success_output | ||||
|             hist.save() | ||||
|  | ||||
|             hist1 = AgentHistory() | ||||
|             hist1.agent = agent | ||||
|             hist1.type = "script_run" | ||||
|             hist1.script = clear_spool | ||||
|             hist1.script_results = { | ||||
|                 "id": 1, | ||||
|                 "stderr": "", | ||||
|                 "stdout": spooler_stdout, | ||||
|                 "execution_time": 3.5554593, | ||||
|                 "retcode": 0, | ||||
|             } | ||||
|             hist1.save() | ||||
|  | ||||
|             # disk space check | ||||
|             check1 = Check() | ||||
|             check1.agent = agent | ||||
|             check1.check_type = "diskspace" | ||||
|             check1.status = "passing" | ||||
|             check1.last_run = djangotime.now() | ||||
|             check1.more_info = "Total: 498.7GB, Free: 287.4GB" | ||||
|             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() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check1_history = CheckHistory() | ||||
|                 check1_history.check_id = check1.id | ||||
|                 check1_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check1_history.y = random.randint(13, 40) | ||||
|                 check1_history.save() | ||||
|  | ||||
|             # ping check | ||||
|             check2 = Check() | ||||
|             check2.agent = agent | ||||
|             check2.check_type = "ping" | ||||
|             check2.last_run = djangotime.now() | ||||
|             check2.email_alert = random.choice([True, False]) | ||||
|             check2.text_alert = random.choice([True, False]) | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check2.name = "Synology NAS" | ||||
|                 check2.status = "failing" | ||||
|                 check2.ip = "172.17.14.26" | ||||
|                 check2.more_info = ping_fail_output | ||||
|             else: | ||||
|                 check2.name = "Google" | ||||
|                 check2.status = "passing" | ||||
|                 check2.ip = "8.8.8.8" | ||||
|                 check2.more_info = ping_success_output | ||||
|  | ||||
|             check2.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check2_history = CheckHistory() | ||||
|                 check2_history.check_id = check2.id | ||||
|                 check2_history.x = djangotime.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 = "cpuload" | ||||
|             check3.status = "passing" | ||||
|             check3.last_run = djangotime.now() | ||||
|             check3.warning_threshold = 70 | ||||
|             check3.error_threshold = 90 | ||||
|             check3.history = [15, 23, 16, 22, 22, 27, 15, 23, 23, 20, 10, 10, 13, 34] | ||||
|             check3.email_alert = random.choice([True, False]) | ||||
|             check3.text_alert = random.choice([True, False]) | ||||
|             check3.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check3_history = CheckHistory() | ||||
|                 check3_history.check_id = check3.id | ||||
|                 check3_history.x = djangotime.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 = "memory" | ||||
|             check4.status = "passing" | ||||
|             check4.warning_threshold = 70 | ||||
|             check4.error_threshold = 85 | ||||
|             check4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34] | ||||
|             check4.email_alert = random.choice([True, False]) | ||||
|             check4.text_alert = random.choice([True, False]) | ||||
|             check4.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check4_history = CheckHistory() | ||||
|                 check4_history.check_id = check4.id | ||||
|                 check4_history.x = djangotime.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 = "script" | ||||
|             check5.status = "passing" | ||||
|             check5.last_run = djangotime.now() | ||||
|             check5.email_alert = random.choice([True, False]) | ||||
|             check5.text_alert = random.choice([True, False]) | ||||
|             check5.timeout = 120 | ||||
|             check5.retcode = 0 | ||||
|             check5.execution_time = "4.0000" | ||||
|             check5.script = check_pool_health | ||||
|             check5.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check5_history = CheckHistory() | ||||
|                 check5_history.check_id = check5.id | ||||
|                 check5_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check5_history.y = 1 | ||||
|                 else: | ||||
|                     check5_history.y = 0 | ||||
|                 check5_history.save() | ||||
|  | ||||
|             check6 = Check() | ||||
|             check6.agent = agent | ||||
|             check6.check_type = "script" | ||||
|             check6.status = "passing" | ||||
|             check6.last_run = djangotime.now() | ||||
|             check6.email_alert = random.choice([True, False]) | ||||
|             check6.text_alert = random.choice([True, False]) | ||||
|             check6.timeout = 120 | ||||
|             check6.retcode = 0 | ||||
|             check6.execution_time = "4.0000" | ||||
|             check6.script = check_net_aware | ||||
|             check6.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check6_history = CheckHistory() | ||||
|                 check6_history.check_id = check6.id | ||||
|                 check6_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check6_history.y = 0 | ||||
|                 check6_history.save() | ||||
|  | ||||
|             nla_task = AutomatedTask() | ||||
|             nla_task.agent = agent | ||||
|             nla_task.script = restart_nla | ||||
|             nla_task.assigned_check = check6 | ||||
|             nla_task.name = "Restart NLA" | ||||
|             nla_task.task_type = "checkfailure" | ||||
|             nla_task.win_task_name = "demotask123" | ||||
|             nla_task.execution_time = "1.8443" | ||||
|             nla_task.last_run = djangotime.now() | ||||
|             nla_task.stdout = "no stdout" | ||||
|             nla_task.retcode = 0 | ||||
|             nla_task.sync_status = "synced" | ||||
|             nla_task.save() | ||||
|  | ||||
|             spool_task = AutomatedTask() | ||||
|             spool_task.agent = agent | ||||
|             spool_task.script = clear_spool | ||||
|             spool_task.name = "Clear the print spooler" | ||||
|             spool_task.task_type = "scheduled" | ||||
|             spool_task.run_time_bit_weekdays = 127 | ||||
|             spool_task.run_time_minute = "04:45" | ||||
|             spool_task.win_task_name = "demospool123" | ||||
|             spool_task.last_run = djangotime.now() | ||||
|             spool_task.retcode = 0 | ||||
|             spool_task.stdout = spooler_stdout | ||||
|             spool_task.sync_status = "synced" | ||||
|             spool_task.save() | ||||
|  | ||||
|             tmp_dir_task = AutomatedTask() | ||||
|             tmp_dir_task.agent = agent | ||||
|             tmp_dir_task.name = "show temp dir files" | ||||
|             tmp_dir_task.script = show_tmp_dir_script | ||||
|             tmp_dir_task.task_type = "manual" | ||||
|             tmp_dir_task.win_task_name = "demotemp" | ||||
|             tmp_dir_task.last_run = djangotime.now() | ||||
|             tmp_dir_task.stdout = temp_dir_stdout | ||||
|             tmp_dir_task.retcode = 0 | ||||
|             tmp_dir_task.sync_status = "synced" | ||||
|             tmp_dir_task.save() | ||||
|  | ||||
|             check7 = Check() | ||||
|             check7.agent = agent | ||||
|             check7.check_type = "script" | ||||
|             check7.status = "passing" | ||||
|             check7.last_run = djangotime.now() | ||||
|             check7.email_alert = random.choice([True, False]) | ||||
|             check7.text_alert = random.choice([True, False]) | ||||
|             check7.timeout = 120 | ||||
|             check7.retcode = 0 | ||||
|             check7.execution_time = "3.1337" | ||||
|             check7.script = clear_spool | ||||
|             check7.stdout = spooler_stdout | ||||
|             check7.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check7_history = CheckHistory() | ||||
|                 check7_history.check_id = check7.id | ||||
|                 check7_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check7_history.y = 0 | ||||
|                 check7_history.save() | ||||
|  | ||||
|             check8 = Check() | ||||
|             check8.agent = agent | ||||
|             check8.check_type = "winsvc" | ||||
|             check8.status = "passing" | ||||
|             check8.last_run = djangotime.now() | ||||
|             check8.email_alert = random.choice([True, False]) | ||||
|             check8.text_alert = random.choice([True, False]) | ||||
|             check8.more_info = "Status RUNNING" | ||||
|             check8.fails_b4_alert = 4 | ||||
|             check8.svc_name = "Spooler" | ||||
|             check8.svc_display_name = "Print Spooler" | ||||
|             check8.pass_if_start_pending = False | ||||
|             check8.restart_if_stopped = True | ||||
|             check8.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check8_history = CheckHistory() | ||||
|                 check8_history.check_id = check8.id | ||||
|                 check8_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check8_history.y = 1 | ||||
|                     check8_history.results = "Status STOPPED" | ||||
|                 else: | ||||
|                     check8_history.y = 0 | ||||
|                     check8_history.results = "Status RUNNING" | ||||
|                 check8_history.save() | ||||
|  | ||||
|             check9 = Check() | ||||
|             check9.agent = agent | ||||
|             check9.check_type = "eventlog" | ||||
|             check9.name = "unexpected shutdown" | ||||
|  | ||||
|             check9.last_run = djangotime.now() | ||||
|             check9.email_alert = random.choice([True, False]) | ||||
|             check9.text_alert = random.choice([True, False]) | ||||
|             check9.fails_b4_alert = 2 | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check9.extra_details = eventlog_check_fail_data | ||||
|                 check9.status = "failing" | ||||
|             else: | ||||
|                 check9.extra_details = {"log": []} | ||||
|                 check9.status = "passing" | ||||
|  | ||||
|             check9.log_name = "Application" | ||||
|             check9.event_id = 1001 | ||||
|             check9.event_type = "INFO" | ||||
|             check9.fail_when = "contains" | ||||
|             check9.search_last_days = 30 | ||||
|  | ||||
|             check9.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check9_history = CheckHistory() | ||||
|                 check9_history.check_id = check9.id | ||||
|                 check9_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check9_history.y = 1 | ||||
|                     check9_history.results = "Events Found: 16" | ||||
|                 else: | ||||
|                     check9_history.y = 0 | ||||
|                     check9_history.results = "Events Found: 0" | ||||
|                 check9_history.save() | ||||
|  | ||||
|             pick = random.randint(1, 10) | ||||
|  | ||||
|             if pick == 5 or pick == 3: | ||||
|  | ||||
|                 reboot_time = djangotime.now() + djangotime.timedelta( | ||||
|                     minutes=random.randint(1000, 500000) | ||||
|                 ) | ||||
|                 date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|                 obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|                 task_name = "TacticalRMM_SchedReboot_" + "".join( | ||||
|                     random.choice(string.ascii_letters) for _ in range(10) | ||||
|                 ) | ||||
|  | ||||
|                 sched_reboot = PendingAction() | ||||
|                 sched_reboot.agent = agent | ||||
|                 sched_reboot.action_type = "schedreboot" | ||||
|                 sched_reboot.details = { | ||||
|                     "time": str(obj), | ||||
|                     "taskname": task_name, | ||||
|                 } | ||||
|                 sched_reboot.save() | ||||
|  | ||||
|             self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}")) | ||||
|  | ||||
|         self.stdout.write("done") | ||||
							
								
								
									
										25
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
| 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 | ||||
|  | ||||
|  | ||||
| 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 | ||||
|             return | ||||
|  | ||||
|         q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER) | ||||
|         agent_ids: list[str] = [ | ||||
|             i.agent_id | ||||
|             for i in q | ||||
|             if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|         ] | ||||
|         send_agent_update_task.delay(agent_ids=agent_ids) | ||||
| @@ -18,7 +18,6 @@ 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 packaging import version as pyver | ||||
|  | ||||
| from core.models import TZ_CHOICES, CoreSettings | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| @@ -98,7 +97,7 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|         # 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 site has changed on agent and if so generate policies | ||||
|         # or if agent was changed from server or workstation | ||||
|         if ( | ||||
|             not old_agent | ||||
| @@ -109,10 +108,6 @@ class Agent(BaseAuditModel): | ||||
|         ): | ||||
|             generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True) | ||||
|  | ||||
|         # calculate alert template for new agents | ||||
|         if not old_agent: | ||||
|             self.set_alert_template() | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.hostname | ||||
|  | ||||
| @@ -349,7 +344,7 @@ class Agent(BaseAuditModel): | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         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 | ||||
| @@ -748,8 +743,8 @@ class Agent(BaseAuditModel): | ||||
|                 try: | ||||
|                     ret = msgpack.loads(msg.data)  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     DebugLog.error(agent=self, log_type="agent_issues", message=e) | ||||
|                     ret = str(e) | ||||
|                     DebugLog.error(agent=self, log_type="agent_issues", message=ret) | ||||
|  | ||||
|             await nc.close() | ||||
|             return ret | ||||
| @@ -930,7 +925,7 @@ class AgentCustomField(models.Model): | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.field | ||||
|         return self.field.name | ||||
|  | ||||
|     @property | ||||
|     def value(self): | ||||
|   | ||||
| @@ -38,13 +38,15 @@ class AgentSerializer(serializers.ModelSerializer): | ||||
|     client = serializers.ReadOnlyField(source="client.name") | ||||
|     site_name = serializers.ReadOnlyField(source="site.name") | ||||
|     custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) | ||||
|     patches_last_installed = serializers.ReadOnlyField() | ||||
|     last_seen = serializers.ReadOnlyField() | ||||
|  | ||||
|     def get_all_timezones(self, obj): | ||||
|         return pytz.all_timezones | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         exclude = ["last_seen", "id", "patches_last_installed"] | ||||
|         exclude = ["id"] | ||||
|  | ||||
|  | ||||
| class AgentTableSerializer(serializers.ModelSerializer): | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import random | ||||
| from time import sleep | ||||
| from typing import Union | ||||
|  | ||||
| from alerts.models import Alert | ||||
| from core.models import CoreSettings | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| @@ -12,7 +11,6 @@ from logs.models import DebugLog, PendingAction | ||||
| from packaging import version as pyver | ||||
| from scripts.models import Script | ||||
| from tacticalrmm.celery import app | ||||
| from tacticalrmm.utils import run_nats_api_cmd | ||||
|  | ||||
| from agents.models import Agent | ||||
| from agents.utils import get_winagent_url | ||||
| @@ -80,7 +78,7 @@ def force_code_sign(agent_ids: list[str]) -> None: | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30)) | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
| @@ -268,7 +266,7 @@ def run_script_email_results_task( | ||||
|                 server.send_message(msg) | ||||
|                 server.quit() | ||||
|     except Exception as e: | ||||
|         DebugLog.error(message=e) | ||||
|         DebugLog.error(message=str(e)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| @@ -299,25 +297,6 @@ def clear_faults_task(older_than_days: int) -> None: | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def get_wmi_task() -> None: | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "agent_id", "last_seen", "overdue_time", "offline_time" | ||||
|     ) | ||||
|     ids = [i.agent_id for i in agents if i.status == "online"] | ||||
|     run_nats_api_cmd("wmi", ids, timeout=45) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_checkin_task() -> None: | ||||
|     run_nats_api_cmd("checkin", timeout=30) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_getinfo_task() -> None: | ||||
|     run_nats_api_cmd("agentinfo", timeout=30) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def prune_agent_history(older_than_days: int) -> str: | ||||
|     from .models import AgentHistory | ||||
| @@ -327,45 +306,3 @@ def prune_agent_history(older_than_days: int) -> str: | ||||
|     ).delete() | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_agents_task() -> None: | ||||
|     q = Agent.objects.prefetch_related("pendingactions", "autotasks").only( | ||||
|         "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time" | ||||
|     ) | ||||
|     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 | ||||
|   | ||||
| @@ -306,8 +306,8 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         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) | ||||
| @@ -626,7 +626,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|     @patch("agents.tasks.run_script_email_results_task.delay") | ||||
|     @patch("agents.models.Agent.run_script") | ||||
|     def test_run_script(self, run_script, email_task): | ||||
|         from .models import AgentCustomField, Note | ||||
|         from .models import AgentCustomField, Note, AgentHistory | ||||
|         from clients.models import ClientCustomField, SiteCustomField | ||||
|  | ||||
|         run_script.return_value = "ok" | ||||
| @@ -643,8 +643,9 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() | ||||
|         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_script.reset_mock() | ||||
|  | ||||
| @@ -690,8 +691,9 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() | ||||
|         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_script.reset_mock() | ||||
|  | ||||
| @@ -710,12 +712,13 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() | ||||
|         run_script.assert_called_with( | ||||
|             scriptpk=script.pk, | ||||
|             args=["hello", "world"], | ||||
|             timeout=25, | ||||
|             wait=True, | ||||
|             history_pk=0, | ||||
|             history_pk=hist.pk, | ||||
|         ) | ||||
|         run_script.reset_mock() | ||||
|  | ||||
| @@ -737,12 +740,13 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() | ||||
|         run_script.assert_called_with( | ||||
|             scriptpk=script.pk, | ||||
|             args=["hello", "world"], | ||||
|             timeout=25, | ||||
|             wait=True, | ||||
|             history_pk=0, | ||||
|             history_pk=hist.pk, | ||||
|         ) | ||||
|         run_script.reset_mock() | ||||
|  | ||||
| @@ -766,12 +770,13 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() | ||||
|         run_script.assert_called_with( | ||||
|             scriptpk=script.pk, | ||||
|             args=["hello", "world"], | ||||
|             timeout=25, | ||||
|             wait=True, | ||||
|             history_pk=0, | ||||
|             history_pk=hist.pk, | ||||
|         ) | ||||
|         run_script.reset_mock() | ||||
|  | ||||
| @@ -792,12 +797,13 @@ class TestAgentViews(TacticalTestCase): | ||||
|  | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         hist = AgentHistory.objects.filter(agent=self.agent, script=script).last() | ||||
|         run_script.assert_called_with( | ||||
|             scriptpk=script.pk, | ||||
|             args=["hello", "world"], | ||||
|             timeout=25, | ||||
|             wait=True, | ||||
|             history_pk=0, | ||||
|             history_pk=hist.pk, | ||||
|         ) | ||||
|         run_script.reset_mock() | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,12 @@ from core.models import CoreSettings | ||||
| from logs.models import AuditLog, DebugLog, PendingAction | ||||
| from scripts.models import Script | ||||
| from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task | ||||
| from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats | ||||
| from tacticalrmm.utils import ( | ||||
|     get_default_timezone, | ||||
|     notify_error, | ||||
|     reload_nats, | ||||
|     AGENT_DEFER, | ||||
| ) | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task | ||||
| from tacticalrmm.permissions import ( | ||||
| @@ -74,34 +79,13 @@ class GetAgents(APIView): | ||||
|             or "detail" in request.query_params.keys() | ||||
|             and request.query_params["detail"] == "true" | ||||
|         ): | ||||
|  | ||||
|             agents = ( | ||||
|                 Agent.objects.filter_by_role(request.user) | ||||
|                 Agent.objects.filter_by_role(request.user)  # type: ignore | ||||
|                 .select_related("site", "policy", "alert_template") | ||||
|                 .prefetch_related("agentchecks") | ||||
|                 .filter(filter) | ||||
|                 .only( | ||||
|                     "pk", | ||||
|                     "hostname", | ||||
|                     "agent_id", | ||||
|                     "site", | ||||
|                     "policy", | ||||
|                     "alert_template", | ||||
|                     "monitoring_type", | ||||
|                     "description", | ||||
|                     "needs_reboot", | ||||
|                     "overdue_text_alert", | ||||
|                     "overdue_email_alert", | ||||
|                     "overdue_time", | ||||
|                     "offline_time", | ||||
|                     "last_seen", | ||||
|                     "boot_time", | ||||
|                     "logged_in_username", | ||||
|                     "last_logged_in_user", | ||||
|                     "time_zone", | ||||
|                     "maintenance_mode", | ||||
|                     "pending_actions_count", | ||||
|                     "has_patches_pending", | ||||
|                 ) | ||||
|                 .defer(*AGENT_DEFER) | ||||
|             ) | ||||
|             ctx = {"default_tz": get_default_timezone()} | ||||
|             serializer = AgentTableSerializer(agents, many=True, context=ctx) | ||||
| @@ -109,7 +93,7 @@ class GetAgents(APIView): | ||||
|         # if detail=false | ||||
|         else: | ||||
|             agents = ( | ||||
|                 Agent.objects.filter_by_role(request.user) | ||||
|                 Agent.objects.filter_by_role(request.user)  # type: ignore | ||||
|                 .select_related("site") | ||||
|                 .filter(filter) | ||||
|                 .only("agent_id", "hostname", "site") | ||||
| @@ -125,9 +109,7 @@ class GetUpdateDeleteAgent(APIView): | ||||
|     # get agent details | ||||
|     def get(self, request, agent_id): | ||||
|         agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|         return Response( | ||||
|             AgentSerializer(agent, context={"default_tz": get_default_timezone()}).data | ||||
|         ) | ||||
|         return Response(AgentSerializer(agent).data) | ||||
|  | ||||
|     # edit agent | ||||
|     def put(self, request, agent_id): | ||||
| @@ -185,6 +167,11 @@ 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": | ||||
| @@ -311,6 +298,11 @@ 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 | ||||
|  | ||||
| @@ -343,14 +335,13 @@ def send_raw_cmd(request, agent_id): | ||||
|         }, | ||||
|     } | ||||
|  | ||||
|     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="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)) | ||||
|  | ||||
| @@ -621,15 +612,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="script_run", | ||||
|         script=script, | ||||
|         username=request.user.username[:50], | ||||
|     ) | ||||
|     history_pk = hist.pk | ||||
|  | ||||
|     if output == "wait": | ||||
|         r = agent.run_script( | ||||
|   | ||||
| @@ -456,7 +456,8 @@ class Alert(models.Model): | ||||
|             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 | ||||
| @@ -464,7 +465,7 @@ class Alert(models.Model): | ||||
|                 try: | ||||
|                     temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     DebugLog.error(log_type="scripting", message=e) | ||||
|                     DebugLog.error(log_type="scripting", message=str(e)) | ||||
|                     continue | ||||
|  | ||||
|             else: | ||||
|   | ||||
| @@ -10,12 +10,29 @@ 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) | ||||
|     hostname = SerializerMethodField() | ||||
|     agent_id = SerializerMethodField() | ||||
|     client = SerializerMethodField() | ||||
|     site = SerializerMethodField() | ||||
|     alert_time = SerializerMethodField() | ||||
|     resolve_on = SerializerMethodField() | ||||
|     snoozed_until = SerializerMethodField() | ||||
|  | ||||
|     def get_agent_id(self, instance): | ||||
|         if instance.alert_type == "availability": | ||||
|             return instance.agent.agent_id if instance.agent else "" | ||||
|         elif instance.alert_type == "check": | ||||
|             return ( | ||||
|                 instance.assigned_check.agent.agent_id | ||||
|                 if instance.assigned_check | ||||
|                 else "" | ||||
|             ) | ||||
|         elif instance.alert_type == "task": | ||||
|             return ( | ||||
|                 instance.assigned_task.agent.agent_id if instance.assigned_task else "" | ||||
|             ) | ||||
|         else: | ||||
|             return "" | ||||
|  | ||||
|     def get_hostname(self, instance): | ||||
|         if instance.alert_type == "availability": | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from model_bakery import baker, seq | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from alerts.tasks import cache_agents_alert_template | ||||
| from core.tasks import cache_db_fields_task | ||||
|  | ||||
| from .models import Alert, AlertTemplate | ||||
| from .serializers import ( | ||||
| @@ -676,25 +677,14 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         url = "/api/v3/checkin/" | ||||
|  | ||||
|         agent_template_text.version = settings.LATEST_AGENT_VER | ||||
|         agent_template_text.last_seen = djangotime.now() | ||||
|         agent_template_text.save() | ||||
|  | ||||
|         agent_template_email.version = settings.LATEST_AGENT_VER | ||||
|         agent_template_email.last_seen = djangotime.now() | ||||
|         agent_template_email.save() | ||||
|  | ||||
|         data = { | ||||
|             "agent_id": agent_template_text.agent_id, | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         data = { | ||||
|             "agent_id": agent_template_email.agent_id, | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         cache_db_fields_task() | ||||
|  | ||||
|         recovery_sms.assert_called_with( | ||||
|             pk=Alert.objects.get(agent=agent_template_text).pk | ||||
| @@ -1365,15 +1355,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save() | ||||
|  | ||||
|         url = "/api/v3/checkin/" | ||||
|  | ||||
|         data = { | ||||
|             "agent_id": agent.agent_id, | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         cache_db_fields_task() | ||||
|  | ||||
|         # this is what data should be | ||||
|         data = { | ||||
|   | ||||
| @@ -130,42 +130,6 @@ class TestAPIv3(TacticalTestCase): | ||||
|         self.assertIsInstance(r.json()["check_interval"], int) | ||||
|         self.assertEqual(len(r.json()["checks"]), 15) | ||||
|  | ||||
|     def test_checkin_patch(self): | ||||
|         from logs.models import PendingAction | ||||
|  | ||||
|         url = "/api/v3/checkin/" | ||||
|         agent_updated = baker.make_recipe("agents.agent", version="1.3.0") | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent_updated, | ||||
|             action_type="agentupdate", | ||||
|             details={ | ||||
|                 "url": agent_updated.winagent_dl, | ||||
|                 "version": agent_updated.version, | ||||
|                 "inno": agent_updated.win_inno_exe, | ||||
|             }, | ||||
|         ) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "pending") | ||||
|  | ||||
|         # test agent failed to update and still on same version | ||||
|         payload = { | ||||
|             "func": "hello", | ||||
|             "agent_id": agent_updated.agent_id, | ||||
|             "version": "1.3.0", | ||||
|         } | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "pending") | ||||
|  | ||||
|         # test agent successful update | ||||
|         payload["version"] = settings.LATEST_AGENT_VER | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "completed") | ||||
|         action.delete() | ||||
|  | ||||
|     @patch("apiv3.views.reload_nats") | ||||
|     def test_agent_recovery(self, reload_nats): | ||||
|         reload_nats.return_value = "ok" | ||||
|   | ||||
| @@ -15,15 +15,14 @@ from rest_framework.views import APIView | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from agents.serializers import WinAgentSerializer, AgentHistorySerializer | ||||
| from agents.serializers import AgentHistorySerializer | ||||
| from autotasks.models import AutomatedTask | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | ||||
| from checks.models import Check | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from checks.utils import bytes2human | ||||
| from logs.models import PendingAction, DebugLog | ||||
| from software.models import InstalledSoftware | ||||
| from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats | ||||
| from tacticalrmm.utils import notify_error, reload_nats | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
|  | ||||
|  | ||||
| @@ -32,101 +31,6 @@ class CheckIn(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         """ | ||||
|         !!! DEPRECATED AS OF AGENT 1.6.0 !!! | ||||
|         Endpoint be removed in a future release | ||||
|         """ | ||||
|         from alerts.models import Alert | ||||
|  | ||||
|         updated = False | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         if pyver.parse(request.data["version"]) > pyver.parse( | ||||
|             agent.version | ||||
|         ) or pyver.parse(request.data["version"]) == pyver.parse( | ||||
|             settings.LATEST_AGENT_VER | ||||
|         ): | ||||
|             updated = True | ||||
|         agent.version = request.data["version"] | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["version", "last_seen"]) | ||||
|  | ||||
|         # change agent update pending status to completed if agent has just updated | ||||
|         if ( | ||||
|             updated | ||||
|             and agent.pendingactions.filter(  # type: ignore | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).exists() | ||||
|         ): | ||||
|             agent.pendingactions.filter(  # type: ignore | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).update(status="completed") | ||||
|  | ||||
|         # handles any alerting actions | ||||
|         if Alert.objects.filter(agent=agent, resolved=False).exists(): | ||||
|             Alert.handle_alert_resolve(agent) | ||||
|  | ||||
|         # sync scheduled tasks | ||||
|         if agent.autotasks.exclude(sync_status="synced").exists():  # type: ignore | ||||
|             tasks = agent.autotasks.exclude(sync_status="synced")  # type: ignore | ||||
|  | ||||
|             for task in tasks: | ||||
|                 if task.sync_status == "pendingdeletion": | ||||
|                     task.delete_task_on_agent() | ||||
|                 elif task.sync_status == "initial": | ||||
|                     task.modify_task_on_agent() | ||||
|                 elif task.sync_status == "notsynced": | ||||
|                     task.create_task_on_agent() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|  | ||||
|         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": | ||||
|             raw: SoftwareList = request.data["software"] | ||||
|             if not isinstance(raw, list): | ||||
|                 return notify_error("err") | ||||
|  | ||||
|             sw = filter_software(raw) | ||||
|             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"]) | ||||
| @@ -168,18 +72,18 @@ class WinUpdates(APIView): | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, 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)) | ||||
| @@ -249,14 +153,6 @@ class WinUpdates(APIView): | ||||
|                 ).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") | ||||
|  | ||||
|  | ||||
| @@ -326,8 +222,6 @@ class CheckRunner(APIView): | ||||
|  | ||||
|     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") | ||||
|  | ||||
|         check.last_run = djangotime.now() | ||||
|         check.save(update_fields=["last_run"]) | ||||
| @@ -371,6 +265,13 @@ class TaskRunner(APIView): | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         new_task = serializer.save(last_run=djangotime.now()) | ||||
|  | ||||
|         AgentHistory.objects.create( | ||||
|             agent=agent, | ||||
|             type="task_run", | ||||
|             script=task.script, | ||||
|             script_results=request.data, | ||||
|         ) | ||||
|  | ||||
|         # check if task is a collector and update the custom field | ||||
|         if task.custom_field: | ||||
|             if not task.stderr: | ||||
| @@ -500,11 +401,7 @@ class Software(APIView): | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         raw: SoftwareList = request.data["software"] | ||||
|         if not isinstance(raw, list): | ||||
|             return notify_error("err") | ||||
|  | ||||
|         sw = filter_software(raw) | ||||
|         sw = request.data["software"] | ||||
|         if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|             InstalledSoftware(agent=agent, software=sw).save() | ||||
|         else: | ||||
| @@ -570,7 +467,18 @@ class AgentRecovery(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.prefetch_related("recoveryactions").only( | ||||
|                 "pk", "agent_id", "last_seen" | ||||
|             ), | ||||
|             agent_id=agentid, | ||||
|         ) | ||||
|  | ||||
|         # TODO remove these 2 lines after agent v1.7.0 has been out for a while | ||||
|         # this is handled now by nats-api service | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["last_seen"]) | ||||
|  | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last()  # type: ignore | ||||
|         ret = {"mode": "pass", "shellcmd": ""} | ||||
|         if recovery is None: | ||||
|   | ||||
| @@ -54,6 +54,8 @@ def generate_agent_checks_task( | ||||
|         if create_tasks: | ||||
|             agent.generate_tasks_from_policies() | ||||
|  | ||||
|         agent.set_alert_template() | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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)]), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -3,6 +3,7 @@ import datetime as dt | ||||
| import random | ||||
| import string | ||||
| from typing import List | ||||
| from django.db.models.fields.json import JSONField | ||||
|  | ||||
| import pytz | ||||
| from alerts.models import SEVERITY_CHOICES | ||||
| @@ -10,27 +11,28 @@ from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
| from django.db.models.fields import DateTimeField | ||||
| from django.db.utils import DatabaseError | ||||
| from django.core.validators import MaxValueValidator, MinValueValidator | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| 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"), | ||||
| ] | ||||
| from tacticalrmm.utils import ( | ||||
|     bitdays_to_string, | ||||
|     bitmonthdays_to_string, | ||||
|     bitmonths_to_string, | ||||
|     bitweeks_to_string, | ||||
|     convert_to_iso_duration, | ||||
| ) | ||||
|  | ||||
| TASK_TYPE_CHOICES = [ | ||||
|     ("scheduled", "Scheduled"), | ||||
|     ("daily", "Daily"), | ||||
|     ("weekly", "Weekly"), | ||||
|     ("monthly", "Monthly"), | ||||
|     ("monthlydow", "Monthly Day of Week"), | ||||
|     ("checkfailure", "On Check Failure"), | ||||
|     ("manual", "Manual"), | ||||
|     ("runonce", "Run Once"), | ||||
|     ("scheduled", "Scheduled"),  # deprecated | ||||
| ] | ||||
|  | ||||
| SYNC_STATUS_CHOICES = [ | ||||
| @@ -71,6 +73,8 @@ class AutomatedTask(BaseAuditModel): | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|  | ||||
|     # deprecated | ||||
|     script = models.ForeignKey( | ||||
|         "scripts.Script", | ||||
|         null=True, | ||||
| @@ -78,12 +82,18 @@ class AutomatedTask(BaseAuditModel): | ||||
|         related_name="autoscript", | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     # deprecated | ||||
|     script_args = ArrayField( | ||||
|         models.CharField(max_length=255, null=True, blank=True), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     # deprecated | ||||
|     timeout = models.PositiveIntegerField(blank=True, default=120) | ||||
|  | ||||
|     # format -> {"actions": [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]} | ||||
|     actions = JSONField(default=list) | ||||
|     assigned_check = models.ForeignKey( | ||||
|         "checks.Check", | ||||
|         null=True, | ||||
| @@ -92,26 +102,9 @@ class AutomatedTask(BaseAuditModel): | ||||
|         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) | ||||
| @@ -119,6 +112,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|     execution_time = models.CharField(max_length=100, default="0.0000") | ||||
|     last_run = models.DateTimeField(null=True, blank=True) | ||||
|     enabled = models.BooleanField(default=True) | ||||
|     continue_on_error = models.BooleanField(default=True) | ||||
|     status = models.CharField( | ||||
|         max_length=30, choices=TASK_STATUS_CHOICES, default="pending" | ||||
|     ) | ||||
| @@ -132,33 +126,79 @@ class AutomatedTask(BaseAuditModel): | ||||
|     text_alert = models.BooleanField(default=False) | ||||
|     dashboard_alert = models.BooleanField(default=False) | ||||
|  | ||||
|     # options sent to agent for task creation | ||||
|     # general task settings | ||||
|     task_type = models.CharField( | ||||
|         max_length=100, choices=TASK_TYPE_CHOICES, default="manual" | ||||
|     ) | ||||
|     win_task_name = models.CharField(max_length=255, null=True, blank=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) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         from autotasks.tasks import enable_or_disable_win_task | ||||
|         from autotasks.tasks import modify_win_task | ||||
|         from automation.tasks import update_policy_autotasks_fields_task | ||||
|  | ||||
|         # get old agent 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 fields were updated that require a sync to the agent | ||||
|         update_agent = False | ||||
|         if old_task: | ||||
|             for field in self.fields_that_trigger_task_update_on_agent: | ||||
|                 if getattr(self, field) != getattr(old_task, field): | ||||
|                     update_agent = True | ||||
|                     break | ||||
|  | ||||
|         # check if automated task was enabled/disabled and send celery task | ||||
|         if old_task and old_task.agent and update_agent: | ||||
|             modify_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: | ||||
|                 if getattr(self, field) != getattr(old_task, field): | ||||
|                     update_policy_autotasks_fields_task.delay(task=self.pk) | ||||
|                     break | ||||
|             if update_agent: | ||||
|                 update_policy_autotasks_fields_task.delay( | ||||
|                     task=self.pk, update_agent=update_agent | ||||
|                 ) | ||||
|             else: | ||||
|                 for field in self.policy_fields_to_copy: | ||||
|                     if getattr(self, field) != getattr(old_task, field): | ||||
|                         update_policy_autotasks_fields_task.delay(task=self.pk) | ||||
|                         break | ||||
|  | ||||
|     @property | ||||
|     def schedule(self): | ||||
| @@ -168,13 +208,30 @@ class AutomatedTask(BaseAuditModel): | ||||
|             return "Every time check fails" | ||||
|         elif self.task_type == "runonce": | ||||
|             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 == "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 == "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}" | ||||
|             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 == "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 == "monthlydow": | ||||
|             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}" | ||||
|  | ||||
|     @property | ||||
|     def last_run_as_timezone(self): | ||||
| @@ -193,22 +250,53 @@ class AutomatedTask(BaseAuditModel): | ||||
|             "email_alert", | ||||
|             "text_alert", | ||||
|             "dashboard_alert", | ||||
|             "script", | ||||
|             "script_args", | ||||
|             "assigned_check", | ||||
|             "name", | ||||
|             "run_time_days", | ||||
|             "run_time_minute", | ||||
|             "actions", | ||||
|             "run_time_bit_weekdays", | ||||
|             "run_time_date", | ||||
|             "expire_date", | ||||
|             "daily_interval", | ||||
|             "weekly_interval", | ||||
|             "task_type", | ||||
|             "win_task_name", | ||||
|             "timeout", | ||||
|             "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", | ||||
|         ] | ||||
|  | ||||
|     @property | ||||
|     def fields_that_trigger_task_update_on_agent(self) -> List[str]: | ||||
|         return [ | ||||
|             "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", | ||||
|         ] | ||||
|  | ||||
|     @staticmethod | ||||
| @@ -261,79 +349,161 @@ class AutomatedTask(BaseAuditModel): | ||||
|         if agent: | ||||
|             task.create_task_on_agent() | ||||
|  | ||||
|     # agent version >= 1.8.0 | ||||
|     def generate_nats_task_payload(self, editing=False): | ||||
|         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 != "checkfailure" else "manual", | ||||
|             "multiple_instances": self.task_instance_policy | ||||
|             if self.task_instance_policy | ||||
|             else 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 != "runonce" | ||||
|             else True, | ||||
|         } | ||||
|  | ||||
|         if self.task_type in ["runonce", "daily", "weekly", "monthly", "monthlydow"]: | ||||
|  | ||||
|             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 == "daily": | ||||
|                 task["day_interval"] = self.daily_interval | ||||
|  | ||||
|             elif self.task_type == "weekly": | ||||
|                 task["week_interval"] = self.weekly_interval | ||||
|                 task["days_of_week"] = self.run_time_bit_weekdays | ||||
|  | ||||
|             elif self.task_type == "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 | ||||
|  | ||||
|                 task["months_of_year"] = self.monthly_months_of_year | ||||
|  | ||||
|             elif self.task_type == "monthlydow": | ||||
|                 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): | ||||
|         from agents.models import Agent | ||||
|  | ||||
|         agent = ( | ||||
|             Agent.objects.filter(pk=self.agent.pk) | ||||
|             .only("pk", "version", "hostname", "agent_id") | ||||
|             .first() | ||||
|             .get() | ||||
|         ) | ||||
|  | ||||
|         if self.task_type == "scheduled": | ||||
|         if pyver.parse(agent.version) >= pyver.parse("1.8.0"): | ||||
|             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, | ||||
|                 }, | ||||
|                 "schedtaskpayload": self.generate_nats_task_payload(), | ||||
|             } | ||||
|         else: | ||||
|             return "error" | ||||
|  | ||||
|         r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))  # type: ignore | ||||
|             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) | ||||
|                 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: | ||||
|                     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, | ||||
|                     }, | ||||
|                 } | ||||
|             else: | ||||
|                 return "error" | ||||
|  | ||||
|         r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) | ||||
|  | ||||
|         if r != "ok": | ||||
|             self.sync_status = "initial" | ||||
| @@ -341,7 +511,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             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 | ||||
|                 message=f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in.", | ||||
|             ) | ||||
|             return "timeout" | ||||
|         else: | ||||
| @@ -350,7 +520,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 message=f"{agent.hostname} task {self.name} was successfully created",  # type: ignore | ||||
|                 message=f"{agent.hostname} task {self.name} was successfully created", | ||||
|             ) | ||||
|  | ||||
|         return "ok" | ||||
| @@ -361,17 +531,23 @@ class AutomatedTask(BaseAuditModel): | ||||
|         agent = ( | ||||
|             Agent.objects.filter(pk=self.agent.pk) | ||||
|             .only("pk", "version", "hostname", "agent_id") | ||||
|             .first() | ||||
|             .get() | ||||
|         ) | ||||
|  | ||||
|         nats_data = { | ||||
|             "func": "enableschedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "name": self.win_task_name, | ||||
|                 "enabled": self.enabled, | ||||
|             }, | ||||
|         } | ||||
|         r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))  # type: ignore | ||||
|         if pyver.parse(agent.version) >= pyver.parse("1.8.0"): | ||||
|             nats_data = { | ||||
|                 "func": "schedtask", | ||||
|                 "schedtaskpayload": self.generate_nats_task_payload(editing=True), | ||||
|             } | ||||
|         else: | ||||
|             nats_data = { | ||||
|                 "func": "enableschedtask", | ||||
|                 "schedtaskpayload": { | ||||
|                     "name": self.win_task_name, | ||||
|                     "enabled": self.enabled, | ||||
|                 }, | ||||
|             } | ||||
|         r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) | ||||
|  | ||||
|         if r != "ok": | ||||
|             self.sync_status = "notsynced" | ||||
| @@ -379,7 +555,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             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 | ||||
|                 message=f"Unable to modify scheduled task {self.name} on {agent.hostname}({agent.pk}). It will try again on next agent checkin", | ||||
|             ) | ||||
|             return "timeout" | ||||
|         else: | ||||
| @@ -388,7 +564,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 message=f"{agent.hostname} task {self.name} was successfully modified",  # type: ignore | ||||
|                 message=f"{agent.hostname} task {self.name} was successfully modified", | ||||
|             ) | ||||
|  | ||||
|         return "ok" | ||||
| @@ -399,14 +575,14 @@ class AutomatedTask(BaseAuditModel): | ||||
|         agent = ( | ||||
|             Agent.objects.filter(pk=self.agent.pk) | ||||
|             .only("pk", "version", "hostname", "agent_id") | ||||
|             .first() | ||||
|             .get() | ||||
|         ) | ||||
|  | ||||
|         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(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" | ||||
| @@ -419,7 +595,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             DebugLog.warning( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 message=f"{agent.hostname} task {self.name} will be deleted on next checkin",  # type: ignore | ||||
|                 message=f"{agent.hostname} task {self.name} will be deleted on next checkin", | ||||
|             ) | ||||
|             return "timeout" | ||||
|         else: | ||||
| @@ -427,7 +603,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted",  # type: ignore | ||||
|                 message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted", | ||||
|             ) | ||||
|  | ||||
|         return "ok" | ||||
| @@ -438,10 +614,10 @@ class AutomatedTask(BaseAuditModel): | ||||
|         agent = ( | ||||
|             Agent.objects.filter(pk=self.agent.pk) | ||||
|             .only("pk", "version", "hostname", "agent_id") | ||||
|             .first() | ||||
|             .get() | ||||
|         ) | ||||
|  | ||||
|         asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))  # type: ignore | ||||
|         asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False)) | ||||
|         return "ok" | ||||
|  | ||||
|     def save_collector_results(self): | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| 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 .models import AutomatedTask | ||||
|  | ||||
| @@ -14,6 +11,153 @@ class TaskSerializer(serializers.ModelSerializer): | ||||
|     schedule = serializers.ReadOnlyField() | ||||
|     last_run = serializers.ReadOnlyField(source="last_run_as_timezone") | ||||
|     alert_template = serializers.SerializerMethodField() | ||||
|     run_time_date = serializers.DateTimeField(format="iso-8601", required=False) | ||||
|     expire_date = serializers.DateTimeField( | ||||
|         format="iso-8601", allow_null=True, required=False | ||||
|     ) | ||||
|  | ||||
|     def validate_actions(self, value): | ||||
|  | ||||
|         if not value: | ||||
|             raise serializers.ValidationError( | ||||
|                 f"There must be at least one action configured" | ||||
|             ) | ||||
|  | ||||
|         for action in value: | ||||
|             if "type" not in action: | ||||
|                 raise serializers.ValidationError( | ||||
|                     f"Each action must have a type field of either 'script' or 'cmd'" | ||||
|                 ) | ||||
|  | ||||
|             if action["type"] == "script": | ||||
|                 if "script" not in action: | ||||
|                     raise serializers.ValidationError( | ||||
|                         f"A script action type must have a 'script' field with primary key of script" | ||||
|                     ) | ||||
|  | ||||
|                 if "script_args" not in action: | ||||
|                     raise serializers.ValidationError( | ||||
|                         f"A script action type must have a 'script_args' field with an array of arguments" | ||||
|                     ) | ||||
|  | ||||
|                 if "timeout" not in action: | ||||
|                     raise serializers.ValidationError( | ||||
|                         f"A script action type must have a 'timeout' field" | ||||
|                     ) | ||||
|  | ||||
|             if action["type"] == "cmd": | ||||
|                 if "command" not in action: | ||||
|                     raise serializers.ValidationError( | ||||
|                         f"A command action type must have a 'command' field" | ||||
|                     ) | ||||
|  | ||||
|                 if "timeout" not in action: | ||||
|                     raise serializers.ValidationError( | ||||
|                         f"A command action type must have a 'timeout' field" | ||||
|                     ) | ||||
|  | ||||
|         return value | ||||
|  | ||||
|     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 | ||||
|  | ||||
|         # run_time_date required | ||||
|         if ( | ||||
|             data["task_type"] in ["runonce", "daily", "weekly", "monthly", "monthlydow"] | ||||
|             and not data["run_time_date"] | ||||
|         ): | ||||
|             raise serializers.ValidationError( | ||||
|                 f"run_time_date is required for task_type '{data['task_type']}'" | ||||
|             ) | ||||
|  | ||||
|         # daily task type validation | ||||
|         if data["task_type"] == "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"] == "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"] == "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"] == "monthlydow": | ||||
|             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"] == "checkfailure": | ||||
|             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): | ||||
|  | ||||
| @@ -37,34 +181,46 @@ class TaskSerializer(serializers.ModelSerializer): | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| # below is for the windows agent | ||||
| class TaskRunnerScriptField(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Script | ||||
|         fields = ["id", "filepath", "filename", "shell", "script_type"] | ||||
|  | ||||
|  | ||||
| class TaskRunnerGetSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|     script = TaskRunnerScriptField(read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = AutomatedTask | ||||
|         fields = ["id", "script", "timeout", "enabled", "script_args"] | ||||
|  | ||||
|  | ||||
| class TaskGOGetSerializer(serializers.ModelSerializer): | ||||
|     script = ScriptCheckSerializer(read_only=True) | ||||
|     script_args = serializers.SerializerMethodField() | ||||
|     task_actions = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_script_args(self, obj): | ||||
|         return Script.parse_script_args( | ||||
|             agent=obj.agent, shell=obj.script.shell, args=obj.script_args | ||||
|         ) | ||||
|     def get_task_actions(self, obj): | ||||
|         tmp = [] | ||||
|         for action in obj.actions: | ||||
|             if action["type"] == "cmd": | ||||
|                 tmp.append( | ||||
|                     { | ||||
|                         "type": "cmd", | ||||
|                         "command": Script.parse_script_args( | ||||
|                             agent=obj.agent, | ||||
|                             shell=action["shell"], | ||||
|                             args=[action["command"]], | ||||
|                         )[0], | ||||
|                         "shell": action["shell"], | ||||
|                         "timeout": action["timeout"], | ||||
|                     } | ||||
|                 ) | ||||
|             elif action["type"] == "script": | ||||
|                 script = Script.objects.get(pk=action["script"]) | ||||
|                 tmp.append( | ||||
|                     { | ||||
|                         "type": "script", | ||||
|                         "script_name": script.name, | ||||
|                         "code": script.code, | ||||
|                         "script_args": Script.parse_script_args( | ||||
|                             agent=obj.agent, | ||||
|                             shell=script.shell, | ||||
|                             args=action["script_args"], | ||||
|                         ), | ||||
|                         "shell": script.shell, | ||||
|                         "timeout": action["timeout"], | ||||
|                     } | ||||
|                 ) | ||||
|         return tmp | ||||
|  | ||||
|     class Meta: | ||||
|         model = AutomatedTask | ||||
|         fields = ["id", "script", "timeout", "enabled", "script_args"] | ||||
|         fields = ["id", "continue_on_error", "enabled", "task_actions"] | ||||
|  | ||||
|  | ||||
| class TaskRunnerPatchSerializer(serializers.ModelSerializer): | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import asyncio | ||||
| import datetime as dt | ||||
| from logging import log | ||||
| import random | ||||
| from time import sleep | ||||
| from typing import Union | ||||
| @@ -22,7 +21,7 @@ def create_win_task_schedule(pk): | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def enable_or_disable_win_task(pk): | ||||
| def modify_win_task(pk): | ||||
|     task = AutomatedTask.objects.get(pk=pk) | ||||
|  | ||||
|     task.modify_task_on_agent() | ||||
|   | ||||
| @@ -56,6 +56,12 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         policy = baker.make("automation.Policy") | ||||
|         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,17 +71,164 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         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", | ||||
|             "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 daily task_type to agent | ||||
|         data = { | ||||
|             "agent": agent.agent_id, | ||||
|             "name": "Daily", | ||||
|             "enabled": True, | ||||
|             "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(), | ||||
|             "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(), | ||||
|             "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 | ||||
|         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(), | ||||
|             "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(), | ||||
|             "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, | ||||
|         } | ||||
|  | ||||
| @@ -83,18 +236,15 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         create_win_task_schedule.assert_called() | ||||
|         create_win_task_schedule.reset_mock() | ||||
|  | ||||
|         # test add task to policy | ||||
|         data = { | ||||
|             "policy": policy.id,  # type: ignore | ||||
|             "name": "Test Task Manual", | ||||
|             "run_time_days": [], | ||||
|             "timeout": 120, | ||||
|             "enabled": True, | ||||
|             "script": script.id, | ||||
|             "script_args": None, | ||||
|             "task_type": "manual", | ||||
|             "assigned_check": None, | ||||
|             "actions": actions, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, data, format="json") | ||||
| @@ -120,16 +270,23 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("autotasks.tasks.enable_or_disable_win_task.delay") | ||||
|     @patch("autotasks.tasks.modify_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 | ||||
|         self, update_policy_autotasks_fields_task, modify_win_task | ||||
|     ): | ||||
|         # 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) | ||||
|         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") | ||||
| @@ -137,20 +294,64 @@ class TestAutotaskViews(TacticalTestCase): | ||||
|  | ||||
|         url = f"{base_url}/{agent_task.id}/"  # type: ignore | ||||
|  | ||||
|         # 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 | ||||
|         modify_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 | ||||
|         modify_win_task.assert_called_with(pk=agent_task.id)  # type: ignore | ||||
|         modify_win_task.reset_mock() | ||||
|  | ||||
|         # 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(), | ||||
|             "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) | ||||
|         modify_win_task.assert_called_with(pk=agent_task.id)  # type: ignore | ||||
|         modify_win_task.reset_mock() | ||||
|  | ||||
|         # 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(), | ||||
|             "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, 400) | ||||
|         modify_win_task.assert_not_called  # type: ignore | ||||
|  | ||||
|         # test editing policy tasks | ||||
|         url = f"{base_url}/{policy_task.id}/"  # type: ignore | ||||
|  | ||||
|         # test editing policy task | ||||
| @@ -654,3 +855,9 @@ class TestTaskPermissions(TacticalTestCase): | ||||
|  | ||||
|         self.check_authorized("post", url) | ||||
|         self.check_not_authorized("post", unauthorized_url) | ||||
|  | ||||
|     def test_policy_fields_to_copy_exists(self): | ||||
|         fields = [i.name for i in AutomatedTask._meta.get_fields()] | ||||
|         task = baker.make("autotasks.AutomatedTask") | ||||
|         for i in task.policy_fields_to_copy:  # type: ignore | ||||
|             self.assertIn(i, fields) | ||||
|   | ||||
| @@ -6,7 +6,6 @@ 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 | ||||
| @@ -44,17 +43,10 @@ class GetAddAutoTasks(APIView): | ||||
|  | ||||
|             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, | ||||
|         ) | ||||
|  | ||||
|         if task.agent: | ||||
|   | ||||
| @@ -1,13 +1,9 @@ | ||||
| import json | ||||
| import os | ||||
| import string | ||||
| from statistics import mean | ||||
| from typing import Any | ||||
|  | ||||
| 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.validators import MaxValueValidator, MinValueValidator | ||||
| from django.db import models | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
| from model_bakery import baker | ||||
|  | ||||
| from checks.models import CheckHistory | ||||
| @@ -215,18 +216,12 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|     @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,7 +229,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|         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}") | ||||
| @@ -242,12 +237,12 @@ class TestCheckViews(TacticalTestCase): | ||||
|         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 | ||||
| @@ -1017,7 +1012,8 @@ class TestCheckPermissions(TacticalTestCase): | ||||
|             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") | ||||
| @@ -1096,3 +1092,12 @@ class TestCheckPermissions(TacticalTestCase): | ||||
|  | ||||
|         self.check_authorized("patch", url) | ||||
|         self.check_not_authorized("patch", unauthorized_url) | ||||
|  | ||||
|     def test_policy_fields_to_copy_exists(self): | ||||
|         from .models import Check | ||||
|  | ||||
|         fields = [i.name for i in Check._meta.get_fields()] | ||||
|         check = baker.make("checks.Check") | ||||
|  | ||||
|         for i in check.policy_fields_to_copy:  # type: ignore | ||||
|             self.assertIn(i, fields) | ||||
|   | ||||
| @@ -4,7 +4,6 @@ from datetime import datetime as dt | ||||
| from django.db.models import 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.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| @@ -195,19 +194,15 @@ class GetCheckHistory(APIView): | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @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)) | ||||
|     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") | ||||
|   | ||||
| @@ -0,0 +1,34 @@ | ||||
| # Generated by Django 3.2.10 on 2021-12-26 05:47 | ||||
|  | ||||
| import clients.models | ||||
| from django.db import migrations, 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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -6,6 +6,11 @@ from django.db import models | ||||
| from agents.models import Agent | ||||
| from logs.models import BaseAuditModel | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
| from tacticalrmm.utils import AGENT_DEFER | ||||
|  | ||||
|  | ||||
| def _default_failing_checks_data(): | ||||
|     return {"error": False, "warning": False} | ||||
|  | ||||
|  | ||||
| class Client(BaseAuditModel): | ||||
| @@ -13,6 +18,8 @@ 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) | ||||
|     agent_count = models.PositiveIntegerField(default=0) | ||||
|     workstation_policy = models.ForeignKey( | ||||
|         "automation.Policy", | ||||
|         related_name="workstation_clients", | ||||
| @@ -71,58 +78,18 @@ class Client(BaseAuditModel): | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @property | ||||
|     def agent_count(self) -> int: | ||||
|         return Agent.objects.filter(site__client=self).count() | ||||
|  | ||||
|     @property | ||||
|     def has_maintenanace_mode_agents(self): | ||||
|         return ( | ||||
|             Agent.objects.filter(site__client=self, maintenance_mode=True).count() > 0 | ||||
|             Agent.objects.defer(*AGENT_DEFER) | ||||
|             .filter(site__client=self, maintenance_mode=True) | ||||
|             .count() | ||||
|             > 0 | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def has_failing_checks(self): | ||||
|         agents = ( | ||||
|             Agent.objects.only( | ||||
|                 "pk", | ||||
|                 "overdue_email_alert", | ||||
|                 "overdue_text_alert", | ||||
|                 "last_seen", | ||||
|                 "overdue_time", | ||||
|                 "offline_time", | ||||
|             ) | ||||
|             .filter(site__client=self) | ||||
|             .prefetch_related("agentchecks", "autotasks") | ||||
|         ) | ||||
|  | ||||
|         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 Agent.objects.defer(*AGENT_DEFER).filter(site__client=self).count() | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(client): | ||||
| @@ -138,6 +105,8 @@ 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) | ||||
|     agent_count = models.PositiveIntegerField(default=0) | ||||
|     workstation_policy = models.ForeignKey( | ||||
|         "automation.Policy", | ||||
|         related_name="workstation_sites", | ||||
| @@ -192,55 +161,13 @@ class Site(BaseAuditModel): | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @property | ||||
|     def agent_count(self) -> int: | ||||
|         return Agent.objects.filter(site=self).count() | ||||
|  | ||||
|     @property | ||||
|     def has_maintenanace_mode_agents(self): | ||||
|         return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0 | ||||
|         return self.agents.defer(*AGENT_DEFER).filter(maintenance_mode=True).count() > 0  # type: ignore | ||||
|  | ||||
|     @property | ||||
|     def has_failing_checks(self): | ||||
|         agents = ( | ||||
|             Agent.objects.only( | ||||
|                 "pk", | ||||
|                 "overdue_email_alert", | ||||
|                 "overdue_text_alert", | ||||
|                 "last_seen", | ||||
|                 "overdue_time", | ||||
|                 "offline_time", | ||||
|             ) | ||||
|             .filter(site=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): | ||||
|   | ||||
| @@ -30,9 +30,7 @@ class SiteCustomFieldSerializer(ModelSerializer): | ||||
| class SiteSerializer(ModelSerializer): | ||||
|     client_name = ReadOnlyField(source="client.name") | ||||
|     custom_fields = SiteCustomFieldSerializer(many=True, read_only=True) | ||||
|     agent_count = ReadOnlyField() | ||||
|     maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents") | ||||
|     failing_checks = ReadOnlyField(source="has_failing_checks") | ||||
|  | ||||
|     class Meta: | ||||
|         model = Site | ||||
| @@ -94,9 +92,7 @@ class ClientCustomFieldSerializer(ModelSerializer): | ||||
| class ClientSerializer(ModelSerializer): | ||||
|     sites = SerializerMethodField() | ||||
|     custom_fields = ClientCustomFieldSerializer(many=True, read_only=True) | ||||
|     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( | ||||
|   | ||||
| @@ -126,15 +126,16 @@ class GetUpdateDeleteClient(APIView): | ||||
|         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" | ||||
|             ) | ||||
| @@ -230,13 +231,14 @@ class GetUpdateDeleteSite(APIView): | ||||
|             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" | ||||
|             ) | ||||
|   | ||||
| @@ -0,0 +1,24 @@ | ||||
| import os | ||||
| import json | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.conf import settings | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Generate conf for nats-api" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         db = settings.DATABASES["default"] | ||||
|         config = { | ||||
|             "key": settings.SECRET_KEY, | ||||
|             "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", | ||||
|             "user": db["USER"], | ||||
|             "pass": db["PASSWORD"], | ||||
|             "host": db["HOST"], | ||||
|             "port": int(db["PORT"]), | ||||
|             "dbname": db["NAME"], | ||||
|         } | ||||
|         conf = os.path.join(settings.BASE_DIR, "nats-api.conf") | ||||
|         with open(conf, "w") as f: | ||||
|             json.dump(config, f) | ||||
| @@ -17,8 +17,7 @@ class Command(BaseCommand): | ||||
|         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}" | ||||
|             uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" | ||||
|         else: | ||||
|             site = mesh_settings.mesh_site.replace("https", "wss") | ||||
|             uri = f"{site}/control.ashx?auth={token}" | ||||
|   | ||||
| @@ -18,8 +18,7 @@ class Command(BaseCommand): | ||||
|         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}" | ||||
|             uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" | ||||
|         else: | ||||
|             site = mesh_settings.mesh_site.replace("https", "wss") | ||||
|             uri = f"{site}/control.ashx?auth={token}" | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| from scripts.models import Script | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.conf import settings | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "loads scripts for demo" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         scripts_dir = settings.BASE_DIR.joinpath("tacticalrmm/test_data/demo_scripts") | ||||
|         scripts = Script.objects.filter(script_type="userdefined") | ||||
|         for script in scripts: | ||||
|             filepath = scripts_dir.joinpath(script.filename) | ||||
|             with open(filepath, "rb") as f: | ||||
|                 script.script_body = f.read().decode("utf-8") | ||||
|                 script.save(update_fields=["script_body"]) | ||||
|  | ||||
|         self.stdout.write(self.style.SUCCESS("Added userdefined scripts")) | ||||
| @@ -1,7 +1,11 @@ | ||||
| import base64 | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils.timezone import make_aware | ||||
| import datetime as dt | ||||
|  | ||||
| from logs.models import PendingAction | ||||
| from scripts.models import Script | ||||
| from autotasks.models import AutomatedTask | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| @@ -20,3 +24,44 @@ class Command(BaseCommand): | ||||
|             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="builtin").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"]) | ||||
|  | ||||
|         # convert autotask to the new format | ||||
|         for task in AutomatedTask.objects.all(): | ||||
|             edited = False | ||||
|  | ||||
|             # convert scheduled task_type | ||||
|             if task.task_type == "scheduled": | ||||
|                 task.task_type = "daily" | ||||
|                 task.run_time_date = make_aware( | ||||
|                     dt.datetime.strptime(task.run_time_minute, "%H:%M") | ||||
|                 ) | ||||
|                 task.daily_interval = 1 | ||||
|                 edited = True | ||||
|  | ||||
|             # convert actions | ||||
|             if not task.actions: | ||||
|                 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() | ||||
|   | ||||
| @@ -119,7 +119,6 @@ class CoreSettings(BaseAuditModel): | ||||
|     def sms_is_configured(self): | ||||
|         return all( | ||||
|             [ | ||||
|                 self.sms_alert_recipients, | ||||
|                 self.twilio_auth_token, | ||||
|                 self.twilio_account_sid, | ||||
|                 self.twilio_number, | ||||
| @@ -131,7 +130,6 @@ class CoreSettings(BaseAuditModel): | ||||
|         # smtp with username/password authentication | ||||
|         if ( | ||||
|             self.smtp_requires_auth | ||||
|             and self.email_alert_recipients | ||||
|             and self.smtp_from_email | ||||
|             and self.smtp_host | ||||
|             and self.smtp_host_user | ||||
| @@ -142,7 +140,6 @@ class CoreSettings(BaseAuditModel): | ||||
|         # smtp relay | ||||
|         elif ( | ||||
|             not self.smtp_requires_auth | ||||
|             and self.email_alert_recipients | ||||
|             and self.smtp_from_email | ||||
|             and self.smtp_host | ||||
|             and self.smtp_port | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import pytz | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from autotasks.models import AutomatedTask | ||||
| from autotasks.tasks import delete_win_task_schedule | ||||
| @@ -9,6 +11,10 @@ from alerts.tasks import prune_resolved_alerts | ||||
| from core.models import CoreSettings | ||||
| from logs.tasks import prune_debug_log, prune_audit_log | ||||
| from tacticalrmm.celery import app | ||||
| from tacticalrmm.utils import AGENT_DEFER | ||||
| from agents.models import Agent | ||||
| from clients.models import Client, Site | ||||
| from alerts.models import Alert | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| @@ -54,13 +60,89 @@ def core_maintenance_tasks(): | ||||
|         clear_faults_task.delay(core.clear_faults_days)  # type: ignore | ||||
|  | ||||
|  | ||||
| def _get_failing_data(agents): | ||||
|     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 | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def cache_db_fields_task(): | ||||
|     from agents.models import Agent | ||||
|     # update client/site failing check fields and agent counts | ||||
|     for site in Site.objects.all(): | ||||
|         agents = site.agents.defer(*AGENT_DEFER) | ||||
|         site.failing_checks = _get_failing_data(agents) | ||||
|         site.agent_count = agents.count() | ||||
|         site.save(update_fields=["failing_checks", "agent_count"]) | ||||
|  | ||||
|     for agent in Agent.objects.prefetch_related("winupdates", "pendingactions").only( | ||||
|         "pending_actions_count", "has_patches_pending", "pk" | ||||
|     ): | ||||
|     for client in Client.objects.all(): | ||||
|         agents = Agent.objects.defer(*AGENT_DEFER).filter(site__client=client) | ||||
|         client.failing_checks = _get_failing_data(agents) | ||||
|         client.agent_count = agents.count() | ||||
|         client.save(update_fields=["failing_checks", "agent_count"]) | ||||
|  | ||||
|     for agent in Agent.objects.defer(*AGENT_DEFER): | ||||
|         if ( | ||||
|             pyver.parse(agent.version) >= pyver.parse("1.6.0") | ||||
|             and agent.status == "online" | ||||
|         ): | ||||
|             # change agent update pending status to completed if agent has just updated | ||||
|             if ( | ||||
|                 pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER) | ||||
|                 and agent.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: | ||||
|                     try: | ||||
|                         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() | ||||
|                     except: | ||||
|                         continue | ||||
|  | ||||
|             # handles any alerting actions | ||||
|             if Alert.objects.filter(agent=agent, resolved=False).exists(): | ||||
|                 try: | ||||
|                     Alert.handle_alert_resolve(agent) | ||||
|                 except: | ||||
|                     continue | ||||
|  | ||||
|         # update pending patches and pending action counts | ||||
|         agent.pending_actions_count = agent.pendingactions.filter( | ||||
|             status="pending" | ||||
|         ).count() | ||||
|   | ||||
| @@ -98,7 +98,7 @@ 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), | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ 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 | ||||
| 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 | ||||
| @@ -93,10 +93,16 @@ class PendingActions(APIView): | ||||
|  | ||||
|     def get(self, request, agent_id=None): | ||||
|         if agent_id: | ||||
|             agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|             agent = get_object_or_404( | ||||
|                 Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id | ||||
|             ) | ||||
|             actions = PendingAction.objects.filter(agent=agent) | ||||
|         else: | ||||
|             actions = PendingAction.objects.filter_by_role(request.user) | ||||
|             actions = ( | ||||
|                 PendingAction.objects.select_related("agent") | ||||
|                 .defer("agent__services", "agent__wmi_detail") | ||||
|                 .filter_by_role(request.user)  # type: ignore | ||||
|             ) | ||||
|  | ||||
|         return Response(PendingActionSerializer(actions, many=True).data) | ||||
|  | ||||
|   | ||||
| @@ -8,4 +8,3 @@ Pygments | ||||
| isort | ||||
| mypy | ||||
| types-pytz | ||||
| types-pytz | ||||
| @@ -1,37 +1,42 @@ | ||||
| asgiref==3.4.1 | ||||
| asyncio-nats-client==0.11.4 | ||||
| celery==5.1.2 | ||||
| asyncio-nats-client==0.11.5 | ||||
| celery==5.2.3 | ||||
| certifi==2021.10.8 | ||||
| cffi==1.15.0 | ||||
| channels==3.0.4 | ||||
| channels_redis==3.3.1 | ||||
| chardet==4.0.0 | ||||
| cryptography==3.4.8 | ||||
| cryptography==36.0.1 | ||||
| daphne==3.0.2 | ||||
| Django==3.2.9 | ||||
| django-cors-headers==3.10.0 | ||||
| django-ipware==4.0.0 | ||||
| <<<<<<< HEAD | ||||
| Django==3.2.10 | ||||
| ======= | ||||
| Django==3.2.11 | ||||
| >>>>>>> develop | ||||
| django-cors-headers==3.10.1 | ||||
| django-ipware==4.0.2 | ||||
| django-rest-knox==4.1.0 | ||||
| djangorestframework==3.12.4 | ||||
| djangorestframework==3.13.1 | ||||
| future==0.18.2 | ||||
| loguru==0.5.3 | ||||
| msgpack==1.0.2 | ||||
| packaging==21.2 | ||||
| psycopg2-binary==2.9.1 | ||||
| msgpack==1.0.3 | ||||
| packaging==21.3 | ||||
| psycopg2-binary==2.9.3 | ||||
| pycparser==2.21 | ||||
| pycryptodome==3.11.0 | ||||
| pycryptodome==3.12.0 | ||||
| pyotp==2.6.0 | ||||
| pyparsing==2.4.7 | ||||
| pyparsing==3.0.6 | ||||
| pytz==2021.3 | ||||
| qrcode==6.1 | ||||
| redis==3.5.3 | ||||
| requests==2.26.0 | ||||
| qrcode==7.3.1 | ||||
| redis==4.1.0 | ||||
| requests==2.27.1 | ||||
| six==1.16.0 | ||||
| sqlparse==0.4.2 | ||||
| twilio==7.3.0 | ||||
| urllib3==1.26.7 | ||||
| twilio==7.4.0 | ||||
| urllib3==1.26.8 | ||||
| uWSGI==2.0.20 | ||||
| validators==0.18.2 | ||||
| vine==5.0.0 | ||||
| websockets==9.1 | ||||
| zipp==3.6.0 | ||||
| websockets==10.1 | ||||
| zipp==3.7.0 | ||||
| drf_spectacular==0.21.1 | ||||
| @@ -9,6 +9,16 @@ | ||||
|     "category": "TRMM (Win):Browsers", | ||||
|     "default_timeout": "300" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "720edbb7-8faf-4a77-9283-29935e8880d0", | ||||
|     "filename": "Win_Printer_ClearandRestart.bat", | ||||
|     "submittedBy": "https://github.com/wh1te909", | ||||
|     "name": "Printers - Clear all print jobs", | ||||
|     "description": "This script will stop the spooler, delete all pending print jobs and restart the spooler", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):Printing", | ||||
|     "default_timeout": "300" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443", | ||||
|     "filename": "Win_Google_Chrome_Clear_Cache.ps1", | ||||
| @@ -19,6 +29,16 @@ | ||||
|     "category": "TRMM (Win):Browsers", | ||||
|     "default_timeout": "300" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "d3c74105-d1e5-40d8-94ff-b4d6b216fe0f", | ||||
|     "filename": "Win_Chocolatey_List_Installed.bat", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Chocolatey - List Installed apps", | ||||
|     "description": "Lists apps locally installed by chocolatey", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):3rd Party Software>Chocolatey", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc", | ||||
|     "filename": "Win_Install_Adobe_Reader.ps1", | ||||
| @@ -48,6 +68,16 @@ | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software>Monitoring" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "5a60c13b-1882-4a92-bdfb-6dd1f6a11dd14", | ||||
|     "filename": "Win_Windows_Update_RevertToDefault.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Windows Update - Re-enable Microsoft managed Windows Update", | ||||
|     "description": "TRMM agent will set registry key to disable Windows Auto Updates. This will re-enable Windows standard update settings", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Updates", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04", | ||||
|     "filename": "Win_Windows_Update_Reset.ps1", | ||||
| @@ -63,7 +93,7 @@ | ||||
|     "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.", | ||||
|     "description": "Cleans the C: drive's Window Temporary 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" | ||||
| @@ -102,9 +132,7 @@ | ||||
|     "submittedBy": "https://github.com/bradhawkins85", | ||||
|     "name": "TacticalRMM - Agent Rename", | ||||
|     "description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.", | ||||
|     "args": [ | ||||
|       "<string>" | ||||
|     ], | ||||
|     "syntax": "<string>", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):TacticalRMM Related" | ||||
|   }, | ||||
| @@ -114,9 +142,7 @@ | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Bitlocker - Check Drive for Status", | ||||
|     "description": "Runs a check on drive for Bitlocker status. Returns 0 if Bitlocker is not enabled, 1 if Bitlocker is enabled", | ||||
|     "args": [ | ||||
|       "[Drive <string>]" | ||||
|     ], | ||||
|     "syntax": "[Drive <string>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Storage" | ||||
|   }, | ||||
| @@ -147,6 +173,15 @@ | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Storage" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "11be7136-0416-47b4-a6dd-9776fa857dca", | ||||
|     "filename": "Win_Storage_CheckPools.ps1", | ||||
|     "submittedBy": "https://github.com/wh1te909", | ||||
|     "name": "Storage Pools - Check Health", | ||||
|     "description": "Checks all storage pools for health, returns error 1 if unhealthy", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Monitoring" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "cfa14c28-4dfc-4d4e-95ee-a380652e058d", | ||||
|     "filename": "Win_Bios_Check.ps1", | ||||
| @@ -174,6 +209,15 @@ | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Hardware" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2cf918d1-1cc8-4208-bc94-9ca7b34e61c2", | ||||
|     "filename": "Win_Lenovo_Driver_Updates.ps1", | ||||
|     "submittedBy": "https://github.com/maltekiefer", | ||||
|     "name": "Lenovo - Driver Updates", | ||||
|     "description": "Searches the Lenovo support system for new drivers and installs them", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Hardware" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "72c56717-28ed-4cc6-b30f-b362d30fb4b6", | ||||
|     "filename": "Win_Hardware_SN.ps1", | ||||
| @@ -188,19 +232,31 @@ | ||||
|     "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. ", | ||||
|     "description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use.", | ||||
|     "args": [ | ||||
|       "{{client.ScreenConnectService}}" | ||||
|     ], | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Collectors" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "bbe5645f-c8d8-4d86-bddd-c8dbea45c974", | ||||
|     "filename": "Win_Splashtop_Get_ID.ps1", | ||||
|     "submittedBy": "https://github.com/r3die", | ||||
|     "name": "Splashtop - Get SUUID for client", | ||||
|     "description": "Returns Splashtop SUUID for client - Use with Custom Fields for later use.", | ||||
|     "args": [ | ||||
|       "{{agent.SplashtopSUUID}}" | ||||
|     ], | ||||
|     "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. ", | ||||
|     "description": "Returns Teamviewer ClientID for client - Use with Custom Fields for later use.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Collectors" | ||||
|   }, | ||||
| @@ -209,7 +265,16 @@ | ||||
|     "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. ", | ||||
|     "description": "Returns AnyNetID for client - Use with Custom Fields for later use.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Collectors" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "672403e4-f9b5-4442-8d8c-4fb376dd0a62", | ||||
|     "filename": "Win_Securepoint_Get_DeviceId.ps1", | ||||
|     "submittedBy": "https://github.com/maltekiefer", | ||||
|     "name": "Securepoint - Get DeviceId for client", | ||||
|     "description": "Returns Securepoint DeviceId for client - Use with Custom Fields for later use.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Collectors" | ||||
|   }, | ||||
| @@ -241,21 +306,43 @@ | ||||
|     "category": "TRMM (Win):Updates", | ||||
|     "default_timeout": "25000" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "4d0ba685-2259-44be-9010-8ed2fa48bf74", | ||||
|     "filename": "Win_Win11_Ready.ps1", | ||||
|     "submittedBy": "https://github.com/adamjrberry/", | ||||
|     "name": "Windows 11 Upgrade capable check", | ||||
|     "description": "Checks to see if machine is Win11 capable", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Updates", | ||||
|     "default_timeout": "3600" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "375323e5-cac6-4f35-a304-bb7cef35902d", | ||||
|     "filename": "Win_Disk_Status.ps1", | ||||
|     "filename": "Win_Disk_Volume_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Disk Hardware Health Check (using Event Viewer errors)", | ||||
|     "description": "Checks local disks for errors reported in event viewer within the last 24 hours", | ||||
|     "name": "Disk Drive Volume Health Check (using Event Viewer errors)", | ||||
|     "description": "Checks Drive Volumes for errors reported in event viewer within the last 24 hours", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Hardware" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "4ace28ee-98f7-4931-9ac9-0adaf1a757ed", | ||||
|     "filename": "Win_Software_Install_Report.ps1", | ||||
|     "submittedBy": "https://github.com/silversword", | ||||
|     "name": "Software Install - Reports new installs", | ||||
|     "description": "This will check for software install events in the application Event Viewer log. If a number is provided as a command parameter it will search that number of days back.", | ||||
|     "syntax": "[<int>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Monitoring", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "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 <string>\n[-u <uninstall string>]\n[-u quiet <uninstall string>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software", | ||||
|     "default_timeout": "600" | ||||
| @@ -266,6 +353,7 @@ | ||||
|     "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}}" | ||||
| @@ -274,10 +362,37 @@ | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "bfd61545-839b-45da-8b3d-75ffc4d43272", | ||||
|     "filename": "Win_Sophos_EndpointProtection_Install.ps1", | ||||
|     "submittedBy": "https://github.com/bc24fl/", | ||||
|     "name": "Sophos Endpoint Protection Install", | ||||
|     "description": "Installs Sophos Endpoint Protection via the Sophos API.  Products include Antivirus, InterceptX, MDR, Device Encryption.  The script requires API credentials, Custom Fields, and Arguments passed to script.  See script comments for details", | ||||
|     "args": [ | ||||
|       "-ClientId {{client.SophosClientId}}", | ||||
|       "-ClientSecret {{client.SophosClientSecret}}", | ||||
|       "-TenantName {{client.SophosTenantName}}", | ||||
|       "-Products antivirus,intercept" | ||||
|     ], | ||||
|     "default_timeout": "3600", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "a9d2a6c0-8afa-4d69-8faf-f83b49c11702", | ||||
|     "filename": "Win_Printer_Restart_Jobs.ps1", | ||||
|     "submittedBy": "https://github.com/bc24fl/", | ||||
|     "name": "Printers - Restarts stuck printer jobs.", | ||||
|     "description": "Cycles through each printer and restarts any jobs that are stuck with error status.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Printing", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "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", | ||||
| @@ -368,6 +483,7 @@ | ||||
|     "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": "[<int>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
| @@ -403,6 +519,7 @@ | ||||
|     "filename": "Win_Display_Message_To_User.ps1", | ||||
|     "submittedBy": "https://github.com/bradhawkins85", | ||||
|     "name": "Message Popup To User", | ||||
|     "syntax": "<string>", | ||||
|     "description": "Displays a popup message to the currently logged on user", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other" | ||||
| @@ -412,6 +529,7 @@ | ||||
|     "filename": "Win_Antivirus_Verify.ps1", | ||||
|     "submittedBy": "https://github.com/beejayzed", | ||||
|     "name": "Antivirus - Verify Status", | ||||
|     "syntax": "[-antivirusName <string>]", | ||||
|     "description": "Verify and display status for all installed Antiviruses", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
| @@ -431,11 +549,7 @@ | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Chocolatey - Install, Uninstall and Upgrade Software", | ||||
|     "description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x", | ||||
|     "args": [ | ||||
|       "-$PackageName <string>", | ||||
|       "[-Hosts <string>]", | ||||
|       "[-mode {(install) | update | uninstall}]" | ||||
|     ], | ||||
|     "syntax": "-$PackageName <string>\n[-Hosts <string>]\n[-mode {(install) | update | uninstall}]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software>Chocolatey", | ||||
|     "default_timeout": "600" | ||||
| @@ -460,10 +574,11 @@ | ||||
|   }, | ||||
|   { | ||||
|     "guid": "71090fc4-faa6-460b-adb0-95d7863544e1", | ||||
|     "filename": "Win_Check_Events_for_Bluescreens.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "filename": "Win_Bluescreen_Report.ps1", | ||||
|     "submittedBy": "https://github.com/bbrendon", | ||||
|     "name": "Event Viewer - Bluescreen Notification", | ||||
|     "description": "Event Viewer Monitor - Notify Bluescreen events on your system", | ||||
|     "syntax": "[<int>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Monitoring" | ||||
|   }, | ||||
| @@ -472,7 +587,8 @@ | ||||
|     "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", | ||||
|     "description": "Event Viewer Monitor - Notify when new Local user is created. If parameter provided will search back that number of days", | ||||
|     "syntax": "[<int>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Monitoring" | ||||
|   }, | ||||
| @@ -482,6 +598,7 @@ | ||||
|     "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", | ||||
|     "syntax": "[<int>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Monitoring" | ||||
|   }, | ||||
| @@ -500,12 +617,7 @@ | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Rename Computer", | ||||
|     "description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine", | ||||
|     "args": [ | ||||
|       "-NewName <string>", | ||||
|       "[-Username <string>]", | ||||
|       "[-Password <string>]", | ||||
|       "[-Restart]" | ||||
|     ], | ||||
|     "syntax": "-NewName <string>\n[-Username <string>]\n[-Password <string>]\n[-Restart]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other", | ||||
|     "default_timeout": 30 | ||||
| @@ -516,9 +628,7 @@ | ||||
|     "submittedBy": "https://github.com/tremor021", | ||||
|     "name": "Power - Restart or Shutdown PC", | ||||
|     "description": "Restart PC. Add parameter: shutdown if you want to shutdown computer", | ||||
|     "args": [ | ||||
|       "[shutdown]" | ||||
|     ], | ||||
|     "syntax": "[shutdown]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Updates" | ||||
|   }, | ||||
| @@ -697,6 +807,15 @@ | ||||
|     "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", | ||||
| @@ -757,13 +876,17 @@ | ||||
|     "submittedBy": "https://github.com/brodur", | ||||
|     "name": "User - Create Local", | ||||
|     "description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)", | ||||
|     "args": [ | ||||
|       "-username <string>", | ||||
|       "-password <string>", | ||||
|       "[-description <string>]", | ||||
|       "[-fullname <string>]", | ||||
|       "[-group <string>]" | ||||
|     ], | ||||
|     "syntax": "-username <string>\n-password <string>\n[-description <string>]\n[-fullname <string>]\n[-group <string>]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):User Management" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "6e27d5341-88fa-4c2f-9c91-c3aeb1740e85", | ||||
|     "filename": "Win_User_EnableDisable.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "User - Enable or disable a user", | ||||
|     "description": "Used to enable or disable local user", | ||||
|     "syntax": "-Name <string>\n-Enabled { yes | no }", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):User Management" | ||||
|   }, | ||||
| @@ -810,6 +933,7 @@ | ||||
|     "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 <string>\n-destination <string>\n[-recursive {True | False}]", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Misc>Reference", | ||||
|     "default_timeout": "1" | ||||
| @@ -829,6 +953,7 @@ | ||||
|     "filename": "Win_AD_Join_Computer.ps1", | ||||
|     "submittedBy": "https://github.com/rfost52", | ||||
|     "name": "AD - Join Computer to Domain", | ||||
|     "syntax": "-domain <string>\n-password <string>\n-UserAccount ADMINaccount\n[-OUPath <OU=testOU,DC=test,DC=local>]", | ||||
|     "description": "Join computer to a domain in Active Directory", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Active Directory", | ||||
| @@ -839,6 +964,7 @@ | ||||
|     "filename": "Win_Collect_System_Report_And_Email.ps1", | ||||
|     "submittedBy": "https://github.com/rfost52", | ||||
|     "name": "Collect System Report and Email", | ||||
|     "syntax": "-agentname <string>\n-file <string enter file name with the extension .HTM or .HTML>\n-fromaddress <string>\n-toaddress <string>\n-smtpserver <string>\n-password <string>\n-port <int 587 is the standard port for sending mail over TLS>", | ||||
|     "description": "Generates a system report in HTML format, then emails it", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other", | ||||
|   | ||||
							
								
								
									
										18
									
								
								api/tacticalrmm/scripts/migrations/0013_script_syntax.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/scripts/migrations/0013_script_syntax.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.6 on 2021-11-13 16:25 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('scripts', '0012_auto_20210917_1954'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='script', | ||||
|             name='syntax', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.6 on 2021-11-19 15:44 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('scripts', '0013_script_syntax'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='script', | ||||
|             name='filename', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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=''), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,5 +1,6 @@ | ||||
| import base64 | ||||
| import re | ||||
| import hmac | ||||
| import hashlib | ||||
| from typing import List | ||||
|  | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| @@ -24,7 +25,7 @@ class Script(BaseAuditModel): | ||||
|     guid = models.CharField(max_length=64, null=True, blank=True) | ||||
|     name = models.CharField(max_length=255) | ||||
|     description = models.TextField(null=True, blank=True, default="") | ||||
|     filename = models.CharField(max_length=255)  # deprecated | ||||
|     filename = models.CharField(max_length=255, null=True, blank=True) | ||||
|     shell = models.CharField( | ||||
|         max_length=100, choices=SCRIPT_SHELLS, default="powershell" | ||||
|     ) | ||||
| @@ -37,9 +38,12 @@ class Script(BaseAuditModel): | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     syntax = TextField(null=True, blank=True) | ||||
|     favorite = models.BooleanField(default=False) | ||||
|     category = models.CharField(max_length=100, null=True, blank=True) | ||||
|     code_base64 = models.TextField(null=True, blank=True, default="") | ||||
|     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) | ||||
|  | ||||
|     def __str__(self): | ||||
| @@ -47,12 +51,7 @@ class Script(BaseAuditModel): | ||||
|  | ||||
|     @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 if self.script_body else "" | ||||
|  | ||||
|     @property | ||||
|     def code(self): | ||||
| @@ -77,6 +76,12 @@ class Script(BaseAuditModel): | ||||
|         else: | ||||
|             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 | ||||
| @@ -99,6 +104,9 @@ class Script(BaseAuditModel): | ||||
|         ) 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"]) | ||||
| @@ -115,83 +123,36 @@ class Script(BaseAuditModel): | ||||
|  | ||||
|                 args = script["args"] if "args" in script.keys() else [] | ||||
|  | ||||
|                 syntax = script["syntax"] if "syntax" 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: 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"] | ||||
|  | ||||
|                     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.script_body = f.read().decode("utf-8") | ||||
|                         # i.hash_script_body() | ||||
|                         i.save() | ||||
|  | ||||
|                     i.save(  # type: ignore | ||||
|                         update_fields=[ | ||||
|                             "name", | ||||
|                             "description", | ||||
|                             "category", | ||||
|                             "default_timeout", | ||||
|                             "code_base64", | ||||
|                             "shell", | ||||
|                             "args", | ||||
|                         ] | ||||
|                     ) | ||||
|  | ||||
|                 # check if script was added without a guid | ||||
|                 elif cls.objects.filter( | ||||
|                     script_type="builtin", name=script["name"] | ||||
|                 ).exists(): | ||||
|                     s = cls.objects.get(script_type="builtin", name=script["name"]) | ||||
|  | ||||
|                     if not s.guid: | ||||
|                         print(f"Updating GUID for: {script['name']}") | ||||
|                         s.guid = script["guid"] | ||||
|                         s.name = script["name"] | ||||
|                         s.description = script["description"] | ||||
|                         s.category = category | ||||
|                         s.shell = script["shell"] | ||||
|                         s.default_timeout = default_timeout | ||||
|                         s.args = args | ||||
|  | ||||
|                         with open( | ||||
|                             os.path.join(scripts_dir, script["filename"]), "rb" | ||||
|                         ) as f: | ||||
|                             script_bytes = ( | ||||
|                                 f.read().decode("utf-8").encode("ascii", "ignore") | ||||
|                             ) | ||||
|                             s.code_base64 = base64.b64encode(script_bytes).decode( | ||||
|                                 "ascii" | ||||
|                             ) | ||||
|  | ||||
|                         s.save( | ||||
|                             update_fields=[ | ||||
|                                 "guid", | ||||
|                                 "name", | ||||
|                                 "description", | ||||
|                                 "category", | ||||
|                                 "default_timeout", | ||||
|                                 "code_base64", | ||||
|                                 "shell", | ||||
|                                 "args", | ||||
|                             ] | ||||
|                         ) | ||||
|                     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"], | ||||
| @@ -200,10 +161,24 @@ class Script(BaseAuditModel): | ||||
|                             category=category, | ||||
|                             default_timeout=default_timeout, | ||||
|                             args=args, | ||||
|                         ).save() | ||||
|                             filename=script["filename"], | ||||
|                             syntax=syntax, | ||||
|                         ) | ||||
|                         # new_script.hash_script_body()  # also saves script | ||||
|                         new_script.save() | ||||
|  | ||||
|         # delete community scripts that had their name changed | ||||
|         cls.objects.filter(script_type="builtin", guid=None).delete() | ||||
|                         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="builtin") | ||||
|             .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): | ||||
|   | ||||
| @@ -16,10 +16,14 @@ class ScriptTableSerializer(ModelSerializer): | ||||
|             "category", | ||||
|             "favorite", | ||||
|             "default_timeout", | ||||
|             "syntax", | ||||
|             "filename", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class ScriptSerializer(ModelSerializer): | ||||
|     script_hash = ReadOnlyField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Script | ||||
|         fields = [ | ||||
| @@ -30,17 +34,21 @@ class ScriptSerializer(ModelSerializer): | ||||
|             "args", | ||||
|             "category", | ||||
|             "favorite", | ||||
|             "code_base64", | ||||
|             "script_body", | ||||
|             "script_hash", | ||||
|             "default_timeout", | ||||
|             "syntax", | ||||
|             "filename", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class ScriptCheckSerializer(ModelSerializer): | ||||
|     code = ReadOnlyField() | ||||
|     script_hash = ReadOnlyField | ||||
|  | ||||
|     class Meta: | ||||
|         model = Script | ||||
|         fields = ["code", "shell"] | ||||
|         fields = ["code", "shell", "script_hash"] | ||||
|  | ||||
|  | ||||
| class ScriptSnippetSerializer(ModelSerializer): | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import asyncio | ||||
|  | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent, AgentHistory | ||||
| from scripts.models import Script | ||||
| @@ -20,14 +19,13 @@ def handle_bulk_command_task( | ||||
|         }, | ||||
|     } | ||||
|     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 | ||||
|         hist = AgentHistory.objects.create( | ||||
|             agent=agent, | ||||
|             type="cmd_run", | ||||
|             command=cmd, | ||||
|             username=username, | ||||
|         ) | ||||
|         nats_data["id"] = hist.pk | ||||
|  | ||||
|         asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|  | ||||
| @@ -36,15 +34,12 @@ def handle_bulk_command_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 | ||||
|         hist = AgentHistory.objects.create( | ||||
|             agent=agent, | ||||
|             type="script_run", | ||||
|             script=script, | ||||
|             username=username, | ||||
|         ) | ||||
|         agent.run_script( | ||||
|             scriptpk=script.pk, args=args, timeout=timeout, history_pk=hist.pk | ||||
|         ) | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| import json | ||||
| import os | ||||
| import hmac | ||||
| import hashlib | ||||
|  | ||||
| from pathlib import Path | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from django.test import override_settings | ||||
| from django.conf import settings | ||||
| from model_bakery import baker | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| @@ -31,6 +35,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @override_settings(SECRET_KEY="Test Secret Key") | ||||
|     def test_add_script(self): | ||||
|         url = f"/scripts/" | ||||
|  | ||||
| @@ -39,7 +44,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|             "description": "Description", | ||||
|             "shell": "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 @@ class TestScriptViews(TacticalTestCase): | ||||
|         # 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 @@ class TestScriptViews(TacticalTestCase): | ||||
|             "name": script.name, | ||||
|             "description": "Description Change", | ||||
|             "shell": script.shell, | ||||
|             "code_base64": "VGVzdA==",  # Test | ||||
|             "script_body": "Test Script Body",  # Test | ||||
|             "default_timeout": 13344556, | ||||
|         } | ||||
|  | ||||
| @@ -75,14 +87,17 @@ class TestScriptViews(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         script = Script.objects.get(pk=script.pk) | ||||
|         self.assertEquals(script.description, "Description Change") | ||||
|         self.assertEquals(script.code, "Test") | ||||
|  | ||||
|         # 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") | ||||
|  | ||||
| @@ -94,7 +109,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|             "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 | ||||
| @@ -166,29 +181,33 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         # test powershell file | ||||
|         script = baker.make( | ||||
|             "scripts.Script", code_base64="VGVzdA==", shell="powershell" | ||||
|             "scripts.Script", script_body="Test Script Body", shell="powershell" | ||||
|         ) | ||||
|         url = f"/scripts/{script.pk}/download/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, {"filename": f"{script.name}.ps1", "code": "Test"})  # type: ignore | ||||
|         self.assertEqual(resp.data, {"filename": f"{script.name}.ps1", "code": "Test Script Body"})  # type: ignore | ||||
|  | ||||
|         # test batch file | ||||
|         script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd") | ||||
|         script = baker.make( | ||||
|             "scripts.Script", script_body="Test Script Body", shell="cmd" | ||||
|         ) | ||||
|         url = f"/scripts/{script.pk}/download/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, {"filename": f"{script.name}.bat", "code": "Test"})  # type: ignore | ||||
|         self.assertEqual(resp.data, {"filename": f"{script.name}.bat", "code": "Test Script Body"})  # type: ignore | ||||
|  | ||||
|         # test python file | ||||
|         script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python") | ||||
|         script = baker.make( | ||||
|             "scripts.Script", script_body="Test Script Body", shell="python" | ||||
|         ) | ||||
|         url = f"/scripts/{script.pk}/download/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, {"filename": f"{script.name}.py", "code": "Test"})  # type: ignore | ||||
|         self.assertEqual(resp.data, {"filename": f"{script.name}.py", "code": "Test Script Body"})  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import base64 | ||||
| import asyncio | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
| @@ -37,6 +36,8 @@ class GetAddScripts(APIView): | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         obj = serializer.save() | ||||
|  | ||||
|         # obj.hash_script_body() | ||||
|  | ||||
|         return Response(f"{obj.name} was added!") | ||||
|  | ||||
|  | ||||
| @@ -64,6 +65,8 @@ class GetUpdateDeleteScript(APIView): | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         obj = serializer.save() | ||||
|  | ||||
|         # obj.hash_script_body() | ||||
|  | ||||
|         return Response(f"{obj.name} was edited!") | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import asyncio | ||||
|  | ||||
| from agents.models import Agent | ||||
| from checks.models import Check | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.conf import settings | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| @@ -15,6 +15,11 @@ 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)) | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import asyncio | ||||
| 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,7 @@ from rest_framework.views import APIView | ||||
|  | ||||
| from agents.models import Agent | ||||
| from logs.models import PendingAction | ||||
| from tacticalrmm.utils import filter_software, notify_error | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| from .models import ChocoSoftware, InstalledSoftware | ||||
| from .permissions import SoftwarePerms | ||||
| @@ -42,9 +41,6 @@ class GetSoftware(APIView): | ||||
|     # 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") | ||||
|  | ||||
|         name = request.data["name"] | ||||
|  | ||||
|         action = PendingAction.objects.create( | ||||
| @@ -76,13 +72,11 @@ class GetSoftware(APIView): | ||||
|         if r == "timeout" or r == "natsdown": | ||||
|             return notify_error("Unable to contact the agent") | ||||
|  | ||||
|         sw = filter_software(r) | ||||
|  | ||||
|         if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|             InstalledSoftware(agent=agent, software=sw).save() | ||||
|             InstalledSoftware(agent=agent, software=r).save() | ||||
|         else: | ||||
|             s = agent.installedsoftware_set.first()  # type: ignore | ||||
|             s.software = sw | ||||
|             s.software = r | ||||
|             s.save(update_fields=["software"]) | ||||
|  | ||||
|         return Response("ok") | ||||
|   | ||||
| @@ -20,8 +20,9 @@ app.accept_content = ["application/json"]  # type: ignore | ||||
| app.result_serializer = "json"  # type: ignore | ||||
| app.task_serializer = "json"  # type: ignore | ||||
| app.conf.task_track_started = True | ||||
| app.autodiscover_tasks() | ||||
| app.conf.worker_proc_alive_timeout = 30 | ||||
| app.conf.worker_max_tasks_per_child = 2 | ||||
| app.autodiscover_tasks() | ||||
|  | ||||
| app.conf.beat_schedule = { | ||||
|     "auto-approve-win-updates": { | ||||
| @@ -36,18 +37,6 @@ app.conf.beat_schedule = { | ||||
|         "task": "agents.tasks.auto_self_agent_update_task", | ||||
|         "schedule": crontab(minute=35, hour="*"), | ||||
|     }, | ||||
|     "handle-agents": { | ||||
|         "task": "agents.tasks.handle_agents_task", | ||||
|         "schedule": crontab(minute="*"), | ||||
|     }, | ||||
|     "get-agentinfo": { | ||||
|         "task": "agents.tasks.agent_getinfo_task", | ||||
|         "schedule": crontab(minute="*"), | ||||
|     }, | ||||
|     "get-wmi": { | ||||
|         "task": "agents.tasks.get_wmi_task", | ||||
|         "schedule": crontab(minute=18, hour="*/5"), | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -56,15 +45,14 @@ def debug_task(self): | ||||
|     print("Request: {0!r}".format(self.request)) | ||||
|  | ||||
|  | ||||
| @app.on_after_finalize.connect | ||||
| @app.on_after_finalize.connect  # type: ignore | ||||
| def setup_periodic_tasks(sender, **kwargs): | ||||
|  | ||||
|     from agents.tasks import agent_outages_task, agent_checkin_task | ||||
|     from agents.tasks import agent_outages_task | ||||
|     from alerts.tasks import unsnooze_alerts | ||||
|     from core.tasks import core_maintenance_tasks, cache_db_fields_task | ||||
|  | ||||
|     sender.add_periodic_task(45.0, agent_checkin_task.s()) | ||||
|     sender.add_periodic_task(60.0, agent_outages_task.s()) | ||||
|     sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s()) | ||||
|     sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s()) | ||||
|     sender.add_periodic_task(90.0, cache_db_fields_task.s()) | ||||
|     sender.add_periodic_task(85.0, cache_db_fields_task.s()) | ||||
|   | ||||
							
								
								
									
										539
									
								
								api/tacticalrmm/tacticalrmm/demo_data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										539
									
								
								api/tacticalrmm/tacticalrmm/demo_data.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,539 @@ | ||||
| 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, | ||||
|         } | ||||
|     ], | ||||
| ] | ||||
|  | ||||
| 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} | ||||
| """ | ||||
							
								
								
									
										42
									
								
								api/tacticalrmm/tacticalrmm/demo_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								api/tacticalrmm/tacticalrmm/demo_views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import json | ||||
| from django.conf import settings | ||||
| import random | ||||
| 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) | ||||
| @@ -21,6 +21,7 @@ EXCLUDE_PATHS = ( | ||||
|     f"/{settings.ADMIN_URL}", | ||||
|     "/logout", | ||||
|     "/agents/installer", | ||||
|     "/api/schema", | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -92,3 +93,61 @@ class LogIPMiddleware: | ||||
|         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 = [ | ||||
|             {"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": "get_mesh_exe", "methods": ["POST"]}, | ||||
|             {"name": "GenerateAgent", "methods": ["GET"]}, | ||||
|             {"name": "UploadMeshAgent", "methods": ["PUT"]}, | ||||
|             {"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"]}, | ||||
|         ] | ||||
|  | ||||
|     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()  # type: ignore | ||||
|  | ||||
|     def process_view(self, request, view_func, view_args, view_kwargs): | ||||
|         from .utils import notify_error | ||||
|  | ||||
|         err = "Not available in demo" | ||||
|         excludes = ("/api/v3",) | ||||
|  | ||||
|         if request.path.startswith(excludes): | ||||
|             return self.drf_mock_response(request, notify_error(err)) | ||||
|  | ||||
|         for i in self.not_allowed: | ||||
|             if view_func.__name__ == i["name"] and request.method in i["methods"]: | ||||
|                 return self.drf_mock_response(request, notify_error(err)) | ||||
|   | ||||
| @@ -15,25 +15,25 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe") | ||||
| AUTH_USER_MODEL = "accounts.User" | ||||
|  | ||||
| # latest release | ||||
| TRMM_VERSION = "0.9.2" | ||||
| TRMM_VERSION = "0.11.0" | ||||
|  | ||||
| # bump this version everytime vue code is changed | ||||
| # to alert user they need to manually refresh their browser | ||||
| APP_VER = "0.0.150" | ||||
| APP_VER = "0.0.156" | ||||
|  | ||||
| # https://github.com/wh1te909/rmmagent | ||||
| LATEST_AGENT_VER = "1.6.2" | ||||
| LATEST_AGENT_VER = "1.8.0" | ||||
|  | ||||
| MESH_VER = "0.9.45" | ||||
| MESH_VER = "0.9.67" | ||||
|  | ||||
| NATS_SERVER_VER = "2.3.3" | ||||
| NATS_SERVER_VER = "2.6.6" | ||||
|  | ||||
| # for the update script, bump when need to recreate venv or npm install | ||||
| PIP_VER = "23" | ||||
| NPM_VER = "24" | ||||
| PIP_VER = "26" | ||||
| NPM_VER = "28" | ||||
|  | ||||
| SETUPTOOLS_VER = "58.5.3" | ||||
| WHEEL_VER = "0.37.0" | ||||
| SETUPTOOLS_VER = "59.6.0" | ||||
| WHEEL_VER = "0.37.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" | ||||
| @@ -65,6 +65,13 @@ REST_FRAMEWORK = { | ||||
|         "knox.auth.TokenAuthentication", | ||||
|         "tacticalrmm.auth.APIAuthentication", | ||||
|     ), | ||||
|     "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", | ||||
| } | ||||
|  | ||||
| SPECTACULAR_SETTINGS = { | ||||
|     "TITLE": "Tactical RMM API", | ||||
|     "DESCRIPTION": "Simple and Fast remote monitoring and management tool", | ||||
|     "VERSION": TRMM_VERSION, | ||||
| } | ||||
|  | ||||
| if not "AZPIPELINE" in os.environ: | ||||
| @@ -97,6 +104,7 @@ INSTALLED_APPS = [ | ||||
|     "logs", | ||||
|     "scripts", | ||||
|     "alerts", | ||||
|     "drf_spectacular", | ||||
| ] | ||||
|  | ||||
| if not "AZPIPELINE" in os.environ: | ||||
| @@ -137,6 +145,11 @@ MIDDLEWARE = [ | ||||
| if ADMIN_ENABLED:  # type: ignore | ||||
|     MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",) | ||||
|  | ||||
| try: | ||||
|     if DEMO:  # type: ignore | ||||
|         MIDDLEWARE += ("tacticalrmm.middleware.DemoMiddleware",) | ||||
| except: | ||||
|     pass | ||||
|  | ||||
| ROOT_URLCONF = "tacticalrmm.urls" | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| $networkstatus = Get-NetConnectionProfile | Select NetworkCategory | Out-String | ||||
|  | ||||
| if ($networkstatus.Contains("DomainAuthenticated")) { | ||||
|     exit 0 | ||||
| } else { | ||||
|     exit 1 | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| $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 | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| @echo off | ||||
|  | ||||
| sc stop spooler | ||||
|  | ||||
| timeout /t 5 /nobreak > NUL | ||||
|  | ||||
| del C:\Windows\System32\spool\printers\* /Q /F /S | ||||
|  | ||||
| sc start spooler | ||||
| @@ -0,0 +1 @@ | ||||
| Restart-Service NlaSvc -Force | ||||
| @@ -0,0 +1,25 @@ | ||||
| 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) | ||||
							
								
								
									
										132
									
								
								api/tacticalrmm/tacticalrmm/test_data/eventlog_check_fail.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								api/tacticalrmm/tacticalrmm/test_data/eventlog_check_fail.json
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										138
									
								
								api/tacticalrmm/tacticalrmm/test_data/software2.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								api/tacticalrmm/tacticalrmm/test_data/software2.json
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										2444
									
								
								api/tacticalrmm/tacticalrmm/test_data/winsvcs.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2444
									
								
								api/tacticalrmm/tacticalrmm/test_data/winsvcs.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										319
									
								
								api/tacticalrmm/tacticalrmm/test_data/winupdates.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								api/tacticalrmm/tacticalrmm/test_data/winupdates.json
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2397
									
								
								api/tacticalrmm/tacticalrmm/test_data/wmi1.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2397
									
								
								api/tacticalrmm/tacticalrmm/test_data/wmi1.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										3613
									
								
								api/tacticalrmm/tacticalrmm/test_data/wmi2.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3613
									
								
								api/tacticalrmm/tacticalrmm/test_data/wmi2.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										3102
									
								
								api/tacticalrmm/tacticalrmm/test_data/wmi3.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3102
									
								
								api/tacticalrmm/tacticalrmm/test_data/wmi3.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,19 +1,15 @@ | ||||
| import json | ||||
| import os | ||||
| from unittest.mock import mock_open, patch | ||||
|  | ||||
| import requests | ||||
| from django.conf import settings | ||||
| from django.test import override_settings | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from .utils import ( | ||||
|     bitdays_to_string, | ||||
|     filter_software, | ||||
|     generate_winagent_exe, | ||||
|     get_bit_days, | ||||
|     reload_nats, | ||||
|     run_nats_api_cmd, | ||||
|     AGENT_DEFER, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -78,12 +74,6 @@ class TestUtils(TacticalTestCase): | ||||
|  | ||||
|         mock_subprocess.assert_called_once() | ||||
|  | ||||
|     @patch("subprocess.run") | ||||
|     def test_run_nats_api_cmd(self, mock_subprocess): | ||||
|         ids = ["a", "b", "c"] | ||||
|         _ = run_nats_api_cmd("wmi", ids) | ||||
|         mock_subprocess.assert_called_once() | ||||
|  | ||||
|     def test_bitdays_to_string(self): | ||||
|         a = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] | ||||
|         all_days = [ | ||||
| @@ -104,11 +94,10 @@ class TestUtils(TacticalTestCase): | ||||
|         r = bitdays_to_string(bit_weekdays) | ||||
|         self.assertEqual(r, "Every day") | ||||
|  | ||||
|     def test_filter_software(self): | ||||
|         with open( | ||||
|             os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/software1.json") | ||||
|         ) as f: | ||||
|             sw = json.load(f) | ||||
|     def test_defer_fields_exist(self): | ||||
|         from agents.models import Agent | ||||
|  | ||||
|         r = filter_software(sw) | ||||
|         self.assertIsInstance(r, list) | ||||
|         fields = [i.name for i in Agent._meta.get_fields()] | ||||
|  | ||||
|         for i in AGENT_DEFER: | ||||
|             self.assertIn(i, fields) | ||||
|   | ||||
| @@ -39,11 +39,23 @@ urlpatterns = [ | ||||
|     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 getattr(settings, "SWAGGER_ENABLED", False): | ||||
|     from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView | ||||
|  | ||||
|     urlpatterns += ( | ||||
|         path("api/schema/", SpectacularAPIView.as_view(), name="schema"), | ||||
|         path( | ||||
|             "api/schema/swagger-ui/", | ||||
|             SpectacularSwaggerView.as_view(url_name="schema"), | ||||
|             name="swagger-ui", | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
| ws_urlpatterns = [ | ||||
|     path("ws/dashinfo/", DashInfo.as_asgi()),  # type: ignore | ||||
| ] | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import json | ||||
| import os | ||||
| import string | ||||
| import subprocess | ||||
| import tempfile | ||||
| import time | ||||
| from typing import Optional, Union | ||||
| from typing import List, Optional, Union | ||||
|  | ||||
| import pytz | ||||
| import requests | ||||
| @@ -23,7 +22,7 @@ from agents.models import Agent | ||||
|  | ||||
| notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
| SoftwareList = list[dict[str, str]] | ||||
| AGENT_DEFER = ["wmi_detail", "services"] | ||||
|  | ||||
| WEEK_DAYS = { | ||||
|     "Sunday": 0x1, | ||||
| @@ -35,6 +34,32 @@ WEEK_DAYS = { | ||||
|     "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 | ||||
|  | ||||
|  | ||||
| def generate_winagent_exe( | ||||
|     client: int, | ||||
| @@ -125,46 +150,58 @@ def get_bit_days(days: list[str]) -> int: | ||||
|  | ||||
|  | ||||
| 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 & int(value): | ||||
|             ret.append(key) | ||||
|     return ", ".join(ret) | ||||
|  | ||||
|  | ||||
| def filter_software(sw: SoftwareList) -> SoftwareList: | ||||
|     ret: SoftwareList = [] | ||||
|     printable = set(string.printable) | ||||
|     for s in sw: | ||||
|         ret.append( | ||||
|             { | ||||
|                 "name": "".join(filter(lambda x: x in printable, s["name"])), | ||||
|                 "version": "".join(filter(lambda x: x in printable, s["version"])), | ||||
|                 "publisher": "".join(filter(lambda x: x in printable, s["publisher"])), | ||||
|                 "install_date": s["install_date"], | ||||
|                 "size": s["size"], | ||||
|                 "source": s["source"], | ||||
|                 "location": s["location"], | ||||
|                 "uninstall": s["uninstall"], | ||||
|             } | ||||
|         ) | ||||
| def bitmonths_to_string(month: int) -> str: | ||||
|     ret: List[str] = [] | ||||
|     if month == 4095: | ||||
|         return "Every month" | ||||
|  | ||||
|     return ret | ||||
|     for key, value in MONTHS.items(): | ||||
|         if month & int(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 & int(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 == 2147483647 or day == 4294967295: | ||||
|         return "Every day" | ||||
|  | ||||
|     for key, value in MONTH_DAYS.items(): | ||||
|         if day & int(value): | ||||
|             ret.append(key) | ||||
|     return ", ".join(ret) | ||||
|  | ||||
|  | ||||
| def convert_to_iso_duration(string: str) -> str: | ||||
|     tmp = string.upper() | ||||
|     if "D" in tmp: | ||||
|         return f"P{tmp.replace('D', 'DT')}" | ||||
|     else: | ||||
|         return f"PT{tmp}" | ||||
|  | ||||
|  | ||||
| def reload_nats(): | ||||
| @@ -188,9 +225,8 @@ def reload_nats(): | ||||
|     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 = settings.CERT_FILE | ||||
|         key_file = settings.KEY_FILE | ||||
|  | ||||
|     config = { | ||||
|         "tls": { | ||||
| @@ -239,38 +275,6 @@ KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def run_nats_api_cmd(mode: str, ids: list[str] = [], timeout: int = 30) -> None: | ||||
|     if mode == "wmi": | ||||
|         config = { | ||||
|             "key": settings.SECRET_KEY, | ||||
|             "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", | ||||
|             "agents": ids, | ||||
|         } | ||||
|     else: | ||||
|         db = settings.DATABASES["default"] | ||||
|         config = { | ||||
|             "key": settings.SECRET_KEY, | ||||
|             "natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", | ||||
|             "user": db["USER"], | ||||
|             "pass": db["PASSWORD"], | ||||
|             "host": db["HOST"], | ||||
|             "port": int(db["PORT"]), | ||||
|             "dbname": db["NAME"], | ||||
|         } | ||||
|  | ||||
|     with tempfile.NamedTemporaryFile( | ||||
|         dir="/opt/tactical/tmp" if settings.DOCKER_BUILD else None | ||||
|     ) as fp: | ||||
|         with open(fp.name, "w") as f: | ||||
|             json.dump(config, f) | ||||
|  | ||||
|         cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", mode] | ||||
|         try: | ||||
|             subprocess.run(cmd, timeout=timeout) | ||||
|         except Exception as e: | ||||
|             DebugLog.error(message=e) | ||||
|  | ||||
|  | ||||
| def get_latest_trmm_ver() -> str: | ||||
|     url = "https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py" | ||||
|     try: | ||||
| @@ -283,7 +287,7 @@ def get_latest_trmm_ver() -> str: | ||||
|             if "TRMM_VERSION" in line: | ||||
|                 return line.split(" ")[2].strip('"') | ||||
|     except Exception as e: | ||||
|         DebugLog.error(message=e) | ||||
|         DebugLog.error(message=str(e)) | ||||
|  | ||||
|     return "error" | ||||
|  | ||||
| @@ -352,7 +356,8 @@ def replace_db_values( | ||||
|     if not obj: | ||||
|         return "" | ||||
|  | ||||
|     if hasattr(obj, temp[1]): | ||||
|     # check if attr exists and isn't a function | ||||
|     if hasattr(obj, temp[1]) and not callable(getattr(obj, temp[1])): | ||||
|         value = f"'{getattr(obj, temp[1])}'" if quotes else getattr(obj, temp[1]) | ||||
|  | ||||
|     elif CustomField.objects.filter(model=model, name=temp[1]).exists(): | ||||
|   | ||||
							
								
								
									
										10
									
								
								backup.sh
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								backup.sh
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| SCRIPT_VERSION="15" | ||||
| SCRIPT_VERSION="17" | ||||
| SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh' | ||||
|  | ||||
| GREEN='\033[0;32m' | ||||
| @@ -75,9 +75,9 @@ sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d . | ||||
|  | ||||
| sudo gzip -9 -c /var/lib/redis/appendonly.aof > ${tmp_dir}/redis/appendonly.aof.gz | ||||
|  | ||||
| sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/ | ||||
| if [ -f "${sysd}/daphne.service" ]; then | ||||
|     sudo cp ${sysd}/daphne.service ${tmp_dir}/systemd/ | ||||
| sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/daphne.service ${tmp_dir}/systemd/ | ||||
| if [ -f "${sysd}/nats-api.service" ]; then | ||||
|     sudo cp ${sysd}/nats-api.service ${tmp_dir}/systemd/ | ||||
| fi | ||||
|  | ||||
| cat /rmm/api/tacticalrmm/tacticalrmm/private/log/django_debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz | ||||
| @@ -89,4 +89,4 @@ tar -cf /rmmbackups/rmm-backup-${dt_now}.tar -C ${tmp_dir} . | ||||
|  | ||||
| rm -rf ${tmp_dir} | ||||
|  | ||||
| echo -ne "${GREEN}Backup saved to /rmmbackups/rmm-backup-${dt_now}.tar${NC}\n" | ||||
| echo -ne "${GREEN}Backup saved to /rmmbackups/rmm-backup-${dt_now}.tar${NC}\n" | ||||
|   | ||||
| @@ -10,6 +10,13 @@ set -e | ||||
| : "${MONGODB_PORT:=27017}" | ||||
| : "${NGINX_HOST_IP:=172.20.0.20}" | ||||
| : "${MESH_PERSISTENT_CONFIG:=0}" | ||||
| : "${WS_MASK_OVERRIDE:=0}" | ||||
| : "${SMTP_HOST:=smtp.example.com}" | ||||
| : "${SMTP_PORT:=587}" | ||||
| : "${SMTP_FROM:=mesh@example.com}" | ||||
| : "${SMTP_USER:=mesh@example.com}" | ||||
| : "${SMTP_PASS:=mesh-smtp-pass}" | ||||
| : "${SMTP_TLS:=false}" | ||||
|  | ||||
| mkdir -p /home/node/app/meshcentral-data | ||||
| mkdir -p ${TACTICAL_DIR}/tmp | ||||
| @@ -50,8 +57,17 @@ mesh_config="$(cat << EOF | ||||
|       "NewAccounts": false, | ||||
|       "mstsc": true, | ||||
|       "GeoLocation": true, | ||||
|       "CertUrl": "https://${NGINX_HOST_IP}:443" | ||||
|       "CertUrl": "https://${NGINX_HOST_IP}:443", | ||||
|       "agentConfig": [ "webSocketMaskOverride=${WS_MASK_OVERRIDE}" ] | ||||
|     } | ||||
|   }, | ||||
|   "smtp": { | ||||
|     "host": "${SMTP_HOST}", | ||||
|     "port": ${SMTP_PORT}, | ||||
|     "from": "${SMTP_FROM}", | ||||
|     "user": "${SMTP_USER}", | ||||
|     "pass": "${SMTP_PASS}", | ||||
|     "tls": ${SMTP_TLS} | ||||
|   } | ||||
| } | ||||
| EOF | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| FROM nats:2.3.3-alpine | ||||
| FROM nats:2.6.6-alpine | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
|  | ||||
| RUN apk add --no-cache inotify-tools supervisor bash | ||||
| RUN apk add --no-cache supervisor bash | ||||
|  | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| COPY natsapi/bin/nats-api /usr/local/bin/ | ||||
| RUN chmod +x /usr/local/bin/nats-api | ||||
|  | ||||
| COPY docker/containers/tactical-nats/entrypoint.sh / | ||||
| RUN chmod +x /entrypoint.sh | ||||
|  | ||||
|   | ||||
| @@ -3,11 +3,14 @@ | ||||
| set -e | ||||
|  | ||||
| : "${DEV:=0}" | ||||
| : "${NATS_CONFIG_CHECK_INTERVAL:=1}" | ||||
|  | ||||
| if [ "${DEV}" = 1 ]; then | ||||
|   NATS_CONFIG=/workspace/api/tacticalrmm/nats-rmm.conf | ||||
|   NATS_API_CONFIG=/workspace/api/tacticalrmm/nats-api.conf | ||||
| else | ||||
|   NATS_CONFIG="${TACTICAL_DIR}/api/nats-rmm.conf" | ||||
|   NATS_API_CONFIG="${TACTICAL_DIR}/api/nats-api.conf" | ||||
| fi | ||||
|  | ||||
| sleep 15 | ||||
| @@ -16,6 +19,27 @@ until [ -f "${TACTICAL_READY_FILE}" ]; do | ||||
|   sleep 10 | ||||
| done | ||||
|  | ||||
| config_watcher="$(cat << EOF | ||||
| while true; do | ||||
|     sleep ${NATS_CONFIG_CHECK_INTERVAL}; | ||||
|     if [[ ! -z \${NATS_CHECK} ]]; then | ||||
|         NATS_RELOAD=\$(date -r '${NATS_CONFIG}') | ||||
|         if [[ \$NATS_RELOAD == \$NATS_CHECK ]]; then | ||||
|             : | ||||
|         else | ||||
|             nats-server --signal reload; | ||||
|             NATS_CHECK=\$(date -r '${NATS_CONFIG}'); | ||||
|         fi | ||||
|     else NATS_CHECK=\$(date -r '${NATS_CONFIG}'); | ||||
|     fi | ||||
| done | ||||
|  | ||||
| EOF | ||||
| )" | ||||
|  | ||||
| echo "${config_watcher}" > /usr/local/bin/config_watcher.sh | ||||
| chmod +x /usr/local/bin/config_watcher.sh | ||||
|  | ||||
| mkdir -p /var/log/supervisor | ||||
| mkdir -p /etc/supervisor/conf.d | ||||
|  | ||||
| @@ -32,7 +56,16 @@ stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
|  | ||||
| [program:config-watcher] | ||||
| command=/bin/bash -c "inotifywait -mq -e modify "${NATS_CONFIG}" | while read event; do nats-server --signal reload; done;" | ||||
| command=/bin/bash /usr/local/bin/config_watcher.sh | ||||
| startsecs=10 | ||||
| autorestart=true | ||||
| startretries=1 | ||||
| stdout_logfile=/dev/fd/1 | ||||
| stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
|  | ||||
| [program:nats-api] | ||||
| command=/bin/bash -c "/usr/local/bin/nats-api -config ${NATS_API_CONFIG}" | ||||
| stdout_logfile=/dev/fd/1 | ||||
| stdout_logfile_maxbytes=0 | ||||
| redirect_stderr=true | ||||
|   | ||||
| @@ -5,10 +5,15 @@ set -e | ||||
| : "${WORKER_CONNECTIONS:=2048}" | ||||
| : "${APP_PORT:=80}" | ||||
| : "${API_PORT:=80}" | ||||
| : "${NGINX_RESOLVER:=127.0.0.11}" | ||||
| : "${BACKEND_SERVICE:=tactical-backend}" | ||||
| : "${FRONTEND_SERVICE:=tactical-frontend}" | ||||
| : "${MESH_SERVICE:=tactical-meshcentral}" | ||||
| : "${WEBSOCKETS_SERVICE:=tactical-websockets}" | ||||
| : "${DEV:=0}" | ||||
|  | ||||
| CERT_PRIV_PATH=${TACTICAL_DIR}/certs/privkey.pem | ||||
| CERT_PUB_PATH=${TACTICAL_DIR}/certs/fullchain.pem | ||||
| : "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}" | ||||
| : "${CERT_PUB_PATH:=${TACTICAL_DIR}/certs/fullchain.pem}" | ||||
|  | ||||
| mkdir -p "${TACTICAL_DIR}/certs" | ||||
|  | ||||
| @@ -34,7 +39,7 @@ fi | ||||
| if [[ $DEV -eq 1 ]]; then | ||||
|     API_NGINX=" | ||||
|         #Using variable to disable start checks | ||||
|         set \$api http://tactical-backend:${API_PORT}; | ||||
|         set \$api http://${BACKEND_SERVICE}:${API_PORT}; | ||||
|         proxy_pass \$api; | ||||
|         proxy_http_version  1.1; | ||||
|         proxy_cache_bypass  \$http_upgrade; | ||||
| @@ -51,7 +56,7 @@ if [[ $DEV -eq 1 ]]; then | ||||
| else | ||||
|     API_NGINX=" | ||||
|         #Using variable to disable start checks | ||||
|         set \$api tactical-backend:${API_PORT}; | ||||
|         set \$api ${BACKEND_SERVICE}:${API_PORT}; | ||||
|  | ||||
|         include         uwsgi_params; | ||||
|         uwsgi_pass      \$api; | ||||
| @@ -61,7 +66,7 @@ fi | ||||
| nginx_config="$(cat << EOF | ||||
| # backend config | ||||
| server  { | ||||
|     resolver 127.0.0.11 valid=30s; | ||||
|     resolver ${NGINX_RESOLVER} valid=30s; | ||||
|  | ||||
|     server_name ${API_HOST}; | ||||
|  | ||||
| @@ -80,7 +85,7 @@ server  { | ||||
|     } | ||||
|  | ||||
|     location ~ ^/ws/ { | ||||
|         set \$api http://tactical-websockets:8383; | ||||
|         set \$api http://${WEBSOCKETS_SERVICE}:8383; | ||||
|         proxy_pass \$api; | ||||
|  | ||||
|         proxy_http_version 1.1; | ||||
| @@ -111,13 +116,13 @@ server { | ||||
|  | ||||
| # frontend config | ||||
| server  { | ||||
|     resolver 127.0.0.11 valid=30s; | ||||
|     resolver ${NGINX_RESOLVER} valid=30s; | ||||
|      | ||||
|     server_name ${APP_HOST}; | ||||
|  | ||||
|     location / { | ||||
|         #Using variable to disable start checks | ||||
|         set \$app http://tactical-frontend:${APP_PORT}; | ||||
|         set \$app http://${FRONTEND_SERVICE}:${APP_PORT}; | ||||
|  | ||||
|         proxy_pass \$app; | ||||
|         proxy_http_version  1.1; | ||||
| @@ -149,7 +154,7 @@ server { | ||||
|  | ||||
| # meshcentral config | ||||
| server { | ||||
|     resolver 127.0.0.11 valid=30s; | ||||
|     resolver ${NGINX_RESOLVER} valid=30s; | ||||
|  | ||||
|     listen 443 ssl; | ||||
|     proxy_send_timeout 330s; | ||||
| @@ -163,7 +168,7 @@ server { | ||||
|  | ||||
|     location / { | ||||
|         #Using variable to disable start checks | ||||
|         set \$meshcentral http://tactical-meshcentral:443; | ||||
|         set \$meshcentral http://${MESH_SERVICE}:443; | ||||
|  | ||||
|         proxy_pass \$meshcentral; | ||||
|         proxy_http_version 1.1; | ||||
| @@ -180,7 +185,7 @@ server { | ||||
| } | ||||
|  | ||||
| server { | ||||
|     resolver 127.0.0.11 valid=30s; | ||||
|     resolver ${NGINX_RESOLVER} valid=30s; | ||||
|  | ||||
|     listen 80; | ||||
|     server_name ${MESH_HOST}; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # creates python virtual env | ||||
| FROM python:3.9.6-slim AS CREATE_VENV_STAGE | ||||
| FROM python:3.9.9-slim AS CREATE_VENV_STAGE | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
| @@ -23,7 +23,7 @@ RUN apt-get update && \ | ||||
|  | ||||
|  | ||||
| # runtime image | ||||
| FROM python:3.9.6-slim | ||||
| FROM python:3.9.9-slim | ||||
|  | ||||
| # set env variables | ||||
| ENV VIRTUAL_ENV /opt/venv | ||||
| @@ -50,10 +50,6 @@ RUN apt-get update && \ | ||||
|  | ||||
| SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] | ||||
|  | ||||
| # copy nats-api file | ||||
| COPY natsapi/bin/nats-api /usr/local/bin/ | ||||
| RUN chmod +x /usr/local/bin/nats-api | ||||
|  | ||||
| # docker init | ||||
| COPY docker/containers/tactical/entrypoint.sh / | ||||
| RUN chmod +x /entrypoint.sh | ||||
|   | ||||
| @@ -9,7 +9,8 @@ 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}:443}" | ||||
| : "${MESH_USER:=meshcentral}" | ||||
| : "${MESH_PASS:=meshcentralpass}" | ||||
| : "${MESH_HOST:=tactical-meshcentral}" | ||||
| @@ -17,6 +18,8 @@ set -e | ||||
| : "${APP_HOST:=tactical-frontend}" | ||||
| : "${REDIS_HOST:=tactical-redis}" | ||||
|  | ||||
| : "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}" | ||||
| : "${CERT_PUB_PATH:=${TACTICAL_DIR}/certs/fullchain.pem}" | ||||
|  | ||||
| function check_tactical_ready { | ||||
|   sleep 15 | ||||
| @@ -44,7 +47,7 @@ if [ "$1" = 'tactical-init' ]; then | ||||
|     sleep 5 | ||||
|   done | ||||
|  | ||||
|   until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do | ||||
|   until (echo > /dev/tcp/"${MESH_SERVICE}"/443) &> /dev/null; do | ||||
|     echo "waiting for meshcentral container to be ready..." | ||||
|     sleep 5 | ||||
|   done | ||||
| @@ -61,8 +64,8 @@ DEBUG = False | ||||
|  | ||||
| DOCKER_BUILD = True | ||||
|  | ||||
| CERT_FILE = '/opt/tactical/certs/fullchain.pem' | ||||
| KEY_FILE = '/opt/tactical/certs/privkey.pem' | ||||
| CERT_FILE = '${CERT_PUB_PATH}' | ||||
| KEY_FILE = '${CERT_PRIV_PATH}' | ||||
|  | ||||
| EXE_DIR = '/opt/tactical/api/tacticalrmm/private/exe' | ||||
| LOG_DIR = '/opt/tactical/api/tacticalrmm/private/log' | ||||
| @@ -92,7 +95,7 @@ MESH_USERNAME = '${MESH_USER}' | ||||
| MESH_SITE = 'https://${MESH_HOST}' | ||||
| MESH_TOKEN_KEY = '${MESH_TOKEN}' | ||||
| REDIS_HOST    = '${REDIS_HOST}' | ||||
| MESH_WS_URL = 'ws://${MESH_CONTAINER}:443' | ||||
| MESH_WS_URL = '${MESH_WS_URL}' | ||||
| ADMIN_ENABLED = False | ||||
| EOF | ||||
| )" | ||||
| @@ -129,7 +132,9 @@ EOF | ||||
|   python manage.py load_chocos | ||||
|   python manage.py load_community_scripts | ||||
|   python manage.py reload_nats | ||||
|   python manage.py create_natsapi_conf | ||||
|   python manage.py create_installer_user | ||||
|   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 | ||||
| @@ -167,4 +172,4 @@ if [ "$1" = 'tactical-websockets' ]; then | ||||
|   export DJANGO_SETTINGS_MODULE=tacticalrmm.settings | ||||
|  | ||||
|   daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0 | ||||
| fi | ||||
| fi | ||||
|   | ||||
| @@ -8,17 +8,16 @@ networks: | ||||
|       driver: default | ||||
|       config: | ||||
|         - subnet: 172.20.0.0/24 | ||||
|   api-db: | ||||
|   redis: | ||||
|   mesh-db: | ||||
|   api-db: null | ||||
|   redis: null | ||||
|   mesh-db: null # docker managed persistent volumes | ||||
|  | ||||
| # docker managed persistent volumes | ||||
| volumes: | ||||
|   tactical_data: | ||||
|   postgres_data: | ||||
|   mongo_data: | ||||
|   mesh_data: | ||||
|   redis_data: | ||||
|   tactical_data: null | ||||
|   postgres_data: null | ||||
|   mongo_data: null | ||||
|   mesh_data: null | ||||
|   redis_data: null | ||||
|  | ||||
| services: | ||||
|   # postgres database for api service | ||||
| @@ -41,7 +40,7 @@ services: | ||||
|     image: redis:6.0-alpine | ||||
|     command: redis-server --appendonly yes | ||||
|     restart: always | ||||
|     volumes:  | ||||
|     volumes: | ||||
|       - redis_data:/data | ||||
|     networks: | ||||
|       - redis | ||||
| @@ -51,7 +50,7 @@ services: | ||||
|     container_name: trmm-init | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     restart: on-failure | ||||
|     command: ["tactical-init"] | ||||
|     command: [ "tactical-init" ] | ||||
|     environment: | ||||
|       POSTGRES_USER: ${POSTGRES_USER} | ||||
|       POSTGRES_PASS: ${POSTGRES_PASS} | ||||
| @@ -63,13 +62,13 @@ services: | ||||
|       TRMM_PASS: ${TRMM_PASS} | ||||
|     depends_on: | ||||
|       - tactical-postgres | ||||
|       - tactical-meshcentral     | ||||
|       - tactical-meshcentral | ||||
|     networks: | ||||
|       - api-db | ||||
|       - proxy | ||||
|     volumes: | ||||
|       - tactical_data:/opt/tactical | ||||
|    | ||||
|  | ||||
|   # nats | ||||
|   tactical-nats: | ||||
|     container_name: trmm-nats | ||||
| @@ -82,6 +81,7 @@ services: | ||||
|     volumes: | ||||
|       - tactical_data:/opt/tactical | ||||
|     networks: | ||||
|       api-db: null | ||||
|       proxy: | ||||
|         aliases: | ||||
|           - ${API_HOST} | ||||
| @@ -91,7 +91,7 @@ services: | ||||
|     container_name: trmm-meshcentral | ||||
|     image: ${IMAGE_REPO}tactical-meshcentral:${VERSION} | ||||
|     restart: always | ||||
|     environment:  | ||||
|     environment: | ||||
|       MESH_HOST: ${MESH_HOST} | ||||
|       MESH_USER: ${MESH_USER} | ||||
|       MESH_PASS: ${MESH_PASS} | ||||
| @@ -102,7 +102,7 @@ services: | ||||
|       proxy: | ||||
|         aliases: | ||||
|           - ${MESH_HOST} | ||||
|       mesh-db: | ||||
|       mesh-db: null | ||||
|     volumes: | ||||
|       - tactical_data:/opt/tactical | ||||
|       - mesh_data:/home/node/app/meshcentral-data | ||||
| @@ -137,7 +137,7 @@ services: | ||||
|   tactical-backend: | ||||
|     container_name: trmm-backend | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     command: ["tactical-backend"] | ||||
|     command: [ "tactical-backend" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - proxy | ||||
| @@ -152,7 +152,7 @@ services: | ||||
|   tactical-websockets: | ||||
|     container_name: trmm-websockets | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     command: ["tactical-websockets"] | ||||
|     command: [ "tactical-websockets" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - proxy | ||||
| @@ -188,7 +188,7 @@ services: | ||||
|   tactical-celery: | ||||
|     container_name: trmm-celery | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     command: ["tactical-celery"] | ||||
|     command: [ "tactical-celery" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - redis | ||||
| @@ -204,7 +204,7 @@ services: | ||||
|   tactical-celerybeat: | ||||
|     container_name: trmm-celerybeat | ||||
|     image: ${IMAGE_REPO}tactical:${VERSION} | ||||
|     command: ["tactical-celerybeat"] | ||||
|     command: [ "tactical-celerybeat" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - proxy | ||||
|   | ||||
| @@ -42,7 +42,7 @@ Navigate to an agent with ConnectWise Service running (or apply using **Settings | ||||
| Go to Tasks.</br> | ||||
| Add Task</br> | ||||
| **Select Script** = `ScreenConnect - Get GUID for client` (this is a builtin script from script library)</br> | ||||
| **Script argument** = `-serviceName{{client.ScreenConnectService}}`</br> | ||||
| **Script argument** = `-serviceName {{client.ScreenConnectService}}`</br> | ||||
| **Descriptive name of task** = `Collects the Machine GUID for ScreenConnect.`</br> | ||||
| **Collector Task** = `CHECKED`</br> | ||||
| **Custom Field to update** = `ScreenConectGUID`</br> | ||||
| @@ -61,6 +61,12 @@ It should ask you to sign into your Connectwise Control server if you are not al | ||||
|  | ||||
| ***** | ||||
|  | ||||
| ## Install Screenconnect via Tactical | ||||
|  | ||||
| Use the [Screenconnect AIO script](https://github.com/wh1te909/tacticalrmm/blob/develop/scripts/Win_ScreenConnectAIO.ps1) | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Install Tactical RMM via Screeconnect commands window | ||||
|  | ||||
| 1. Create a Deplopment under **Agents > Manage Deployments** | ||||
|   | ||||
							
								
								
									
										42
									
								
								docs/docs/3rdparty_splashtop.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								docs/docs/3rdparty_splashtop.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| # Splashtop | ||||
|  | ||||
| ## Splashtop Integration | ||||
|  | ||||
|  | ||||
| From the UI go to **Settings > Global Settings > CUSTOM FIELDS > Agents** | ||||
|  | ||||
| Add Custom Field</br> | ||||
| **Target** = `Agent`</br> | ||||
| **Name** = `SplashtopSUUID`</br> | ||||
| **Field Type** = `Text`</br> | ||||
|  | ||||
|  | ||||
|  | ||||
| While in Global Settings go to **URL ACTIONS** | ||||
|  | ||||
| Add a URL Action</br> | ||||
| **Name** = `Splashtop`</br> | ||||
| **Description** = `Connect to a Splashtop client`</br> | ||||
| **URL Pattern** = | ||||
|  | ||||
| ```html | ||||
| st-business://com.splashtop.business?account=&uuid={{agent.SplashtopSUUID}}&sessiontype=remote | ||||
| ``` | ||||
|  | ||||
| Navigate to an agent with Splashtop running (or apply using **Settings > Automation Manager**).</br> | ||||
| Go to Tasks.</br> | ||||
| Add Task</br> | ||||
| **Select Script** = `Splashtop - Get SUUID for client` (this is a builtin script from script library)</br> | ||||
| **Descriptive name of task** = `Obtain Splashtop SUUID from device registry.`</br> | ||||
| **Collector Task** = `CHECKED`</br> | ||||
| **Custom Field to update** = `SplashtopSUUID`</br> | ||||
|  | ||||
|  | ||||
|  | ||||
| Click **Next**</br> | ||||
| Check **Manual**</br> | ||||
| Click **Add Task** | ||||
|  | ||||
| Right click on the newly created task and click **Run Task Now**. | ||||
|  | ||||
| Give it a second to execute then right click the agent that you are working with and go to **Run URL Action > Splashtop** | ||||
							
								
								
									
										144
									
								
								docs/docs/av.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								docs/docs/av.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
|  | ||||
| # Antivirus | ||||
|  | ||||
| They are usually fraught with false-positives because we live in a world of complex greys, not black and white.  | ||||
|  | ||||
| At the moment, Microsoft Windows Defender thinks a go executable with virtually nothing in it is the "Trojan:Win32/Wacatac.B!ml" virus <https://old.reddit.com/r/golang/comments/s1bh01/goexecutables_and_windows_defender/> | ||||
|  | ||||
| At Tactical we recommend:  | ||||
|  | ||||
| 1. No 3rd party AV | ||||
| 2. Use the `Defender Status Report` script (Task > Run Daily - Use Automation manager) to monitor machines: <https://github.com/wh1te909/tacticalrmm/blob/develop/scripts/Win_Defender_Status_Report.ps1> | ||||
| 3. If you want to lock a system down, run the `Defender Enable` script (test in your environment, because it can stop Microsoft Office from opening docs) that will turn on Protected Folders: <https://github.com/wh1te909/tacticalrmm/blob/develop/scripts/Win_Defender_Enable.ps1> and you will be extremely safe. Annoyed, but safe. Use [this](https://github.com/amidaware/trmm-awesome/blob/main/scripts/Windows_Defender_Allowed_List.ps1) as an Exclusion List for Protected Folders items. | ||||
|  | ||||
| Be aware there is also [a powershell script](https://github.com/wh1te909/tacticalrmm/blob/develop/scripts/Win_TRMM_AV_Update_Exclusion.ps1) to add TRMM exclusions specific to Windows Defender | ||||
|  | ||||
| !!!note | ||||
|     If you need to use 3rd party AV, add the necessary exclusions (see below for examples) and submit the exe's as safe | ||||
|  | ||||
| ## Bitdefender Gravityzone | ||||
|  | ||||
| Admin URL: <https://cloud.gravityzone.bitdefender.com/> | ||||
|  | ||||
| To exclude URLs: Policies > {policy name} > Network Protection > Content Control > Settings > Exclusions | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Webroot | ||||
|  | ||||
| Admin URL: | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Sophos | ||||
|  | ||||
| ### Sophos Central Admin | ||||
|  | ||||
| Go To Global Settings >> General >> Global Exclusions >> Add Exclusion | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### Sophos XG Firewall | ||||
|  | ||||
|  | ||||
|  | ||||
| Log into Sophos Central Admin | ||||
|  | ||||
| Admin URL: <https://cloud.sophos.com/> | ||||
|  | ||||
| Log into the Sophos XG Firewall | ||||
|  | ||||
| Go To System >> Hosts and services >> FQDN Host Group and create a new group | ||||
|  | ||||
|  | ||||
|  | ||||
| Go To System >> Hosts and services >> FQDN Host | ||||
|  | ||||
| Create the following 3 hosts and add each to your FQDN host group. | ||||
|  | ||||
| - api.yourdomain.com | ||||
| - mesh.yourdomain.com | ||||
| - rmm.yourdomain.com (Optional if you want your client to have GUI access to Tactical RMM) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| Go To Hosts and services >> Services and create the following services | ||||
|  | ||||
| - Name: Tactical-Service-4222 | ||||
|     - Protocol: TCP | ||||
|     - Source port: 1:65535 | ||||
|     - Destination port: 4222 | ||||
| - Name: Tactical-Service-443 | ||||
|     - Protocol: TCP | ||||
|     - Source port: 1:65535 | ||||
|     - Destination port: 443 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| Go To Hosts and services >> Service group and create the following service group | ||||
|  | ||||
|  | ||||
|  | ||||
| Go To Protect >> Rules and policies and add a firewall rule | ||||
|  | ||||
| - Rule name: Tactical Rule | ||||
| - Rule position: Top | ||||
| - Source zones: LAN | ||||
| - Source networks: ANY | ||||
| - Destination zones: WAN | ||||
| - Destination networks: Your FQDN Host Group | ||||
| - Services: Tactical Services | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| Optionally select Log Firewall Traffic checkbox for troubleshooting. | ||||
|  | ||||
| ## ESET ESMC Console | ||||
|  | ||||
| There are two spots: | ||||
|  | ||||
| 1. In the Detection Engine -> Performance Exclusions | ||||
| 2. Web Access Protection -> URL Address Management | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Backing up the RMM | ||||
| ## Backing up the RMM | ||||
|  | ||||
| !!!note | ||||
|         This is only applicable for the standard install, not Docker installs. | ||||
| @@ -27,3 +27,21 @@ chmod +x backup.sh | ||||
| The backup tar file will be saved in `/rmmbackups` with the following format: | ||||
|  | ||||
| `rmm-backup-CURRENTDATETIME.tar` | ||||
|  | ||||
| ## Schedule to run daily via cron | ||||
|  | ||||
| Make a symlink in `/etc/cron.d` (daily cron jobs) with these contents `00 18 * * * tactical /rmm/backup.sh` to run at 6pm daily. | ||||
|  | ||||
| ```bash | ||||
| echo -e "\n" >> /rmm/backup.sh | ||||
| sudo ln -s /rmm/backup.sh /etc/cron.daily/ | ||||
| ``` | ||||
|  | ||||
| !!!warning | ||||
|     Currently the backup script doesn't have any pruning functions so the folder will grow forever without periodic cleanup | ||||
|  | ||||
| ## Video Walkthru | ||||
|  | ||||
| <div class="video-wrapper"> | ||||
|   <iframe width="320" height="180" src="https://www.youtube.com/embed/rC0NgYJUf_8" frameborder="0" allowfullscreen></iframe> | ||||
| </div> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|  | ||||
| Tactical RMM agents are now [code signed](https://comodosslstore.com/resources/what-is-microsoft-authenticode-code-signing-certificate/)! | ||||
|  | ||||
| To get access to code signed agents, you must be a [Github Sponsor](https://github.com/sponsors/wh1te909) with a minumum monthly donation of $50.00 | ||||
| To get access to code signed agents, you must be a [Github Sponsor](https://github.com/sponsors/wh1te909) with a minumum **monthly** donation of $50.00. If you signup for the $50, and then downgrade your auth token _**will be**_ invalidated and stop working. | ||||
|  | ||||
| Once you have become a sponsor, please email **support@amidaware.com** with your Github username (and Discord username if you're on our [Discord](https://discord.gg/upGTkWp)) | ||||
|  | ||||
|   | ||||
| @@ -87,7 +87,8 @@ npm install -g @quasar/cli | ||||
| quasar dev | ||||
| ``` | ||||
|  | ||||
| !!!info If you receive a CORS error when trying to log into your server via localhost or IP, try the following | ||||
| !!!info  | ||||
|     If you receive a CORS error when trying to log into your server via localhost or IP, try the following | ||||
| ```bash | ||||
| rm -rf node_modules .quasar | ||||
| npm install | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user