Compare commits

...

138 Commits

Author SHA1 Message Date
wh1te909
da185875bb Release 0.15.1 2022-10-19 22:51:33 +00:00
wh1te909
af16912541 bump versions [skip ci] 2022-10-19 22:50:59 +00:00
wh1te909
1bf9e2a5e6 update reqs 2022-10-19 06:50:01 +00:00
wh1te909
5a572651ff add token expired check 2022-10-19 06:45:53 +00:00
wh1te909
5a191e387f update tests 2022-10-19 04:02:21 +00:00
wh1te909
18f29f5790 update reqs 2022-10-18 19:54:48 +00:00
wh1te909
054a73e0f8 fix tests 2022-10-18 05:07:00 +00:00
wh1te909
14824db7b0 fix tests 2022-10-18 05:02:35 +00:00
wh1te909
721c48ea88 python 3.10.8 2022-10-18 04:54:04 +00:00
wh1te909
ed7bfcfb58 daphne/channels 4 2022-10-18 04:52:04 +00:00
wh1te909
773a40a126 bump docker nats 2022-10-18 04:50:48 +00:00
wh1te909
961252ef26 rework uwsgi conf 2022-10-18 00:23:10 +00:00
wh1te909
a2650f3c47 update setuptools 2022-10-18 00:21:11 +00:00
wh1te909
d71ee194e1 add optional nats monitoring 2022-10-18 00:12:57 +00:00
wh1te909
22e1a4cf41 update reqs 2022-10-14 07:41:16 +00:00
wh1te909
a50bf901d3 add check for wget fixes #1317 2022-10-14 07:39:27 +00:00
wh1te909
c9469635b5 remove unneeded chmod fixes #1307 2022-10-14 07:21:43 +00:00
Dan
36df3278e5 Merge pull request #1311 from dinger1986/develop
Update agent_linux.sh
2022-10-11 16:14:06 -07:00
dinger1986
cb2258aaa8 Update agent_linux.sh 2022-10-10 23:53:24 +01:00
wh1te909
0391d9eb7e update reqs 2022-10-10 17:07:00 +00:00
Dan
12698b4c20 Merge pull request #1310 from dinger1986/develop
Update agent_linux.sh
2022-10-10 10:01:50 -07:00
dinger1986
f7b9d459ab Update agent_linux.sh 2022-10-10 17:51:35 +01:00
wh1te909
65ab14e68b cleanup socket fixes #1210 2022-10-07 17:15:37 +00:00
Dan
93a5dd5de4 Merge pull request #1213 from stavros-k/json
Updating meshcentral's config casing to match upstreams json schema.
2022-10-06 09:38:16 -07:00
Dan
61807bdaaa Merge pull request #1301 from af7567/policyselect2
break from loop once a valid policy is found. remove old unused list
2022-10-06 09:36:59 -07:00
Dan
a1a5d1adba Merge pull request #1295 from silversword411/develop
typo
2022-10-06 09:36:20 -07:00
silversword411
9dd4aefea5 Merge branch 'amidaware:develop' into develop 2022-10-06 07:52:47 -04:00
Adam
db4540089a remove parentheses from if statement to fix codestyle test
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 13:17:48 +01:00
Adam
24c899c91a break from loop once a valid policy is found. remove old unused list
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 12:05:32 +01:00
wh1te909
ade1a73966 code cleanup and optimizations 2022-10-02 21:42:46 +00:00
wh1te909
fb9ec2b040 update reqs 2022-10-02 21:42:03 +00:00
wh1te909
3a683812e9 change tempdir 2022-10-02 21:37:04 +00:00
wh1te909
6d317603c9 fix script snippets fixes #1298 2022-09-27 23:21:26 +00:00
silversword411
5a3d2d196c typo 2022-09-25 17:21:17 -04:00
wh1te909
70e75a355c Release 0.15.0 2022-09-24 03:09:12 +00:00
wh1te909
4f885c9a79 bump versions 2022-09-24 03:08:44 +00:00
wh1te909
b519d2afac update readme for macOS support 2022-09-24 03:07:38 +00:00
wh1te909
6b61e3b76b bump docker nats 2022-09-24 02:52:12 +00:00
wh1te909
30b9c72c31 bump nats 2022-09-24 01:51:59 +00:00
wh1te909
385bf74f6e change dl cmd 2022-09-24 01:51:52 +00:00
wh1te909
be5615e530 bump web dev 2022-09-23 23:07:48 +00:00
wh1te909
d81a03c093 mac agent 2022-09-23 22:57:29 +00:00
wh1te909
f8249c8267 switch to corp funding 2022-09-23 22:10:05 +00:00
wh1te909
5a1cbdcd3b update reqs 2022-09-23 06:59:44 +00:00
wh1te909
e0c99d87bd update template 2022-09-23 06:59:26 +00:00
wh1te909
548250029d add missing field to serializer fixes #1283 2022-09-13 19:18:16 +00:00
wh1te909
66a354dbdc back to dev [skip ci] 2022-09-12 07:03:05 +00:00
wh1te909
834e602686 Release 0.14.8 2022-09-12 06:57:39 +00:00
wh1te909
1f693ca4f6 bump versions 2022-09-12 06:50:55 +00:00
wh1te909
97a0bc6045 optimize query to use less ram 2022-09-12 05:33:07 +00:00
wh1te909
8b75cdfefd update bin 2022-09-12 01:44:21 +00:00
wh1te909
917aecf1ff update deps and go 1.19 2022-09-12 01:42:24 +00:00
wh1te909
663dcd0396 fix tests 2022-09-11 20:51:57 +00:00
wh1te909
8f2dffb1ad update reqs 2022-09-11 20:29:45 +00:00
wh1te909
20228e3d19 add postgres ready check 2022-09-11 20:23:18 +00:00
wh1te909
81c6cc11b3 update supported version 2022-09-11 01:37:45 +00:00
wh1te909
2ccacbe5f3 fix for newer mesh 2022-09-11 01:33:00 +00:00
Dan
a5345e8468 Merge pull request #1277 from af7567/winupdatesfix
When checking if patches are to be installed, don't exit the function after finding a patched machine.
2022-09-06 12:02:26 -07:00
Dan
8f5d62bb81 Merge pull request #1273 from silversword411/develop
Adding client and site to mgmt command find_software
2022-09-06 11:59:07 -07:00
Adam
28f6838560 continue to next agent rather than return from function, otherwise other agents won't receive updates
Signed-off-by: Adam <adam@csparker.co.uk>
2022-09-04 18:09:58 +01:00
silversword411
c86aacb31c format tweak 2022-09-02 17:24:33 -04:00
silversword411
f62f5192d6 Adding client and site to mgmt command find_software 2022-09-02 14:28:45 -04:00
wh1te909
b14ea1fe3e fix maintenance mode and assigning policy via agent context menu not saving 2022-08-24 08:29:13 +00:00
wh1te909
552633a00b fix configs 2022-08-24 08:27:50 +00:00
wh1te909
7faba2a690 add migrations for updated pytz 2022-08-24 07:33:55 +00:00
wh1te909
db910aff06 back to dev 2022-08-23 06:02:48 +00:00
wh1te909
72126052ad Release 0.14.7 2022-08-23 05:59:29 +00:00
wh1te909
75d9f6a7e7 bump versions 2022-08-23 05:09:55 +00:00
wh1te909
de677294c6 refactor 2022-08-23 00:54:07 +00:00
wh1te909
da1e6b8259 update reqs 2022-08-22 06:47:52 +00:00
wh1te909
a9633b3990 update reqs 2022-08-20 20:09:53 +00:00
sadnub
7ac9af1cc1 fix sed command to do an inplace update of nginx.conf 2022-08-17 11:52:54 -04:00
Dan
00d8b8cd61 Merge pull request #1244 from dinger1986/develop
Update troubleshoot_server.sh
2022-08-17 08:43:13 -07:00
dinger1986
af7ff7f5cf Update troubleshoot_server.sh
Change SSL check as wasnt working and add output
2022-08-17 11:42:05 +01:00
wh1te909
2a20719130 fix demo script check history graph 2022-08-17 06:57:05 +00:00
sadnub
f481940180 fix tests 2022-08-14 21:56:09 -04:00
sadnub
ca8824d1e3 fix clients not filtering by role in policy overview 2022-08-14 21:49:38 -04:00
sadnub
f4be199b77 fix checks/tasks return cache values for other plats 2022-08-14 19:45:11 -04:00
sadnub
6bcef8334e fix nginx entypoint 2022-08-14 10:26:55 -04:00
wh1te909
3955eff683 more dev setup 2022-08-14 08:17:08 +00:00
wh1te909
aa0f6ecd75 python 3.10.6 2022-08-14 08:14:59 +00:00
wh1te909
ef4a94ed78 more ansible 2022-08-12 17:21:25 +00:00
wh1te909
b5c803ce65 update reqs 2022-08-12 17:19:04 +00:00
wh1te909
d4325ed82e more ansible dev 2022-08-12 06:15:21 +00:00
wh1te909
3805fb8f26 expose more fields in search amidaware/tacticalrmm-web@93dbc74e33 closes #652 2022-08-12 01:23:33 +00:00
wh1te909
d4d938c655 update paths 2022-08-12 01:06:56 +00:00
wh1te909
1c6911e361 start moving to nested serializers 2022-08-10 07:15:23 +00:00
wh1te909
a1b364f337 drop support for agent < 2.0.0 2022-08-10 07:12:51 +00:00
wh1te909
ece5c3da86 update reqs 2022-08-10 00:49:20 +00:00
wh1te909
5d1ae6047b back to dev 2022-08-10 00:38:13 +00:00
wh1te909
5605c72253 Release 0.14.6 2022-08-09 21:47:20 +00:00
wh1te909
66bbcf0733 fix tests 2022-08-09 21:35:23 +00:00
wh1te909
acc23ea7bb bump versions 2022-08-09 21:18:41 +00:00
wh1te909
663bd0c9f0 remove dead code 2022-08-09 21:16:18 +00:00
wh1te909
39b1025dfa fix tests 2022-08-05 17:35:40 +00:00
sadnub
d2875e90b2 fix docker dev 2022-08-05 12:10:52 -04:00
wh1te909
ff461d1d02 fixes #1174 amidaware/tacticalrmm-web@76f330fb9c 2022-08-05 07:22:46 +00:00
wh1te909
58164ea2d3 dev 2022-08-05 05:57:38 +00:00
wh1te909
1bf4834004 Django 4.1 2022-08-04 23:43:57 +00:00
wh1te909
bf58d78281 fix return tuple formatting 2022-08-04 23:40:35 +00:00
wh1te909
0dc749bb3d Release 0.14.5 2022-08-01 22:57:01 +00:00
wh1te909
a8aedfde55 bump version 2022-08-01 22:56:21 +00:00
wh1te909
b174a89032 Release 0.14.4 2022-08-01 18:09:18 +00:00
wh1te909
9b92d1b673 bump version 2022-08-01 17:50:33 +00:00
wh1te909
febc9aed11 feat: run as user amidaware/tacticalrmm-web@137a5648ce amidaware/rmmagent@50cebb950d 2022-07-31 22:23:19 +00:00
wh1te909
de2462677e fix working dir 2022-07-31 21:31:58 +00:00
wh1te909
8bd94d46eb fix empty statement 2022-07-28 17:28:49 +00:00
wh1te909
d43cefe28f add file associations for yaml [skip ci] 2022-07-27 07:31:54 +00:00
wh1te909
b82874e261 back to develop 2022-07-27 07:30:53 +00:00
wh1te909
8554cb5d6c Release 0.14.3 2022-07-27 07:18:55 +00:00
wh1te909
f901614056 bump version 2022-07-27 06:11:41 +00:00
wh1te909
b555d217ab remove check 2022-07-27 06:10:33 +00:00
wh1te909
775c600234 docker nginx changes 2022-07-27 04:19:17 +00:00
wh1te909
128f2570b8 catch exception if mesh is down 2022-07-27 02:01:29 +00:00
wh1te909
3cd53e79b4 add signing key 2022-07-27 01:59:55 +00:00
wh1te909
ebba84ffda switch to official nginx repo to get latest version 2022-07-26 08:09:49 +00:00
wh1te909
1e1a42fe98 update web ver 2022-07-26 08:08:43 +00:00
wh1te909
8a744a440d update reqs 2022-07-26 07:47:21 +00:00
wh1te909
f4fc3c7d55 unused var 2022-07-26 04:39:11 +00:00
wh1te909
0594d121de add agent ver to status closes #1224 2022-07-24 01:18:54 +00:00
wh1te909
12c85d6234 start ansible role to deploy dev environment 2022-07-20 07:16:47 +00:00
Stavros kois
87d05223af apply json fix to docker aswell 2022-07-18 22:08:06 +03:00
Stavros kois
babf6366e8 chore(install script): use same casing as the json schema 2022-07-18 21:13:20 +03:00
wh1te909
5e37728f66 remove devskum 2022-07-18 17:22:24 +00:00
wh1te909
e8e19fede7 don't allow dates in past #1174 2022-07-18 08:04:15 +00:00
wh1te909
e565dbfa66 invalidate cache on policy script change 2022-07-12 20:16:07 +00:00
wh1te909
d180d6820c back to develop 2022-07-10 03:35:15 +00:00
wh1te909
7f252e9b7c Release 0.14.2 2022-07-10 03:34:02 +00:00
wh1te909
41db8681f8 no sudo 2022-07-10 00:38:09 +00:00
wh1te909
26cd58fd6d bump version 2022-07-10 00:16:32 +00:00
wh1te909
63c7e1aa9d update reqs 2022-07-10 00:16:13 +00:00
wh1te909
d5a6063e5e remove extra space 2022-07-09 09:31:05 +00:00
wh1te909
00affdbdec update supported version 2022-07-09 09:30:58 +00:00
wh1te909
db3f0bbd4f increase nginx open file limit 2022-07-09 08:09:24 +00:00
wh1te909
020a59cb97 remove un-needed expose 2022-07-09 08:08:09 +00:00
wh1te909
ff4fa6402d back to dev 2022-07-08 06:40:55 +00:00
wh1te909
80f7555499 Release 0.14.1 2022-07-08 06:40:15 +00:00
wh1te909
10cc187c5d bump versions 2022-07-08 06:38:20 +00:00
118 changed files with 3330 additions and 822 deletions

View File

@@ -1,11 +1,11 @@
# pulls community scripts from git repo
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
FROM python:3.10.8-slim AS GET_SCRIPTS_STAGE
RUN apt-get update && \
apt-get install -y --no-install-recommends git && \
git clone https://github.com/amidaware/community-scripts.git /community-scripts
FROM python:3.10-slim
FROM python:3.10.8-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

View File

@@ -22,22 +22,6 @@ services:
aliases:
- tactical-backend
app-dev:
container_name: trmm-app-dev
image: node:16-alpine
restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
user: 1000:1000
working_dir: /workspace/web
volumes:
- ..:/workspace:cached
ports:
- "8080:${APP_PORT}"
networks:
dev:
aliases:
- tactical-frontend
# nats
nats-dev:
container_name: trmm-nats-dev

View File

