Compare commits

...

155 Commits

Author SHA1 Message Date
wh1te909
83ba480863 Merge branch 'master' of https://github.com/wh1te909/tacticalrmm 2021-03-25 23:14:38 +00:00
wh1te909
f158ea25e9 Release 0.4.30 2021-03-25 23:14:16 +00:00
wh1te909
0227519eab bump versions 2021-03-25 23:13:41 +00:00
wh1te909
616a9685fa update reqs 2021-03-25 22:15:58 +00:00
wh1te909
fe61b01320 fix celery async errors 2021-03-24 22:13:02 +00:00
wh1te909
7b25144311 add docs for django admin 2021-03-24 07:12:26 +00:00
sadnub
9d42fbbdd7 exclude mesh agent and debug logs 2021-03-23 10:41:15 -04:00
sadnub
39ac5b088b Update entrypoint.sh 2021-03-23 10:41:04 -04:00
sadnub
c14ffd08a0 exclude mesh agent and debug logs 2021-03-23 09:04:26 -04:00
sadnub
6e1239340b Update entrypoint.sh 2021-03-23 08:56:43 -04:00
wh1te909
a297dc8b3b re-run update.sh when old version detected 2021-03-23 07:39:06 +00:00
wh1te909
8d4ecc0898 update reqs 2021-03-23 07:10:45 +00:00
wh1te909
eae9c04429 Release 0.4.29 2021-03-22 22:35:52 +00:00
wh1te909
a41c48a9c5 bump versions 2021-03-22 22:35:43 +00:00
sadnub
ff2a94bd9b Update dockerfile 2021-03-22 18:12:57 -04:00
wh1te909
4a1f5558b8 Release 0.4.28 2021-03-22 20:48:59 +00:00
wh1te909
608db9889f bump versions 2021-03-22 20:48:39 +00:00
sadnub
012b697337 Update dockerfile 2021-03-22 15:18:11 -04:00
sadnub
0580506cf3 Update entrypoint.sh 2021-03-22 15:17:49 -04:00
Dan
ff4ab9b661 Update issue templates 2021-03-18 23:25:46 -07:00
wh1te909
b7ce5fdd3e Release 0.4.27 2021-03-19 05:21:46 +00:00
wh1te909
a11e617322 bump versions 2021-03-19 05:21:37 +00:00
Dan
d0beac7e2b Merge pull request #330 from silversword411/develop
Added Rename Computer Community Script
2021-03-18 22:19:20 -07:00
silversword411
9db497092f Update Rename_Computer.ps1 2021-03-18 00:38:40 -04:00
wh1te909
8eb91c08aa Release 0.4.26 2021-03-17 17:58:29 +00:00
wh1te909
ded5437522 bump versions 2021-03-17 17:50:56 +00:00
wh1te909
9348657951 fix script manager freezing on latest chrome 2021-03-17 17:36:24 +00:00
wh1te909
bca85933f7 make sure postgres is enabled and running. update npm 2021-03-16 23:09:38 +00:00
silversword411
c32bb35f1c Added Rename Computer Community Script 2021-03-16 17:10:23 -04:00
Dan
4b84062d62 Merge pull request #329 from silversword411/develop
Adding Bluescreen script
2021-03-16 12:04:37 -07:00
silversword411
d6d0f8fa17 fixed description 2021-03-16 14:16:38 -04:00
silversword411
dd72c875d3 Add Bluescreen script
From dinger1986
2021-03-16 14:13:30 -04:00
wh1te909
1a1df50300 show all severity levels closes #326 2021-03-16 17:54:25 +00:00
wh1te909
53cbb527b4 nats 2.2.0 2021-03-16 17:03:09 +00:00
Dan
8b87b2717e Merge pull request #327 from silversword411/develop
Adding AD Recycle Bin script
2021-03-16 09:50:20 -07:00
silversword411
1007d6dac7 Adding AD Recycle Bin script
Check and Enable AD Recycle Bin
2021-03-16 11:39:44 -04:00
Dan
6799fac120 Merge pull request #325 from silversword411/patch-4
Add Chocolatey Update script to community scripts
2021-03-15 10:35:40 -07:00
silversword411
558e6288ca Merge pull request #1 from silversword411/patch-5
Adding Chocolatey updates to community scripts
2021-03-15 05:10:12 -04:00
silversword411
d9cb73291b Adding Chocolatey updates to community scripts 2021-03-15 05:04:32 -04:00
silversword411
d0f7be3ac3 Create Chocolatey_Update_Installed.bat 2021-03-15 04:57:46 -04:00
wh1te909
331e16d3ca bump mesh closes #323 2021-03-13 23:57:46 +00:00
Dan
0db246c311 Merge pull request #324 from silversword411/patch-3
Avoid multiple update file versions
2021-03-13 14:13:03 -08:00
silversword411
94dc62ff58 Avoid multiple update file versions
Kept getting update.sh.1, update.sh.2 etc with each run and then the auto-pasted command wouldn't be running the latest version of the file.
2021-03-13 12:14:53 -05:00
wh1te909
e68ecf6844 update demo link 2021-03-12 08:22:02 +00:00
Dan
5167b0a8c6 Merge pull request #322 from silversword411/patch-3
Removing extra folder
2021-03-12 00:00:06 -08:00
silversword411
77e3d3786d Removing extra folder 2021-03-11 19:16:27 -05:00
wh1te909
708d4d39bc add test 2021-03-11 19:26:36 +00:00
Dan
2a8cda2a1e Merge pull request #321 from silversword411/patch-3
Updating to match install scripts
2021-03-11 10:47:10 -08:00
silversword411
8d783840ad Updating to match install scripts 2021-03-11 12:02:56 -05:00
wh1te909
abe39d5790 remove checks for older agents 2021-03-11 10:53:27 +00:00
wh1te909
d7868e9e5a Release 0.4.25 2021-03-11 10:11:45 +00:00
wh1te909
7b84e36e15 bump versions 2021-03-11 10:11:13 +00:00
wh1te909
6cab6d69d8 Release 0.4.24 2021-03-11 04:36:34 +00:00
wh1te909
87846d7aef bump versions 2021-03-11 04:36:14 +00:00
wh1te909
2557769c6a fix runchecks wh1te909/rmmagent@739e7434ae 2021-03-11 04:20:18 +00:00
wh1te909
48375f3878 Release 0.4.23 2021-03-11 00:35:02 +00:00
wh1te909
176c85d8c1 bump versions 2021-03-11 00:32:31 +00:00
wh1te909
17cad71ede typo 2021-03-10 22:46:11 +00:00
wh1te909
e8bf9d4e6f change thresholds for check run interval 2021-03-10 22:39:16 +00:00
wh1te909
7bdd2038ef enable django admin during install so that it installs properly, disable it at end of install 2021-03-10 22:32:36 +00:00
wh1te909
e9f6e7943a bump mesh 2021-03-10 19:52:37 +00:00
wh1te909
e74ba387ab update reqs 2021-03-10 19:03:11 +00:00
wh1te909
27c79e5b99 refactor method 2021-03-09 09:39:58 +00:00
wh1te909
8170d5ea73 feat: add client tree sorting closes #316 2021-03-09 03:17:43 +00:00
wh1te909
196f73705d isort 2021-03-09 03:14:56 +00:00
wh1te909
ad0bbf5248 add sorting back to status closes #305 2021-03-08 21:17:26 +00:00
wh1te909
4cae9cd90d add hostname to email subject 2021-03-08 06:58:02 +00:00
wh1te909
be7bc55a76 remove redundant buttons that are already in context menus 2021-03-07 10:21:46 +00:00
wh1te909
684b545e8f exclude date 2021-03-07 10:21:08 +00:00
wh1te909
7835cc3b10 update community scripts 2021-03-06 22:11:58 +00:00
Tragic Bronson
f8706b51e8 Merge pull request #314 from nr-plaxon/patch-3
Adding script to create an all-user logon script
2021-03-06 13:56:32 -08:00
nr-plaxon
d97f8fd5da Adding script to create an all-user logon script 2021-03-06 14:40:53 +01:00
sadnub
f8fa87441e black 2021-03-05 23:32:40 -05:00
sadnub
d42537814a sort of addresses #177. Allow ability to override check intervals 2021-03-05 23:27:54 -05:00
sadnub
792421b0e2 adds #66. EventLog Check: Set the number of event logs found before passing/failing 2021-03-05 21:52:08 -05:00
wh1te909
72d55a010b Release 0.4.22 2021-03-05 23:05:17 +00:00
wh1te909
880d8258ce bump versions 2021-03-05 23:02:08 +00:00
wh1te909
b79bf82efb update docs 2021-03-05 22:22:49 +00:00
wh1te909
b3118b6253 add fields to queryset 2021-03-05 09:30:53 +00:00
sadnub
ba172e2e25 fix issue with exception when other pending actions types exists 2021-03-04 16:31:25 -05:00
sadnub
892d53abeb move alert_template to property on agent versus dynamically generating it everytime 2021-03-04 16:27:05 -05:00
sadnub
5cbaa1ce98 fix tests 2021-03-03 22:25:02 -05:00
sadnub
7b35d9ad2e add policy sync to automation manager 2021-03-03 22:03:11 -05:00
wh1te909
8462de7911 fix wording 2021-03-04 02:20:54 +00:00
wh1te909
8721f44298 fix tests 2021-03-04 01:10:52 +00:00
wh1te909
c7a2d69afa rework agent recovery wh1te909/rmmagent@cef1a0efed 2021-03-04 00:51:03 +00:00
wh1te909
0453d81e7a fix pendingactions count 2021-03-03 11:07:20 +00:00
wh1te909
501c04ac2b Release 0.4.21 2021-03-03 10:44:49 +00:00
wh1te909
0ef4e9a5c3 bump versions 2021-03-03 10:44:34 +00:00
wh1te909
129c50e598 fix search/sort 2021-03-03 10:17:45 +00:00
wh1te909
3e276fc2ac isort 2021-03-03 10:17:06 +00:00
sadnub
658d5e05ae black 2021-03-02 23:38:13 -05:00
sadnub
4e7d5d476e add policy exclusions 2021-03-02 23:33:34 -05:00
wh1te909
6a55ca20f3 Release 0.4.20 2021-03-02 23:42:38 +00:00
wh1te909
c56c537f7f HOTFIX 0.4.20 temporarily disable some sorting 2021-03-02 23:42:00 +00:00
wh1te909
fd7d776121 Release 0.4.19 2021-03-02 22:18:18 +00:00
wh1te909
1af28190d8 bump versions 2021-03-02 22:11:40 +00:00
wh1te909
6b305be567 add dash 2021-03-02 22:08:15 +00:00
wh1te909
3bf70513b7 isort 2021-03-02 09:18:35 +00:00
wh1te909
7e64404654 add type hints 2021-03-02 09:13:24 +00:00
wh1te909
e1b5226f34 fix alert 2021-03-02 08:46:41 +00:00
wh1te909
0d7128ad31 Revert "bump versions"
This reverts commit 5778626087.
2021-03-02 08:41:17 +00:00
wh1te909
5778626087 bump versions 2021-03-02 08:07:39 +00:00
wh1te909
3ff48756ed continue on defender errors 2021-03-02 07:38:14 +00:00
sadnub
0ce9a6eeba black 2021-03-01 22:14:48 -05:00
sadnub
ad527b4aed alerts rework and tests 2021-03-01 22:10:38 -05:00
sadnub
6633bb452e remove jest and add cypress for frontend testing 2021-03-01 22:10:38 -05:00
wh1te909
efeb0b4feb add tests 2021-03-02 00:45:37 +00:00
wh1te909
8cc11fc102 fix pendingactions ui 2021-03-02 00:39:42 +00:00
Tragic Bronson
ee6a167220 Merge pull request #302 from silversword411/patch-2
tweak for workflow
2021-03-01 16:16:38 -08:00
silversword411
8d4ad3c405 tweak for workflow 2021-03-01 19:11:01 -05:00
Tragic Bronson
072fbf4d60 Merge pull request #299 from silversword411/patch-3
Linking to FAQ
2021-03-01 15:24:55 -08:00
silversword411
727c41c283 Update install_server.md 2021-03-01 18:15:12 -05:00
silversword411
e2266838b6 Linking to FAQ
minor update and link to FAQ
2021-03-01 17:59:53 -05:00
Tragic Bronson
775762d615 Merge pull request #298 from silversword411/patch-2
Fixing bash commands
2021-03-01 14:56:34 -08:00
silversword411
900c3008cb Fixing bash commands
Removing ID/server so paste will work
2021-03-01 17:44:35 -05:00
sadnub
09379213a6 fix formatting 2021-03-01 17:37:24 -05:00
sadnub
ceb97048e3 Update mkdocs.yml 2021-03-01 17:34:27 -05:00
sadnub
4561515517 Create update_docker.md 2021-03-01 17:33:41 -05:00
wh1te909
a7b285759f delete chocolog model 2021-03-01 21:43:54 +00:00
wh1te909
b4531b2a12 ui tweaks 2021-03-01 21:37:59 +00:00
wh1te909
9e1d261c76 update faq 2021-03-01 21:09:12 +00:00
Tragic Bronson
e35fa15cd2 Merge pull request #297 from silversword411/patch-1
Docs addition - Recover login for Mesh Central
2021-03-01 13:01:11 -08:00
wh1te909
dbd1f0d4f9 pending actions refactor 2021-03-01 20:40:46 +00:00
wh1te909
9ade78b703 fix restore docs 2021-03-01 19:45:10 +00:00
silversword411
f20e244b5f Recover login for Mesh Central 2021-03-01 12:50:56 -05:00
wh1te909
0989308b7e fix tests 2021-03-01 09:35:26 +00:00
wh1te909
12c7140536 more choco rework 2021-03-01 09:26:37 +00:00
wh1te909
2a0b605e92 return empty val for missing software install date 2021-03-01 08:21:56 +00:00
wh1te909
6978890e6a add contributing docs 2021-03-01 07:51:31 +00:00
Tragic Bronson
561abd6cb9 Merge pull request #296 from beejayzed/develop
Add community script to verify antivirus status
2021-02-28 23:32:55 -08:00
beejayzed
4dd6227f0b Update community_scripts.json 2021-03-01 07:55:31 +07:00
beejayzed
1ec314c31c Rename VerifyAntivirus to VerifyAntivirus.ps1 2021-03-01 07:52:43 +07:00
beejayzed
a2be5a00be Create VerifyAntivirus 2021-03-01 07:50:56 +07:00
wh1te909
4e2241c115 start chocolatey rework 2021-02-28 11:00:45 +00:00
wh1te909
8459bca64a fix nats ping dict 2021-02-28 09:54:53 +00:00
wh1te909
24cb0565b9 add pagination to agent table 2021-02-28 09:18:04 +00:00
wh1te909
9442acb028 fix pipeline typo 2021-02-27 23:41:08 +00:00
wh1te909
4f7f181a42 fix pipeline 2021-02-27 23:24:06 +00:00
wh1te909
b7dd8737a7 make django admin disabled by default 2021-02-27 23:19:35 +00:00
wh1te909
2207eeb727 add missing import 2021-02-27 23:09:01 +00:00
wh1te909
89dad7dfe7 add sponsors info to docs 2021-02-27 22:37:11 +00:00
wh1te909
e5803d0cf3 bump mesh 2021-02-27 07:45:56 +00:00
wh1te909
c1fffe9ae6 add timeout to net 2021-02-27 06:08:42 +00:00
wh1te909
9e6cbd3d32 set uwsgi procs based on cpu count 2021-02-27 05:28:42 +00:00
wh1te909
2ea8742510 natsapi refactor 2021-02-27 00:23:03 +00:00
wh1te909
5cfa0254f9 isort 2021-02-26 23:25:44 +00:00
wh1te909
8cd2544f78 add new management command 2021-02-26 22:05:42 +00:00
wh1te909
c03b768364 fix typos 2021-02-26 09:01:14 +00:00
wh1te909
d60481ead4 add docs for management commands 2021-02-25 20:55:56 +00:00
Tragic Bronson
126be3827d Merge pull request #292 from bradhawkins85/patch-6
Update installer.ps1
2021-02-25 10:06:04 -08:00
bradhawkins85
121274dca2 Update installer.ps1
Don't try and add Windows Defender exceptions if Defender is not enabled, prevents errors during script execution.
2021-02-25 19:59:29 +10:00
wh1te909
0ecf8da27e add management commands for resetting pw/2fa 2021-02-25 07:56:17 +00:00
wh1te909
4a6bcb525d update docs 2021-02-25 07:55:13 +00:00
wh1te909
83f9ee50dd add management commands for resetting pw/2fa 2021-02-25 07:55:03 +00:00
150 changed files with 30329 additions and 56509 deletions

View File

@@ -100,6 +100,7 @@ MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
REDIS_HOST = '${REDIS_HOST}'
ADMIN_ENABLED = True
EOF
)"

2
.github/FUNDING.yml vendored
View File

@@ -3,7 +3,7 @@
github: wh1te909
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
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

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,40 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: ''
assignees: ''
---
**Server Info (please complete the following information):**
- OS: [e.g. Ubuntu 20.04, Debian 10]
- Browser: [e.g. chrome, safari]
- RMM Version (as shown in top left of web UI):
**Installation Method:**
- [ ] Standard
- [ ] Docker
**Agent Info (please complete the following information):**
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -8,7 +8,7 @@
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
# [LIVE DEMO](https://rmm.xlawgaming.com/)
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*

View File

@@ -0,0 +1,57 @@
import os
import subprocess
import pyotp
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Reset 2fa"
def add_arguments(self, parser):
parser.add_argument("username", type=str)
def handle(self, *args, **kwargs):
username = kwargs["username"]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
return
domain = "Tactical RMM"
nginx = "/etc/nginx/sites-available/frontend.conf"
found = None
if os.path.exists(nginx):
try:
with open(nginx, "r") as f:
for line in f:
if "server_name" in line:
found = line
break
if found:
rep = found.replace("server_name", "").replace(";", "")
domain = "".join(rep.split())
except:
pass
code = pyotp.random_base32()
user.totp_key = code
user.save(update_fields=["totp_key"])
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
subprocess.run(f'qr "{url}"', shell=True)
self.stdout.write(
self.style.WARNING("Scan the barcode above with your authenticator app")
)
self.stdout.write(
self.style.WARNING(
f"If that doesn't work you may manually enter the setup key: {code}"
)
)
self.stdout.write(
self.style.SUCCESS(f"2fa was successfully reset for user {username}")
)

View File