@@ -15,10 +15,7 @@ set -e
: "${MESH_PASS:=meshcentralpass}"
: "${MESH_HOST:=tactical-meshcentral}"
: "${API_HOST:=tactical-backend}"
: "${APP_HOST:=tactical-frontend}"
: "${REDIS_HOST:=tactical-redis}"
: "${HTTP_PROTOCOL:=http}"
: "${APP_PORT:=8080}"
: "${API_PORT:=8000}"
: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}"
@@ -142,16 +139,6 @@ if [ "$1" = 'tactical-init-dev' ]; then
django_setup
# create .env file for frontend
webenv="$(cat << EOF
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_PORT = ${APP_PORT}
DOCKER_BUILD = 1
EOF
)"
echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
# chown everything to tactical user
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"

View File

@@ -1,41 +1,3 @@
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
asgiref==3.5.0
celery==5.2.6
channels==3.0.4
channels_redis==3.4.0
daphne==3.0.2
Django==4.0.4
django-cors-headers==3.11.0
django-ipware==4.0.2
django-rest-knox==4.2.0
djangorestframework==3.13.1
future==0.18.2
msgpack==1.0.3
nats-py==2.1.0
packaging==21.3
psycopg2-binary==2.9.3
pycryptodome==3.14.1
pyotp==2.6.0
pytz==2022.1
qrcode==7.3.1
redis==4.2.2
requests==2.27.1
twilio==7.8.1
urllib3==1.26.9
validators==0.18.2
websockets==10.2
drf_spectacular==0.22.0
meshctrl==0.1.15
hiredis==2.0.0
# dev
black==22.3.0
django-extensions==3.1.5
isort==5.10.1
mypy==0.942
types-pytz==2021.3.6
model-bakery==1.5.0
coverage==6.3.2
django-silk==4.3.0
django-stubs==1.10.1
djangorestframework-stubs==1.5.0
-r /workspace/api/tacticalrmm/requirements.txt
-r /workspace/api/tacticalrmm/requirements-dev.txt
-r /workspace/api/tacticalrmm/requirements-test.txt

4
.github/FUNDING.yml vendored
View File

@@ -1,9 +1,9 @@
# These are supported funding model platforms
github: wh1te909
github: amidaware
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: tacticalrmm
ko_fi: # tacticalrmm
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username

View File

@@ -14,22 +14,23 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ['3.10.4']
python-version: ["3.10.8"]
steps:
- uses: actions/checkout@v3
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '14'
postgresql db: 'pipeline'
postgresql user: 'pipeline'
postgresql password: 'pipeline123456'
postgresql version: "14"
postgresql db: "pipeline"
postgresql user: "pipeline"
postgresql password: "pipeline123456"
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Install redis
run: |
@@ -49,13 +50,13 @@ jobs:
pip install -r requirements.txt -r requirements-test.txt
- name: Codestyle black
working-directory: api/tacticalrmm
working-directory: api
run: |
black --exclude migrations/ --check tacticalrmm
if [ $? -ne 0 ]; then
exit 1
fi
- name: Run django tests
env:
GHACTIONS: "yes"

View File

@@ -1,34 +0,0 @@
# 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

1
.gitignore vendored
View File

@@ -55,3 +55,4 @@ coverage.lcov
daphne.sock.lock
.pytest_cache
coverage.xml
setup_dev.yml

17
.vscode/settings.json vendored
View File

@@ -1,7 +1,10 @@
{
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
"python.defaultInterpreterPath": "api/env/bin/python",
"python.languageServer": "Pylance",
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
"python.analysis.extraPaths": [
"api/tacticalrmm",
"api/env"
],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error",
"reportDuplicateImport": "error",
@@ -22,11 +25,17 @@
"**env/**"
],
"python.formatting.provider": "black",
"mypy.targets": ["api/tacticalrmm"],
"mypy.targets": [
"api/tacticalrmm"
],
"mypy.runUsingActiveInterpreter": true,
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"files.associations": {
"**/ansible/**/*.yml": "ansible",
"**/docker/**/docker-compose*.yml": "dockercompose"
},
"files.watcherExclude": {
"files.watcherExclude": {
"**/.git/objects/**": true,
@@ -66,4 +75,4 @@
"completeUnimported": true,
"staticcheck": true
}
}
}

View File

@@ -35,6 +35,9 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
## Linux agent versions supported
- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more!
## Mac agent versions supported
- 64 bit Intel and Apple Silicon (M1, M2)
## Installation / Backup / Restore / Usage
### Refer to the [documentation](https://docs.tacticalrmm.com)

View File

@@ -2,10 +2,7 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.12.2 | :white_check_mark: |
| < 0.12.2 | :x: |
[Latest](https://github.com/amidaware/tacticalrmm/releases/latest) release
## Reporting a Vulnerability

3
ansible/README.md Normal file
View File

@@ -0,0 +1,3 @@
### tacticalrmm ansible WIP
ansible role to setup a Debian 11 VM for tacticalrmm local development

View File

@@ -0,0 +1,40 @@
---
user: "tactical"
python_ver: "3.10.8"
go_ver: "1.18.5"
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
scripts_repo: "https://github.com/amidaware/community-scripts.git"
backend_dir: "/opt/trmm"
frontend_dir: "/opt/trmm-web"
scripts_dir: "/opt/trmm-community-scripts"
trmm_dir: "{{ backend_dir }}/api/tacticalrmm/tacticalrmm"
mesh_dir: "/opt/meshcentral"
settings_file: "{{ trmm_dir }}/settings.py"
local_settings_file: "{{ trmm_dir }}/local_settings.py"
fullchain_dest: /etc/ssl/certs/fullchain.pem
privkey_dest: /etc/ssl/certs/privkey.pem
base_pkgs:
- build-essential
- curl
- wget
- dirmngr
- gnupg
- openssl
- gcc
- g++
- make
- ca-certificates
- git
python_pkgs:
- zlib1g-dev
- libncurses5-dev
- libgdbm-dev
- libnss3-dev
- libssl-dev
- libreadline-dev
- libffi-dev
- libsqlite3-dev
- libbz2-dev

View File

@@ -0,0 +1,31 @@
worker_rlimit_nofile 1000000;
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
}
http {
sendfile on;
server_tokens off;
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Content-Type-Options nosniff;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -0,0 +1,2 @@
deb https://nginx.org/packages/debian/ bullseye nginx
deb-src https://nginx.org/packages/debian/ bullseye nginx

View File

@@ -0,0 +1,20 @@
" This file loads the default vim options at the beginning and prevents
" that they are being loaded again later. All other options that will be set,
" are added, or overwrite the default settings. Add as many options as you
" whish at the end of this file.
" Load the defaults
source $VIMRUNTIME/defaults.vim
" Prevent the defaults from being loaded again later, if the user doesn't
" have a local vimrc (~/.vimrc)
let skip_defaults_vim = 1
" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim)
" Add as many options as you whish
" Set the mouse mode to 'r'
if has('mouse')
set mouse=r
endif

View File

@@ -0,0 +1,633 @@
---
- name: set mouse mode for vim
tags: vim
become: yes
ansible.builtin.copy:
src: vimrc.local
dest: /etc/vim/vimrc.local
owner: "root"
group: "root"
mode: "0644"
- name: set max_user_watches
tags: sysctl
become: yes
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
line: fs.inotify.max_user_watches=524288
- name: reload sysctl
tags: sysctl
become: yes
ansible.builtin.command:
cmd: sysctl -p
- name: install base packages
tags: base
become: yes
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
update_cache: yes
with_items:
- "{{ base_pkgs }}"
- name: download and install golang
tags: golang
become: yes
ansible.builtin.unarchive:
src: "https://go.dev/dl/go{{ go_ver }}.linux-amd64.tar.gz"
dest: /usr/local
remote_src: yes
- name: add golang to path
become: yes
tags: golang
ansible.builtin.copy:
dest: /etc/profile.d/golang.sh
content: "PATH=$PATH:/usr/local/go/bin"
- name: install python prereqs
tags: python
become: yes
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
with_items:
- "{{ python_pkgs }}"
- name: get cpu core count
tags: python
ansible.builtin.command: nproc
register: numprocs
- name: Create python tmpdir
tags: python
ansible.builtin.tempfile:
state: directory
suffix: python
register: python_tmp
- name: download and extract python
tags: python
ansible.builtin.unarchive:
src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz"
dest: "{{ python_tmp.path }}"
remote_src: yes
- name: compile python
tags: python
ansible.builtin.shell:
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
cmd: |
./configure --enable-optimizations
make -j {{ numprocs.stdout }}
- name: alt install python
tags: python
become: yes
ansible.builtin.shell:
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
cmd: |
make altinstall
- name: install redis
tags: redis
become: yes
ansible.builtin.apt:
pkg: redis
state: present
- name: create postgres repo
tags: postgres
become: yes
ansible.builtin.copy:
content: "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main"
dest: /etc/apt/sources.list.d/pgdg.list
owner: root
group: root
mode: "0644"
- name: import postgres repo signing key
tags: postgres
become: yes
ansible.builtin.apt_key:
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
state: present
- name: install postgresql
tags: postgres
become: yes
ansible.builtin.apt:
pkg: postgresql-14
state: present
update_cache: yes
- name: ensure postgres enabled and started
tags: postgres
become: yes
ansible.builtin.service:
name: postgresql
enabled: yes
state: started
- name: setup database
tags: postgres
become: yes
become_user: postgres
ansible.builtin.shell:
cmd: |
psql -c "CREATE DATABASE tacticalrmm"
psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'"
psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'"
psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'"
psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'"
psql -c "ALTER ROLE {{ db_user }} CREATEDB"
psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}"
- name: create repo dirs
become: yes
tags: git
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
with_items:
- "{{ backend_dir }}"
- "{{ frontend_dir }}"
- "{{ scripts_dir }}"
- name: git clone repos
tags: git
ansible.builtin.git:
repo: "{{ item.repo }}"
dest: "{{ item.dest }}"
version: "{{ item.version }}"
with_items:
- {
repo: "{{ backend_repo }}",
dest: "{{ backend_dir }}",
version: develop,
}
- {
repo: "{{ frontend_repo }}",
dest: "{{ frontend_dir }}",
version: develop,
}
- { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main }
- name: get nats_server_ver
tags: nats
ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: nats_server_ver
- name: Create nats tmpdir
tags: nats
ansible.builtin.tempfile:
state: directory
suffix: nats
register: nats_tmp
- name: download and extract nats
tags: nats
ansible.builtin.unarchive:
src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64.tar.gz"
dest: "{{ nats_tmp.path }}"
remote_src: yes
- name: install nats
tags: nats
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64/nats-server"
dest: /usr/local/bin/nats-server
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: Create nodejs tmpdir
tags: nodejs
ansible.builtin.tempfile:
state: directory
suffix: nodejs
register: nodejs_tmp
- name: download nodejs setup
tags: nodejs
ansible.builtin.get_url:
url: https://deb.nodesource.com/setup_16.x
dest: "{{ nodejs_tmp.path }}/setup_node.sh"
mode: "0755"
- name: run node setup script
tags: nodejs
become: yes
ansible.builtin.command:
cmd: "{{ nodejs_tmp.path }}/setup_node.sh"
- name: install nodejs
tags: nodejs
become: yes
ansible.builtin.apt:
pkg: nodejs
state: present
update_cache: yes
- name: update npm
tags: nodejs
become: yes
ansible.builtin.shell:
cmd: npm install -g npm
- name: install quasar cli
tags: quasar
become: yes
ansible.builtin.shell:
cmd: npm install -g @quasar/cli
- name: install frontend
tags: quasar
ansible.builtin.shell:
chdir: "{{ frontend_dir }}"
cmd: npm install
- name: add quasar env
tags: quasar
ansible.builtin.template:
src: quasar.env.j2
dest: "{{ frontend_dir }}/.env"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0644"
- name: remove tempdirs
tags: cleanup
become: yes
ignore_errors: yes
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "{{ nats_tmp.path }}"
- "{{ python_tmp.path }}"
- "{{ nodejs_tmp.path }}"
- name: deploy fullchain
tags: certs
become: yes
ansible.builtin.copy:
src: "{{ fullchain_src }}"
dest: "{{ fullchain_dest }}"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0440"
- name: deploy privkey
tags: certs
become: yes
ansible.builtin.copy:
src: "{{ privkey_src }}"
dest: "{{ privkey_dest }}"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0440"
- name: import nginx signing key
tags: nginx
become: yes
ansible.builtin.apt_key:
url: https://nginx.org/packages/keys/nginx_signing.key
state: present
- name: add nginx repo
tags: nginx
become: yes
ansible.builtin.copy:
src: nginx.repo
dest: /etc/apt/sources.list.d/nginx.list
owner: "root"
group: "root"
mode: "0644"
- name: install nginx
tags: nginx
become: yes
ansible.builtin.apt:
pkg: nginx
state: present
update_cache: yes
- name: set nginx default conf
tags: nginx
become: yes
ansible.builtin.copy:
src: nginx-default.conf
dest: /etc/nginx/nginx.conf
owner: "root"
group: "root"
mode: "0644"
- name: create nginx dirs
become: yes
tags: nginx
ansible.builtin.file:
state: directory
path: "{{ item }}"
mode: "0755"
with_items:
- /etc/nginx/sites-available
- /etc/nginx/sites-enabled
- name: deploy nginx sites
become: yes
tags: nginx
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: root
group: root
with_items:
- { src: backend.nginx.j2, dest: /etc/nginx/sites-available/backend.conf }
- { src: mesh.nginx.j2, dest: /etc/nginx/sites-available/mesh.conf }
- name: enable nginx sites
become: yes
tags: nginx
ansible.builtin.file:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: root
group: root
state: link
with_items:
- {
src: /etc/nginx/sites-available/backend.conf,
dest: /etc/nginx/sites-enabled/backend.conf,
}
- {
src: /etc/nginx/sites-available/mesh.conf,
dest: /etc/nginx/sites-enabled/mesh.conf,
}
- name: ensure nginx enabled and restarted
tags: nginx
become: yes
ansible.builtin.service:
name: nginx
enabled: yes
state: restarted
- name: copy nats-api bin
tags: nats-api
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ backend_dir }}/natsapi/bin/nats-api"
dest: /usr/local/bin/nats-api
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: get setuptools_ver
tags: pip
ansible.builtin.shell: grep "^SETUPTOOLS_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: setuptools_ver
- name: get wheel_ver
tags: pip
ansible.builtin.shell: grep "^WHEEL_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: wheel_ver
- name: setup virtual env
tags: pip
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api"
cmd: python3.10 -m venv env
- name: update pip to latest
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
name: pip
state: latest
- name: install setuptools and wheel
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
name: "{{ item }}"
with_items:
- "setuptools=={{ setuptools_ver.stdout }}"
- "wheel=={{ wheel_ver.stdout }}"
- name: install python packages
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
chdir: "{{ backend_dir }}/api/tacticalrmm"
requirements: "{{ item }}"
with_items:
- requirements.txt
- requirements-dev.txt
- requirements-test.txt
- name: deploy django local settings
tags: django
ansible.builtin.template:
src: local_settings.j2
dest: "{{ local_settings_file }}"
mode: "0644"
owner: "{{ user }}"
group: "{{ user }}"
- name: setup django
tags: django
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api/tacticalrmm"
cmd: |
. ../env/bin/activate
python manage.py migrate --no-input
python manage.py collectstatic --no-input
python manage.py create_natsapi_conf
python manage.py load_chocos
python manage.py load_community_scripts
echo "from accounts.models import User; User.objects.create_superuser('{{ django_user }}', '{{ github_email }}', '{{ django_password }}') if not User.objects.filter(username='{{ django_user }}').exists() else 0;" | python manage.py shell
python manage.py create_installer_user
- name: deploy services
tags: services
become: yes
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: "root"
group: "root"
with_items:
- { src: nats-api.systemd.j2, dest: /etc/systemd/system/nats-api.service }
- { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service }
- { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service }
- name: import mongodb repo signing key
tags: mongo
become: yes
ansible.builtin.apt_key:
url: https://www.mongodb.org/static/pgp/server-4.4.asc
state: present
- name: setup mongodb repo
tags: mongo
become: yes
ansible.builtin.copy:
content: "deb https://repo.mongodb.org/apt/debian buster/mongodb-org/4.4 main"
dest: /etc/apt/sources.list.d/mongodb-org-4.4.list
owner: root
group: root
mode: "0644"
- name: install mongodb
tags: mongo
become: yes
ansible.builtin.apt:
pkg: mongodb-org
state: present
update_cache: yes
- name: ensure mongodb enabled and started
tags: mongo
become: yes
ansible.builtin.service:
name: mongod
enabled: yes
state: started
- name: get mesh_ver
tags: mesh
ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: mesh_ver
- name: create meshcentral data directory
tags: mesh
become: yes
ansible.builtin.file:
path: "{{ mesh_dir }}/meshcentral-data"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: install meshcentral
tags: mesh
ansible.builtin.command:
chdir: "{{ mesh_dir }}"
cmd: "npm install meshcentral@{{ mesh_ver.stdout }}"
- name: deploy mesh config
tags: mesh
ansible.builtin.template:
src: mesh.cfg.j2
dest: "{{ mesh_dir }}/meshcentral-data/config.json"
mode: "0644"
owner: "{{ user }}"
group: "{{ user }}"
- name: start meshcentral
tags: mesh
become: yes
ansible.builtin.systemd:
name: meshcentral.service
state: started
enabled: yes
daemon_reload: yes
- name: wait for meshcentral to be ready
tags: mesh
uri:
url: "https://{{ mesh }}"
return_content: yes
validate_certs: yes
status_code: 200
register: mesh_status
until: mesh_status.status == 200
retries: 20
delay: 3
- name: get meshcentral login token key
tags: mesh_key
ansible.builtin.command:
chdir: "{{ mesh_dir }}"
cmd: node node_modules/meshcentral --logintokenkey
register: mesh_token_key
- name: add mesh key to django settings file
tags: mesh_key
ansible.builtin.lineinfile:
path: "{{ local_settings_file }}"
line: 'MESH_TOKEN_KEY = "{{ mesh_token_key.stdout }}"'
- name: stop meshcentral service
tags: mesh_user
become: yes
ansible.builtin.service:
name: meshcentral.service
state: stopped
- name: create mesh user
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ mesh_dir }}"
cmd: |
node node_modules/meshcentral --createaccount {{ mesh_user }} --pass {{ mesh_password }} --email {{ github_email }}
node node_modules/meshcentral --adminaccount {{ mesh_user }}
- name: start meshcentral service
tags: mesh_user
become: yes
ansible.builtin.service:
name: meshcentral.service
state: started
- name: wait for meshcentral to be ready
tags: mesh_user
uri:
url: "https://{{ mesh }}"
return_content: yes
validate_certs: yes
status_code: 200
register: mesh_status
until: mesh_status.status == 200
retries: 20
delay: 3
- name: create mesh device group
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ mesh_dir }}"
cmd: |
node node_modules/meshcentral/meshctrl.js --url wss://{{ mesh }}:443 --loginuser {{ mesh_user }} --loginpass {{ mesh_password }} AddDeviceGroup --name TacticalRMM
- name: finish up django
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api/tacticalrmm"
cmd: |
. ../env/bin/activate
python manage.py initial_db_setup
python manage.py reload_nats
- name: restart services
tags: services
become: yes
ansible.builtin.systemd:
daemon_reload: yes
enabled: yes
state: restarted
name: "{{ item }}.service"
with_items:
- nats
- nats-api