@@ -0,0 +1,22 @@
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Reset password for user"
def add_arguments(self, parser):
parser.add_argument("username", type=str)
def handle(self, *args, **kwargs):
username = kwargs["username"]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
return
passwd = input("Enter new password: ")
user.set_password(passwd)
user.save()
self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-02-28 06:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0011_user_default_agent_tbl_tab'),
]
operations = [
migrations.AddField(
model_name='user',
name='agents_per_page',
field=models.PositiveIntegerField(default=50),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-09 02:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0012_user_agents_per_page'),
]
operations = [
migrations.AddField(
model_name='user',
name='client_tree_sort',
field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
),
]

View File

@@ -15,6 +15,11 @@ AGENT_TBL_TAB_CHOICES = [
("mixed", "Mixed"),
]
CLIENT_TREE_SORT_CHOICES = [
("alphafail", "Move failing clients to the top"),
("alpha", "Sort alphabetically"),
]
class User(AbstractUser, BaseAuditModel):
is_active = models.BooleanField(default=True)
@@ -27,6 +32,10 @@ class User(AbstractUser, BaseAuditModel):
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
)
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
client_tree_sort = models.CharField(
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
)
agent = models.OneToOneField(
"agents.Agent",

View File

@@ -4,6 +4,18 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import User
class UserUISerializer(ModelSerializer):
class Meta:
model = User
fields = [
"dark_mode",
"show_community_scripts",
"agent_dblclick_action",
"default_agent_tbl_tab",
"client_tree_sort",
]
class UserSerializer(ModelSerializer):
class Meta:
model = User

View File

@@ -271,18 +271,13 @@ class TestUserAction(TacticalTestCase):
def test_user_ui(self):
url = "/accounts/users/ui/"
data = {"dark_mode": False}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {"show_community_scripts": True}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
data = {
"userui": True,
"dark_mode": True,
"show_community_scripts": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
"client_tree_sort": "alpha",
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)

View File

@@ -14,7 +14,15 @@ from logs.models import AuditLog
from tacticalrmm.utils import notify_error
from .models import User
from .serializers import TOTPSetupSerializer, UserSerializer
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
def _is_root_user(request, user) -> bool:
return (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
class CheckCreds(KnoxLoginView):
@@ -105,11 +113,7 @@ class GetUpdateDeleteUser(APIView):
def put(self, request, pk):
user = get_object_or_404(User, pk=pk)
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
serializer = UserSerializer(instance=user, data=request.data, partial=True)
@@ -120,11 +124,7 @@ class GetUpdateDeleteUser(APIView):
def delete(self, request, pk):
user = get_object_or_404(User, pk=pk)
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be deleted from the UI")
user.delete()
@@ -137,11 +137,7 @@ class UserActions(APIView):
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
user.set_password(request.data["password"])
@@ -152,11 +148,7 @@ class UserActions(APIView):
# reset two factor token
def put(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
):
if _is_root_user(request, user):
return notify_error("The root user cannot be modified from the UI")
user.totp_key = ""
@@ -184,19 +176,9 @@ class TOTPSetup(APIView):
class UserUI(APIView):
def patch(self, request):
user = request.user
if "dark_mode" in request.data.keys():
user.dark_mode = request.data["dark_mode"]
user.save(update_fields=["dark_mode"])
if "show_community_scripts" in request.data.keys():
user.show_community_scripts = request.data["show_community_scripts"]
user.save(update_fields=["show_community_scripts"])
if "userui" in request.data.keys():
user.agent_dblclick_action = request.data["agent_dblclick_action"]
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
serializer = UserUISerializer(
instance=request.user, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")

View File

@@ -43,6 +43,10 @@ workstation_agent = agent.extend(
online_agent = agent.extend(last_seen=djangotime.now())
offline_agent = agent.extend(
last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
)
overdue_agent = agent.extend(
last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
)

View File

@@ -0,0 +1,93 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from clients.models import Client, Site
class Command(BaseCommand):
help = "Bulk update agent offline/overdue time"
def add_arguments(self, parser):
parser.add_argument("time", type=int, help="Time in minutes")
parser.add_argument(
"--client",
type=str,
help="Client Name",
)
parser.add_argument(
"--site",
type=str,
help="Site Name",
)
parser.add_argument(
"--offline",
action="store_true",
help="Offline",
)
parser.add_argument(
"--overdue",
action="store_true",
help="Overdue",
)
parser.add_argument(
"--all",
action="store_true",
help="All agents",
)
def handle(self, *args, **kwargs):
time = kwargs["time"]
client_name = kwargs["client"]
site_name = kwargs["site"]
all_agents = kwargs["all"]
offline = kwargs["offline"]
overdue = kwargs["overdue"]
agents = None
if offline and time < 2:
self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes"))
return
if overdue and time < 3:
self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes"))
return
if client_name:
try:
client = Client.objects.get(name=client_name)
except Client.DoesNotExist:
self.stdout.write(
self.style.ERROR(f"Client {client_name} doesn't exist")
)
return
agents = Agent.objects.filter(site__client=client)
elif site_name:
try:
site = Site.objects.get(name=site_name)
except Site.DoesNotExist:
self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist"))
return
agents = Agent.objects.filter(site=site)
elif all_agents:
agents = Agent.objects.all()
if agents:
if offline:
agents.update(offline_time=time)
self.stdout.write(
self.style.SUCCESS(
f"Changed offline time on {len(agents)} agents to {time} minutes"
)
)
if overdue:
agents.update(overdue_time=time)
self.stdout.write(
self.style.SUCCESS(
f"Changed overdue time on {len(agents)} agents to {time} minutes"
)
)

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-03-04 03:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0006_auto_20210217_1736'),
('agents', '0030_agent_offline_time'),
]
operations = [
migrations.AddField(
model_name='agent',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
),
]

View File

@@ -20,7 +20,6 @@ from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from packaging import version as pyver
from alerts.models import AlertTemplate
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
@@ -64,6 +63,13 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
site = models.ForeignKey(
"clients.Site",
related_name="agents",
@@ -85,7 +91,7 @@ class Agent(BaseAuditModel):
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
# check if new agent has been create
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
if (
@@ -104,14 +110,6 @@ class Agent(BaseAuditModel):
def client(self):
return self.site.client
@property
def has_nats(self):
return pyver.parse(self.version) >= pyver.parse("1.1.0")
@property
def has_gotasks(self):
return pyver.parse(self.version) >= pyver.parse("1.1.1")
@property
def timezone(self):
# return the default timezone unless the timezone is explicity set per agent
@@ -241,6 +239,7 @@ class Agent(BaseAuditModel):
pass
try:
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
except:
pass
@@ -270,6 +269,20 @@ class Agent(BaseAuditModel):
except:
return ["unknown disk"]
def check_run_interval(self) -> int:
interval = self.check_interval
# determine if any agent checks have a custom interval and set the lowest interval
for check in self.agentchecks.filter(overriden_by_policy=False): # type: ignore
if check.run_interval and check.run_interval < interval:
# don't allow check runs less than 15s
if check.run_interval < 15:
interval = 15
else:
interval = check.run_interval
return interval
def run_script(
self,
scriptpk: int,
@@ -295,10 +308,10 @@ class Agent(BaseAuditModel):
running_agent = self
if run_on_any:
nats_ping = {"func": "ping", "timeout": 1}
nats_ping = {"func": "ping"}
# try on self first
r = asyncio.run(self.nats_cmd(nats_ping))
r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
if r == "pong":
running_agent = self
@@ -312,7 +325,7 @@ class Agent(BaseAuditModel):
]
for agent in online:
r = asyncio.run(agent.nats_cmd(nats_ping))
r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
if r == "pong":
running_agent = agent
break
@@ -460,9 +473,9 @@ class Agent(BaseAuditModel):
)
)
# returns alert template assigned in the following order: policy, site, client, global
# will return None if nothing is found
def get_alert_template(self) -> Union[AlertTemplate, None]:
# sets alert template assigned in the following order: policy, site, client, global
# sets None if nothing is found
def set_alert_template(self):
site = self.site
client = self.client
@@ -562,9 +575,16 @@ class Agent(BaseAuditModel):
continue
else:
# save alert_template to agent cache field
self.alert_template = template
self.save()
return template
# no alert templates found or agent has been excluded
self.alert_template = None
self.save()
return None
def generate_checks_from_policies(self):
@@ -702,165 +722,29 @@ class Agent(BaseAuditModel):
# for clearing duplicate pending actions on agent
def remove_matching_pending_task_actions(self, task_id):
# remove any other pending actions on agent with same task_id
for action in self.pendingactions.exclude(status="completed"): # type: ignore
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
if action.details["task_id"] == task_id:
action.delete()
def handle_alert(self, checkin: bool = False) -> None:
from agents.tasks import (
agent_outage_email_task,
agent_outage_sms_task,
agent_recovery_email_task,
agent_recovery_sms_task,
def should_create_alert(self, alert_template=None):
return (
self.overdue_dashboard_alert
or self.overdue_email_alert
or self.overdue_text_alert
or (
alert_template
and (
alert_template.agent_always_alert
or alert_template.agent_always_email
or alert_template.agent_always_text
)
)
)
from alerts.models import Alert
# return if agent is in maintenace mode
if self.maintenance_mode:
return
alert_template = self.get_alert_template()
# called when agent is back online
if checkin:
if Alert.objects.filter(agent=self, resolved=False).exists():
# resolve alert if exists
alert = Alert.objects.get(agent=self, resolved=False)
alert.resolve()
# check if a resolved notification should be emailed
if (
alert_template
and alert_template.agent_email_on_resolved
and not alert.resolved_email_sent
):
agent_recovery_email_task.delay(pk=alert.pk)
# check if a resolved notification should be texted
if (
alert_template
and alert_template.agent_text_on_resolved
and not alert.resolved_sms_sent
):
agent_recovery_sms_task.delay(pk=alert.pk)
# check if any scripts should be run
if not alert.resolved_action_run and (
alert_template and alert_template.resolved_action
):
r = self.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
)
# called when agent is offline
else:
# check if alert hasn't been created yet so create it
if not Alert.objects.filter(agent=self, resolved=False).exists():
# check if alert should be created and if not return
if (
self.overdue_dashboard_alert
or self.overdue_email_alert
or self.overdue_text_alert
or (
alert_template
and (
alert_template.agent_always_alert
or alert_template.agent_always_email
or alert_template.agent_always_text
)
)
):
alert = Alert.create_availability_alert(self)
else:
return
# add a null check history to allow gaps in graph
for check in self.agentchecks.all(): # type: ignore
check.add_check_history(None)
else:
alert = Alert.objects.get(agent=self, resolved=False)
# create dashboard alert if enabled
if self.overdue_dashboard_alert or (
alert_template and alert_template.agent_always_alert
):
alert.hidden = False
alert.save()
# send email alert if enabled
if self.overdue_email_alert or (
alert_template and alert_template.agent_always_email
):
agent_outage_email_task.delay(
pk=alert.pk,
alert_interval=alert_template.agent_periodic_alert_days
if alert_template
else None,
)
# send text message if enabled
if self.overdue_text_alert or (
alert_template and alert_template.agent_always_text
):
agent_outage_sms_task.delay(
pk=alert.pk,
alert_interval=alert_template.agent_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if not alert.action_run and alert_template and alert_template.action:
r = self.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if isinstance(r, dict):
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
)
def send_outage_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail(
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
(
@@ -869,14 +753,13 @@ class Agent(BaseAuditModel):
f"agent {self.hostname} "
"within the expected time."
),
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_recovery_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail(
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
(
@@ -885,27 +768,25 @@ class Agent(BaseAuditModel):
f"agent {self.hostname} "
"after an interruption in data transmission."
),
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_outage_sms(self):
from core.models import CoreSettings
alert_template = self.get_alert_template()
CORE = CoreSettings.objects.first()
CORE.send_sms(
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=alert_template,
alert_template=self.alert_template,
)
def send_recovery_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_sms(
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=alert_template,
alert_template=self.alert_template,
)

View File

@@ -57,16 +57,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
alert_template = serializers.SerializerMethodField()
def get_alert_template(self, obj):
alert_template = obj.get_alert_template()
if not alert_template:
if not obj.alert_template:
return None
else:
return {
"name": alert_template.name,
"always_email": alert_template.agent_always_email,
"always_text": alert_template.agent_always_text,
"always_alert": alert_template.agent_always_alert,
"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_pending_actions(self, obj):

View File

@@ -183,6 +183,8 @@ def agent_recovery_sms_task(pk: int) -> str:
@app.task
def agent_outages_task() -> None:
from alerts.models import Alert
agents = Agent.objects.only(
"pk",
"last_seen",
@@ -195,21 +197,7 @@ def agent_outages_task() -> None:
for agent in agents:
if agent.status == "overdue":
agent.handle_alert()
@app.task
def handle_agent_recovery_task(pk: int) -> None:
sleep(10)
from agents.models import RecoveryAction
action = RecoveryAction.objects.get(pk=pk)
if action.mode == "command":
data = {"func": "recoverycmd", "recoverycommand": action.command}
else:
data = {"func": "recover", "payload": {"mode": action.mode}}
asyncio.run(action.agent.nats_cmd(data, wait=False))
Alert.handle_alert_failure(agent)
@app.task

View File

@@ -17,6 +17,63 @@ from .serializers import AgentSerializer
from .tasks import auto_self_agent_update_task
class TestAgentsList(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_agents_list(self):
url = "/agents/listagents/"
# 36 total agents
company1 = baker.make("clients.Client")
company2 = baker.make("clients.Client")
site1 = baker.make("clients.Site", client=company1)
site2 = baker.make("clients.Site", client=company1)
site3 = baker.make("clients.Site", client=company2)
baker.make_recipe(
"agents.online_agent", site=site1, monitoring_type="server", _quantity=15
)
baker.make_recipe(
"agents.online_agent",
site=site2,
monitoring_type="workstation",
_quantity=10,
)
baker.make_recipe(
"agents.online_agent",
site=site3,
monitoring_type="server",
_quantity=4,
)
baker.make_recipe(
"agents.online_agent",
site=site3,
monitoring_type="workstation",
_quantity=7,
)
# test all agents
r = self.client.patch(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 36) # type: ignore
# test client1
data = {"clientPK": company1.pk} # type: ignore
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 25) # type: ignore
# test site3
data = {"sitePK": site3.pk} # type: ignore
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 11) # type: ignore
self.check_not_authenticated("patch", url)
class TestAgentViews(TacticalTestCase):
def setUp(self):
self.authenticate()
@@ -141,11 +198,6 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_get_processes(self, mock_ret):
agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
url_old = f"/agents/{agent_old.pk}/getprocs/"
r = self.client.get(url_old)
self.assertEqual(r.status_code, 400)
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
url = f"/agents/{agent.pk}/getprocs/"
@@ -256,7 +308,7 @@ class TestAgentViews(TacticalTestCase):
mock_ret.return_value = "nt authority\system"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertIsInstance(r.data, str)
self.assertIsInstance(r.data, str) # type: ignore
mock_ret.return_value = "timeout"
r = self.client.post(url, data, format="json")
@@ -276,15 +328,16 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.return_value = "ok"
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["agent"], self.agent.hostname)
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") # type: ignore
self.assertEqual(r.data["agent"], self.agent.hostname) # type: ignore
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "schedreboot",
"deleteafter": True,
"trigger": "once",
"name": r.data["task_name"],
"name": r.data["task_name"], # type: ignore
"year": 2025,
"month": "August",
"day": 29,
@@ -305,7 +358,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.patch(url, data_invalid, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "Invalid date")
self.assertEqual(r.data, "Invalid date") # type: ignore
self.check_not_authenticated("patch", url)
@@ -316,8 +369,8 @@ class TestAgentViews(TacticalTestCase):
site = baker.make("clients.Site")
data = {
"client": site.client.id,
"site": site.id,
"client": site.client.id, # type: ignore
"site": site.id, # type: ignore
"arch": "64",
"expires": 23,
"installMethod": "exe",
@@ -364,50 +417,69 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url)
def test_recover(self):
@patch("agents.models.Agent.nats_cmd")
def test_recover(self, nats_cmd):
from agents.models import RecoveryAction
self.agent.version = "0.11.1"
self.agent.save(update_fields=["version"])
RecoveryAction.objects.all().delete()
url = "/agents/recover/"
data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
agent = baker.make_recipe("agents.online_agent")
# test mesh realtime
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
)
nats_cmd.reset_mock()
data["mode"] = "mesh"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("pending", r.json())
RecoveryAction.objects.all().delete()
data["mode"] = "command"
data["cmd"] = "ipconfig /flushdns"
# test mesh with agent rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
RecoveryAction.objects.all().delete()
data["cmd"] = None
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 1)
mesh_recovery = RecoveryAction.objects.first()
self.assertEqual(mesh_recovery.mode, "mesh")
nats_cmd.reset_mock()
RecoveryAction.objects.all().delete()
self.agent.version = "0.9.4"
self.agent.save(update_fields=["version"])
data["mode"] = "mesh"
# test tacagent realtime
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertIn("0.9.5", r.json())
self.check_not_authenticated("post", url)
def test_agents_list(self):
url = "/agents/listagents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
)
nats_cmd.reset_mock()
self.check_not_authenticated("get", url)
# test tacagent with rpc not working
data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 0)
nats_cmd.reset_mock()
# test shell cmd without command
data = {"pk": agent.pk, "cmd": None, "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(RecoveryAction.objects.count(), 0)
# test shell cmd
data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(RecoveryAction.objects.count(), 1)
cmd_recovery = RecoveryAction.objects.first()
self.assertEqual(cmd_recovery.mode, "command")
self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
def test_agents_agent_detail(self):
url = f"/agents/{self.agent.pk}/agentdetail/"
@@ -425,7 +497,7 @@ class TestAgentViews(TacticalTestCase):
edit = {
"id": self.agent.pk,
"site": site.id,
"site": site.id, # type: ignore
"monitoring_type": "workstation",
"description": "asjdk234andasd",
"offline_time": 4,
@@ -456,7 +528,7 @@ class TestAgentViews(TacticalTestCase):
agent = Agent.objects.get(pk=self.agent.pk)
data = AgentSerializer(agent).data
self.assertEqual(data["site"], site.id)
self.assertEqual(data["site"], site.id) # type: ignore
policy = WinUpdatePolicy.objects.get(agent=self.agent)
data = WinUpdatePolicySerializer(policy).data
@@ -474,21 +546,21 @@ class TestAgentViews(TacticalTestCase):
# TODO
# decode the cookie
self.assertIn("&viewmode=13", r.data["file"])
self.assertIn("&viewmode=12", r.data["terminal"])
self.assertIn("&viewmode=11", r.data["control"])
self.assertIn("&viewmode=13", r.data["file"]) # type: ignore
self.assertIn("&viewmode=12", r.data["terminal"]) # type: ignore
self.assertIn("&viewmode=11", r.data["control"]) # type: ignore
self.assertIn("&gotonode=", r.data["file"])
self.assertIn("&gotonode=", r.data["terminal"])
self.assertIn("&gotonode=", r.data["control"])
self.assertIn("&gotonode=", r.data["file"]) # type: ignore
self.assertIn("&gotonode=", r.data["terminal"]) # type: ignore
self.assertIn("&gotonode=", r.data["control"]) # type: ignore
self.assertIn("?login=", r.data["file"])
self.assertIn("?login=", r.data["terminal"])
self.assertIn("?login=", r.data["control"])
self.assertIn("?login=", r.data["file"]) # type: ignore
self.assertIn("?login=", r.data["terminal"]) # type: ignore
self.assertIn("?login=", r.data["control"]) # type: ignore
self.assertEqual(self.agent.hostname, r.data["hostname"])
self.assertEqual(self.agent.client.name, r.data["client"])
self.assertEqual(self.agent.site.name, r.data["site"])
self.assertEqual(self.agent.hostname, r.data["hostname"]) # type: ignore
self.assertEqual(self.agent.client.name, r.data["client"]) # type: ignore
self.assertEqual(self.agent.site.name, r.data["site"]) # type: ignore
self.assertEqual(r.status_code, 200)
@@ -498,32 +570,6 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_by_client(self):
url = f"/agents/byclient/{self.agent.client.id}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.data)
url = f"/agents/byclient/500/"
r = self.client.get(url)
self.assertFalse(r.data) # returns empty list
self.check_not_authenticated("get", url)
def test_by_site(self):
url = f"/agents/bysite/{self.agent.site.id}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.data)
url = f"/agents/bysite/500/"
r = self.client.get(url)
self.assertEqual(r.data, [])
self.check_not_authenticated("get", url)
def test_overdue_action(self):
url = "/agents/overdueaction/"
@@ -532,14 +578,14 @@ class TestAgentViews(TacticalTestCase):
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
self.assertEqual(self.agent.hostname, r.data) # type: ignore
payload = {"pk": self.agent.pk, "overdue_text_alert": False}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
self.assertEqual(self.agent.hostname, r.data) # type: ignore
self.check_not_authenticated("post", url)
@@ -683,7 +729,7 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.return_value = "ok"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn(self.agent.hostname, r.data)
self.assertIn(self.agent.hostname, r.data) # type: ignore
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
)
@@ -800,7 +846,7 @@ class TestAgentViewsNew(TacticalTestCase):
r = self.client.post(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, data)
self.assertEqual(r.data, data) # type: ignore
self.check_not_authenticated("post", url)
@@ -812,14 +858,14 @@ class TestAgentViewsNew(TacticalTestCase):
agent = baker.make_recipe("agents.agent", site=site)
# Test client toggle maintenance mode
data = {"type": "Client", "id": site.client.id, "action": True}
data = {"type": "Client", "id": site.client.id, "action": True} # type: ignore
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode)
# Test site toggle maintenance mode
data = {"type": "Site", "id": site.id, "action": False}
data = {"type": "Site", "id": site.id, "action": False} # type: ignore
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)