View File

@@ -0,0 +1,20 @@
server {
listen 443 ssl reuseport;
listen [::]:443 ssl;
server_name {{ api }};
client_max_body_size 300M;
ssl_certificate {{ fullchain_dest }};
ssl_certificate_key {{ privkey_dest }};
location ~ ^/natsws {
proxy_pass http://127.0.0.1:9235;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,25 @@
SECRET_KEY = "{{ django_secret }}"
DEBUG = True
ALLOWED_HOSTS = ['{{ api }}']
ADMIN_URL = "admin/"
CORS_ORIGIN_WHITELIST = [
"http://{{ rmm }}:8080",
"https://{{ rmm }}:8080",
]
CORS_ORIGIN_ALLOW_ALL = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'tacticalrmm',
'USER': '{{ db_user }}',
'PASSWORD': '{{ db_passwd }}',
'HOST': 'localhost',
'PORT': '5432',
}
}
REDIS_HOST = "localhost"
ADMIN_ENABLED = True
CERT_FILE = "{{ fullchain_src }}"
KEY_FILE = "{{ privkey_src }}"
MESH_USERNAME = "{{ mesh_user }}"
MESH_SITE = "https://{{ mesh }}"

View File

@@ -0,0 +1,33 @@
{
"settings": {
"Cert": "{{ mesh }}",
"MongoDb": "mongodb://127.0.0.1:27017",
"MongoDbName": "meshcentral",
"WANonly": true,
"Minify": 1,
"Port": 4430,
"AliasPort": 443,
"RedirPort": 800,
"AllowLoginToken": true,
"AllowFraming": true,
"AgentPong": 300,
"AllowHighQualityDesktop": true,
"TlsOffload": "127.0.0.1",
"agentCoreDump": false,
"Compression": true,
"WsCompression": true,
"AgentWsCompression": true,
"MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 }
},
"domains": {
"": {
"Title": "Tactical RMM",
"Title2": "Tactical RMM",
"NewAccounts": false,
"CertUrl": "https://{{ mesh }}:443/",
"GeoLocation": true,
"CookieIpCheck": false,
"mstsc": true
}
}
}

View File