View File

@@ -6,8 +6,6 @@ urlpatterns = [
path("listagents/", views.AgentsTableList.as_view()),
path("listagentsnodetail/", views.list_agents_no_detail),
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
path("byclient/<int:clientpk>/", views.by_client),
path("bysite/<int:sitepk>/", views.by_site),
path("overdueaction/", views.overdue_action),
path("sendrawcmd/", views.send_raw_cmd),
path("<pk>/agentdetail/", views.agent_detail),

View File

@@ -9,7 +9,7 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from loguru import logger
from packaging import version as pyver
from rest_framework import generics, status
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -69,10 +69,9 @@ def update_agents(request):
def ping(request, pk):
agent = get_object_or_404(Agent, pk=pk)
status = "offline"
if agent.has_nats:
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
return Response({"name": agent.hostname, "status": status})
@@ -80,8 +79,7 @@ def ping(request, pk):
@api_view(["DELETE"])
def uninstall(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
name = agent.hostname
agent.delete()
@@ -147,9 +145,6 @@ def agent_detail(request, pk):
@api_view()
def get_processes(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if pyver.parse(agent.version) < pyver.parse("1.2.0"):
return notify_error("Requires agent version 1.2.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout":
return notify_error("Unable to contact the agent")
@@ -159,9 +154,6 @@ def get_processes(request, pk):
@api_view()
def kill_proc(request, pk, pid):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
)
@@ -177,8 +169,6 @@ def kill_proc(request, pk, pid):
@api_view()
def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = 180 if logtype == "Security" else 30
data = {
"func": "eventlog",
@@ -198,8 +188,6 @@ def get_event_log(request, pk, logtype, days):
@api_view(["POST"])
def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = int(request.data["timeout"])
data = {
"func": "rawcmd",
@@ -224,15 +212,32 @@ def send_raw_cmd(request):
return Response(r)
class AgentsTableList(generics.ListAPIView):
queryset = (
Agent.objects.select_related("site")
.prefetch_related("agentchecks")
.only(
class AgentsTableList(APIView):
def patch(self, request):
if "sitePK" in request.data.keys():
queryset = (
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site_id=request.data["sitePK"])
)
elif "clientPK" in request.data.keys():
queryset = (
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site__client_id=request.data["clientPK"])
)
else:
queryset = Agent.objects.select_related(
"site", "policy", "alert_template"
).prefetch_related("agentchecks")
queryset = queryset.only(
"pk",
"hostname",
"agent_id",
"site",
"policy",
"alert_template",
"monitoring_type",
"description",
"needs_reboot",
@@ -247,11 +252,6 @@ class AgentsTableList(generics.ListAPIView):
"time_zone",
"maintenance_mode",
)
)
serializer_class = AgentTableSerializer
def list(self, request):
queryset = self.get_queryset()
ctx = {"default_tz": get_default_timezone()}
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
return Response(serializer.data)
@@ -269,66 +269,6 @@ def agent_edit_details(request, pk):
return Response(AgentEditSerializer(agent).data)
@api_view()
def by_client(request, clientpk):
agents = (
Agent.objects.select_related("site")
.filter(site__client_id=clientpk)
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"site",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
)
)
ctx = {"default_tz": get_default_timezone()}
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
@api_view()
def by_site(request, sitepk):
agents = (
Agent.objects.filter(site_id=sitepk)
.select_related("site")
.prefetch_related("agentchecks")
.only(
"pk",
"hostname",
"agent_id",
"site",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
)
)
ctx = {"default_tz": get_default_timezone()}
return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
@api_view(["POST"])
def overdue_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
@@ -344,9 +284,6 @@ class Reboot(APIView):
# reboot now
def post(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")
@@ -356,8 +293,6 @@ class Reboot(APIView):
# reboot later
def patch(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
try:
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
@@ -372,6 +307,7 @@ class Reboot(APIView):
"func": "schedtask",
"schedtaskpayload": {
"type": "schedreboot",
"deleteafter": True,
"trigger": "once",
"name": task_name,
"year": int(dt.datetime.strftime(obj, "%Y")),
@@ -382,9 +318,6 @@ class Reboot(APIView):
},
}
if pyver.parse(agent.version) >= pyver.parse("1.1.2"):
nats_data["schedtaskpayload"]["deleteafter"] = True
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
return notify_error(r)
@@ -537,20 +470,12 @@ def recover(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
mode = request.data["mode"]
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
return notify_error("Only available in agent version greater than 0.9.5")
if not agent.has_nats:
if mode == "tacagent" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater")
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats:
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
# attempt a realtime recovery, otherwise fall back to old recovery method
if mode == "tacagent" or mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
if agent.recoveryactions.filter(last_run=None).exists(): # type: ignore
return notify_error(
@@ -617,9 +542,6 @@ def run_script(request):
@api_view()
def recover_mesh(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {"func": "recover", "payload": {"mode": "mesh"}}
r = asyncio.run(agent.nats_cmd(data, timeout=45))
if r != "ok":
@@ -799,9 +721,6 @@ def agent_maintenance(request):
class WMI(APIView):
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
if pyver.parse(agent.version) < pyver.parse("1.1.2"):
return notify_error("Requires agent version 1.1.2 or greater")
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
if r != "ok":
return notify_error("Unable to contact the agent")

View File

@@ -1,7 +1,20 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Union
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import BooleanField, PositiveIntegerField
from django.utils import timezone as djangotime
from loguru import logger
if TYPE_CHECKING:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
logger.configure(**settings.LOG_CONFIG)
SEVERITY_CHOICES = [
("info", "Informational"),
@@ -78,7 +91,7 @@ class Alert(models.Model):
self.save()
@classmethod
def create_availability_alert(cls, agent):
def create_or_return_availability_alert(cls, agent):
if not cls.objects.filter(agent=agent, resolved=False).exists():
return cls.objects.create(
agent=agent,
@@ -87,9 +100,11 @@ class Alert(models.Model):
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
hidden=True,
)
else:
return cls.objects.get(agent=agent, resolved=False)
@classmethod
def create_check_alert(cls, check):
def create_or_return_check_alert(cls, check):
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
return cls.objects.create(
@@ -99,9 +114,11 @@ class Alert(models.Model):
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True,
)
else:
return cls.objects.get(assigned_check=check, resolved=False)
@classmethod
def create_task_alert(cls, task):
def create_or_return_task_alert(cls, task):
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
return cls.objects.create(
@@ -111,6 +128,305 @@ class Alert(models.Model):
message=f"{task.agent.hostname} has task: {task.name} that failed.",
hidden=True,
)
else:
return cls.objects.get(assigned_task=task, resolved=False)
@classmethod
def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
# set variables
dashboard_severities = None
email_severities = None
text_severities = None
always_dashboard = None
always_email = None
always_text = None
alert_interval = None
email_task = None
text_task = None
# check what the instance passed is
if isinstance(instance, Agent):
from agents.tasks import agent_outage_email_task, agent_outage_sms_task
email_task = agent_outage_email_task
text_task = agent_outage_sms_task
email_alert = instance.overdue_email_alert
text_alert = instance.overdue_text_alert
dashboard_alert = instance.overdue_dashboard_alert
alert_template = instance.alert_template
maintenance_mode = instance.maintenance_mode
alert_severity = "error"
agent = instance
# set alert_template settings
if alert_template:
dashboard_severities = ["error"]
email_severities = ["error"]
text_severities = ["error"]
always_dashboard = alert_template.agent_always_alert
always_email = alert_template.agent_always_email
always_text = alert_template.agent_always_text
alert_interval = alert_template.agent_periodic_alert_days
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_availability_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(agent=instance, resolved=False).exists():
alert = cls.objects.get(agent=instance, resolved=False)
else:
alert = None
elif isinstance(instance, Check):
from checks.tasks import (
handle_check_email_alert_task,
handle_check_sms_alert_task,
)
email_task = handle_check_email_alert_task
text_task = handle_check_sms_alert_task
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
# set alert_template settings
if alert_template:
dashboard_severities = alert_template.check_dashboard_alert_severity
email_severities = alert_template.check_email_alert_severity
text_severities = alert_template.check_text_alert_severity
always_dashboard = alert_template.check_always_alert
always_email = alert_template.check_always_email
always_text = alert_template.check_always_text
alert_interval = alert_template.check_periodic_alert_days
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_check_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(assigned_check=instance, resolved=False).exists():
alert = cls.objects.get(assigned_check=instance, resolved=False)
else:
alert = None
elif isinstance(instance, AutomatedTask):
from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
email_task = handle_task_email_alert
text_task = handle_task_sms_alert
email_alert = instance.email_alert
text_alert = instance.text_alert
dashboard_alert = instance.dashboard_alert
alert_template = instance.agent.alert_template
maintenance_mode = instance.agent.maintenance_mode
alert_severity = instance.alert_severity
agent = instance.agent
# set alert_template settings
if alert_template:
dashboard_severities = alert_template.task_dashboard_alert_severity
email_severities = alert_template.task_email_alert_severity
text_severities = alert_template.task_text_alert_severity
always_dashboard = alert_template.task_always_alert
always_email = alert_template.task_always_email
always_text = alert_template.task_always_text
alert_interval = alert_template.task_periodic_alert_days
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_task_alert(instance)
else:
# check if there is an alert that exists
if cls.objects.filter(assigned_task=instance, resolved=False).exists():
alert = cls.objects.get(assigned_task=instance, resolved=False)
else:
alert = None
else:
return
# return if agent is in maintenance mode
if maintenance_mode or not alert:
return
# check if alert severity changed on check and update the alert
if alert_severity != alert.severity:
alert.severity = alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if dashboard_alert or always_dashboard:
# check if alert template is set and specific severities are configured
if alert_template and alert.severity not in dashboard_severities: # type: ignore
pass
else:
alert.hidden = False
alert.save()
# send email if enabled
if email_alert or always_email:
# check if alert template is set and specific severities are configured
if alert_template and alert.severity not in email_severities: # type: ignore
pass
else:
email_task.delay(
pk=alert.pk,
alert_interval=alert_interval,
)
# send text if enabled
if text_alert or always_text:
# check if alert template is set and specific severities are configured
if alert_template and alert.severity not in text_severities: # type: ignore
pass
else:
text_task.delay(pk=alert.pk, alert_interval=alert_interval)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
)
@classmethod
def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
# set variables
email_on_resolved = False
text_on_resolved = False
resolved_email_task = None
resolved_text_task = None
# check what the instance passed is
if isinstance(instance, Agent):
from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
resolved_email_task = agent_recovery_email_task
resolved_text_task = agent_recovery_sms_task
alert_template = instance.alert_template
alert = cls.objects.get(agent=instance, resolved=False)
maintenance_mode = instance.maintenance_mode
agent = instance
if alert_template:
email_on_resolved = alert_template.agent_email_on_resolved
text_on_resolved = alert_template.agent_text_on_resolved
elif isinstance(instance, Check):
from checks.tasks import (
handle_resolved_check_email_alert_task,
handle_resolved_check_sms_alert_task,
)
resolved_email_task = handle_resolved_check_email_alert_task
resolved_text_task = handle_resolved_check_sms_alert_task
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_check=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
if alert_template:
email_on_resolved = alert_template.check_email_on_resolved
text_on_resolved = alert_template.check_text_on_resolved
elif isinstance(instance, AutomatedTask):
from autotasks.tasks import (
handle_resolved_task_email_alert,
handle_resolved_task_sms_alert,
)
resolved_email_task = handle_resolved_task_email_alert
resolved_text_task = handle_resolved_task_sms_alert
alert_template = instance.agent.alert_template
alert = cls.objects.get(assigned_task=instance, resolved=False)
maintenance_mode = instance.agent.maintenance_mode
agent = instance.agent
if alert_template:
email_on_resolved = alert_template.task_email_on_resolved
text_on_resolved = alert_template.task_text_on_resolved
else:
return
# return if agent is in maintenance mode
if maintenance_mode:
return
alert.resolve()
# check if a resolved email notification should be send
if email_on_resolved and not alert.resolved_email_sent:
resolved_email_task.delay(pk=alert.pk)
# check if resolved text should be sent
if text_on_resolved and not alert.resolved_sms_sent:
resolved_text_task.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
)
class AlertTemplate(models.Model):

View File

@@ -12,3 +12,13 @@ def unsnooze_alerts() -> str:
)
return "ok"
@app.task
def cache_agents_alert_template():
from agents.models import Agent
for agent in Agent.objects.only("pk"):
agent.set_alert_template()
return "ok"

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ from .serializers import (
AlertTemplateRelationSerializer,
AlertTemplateSerializer,
)
from .tasks import cache_agents_alert_template
class GetAddAlerts(APIView):
@@ -194,6 +195,9 @@ class GetAddAlertTemplates(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")
@@ -212,11 +216,17 @@ class GetUpdateDeleteAlertTemplate(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(AlertTemplate, pk=pk).delete()
# cache alert_template value on agents
cache_agents_alert_template.delay()
return Response("ok")

View File

@@ -1,9 +1,9 @@
import json
import os
from itertools import cycle
from unittest.mock import patch
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
@@ -18,8 +18,44 @@ class TestAPIv3(TacticalTestCase):
def test_get_checks(self):
url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
# add a check
check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], self.agent.check_interval) # type: ignore
self.assertEqual(len(r.data["checks"]), 1) # type: ignore
# override check run interval
check2 = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=20
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEqual(len(r.data["checks"]), 2) # type: ignore
# Set last_run on both checks and should return an empty list
check1.last_run = djangotime.now()
check1.save()
check2.last_run = djangotime.now()
check2.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertFalse(r.data["checks"]) # type: ignore
# set last_run greater than interval
check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check1.save()
check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
check2.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["check_interval"], 20) # type: ignore
self.assertEquals(len(r.data["checks"]), 2) # type: ignore
url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
r = self.client.get(url)
@@ -53,3 +89,117 @@ class TestAPIv3(TacticalTestCase):
r.json(),
{"agent": self.agent.pk, "check_interval": self.agent.check_interval},
)
# add check to agent with check interval set
check = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=30
)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": 30},
)
# minimum check run interval is 15 seconds
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(),
{"agent": self.agent.pk, "check_interval": 15},
)
def test_run_checks(self):
# force run all checks regardless of interval
agent = baker.make_recipe("agents.online_agent")
baker.make_recipe("checks.ping_check", agent=agent)
baker.make_recipe("checks.diskspace_check", agent=agent)
baker.make_recipe("checks.cpuload_check", agent=agent)
baker.make_recipe("checks.memory_check", agent=agent)
baker.make_recipe("checks.eventlog_check", agent=agent)
for _ in range(10):
baker.make_recipe("checks.script_check", agent=agent)
url = f"/api/v3/{agent.agent_id}/runchecks/"
r = self.client.get(url)
self.assertEqual(r.json()["agent"], agent.pk)
self.assertIsInstance(r.json()["check_interval"], int)
self.assertEqual(len(r.json()["checks"]), 15)
def test_checkin_patch(self):
from logs.models import PendingAction
url = "/api/v3/checkin/"
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
PendingAction.objects.create(
agent=agent_updated,
action_type="agentupdate",
details={
"url": agent_updated.winagent_dl,
"version": agent_updated.version,
"inno": agent_updated.win_inno_exe,
},
)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent failed to update and still on same version
payload = {
"func": "hello",
"agent_id": agent_updated.agent_id,
"version": "1.3.0",
}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent successful update
payload["version"] = settings.LATEST_AGENT_VER
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "completed")
action.delete()
@patch("apiv3.views.reload_nats")
def test_agent_recovery(self, reload_nats):
reload_nats.return_value = "ok"
r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
self.assertEqual(r.status_code, 404)
agent = baker.make_recipe("agents.online_agent")
url = f"/api/v3/{agent.agent_id}/recovery/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
reload_nats.assert_not_called()
baker.make(
"agents.RecoveryAction",
agent=agent,
mode="command",
command="shutdown /r /t 5 /f",
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
)
reload_nats.assert_not_called()
baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
reload_nats.assert_called_once()

View File