@@ -0,0 +1,22 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
proxy_send_timeout 330s;
proxy_read_timeout 330s;
server_name {{ mesh }};
ssl_certificate {{ fullchain_dest }};
ssl_certificate_key {{ privkey_dest }};
ssl_session_cache shared:WEBSSL:10m;
location / {
proxy_pass http://127.0.0.1:4430/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,17 @@
[Unit]
Description=MeshCentral Server
After=network.target mongod.service nginx.service
[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/usr/bin/node node_modules/meshcentral
Environment=NODE_ENV=production
WorkingDirectory={{ mesh_dir }}
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=TacticalRMM Nats Api
After=nats.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nats-api -config {{ backend_dir }}/api/tacticalrmm/nats-api.conf
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,18 @@
[Unit]
Description=NATS Server
After=network.target
[Service]
PrivateTmp=true
Type=simple
ExecStart=/usr/local/bin/nats-server -c {{ backend_dir }}/api/tacticalrmm/nats-rmm.conf
ExecReload=/usr/bin/kill -s HUP $MAINPID
ExecStop=/usr/bin/kill -s SIGINT $MAINPID
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=5s
LimitNOFILE=1000000
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,4 @@
DEV_URL = "http://{{ api }}:8000"
DEV_HOST = "{{ rmm }}"
DEV_PORT = "8080"
USE_HTTPS = false

View File

@@ -0,0 +1,20 @@
---
- hosts: "{{ target }}"
vars:
ansible_user: tactical
fullchain_src: /path/to/fullchain.pem
privkey_src: /path/to/privkey.pem
api: "api.example.com"
rmm: "rmm.example.com"
mesh: "mesh.example.com"
github_username: "changeme"
github_email: "changeme@example.com"
mesh_user: "changeme"
mesh_password: "changeme"
db_user: "changeme"
db_passwd: "changeme"
django_secret: "changeme"
django_user: "changeme"
django_password: "changeme"
roles:
- trmm_dev

View File

@@ -1,5 +1,6 @@
import os
import subprocess
from contextlib import suppress
import pyotp
from django.core.management.base import BaseCommand
@@ -25,7 +26,7 @@ class Command(BaseCommand):
nginx = "/etc/nginx/sites-available/frontend.conf"
found = None
if os.path.exists(nginx):
try:
with suppress(Exception):
with open(nginx, "r") as f:
for line in f:
if "server_name" in line:
@@ -35,8 +36,6 @@ class Command(BaseCommand):
if found:
rep = found.replace("server_name", "").replace(";", "")
domain = "".join(rep.split())
except:
pass
code = pyotp.random_base32()
user.totp_key = code

View File

@@ -31,8 +31,8 @@ class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_roles")
else:
return _has_perm(r, "can_manage_roles")
return _has_perm(r, "can_manage_roles")
class APIKeyPerms(permissions.BasePermission):

View File

@@ -0,0 +1,18 @@
from typing import TYPE_CHECKING
from django.conf import settings
if TYPE_CHECKING:
from django.http import HttpRequest
from accounts.models import User
def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
root = (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
demo = (
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo

View File

@@ -22,18 +22,7 @@ from .serializers import (
UserSerializer,
UserUISerializer,
)
def _is_root_user(request, user) -> bool:
root = (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
demo = (
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo
from accounts.utils import is_root_user
class CheckCreds(KnoxLoginView):
@@ -93,7 +82,7 @@ class LoginView(KnoxLoginView):
login(request, user)
# save ip information
client_ip, is_routable = get_client_ip(request)
client_ip, _ = get_client_ip(request)
user.last_login_ip = client_ip
user.save()
@@ -159,7 +148,7 @@ class GetUpdateDeleteUser(APIView):
def put(self, request, pk):
user = get_object_or_404(User, pk=pk)
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI")
serializer = UserSerializer(instance=user, data=request.data, partial=True)
@@ -170,7 +159,7 @@ class GetUpdateDeleteUser(APIView):
def delete(self, request, pk):
user = get_object_or_404(User, pk=pk)
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be deleted from the UI")
user.delete()
@@ -183,7 +172,7 @@ class UserActions(APIView):
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI")
user.set_password(request.data["password"])
@@ -194,7 +183,7 @@ class UserActions(APIView):
# reset two factor token
def put(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI")
user.totp_key = ""

View File

@@ -64,7 +64,7 @@ class Command(BaseCommand):
try:
agent.delete()
except Exception as e:
err = f"Failed to delete agent {agent.hostname}: {str(e)}"
err = f"Failed to delete agent {agent.hostname}: {e}"
self.stdout.write(self.style.ERROR(err))
else:
deleted_count += 1

View File

@@ -568,6 +568,12 @@ class Command(BaseCommand):
check5_history.y = 1
else:
check5_history.y = 0
check5_history.results = {
"retcode": 0,
"stdout": None,
"stderr": None,
"execution_time": "4.0000",
}
check5_history.save()
check6 = Check()
@@ -595,6 +601,12 @@ class Command(BaseCommand):
check6_history.agent_id = agent.agent_id
check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check6_history.y = 0
check6_history.results = {
"retcode": 0,
"stdout": None,
"stderr": None,
"execution_time": "4.0000",
}
check6_history.save()
nla_task = AutomatedTask()
@@ -712,6 +724,12 @@ class Command(BaseCommand):
check7_history.agent_id = agent.agent_id
check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check7_history.y = 0
check7_history.results = {
"retcode": 0,
"stdout": spooler_stdout,
"stderr": None,
"execution_time": "3.1337",
}
check7_history.save()
if agent.plat == AgentPlat.WINDOWS:

View File

@@ -0,0 +1,631 @@
# Generated by Django 4.1 on 2022-08-24 07:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("agents", "0054_alter_agent_goarch"),
]
operations = [
migrations.AlterField(
model_name="agent",
name="time_zone",
field=models.CharField(
blank=True,
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
max_length=255,
null=True,
),
),
]

View File

@@ -1,6 +1,7 @@
import asyncio
import re
from collections import Counter
from contextlib import suppress
from distutils.version import LooseVersion
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
@@ -130,8 +131,8 @@ class Agent(BaseAuditModel):
# return the default timezone unless the timezone is explicity set per agent
if self.time_zone:
return self.time_zone
else:
return get_core_settings().default_time_zone
return get_core_settings().default_time_zone
@property
def is_posix(self) -> bool:
@@ -232,12 +233,12 @@ class Agent(BaseAuditModel):
alert_severity = (
check.check_result.alert_severity
if check.check_type
in [
in (
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
]
)
else check.alert_severity
)
if alert_severity == AlertSeverity.ERROR:
@@ -333,8 +334,8 @@ class Agent(BaseAuditModel):
if len(ret) == 1:
return cast(str, ret[0])
else:
return ", ".join(ret) if ret else "error getting local ips"
return ", ".join(ret) if ret else "error getting local ips"
@property
def make_model(self) -> str:
@@ -344,7 +345,7 @@ class Agent(BaseAuditModel):
except:
return "error getting make/model"
try:
with suppress(Exception):
comp_sys = self.wmi_detail["comp_sys"][0]
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
@@ -361,14 +362,10 @@ class Agent(BaseAuditModel):
model = sysfam
return f"{make} {model}"
except:
pass
try:
with suppress(Exception):
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0])
except:
pass
return "unknown make/model"
@@ -479,7 +476,7 @@ class Agent(BaseAuditModel):
models.prefetch_related_objects(
[
policy
for policy in [self.policy, site_policy, client_policy, default_policy]
for policy in (self.policy, site_policy, client_policy, default_policy)
if policy
],
"excluded_agents",
@@ -532,12 +529,17 @@ class Agent(BaseAuditModel):
wait: bool = False,
run_on_any: bool = False,
history_pk: int = 0,
run_as_user: bool = False,
) -> Any:
from scripts.models import Script
script = Script.objects.get(pk=scriptpk)
# always override if set on script model
if script.run_as_user:
run_as_user = True
parsed_args = script.parse_script_args(self, script.shell, args)
data = {
@@ -548,6 +550,7 @@ class Agent(BaseAuditModel):
"code": script.code,
"shell": script.shell,
},
"run_as_user": run_as_user,
}
if history_pk != 0:
@@ -583,7 +586,7 @@ class Agent(BaseAuditModel):
def approve_updates(self) -> None:
patch_policy = self.get_patch_policy()
severity_list = list()
severity_list = []
if patch_policy.critical == "approve":
severity_list.append("Critical")
@@ -615,17 +618,14 @@ class Agent(BaseAuditModel):
if not agent_policy:
agent_policy = WinUpdatePolicy.objects.create(agent=self)
# Get the list of policies applied to the agent and select the
# highest priority one.
policies = self.get_agent_policies()
processed_policies: List[int] = list()
for _, policy in policies.items():
if (
policy
and policy.active
and policy.pk not in processed_policies
and policy.winupdatepolicy.exists()
):
if policy and policy.active and policy.winupdatepolicy.exists():
patch_policy = policy.winupdatepolicy.first()
break
# if policy still doesn't exist return the agent patch policy
if not patch_policy:
@@ -677,7 +677,7 @@ class Agent(BaseAuditModel):
policies = self.get_agent_policies()
# loop through all policies applied to agent and return an alert_template if found
processed_policies: List[int] = list()
processed_policies: List[int] = []
for key, policy in policies.items():
# default alert_template will override a default policy with alert template applied
if (
@@ -742,10 +742,10 @@ class Agent(BaseAuditModel):
cache_key = f"agent_{self.agent_id}_checks"
elif self.policy:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_checks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_checks"
else:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_checks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_checks"
cached_checks = cache.get(cache_key)
if isinstance(cached_checks, list):
@@ -767,10 +767,10 @@ class Agent(BaseAuditModel):
cache_key = f"agent_{self.agent_id}_tasks"
elif self.policy:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_tasks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_tasks"
else:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_tasks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_tasks"
cached_tasks = cache.get(cache_key)
if isinstance(cached_tasks, list):
@@ -778,7 +778,7 @@ class Agent(BaseAuditModel):
else:
# get agent tasks based on policies
tasks = Policy.get_policy_tasks(self)
cache.set(f"site_{self.site_id}_tasks", tasks, 600)
cache.set(cache_key, tasks, 600)
return tasks
def _do_nats_debug(self, agent: "Agent", message: str) -> None:
@@ -829,9 +829,12 @@ class Agent(BaseAuditModel):
Return type: tuple(message: str, error: bool)
"""
if mode == "tacagent":
if self.is_posix:
if self.plat == AgentPlat.LINUX:
cmd = "systemctl restart tacticalagent.service"
shell = 3
elif self.plat == AgentPlat.DARWIN:
cmd = "launchctl kickstart -k system/tacticalagent"
shell = 3
else:
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
shell = 1
@@ -839,22 +842,22 @@ class Agent(BaseAuditModel):
asyncio.run(
send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
)
return ("ok", False)
return "ok", False
elif mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
if wait:
r = asyncio.run(self.nats_cmd(data, timeout=20))
if r == "ok":
return ("ok", False)
return "ok", False
else:
return (str(r), True)
return str(r), True
else:
asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
return ("ok", False)
return "ok", False
return ("invalid", True)
return "invalid", True
@staticmethod
def serialize(agent: "Agent") -> Dict[str, Any]:
@@ -864,7 +867,7 @@ class Agent(BaseAuditModel):
return AgentAuditSerializer(agent).data
def delete_superseded_updates(self) -> None:
try:
with suppress(Exception):
pks = [] # list of pks to delete
kbs = list(self.winupdates.values_list("kb", flat=True))
d = Counter(kbs)
@@ -889,8 +892,6 @@ class Agent(BaseAuditModel):
pks = list(set(pks))
self.winupdates.filter(pk__in=pks).delete()
except:
pass
def should_create_alert(
self, alert_template: "Optional[AlertTemplate]" = None
@@ -1009,16 +1010,16 @@ class AgentCustomField(models.Model):
return cast(List[str], self.multiple_value)
elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value
else:
return cast(str, self.string_value)
return cast(str, self.string_value)
def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
if self.field.type in [
if self.field.type in (
CustomFieldType.TEXT,
CustomFieldType.NUMBER,
CustomFieldType.SINGLE,
CustomFieldType.DATETIME,
]:
):
self.string_value = cast(str, value)
self.save()
elif self.field.type == CustomFieldType.MULTIPLE:

View File

@@ -122,5 +122,5 @@ class AgentHistoryPerms(permissions.BasePermission):
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_agent_history")
return _has_perm(r, "can_list_agent_history")

View File

@@ -90,26 +90,31 @@ class AgentTableSerializer(serializers.ModelSerializer):
last_seen = serializers.ReadOnlyField()
pending_actions_count = serializers.ReadOnlyField()
has_patches_pending = serializers.ReadOnlyField()
cpu_model = serializers.ReadOnlyField()
graphics = serializers.ReadOnlyField()
local_ips = serializers.ReadOnlyField()
make_model = serializers.ReadOnlyField()
physical_disks = serializers.ReadOnlyField()
def get_alert_template(self, obj):
if not obj.alert_template:
return None
else:
return {
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}
return {
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}
def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE:
return obj.last_logged_in_user
elif obj.logged_in_username != "None":
return obj.logged_in_username
else:
return "-"
return "-"
def get_italic(self, obj) -> bool:
return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE
@@ -141,16 +146,18 @@ class AgentTableSerializer(serializers.ModelSerializer):
"plat",
"goarch",
"has_patches_pending",
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
]
depth = 2
class WinAgentSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = "__all__"
class AgentHostnameSerializer(serializers.ModelSerializer):
client = serializers.ReadOnlyField(source="client.name")
site = serializers.ReadOnlyField(source="site.name")

View File

@@ -153,6 +153,7 @@ def run_script_email_results_task(
emails: list[str],
args: list[str] = [],
history_pk: int = 0,
run_as_user: bool = False,
):
agent = Agent.objects.get(pk=agentpk)
script = Script.objects.get(pk=scriptpk)
@@ -163,6 +164,7 @@ def run_script_email_results_task(
timeout=nats_timeout,
wait=True,
history_pk=history_pk,
run_as_user=run_as_user,
)
if r == "timeout":
DebugLog.error(

View File

@@ -84,7 +84,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
version="2.1.1",
)
r = agent_noarch.do_update(token="", force=True)
self.assertEqual(r, "noarch")
@@ -106,7 +106,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
version="2.1.1",
goarch=GoArch.AMD64,
)
@@ -115,7 +115,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site3,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.3.0",
version="2.1.1",
goarch=GoArch.ARM32,
)
@@ -193,7 +193,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
version="2.1.1",
goarch=GoArch.AMD64,
_quantity=6,
)
@@ -215,7 +215,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.3.0",
version="2.1.1",
goarch=GoArch.AMD64,
_quantity=7,
)

View File

@@ -50,7 +50,7 @@ class TestAgentUtils(TacticalTestCase):
self.assertIn(r"agentDL='asdasd3423'", ret)
self.assertIn(
r"meshDL='meshsite/meshagents?id=meshid&installflags=0&meshinstall=6'", ret
r"meshDL='meshsite/meshagents?id=meshid&installflags=2&meshinstall=6'", ret
)
self.assertIn(r"apiURL='api.example.com'", ret)
self.assertIn(r"agentDL='asdasd3423'", ret)

View File

@@ -403,6 +403,7 @@ class TestAgentViews(TacticalTestCase):
"cmd": "ipconfig",
"shell": "cmd",
"timeout": 30,
"run_as_user": False,
}
mock_ret.return_value = "nt authority\\system"
r = self.client.post(url, data, format="json")
@@ -417,16 +418,20 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_reboot_later(self, nats_cmd):
nats_cmd.return_value = "ok"
url = f"{base_url}/{self.agent.agent_id}/reboot/"
data = {
"datetime": "2025-08-29T18:41:02",
}
# ensure we don't allow dates in past
data = {"datetime": "2022-07-11T01:51"}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "Date cannot be set in the past")
nats_cmd.return_value = "ok"
# test with date in future
data["datetime"] = "2027-08-29T18:41"
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM")
self.assertEqual(r.data["agent"], self.agent.hostname)
nats_data = {
@@ -439,12 +444,12 @@ class TestAgentViews(TacticalTestCase):
"multiple_instances": 2,
"trigger": "runonce",
"name": r.data["task_name"],
"start_year": 2025,
"start_year": 2027,
"start_month": 8,
"start_day": 29,
"start_hour": 18,
"start_min": 41,
"expire_year": 2025,
"expire_year": 2027,
"expire_month": 8,
"expire_day": 29,
"expire_hour": 18,
@@ -534,6 +539,7 @@ class TestAgentViews(TacticalTestCase):
"output": "wait",
"args": [],
"timeout": 15,
"run_as_user": False,
}
r = self.client.post(url, data, format="json")
@@ -543,7 +549,12 @@ class TestAgentViews(TacticalTestCase):
raise AgentHistory.DoesNotExist
run_script.assert_called_with(
scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=hist.pk
scriptpk=script.pk,
args=[],
timeout=18,
wait=True,
history_pk=hist.pk,
run_as_user=False,
)
run_script.reset_mock()
@@ -555,6 +566,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 15,
"emailMode": "default",
"emails": ["admin@example.com", "bob@example.com"],
"run_as_user": False,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
@@ -564,6 +576,7 @@ class TestAgentViews(TacticalTestCase):
nats_timeout=18,
emails=[],
args=["abc", "123"],
run_as_user=False,
)
email_task.reset_mock()
@@ -577,6 +590,7 @@ class TestAgentViews(TacticalTestCase):
nats_timeout=18,
emails=["admin@example.com", "bob@example.com"],
args=["abc", "123"],
run_as_user=False,
)
# test fire and forget
@@ -585,6 +599,7 @@ class TestAgentViews(TacticalTestCase):
"output": "forget",
"args": ["hello", "world"],
"timeout": 22,
"run_as_user": True,
}
r = self.client.post(url, data, format="json")
@@ -594,7 +609,11 @@ class TestAgentViews(TacticalTestCase):
raise AgentHistory.DoesNotExist
run_script.assert_called_with(
scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=hist.pk
scriptpk=script.pk,
args=["hello", "world"],
timeout=25,
history_pk=hist.pk,
run_as_user=True,
)
run_script.reset_mock()
@@ -609,6 +628,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22,
"custom_field": custom_field.pk,
"save_all_output": True,
"run_as_user": False,
}
r = self.client.post(url, data, format="json")
@@ -623,6 +643,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
)
run_script.reset_mock()
@@ -640,6 +661,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22,
"custom_field": custom_field.pk,
"save_all_output": False,
"run_as_user": False,
}
r = self.client.post(url, data, format="json")
@@ -654,6 +676,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
)
run_script.reset_mock()
@@ -673,6 +696,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22,
"custom_field": custom_field.pk,
"save_all_output": False,
"run_as_user": False,
}
r = self.client.post(url, data, format="json")
@@ -687,6 +711,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
)
run_script.reset_mock()
@@ -703,6 +728,7 @@ class TestAgentViews(TacticalTestCase):
"output": "note",
"args": ["hello", "world"],
"timeout": 22,
"run_as_user": False,
}
r = self.client.post(url, data, format="json")
@@ -717,6 +743,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
)
run_script.reset_mock()

View File

@@ -1,6 +1,7 @@
import asyncio
import tempfile
import urllib.parse
from pathlib import Path
from django.conf import settings
from django.http import FileResponse
@@ -51,12 +52,10 @@ def generate_linux_install(
uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
mesh_dl = (
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}"
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=2&meshinstall={arch_id}"
)
sh = settings.LINUX_AGENT_SCRIPT
with open(sh, "r") as f:
text = f.read()
text = Path(settings.LINUX_AGENT_SCRIPT).read_text()
replace = {
"agentDLChange": download_url,

View File

@@ -4,6 +4,7 @@ import os
import random
import string
import time
from pathlib import Path
from django.conf import settings
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
@@ -12,6 +13,7 @@ from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from meshctrl.utils import get_login_token
from packaging import version as pyver
from rest_framework import serializers
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
@@ -31,15 +33,17 @@ from tacticalrmm.constants import (
AGENT_DEFER,
AGENT_STATUS_OFFLINE,
AGENT_STATUS_ONLINE,
AGENT_TABLE_DEFER,
AgentHistoryType,
AgentMonType,
AgentPlat,
CustomFieldModel,
DebugLogType,
EvtLogNames,
PAAction,
PAStatus,
)
from tacticalrmm.helpers import notify_error
from tacticalrmm.helpers import date_is_in_past, notify_error
from tacticalrmm.permissions import (
_has_perm_on_agent,
_has_perm_on_client,
@@ -113,7 +117,7 @@ class GetAgents(APIView):
Agent.objects.filter_by_role(request.user) # type: ignore
.filter(monitoring_type_filter)
.filter(client_site_filter)
.defer(*AGENT_DEFER)
.defer(*AGENT_TABLE_DEFER)
.select_related(
"site__server_policy",
"site__workstation_policy",
@@ -165,6 +169,25 @@ class GetAgents(APIView):
class GetUpdateDeleteAgent(APIView):
permission_classes = [IsAuthenticated, AgentPerms]
class InputSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"maintenance_mode", # TODO separate this
"policy", # TODO separate this
"block_policy_inheritance", # TODO separate this
"monitoring_type",
"description",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
"offline_time",
"overdue_time",
"check_interval",
"time_zone",
"site",
]
# get agent details
def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
@@ -174,9 +197,9 @@ class GetUpdateDeleteAgent(APIView):
def put(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True)
a_serializer.save()
s = self.InputSerializer(instance=agent, data=request.data, partial=True)
s.is_valid(raise_exception=True)
s.save()
if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get() # type: ignore
@@ -215,18 +238,25 @@ class GetUpdateDeleteAgent(APIView):
def delete(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
code = "foo"
code = "foo" # stub for windows
if agent.plat == AgentPlat.LINUX:
with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
code = f.read()
code = Path(settings.LINUX_AGENT_SCRIPT).read_text()
elif agent.plat == AgentPlat.DARWIN:
code = Path(settings.MAC_UNINSTALL).read_text()
asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
name = agent.hostname
mesh_id = agent.mesh_node_id
agent.delete()
reload_nats()
uri = get_mesh_ws_url()
asyncio.run(remove_mesh_agent(uri, mesh_id))
try:
uri = get_mesh_ws_url()
asyncio.run(remove_mesh_agent(uri, mesh_id))
except Exception as e:
DebugLog.error(
message=f"Unable to remove agent {name} from meshcentral database: {e}",
log_type=DebugLogType.AGENT_ISSUES,
)
return Response(f"{name} will now be uninstalled.")
@@ -242,7 +272,7 @@ class AgentProcesses(APIView):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout" or r == "natsdown":
if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent")
return Response(r)
@@ -253,7 +283,7 @@ class AgentProcesses(APIView):
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
)
if r == "timeout" or r == "natsdown":
if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent")
elif r != "ok":
return notify_error(r)
@@ -385,7 +415,7 @@ def get_event_log(request, agent_id, logtype, days):
},
}
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout" or r == "natsdown":
if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent")
return Response(r)
@@ -408,6 +438,7 @@ def send_raw_cmd(request, agent_id):
"command": request.data["cmd"],
"shell": shell,
},
"run_as_user": request.data["run_as_user"],
}
hist = AgentHistory.objects.create(
@@ -452,10 +483,13 @@ class Reboot(APIView):
return notify_error(f"Not currently implemented for {agent.plat}")
try:
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S")
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M")
except Exception:
return notify_error("Invalid date")
if date_is_in_past(datetime_obj=obj, agent_tz=agent.timezone):
return notify_error("Date cannot be set in the past")
task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
@@ -519,7 +553,15 @@ def install_agent(request):
codesign_token, is_valid = token_is_valid()
inno = f"tacticalagent-v{version}-{plat}-{goarch}.exe"
if request.data["installMethod"] in {"bash", "mac"} and not is_valid:
return notify_error(
"Missing code signing token, or token is no longer valid. Please read the docs for more info."
)
inno = f"tacticalagent-v{version}-{plat}-{goarch}"
if plat == AgentPlat.WINDOWS:
inno += ".exe"
download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token)
installer_user = User.objects.filter(is_installer_user=True).first()
@@ -528,6 +570,21 @@ def install_agent(request):
user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
)
install_flags = [
"-m",
"install",
"--api",
request.data["api"],
"--client-id",
client_id,
"--site-id",
site_id,
"--agent-type",
request.data["agenttype"],
"--auth",
token,
]
if request.data["installMethod"] == "exe":
from tacticalrmm.utils import generate_winagent_exe
@@ -545,14 +602,6 @@ def install_agent(request):
)
elif request.data["installMethod"] == "bash":
# TODO
# linux agents are in beta for now, only available for sponsors for testing
# remove this after it's out of beta
if not is_valid:
return notify_error(
"Missing code signing token, or token is no longer valid. Please read the docs for more info."
)
from agents.utils import generate_linux_install
@@ -566,52 +615,45 @@ def install_agent(request):
download_url=download_url,
)
elif request.data["installMethod"] == "manual":
cmd = [
inno,
"/VERYSILENT",
"/SUPPRESSMSGBOXES",
"&&",
"ping",
"127.0.0.1",
"-n",
"5",
"&&",
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
"-m",
"install",
"--api",
request.data["api"],
"--client-id",
client_id,
"--site-id",
site_id,
"--agent-type",
request.data["agenttype"],
"--auth",
token,
]
elif request.data["installMethod"] in {"manual", "mac"}:
resp = {}
if request.data["installMethod"] == "manual":
cmd = [
inno,
"/VERYSILENT",
"/SUPPRESSMSGBOXES",
"&&",
"ping",
"127.0.0.1",
"-n",
"5",
"&&",
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
] + install_flags
if int(request.data["rdp"]):
cmd.append("--rdp")
if int(request.data["ping"]):
cmd.append("--ping")
if int(request.data["power"]):
cmd.append("--power")
if int(request.data["rdp"]):
cmd.append("--rdp")
if int(request.data["ping"]):
cmd.append("--ping")
if int(request.data["power"]):
cmd.append("--power")
resp = {
"cmd": " ".join(str(i) for i in cmd),
"url": download_url,
}
resp["cmd"] = " ".join(str(i) for i in cmd)
else:
install_flags.insert(0, f"sudo ./{inno}")
cmd = install_flags.copy()
dl = f"curl -L -o {inno} '{download_url}'"
resp["cmd"] = (
dl + f" && chmod +x {inno} && " + " ".join(str(i) for i in cmd)
)
resp["url"] = download_url
return Response(resp)
elif request.data["installMethod"] == "powershell":
ps = os.path.join(settings.BASE_DIR, "core/installer.ps1")
with open(ps, "r") as f:
text = f.read()
text = Path(settings.BASE_DIR / "core" / "installer.ps1").read_text()
replace_dict = {
"innosetupchange": inno,
@@ -638,8 +680,7 @@ def install_agent(request):
except Exception as e:
DebugLog.error(message=str(e))
with open(ps1, "w") as f:
f.write(text)
Path(ps1).write_text(text)
if settings.DEBUG:
with open(ps1, "r") as f:
@@ -681,6 +722,7 @@ def run_script(request, agent_id):
script = get_object_or_404(Script, pk=request.data["script"])
output = request.data["output"]
args = request.data["args"]
run_as_user: bool = request.data["run_as_user"]
req_timeout = int(request.data["timeout"]) + 3
AuditLog.audit_script_run(
@@ -705,6 +747,7 @@ def run_script(request, agent_id):
timeout=req_timeout,
wait=True,
history_pk=history_pk,
run_as_user=run_as_user,
)
return Response(r)
@@ -718,6 +761,7 @@ def run_script(request, agent_id):
nats_timeout=req_timeout,
emails=emails,
args=args,
run_as_user=run_as_user,
)
elif output == "collector":
from core.models import CustomField
@@ -728,6 +772,7 @@ def run_script(request, agent_id):
timeout=req_timeout,
wait=True,
history_pk=history_pk,
run_as_user=run_as_user,
)
custom_field = CustomField.objects.get(pk=request.data["custom_field"])
@@ -756,13 +801,18 @@ def run_script(request, agent_id):
timeout=req_timeout,
wait=True,
history_pk=history_pk,
run_as_user=run_as_user,
)
Note.objects.create(agent=agent, user=request.user, note=r)
return Response(r)
else:
agent.run_script(
scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk
scriptpk=script.pk,
args=args,
timeout=req_timeout,
history_pk=history_pk,
run_as_user=run_as_user,
)
return Response(f"{script.name} will now be run on {agent.hostname}")
@@ -872,6 +922,8 @@ def bulk(request):
q = q.filter(plat=AgentPlat.WINDOWS)
elif request.data["osType"] == AgentPlat.LINUX:
q = q.filter(plat=AgentPlat.LINUX)
elif request.data["osType"] == AgentPlat.DARWIN:
q = q.filter(plat=AgentPlat.DARWIN)
agents: list[int] = [agent.pk for agent in q]
@@ -897,7 +949,7 @@ def bulk(request):
shell,
request.data["timeout"],
request.user.username[:50],
run_on_offline=request.data["offlineAgents"],
request.data["run_as_user"],
)
return Response(f"Command will now be run on {len(agents)} agents")
@@ -909,6 +961,7 @@ def bulk(request):
request.data["args"],
request.data["timeout"],
request.user.username[:50],
request.data["run_as_user"],
)
return Response(f"{script.name} will now be run on {len(agents)} agents")
@@ -956,10 +1009,10 @@ def agent_maintenance(request):
if count:
action = "disabled" if not request.data["action"] else "enabled"
return Response(f"Maintenance mode has been {action} on {count} agents")
else:
return Response(
f"No agents have been put in maintenance mode. You might not have permissions to the resources."
)
return Response(
f"No agents have been put in maintenance mode. You might not have permissions to the resources."
)
@api_view(["GET"])

View File

@@ -469,6 +469,7 @@ class Alert(models.Model):
wait=True,
full=True,
run_on_any=True,
run_as_user=False,
)
# command was successful
@@ -591,6 +592,7 @@ class Alert(models.Model):
wait=True,
full=True,
run_on_any=True,
run_as_user=False,
)
# command was successful
@@ -615,7 +617,7 @@ class Alert(models.Model):
if not args:
return []
temp_args = list()
temp_args = []
# pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")

View File

@@ -32,7 +32,7 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
class AlertPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET" or r.method == "PATCH":
if r.method in ("GET", "PATCH"):
if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
r.user, view.kwargs["pk"]
@@ -52,5 +52,5 @@ class AlertTemplatePerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_alerttemplates")
else:
return _has_perm(r, "can_manage_alerttemplates")
return _has_perm(r, "can_manage_alerttemplates")

View File

@@ -1424,6 +1424,7 @@ class TestAlertTasks(TacticalTestCase):
"timeout": 30,
"script_args": [],
"payload": {"code": failure_action.code, "shell": failure_action.shell},
"run_as_user": False,
}
nats_cmd.assert_called_with(data, timeout=30, wait=True)
@@ -1452,6 +1453,7 @@ class TestAlertTasks(TacticalTestCase):
"timeout": 35,
"script_args": ["nice_arg"],
"payload": {"code": resolved_action.code, "shell": resolved_action.shell},
"run_as_user": False,
}
nats_cmd.assert_called_with(data, timeout=35, wait=True)

View File

@@ -41,13 +41,13 @@ class GetAddAlerts(APIView):
elif any(
key
in [
in (
"timeFilter",
"clientFilter",
"severityFilter",
"resolvedFilter",
"snoozedFilter",
]
)
for key in request.data.keys()
):
clientFilter = Q()

View File

@@ -24,6 +24,7 @@ from core.utils import (
get_core_settings,
get_mesh_device_id,
get_mesh_ws_url,
get_meshagent_url,
)
from logs.models import DebugLog, PendingAction
from software.models import InstalledSoftware
@@ -398,26 +399,33 @@ class MeshExe(APIView):
def post(self, request):
match request.data:
case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}:
arch = MeshAgentIdent.WIN64
ident = MeshAgentIdent.WIN64
case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
arch = MeshAgentIdent.WIN32
ident = MeshAgentIdent.WIN32
case {"goarch": GoArch.AMD64, "plat": AgentPlat.DARWIN} | {
"goarch": GoArch.ARM64,
"plat": AgentPlat.DARWIN,
}:
ident = MeshAgentIdent.DARWIN_UNIVERSAL
case _:
return notify_error("Arch not specified")
return notify_error("Arch not supported")
core = get_core_settings()
try:
uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
mesh_device_id: str = asyncio.run(
get_mesh_device_id(uri, core.mesh_device_group)
)
except:
return notify_error("Unable to connect to mesh to get group id information")
if settings.DOCKER_BUILD:
dl_url = f"{settings.MESH_WS_URL.replace('ws://', 'http://')}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
else:
dl_url = (
f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
)
dl_url = get_meshagent_url(
ident=ident,
plat=request.data["plat"],
mesh_site=core.mesh_site,
mesh_device_id=mesh_device_id,
)
try:
return download_mesh_agent(dl_url)

View File

@@ -218,12 +218,12 @@ class Policy(BaseAuditModel):
def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]":
# List of all tasks to be applied
tasks = list()
tasks = []
# Get policies applied to agent and agent site and client
policies = agent.get_agent_policies()
processed_policies = list()
processed_policies = []
for _, policy in policies.items():
if policy and policy.active and policy.pk not in processed_policies:
@@ -244,10 +244,10 @@ class Policy(BaseAuditModel):
# Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first
enforced_checks = list()
policy_checks = list()
enforced_checks = []
policy_checks = []
processed_policies = list()
processed_policies = []
for _, policy in policies.items():
if policy and policy.active and policy.pk not in processed_policies:
@@ -263,24 +263,24 @@ class Policy(BaseAuditModel):
return []
# Sorted Checks already added
added_diskspace_checks: List[str] = list()
added_ping_checks: List[str] = list()
added_winsvc_checks: List[str] = list()
added_script_checks: List[int] = list()
added_eventlog_checks: List[List[str]] = list()
added_cpuload_checks: List[int] = list()
added_memory_checks: List[int] = list()
added_diskspace_checks: List[str] = []
added_ping_checks: List[str] = []
added_winsvc_checks: List[str] = []
added_script_checks: List[int] = []
added_eventlog_checks: List[List[str]] = []
added_cpuload_checks: List[int] = []
added_memory_checks: List[int] = []
# Lists all agent and policy checks that will be returned
diskspace_checks: "List[Check]" = list()
ping_checks: "List[Check]" = list()
winsvc_checks: "List[Check]" = list()
script_checks: "List[Check]" = list()
eventlog_checks: "List[Check]" = list()
cpuload_checks: "List[Check]" = list()
memory_checks: "List[Check]" = list()
diskspace_checks: "List[Check]" = []
ping_checks: "List[Check]" = []
winsvc_checks: "List[Check]" = []
script_checks: "List[Check]" = []
eventlog_checks: "List[Check]" = []
cpuload_checks: "List[Check]" = []
memory_checks: "List[Check]" = []
overridden_checks: List[int] = list()
overridden_checks: List[int] = []
# Loop over checks in with enforced policies first, then non-enforced policies
for check in enforced_checks + agent_checks + policy_checks:

View File

@@ -7,5 +7,5 @@ class AutomationPolicyPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_automation_policies")
else:
return _has_perm(r, "can_manage_automation_policies")
return _has_perm(r, "can_manage_automation_policies")

View File

@@ -7,8 +7,11 @@ from rest_framework.serializers import (
from agents.serializers import AgentHostnameSerializer
from autotasks.models import TaskResult
from checks.models import CheckResult
from clients.models import Client
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
from clients.models import Client, Site
from clients.serializers import (
ClientMinimumSerializer,
SiteMinimumSerializer,
)
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
@@ -85,11 +88,29 @@ class PolicyRelatedSerializer(ModelSerializer):
)
class PolicyOverviewSiteSerializer(ModelSerializer):
workstation_policy = PolicySerializer(read_only=True)
server_policy = PolicySerializer(read_only=True)
class Meta:
model = Site
fields = ("pk", "name", "workstation_policy", "server_policy")
class PolicyOverviewSerializer(ModelSerializer):
sites = SerializerMethodField()
workstation_policy = PolicySerializer(read_only=True)
server_policy = PolicySerializer(read_only=True)
def get_sites(self, obj):
return PolicyOverviewSiteSerializer(
obj.filtered_sites,
many=True,
).data
class Meta:
model = Client
fields = ("pk", "name", "sites", "workstation_policy", "server_policy")
depth = 2
class PolicyCheckStatusSerializer(ModelSerializer):

View File

@@ -2,8 +2,9 @@ from itertools import cycle
from unittest.mock import patch
from model_bakery import baker, seq
from django.db.models import Prefetch
from agents.models import Agent
from clients.models import Site
from core.utils import get_core_settings
from tacticalrmm.constants import AgentMonType, TaskSyncStatus
from tacticalrmm.test import TacticalTestCase
@@ -186,7 +187,17 @@ class TestPolicyViews(TacticalTestCase):
baker.make("clients.Site", client=cycle(clients), _quantity=3)
resp = self.client.get(url, format="json")
clients = Client.objects.all()
clients = Client.objects.select_related(
"workstation_policy", "server_policy"
).prefetch_related(
Prefetch(
"sites",
queryset=Site.objects.select_related(
"workstation_policy", "server_policy"
),
to_attr="filtered_sites",
)
)
serializer = PolicyOverviewSerializer(clients, many=True)
self.assertEqual(resp.status_code, 200)

View File

@@ -7,10 +7,11 @@ from rest_framework.views import APIView
from agents.models import Agent
from autotasks.models import TaskResult
from checks.models import CheckResult
from clients.models import Client
from clients.models import Client, Site
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from django.db.models import Prefetch
from .models import Policy
from .permissions import AutomationPolicyPerms
@@ -108,8 +109,18 @@ class PolicyCheck(APIView):
class OverviewPolicy(APIView):
def get(self, request):
clients = Client.objects.filter_by_role(request.user).select_related(
"workstation_policy", "server_policy"
clients = (
Client.objects.filter_by_role(request.user)
.select_related("workstation_policy", "server_policy")
.prefetch_related(
Prefetch(
"sites",
queryset=Site.objects.select_related(
"workstation_policy", "server_policy"
),
to_attr="filtered_sites",
)
)
)
return Response(PolicyOverviewSerializer(clients, many=True).data)

View File

@@ -1,6 +1,7 @@
import asyncio
import random
import string
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import pytz
@@ -262,13 +263,13 @@ class AutomatedTask(BaseAuditModel):
else True,
}
if self.task_type in [
if self.task_type in (
TaskType.RUN_ONCE,
TaskType.DAILY,
TaskType.WEEKLY,
TaskType.MONTHLY,
TaskType.MONTHLY_DOW,
]:
):
# set runonce task in future if creating and run_asap_after_missed is set
if (
not editing
@@ -432,10 +433,8 @@ class AutomatedTask(BaseAuditModel):
if r != "ok" and "The system cannot find the file specified" not in r:
task_result.sync_status = TaskSyncStatus.PENDING_DELETION
try:
with suppress(DatabaseError):
task_result.save(update_fields=["sync_status"])
except DatabaseError:
pass
DebugLog.warning(
agent=agent,

View File

@@ -100,13 +100,13 @@ class TaskSerializer(serializers.ModelSerializer):
# run_time_date required
if (
data["task_type"]
in [
in (
TaskType.RUN_ONCE,
TaskType.DAILY,
TaskType.WEEKLY,
TaskType.MONTHLY,
TaskType.MONTHLY_DOW,
]
)
and not data["run_time_date"]
):
raise serializers.ValidationError(
@@ -188,13 +188,12 @@ class TaskSerializer(serializers.ModelSerializer):
if not alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.task_always_email,
"always_text": alert_template.task_always_text,
"always_alert": alert_template.task_always_alert,
}
return {
"name": alert_template.name,
"always_email": alert_template.task_always_email,
"always_text": alert_template.task_always_text,
"always_alert": alert_template.task_always_alert,
}
class Meta:
model = AutomatedTask
@@ -241,6 +240,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
),
"shell": script.shell,
"timeout": action["timeout"],
"run_as_user": script.run_as_user,
}
)
if actions_to_remove:

View File

@@ -1,6 +1,7 @@
import asyncio
import datetime as dt
import random
from contextlib import suppress
from time import sleep
from typing import Optional, Union
@@ -16,60 +17,64 @@ from tacticalrmm.constants import DebugLogType
@app.task
def create_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
try:
with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk)
if agent_id:
task.create_task_on_agent(Agent.objects.get(agent_id=agent_id))
else:
task.create_task_on_agent()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok"
@app.task
def modify_win_task(pk: int, agent_id: Optional[str] = None) -> str:
try:
with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk)
if agent_id:
task.modify_task_on_agent(Agent.objects.get(agent_id=agent_id))
else:
task.modify_task_on_agent()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok"
@app.task
def delete_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
try:
with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk)
if agent_id:
task.delete_task_on_agent(Agent.objects.get(agent_id=agent_id))
else:
task.delete_task_on_agent()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok"
@app.task
def run_win_task(pk: int, agent_id: Optional[str] = None) -> str:
try:
with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk)
if agent_id:
task.run_win_task(Agent.objects.get(agent_id=agent_id))
else:
task.run_win_task()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok"

View File

@@ -149,8 +149,8 @@ class Check(BaseAuditModel):
def __str__(self):
if self.agent:
return f"{self.agent.hostname} - {self.readable_desc}"
else:
return f"{self.policy.name} - {self.readable_desc}"
return f"{self.policy.name} - {self.readable_desc}"
def save(self, *args, **kwargs):
@@ -198,10 +198,7 @@ class Check(BaseAuditModel):
return f"{display}: Drive {self.disk} - {text}"
elif self.check_type == CheckType.PING:
return f"{display}: {self.name}"
elif (
self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY
):
elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
@@ -215,8 +212,8 @@ class Check(BaseAuditModel):
return f"{display}: {self.name}"
elif self.check_type == CheckType.SCRIPT:
return f"{display}: {self.script.name}"
else:
return "n/a"
return "n/a"
@staticmethod
def non_editable_fields() -> list[str]:
@@ -335,12 +332,12 @@ class CheckResult(models.Model):
def save(self, *args, **kwargs):
# if check is a policy check clear cache on everything
if not self.alert_severity and self.assigned_check.check_type in [
if not self.alert_severity and self.assigned_check.check_type in (
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
]:
):
self.alert_severity = AlertSeverity.WARNING
super(CheckResult, self).save(

View File

@@ -5,7 +5,7 @@ from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ChecksPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET" or r.method == "PATCH":
if r.method in ("GET", "PATCH"):
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]

View File

@@ -43,13 +43,13 @@ class CheckSerializer(serializers.ModelSerializer):
if not alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.check_always_email,
"always_text": alert_template.check_always_text,
"always_alert": alert_template.check_always_alert,
}
return {
"name": alert_template.name,
"always_email": alert_template.check_always_email,
"always_text": alert_template.check_always_text,
"always_alert": alert_template.check_always_alert,
}
class Meta:
model = Check

View File

@@ -170,5 +170,5 @@ def run_checks(request, agent_id):
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")
return notify_error("Unable to contact the agent")

View File

@@ -70,11 +70,11 @@ class Client(BaseAuditModel):
sites = self.sites.all()
if old_client.workstation_policy != self.workstation_policy:
for site in sites:
cache.delete_many_pattern(f"site_workstation_{site.pk}_*")
cache.delete_many_pattern(f"site_workstation_*{site.pk}_*")
if old_client.server_policy != self.server_policy:
for site in sites:
cache.delete_many_pattern(f"site_server_{site.pk}_*")
cache.delete_many_pattern(f"site_server_*{site.pk}_*")
class Meta:
ordering = ("name",)
@@ -145,10 +145,10 @@ class Site(BaseAuditModel):
cache_agents_alert_template.delay()
if old_site.workstation_policy != self.workstation_policy:
cache.delete_many_pattern(f"site_workstation_{self.pk}_*")
cache.delete_many_pattern(f"site_workstation_*{self.pk}_*")
if old_site.server_policy != self.server_policy:
cache.delete_many_pattern(f"site_server_{self.pk}_*")
cache.delete_many_pattern(f"site_server_*{self.pk}_*")
class Meta:
ordering = ("name",)
@@ -229,16 +229,16 @@ class ClientCustomField(models.Model):
return self.multiple_value
elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value
else:
return self.string_value
return self.string_value
def save_to_field(self, value):
if self.field.type in [
if self.field.type in (
CustomFieldType.TEXT,
CustomFieldType.NUMBER,
CustomFieldType.SINGLE,
CustomFieldType.DATETIME,
]:
):
self.string_value = value
self.save()
elif self.field.type == CustomFieldType.MULTIPLE:
@@ -280,16 +280,16 @@ class SiteCustomField(models.Model):
return self.multiple_value
elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value
else:
return self.string_value
return self.string_value
def save_to_field(self, value):
if self.field.type in [
if self.field.type in (
CustomFieldType.TEXT,
CustomFieldType.NUMBER,
CustomFieldType.SINGLE,
CustomFieldType.DATETIME,
]:
):
self.string_value = value
self.save()
elif self.field.type == CustomFieldType.MULTIPLE:

View File

@@ -12,7 +12,7 @@ class ClientsPerms(permissions.BasePermission):
)
else:
return _has_perm(r, "can_list_clients")
elif r.method == "PUT" or r.method == "DELETE":
elif r.method in ("PUT", "DELETE"):
return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
r.user, view.kwargs["pk"]
)
@@ -29,7 +29,7 @@ class SitesPerms(permissions.BasePermission):
)
else:
return _has_perm(r, "can_list_sites")
elif r.method == "PUT" or r.method == "DELETE":
elif r.method in ("PUT", "DELETE"):
return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
r.user, view.kwargs["pk"]
)
@@ -41,5 +41,5 @@ class DeploymentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_deployments")
else:
return _has_perm(r, "can_manage_deployments")
return _has_perm(r, "can_manage_deployments")

View File

@@ -1,6 +1,7 @@
import datetime as dt
import re
import uuid
from contextlib import suppress
from django.db.models import Count, Exists, OuterRef, Prefetch, prefetch_related_objects
from django.shortcuts import get_object_or_404
@@ -338,10 +339,8 @@ class AgentDeployment(APIView):
if not _has_perm_on_site(request.user, d.site.pk):
raise PermissionDenied()
try:
with suppress(Exception):
d.auth_token.delete()
except:
pass
d.delete()
return Response("The deployment was deleted")

View File

@@ -67,8 +67,11 @@ RemoveOldAgent() {
InstallMesh() {
if [ -f /etc/os-release ]; then
distroID=$(. /etc/os-release; echo $ID)
distroIDLIKE=$(. /etc/os-release; echo $ID_LIKE)
if [[ " ${deb[*]} " =~ " ${distroID} " ]]; then
set_locale_deb
elif [[ " ${deb[*]} " =~ " ${distroIDLIKE} " ]]; then
set_locale_deb
elif [[ " ${rhe[*]} " =~ " ${distroID} " ]]; then
set_locale_rhel
else
@@ -78,21 +81,21 @@ InstallMesh() {
meshTmpDir=$(mktemp -d -t "mesh-XXXXXXXXX")
if [ $? -ne 0 ]; then
meshTmpDir='meshtemp'
meshTmpDir='/root/meshtemp'
mkdir -p ${meshTmpDir}
fi
meshTmpBin="${meshTmpDir}/meshagent"
wget --no-check-certificate -q -O ${meshTmpBin} ${meshDL}
chmod +x ${meshTmpBin}
mkdir -p ${meshDir}
env LC_ALL=en_US.UTF-8 LANGUAGE=en_US ${meshTmpBin} -install --installPath=${meshDir}
env LC_ALL=en_US.UTF-8 LANGUAGE=en_US XAUTHORITY=foo DISPLAY=bar ${meshTmpBin} -install --installPath=${meshDir}
sleep 1
rm -rf ${meshTmpDir}
}
RemoveMesh() {
if [ -f "${meshSystemBin}" ]; then
${meshSystemBin} -uninstall
env XAUTHORITY=foo DISPLAY=bar ${meshSystemBin} -uninstall
sleep 1
fi
@@ -119,6 +122,10 @@ RemoveOldAgent
echo "Downloading tactical agent..."
wget -q -O ${agentBin} "${agentDL}"
if [ $? -ne 0 ]; then
echo "ERROR: Unable to download tactical agent"
exit 1
fi
chmod +x ${agentBin}
MESH_NODE_ID=""
@@ -133,7 +140,7 @@ else
InstallMesh
sleep 2
echo "Getting mesh node id..."
MESH_NODE_ID=$(${agentBin} -m nixmeshnodeid)
MESH_NODE_ID=$(env XAUTHORITY=foo DISPLAY=bar ${agentBin} -m nixmeshnodeid)
fi
if [ ! -d "${agentBinPath}" ]; then
@@ -178,4 +185,4 @@ EOF
echo "${tacticalsvc}" | tee ${agentSysD} > /dev/null
systemctl daemon-reload
systemctl enable --now ${agentSvcName}
systemctl enable --now ${agentSvcName}

View File

@@ -1,4 +1,5 @@
import asyncio
from contextlib import suppress
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
@@ -24,10 +25,8 @@ class DashInfo(AsyncJsonWebsocketConsumer):
async def disconnect(self, close_code):
try:
with suppress(Exception):
self.dash_info.cancel()
except:
pass
self.connected = False
await self.close()

View File

@@ -39,9 +39,8 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
$DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled
if ($DefenderStatus -match "True") {
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*'
Add-MpPreference -ExclusionPath 'C:\ProgramData\TacticalRMM\*'
}
}
Catch {

View File

@@ -0,0 +1,8 @@
#!/bin/bash
/usr/local/mesh_services/meshagent/meshagent -fulluninstall
launchctl bootout system /Library/LaunchDaemons/tacticalagent.plist
rm -rf /usr/local/mesh_services
rm -f /etc/tacticalagent
rm -rf /opt/tacticalagent
rm -f /Library/LaunchDaemons/tacticalagent.plist

View File

@@ -0,0 +1,60 @@
import configparser
import os
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Generate conf for uwsgi"
def handle(self, *args, **kwargs):
self.stdout.write("Creating uwsgi conf...")
config = configparser.ConfigParser()
if getattr(settings, "DOCKER_BUILD", False):
home = str(Path(os.getenv("VIRTUAL_ENV"))) # type: ignore
socket = "0.0.0.0:8080"
else:
home = str(settings.BASE_DIR.parents[0] / "env")
socket = str(settings.BASE_DIR / "tacticalrmm.sock")
config["uwsgi"] = {
"chdir": str(settings.BASE_DIR),
"module": "tacticalrmm.wsgi",
"home": home,
"master": str(getattr(settings, "UWSGI_MASTER", True)).lower(),
"enable-threads": str(
getattr(settings, "UWSGI_ENABLE_THREADS", True)
).lower(),
"socket": socket,
"harakiri": str(getattr(settings, "UWSGI_HARAKIRI", 300)),
"chmod-socket": str(getattr(settings, "UWSGI_CHMOD_SOCKET", 660)),
"buffer-size": str(getattr(settings, "UWSGI_BUFFER_SIZE", 65535)),
"vacuum": str(getattr(settings, "UWSGI_VACUUM", True)).lower(),
"die-on-term": str(getattr(settings, "UWSGI_DIE_ON_TERM", True)).lower(),
"max-requests": str(getattr(settings, "UWSGI_MAX_REQUESTS", 500)),
"disable-logging": str(
getattr(settings, "UWSGI_DISABLE_LOGGING", True)
).lower(),
"cheaper-algo": "busyness",
"cheaper": str(getattr(settings, "UWSGI_CHEAPER", 4)),
"cheaper-initial": str(getattr(settings, "UWSGI_CHEAPER_INITIAL", 4)),
"workers": str(getattr(settings, "UWSGI_MAX_WORKERS", 40)),
"cheaper-step": str(getattr(settings, "UWSGI_CHEAPER_STEP", 2)),
"cheaper-overload": str(getattr(settings, "UWSGI_CHEAPER_OVERLOAD", 3)),
"cheaper-busyness-min": str(getattr(settings, "UWSGI_BUSYNESS_MIN", 5)),
"cheaper-busyness-max": str(getattr(settings, "UWSGI_BUSYNESS_MAX", 10)),
}
if getattr(settings, "UWSGI_DEBUG", False):
config["uwsgi"]["stats"] = "/tmp/stats.socket"
config["uwsgi"]["cheaper-busyness-verbose"] = str(True).lower()
with open(settings.BASE_DIR / "app.ini", "w") as fp:
config.write(fp)
self.stdout.write("Created uwsgi conf")

View File

@@ -1,3 +1,5 @@
from contextlib import suppress
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
@@ -8,9 +10,7 @@ class Command(BaseCommand):
help = "Populates the global site settings on first install"
def handle(self, *args, **kwargs):
try:
# can only be 1 instance of this. Prevents error when rebuilding docker container
with suppress(ValidationError):
CoreSettings().save()
self.stdout.write("Core db populated")
except ValidationError:
# can only be 1 instance of this. Prevents error when rebuilding docker container
pass

View File

@@ -7,6 +7,6 @@ class Command(BaseCommand):
help = "Collection of tasks to run after updating the rmm, before migrations"
def handle(self, *args, **kwargs):
self.stdout.write(self.style.WARNING("Clearning the cache"))
self.stdout.write(self.style.WARNING("Cleaning the cache"))
clear_entire_cache()
self.stdout.write(self.style.SUCCESS("Cache was cleared!"))

View File

@@ -0,0 +1,630 @@
# Generated by Django 4.1 on 2022-08-24 07:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0034_alter_customfield_name"),
]
operations = [
migrations.AlterField(
model_name="coresettings",
name="default_time_zone",
field=models.CharField(
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
default="America/Los_Angeles",
max_length=255,
),
),
]

View File

@@ -1,4 +1,5 @@
import smtplib
from contextlib import suppress
from email.message import EmailMessage
from typing import TYPE_CHECKING, List, Optional, cast
@@ -108,12 +109,10 @@ class CoreSettings(BaseAuditModel):
# for install script
if not self.pk:
try:
with suppress(Exception):
self.mesh_site = settings.MESH_SITE
self.mesh_username = settings.MESH_USERNAME.lower()
self.mesh_token = settings.MESH_TOKEN_KEY
except:
pass
old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
@@ -182,10 +181,10 @@ class CoreSettings(BaseAuditModel):
test: bool = False,
) -> tuple[str, bool]:
if test and not self.email_is_configured:
return ("There needs to be at least one email recipient configured", False)
return "There needs to be at least one email recipient configured", False
# return since email must be configured to continue
elif not self.email_is_configured:
return ("SMTP messaging not configured.", False)
return "SMTP messaging not configured.", False
# override email from if alert_template is passed and is set
if alert_template and alert_template.email_from:
@@ -199,7 +198,7 @@ class CoreSettings(BaseAuditModel):
elif self.email_alert_recipients:
email_recipients = ", ".join(cast(List[str], self.email_alert_recipients))
else:
return ("There needs to be at least one email recipient configured", False)
return "There needs to be at least one email recipient configured", False
try:
msg = EmailMessage()
@@ -226,12 +225,12 @@ class CoreSettings(BaseAuditModel):
except Exception as e:
DebugLog.error(message=f"Sending email failed with error: {e}")
if test:
return (str(e), False)
return str(e), False
if test:
return ("Email test ok!", True)
return "Email test ok!", True
return ("ok", True)
return "ok", True
def send_sms(
self,
@@ -240,7 +239,7 @@ class CoreSettings(BaseAuditModel):
test: bool = False,
) -> tuple[str, bool]:
if not self.sms_is_configured:
return ("Sms alerting is not setup correctly.", False)
return "Sms alerting is not setup correctly.", False
# override email recipients if alert_template is passed and is set
if alert_template and alert_template.text_recipients:
@@ -248,7 +247,7 @@ class CoreSettings(BaseAuditModel):
elif self.sms_alert_recipients:
text_recipients = cast(List[str], self.sms_alert_recipients)
else:
return ("No sms recipients found", False)
return "No sms recipients found", False
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
for num in text_recipients:
@@ -257,12 +256,12 @@ class CoreSettings(BaseAuditModel):
except TwilioRestException as e:
DebugLog.error(message=f"SMS failed to send: {e}")
if test:
return (str(e), False)
return str(e), False
if test:
return ("SMS Test sent successfully!", True)
return "SMS Test sent successfully!", True
return ("ok", True)
return "ok", True
@staticmethod
def serialize(core):
@@ -315,8 +314,8 @@ class CustomField(BaseAuditModel):
return self.default_values_multiple
elif self.type == CustomFieldType.CHECKBOX:
return self.default_value_bool
else:
return self.default_value_string
return self.default_value_string
def get_or_create_field_value(self, instance):
from agents.models import Agent, AgentCustomField
@@ -365,6 +364,23 @@ class CodeSignToken(models.Model):
return r.status_code == 200
@property
def is_expired(self) -> bool:
if not self.token:
return False
try:
r = requests.post(
settings.CHECK_TOKEN_URL,
json={"token": self.token, "api": settings.ALLOWED_HOSTS[0]},
headers={"Content-type": "application/json"},
timeout=15,
)
except:
return False
return r.status_code == 401
def __str__(self):
return "Code signing token"

View File

@@ -7,8 +7,8 @@ class CoreSettingsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_view_core_settings")
else:
return _has_perm(r, "can_edit_core_settings")
return _has_perm(r, "can_edit_core_settings")
class URLActionPerms(permissions.BasePermission):
@@ -30,5 +30,5 @@ class CustomFieldPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_view_customfields")
else:
return _has_perm(r, "can_manage_customfields")
return _has_perm(r, "can_manage_customfields")

View File

@@ -174,7 +174,7 @@ def _get_failing_data(agents: "QuerySet[Any]") -> Dict[str, bool]:
and task.task_result.status == TaskStatus.FAILING
and task.alert_severity == AlertSeverity.WARNING
):
data["warning"]
data["warning"] = True
return data

View File

@@ -5,13 +5,20 @@ from channels.db import database_sync_to_async
from channels.testing import WebsocketCommunicator
from django.conf import settings
from django.core.management import call_command
from django.test import override_settings
from model_bakery import baker
from rest_framework.authtoken.models import Token
from agents.models import Agent
from core.utils import get_core_settings
from core.utils import get_core_settings, get_meshagent_url
from logs.models import PendingAction
from tacticalrmm.constants import CONFIG_MGMT_CMDS, CustomFieldModel, PAAction, PAStatus
from tacticalrmm.constants import (
CONFIG_MGMT_CMDS,
CustomFieldModel,
MeshAgentIdent,
PAAction,
PAStatus,
)
from tacticalrmm.test import TacticalTestCase
from .consumers import DashInfo
@@ -444,3 +451,58 @@ class TestCorePermissions(TacticalTestCase):
def setUp(self):
self.setup_client()
self.setup_coresettings()
class TestCoreUtils(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
def test_get_meshagent_url_standard(self):
r = get_meshagent_url(
ident=MeshAgentIdent.DARWIN_UNIVERSAL,
plat="darwin",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"https://mesh.example.com/meshagents?id=abc123&installflags=2&meshinstall=10005",
)
r = get_meshagent_url(
ident=MeshAgentIdent.WIN64,
plat="windows",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"https://mesh.example.com/meshagents?id=4&meshid=abc123&installflags=0",
)
@override_settings(DOCKER_BUILD=True)
@override_settings(MESH_WS_URL="ws://tactical-meshcentral:4443")
def test_get_meshagent_url_docker(self):
r = get_meshagent_url(
ident=MeshAgentIdent.DARWIN_UNIVERSAL,
plat="darwin",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"http://tactical-meshcentral:4443/meshagents?id=abc123&installflags=2&meshinstall=10005",
)
r = get_meshagent_url(
ident=MeshAgentIdent.WIN64,
plat="windows",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"http://tactical-meshcentral:4443/meshagents?id=4&meshid=abc123&installflags=0",
)

View File

@@ -1,6 +1,7 @@
import json
import subprocess
import tempfile
import urllib.parse
from base64 import b64encode
from typing import TYPE_CHECKING, Optional, cast
@@ -11,7 +12,12 @@ from django.core.cache import cache
from django.http import FileResponse
from meshctrl.utils import get_auth_token
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, ROLE_CACHE_PREFIX
from tacticalrmm.constants import (
CORESETTINGS_CACHE_KEY,
ROLE_CACHE_PREFIX,
AgentPlat,
MeshAgentIdent,
)
if TYPE_CHECKING:
from core.models import CodeSignToken, CoreSettings
@@ -47,6 +53,16 @@ def token_is_valid() -> tuple[str, bool]:
return "", False
def token_is_expired() -> bool:
from core.models import CodeSignToken
t: "CodeSignToken" = CodeSignToken.objects.first()
if not t or not t.token:
return False
return t.is_expired
def get_core_settings() -> "CoreSettings":
from core.models import CORESETTINGS_CACHE_KEY, CoreSettings
@@ -142,3 +158,28 @@ def sysd_svc_is_running(svc: str) -> bool:
cmd = ["systemctl", "is-active", "--quiet", svc]
r = subprocess.run(cmd, capture_output=True)
return not r.returncode
def get_meshagent_url(
*, ident: "MeshAgentIdent", plat: str, mesh_site: str, mesh_device_id: str
) -> str:
if settings.DOCKER_BUILD:
base = settings.MESH_WS_URL.replace("ws://", "http://")
else:
base = mesh_site
if plat == AgentPlat.WINDOWS:
params = {
"id": ident,
"meshid": mesh_device_id,
"installflags": 0,
}
else:
params = {
"id": mesh_device_id,
"installflags": 2,
"meshinstall": ident,
}
return base + "/meshagents?" + urllib.parse.urlencode(params)

View File

@@ -1,4 +1,5 @@
import re
from pathlib import Path
import psutil
import pytz
@@ -6,8 +7,8 @@ from cryptography import x509
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone as djangotime
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
@@ -73,6 +74,7 @@ def clear_cache(request):
@api_view()
def dashboard_info(request):
from core.utils import token_is_expired
from tacticalrmm.utils import get_latest_trmm_ver
return Response(
@@ -93,6 +95,7 @@ def dashboard_info(request):
"hosted": getattr(settings, "HOSTED", False),
"date_format": request.user.date_format,
"default_date_format": get_core_settings().date_format,
"token_is_expired": token_is_expired(),
}
)
@@ -175,8 +178,8 @@ class GetAddCustomFields(APIView):
if "model" in request.data.keys():
fields = CustomField.objects.filter(model=request.data["model"])
return Response(CustomFieldSerializer(fields, many=True).data)
else:
return notify_error("The request was invalid")
return notify_error("The request was invalid")
def post(self, request):
serializer = CustomFieldSerializer(data=request.data, partial=True)
@@ -231,7 +234,7 @@ class CodeSign(APIView):
except Exception as e:
return notify_error(str(e))
if r.status_code == 400 or r.status_code == 401:
if r.status_code in (400, 401):
return notify_error(r.json()["ret"])
elif r.status_code == 200:
t = CodeSignToken.objects.first()
@@ -414,8 +417,7 @@ def status(request):
mem_usage: int = round(psutil.virtual_memory().percent)
cert_file, _ = get_certs()
with open(cert_file, "rb") as f:
cert_bytes = f.read()
cert_bytes = Path(cert_file).read_bytes()
cert = x509.load_pem_x509_certificate(cert_bytes)
expires = pytz.utc.localize(cert.not_valid_after)
@@ -424,6 +426,7 @@ def status(request):
ret = {
"version": settings.TRMM_VERSION,
"latest_agent_version": settings.LATEST_AGENT_VER,
"agent_count": Agent.objects.count(),
"client_count": Client.objects.count(),
"site_count": Site.objects.count(),

View File

@@ -282,7 +282,7 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [DebugLogLevel.INFO]:
if get_debug_level() == DebugLogLevel.INFO:
cls.objects.create(
log_level=DebugLogLevel.INFO,
agent=agent,
@@ -297,7 +297,7 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [DebugLogLevel.INFO, DebugLogLevel.WARN]:
if get_debug_level() in (DebugLogLevel.INFO, DebugLogLevel.WARN):
cls.objects.create(
log_level=DebugLogLevel.INFO,
agent=agent,
@@ -312,11 +312,11 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [
if get_debug_level() in (
DebugLogLevel.INFO,
DebugLogLevel.WARN,
DebugLogLevel.ERROR,
]:
):
cls.objects.create(
log_level=DebugLogLevel.ERROR,
agent=agent,
@@ -331,12 +331,12 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None:
if get_debug_level() in [
if get_debug_level() in (
DebugLogLevel.INFO,
DebugLogLevel.WARN,
DebugLogLevel.ERROR,
DebugLogLevel.CRITICAL,
]:
):
cls.objects.create(
log_level=DebugLogLevel.CRITICAL,
agent=agent,
@@ -376,8 +376,8 @@ class PendingAction(models.Model):
return "Next update cycle"
elif self.action_type == PAAction.CHOCO_INSTALL:
return "ASAP"
else:
return "On next checkin"
return "On next checkin"
@property
def description(self) -> Optional[str]:
@@ -390,15 +390,15 @@ class PendingAction(models.Model):
elif self.action_type == PAAction.CHOCO_INSTALL:
return f"{self.details['name']} software install"
elif self.action_type in [
elif self.action_type in (
PAAction.RUN_CMD,
PAAction.RUN_SCRIPT,
PAAction.RUN_PATCH_SCAN,
PAAction.RUN_PATCH_INSTALL,
]:
):
return f"{self.action_type}"
else:
return None
return None
class BaseAuditModel(models.Model):

View File

@@ -16,8 +16,8 @@ class AuditLogSerializer(serializers.ModelSerializer):
return SiteMinimumSerializer(
Agent.objects.get(agent_id=obj.agent_id).site
).data
else:
return None
return None
class Meta:
model = AuditLog

View File

@@ -1,11 +1,10 @@
import datetime as dt
import pytz
from django.db.models.signals import post_init
from django.dispatch import receiver
from django.utils import timezone as djangotime
from tacticalrmm.constants import PAAction, PAStatus
from tacticalrmm.helpers import date_is_in_past
from .models import PendingAction
@@ -22,14 +21,8 @@ def handle_status(sender, instance: PendingAction, **kwargs):
reboot_time = dt.datetime.strptime(
instance.details["time"], "%Y-%m-%d %H:%M:%S"
)
# need to convert agent tz to UTC in order to compare
agent_tz = pytz.timezone(instance.agent.timezone)
localized = agent_tz.localize(reboot_time)
now = djangotime.now()
reboot_time_utc = localized.astimezone(pytz.utc)
if now > reboot_time_utc:
if date_is_in_past(
datetime_obj=reboot_time, agent_tz=instance.agent.timezone
):
instance.status = PAStatus.COMPLETED
instance.save(update_fields=["status"])

View File

@@ -5,4 +5,5 @@ pytest
pytest-django
pytest-xdist
pytest-cov
codecov
codecov
refurb

View File

@@ -1,39 +1,39 @@
asgiref==3.5.2
celery==5.2.7
certifi==2022.6.15
certifi==2022.9.24
cffi==1.15.1
channels==3.0.5
channels_redis==3.4.0
channels==4.0.0
channels_redis==4.0.0
chardet==4.0.0
cryptography==37.0.4
daphne==3.0.2
Django==4.0.6
cryptography==38.0.1
daphne==4.0.0
Django==4.1.2
django-cors-headers==3.13.0
django-ipware==4.0.2
django-rest-knox==4.2.0
djangorestframework==3.13.1
djangorestframework==3.14.0
drf-spectacular==0.24.2
future==0.18.2
msgpack==1.0.4
nats-py==2.1.3
psutil==5.9.1
psycopg2-binary==2.9.3
nats-py==2.2.0
psutil==5.9.3
psycopg2-binary==2.9.4
pycparser==2.21
pycryptodome==3.15.0
pyotp==2.6.0
pyotp==2.7.0
pyparsing==3.0.9
pytz==2022.1
pytz==2022.5
qrcode==7.3.1
redis==4.3.4
hiredis==2.0.0
requests==2.28.1
six==1.16.0
sqlparse==0.4.2
twilio==7.10.0
urllib3==1.26.9
sqlparse==0.4.3
twilio==7.14.2
urllib3==1.26.12
uWSGI==2.0.20
validators==0.20.0
vine==5.0.0
websockets==10.3
zipp==3.8.0
drf_spectacular==0.22.1
zipp==3.9.0
meshctrl==0.1.15

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-30 21:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0017_auto_20220311_0100'),
]
operations = [
migrations.AddField(
model_name='script',
name='run_as_user',
field=models.BooleanField(default=False),
),
]

View File

@@ -40,13 +40,14 @@ class Script(BaseAuditModel):
supported_platforms = ArrayField(
models.CharField(max_length=20), null=True, blank=True, default=list
)
run_as_user = models.BooleanField(default=False)
def __str__(self):
return self.name
@property
def code_no_snippets(self):
return self.script_body if self.script_body else ""
return self.script_body or ""
@property
def code(self):
@@ -65,11 +66,12 @@ class Script(BaseAuditModel):
else:
value = ""
replaced_code = re.sub(snippet.group(), value, replaced_code)
replaced_code = re.sub(
snippet.group(), value.replace("\\", "\\\\"), replaced_code
)
return replaced_code
else:
return code
return code
def hash_script_body(self):
from django.conf import settings
@@ -111,14 +113,14 @@ class Script(BaseAuditModel):
else 90
)
args = script["args"] if "args" in script.keys() else list()
args = script["args"] if "args" in script.keys() else []
syntax = script["syntax"] if "syntax" in script.keys() else ""
supported_platforms = (
script["supported_platforms"]
if "supported_platforms" in script.keys()
else list()
else []
)
# if community script exists update it
@@ -186,12 +188,12 @@ class Script(BaseAuditModel):
return ScriptSerializer(script).data
@classmethod
def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list:
def parse_script_args(cls, agent, shell: str, args: List[str] = []) -> list:
if not args:
return []
temp_args = list()
temp_args = []
# pattern to match for injection
pattern = re.compile(".*\\{\\{(.*)\\}\\}.*")

View File

@@ -7,5 +7,5 @@ class ScriptsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_scripts")
else:
return _has_perm(r, "can_manage_scripts")
return _has_perm(r, "can_manage_scripts")

View File

@@ -20,6 +20,7 @@ class ScriptTableSerializer(ModelSerializer):
"filename",
"hidden",
"supported_platforms",
"run_as_user",
]
@@ -43,16 +44,17 @@ class ScriptSerializer(ModelSerializer):
"filename",
"hidden",
"supported_platforms",
"run_as_user",
]
class ScriptCheckSerializer(ModelSerializer):
code = ReadOnlyField()
script_hash = ReadOnlyField
script_hash = ReadOnlyField()
class Meta:
model = Script
fields = ["code", "shell", "script_hash"]
fields = ["code", "shell", "run_as_user", "script_hash"]
class ScriptSnippetSerializer(ModelSerializer):

View File

@@ -9,7 +9,12 @@ from tacticalrmm.constants import AgentHistoryType
@app.task
def handle_bulk_command_task(
agentpks, cmd, shell, timeout, username, run_on_offline=False
agentpks: list[int],
cmd: str,
shell: str,
timeout,
username,
run_as_user: bool = False,
) -> None:
nats_data = {
"func": "rawcmd",
@@ -18,7 +23,9 @@ def handle_bulk_command_task(
"command": cmd,
"shell": shell,
},
"run_as_user": run_as_user,
}
agent: "Agent"
for agent in Agent.objects.filter(pk__in=agentpks):
hist = AgentHistory.objects.create(
agent=agent,
@@ -33,9 +40,15 @@ def handle_bulk_command_task(
@app.task
def handle_bulk_script_task(
scriptpk: int, agentpks: List[int], args: List[str], timeout: int, username: str
scriptpk: int,
agentpks: List[int],
args: List[str],
timeout: int,
username: str,
run_as_user: bool = False,
) -> None:
script = Script.objects.get(pk=scriptpk)
agent: "Agent"
for agent in Agent.objects.filter(pk__in=agentpks):
hist = AgentHistory.objects.create(
agent=agent,
@@ -44,5 +57,9 @@ def handle_bulk_script_task(
username=username,
)
agent.run_script(
scriptpk=script.pk, args=args, timeout=timeout, history_pk=hist.pk
scriptpk=script.pk,
args=args,
timeout=timeout,
history_pk=hist.pk,
run_as_user=run_as_user,
)

View File

@@ -145,6 +145,7 @@ class TestScriptViews(TacticalTestCase):
"timeout": 90,
"args": [],
"shell": ScriptShell.POWERSHELL,
"run_as_user": False,
}
resp = self.client.post(url, data, format="json")

View File

@@ -17,6 +17,7 @@ from .serializers import (
ScriptSnippetSerializer,
ScriptTableSerializer,
)
from core.utils import clear_entire_cache
class GetAddScripts(APIView):
@@ -58,7 +59,7 @@ class GetUpdateDeleteScript(APIView):
return Response(ScriptSerializer(script).data)
def put(self, request, pk):
script = get_object_or_404(Script, pk=pk)
script = get_object_or_404(Script.objects.prefetch_related("script"), pk=pk)
data = request.data
@@ -76,7 +77,12 @@ class GetUpdateDeleteScript(APIView):
serializer.is_valid(raise_exception=True)
obj = serializer.save()
# obj.hash_script_body()
# TODO rename the related field from 'script' to 'scriptchecks' so it's not so confusing
if script.script.exists():
for script_check in script.script.all():
if script_check.policy:
clear_entire_cache()
break
return Response(f"{obj.name} was edited!")
@@ -154,6 +160,7 @@ class TestScript(APIView):
"code": Script.replace_with_snippets(request.data["code"]),
"shell": request.data["shell"],
},
"run_as_user": request.data["run_as_user"],
}
r = asyncio.run(

View File

@@ -9,5 +9,5 @@ class WinSvcsPerms(permissions.BasePermission):
return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_manage_winsvcs")
return _has_perm(r, "can_manage_winsvcs")

View File

@@ -26,7 +26,7 @@ def process_nats_response(data: Union[str, Dict]) -> Tuple[bool, bool, str]:
else "timeout"
)
return (success, natserror, errormsg)
return success, natserror, errormsg
class GetServices(APIView):
@@ -41,7 +41,7 @@ class GetServices(APIView):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10))
if r == "timeout" or r == "natsdown":
if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent")
agent.services = r

View File

@@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from software.models import InstalledSoftware
class Command(BaseCommand):
@@ -12,22 +12,15 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
search = kwargs["name"].lower()
agents = Agent.objects.all()
for agent in agents:
try:
sw = agent.installedsoftware_set.first().software
except:
self.stdout.write(
self.style.ERROR(
f"Agent {agent.hostname} missing software list. Try manually refreshing it from the web UI from the software tab."
)
)
continue
for i in sw:
if search in i["name"].lower():
all_sw = InstalledSoftware.objects.select_related(
"agent", "agent__site", "agent__site__client"
)
for instance in all_sw.iterator(chunk_size=20):
for sw in instance.software:
if search in sw["name"].lower():
self.stdout.write(
self.style.SUCCESS(
f"Found {i['name']} installed on {agent.hostname}"
f"Found {sw['name']} installed on: {instance.agent.client.name}\\{instance.agent.site.name}\\{instance.agent.hostname}"
)
)
break

View File

@@ -13,7 +13,6 @@ class SoftwarePerms(permissions.BasePermission):
return _has_perm(r, "can_list_software")
else:
return _has_perm(r, "can_manage_software") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
return _has_perm(r, "can_manage_software") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)

View File

@@ -22,8 +22,8 @@ def chocos(request):
chocos = ChocoSoftware.objects.last()
if not chocos:
return Response({})
else:
return Response(chocos.chocos)
return Response(chocos.chocos)
class GetSoftware(APIView):
@@ -79,7 +79,7 @@ class GetSoftware(APIView):
return notify_error(f"Not available for {agent.plat}")
r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
if r == "timeout" or r == "natsdown":
if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent")
if not InstalledSoftware.objects.filter(agent=agent).exists():

View File

@@ -61,4 +61,4 @@ class APIAuthentication(BaseAuthentication):
if apikey.expiration and apikey.expiration < djangotime.now():
raise exceptions.AuthenticationFailed(_("The token as expired."))
return (apikey.user, apikey.key)
return apikey.user, apikey.key

View File

@@ -10,6 +10,7 @@ class MeshAgentIdent(Enum):
LINUX64 = 6
LINUX_ARM_64 = 26
LINUX_ARM_HF = 25
DARWIN_UNIVERSAL = 10005
def __str__(self):
return str(self.value)
@@ -240,6 +241,14 @@ AGENT_DEFER = (
"modified_time",
)
AGENT_TABLE_DEFER = (
"services",
"created_by",
"created_time",
"modified_by",
"modified_time",
)
ONLINE_AGENTS = (
"pk",
"agent_id",

View File

@@ -1,7 +1,14 @@
from typing import TYPE_CHECKING
import pytz
from django.conf import settings
from django.utils import timezone as djangotime
from rest_framework import status
from rest_framework.response import Response
if TYPE_CHECKING:
from datetime import datetime
def get_certs() -> tuple[str, str]:
domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1]
@@ -12,7 +19,7 @@ def get_certs() -> tuple[str, str]:
cert_file = settings.CERT_FILE
key_file = settings.KEY_FILE
return (cert_file, key_file)
return cert_file, key_file
def notify_error(msg: str) -> Response:
@@ -26,4 +33,16 @@ def get_nats_ports() -> tuple[int, int]:
nats_standard_port = getattr(settings, "NATS_STANDARD_PORT", 4222)
nats_websocket_port = getattr(settings, "NATS_WEBSOCKET_PORT", 9235)
return (nats_standard_port, nats_websocket_port)
return nats_standard_port, nats_websocket_port
def date_is_in_past(*, datetime_obj: "datetime", agent_tz: str) -> bool:
"""
datetime_obj must be a naive datetime
"""
now = djangotime.now()
# convert agent tz to UTC to compare
agent_pytz = pytz.timezone(agent_tz)
localized = agent_pytz.localize(datetime_obj)
utc_time = localized.astimezone(pytz.utc)
return now > utc_time

View File

@@ -1,4 +1,5 @@
import threading
from contextlib import suppress
from typing import Any, Dict, Optional
from django.conf import settings
@@ -62,7 +63,7 @@ class AuditMiddleware:
request = APIView().initialize_request(request)
# check if user is authenticated
try:
with suppress(AuthenticationFailed):
if hasattr(request, "user") and request.user.is_authenticated:
try:
@@ -83,8 +84,6 @@ class AuditMiddleware:
# get authenticated user after request
request_local.username = request.user.username
except AuthenticationFailed:
pass
def process_exception(self, request, exception):
request_local.debug_info = None

View File

@@ -26,7 +26,7 @@ class PermissionQuerySet(models.QuerySet):
model_name = self.model._meta.label.split(".")[1]
# checks which sites and clients the user has access to and filters agents
if model_name in ["Agent", "Deployment"]:
if model_name in ("Agent", "Deployment"):
if can_view_clients:
clients_queryset = models.Q(site__client__in=can_view_clients)
@@ -81,7 +81,7 @@ class PermissionQuerySet(models.QuerySet):
return self
# if model that is being filtered is a Check or Automated task we need to allow checks/tasks that are associated with policies
if model_name in ["Check", "AutomatedTask", "DebugLog"] and (
if model_name in ("Check", "AutomatedTask", "DebugLog") and (
can_view_clients or can_view_sites
):
agent_queryset = models.Q(agent=None) # dont filter if agent is None

View File

@@ -1,4 +1,5 @@
import os
from contextlib import suppress
from datetime import timedelta
from pathlib import Path
@@ -14,29 +15,31 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh"
MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh"
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.14.0"
TRMM_VERSION = "0.15.1"
# https://github.com/amidaware/tacticalrmm-web
WEB_VERSION = "0.100.4"
WEB_VERSION = "0.101.3"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.165"
APP_VER = "0.0.172"
# https://github.com/amidaware/rmmagent
LATEST_AGENT_VER = "2.1.0"
LATEST_AGENT_VER = "2.4.0"
MESH_VER = "1.0.43"
MESH_VER = "1.0.85"
NATS_SERVER_VER = "2.8.4"
NATS_SERVER_VER = "2.9.3"
# for the update script, bump when need to recreate venv
PIP_VER = "31"
PIP_VER = "33"
SETUPTOOLS_VER = "62.6.0"
SETUPTOOLS_VER = "65.5.0"
WHEEL_VER = "0.37.1"
AGENT_BASE_URL = "https://agents.tacticalrmm.com"
@@ -71,10 +74,8 @@ HOSTED = False
SWAGGER_ENABLED = False
REDIS_HOST = "127.0.0.1"
try:
with suppress(ImportError):
from .local_settings import *
except ImportError:
pass
if "GHACTIONS" in os.environ:
DEBUG = False
@@ -104,6 +105,7 @@ if not DEBUG:
)
INSTALLED_APPS = [
"daphne",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",

View File

@@ -28,7 +28,7 @@ from tacticalrmm.constants import (
DebugLogType,
ScriptShell,
)
from tacticalrmm.helpers import get_certs, notify_error, get_nats_ports
from tacticalrmm.helpers import get_certs, get_nats_ports, notify_error
def generate_winagent_exe(
@@ -112,7 +112,7 @@ def bitdays_to_string(day: int) -> str:
return "Every day"
for key, value in WEEK_DAYS.items():
if day & int(value):
if day & value:
ret.append(key)
return ", ".join(ret)
@@ -123,7 +123,7 @@ def bitmonths_to_string(month: int) -> str:
return "Every month"
for key, value in MONTHS.items():
if month & int(value):
if month & value:
ret.append(key)
return ", ".join(ret)
@@ -134,7 +134,7 @@ def bitweeks_to_string(week: int) -> str:
return "Every week"
for key, value in WEEKS.items():
if week & int(value):
if week & value:
ret.append(key)
return ", ".join(ret)
@@ -144,11 +144,11 @@ def bitmonthdays_to_string(day: int) -> str:
if day == MONTH_DAYS["Last Day"]:
return "Last day"
elif day == 2147483647 or day == 4294967295:
elif day in (2147483647, 4294967295):
return "Every day"
for key, value in MONTH_DAYS.items():
if day & int(value):
if day & value:
ret.append(key)
return ", ".join(ret)
@@ -157,8 +157,8 @@ 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}"
return f"PT{tmp}"
def reload_nats() -> None:
@@ -209,6 +209,11 @@ def reload_nats() -> None:
},
}
if "NATS_HTTP_PORT" in os.environ:
config["http_port"] = int(os.getenv("NATS_HTTP_PORT")) # type: ignore
elif hasattr(settings, "NATS_HTTP_PORT"):
config["http_port"] = settings.NATS_HTTP_PORT # type: ignore
conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf")
with open(conf, "w") as f:
json.dump(config, f)
@@ -391,5 +396,5 @@ def format_shell_array(value: list[str]) -> str:
def format_shell_bool(value: bool, shell: Optional[str]) -> str:
if shell == ScriptShell.POWERSHELL:
return "$True" if value else "$False"
else:
return "1" if value else "0"
return "1" if value else "0"

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