@@ -5,6 +5,7 @@ from . import views
urlpatterns = [
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/runchecks/", views.RunChecks.as_view()),
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("meshexe/", views.MeshExe.as_view()),
@@ -17,4 +18,6 @@ urlpatterns = [
path("choco/", views.Choco.as_view()),
path("winupdates/", views.WinUpdates.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
]

View File

@@ -22,6 +22,7 @@ from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from checks.models import Check
from checks.serializers import CheckRunnerGetSerializer
from checks.utils import bytes2human
from logs.models import PendingAction
from software.models import InstalledSoftware
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
from winupdate.models import WinUpdate, WinUpdatePolicy
@@ -35,6 +36,8 @@ class CheckIn(APIView):
permission_classes = [IsAuthenticated]
def patch(self, request):
from alerts.models import Alert
updated = False
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if pyver.parse(request.data["version"]) > pyver.parse(
@@ -59,14 +62,8 @@ class CheckIn(APIView):
).update(status="completed")
# handles any alerting actions
agent.handle_alert(checkin=True)
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
handle_agent_recovery_task.delay(pk=recovery.pk) # type: ignore
return Response("ok")
if Alert.objects.filter(agent=agent, resolved=False).exists():
Alert.handle_alert_resolve(agent)
# get any pending actions
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
@@ -263,18 +260,13 @@ class SupersededWinUpdate(APIView):
return Response("ok")
class CheckRunner(APIView):
"""
For the windows golang agent
"""
class RunChecks(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
@@ -282,6 +274,42 @@ class CheckRunner(APIView):
}
return Response(ret)
class CheckRunner(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
checks = agent.agentchecks.filter(overriden_by_policy=False) # type: ignore
run_list = [
check
for check in checks
# always run if check hasn't run yet
if not check.last_run
# if a check interval is set, see if the correct amount of seconds have passed
or (
check.run_interval
and (
check.last_run
< djangotime.now()
- djangotime.timedelta(seconds=check.run_interval)
)
# if check interval isn't set, make sure the agent's check interval has passed before running
)
or (
check.last_run
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
)
]
ret = {
"agent": agent.pk,
"check_interval": agent.check_run_interval(),
"checks": CheckRunnerGetSerializer(run_list, many=True).data,
}
return Response(ret)
def patch(self, request):
check = get_object_or_404(Check, pk=request.data["id"])
check.last_run = djangotime.now()
@@ -297,14 +325,13 @@ class CheckRunnerInterval(APIView):
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
return Response(
{"agent": agent.pk, "check_interval": agent.check_run_interval()}
)
class TaskRunner(APIView):
"""
For the windows golang agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
@@ -314,6 +341,7 @@ class TaskRunner(APIView):
return Response(TaskGOGetSerializer(task).data)
def patch(self, request, pk, agentid):
from alerts.models import Alert
from logs.models import AuditLog
agent = get_object_or_404(Agent, agent_id=agentid)
@@ -325,8 +353,17 @@ class TaskRunner(APIView):
serializer.is_valid(raise_exception=True)
serializer.save(last_run=djangotime.now())
new_task = AutomatedTask.objects.get(pk=task.pk)
new_task.handle_alert()
status = "failing" if task.retcode != 0 else "passing"
new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
new_task.status = status
new_task.save()
if status == "passing":
if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
Alert.handle_alert_resolve(new_task)
else:
Alert.handle_alert_failure(new_task)
AuditLog.objects.create(
username=agent.hostname,
@@ -475,3 +512,59 @@ class Installer(APIView):
)
return Response("ok")
class ChocoResult(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request, pk):
action = get_object_or_404(PendingAction, pk=pk)
results: str = request.data["results"]
software_name = action.details["name"].lower()
success = [
"install",
"of",
software_name,
"was",
"successful",
"installed",
]
duplicate = [software_name, "already", "installed", "--force", "reinstall"]
installed = False
if all(x in results.lower() for x in success):
installed = True
elif all(x in results.lower() for x in duplicate):
installed = True
action.details["output"] = results
action.details["installed"] = installed
action.status = "completed"
action.save(update_fields=["details", "status"])
return Response("ok")
class AgentRecovery(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
ret = {"mode": "pass", "shellcmd": ""}
if recovery is None:
return Response(ret)
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
ret["mode"] = recovery.mode
if recovery.mode == "command":
ret["shellcmd"] = recovery.command
elif recovery.mode == "rpc":
reload_nats()
return Response(ret)

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.7 on 2021-03-02 04:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0030_agent_offline_time'),
('clients', '0009_auto_20210212_1408'),
('automation', '0007_policy_alert_template'),
]
operations = [
migrations.AddField(
model_name='policy',
name='excluded_agents',
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='agents.Agent'),
),
migrations.AddField(
model_name='policy',
name='excluded_clients',
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Client'),
),
migrations.AddField(
model_name='policy',
name='excluded_sites',
field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Site'),
),
]

View File

@@ -17,8 +17,18 @@ class Policy(BaseAuditModel):
null=True,
blank=True,
)
excluded_sites = models.ManyToManyField(
"clients.Site", related_name="policy_exclusions", blank=True
)
excluded_clients = models.ManyToManyField(
"clients.Client", related_name="policy_exclusions", blank=True
)
excluded_agents = models.ManyToManyField(
"agents.Agent", related_name="policy_exclusions", blank=True
)
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_from_policies_task
# get old policy if exists
@@ -33,6 +43,9 @@ class Policy(BaseAuditModel):
create_tasks=True,
)
if old_policy.alert_template != self.alert_template:
cache_agents_alert_template.delay()
def delete(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
@@ -52,19 +65,41 @@ class Policy(BaseAuditModel):
def __str__(self):
return self.name
def is_agent_excluded(self, agent):
return (
agent in self.excluded_agents.all()
or agent.site in self.excluded_sites.all()
or agent.client in self.excluded_clients.all()
)
def related_agents(self):
return self.get_related("server") | self.get_related("workstation")
def get_related(self, mon_type):
explicit_agents = self.agents.filter(monitoring_type=mon_type) # type: ignore
explicit_clients = getattr(self, f"{mon_type}_clients").all()
explicit_sites = getattr(self, f"{mon_type}_sites").all()
explicit_agents = (
self.agents.filter(monitoring_type=mon_type) # type: ignore
.exclude(
pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
)
.exclude(site__in=self.excluded_sites.all())
.exclude(site__client__in=self.excluded_clients.all())
)
explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
pk__in=self.excluded_clients.all()
)
explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
pk__in=self.excluded_sites.all()
)
filtered_agents_pks = Policy.objects.none()
filtered_agents_pks |= Agent.objects.filter(
site__in=[
site for site in explicit_sites if site.client not in explicit_clients
site
for site in explicit_sites
if site.client not in explicit_clients
and site.client not in self.excluded_clients.all()
],
monitoring_type=mon_type,
).values_list("pk", flat=True)
@@ -119,23 +154,39 @@ class Policy(BaseAuditModel):
client_policy = client.workstation_policy
site_policy = site.workstation_policy
if agent_policy and agent_policy.active:
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
for task in agent_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if site_policy and site_policy.active:
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
for task in site_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if client_policy and client_policy.active:
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
for task in client_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
added_task_pks.append(task.pk)
if default_policy and default_policy.active:
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
for task in default_policy.autotasks.all():
if task.pk not in added_task_pks:
tasks.append(task)
@@ -205,7 +256,11 @@ class Policy(BaseAuditModel):
enforced_checks = list()
policy_checks = list()
if agent_policy and agent_policy.active:
if (
agent_policy
and agent_policy.active
and not agent_policy.is_agent_excluded(agent)
):
if agent_policy.enforced:
for check in agent_policy.policychecks.all():
enforced_checks.append(check)
@@ -213,7 +268,11 @@ class Policy(BaseAuditModel):
for check in agent_policy.policychecks.all():
policy_checks.append(check)
if site_policy and site_policy.active:
if (
site_policy
and site_policy.active
and not site_policy.is_agent_excluded(agent)
):
if site_policy.enforced:
for check in site_policy.policychecks.all():
enforced_checks.append(check)
@@ -221,7 +280,11 @@ class Policy(BaseAuditModel):
for check in site_policy.policychecks.all():
policy_checks.append(check)
if client_policy and client_policy.active:
if (
client_policy
and client_policy.active
and not client_policy.is_agent_excluded(agent)
):
if client_policy.enforced:
for check in client_policy.policychecks.all():
enforced_checks.append(check)
@@ -229,7 +292,11 @@ class Policy(BaseAuditModel):
for check in client_policy.policychecks.all():
policy_checks.append(check)
if default_policy and default_policy.active:
if (
default_policy
and default_policy.active
and not default_policy.is_agent_excluded(agent)
):
if default_policy.enforced:
for check in default_policy.policychecks.all():
enforced_checks.append(check)

View File

@@ -4,9 +4,11 @@ from rest_framework.serializers import (
SerializerMethodField,
)
from agents.serializers import AgentHostnameSerializer
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
@@ -25,6 +27,9 @@ class PolicyTableSerializer(ModelSerializer):
agents_count = SerializerMethodField(read_only=True)
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
alert_template = ReadOnlyField(source="alert_template.id")
excluded_clients = ClientSerializer(many=True)
excluded_sites = SiteSerializer(many=True)
excluded_agents = AgentHostnameSerializer(many=True)
class Meta:
model = Policy

View File

@@ -79,6 +79,7 @@ def update_policy_check_fields_task(checkpk):
error_threshold=check.error_threshold,
alert_severity=check.alert_severity,
name=check.name,
run_interval=check.run_interval,
disk=check.disk,
fails_b4_alert=check.fails_b4_alert,
ip=check.ip,
@@ -98,6 +99,7 @@ def update_policy_check_fields_task(checkpk):
event_message=check.event_message,
fail_when=check.fail_when,
search_last_days=check.search_last_days,
number_of_events_b4_alert=check.number_of_events_b4_alert,
email_alert=check.email_alert,
text_alert=check.text_alert,
dashboard_alert=check.dashboard_alert,

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
from model_bakery import baker, seq
from agents.models import Agent
from core.models import CoreSettings
from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
@@ -31,7 +32,7 @@ class TestPolicyViews(TacticalTestCase):
serializer = PolicyTableSerializer(policies, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@@ -41,13 +42,13 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
policy = baker.make("automation.Policy")
url = f"/automation/policies/{policy.pk}/"
url = f"/automation/policies/{policy.pk}/" # type: ignore
resp = self.client.get(url, format="json")
serializer = PolicySerializer(policy)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@@ -79,13 +80,13 @@ class TestPolicyViews(TacticalTestCase):
"desc": "policy desc",
"active": True,
"enforced": False,
"copyId": policy.pk,
"copyId": policy.pk, # type: ignore
}
resp = self.client.post(f"/automation/policies/", data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(policy.autotasks.count(), 3)
self.assertEqual(policy.policychecks.count(), 7)
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
self.check_not_authenticated("post", url)
@@ -96,7 +97,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
policy = baker.make("automation.Policy", active=True, enforced=False)
url = f"/automation/policies/{policy.pk}/"
url = f"/automation/policies/{policy.pk}/" # type: ignore
data = {
"name": "Test Policy Update",
@@ -121,7 +122,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_checks_from_policies_task.assert_called_with(
policypk=policy.pk, create_tasks=True
policypk=policy.pk, create_tasks=True # type: ignore
)
self.check_not_authenticated("put", url)
@@ -138,7 +139,7 @@ class TestPolicyViews(TacticalTestCase):
agents = baker.make_recipe(
"agents.agent", site=site, policy=policy, _quantity=3
)
url = f"/automation/policies/{policy.pk}/"
url = f"/automation/policies/{policy.pk}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -153,14 +154,14 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks
policy = baker.make("automation.Policy")
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
url = f"/automation/{policy.pk}/policyautomatedtasks/"
url = f"/automation/{policy.pk}/policyautomatedtasks/" # type: ignore
resp = self.client.get(url, format="json")
serializer = AutoTasksFieldSerializer(tasks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data), 3)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 3) # type: ignore
self.check_not_authenticated("get", url)
@@ -170,14 +171,14 @@ class TestPolicyViews(TacticalTestCase):
policy = baker.make("automation.Policy")
checks = self.create_checks(policy=policy)
url = f"/automation/{policy.pk}/policychecks/"
url = f"/automation/{policy.pk}/policychecks/" # type: ignore
resp = self.client.get(url, format="json")
serializer = PolicyCheckSerializer(checks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data), 7)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 7) # type: ignore
self.check_not_authenticated("get", url)
@@ -199,7 +200,7 @@ class TestPolicyViews(TacticalTestCase):
serializer = PolicyCheckStatusSerializer([managed_check], many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("patch", url)
def test_policy_overview(self):
@@ -212,40 +213,40 @@ class TestPolicyViews(TacticalTestCase):
)
clients = baker.make(
"clients.Client",
server_policy=cycle(policies),
workstation_policy=cycle(policies),
server_policy=cycle(policies), # type: ignore
workstation_policy=cycle(policies), # type: ignore
_quantity=5,
)
baker.make(
"clients.Site",
client=cycle(clients),
server_policy=cycle(policies),
workstation_policy=cycle(policies),
client=cycle(clients), # type: ignore
server_policy=cycle(policies), # type: ignore
workstation_policy=cycle(policies), # type: ignore
_quantity=4,
)
baker.make("clients.Site", client=cycle(clients), _quantity=3)
baker.make("clients.Site", client=cycle(clients), _quantity=3) # type: ignore
resp = self.client.get(url, format="json")
clients = Client.objects.all()
serializer = PolicyOverviewSerializer(clients, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_get_related(self):
policy = baker.make("automation.Policy")
url = f"/automation/policies/{policy.pk}/related/"
url = f"/automation/policies/{policy.pk}/related/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertIsInstance(resp.data["server_clients"], list)
self.assertIsInstance(resp.data["workstation_clients"], list)
self.assertIsInstance(resp.data["server_sites"], list)
self.assertIsInstance(resp.data["workstation_sites"], list)
self.assertIsInstance(resp.data["agents"], list)
self.assertIsInstance(resp.data["server_clients"], list) # type: ignore
self.assertIsInstance(resp.data["workstation_clients"], list) # type: ignore
self.assertIsInstance(resp.data["server_sites"], list) # type: ignore
self.assertIsInstance(resp.data["workstation_sites"], list) # type: ignore
self.assertIsInstance(resp.data["agents"], list) # type: ignore
self.check_not_authenticated("get", url)
@@ -257,16 +258,16 @@ class TestPolicyViews(TacticalTestCase):
# create policy managed tasks
policy_tasks = baker.make(
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore
)
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/" # type: ignore
serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
resp = self.client.patch(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data), 5)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 5) # type: ignore
self.check_not_authenticated("patch", url)
@@ -284,7 +285,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_task.assert_called_once_with([task.pk for task in tasks])
mock_task.assert_called_once_with([task.pk for task in tasks]) # type: ignore
self.check_not_authenticated("put", url)
@@ -299,7 +300,7 @@ class TestPolicyViews(TacticalTestCase):
policy = baker.make("automation.Policy")
data = {
"policy": policy.pk,
"policy": policy.pk, # type: ignore
"critical": "approve",
"important": "approve",
"moderate": "ignore",
@@ -325,11 +326,11 @@ class TestPolicyViews(TacticalTestCase):
policy = baker.make("automation.Policy")
patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
url = f"/automation/winupdatepolicy/{patch_policy.pk}/"
url = f"/automation/winupdatepolicy/{patch_policy.pk}/" # type: ignore
data = {
"id": patch_policy.pk,
"policy": policy.pk,
"id": patch_policy.pk, # type: ignore
"policy": policy.pk, # type: ignore
"critical": "approve",
"important": "approve",
"moderate": "ignore",
@@ -358,10 +359,10 @@ class TestPolicyViews(TacticalTestCase):
}
clients = baker.make("clients.Client", _quantity=6)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10) # type: ignore
agents = baker.make_recipe(
"agents.agent",
site=cycle(sites),
site=cycle(sites), # type: ignore
_quantity=6,
)
@@ -371,24 +372,24 @@ class TestPolicyViews(TacticalTestCase):
)
# test reset agents in site
data = {"site": sites[0].id}
data = {"site": sites[0].id} # type: ignore
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(site=sites[0])
agents = Agent.objects.filter(site=sites[0]) # type: ignore
for agent in agents:
for k, v in inherit_fields.items():
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
# test reset agents in client
data = {"client": clients[1].id}
data = {"client": clients[1].id} # type: ignore
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(site__client=clients[1])
agents = Agent.objects.filter(site__client=clients[1]) # type: ignore
for agent in agents:
for k, v in inherit_fields.items():
@@ -425,6 +426,25 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_sync_policy(self, generate_checks):
url = "/automation/sync/"
# test invalid data
data = {"invalid": 7}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
policy = baker.make("automation.Policy", active=True)
data = {"policy": policy.pk} # type: ignore
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_checks.assert_called_with(policy.pk, create_tasks=True) # type: ignore
self.check_not_authenticated("post", url)
class TestPolicyTasks(TacticalTestCase):
def setUp(self):
@@ -435,46 +455,46 @@ class TestPolicyTasks(TacticalTestCase):
# Get Site and Client from an agent in list
clients = baker.make("clients.Client", _quantity=5)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=25) # type: ignore
server_agents = baker.make_recipe(
"agents.server_agent",
site=cycle(sites),
site=cycle(sites), # type: ignore
_quantity=25,
)
workstation_agents = baker.make_recipe(
"agents.workstation_agent",
site=cycle(sites),
site=cycle(sites), # type: ignore
_quantity=25,
)
policy = baker.make("automation.Policy", active=True)
# Add Client to Policy
policy.server_clients.add(server_agents[13].client)
policy.workstation_clients.add(workstation_agents[15].client)
policy.server_clients.add(server_agents[13].client) # type: ignore
policy.workstation_clients.add(workstation_agents[15].client) # type: ignore
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json"
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
)
self.assertEqual(resp.status_code, 200)
self.assertEquals(len(resp.data["server_clients"]), 1)
self.assertEquals(len(resp.data["server_sites"]), 5)
self.assertEquals(len(resp.data["workstation_clients"]), 1)
self.assertEquals(len(resp.data["workstation_sites"]), 5)
self.assertEquals(len(resp.data["agents"]), 10)
self.assertEquals(len(resp.data["server_clients"]), 1) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
# Add Site to Policy and the agents and sites length shouldn't change
policy.server_sites.add(server_agents[13].site)
policy.workstation_sites.add(workstation_agents[15].site)
self.assertEquals(len(resp.data["server_sites"]), 5)
self.assertEquals(len(resp.data["workstation_sites"]), 5)
self.assertEquals(len(resp.data["agents"]), 10)
policy.server_sites.add(server_agents[13].site) # type: ignore
policy.workstation_sites.add(workstation_agents[15].site) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
# Add Agent to Policy and the agents length shouldn't change
policy.agents.add(server_agents[13])
policy.agents.add(workstation_agents[15])
self.assertEquals(len(resp.data["agents"]), 10)
policy.agents.add(server_agents[13]) # type: ignore
policy.agents.add(workstation_agents[15]) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
def test_generating_agent_policy_checks(self):
from .tasks import generate_agent_checks_from_policies_task
@@ -485,7 +505,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", policy=policy)
# test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id)
generate_agent_checks_from_policies_task(policy.id) # type: ignore
# make sure all checks were created. should be 7
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
@@ -535,7 +555,7 @@ class TestPolicyTasks(TacticalTestCase):
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
self.create_checks(agent=agent, script=script)
generate_agent_checks_from_policies_task(policy.id, create_tasks=True)
generate_agent_checks_from_policies_task(policy.id, create_tasks=True) # type: ignore
# make sure each agent check says overriden_by_policy
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
@@ -843,7 +863,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
# pick a policy check and delete it from the agent
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id # type: ignore
delete_policy_check_task(policy_check_id)
@@ -868,7 +888,7 @@ class TestPolicyTasks(TacticalTestCase):
# pick a policy check and update it with new values
ping_check = (
Policy.objects.get(pk=policy.id)
Policy.objects.get(pk=policy.id) # type: ignore
.policychecks.filter(check_type="ping")
.first()
)
@@ -895,7 +915,7 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_tasks_from_policies_task(policy.id)
generate_agent_tasks_from_policies_task(policy.id) # type: ignore
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
@@ -905,14 +925,14 @@ class TestPolicyTasks(TacticalTestCase):
for task in agent_tasks:
self.assertTrue(task.managed_by_policy)
if task.name == "Task1":
self.assertEqual(task.parent_task, tasks[0].id)
self.assertEqual(task.name, tasks[0].name)
self.assertEqual(task.parent_task, tasks[0].id) # type: ignore
self.assertEqual(task.name, tasks[0].name) # type: ignore
if task.name == "Task2":
self.assertEqual(task.parent_task, tasks[1].id)
self.assertEqual(task.name, tasks[1].name)
self.assertEqual(task.parent_task, tasks[1].id) # type: ignore
self.assertEqual(task.name, tasks[1].name) # type: ignore
if task.name == "Task3":
self.assertEqual(task.parent_task, tasks[2].id)
self.assertEqual(task.name, tasks[2].name)
self.assertEqual(task.parent_task, tasks[2].id) # type: ignore
self.assertEqual(task.name, tasks[2].name) # type: ignore
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_delete_policy_tasks(self, delete_win_task_schedule):
@@ -922,10 +942,10 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
agent = baker.make_recipe("agents.server_agent", policy=policy)
delete_policy_autotask_task(tasks[0].id)
delete_policy_autotask_task(tasks[0].id) # type: ignore
delete_win_task_schedule.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id
agent.autotasks.get(parent_task=tasks[0].id).id # type: ignore
)
@patch("autotasks.tasks.run_win_task.delay")
@@ -934,12 +954,12 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
run_win_policy_autotask_task([task.id for task in tasks])
run_win_policy_autotask_task([task.id for task in tasks]) # type: ignore
run_win_task.side_effect = [task.id for task in tasks]
run_win_task.side_effect = [task.id for task in tasks] # type: ignore
self.assertEqual(run_win_task.call_count, 3)
for task in tasks:
run_win_task.assert_any_call(task.id)
for task in tasks: # type: ignore
run_win_task.assert_any_call(task.id) # type: ignore
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
def test_update_policy_tasks(self, enable_or_disable_win_task):
@@ -952,17 +972,17 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
tasks[0].enabled = False
tasks[0].save()
tasks[0].enabled = False # type: ignore
tasks[0].save() # type: ignore
update_policy_task_fields_task(tasks[0].id)
update_policy_task_fields_task(tasks[0].id) # type: ignore
enable_or_disable_win_task.assert_not_called()
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore
update_policy_task_fields_task(tasks[0].id, update_agent=True)
update_policy_task_fields_task(tasks[0].id, update_agent=True) # type: ignore
enable_or_disable_win_task.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id, False
agent.autotasks.get(parent_task=tasks[0].id).id, False # type: ignore
)
@patch("agents.models.Agent.generate_tasks_from_policies")
@@ -984,3 +1004,110 @@ class TestPolicyTasks(TacticalTestCase):
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
self.assertEquals(generate_checks.call_count, 5)
self.assertEquals(generate_checks.call_count, 5)
@patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_policy_exclusions(self, delete_task):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
task = baker.make("autotasks.AutomatedTask", policy=policy)
agent = baker.make_recipe(
"agents.agent", policy=policy, monitoring_type="server"
)
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add agent to policy exclusions
policy.excluded_agents.set([agent]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks
agent.autotasks.all().delete()
policy.excluded_agents.clear() # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add policy exclusions to site
policy.excluded_sites.set([agent.site]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks and reset
agent.autotasks.all().delete()
policy.excluded_sites.clear() # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add policy exclusions to client
policy.excluded_clients.set([agent.client]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
# delete agent tasks and reset
agent.autotasks.all().delete()
policy.excluded_clients.clear() # type: ignore
agent.policy = None
agent.save()
# test on default policy
core = CoreSettings.objects.first()
core.server_policy = policy
core.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# make sure related agents on policy returns correctly
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
self.assertEqual(agent.autotasks.count(), 1) # type: ignore
# add policy exclusions to client
policy.excluded_clients.set([agent.client]) # type: ignore
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
delete_task.assert_called()
delete_task.reset_mock()
def test_removing_duplicate_pending_task_actions(self):
pass
def test_creating_checks_with_assigned_tasks(self):
pass

View File

@@ -7,6 +7,7 @@ urlpatterns = [
path("policies/<int:pk>/related/", views.GetRelated.as_view()),
path("policies/overview/", views.OverviewPolicy.as_view()),
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("sync/", views.PolicySync.as_view()),
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),

View File

@@ -8,6 +8,7 @@ from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from tacticalrmm.utils import notify_error
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
@@ -72,6 +73,20 @@ class GetUpdateDeletePolicy(APIView):
return Response("ok")
class PolicySync(APIView):
def post(self, request):
if "policy" in request.data.keys():
from automation.tasks import generate_agent_checks_from_policies_task
generate_agent_checks_from_policies_task.delay(
request.data["policy"], create_tasks=True
)
return Response("ok")
else:
return notify_error("The request was invalid")
class PolicyAutoTask(APIView):
# tasks associated with policy

View File

@@ -7,7 +7,6 @@ from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import DateTimeField
from django.utils import timezone as djangotime
from loguru import logger
from alerts.models import SEVERITY_CHOICES
@@ -224,165 +223,25 @@ class AutomatedTask(BaseAuditModel):
create_win_task_schedule.delay(task.pk)
def handle_alert(self) -> None:
from alerts.models import Alert
from autotasks.tasks import (
handle_resolved_task_email_alert,
handle_resolved_task_sms_alert,
handle_task_email_alert,
handle_task_sms_alert,
def should_create_alert(self, alert_template=None):
return (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and (
alert_template.task_always_alert
or alert_template.task_always_email
or alert_template.task_always_text
)
)
)
self.status = "failing" if self.retcode != 0 else "passing"
self.save()
# return if agent is in maintenance mode
if self.agent.maintenance_mode:
return
# see if agent has an alert template and use that
alert_template = self.agent.get_alert_template()
# resolve alert if it exists
if self.status == "passing":
if Alert.objects.filter(assigned_task=self, resolved=False).exists():
alert = Alert.objects.get(assigned_task=self, resolved=False)
alert.resolve()
# check if resolved email should be send
if (
alert_template and alert_template.task_email_on_resolved
) and not alert.resolved_email_sent:
handle_resolved_task_email_alert.delay(pk=alert.pk)
# check if resolved text should be sent
if (
alert_template and alert_template.task_text_on_resolved
) and not alert.resolved_sms_sent:
handle_resolved_task_sms_alert.delay(pk=alert.pk)
# check if resolved script should be run
if not alert.resolved_action_run and (
alert_template and alert_template.resolved_action
):
r = self.agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for task: {self.name}"
)
# create alert if task is failing
else:
if not Alert.objects.filter(assigned_task=self, resolved=False).exists():
# check if alert should be created and if not return
if (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and (
alert_template.task_always_alert
or alert_template.task_always_email
or alert_template.task_always_text
)
)
):
alert = Alert.create_task_alert(self)
else:
return
else:
alert = Alert.objects.get(assigned_task=self, resolved=False)
# check if alert severity changed on task and update the alert
if self.alert_severity != alert.severity:
alert.severity = self.alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if self.dashboard_alert or (
alert_template
and self.alert_severity in alert_template.task_dashboard_alert_severity
and alert_template.task_always_alert
):
alert.hidden = False
alert.save()
# send email if enabled
if self.email_alert or (
alert_template
and self.alert_severity in alert_template.task_email_alert_severity
and alert_template.task_always_email
):
handle_task_email_alert.delay(
pk=alert.pk,
alert_template=alert_template.task_periodic_alert_days
if alert_template
else None,
)
# send text if enabled
if self.text_alert or (
alert_template
and self.alert_severity in alert_template.task_text_alert_severity
and alert_template.task_always_text
):
handle_task_sms_alert.delay(
pk=alert.pk,
alert_template=alert_template.task_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = self.agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for task: {self.name}"
)
def send_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -394,14 +253,13 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template)
CORE.send_mail(subject, body, self.agent.alert_template)
def send_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -413,13 +271,11 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)
def send_resolved_email(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
@@ -427,16 +283,15 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_resolved_sms(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)

View File

@@ -18,7 +18,7 @@ class TaskSerializer(serializers.ModelSerializer):
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
alert_template = obj.agent.alert_template
else:
alert_template = None

View File

@@ -29,7 +29,6 @@ class TestAutotaskViews(TacticalTestCase):
agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
old_agent = baker.make_recipe("agents.agent", version="1.1.0")
# test script set to invalid pk
data = {"autotask": {"script": 500}}
@@ -52,15 +51,6 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 404)
# test old agent version
data = {
"autotask": {"script": script.id},
"agent": old_agent.id,
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test add task to agent
data = {
"autotask": {
@@ -203,13 +193,6 @@ class TestAutotaskViews(TacticalTestCase):
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
nats_cmd.reset_mock()
old_agent = baker.make_recipe("agents.agent", version="1.0.2")
task2 = baker.make("autotasks.AutomatedTask", agent=old_agent)
url = f"/tasks/runwintask/{task2.id}/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
nats_cmd.assert_not_called()
self.check_not_authenticated("get", url)

View File

@@ -34,9 +34,6 @@ class AddAutoTask(APIView):
parent = {"policy": policy}
else:
agent = get_object_or_404(Agent, pk=data["agent"])
if not agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
parent = {"agent": agent}
check = None
@@ -128,8 +125,5 @@ class AutoTask(APIView):
@api_view()
def run_task(request, pk):
task = get_object_or_404(AutomatedTask, pk=pk)
if not task.agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
return Response(f"{task.name} will now be run on {task.agent.hostname}")

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-06 02:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0021_auto_20210212_1429'),
]
operations = [
migrations.AddField(
model_name='check',
name='number_of_events_b4_alert',
field=models.PositiveIntegerField(blank=True, default=1, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-06 02:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0022_check_number_of_events_b4_alert'),
]
operations = [
migrations.AddField(
model_name='check',
name='run_interval',
field=models.PositiveIntegerField(blank=True, default=0),
),
]

View File

@@ -3,26 +3,19 @@ import json
import os
import string
from statistics import mean
from typing import Any, Union
from typing import Any
import pytz
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils import timezone as djangotime
from loguru import logger
from alerts.models import SEVERITY_CHOICES
from core.models import CoreSettings
from logs.models import BaseAuditModel
from .tasks import (
handle_check_email_alert_task,
handle_check_sms_alert_task,
handle_resolved_check_email_alert_task,
handle_resolved_check_sms_alert_task,
)
from .utils import bytes2human
logger.configure(**settings.LOG_CONFIG)
@@ -100,6 +93,7 @@ class Check(BaseAuditModel):
fail_count = models.PositiveIntegerField(default=0)
outage_history = models.JSONField(null=True, blank=True) # store
extra_details = models.JSONField(null=True, blank=True)
run_interval = models.PositiveIntegerField(blank=True, default=0)
# check specific fields
# for eventlog, script, ip, and service alert severity
@@ -188,6 +182,9 @@ class Check(BaseAuditModel):
max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
)
search_last_days = models.PositiveIntegerField(null=True, blank=True)
number_of_events_b4_alert = models.PositiveIntegerField(
null=True, blank=True, default=1
)
def __str__(self):
if self.agent:
@@ -205,7 +202,7 @@ class Check(BaseAuditModel):
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()}: Drive {self.disk} < {text}" # type: ignore
return f"{self.get_check_type_display()}: Drive {self.disk} - {text}" # type: ignore
elif self.check_type == "ping":
return f"{self.get_check_type_display()}: {self.name}" # type: ignore
elif self.check_type == "cpuload" or self.check_type == "memory":
@@ -216,7 +213,7 @@ class Check(BaseAuditModel):
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()} > {text}" # type: ignore
return f"{self.get_check_type_display()} - {text}" # type: ignore
elif self.check_type == "winsvc":
return f"{self.get_check_type_display()}: {self.svc_display_name}" # type: ignore
elif self.check_type == "eventlog":
@@ -266,161 +263,27 @@ class Check(BaseAuditModel):
"modified_time",
]
def handle_alert(self) -> None:
from alerts.models import Alert, AlertTemplate
def should_create_alert(self, alert_template=None):
# return if agent is in maintenance mode
if self.agent.maintenance_mode:
return
# see if agent has an alert template and use that
alert_template: Union[AlertTemplate, None] = self.agent.get_alert_template()
# resolve alert if it exists
if self.status == "passing":
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
alert = Alert.objects.get(assigned_check=self, resolved=False)
alert.resolve()
# check if a resolved email notification should be send
if (
alert_template
and alert_template.check_email_on_resolved
and not alert.resolved_email_sent
):
handle_resolved_check_email_alert_task.delay(pk=alert.pk)
# check if resolved text should be sent
if (
alert_template
and alert_template.check_text_on_resolved
and not alert.resolved_sms_sent
):
handle_resolved_check_sms_alert_task.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = self.agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for {self.check_type} check"
)
elif self.fail_count >= self.fails_b4_alert:
if not Alert.objects.filter(assigned_check=self, resolved=False).exists():
# check if alert should be created and if not return
if (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and (
alert_template.check_always_alert
or alert_template.check_always_email
or alert_template.check_always_text
)
)
):
alert = Alert.create_check_alert(self)
else:
return
else:
alert = Alert.objects.get(assigned_check=self, resolved=False)
# check if alert severity changed on check and update the alert
if self.alert_severity != alert.severity:
alert.severity = self.alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if self.dashboard_alert or (
return (
self.dashboard_alert
or self.email_alert
or self.text_alert
or (
alert_template
and self.alert_severity in alert_template.check_dashboard_alert_severity
and alert_template.check_always_alert
):
alert.hidden = False
alert.save()
# send email if enabled
if (
not alert.email_sent
and self.email_alert
or alert_template
and self.alert_severity in alert_template.check_email_alert_severity
and alert_template.check_always_email
):
handle_check_email_alert_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
and (
alert_template.check_always_alert
or alert_template.check_always_email
or alert_template.check_always_text
)
# send text if enabled
if self.text_alert or (
alert_template
and self.alert_severity in alert_template.check_text_alert_severity
and alert_template.check_always_text
):
handle_check_sms_alert_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = self.agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for {self.check_type} check{r}"
)
)
)
def add_check_history(self, value: int, more_info: Any = None) -> None:
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
def handle_checkv2(self, data):
from alerts.models import Alert
# cpuload or mem checks
if self.check_type == "cpuload" or self.check_type == "memory":
@@ -629,13 +492,13 @@ class Check(BaseAuditModel):
log.append(i)
if self.fail_when == "contains":
if log:
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "failing"
else:
self.status = "passing"
elif self.fail_when == "not_contains":
if log:
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "passing"
else:
self.status = "failing"
@@ -653,11 +516,14 @@ class Check(BaseAuditModel):
self.fail_count += 1
self.save(update_fields=["status", "fail_count", "alert_severity"])
if self.fail_count >= self.fails_b4_alert:
Alert.handle_alert_failure(self)
elif self.status == "passing":
self.fail_count = 0
self.save(update_fields=["status", "fail_count", "alert_severity"])
self.handle_alert()
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
Alert.handle_alert_resolve(self)
return self.status
@@ -701,6 +567,7 @@ class Check(BaseAuditModel):
text_alert=self.text_alert,
fails_b4_alert=self.fails_b4_alert,
extra_details=self.extra_details,
run_interval=self.run_interval,
error_threshold=self.error_threshold,
warning_threshold=self.warning_threshold,
disk=self.disk,
@@ -724,6 +591,7 @@ class Check(BaseAuditModel):
event_message=self.event_message,
fail_when=self.fail_when,
search_last_days=self.search_last_days,
number_of_events_b4_alert=self.number_of_events_b4_alert,
)
def is_duplicate(self, check):
@@ -751,11 +619,10 @@ class Check(BaseAuditModel):
def send_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
else:
subject = f"{self} Failed"
@@ -833,12 +700,11 @@ class Check(BaseAuditModel):
except:
continue
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent:
@@ -882,21 +748,21 @@ class Check(BaseAuditModel):
elif self.check_type == "eventlog":
body = subject
CORE.send_sms(body, alert_template=alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template)
def send_resolved_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = f"{self} is now back to normal"
CORE.send_mail(subject, body, alert_template=alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
def send_resolved_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
CORE.send_sms(subject, alert_template=alert_template)
CORE.send_sms(subject, alert_template=self.agent.alert_template)
class CheckHistory(models.Model):

View File

@@ -25,7 +25,7 @@ class CheckSerializer(serializers.ModelSerializer):
def get_alert_template(self, obj):
if obj.agent:
alert_template = obj.agent.get_alert_template()
alert_template = obj.agent.alert_template
else:
alert_template = None

View File

@@ -1,4 +1,3 @@
from logging import warning
from unittest.mock import patch
from django.utils import timezone as djangotime
@@ -311,14 +310,8 @@ class TestCheckViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_run_checks(self, nats_cmd):
agent = baker.make_recipe("agents.agent", version="1.4.1")
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
url = f"/checks/runchecks/{agent_old.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
url = f"/checks/runchecks/{agent_b4_141.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@@ -970,24 +963,205 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.status, "passing")
def test_handle_eventlog_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
eventlog = baker.make_recipe("checks.eventlog_check", agent=self.agent)
eventlog = baker.make_recipe(
"checks.eventlog_check",
event_type="warning",
fail_when="contains",
event_id=123,
alert_severity="warning",
agent=self.agent,
)
# test failing warning
data = {}
data = {
"id": eventlog.id,
"log": [
{
"eventType": "warning",
"eventID": 150,
"source": "source",
"message": "a test message",
},
{
"eventType": "warning",
"eventID": 123,
"source": "source",
"message": "a test message",
},
{
"eventType": "error",
"eventID": 123,
"source": "source",
"message": "a test message",
},
{
"eventType": "error",
"eventID": 123,
"source": "source",
"message": "a test message",
},
],
}
# resp = self.client.patch(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
# test failing when contains
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# test failing error
data = {}
new_check = Check.objects.get(pk=eventlog.id)
# resp = self.client.patch(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
self.assertEquals(new_check.alert_severity, "warning")
self.assertEquals(new_check.status, "failing")
# test passing
data = {}
# test passing when not contains and message
eventlog.event_message = "doesnt exist"
eventlog.save()
# resp = self.client.patch(url, data, format="json")
# self.assertEqual(resp.status_code, 200)
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test failing when not contains and message and source
eventlog.fail_when = "not_contains"
eventlog.alert_severity = "error"
eventlog.event_message = "doesnt exist"
eventlog.event_source = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "error")
# test passing when contains with source and message
eventlog.event_message = "test"
eventlog.event_source = "source"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test failing with wildcard not contains and source
eventlog.event_id_is_wildcard = True
eventlog.event_source = "doesn't exist"
eventlog.event_message = ""
eventlog.event_id = 0
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "error")
# test passing with wildcard contains
eventlog.event_source = ""
eventlog.event_message = ""
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test failing with wildcard contains and message
eventlog.fail_when = "contains"
eventlog.event_type = "error"
eventlog.alert_severity = "info"
eventlog.event_message = "test"
eventlog.event_source = ""
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "info")
# test passing with wildcard not contains message and source
eventlog.event_message = "doesnt exist"
eventlog.event_source = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# test multiple events found and contains
# this should pass since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "contains"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
# test not contains
# this should fail since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "not_contains"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "failing")
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")

View File

@@ -161,8 +161,6 @@ class CheckHistory(APIView):
@api_view()
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))

View File

@@ -32,6 +32,7 @@ class Client(BaseAuditModel):
)
def save(self, *args, **kw):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_by_location_task
# get old client if exists
@@ -54,6 +55,9 @@ class Client(BaseAuditModel):
create_tasks=True,
)
if old_client and old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -127,6 +131,7 @@ class Site(BaseAuditModel):
)
def save(self, *args, **kw):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_by_location_task
# get old client if exists
@@ -149,6 +154,9 @@ class Site(BaseAuditModel):
create_tasks=True,
)
if old_site and old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)

View File

@@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
@@ -27,6 +28,18 @@ var (
DownloadUrl string
)
var netTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Timeout: time.Second * 900,
Transport: netTransport,
}
func downloadAgent(filepath string) (err error) {
out, err := os.Create(filepath)
@@ -35,7 +48,7 @@ func downloadAgent(filepath string) (err error) {
}
defer out.Close()
resp, err := http.Get(DownloadUrl)
resp, err := netClient.Get(DownloadUrl)
if err != nil {
return err
}
@@ -59,7 +72,6 @@ func main() {
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
cert := flag.String("cert", "", "Path to ca.pem")
timeout := flag.String("timeout", "", "Timeout for subprocess calls")
flag.Parse()
var debug bool = false
@@ -93,10 +105,6 @@ func main() {
cmdArgs = append(cmdArgs, "-cert", *cert)
}
if len(strings.TrimSpace(*timeout)) != 0 {
cmdArgs = append(cmdArgs, "-timeout", *timeout)
}
if Rdp == "1" {
cmdArgs = append(cmdArgs, "-rdp")
}

View File

@@ -33,11 +33,20 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
Try
{
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*\*'
$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*\*'
}
}
Catch {
# pass
}
Try
{
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
write-host ('Extracting...')

View File

@@ -78,6 +78,7 @@ class CoreSettings(BaseAuditModel):
)
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_all_agent_checks_task
if not self.pk and CoreSettings.objects.exists():
@@ -105,6 +106,9 @@ class CoreSettings(BaseAuditModel):
mon_type="workstation", create_tasks=True
)
if old_settings and old_settings.alert_template != self.alert_template:
cache_agents_alert_template.delay()
def __str__(self):
return "Global Site Settings"

View File

@@ -63,6 +63,7 @@ def dashboard_info(request):
"show_community_scripts": request.user.show_community_scripts,
"dbl_click_action": request.user.agent_dblclick_action,
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
"client_tree_sort": request.user.client_tree_sort,
}
)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-02-28 09:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0011_auto_20201119_0854'),
]
operations = [
migrations.AlterField(
model_name='pendingaction',
name='action_type',
field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('taskaction', 'Scheduled Task Action'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install')], max_length=255, null=True),
),
]

View File

@@ -9,6 +9,7 @@ ACTION_TYPE_CHOICES = [
("schedreboot", "Scheduled Reboot"),
("taskaction", "Scheduled Task Action"),
("agentupdate", "Agent Update"),
("chocoinstall", "Chocolatey Software Install"),
]
AUDIT_ACTION_TYPE_CHOICES = [
@@ -249,9 +250,12 @@ class PendingAction(models.Model):
if self.action_type == "schedreboot":
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
elif self.action_type == "taskaction" or self.action_type == "agentupdate":
elif self.action_type == "taskaction":
return "Next agent check-in"
elif self.action_type == "agentupdate":
return "Next update cycle"
elif self.action_type == "chocoinstall":
return "ASAP"
@property
def description(self):
@@ -261,6 +265,9 @@ class PendingAction(models.Model):
elif self.action_type == "agentupdate":
return f"Agent update to {self.details['version']}"
elif self.action_type == "chocoinstall":
return f"{self.details['name']} software install"
elif self.action_type == "taskaction":
if self.details["action"] == "taskdelete":
return "Device pending task deletion"

View File

@@ -3,10 +3,9 @@ from unittest.mock import patch
from model_bakery import baker, seq
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .serializers import PendingActionSerializer
class TestAuditViews(TacticalTestCase):
def setUp(self):
@@ -177,63 +176,97 @@ class TestAuditViews(TacticalTestCase):
self.check_not_authenticated("post", url)
def test_agent_pending_actions(self):
agent = baker.make_recipe("agents.agent")
pending_actions = baker.make(
def test_get_pending_actions(self):
url = "/logs/pendingactions/"
agent1 = baker.make_recipe("agents.online_agent")
agent2 = baker.make_recipe("agents.online_agent")
baker.make(
"logs.PendingAction",
agent=agent,
_quantity=6,
agent=agent1,
action_type="chocoinstall",
details={"name": "googlechrome", "output": None, "installed": False},
_quantity=12,
)
baker.make(
"logs.PendingAction",
agent=agent2,
action_type="chocoinstall",
status="completed",
details={"name": "adobereader", "output": None, "installed": False},
_quantity=14,
)
url = f"/logs/{agent.pk}/pendingactions/"
resp = self.client.get(url, format="json")
serializer = PendingActionSerializer(pending_actions, many=True)
data = {"showCompleted": False}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
self.assertEqual(r.data["completed_count"], 14) # type: ignore
self.assertEqual(r.data["total"], 26) # type: ignore
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 6)
self.assertEqual(resp.data, serializer.data)
PendingAction.objects.filter(action_type="chocoinstall").update(
status="completed"
)
data = {"showCompleted": True}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 26) # type: ignore
self.assertEqual(r.data["completed_count"], 26) # type: ignore
self.assertEqual(r.data["total"], 26) # type: ignore
self.check_not_authenticated("get", url)
data = {"showCompleted": True, "agentPK": agent1.pk}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
self.assertEqual(r.data["completed_count"], 12) # type: ignore
self.assertEqual(r.data["total"], 12) # type: ignore
def test_all_pending_actions(self):
url = "/logs/allpendingactions/"
agent = baker.make_recipe("agents.agent")
pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6)
resp = self.client.get(url, format="json")
serializer = PendingActionSerializer(pending_actions, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 6)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
self.check_not_authenticated("patch", url)
@patch("agents.models.Agent.nats_cmd")
def test_cancel_pending_action(self, nats_cmd):
url = "/logs/cancelpendingaction/"
# TODO fix this TypeError: Object of type coroutine is not JSON serializable
""" agent = baker.make("agents.Agent", version="1.1.1")
pending_action = baker.make(
nats_cmd.return_value = "ok"
url = "/logs/pendingactions/"
agent = baker.make_recipe("agents.online_agent")
action = baker.make(
"logs.PendingAction",
agent=agent,
action_type="schedreboot",
details={
"time": "2021-01-13 18:20:00",
"taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
},
)
data = {"pk": pending_action.id}
resp = self.client.delete(url, data, format="json")
self.assertEqual(resp.status_code, 200)
data = {"pk": action.pk} # type: ignore
r = self.client.delete(url, data, format="json")
self.assertEqual(r.status_code, 200)
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"},
}
nats_cmd.assert_called_with(nats_data, timeout=10)
# try request again and it should fail since pending action doesn't exist
resp = self.client.delete(url, data, format="json")
self.assertEqual(resp.status_code, 404) """
# try request again and it should 404 since pending action doesn't exist
r = self.client.delete(url, data, format="json")
self.assertEqual(r.status_code, 404)
nats_cmd.reset_mock()
action2 = baker.make(
"logs.PendingAction",
agent=agent,
action_type="schedreboot",
details={
"time": "2021-01-13 18:20:00",
"taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
},
)
data = {"pk": action2.pk} # type: ignore
nats_cmd.return_value = "error deleting sched task"
r = self.client.delete(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "error deleting sched task") # type: ignore
self.check_not_authenticated("delete", url)

View File

@@ -3,11 +3,9 @@ from django.urls import path
from . import views
urlpatterns = [
path("pendingactions/", views.PendingActions.as_view()),
path("auditlogs/", views.GetAuditLogs.as_view()),
path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()),
path("<int:pk>/pendingactions/", views.agent_pending_actions),
path("allpendingactions/", views.all_pending_actions),
path("cancelpendingaction/", views.cancel_pending_action),
path("debuglog/<mode>/<hostname>/<order>/", views.debug_log),
path("downloadlog/", views.download_log),
]

View File

@@ -106,34 +106,46 @@ class FilterOptionsAuditLog(APIView):
return Response("error", status=status.HTTP_400_BAD_REQUEST)
@api_view()
def agent_pending_actions(request, pk):
action = PendingAction.objects.filter(agent__pk=pk)
return Response(PendingActionSerializer(action, many=True).data)
class PendingActions(APIView):
def patch(self, request):
status_filter = "completed" if request.data["showCompleted"] else "pending"
if "agentPK" in request.data.keys():
actions = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status=status_filter
)
total = PendingAction.objects.filter(
agent__pk=request.data["agentPK"]
).count()
completed = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status="completed"
).count()
else:
actions = PendingAction.objects.filter(status=status_filter).select_related(
"agent"
)
total = PendingAction.objects.count()
completed = PendingAction.objects.filter(status="completed").count()
@api_view()
def all_pending_actions(request):
actions = PendingAction.objects.all().select_related("agent")
return Response(PendingActionSerializer(actions, many=True).data)
ret = {
"actions": PendingActionSerializer(actions, many=True).data,
"completed_count": completed,
"total": total,
}
return Response(ret)
def delete(self, request):
action = get_object_or_404(PendingAction, pk=request.data["pk"])
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": action.details["taskname"]},
}
r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
return notify_error(r)
@api_view(["DELETE"])
def cancel_pending_action(request):
action = get_object_or_404(PendingAction, pk=request.data["pk"])
if not action.agent.has_gotasks:
return notify_error("Requires agent version 1.1.1 or greater")
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": action.details["taskname"]},
}
r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
return notify_error(r)
action.delete()
return Response(f"{action.agent.hostname}: {action.description} was cancelled")
action.delete()
return Response(f"{action.agent.hostname}: {action.description} was cancelled")
@api_view()

View File

@@ -9,52 +9,28 @@ class TestNatsAPIViews(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
def test_nats_wmi(self):
url = "/natsapi/wmi/"
baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14)
def test_nats_agents(self):
baker.make_recipe(
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=14
)
baker.make_recipe(
"agents.offline_agent", version=settings.LATEST_AGENT_VER, _quantity=6
)
baker.make_recipe(
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
)
baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7)
url = "/natsapi/online/agents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.json()["agent_ids"]), 17)
self.assertEqual(len(r.json()["agent_ids"]), 14)
def test_natscheckin_patch(self):
from logs.models import PendingAction
url = "/natsapi/checkin/"
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
PendingAction.objects.create(
agent=agent_updated,
action_type="agentupdate",
details={
"url": agent_updated.winagent_dl,
"version": agent_updated.version,
"inno": agent_updated.win_inno_exe,
},
)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent failed to update and still on same version
payload = {
"func": "hello",
"agent_id": agent_updated.agent_id,
"version": "1.3.0",
}
r = self.client.patch(url, payload, format="json")
url = "/natsapi/offline/agents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
self.assertEqual(len(r.json()["agent_ids"]), 11)
# test agent successful update
payload["version"] = settings.LATEST_AGENT_VER
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "completed")
action.delete()
url = "/natsapi/asdjaksdasd/agents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)

View File

@@ -4,12 +4,6 @@ from . import views
urlpatterns = [
path("natsinfo/", views.nats_info),
path("checkin/", views.NatsCheckIn.as_view()),
path("syncmesh/", views.SyncMeshNodeID.as_view()),
path("winupdates/", views.NatsWinUpdates.as_view()),
path("choco/", views.NatsChoco.as_view()),
path("wmi/", views.NatsWMI.as_view()),
path("offline/", views.OfflineAgents.as_view()),
path("<str:stat>/agents/", views.NatsAgents.as_view()),
path("logcrash/", views.LogCrash.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
]

View File

@@ -1,11 +1,7 @@
import asyncio
import time
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from rest_framework.decorators import (
api_view,
authentication_classes,
@@ -15,12 +11,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from agents.serializers import WinAgentSerializer
from agents.tasks import handle_agent_recovery_task
from checks.utils import bytes2human
from software.models import InstalledSoftware
from tacticalrmm.utils import SoftwareList, filter_software, notify_error
from winupdate.models import WinUpdate
from tacticalrmm.utils import notify_error
logger.configure(**settings.LOG_CONFIG)
@@ -32,269 +23,24 @@ def nats_info(request):
return Response({"user": "tacticalrmm", "password": settings.SECRET_KEY})
class NatsCheckIn(APIView):
class NatsAgents(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def patch(self, request):
updated = False
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if pyver.parse(request.data["version"]) > pyver.parse(
agent.version
) or pyver.parse(request.data["version"]) == pyver.parse(
settings.LATEST_AGENT_VER
):
updated = True
agent.version = request.data["version"]
agent.last_seen = djangotime.now()
agent.save(update_fields=["version", "last_seen"])
def get(self, request, stat: str):
if stat not in ["online", "offline"]:
return notify_error("invalid request")
# change agent update pending status to completed if agent has just updated
if (
updated
and agent.pendingactions.filter( # type: ignore
action_type="agentupdate", status="pending"
).exists()
):
agent.pendingactions.filter( # type: ignore
action_type="agentupdate", status="pending"
).update(status="completed")
# handles any alerting actions
agent.handle_alert(checkin=True)
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
handle_agent_recovery_task.delay(pk=recovery.pk)
return Response("ok")
# get any pending actions
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
agent.handle_pending_actions()
return Response("ok")
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
if request.data["func"] == "disks":
disks = request.data["disks"]
new = []
for disk in disks:
tmp = {}
for _, _ in disk.items():
tmp["device"] = disk["device"]
tmp["fstype"] = disk["fstype"]
tmp["total"] = bytes2human(disk["total"])
tmp["used"] = bytes2human(disk["used"])
tmp["free"] = bytes2human(disk["free"])
tmp["percent"] = int(disk["percent"])
new.append(tmp)
serializer.is_valid(raise_exception=True)
serializer.save(disks=new)
return Response("ok")
if request.data["func"] == "loggedonuser":
if request.data["logged_in_username"] != "None":
serializer.is_valid(raise_exception=True)
serializer.save(last_logged_in_user=request.data["logged_in_username"])
return Response("ok")
if request.data["func"] == "software":
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = filter_software(raw)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first() # type: ignore
s.software = sw
s.save(update_fields=["software"])
return Response("ok")
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
# called once during tacticalagent windows service startup
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if not agent.choco_installed:
asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
time.sleep(0.5)
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
return Response("ok")
class SyncMeshNodeID(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if agent.mesh_node_id != request.data["nodeid"]:
agent.mesh_node_id = request.data["nodeid"]
agent.save(update_fields=["mesh_node_id"])
return Response("ok")
class NatsChoco(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.choco_installed = request.data["installed"]
agent.save(update_fields=["choco_installed"])
return Response("ok")
class NatsWinUpdates(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy: str = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["needs_reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
agent.delete_superseded_updates()
return Response("ok")
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
u = agent.winupdates.filter(guid=request.data["guid"]).last() # type: ignore
success: bool = request.data["success"]
if success:
u.result = "success"
u.downloaded = True
u.installed = True
u.date_installed = djangotime.now()
u.save(
update_fields=[
"result",
"downloaded",
"installed",
"date_installed",
]
)
ret: list[str] = []
agents = Agent.objects.only(
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
if stat == "online":
ret = [i.agent_id for i in agents if i.status == "online"]
else:
u.result = "failed"
u.save(update_fields=["result"])
ret = [i.agent_id for i in agents if i.status != "online"]
agent.delete_superseded_updates()
return Response("ok")
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = request.data["wua_updates"]
for update in updates:
if agent.winupdates.filter(guid=update["guid"]).exists(): # type: ignore
u = agent.winupdates.filter(guid=update["guid"]).last() # type: ignore
u.downloaded = update["downloaded"]
u.installed = update["installed"]
u.save(update_fields=["downloaded", "installed"])
else:
try:
kb = "KB" + update["kb_article_ids"][0]
except:
continue
WinUpdate(
agent=agent,
guid=update["guid"],
kb=kb,
title=update["title"],
installed=update["installed"],
downloaded=update["downloaded"],
description=update["description"],
severity=update["severity"],
categories=update["categories"],
category_ids=update["category_ids"],
kb_article_ids=update["kb_article_ids"],
more_info_urls=update["more_info_urls"],
support_url=update["support_url"],
revision_number=update["revision_number"],
).save()
agent.delete_superseded_updates()
# more superseded updates cleanup
if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
for u in agent.winupdates.filter( # type: ignore
date_installed__isnull=True, result="failed"
).exclude(installed=True):
u.delete()
return Response("ok")
class SupersededWinUpdate(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = agent.winupdates.filter(guid=request.data["guid"]) # type: ignore
for u in updates:
u.delete()
return Response("ok")
class NatsWMI(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def get(self, request):
agents = Agent.objects.only(
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
online: list[str] = [
i.agent_id
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online"
]
return Response({"agent_ids": online})
class OfflineAgents(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def get(self, request):
agents = Agent.objects.only(
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
offline: list[str] = [
i.agent_id for i in agents if i.has_nats and i.status != "online"
]
return Response({"agent_ids": offline})
return Response({"agent_ids": ret})
class LogCrash(APIView):
@@ -305,4 +51,10 @@ class LogCrash(APIView):
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
if hasattr(settings, "DEBUGTEST") and settings.DEBUGTEST:
logger.info(
f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
)
return Response("ok")

View File

@@ -6,17 +6,17 @@ celery==5.0.5
certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
cryptography==3.4.6
cryptography==3.4.7
decorator==4.4.2
Django==3.1.7
django-cors-headers==3.7.0
django-rest-knox==4.1.0
djangorestframework==3.12.2
djangorestframework==3.12.3
future==0.18.2
kombu==5.0.2
loguru==0.5.3
msgpack==1.0.2
packaging==20.8
packaging==20.9
psycopg2-binary==2.8.6
pycparser==2.20
pycryptodome==3.10.1
@@ -28,10 +28,10 @@ redis==3.5.3
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
twilio==6.52.0
urllib3==1.26.3
twilio==6.55.0
urllib3==1.26.4
uWSGI==2.0.19.1
validators==0.18.2
vine==5.0.0
websockets==8.1
zipp==3.4.0
zipp==3.4.1

View File

@@ -201,5 +201,47 @@
"name": "Display Message To User",
"description": "Displays a popup message to the currently logged on user",
"shell": "powershell"
},
{
"filename": "VerifyAntivirus.ps1",
"submittedBy": "https://github.com/beejayzed",
"name": "Verify Antivirus Status",
"description": "Verify and display status for all installed Antiviruses",
"shell": "powershell"
},
{
"filename": "CreateAllUserLogonScript.ps1",
"submittedBy": "https://github.com/nr-plaxon",
"name": "Create User Logon Script",
"description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.",
"shell": "powershell"
},
{
"filename": "Chocolatey_Update_Installed.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey Update Installed Apps",
"description": "Update all apps that were installed using Chocolatey.",
"shell": "cmd"
},
{
"filename": "AD_Check_And_Enable_AD_Recycle_Bin.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "AD - Check and Enable AD Recycle Bin",
"description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
"shell": "powershell"
},
{
"filename": "Check_Events_for_Bluescreens.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Event Viewer - Check for Bluescreens",
"description": "This will check for Bluescreen events on your system",
"shell": "powershell"
},
{
"filename": "Rename_Computer.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Rename Computer",
"description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
"shell": "powershell"
}
]
]

View File

@@ -7,8 +7,6 @@ from tacticalrmm.celery import app
@app.task
def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
nats_data = {
"func": "rawcmd",
"timeout": timeout,
@@ -17,15 +15,13 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
"shell": shell,
},
}
for agent in agents_nats:
for agent in Agent.objects.filter(pk__in=agentpks):
asyncio.run(agent.nats_cmd(nats_data, wait=False))
@app.task
def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
script = Script.objects.get(pk=scriptpk)
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
nats_data = {
"func": "runscript",
"timeout": timeout,
@@ -35,5 +31,5 @@ def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
"shell": script.shell,
},
}
for agent in agents_nats:
for agent in Agent.objects.filter(pk__in=agentpks):
asyncio.run(agent.nats_cmd(nats_data, wait=False))

View File

@@ -18,8 +18,6 @@ logger.configure(**settings.LOG_CONFIG)
@api_view()
def get_services(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10))
if r == "timeout":
@@ -38,8 +36,6 @@ def default_services(request):
@api_view(["POST"])
def service_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
action = request.data["sv_action"]
data = {
"func": "winsvcaction",
@@ -80,8 +76,6 @@ def service_action(request):
@api_view()
def service_detail(request, pk, svcname):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {"func": "winsvcdetail", "payload": {"name": svcname}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "timeout":
@@ -93,8 +87,6 @@ def service_detail(request, pk, svcname):
@api_view(["POST"])
def edit_service(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {
"func": "editwinsvc",
"payload": {

View File

@@ -1,16 +1,11 @@
from django.contrib import admin
from .models import ChocoLog, ChocoSoftware, InstalledSoftware
from .models import ChocoSoftware, InstalledSoftware
class ChocoAdmin(admin.ModelAdmin):
readonly_fields = ("added",)
class ChocoLogAdmin(admin.ModelAdmin):
readonly_fields = ("time",)
admin.site.register(ChocoSoftware, ChocoAdmin)
admin.site.register(ChocoLog, ChocoLogAdmin)
admin.site.register(InstalledSoftware)

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -18,4 +18,4 @@ class Command(BaseCommand):
ChocoSoftware.objects.all().delete()
ChocoSoftware(chocos=chocos).save()
self.stdout.write("Chocos saved to db")
self.stdout.write(self.style.SUCCESS("Chocos saved to db"))

View File

@@ -0,0 +1,16 @@
# Generated by Django 3.1.7 on 2021-03-01 21:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('software', '0002_auto_20200810_0544'),
]
operations = [
migrations.DeleteModel(
name='ChocoLog',
),
]

View File

@@ -8,23 +8,7 @@ class ChocoSoftware(models.Model):
added = models.DateTimeField(auto_now_add=True)
def __str__(self):
from .serializers import ChocoSoftwareSerializer
return (
str(len(ChocoSoftwareSerializer(self).data["chocos"])) + f" - {self.added}"
)
class ChocoLog(models.Model):
agent = models.ForeignKey(Agent, related_name="chocolog", on_delete=models.CASCADE)
name = models.CharField(max_length=255)
version = models.CharField(max_length=255)
message = models.TextField()
installed = models.BooleanField(default=False)
time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.agent.hostname} | {self.name} | {self.time}"
return f"{len(self.chocos)} - {self.added}"
class InstalledSoftware(models.Model):

View File

@@ -1,12 +1,6 @@
from rest_framework import serializers
from .models import ChocoSoftware, InstalledSoftware
class ChocoSoftwareSerializer(serializers.ModelSerializer):
class Meta:
model = ChocoSoftware
fields = "__all__"
from .models import InstalledSoftware
class InstalledSoftwareSerializer(serializers.ModelSerializer):

View File

@@ -1,57 +0,0 @@
import asyncio
from django.conf import settings
from loguru import logger
from agents.models import Agent
from tacticalrmm.celery import app
from .models import ChocoLog
logger.configure(**settings.LOG_CONFIG)
@app.task
def install_program(pk, name, version):
agent = Agent.objects.get(pk=pk)
nats_data = {
"func": "installwithchoco",
"choco_prog_name": name,
"choco_prog_ver": version,
}
r: str = asyncio.run(agent.nats_cmd(nats_data, timeout=915))
if r == "timeout":
logger.error(f"Failed to install {name} {version} on {agent.salt_id}: timeout")
return
try:
output = r.lower()
except Exception as e:
logger.error(f"Failed to install {name} {version} on {agent.salt_id}: {e}")
return
success = [
"install",
"of",
name.lower(),
"was",
"successful",
"installed",
]
duplicate = [name.lower(), "already", "installed", "--force", "reinstall"]
installed = False
if all(x in output for x in success):
installed = True
logger.info(f"Successfully installed {name} {version} on {agent.salt_id}")
elif all(x in output for x in duplicate):
logger.warning(f"Already installed: {name} {version} on {agent.salt_id}")
else:
logger.error(f"Something went wrong - {name} {version} on {agent.salt_id}")
ChocoLog(
agent=agent, name=name, version=version, message=output, installed=installed
).save()
return "ok"

View File

@@ -1,10 +1,12 @@
from unittest.mock import patch
import json
import os
from django.conf import settings
from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
from .models import ChocoLog
from .models import ChocoSoftware
from .serializers import InstalledSoftwareSerializer
@@ -15,29 +17,17 @@ class TestSoftwareViews(TacticalTestCase):
def test_chocos_get(self):
url = "/software/chocos/"
resp = self.client.get(url, format="json")
with open(os.path.join(settings.BASE_DIR, "software/chocos.json")) as f:
chocos = json.load(f)
if ChocoSoftware.objects.exists():
ChocoSoftware.objects.all().delete()
ChocoSoftware(chocos=chocos).save()
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("get", url)
@patch("software.tasks.install_program.delay")
def test_chocos_install(self, install_program):
url = "/software/install/"
agent = baker.make_recipe("agents.agent")
# test a call where agent doesn't exist
invalid_data = {"pk": 500, "name": "Test Software", "version": "1.0.0"}
resp = self.client.post(url, invalid_data, format="json")
self.assertEqual(resp.status_code, 404)
data = {"pk": agent.pk, "name": "Test Software", "version": "1.0.0"}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
install_program.assert_called_with(data["pk"], data["name"], data["version"])
self.check_not_authenticated("post", url)
def test_chocos_installed(self):
# test a call where agent doesn't exist
resp = self.client.get("/software/installed/500/", format="json")
@@ -64,26 +54,3 @@ class TestSoftwareViews(TacticalTestCase):
self.assertEquals(resp.data, serializer.data)
self.check_not_authenticated("get", url)
class TestSoftwareTasks(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
@patch("agents.models.Agent.nats_cmd")
def test_install_program(self, nats_cmd):
from .tasks import install_program
agent = baker.make_recipe("agents.agent")
nats_cmd.return_value = "install of git was successful"
_ = install_program(agent.pk, "git", "2.3.4")
nats_cmd.assert_called_with(
{
"func": "installwithchoco",
"choco_prog_name": "git",
"choco_prog_ver": "2.3.4",
},
timeout=915,
)
self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists())

View File

@@ -2,31 +2,51 @@ import asyncio
from typing import Any
from django.shortcuts import get_object_or_404
from packaging import version as pyver
from rest_framework.decorators import api_view
from rest_framework.response import Response
from agents.models import Agent
from logs.models import PendingAction
from tacticalrmm.utils import filter_software, notify_error
from .models import ChocoSoftware, InstalledSoftware
from .serializers import ChocoSoftwareSerializer, InstalledSoftwareSerializer
from .tasks import install_program
from .serializers import InstalledSoftwareSerializer
@api_view()
def chocos(request):
chocos = ChocoSoftware.objects.last()
return Response(ChocoSoftwareSerializer(chocos).data["chocos"])
return Response(ChocoSoftware.objects.last().chocos)
@api_view(["POST"])
def install(request):
pk = request.data["pk"]
agent = get_object_or_404(Agent, pk=pk)
agent = get_object_or_404(Agent, pk=request.data["pk"])
if pyver.parse(agent.version) < pyver.parse("1.4.8"):
return notify_error("Requires agent v1.4.8")
name = request.data["name"]
version = request.data["version"]
install_program.delay(pk, name, version)
return Response(f"{name} will be installed shortly on {agent.hostname}")
action = PendingAction.objects.create(
agent=agent,
action_type="chocoinstall",
details={"name": name, "output": None, "installed": False},
)
nats_data = {
"func": "installwithchoco",
"choco_prog_name": name,
"pending_action_pk": action.pk,
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=2))
if r != "ok":
action.delete()
return notify_error("Unable to contact the agent")
return Response(
f"{name} will be installed shortly on {agent.hostname}. Check the Pending Actions menu to see the status/output"
)
@api_view()
@@ -43,8 +63,6 @@ def get_installed(request, pk):
@api_view()
def refresh_installed(request, pk):
agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
if r == "timeout" or r == "natsdown":

View File

@@ -51,6 +51,8 @@ class AuditMiddleware:
# Here's our fully formed and authenticated (or not, depending on credentials) request
request = view.initialize_request(request)
except (AttributeError, TypeError):
from rest_framework.views import APIView
# Can't initialize the request from this view. Fallback to using default permission classes
request = APIView().initialize_request(request)

View File

@@ -2,7 +2,7 @@ import os
from datetime import timedelta
from pathlib import Path
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = "/srv/salt/scripts"
@@ -15,20 +15,20 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.4.18"
TRMM_VERSION = "0.4.30"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.116"
APP_VER = "0.0.123"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.4.7"
LATEST_AGENT_VER = "1.4.13"
MESH_VER = "0.7.73"
MESH_VER = "0.7.93"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "10"
NPM_VER = "8"
PIP_VER = "12"
NPM_VER = "11"
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
@@ -39,11 +39,9 @@ except ImportError:
pass
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework.authtoken",
@@ -66,10 +64,20 @@ INSTALLED_APPS = [
"natsapi",
]
if not "TRAVIS" in os.environ and not "AZPIPELINE" in os.environ:
if DEBUG:
if not "AZPIPELINE" in os.environ:
if DEBUG: # type: ignore
INSTALLED_APPS += ("django_extensions",)
if "AZPIPELINE" in os.environ:
ADMIN_ENABLED = False
if ADMIN_ENABLED: # type: ignore
INSTALLED_APPS += (
"django.contrib.admin",
"django.contrib.messages",
)
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@@ -78,10 +86,11 @@ MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"tacticalrmm.middleware.AuditMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
if ADMIN_ENABLED: # type: ignore
MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
REST_KNOX = {
"TOKEN_TTL": timedelta(hours=5),

View File

@@ -1,12 +1,10 @@
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from knox import views as knox_views
from accounts.views import CheckCreds, LoginView
urlpatterns = [
path(settings.ADMIN_URL, admin.site.urls),
path("checkcreds/", CheckCreds.as_view()),
path("login/", LoginView.as_view()),
path("logout/", knox_views.LogoutView.as_view()),
@@ -27,3 +25,8 @@ urlpatterns = [
path("accounts/", include("accounts.urls")),
path("natsapi/", include("natsapi.urls")),
]
if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
from django.contrib import admin
urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)

View File

@@ -19,7 +19,7 @@ def auto_approve_updates_task():
# scheduled task that checks and approves updates daily
agents = Agent.objects.only(
"pk", "version", "last_seen", "overdue_time", "offline_time"
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
for agent in agents:
agent.delete_superseded_updates()
@@ -46,7 +46,7 @@ def auto_approve_updates_task():
def check_agent_update_schedule_task():
# scheduled task that installs updates on agents if enabled
agents = Agent.objects.only(
"pk", "version", "last_seen", "overdue_time", "offline_time"
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
online = [
i

View File

@@ -3,6 +3,7 @@ FROM node:12-alpine AS builder
WORKDIR /home/node/app
COPY ./web/package.json .
RUN npm install -g npm
RUN npm install
COPY ./web .

View File

@@ -1,4 +1,4 @@
FROM nats:2.1-alpine
FROM nats:2.2-alpine
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

View File

@@ -46,7 +46,7 @@ COPY --from=CREATE_VENV_STAGE ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# install deps
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends git && \
apt-get install -y --no-install-recommends git rsync && \
rm -rf /var/lib/apt/lists/* && \
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo && \
groupadd -g 1000 "${TACTICAL_USER}" && \

View File

@@ -35,7 +35,7 @@ if [ "$1" = 'tactical-init' ]; then
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# copy container data to volume
cp -af ${TACTICAL_TMP_DIR}/. ${TACTICAL_DIR}/
rsync -a --no-perms --no-owner --delete --exclude "tmp/*" --exclude "certs/*" --exclude="api/tacticalrmm/private/*" "${TACTICAL_TMP_DIR}/" "${TACTICAL_DIR}/"
until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
echo "waiting for postgresql container to be ready..."
@@ -106,6 +106,7 @@ MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
REDIS_HOST = '${REDIS_HOST}'
MESH_WS_URL = 'ws://${MESH_CONTAINER}:443'
ADMIN_ENABLED = False
EOF
)"

29
docs/docs/contributing.md Normal file
View File

@@ -0,0 +1,29 @@
# Contributing
### Contributing to the docs
Docs are built with [MKDocs for Material](https://squidfunk.github.io/mkdocs-material/)
To setup a local environment to add/edit to this documentation site:
```bash
mkdir ~/rmmdocs && cd ~/rmmdocs
git clone https://github.com/wh1te909/tacticalrmm.git .
python3 -m venv env
source env/bin/activate
pip install --upgrade pip
pip install --upgrade setuptools wheel
pip install -r api/tacticalrmm/requirements-dev.txt
cd docs
mkdocs serve
```
Open your browser and navigate to `http://yourserverip:8005`
Add/edit markdown files in the `docs/docs` folder and you'll see live changes at the url above.
Edit `docs/mkdocs.yml` to edit structure and add new files.
Full mkdocs documentation [here](https://squidfunk.github.io/mkdocs-material/getting-started/)
Once finished, [create a pull request](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github) to the `develop` branch for review.

View File

@@ -10,29 +10,9 @@ Linux / Mac agents are currently under development.
Yes, you will just need to setup local DNS for the 3 subdomains, either by editing host files on all your agents or through a local DNS server.
#### I am locked out of the web UI. How do I reset my password?
SSH into your server and run these commands:
!!!note
The code below will reset the password for the account that was created during install.
To reset a password for a different user, you should use the web UI (see the next question below), but can also do so through the command line by replacing<br/>
`#!python user = User.objects.first()`<br/>
with<br/>
`#!python user = User.objects.get(username='someuser')`
<br/>
in the code snippet below.
```python
tactical@tacrmm:~$ /rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py shell
Python 3.9.2 (default, Feb 21 2021, 00:50:28)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from accounts.models import User
>>> user = User.objects.first()
>>> user.set_password("superSekret123")
>>> user.save()
>>> exit()
SSH into your server and run:
```bash
/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py reset_password <username>
```
<br/>
@@ -42,17 +22,22 @@ From the web UI, click **Settings > User Administration** and then right-click o
![reset2fa](images/reset2fa.png)
<br/><br/>
Or from the command line:<br/>
```python
tactical@tacrmm:~$ /rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py shell
Python 3.9.2 (default, Feb 21 2021, 00:50:28)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from accounts.models import User
>>> user = User.objects.get(username='someuser')
>>> user.totp_key = None
>>> user.save(update_fields=['totp_key'])
>>> exit()
```bash
/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py reset_2fa <username>
```
<br/>
Then simply log out of the web UI and next time the user logs in they will be redirected to the 2FA setup page which will present a barcode to be scanned with the Authenticator app.
<br/>
#### How do I recover my MeshCentral login credentials?
From Tactical's web UI: *Settings > Global Settings > MeshCentral*
Copy the username then ssh into the server and run:
```bash
cd /meshcentral/
sudo systemctl stop meshcentral
node node_modules/meshcentral --resetaccount <username> --pass <newpassword>
sudo systemctl start meshcentral
```

View File

@@ -0,0 +1,19 @@
# Django Admin
!!!warning
Do not use the django admin unless you really know what you're doing.<br />You should never need to access it unless you are familiar with django or are instructed to do something here by one of the developers.
The django admin is basically a web interface for the postgres database.
As of Tactical RMM v0.4.19, the django admin is disabled by default.
To enable it, edit `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` and change `ADMIN_ENABLED` from `False` to `True` then `sudo systemctl restart rmm`
Login to the django admin using the same credentials as your normal web ui login.
If you did not save the django admin url (which was printed out at the end of the install script), check the `local_settings.py` file referenced above for the `ADMIN_URL` variable. Then simply append the value of this variable to your api domain (`https://api.yourdomain.com/`) to get the full url.
Example of a full django admin url:
```
https://api.example.com/JwboKNYb3v6K93Fvtcz0G3vUM17LMTSZggOUAxa97jQfAh0P5xosEk7u2PPkjEfdOtucUp/
```

View File

@@ -8,8 +8,7 @@
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django, Vue and Golang.
It uses an [agent](https://github.com/wh1te909/rmmagent) written in Golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
## [LIVE DEMO](https://rmm.xlawgaming.com/)
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
## [LIVE DEMO](https://rmm.tacticalrmm.io/)
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*

View File

@@ -58,7 +58,7 @@ The following optional arguments can be passed to any of the installation method
```
-log debug
```
Will print very verbose logging during agent install. Usefull for troubleshooting agent install.
Will print very verbose logging during agent install. Useful for troubleshooting agent install.
```
-silent

View File

@@ -44,7 +44,7 @@ ufw allow proto tcp from any to any port 4222
```
!!!info
SSH is only required for you to remotely login and do basic linux server administration for your rmm. It is not needed for any agent communication.<br/>
SSH (port 22 tcp) is only required for you to remotely login and do basic linux server administration for your rmm. It is not needed for any agent communication.<br/>
Allow ssh from everywhere (__not__ recommended)
```bash
ufw allow ssh
@@ -52,8 +52,8 @@ ufw allow ssh
Allow ssh from only allowed IP's (__highly__ recommended)
```bash
ufw allow from X.X.X.X to any port 22
ufw allow from X.X.X.X to any port 22
ufw allow proto tcp from X.X.X.X to any port 22
ufw allow proto tcp from X.X.X.X to any port 22
```
Enable and activate the firewall
@@ -69,7 +69,7 @@ We'll be using `example.com` as our domain for this example.
The RMM uses 3 different sites. The Vue frontend e.g. `rmm.example.com` which is where you'll be accesing your RMM from the browser, the REST backend e.g. `api.example.com` and Meshcentral e.g. `mesh.example.com`
Get the public IP of your server with `curl icanhazip.com`<br/>
Get the public IP of your server with `curl https://icanhazip.tacticalrmm.io`<br/>
Open the DNS manager of wherever the domain you purchased is hosted.<br/>
Create 3 A records: `rmm`, `api` and `mesh` and point them to the public IP of your server:
@@ -110,7 +110,8 @@ Create a login for the RMM web UI:
![rmmlogin](images/rmmlogin.png)
A bunch of URLS / usernames / passwords will be printed out at the end of the install script. Save these somewhere safe.
A bunch of URLS / usernames / passwords will be printed out at the end of the install script. **Save these somewhere safe.** [Recover them if you didn't](faq.md#how-do-i-recover-my-meshcentral-login-credentials)
Copy the url for the meshagent exe (`https://mesh.example.com/agentinvite?c=......`), paste it in your browser and download the mesh agent:

View File

@@ -0,0 +1,70 @@
# Management Commands
To run any of the management commands you must first activate the python virtual env:
```bash
cd /rmm/api/tacticalrmm
source ../env/bin/activate
```
#### Reset a user's password
```bash
python manage.py reset_password <username>
```
#### Reset a user's 2fa token
```bash
python manage.py reset_2fa <username>
```
#### Find all agents that have X software installed
```bash
python manage.py find_software "adobe"
```
#### Show outdated online agents
```bash
python manage.py show_outdated_agents
```
#### Log out all active web sessions
```bash
python manage.py delete_tokens
```
#### Check for orphaned tasks on all agents and remove them
```bash
python manage.py remove_orphaned_tasks
```
#### Create a MeshCentral agent invite link
```bash
python manage.py get_mesh_exe_url
```
#### Bulk update agent offline/overdue time
```bash
wget -q https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/api/tacticalrmm/agents/management/commands/bulk_change_checkin.py -O /rmm/api/tacticalrmm/agents/management/commands/bulk_change_checkin.py
```
Examples:
Change offline time on all agents to 5 minutes
```bash
python manage.py bulk_change_checkin --offline --all 5
```
Change overdue time on all agents to 10 minutes
```bash
python manage.py bulk_change_checkin --overdue --all 10
```
Change overdue time on all agents in client named *Example Client* to 12 minutes
```bash
python manage.py bulk_change_checkin --overdue --client "Example Client" 12
```
Change offline time on all agents in site named *Example Site* to 2 minutes
```bash
python manage.py bulk_change_checkin --offline --site "Example Site" 2
```

View File

@@ -3,6 +3,8 @@
!!!info
It is currently not possible to restore to a different domain/subdomain, only to a different physical or virtual server.
!!!danger
You must update your old RMM to the latest version using the `update.sh` script before attempting to restore.
#### Prepare the new server
Create the same exact linux user account as you did when you installed the original server.
@@ -11,7 +13,7 @@ Add it to the sudoers group and setup the firewall.
Refer to the [installation instructions](install_server.md) for steps on how to do all of the above.
#### Change DNS A records
Open the DNS manager of whever your domain is hosted.
Open the DNS manager of wherever your domain is hosted.
Change the 3 A records `rmm`, `api` and `mesh` and point them to the public IP of your new server.
@@ -23,11 +25,11 @@ Download the restore script.
```bash
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
chmod +x restore.sh
```
Call the restore script, passing it the backup file as the first argument:
```bash
chmod +x restore.sh
./restore.sh rmm-backup-XXXXXXXXX.tar
```

16
docs/docs/sponsor.md Normal file
View File

@@ -0,0 +1,16 @@
# Sponsor Tactical RMM
Tactical RMM is an MIT-licensed open-source project maintained by [@wh1te909](https://github.com/wh1te909), [@sadnub](https://github.com/sadnub) and a community of open source contributors. We work on behalf of the community to create new features, fix bugs, and maintain Tactical RMM.
Tactical RMM is currently the only open-source RMM available on the market. Responses from a recent survey have rated Tactical RMM as having the fastest and most reliable remote command and script execution compared to all other commercial RMM's. Sponsorships help keep this project alive and motivate the developers to continue to put in more time and effort into the advancement of Tactical RMM. Thousands of hours have been put in to developing, maintaining, improving, and supporting Tactical RMM. Your sponsorship would be greatly appreciated and helpful to keep this project going.
Funds donated will be used to setup a company to purchase a code signing certificate to sign all executables that are used by the RMM, costs needed to maintain demo and documentation sites, to support development of new features and the Linux and Mac agents.
If you have signed up to be a sponsor and have not been added to the Sponsors role on discord after a few hours, or if your github username is different than your discord username, please DM wh1te909 on [Discord](https://discord.gg/upGTkWp) after sponsoring to be added to the Sponsors role.
We are always looking for feedback and ways to improve Tactical RMM to better address your needs. Please feel free to open a [github issue](https://github.com/wh1te909/tacticalrmm/issues) or drop us a message on [Discord](https://discord.gg/upGTkWp) with your feedback or requests.
[Sponsor with Github](https://github.com/wh1te909)
[Sponsor with Ko-fi](https://ko-fi.com/tacticalrmm)

View File

@@ -51,9 +51,16 @@ This will print out a ton of info. You should be able to see the error from the
Please then copy/paste the logs and post them either in our [Discord support chat](https://discord.gg/upGTkWp), or create a [github issue](https://github.com/wh1te909/tacticalrmm/issues).
<br/>
#### Web UI frozen or not loading / website errors / general errors
First, run the [update script](update_server.md#updating-to-the-latest-rmm-version) with the `--force` flag. <br/>This will fix permissions and reinstall python/node packages that might have gotten corrupted.
```bash
./update.sh --force
```
Check all the systemd services that the rmm uses to function and check to make sure they're all active/running and enabled:
```bash

View File

@@ -0,0 +1,35 @@
# Updating the RMM (Docker)
#### Updating to the latest RMM version
Tactical RMM updates the docker images on every release and should be available within a few minutes
SSH into your server as a root user and run the below commands:<br/>
```bash
cd [dir/with/compose/file]
mv docker-compose.yml docker-compose.yml.old
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/docker/docker-compose.yml
sudo docker-compose pull
sudo docker-compose down
sudo docker-compose up -d --remove-orphans
```
#### Keeping your Let's Encrypt certificate up to date
To renew your Let's Encrypt wildcard cert, run the following command, replacing `example.com` with your domain and `admin@example.com` with your email:
```bash
sudo certbot certonly --manual -d *.example.com --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m admin@example.com --no-eff-email
```
Verify the domain with the TXT record. Once issued, run the below commands to base64 encode the certificates and add then to the .env file
```bash
echo "CERT_PUB_KEY=$(sudo base64 -w 0 /etc/letsencrypt/live/${rootdomain}/fullchain.pem)" >> .env
echo "CERT_PRIV_KEY=$(sudo base64 -w 0 /etc/letsencrypt/live/${rootdomain}/privkey.pem)" >> .env
```
!!!warning
You must remove the old and any duplicate entries for CERT_PUB_KEY and CERT_PRIV_KEY in the .env file
Now run `sudo docker-compose restart` and the new certificate will be in effect

View File

@@ -20,9 +20,9 @@ SSH into your server as the linux user you created during install.<br/><br/>
__Never__ run any update scripts or commands as the `root` user.<br/>This will mess up permissions and break your installation.<br/><br/>
Download the update script and run it:<br/>
```bash
tactical@tacrmm:~$ wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
tactical@tacrmm:~$ chmod +x update.sh
tactical@tacrmm:~$ ./update.sh
wget -N https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
chmod +x update.sh
./update.sh
```
<br/>
@@ -30,7 +30,7 @@ tactical@tacrmm:~$ ./update.sh
If you are already on the latest version, the update script will notify you of this and return immediately.<br/><br/>
You can pass the optional `--force` flag to the update script to forcefully run through an update, which will bypass the check for latest version.<br/>
```bash
tactical@tacrmm:~$ ./update.sh --force
./update.sh --force
```
This is usefull for a botched update that might have not completed fully.<br/><br/>
The update script will also fix any permissions that might have gotten messed up during a botched update, or if you accidentally ran the update script as the `root` user.
@@ -48,7 +48,7 @@ The update script will also fix any permissions that might have gotten messed up
#### Keeping your Let's Encrypt certificate up to date
!!!info
Currently, the update script does not automatically renew your Let's Encrypt wildcard certificate, which expires every 3 months, since this non-trivial to automate using the DNS TXT record method.
Currently, the update script does not automatically renew your Let's Encrypt wildcard certificate, which expires every 3 months, since this is non-trivial to automate using the DNS TXT record method.
To renew your Let's Encrypt wildcard cert, run the following command, replacing `example.com` with your domain and `admin@example.com` with your email:
@@ -62,4 +62,4 @@ After this you have renewed the cert, simply run the `update.sh` script, passing
```bash
./update.sh --force
```
```

View File

@@ -1,23 +1,28 @@
site_name: "Tactical RMM Documentation"
nav:
- Home: index.md
- Sponsor: sponsor.md
- RMM Installation:
- "Traditional Install": install_server.md
- "Docker Install": install_docker.md
- Agent Installation: install_agent.md
- Updating:
- "Updating the RMM": update_server.md
- "Updating the RMM (Docker)": update_docker.md
- "Updating Agents": update_agents.md
- Functionality:
- "Remote Background": functions/remote_bg.md
- "Maintenance Mode": functions/maintenance_mode.md
- "Alerting": alerting.md
- "User Interface Preferences": functions/user_ui.md
- "Django Admin": functions/django_admin.md
- Backup: backup.md
- Restore: restore.md
- Troubleshooting: troubleshooting.md
- FAQ: faq.md
- Management Commands: management_cmds.md
- MeshCentral Integration: mesh_integration.md
- Contributing: contributing.md
- License: license.md
site_description: "A remote monitoring and management tool"
site_author: "wh1te909"

3
go.mod
View File

@@ -4,9 +4,8 @@ go 1.16
require (
github.com/go-resty/resty/v2 v2.5.0
github.com/josephspurrier/goversioninfo v1.2.0
github.com/nats-io/nats.go v1.10.1-0.20210107160453-a133396829fc
github.com/ugorji/go/codec v1.2.4
github.com/wh1te909/rmmagent v1.4.6
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b // indirect
)

78
go.sum
View File

@@ -1,18 +1,5 @@
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/capnspacehook/taskmaster v0.0.0-20201022195506-c2d8b114cec0/go.mod h1:257CYs3Wd/CTlLQ3c72jKv+fFE2MV3WPNnV5jiroYUU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elastic/go-sysinfo v1.5.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-resty/resty/v2 v2.5.0 h1:WFb5bD49/85PO7WgAjZ+/TJQ+Ty1XOcWEfD1zIFCM1c=
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -20,19 +7,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/gonutz/w32 v1.0.1-0.20201105145118-e88c649a9470/go.mod h1:Rc/YP5K9gv0FW4p6X9qL3E7Y56lfMflEol1fLElfMW4=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iamacarpet/go-win64api v0.0.0-20200715182619-8cbc936e1a5a/go.mod h1:oGJx9dz0Ny7HC7U55RZ0Smd6N9p3hXP/+hOFtuYrAxM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA=
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
@@ -58,77 +35,33 @@ github.com/nats-io/nkeys v0.2.0 h1:WXKF7diOaPU9cJdLD7nuzwasQy9vT1tBqzXZZf3AMJM=
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs=
github.com/rickb777/date v1.15.3/go.mod h1:+spwdRnUrpqbYLOmRM6y8FbQMXwpNwHrNcWuOUipge4=
github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
github.com/shirou/gopsutil/v3 v3.21.1/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc=
github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4=
github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8=
github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA=
github.com/wh1te909/go-win64api v0.0.0-20201021040544-8fba2a0fc3d0/go.mod h1:cfD5/vNQFm5PD5Q32YYYBJ6VIs9etzp8CJ9dinUcpUA=
github.com/wh1te909/rmmagent v1.4.6 h1:6cHJQRGe0YCcPJwggPU7X9tlF6Cxn41OX4Vt4YADt0Y=
github.com/wh1te909/rmmagent v1.4.6/go.mod h1:qh346DIU177vsveCjMLjdrsMVvt0hFYQHC4uRYL7RLU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622182413-4b0db7f3f76b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b h1:HSSdksA3iHk8fuZz7C7+A6tDgtIRF+7FSXu5TgK09I8=
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -136,14 +69,3 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="40"
SCRIPT_VERSION="43"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
sudo apt install -y curl wget dirmngr gnupg lsb-release
@@ -185,9 +185,9 @@ print_green 'Installing golang'
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.16.linux-amd64.tar.gz -P ${go_tmp}
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.16.linux-amd64.tar.gz -C ${go_tmp}
tar -xzf ${go_tmp}/go1.16.2.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
@@ -195,11 +195,11 @@ rm -rf ${go_tmp}
print_green 'Downloading NATS'
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp}
wget https://github.com/nats-io/nats-server/releases/download/v2.2.0/nats-server-v2.2.0-linux-amd64.tar.gz -P ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.2.0-linux-amd64.tar.gz -C ${nats_tmp}
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
sudo mv ${nats_tmp}/nats-server-v2.2.0-linux-amd64/nats-server /usr/local/bin/
sudo chmod +x /usr/local/bin/nats-server
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
rm -rf ${nats_tmp}
@@ -216,6 +216,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
sudo npm install -g npm
print_green 'Installing MongoDB'
@@ -251,6 +252,10 @@ echo "$postgresql_repo" | sudo tee /etc/apt/sources.list.d/pgdg.list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-13
sleep 2
sudo systemctl enable postgresql
sudo systemctl restart postgresql
sleep 5
print_green 'Creating database for the rmm'
@@ -366,6 +371,7 @@ MESH_USERNAME = "${meshusername}"
MESH_SITE = "https://${meshdomain}"
REDIS_HOST = "localhost"
KEEP_SALT = False
ADMIN_ENABLED = True
EOF
)"
echo "${localvars}" > /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
@@ -406,6 +412,12 @@ python manage.py generate_barcode ${RANDBASE} ${djangousername} ${frontenddomain
deactivate
read -n 1 -s -r -p "Press any key to continue..."
uwsgiprocs=4
if [[ "$numprocs" == "1" ]]; then
uwsgiprocs=2
else
uwsgiprocs=$numprocs
fi
uwsgini="$(cat << EOF
[uwsgi]
@@ -413,8 +425,8 @@ chdir = /rmm/api/tacticalrmm
module = tacticalrmm.wsgi
home = /rmm/api/env
master = true
processes = 6
threads = 6
processes = ${uwsgiprocs}
threads = ${uwsgiprocs}
enable-threads = true
socket = /rmm/api/tacticalrmm/tacticalrmm.sock
harakiri = 300
@@ -423,7 +435,6 @@ buffer-size = 65535
vacuum = true
die-on-term = true
max-requests = 500
max-requests-delta = 1000
EOF
)"
echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini
@@ -805,6 +816,8 @@ python manage.py reload_nats
deactivate
sudo systemctl start nats.service
## disable django admin
sed -i 's/ADMIN_ENABLED = True/ADMIN_ENABLED = False/g' /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
print_green 'Restarting services'
for i in rmm.service celery.service celerybeat.service natsapi.service

View File

@@ -9,7 +9,7 @@ import (
"github.com/wh1te909/tacticalrmm/natsapi"
)
var version = "1.0.8"
var version = "1.1.0"
func main() {
ver := flag.Bool("version", false, "Prints version")

View File

@@ -6,14 +6,13 @@ import (
"fmt"
"log"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
nats "github.com/nats-io/nats.go"
"github.com/ugorji/go/codec"
rmm "github.com/wh1te909/rmmagent/shared"
)
var rClient = resty.New()
@@ -51,7 +50,7 @@ func Listen(apihost, natshost, version string, debug bool) {
log.Println("Nats connection url: ", natsurl)
rClient.SetHostURL(api)
rClient.SetTimeout(30 * time.Second)
rClient.SetTimeout(10 * time.Second)
natsinfo, err := rClient.R().SetResult(&NatsInfo{}).Get("/natsinfo/")
if err != nil {
log.Fatalln(err)
@@ -75,122 +74,9 @@ func Listen(apihost, natshost, version string, debug bool) {
log.Fatalln(err)
}
var wg sync.WaitGroup
wg.Add(1)
go getWMI(rClient, nc)
go monitorAgents(rClient, nc)
nc.Subscribe("*", func(msg *nats.Msg) {
var mh codec.MsgpackHandle
mh.RawToString = true
dec := codec.NewDecoderBytes(msg.Data, &mh)
switch msg.Reply {
case "hello":
go func() {
var p *rmm.CheckIn
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Patch("/checkin/")
}
}()
case "startup":
go func() {
var p *rmm.CheckIn
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Post("/checkin/")
}
}()
case "osinfo":
go func() {
var p *rmm.CheckInOS
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/checkin/")
}
}()
case "winservices":
go func() {
var p *rmm.CheckInWinServices
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/checkin/")
}
}()
case "publicip":
go func() {
var p *rmm.CheckInPublicIP
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/checkin/")
}
}()
case "disks":
go func() {
var p *rmm.CheckInDisk
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/checkin/")
}
}()
case "loggedonuser":
go func() {
var p *rmm.CheckInLoggedUser
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/checkin/")
}
}()
case "software":
go func() {
var p *rmm.CheckInSW
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/checkin/")
}
}()
case "syncmesh":
go func() {
var p *rmm.MeshNodeID
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Post("/syncmesh/")
}
}()
case "getwinupdates":
go func() {
var p *rmm.WinUpdateResult
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Post("/winupdates/")
}
}()
case "winupdateresult":
go func() {
var p *rmm.WinUpdateInstallResult
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Patch("/winupdates/")
}
}()
case "superseded":
go func() {
var p *rmm.SupersededUpdate
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Post("/superseded/")
}
}()
case "needsreboot":
go func() {
var p *rmm.AgentNeedsReboot
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Put("/winupdates/")
}
}()
case "chocoinstall":
go func() {
var p *rmm.ChocoInstalled
if err := dec.Decode(&p); err == nil {
rClient.R().SetBody(p).Post("/choco/")
}
}()
}
})
nc.Flush()
if err := nc.LastError(); err != nil {
fmt.Println(err)
os.Exit(1)
}
runtime.Goexit()
wg.Wait()
}

Binary file not shown.

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