Compare commits

...

365 Commits

Author SHA1 Message Date
wh1te909
bc6faf817f Release 0.7.0 2021-06-27 06:58:48 +00:00
wh1te909
d46ae55863 bump versions 2021-06-27 06:58:06 +00:00
wh1te909
bbd900ab25 move checkin to go 2021-06-27 06:23:37 +00:00
Dan
129ae93e2b Merge pull request #596 from rfost52/develop
Submitting System Report Generator to Community Scripts
2021-06-26 21:58:23 -07:00
rfost52
44dd59fa3f Merge branch 'develop' of https://github.com/rfost52/tacticalrmm into develop 2021-06-26 22:31:00 -04:00
rfost52
ec4e7559b0 updated script header 2021-06-26 22:30:52 -04:00
rfost52
dce40611cf Merge branch 'wh1te909:develop' into develop 2021-06-26 22:17:31 -04:00
rfost52
e71b8546f9 Submitting System Report Generator to Community Scripts 2021-06-26 22:09:56 -04:00
wh1te909
f827348467 style changes 2021-06-27 01:15:47 +00:00
wh1te909
f3978343db cache some values to speed up agent table loading 2021-06-27 00:51:34 +00:00
wh1te909
2654a7ea70 remove extra param 2021-06-27 00:05:00 +00:00
wh1te909
1068bf4ef7 fix row highlight 2021-06-26 17:53:06 +00:00
Dan
e7fccc97cc Merge pull request #595 from rfost52/develop
Initial Parameterization of System Report WIP Script
2021-06-25 23:57:11 -07:00
Dan
733e289852 Merge pull request #592 from silversword411/develop
Docs tweaks
2021-06-25 23:56:44 -07:00
rfost52
29d71a104c include check for C:\Temp folder 2021-06-25 00:36:16 -04:00
rfost52
05200420ad Merge branch 'develop' of https://github.com/rfost52/tacticalrmm into develop 2021-06-24 23:53:26 -04:00
rfost52
eb762d4bfd Initial Parameterization of variables 2021-06-24 23:53:06 -04:00
silversword411
58ace9eda1 Adding wip scripts 2021-06-24 17:20:49 -04:00
sadnub
eeb2623be0 Merge pull request #516 from sadnub/quasar-update
Quasar update to v2
2021-06-24 13:48:47 -04:00
sadnub
cfa242c2fe update loading bar delay 2021-06-24 13:41:34 -04:00
sadnub
ec0441ccc2 fix collector dropdown in policy task edit 2021-06-24 13:41:34 -04:00
sadnub
ae2782a8fe update quasar to v2 release 2021-06-24 13:41:34 -04:00
sadnub
58ff570251 fix assets tab 2021-06-24 13:41:34 -04:00
sadnub
7b554b12c7 update packages 2021-06-24 13:41:34 -04:00
sadnub
58f7603d4f fix agent drowndown in audit manager 2021-06-24 13:41:34 -04:00
sadnub
8895994c54 update packages 2021-06-24 13:41:34 -04:00
sadnub
de8f7e36d5 fix q-checkboxes that need to trigger actions and replace @input with @update:model-value 2021-06-24 13:41:34 -04:00
sadnub
88d7a50265 refactor user administration without vuex 2021-06-24 13:41:34 -04:00
sadnub
21e19fc7e5 add keys back to v-fors 2021-06-24 13:41:34 -04:00
sadnub
faf4935a69 fix saving custom field values and change sites dropdown in edit agent modal 2021-06-24 13:41:34 -04:00
sadnub
71a1f9d74a update reqs and fix custom field values 2021-06-24 13:41:34 -04:00
sadnub
bd8d523e10 stop blinking when loading 2021-06-24 13:41:34 -04:00
sadnub
60cae0e3ac remove 'created' hooks from components and fix agent and script optino dropdowns 2021-06-24 13:41:34 -04:00
sadnub
5a342ac012 removed key from v-for. Fixed custom dropdowns. other fixes 2021-06-24 13:41:34 -04:00
sadnub
bb8767dfc3 fix darkmode and policy check and task tables 2021-06-24 13:41:34 -04:00
sadnub
fcb2779c15 update quasar 2021-06-24 13:41:34 -04:00
sadnub
77dd6c1f61 more fixes 2021-06-24 13:41:34 -04:00
sadnub
8118eef300 upgrade to quasar v2 and vue3 initial 2021-06-24 13:41:34 -04:00
silversword411
802d1489fe adding to howitallworks 2021-06-24 02:42:41 -04:00
silversword411
443a029185 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-24 02:00:51 -04:00
silversword411
4ee508fdd0 Docs tweaks 2021-06-24 01:55:50 -04:00
wh1te909
aa5608f7e8 fix custom field args in bulk script fixes #591 2021-06-24 01:34:14 +00:00
wh1te909
cc472b4613 update celery 2021-06-24 01:32:07 +00:00
wh1te909
764b945ddc fix pipelines 2 2021-06-22 06:51:44 +00:00
wh1te909
fd2206ce4c fix pipelines 2021-06-22 06:47:17 +00:00
Dan
48c0ac9f00 Merge pull request #588 from rfost52/develop
Moving Win_AD_Join_Computer.ps1 from WIP scripts to Community Scripts
2021-06-21 23:38:18 -07:00
silversword411
84eb4fe9ed Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-21 11:35:04 -04:00
silversword411
4a5428812c Docs tweaks 2021-06-21 11:34:10 -04:00
silversword411
023f98a89d Docs tweaks 2021-06-21 11:32:56 -04:00
rfost52
66893dd0c1 Update Win_AD_Join_Computer.ps1 2021-06-19 20:50:56 -04:00
rfost52
25a6666e35 Adding AD PC Join to Listings 2021-06-19 20:47:11 -04:00
rfost52
19d75309b5 Merge branch 'develop' of https://github.com/rfost52/tacticalrmm into develop 2021-06-19 20:21:21 -04:00
rfost52
11110d65c1 Adding to Community Scripts
Moving from WIP Scripts to Community Scripts after successful testing.
2021-06-19 20:21:11 -04:00
Dan
a348f58fe2 Merge pull request #585 from rfost52/develop
First rework of Join to AD PowerShell WIP Script
2021-06-19 11:41:52 -07:00
rfost52
13851dd976 Added new line at end of code 2021-06-18 23:25:15 -04:00
rfost52
2ec37c5da9 1st Code rework with parameterization 2021-06-18 22:57:23 -04:00
rfost52
8c127160de Updated synopsis and description 2021-06-18 22:51:21 -04:00
rfost52
2af820de9a Update Win_AD_Join_Computer.ps1
Parameters, error checking with exit codes
2021-06-18 22:43:26 -04:00
Dan
55fb0bb3a0 Merge pull request #584 from silversword411/develop
community script updates
2021-06-18 10:58:00 -07:00
silversword411
9f9ecc521f community script updates 2021-06-17 15:27:40 -04:00
Dan
dfd01df5ba Merge pull request #581 from silversword411/develop
Adding docs
2021-06-16 22:55:18 -07:00
silversword411
474090698c Merge branch 'wh1te909:develop' into develop 2021-06-17 01:00:40 -04:00
silversword411
6b71cdeea4 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-17 00:53:58 -04:00
wh1te909
581e974236 add view setting perms closes #569 2021-06-17 04:36:34 +00:00
wh1te909
ba3c3a42ce add missing mypy types 2021-06-17 04:35:51 +00:00
silversword411
c8bc5671c5 adding all possible script variables to docs 2021-06-17 00:34:11 -04:00
wh1te909
ff9401a040 make failing tasks fail client tree closes #571 2021-06-17 03:51:20 +00:00
wh1te909
5e1bc1989f update reqs 2021-06-17 03:50:00 +00:00
wh1te909
a1dc91cd7d fix typo in docs #580 2021-06-16 16:46:24 +00:00
sadnub
99f2772bb3 Fixes #577 2021-06-14 20:27:41 -04:00
sadnub
e5d0e42655 fix agent policies not updating when monitoring mode is changed 2021-06-14 20:18:56 -04:00
Dan
2c914cc374 Merge pull request #576 from bradhawkins85/patch-19
Update installer.ps1
2021-06-14 09:45:13 -07:00
Dan
9bceb62381 Merge pull request #575 from nextgi/zak-develop
Updates to Devcontainer and Added #467
2021-06-14 09:44:58 -07:00
Zak
de7518a800 Added new community script
New script for auto documenting ADDS.
2021-06-13 17:56:44 -07:00
bradhawkins85
304fb63453 Update installer.ps1
Fix spelling errors
2021-06-13 17:22:13 +10:00
Zak
0f7ef60ca0 Added #467
Added QTooltip to the label of the QItem in the QTree.
2021-06-12 20:50:59 -07:00
Zak
07c74e4641 Updated devcontainer
Prior it was statically set to use a specific range of IPs. I changed this so it could be set via environment variables. Also, NATS port 4222 is a reserved port for Hyper-V. I updated this so it could be set in env variables as well.
2021-06-12 20:49:10 -07:00
wh1te909
de7f325cfb fix redis appendonly backup/restore 2021-06-13 00:10:58 +00:00
wh1te909
42cdf70cb4 Release 0.6.15 2021-06-12 20:41:19 +00:00
wh1te909
6beb6be131 bump version 2021-06-12 20:40:54 +00:00
wh1te909
fa4fc2a708 only parse script args for script checks 2021-06-12 20:24:51 +00:00
wh1te909
2db9758260 fix custom fields in script checks #568 2021-06-12 19:41:49 +00:00
wh1te909
715982e40a Release 0.6.14 2021-06-11 04:41:48 +00:00
wh1te909
d00cd4453a bump versions 2021-06-11 04:40:57 +00:00
wh1te909
429c08c24a fix width on q-file caused by recent quasar update 2021-06-11 03:58:57 +00:00
wh1te909
6a71490e20 update reqs 2021-06-11 02:40:22 +00:00
Dan
9bceda0646 Merge pull request #562 from diekinderwelt/nginx_enable_ipv6
enable ipv6 in nginx config
2021-06-10 18:59:34 -07:00
Dan
a1027a6773 Merge pull request #565 from silversword411/develop
Docs Update - adding design and tipsntricks
2021-06-10 18:59:12 -07:00
silversword411
302d4b75f9 formatting fix 2021-06-08 15:39:43 -04:00
silversword411
5f6ee0e883 Docs Update - adding design and tipsntricks 2021-06-08 14:45:02 -04:00
Silvio
27f9720de1 enable ipv6 in nginx config
Signed-off-by: Silvio <silvio.zimmer@die-kinderwelt.com>
2021-06-08 11:43:55 +02:00
sadnub
22aa3fdbbc fix bug with policy copy and task that triggers on check failure. Fix check history tests 2021-06-06 23:19:07 -04:00
sadnub
069ecdd33f apply redis configuration after restore 2021-06-06 22:58:32 -04:00
sadnub
dd545ae933 catch an exception that a celery task could potentially throw and configure automation task retries 2021-06-06 22:55:47 -04:00
sadnub
6650b705c4 configure redis to use an appendonly file for celery task reliability 2021-06-06 22:54:52 -04:00
sadnub
59b0350289 fix duplicate tasks when there is an assigned check 2021-06-06 22:54:06 -04:00
sadnub
1ad159f820 remove foreign key from checkhistory to make mass check deletes reliable. (This will not migrate check history data) 2021-06-06 22:53:11 -04:00
Dan
0bf42190e9 Merge pull request #544 from bbrendon/patch-1
check for proper OS support
2021-05-30 23:10:21 -07:00
bbrendon
d2fa836232 check for proper OS support 2021-05-30 10:39:08 -07:00
Dan
c387774093 Merge pull request #543 from bbrendon/develop
fixed an edge case and warning notes
2021-05-29 22:39:52 -07:00
bbrendon
e99736ba3c fixed an edge case and warning notes 2021-05-29 19:25:53 -07:00
wh1te909
16cb54fcc9 fix multiline output not working for automation task 2021-05-29 18:47:09 +00:00
wh1te909
5aa15c51ec Release 0.6.13 2021-05-29 07:35:29 +00:00
wh1te909
a8aedd9cf3 bump version 2021-05-29 07:35:10 +00:00
wh1te909
b851b632bc fix agent_outages_task async error 2021-05-29 07:26:10 +00:00
wh1te909
541e07fb65 Release 0.6.12 2021-05-29 05:16:37 +00:00
wh1te909
6ad16a897d bump versions 2021-05-29 05:15:26 +00:00
wh1te909
72f1053a93 change interval 2021-05-29 04:49:17 +00:00
sadnub
fb15a2762c allow saving multiple script output in custom fields #533 2021-05-28 23:52:23 -04:00
wh1te909
9165248b91 update go/codec 2021-05-29 03:20:12 +00:00
sadnub
add18b29db fix agent dropdown 2021-05-28 22:59:44 -04:00
wh1te909
1971653548 bump nats/mesh 2021-05-29 02:53:16 +00:00
wh1te909
392cd64d7b hide settings in hosted 2021-05-29 02:20:07 +00:00
wh1te909
b5affbb7c8 change function name 2021-05-29 02:18:57 +00:00
wh1te909
71d1206277 more checks rework 2021-05-29 01:37:20 +00:00
wh1te909
26e6a8c409 update reqs 2021-05-28 18:12:32 +00:00
wh1te909
eb54fae11a more checks rework 2021-05-28 17:54:57 +00:00
wh1te909
ee773e5966 remove deprecated func 2021-05-28 17:54:14 +00:00
wh1te909
7218ccdba8 start checks rework 2021-05-27 07:16:06 +00:00
wh1te909
332400e48a autogrow text field fixes #533 2021-05-27 07:09:40 +00:00
Dan
ad1a5d3702 Merge pull request #534 from silversword411/develop
Script library and docs tweaks
2021-05-26 23:59:08 -07:00
silversword411
3006b4184d Docs update on regular patching 2021-05-26 21:36:28 -04:00
silversword411
84eb84a080 Script library adding comments 2021-05-26 10:19:30 -04:00
sadnub
60beea548b Allow clearing resolved/failure actions in alert template 2021-05-24 22:18:12 -04:00
Dan
5f9c149e59 Merge pull request #528 from bbrendon/develop
updated timeouts and fixed one script
2021-05-21 18:36:07 -07:00
bbrendon
53367c6f04 update timeouts on some scripts 2021-05-21 18:01:16 -07:00
bbrendon
d7f817ee44 syntax error fix. 2021-05-21 17:56:53 -07:00
Dan
d33a87da54 Merge pull request #526 from silversword411/develop
script library - Screenconnect collector
2021-05-20 20:13:51 -07:00
silversword411
3aebfb12b7 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-20 21:50:10 -04:00
silversword411
1d6c55ffa6 Script library - screenconnect collector 2021-05-20 21:49:01 -04:00
Dan
5e7080aac3 Merge pull request #522 from silversword411/develop
Docs Example and wip tweaks
2021-05-20 18:37:33 -07:00
silversword411
fad739bc01 Updating script delegated folders 2021-05-20 10:10:59 -04:00
silversword411
c6b7f23884 Adding URL Action Example to docs 2021-05-19 02:46:51 -04:00
silversword411
a6f7e446de tweaking wip scripts 2021-05-18 23:22:45 -04:00
wh1te909
89d95d3ae1 Release 0.6.11 2021-05-19 03:08:29 +00:00
wh1te909
764208698f bump version 2021-05-19 03:04:06 +00:00
Dan
57129cf934 Merge pull request #521 from agit8or/develop
Create Win_Shortcut_Creator.ps1
2021-05-18 18:10:33 -07:00
Dan
aae1a842d5 Merge pull request #519 from silversword411/develop
add script to wip
2021-05-18 18:10:03 -07:00
agit8or
623f35aec7 Create Win_Shortcut_Creator2.ps1 2021-05-18 13:05:46 -04:00
agit8or
870bf842cf Create Win_Shortcut_Creator.ps1 2021-05-18 13:00:26 -04:00
silversword411
07f2d7dd5c wip additions for printers 2021-05-18 02:00:55 -04:00
silversword411
f223f2edc5 Merge branch 'wh1te909:develop' into develop 2021-05-17 22:47:22 -04:00
wh1te909
e848a9a577 fix tests 2021-05-17 06:45:43 +00:00
wh1te909
7569d98e07 fix task args fixes #514 2021-05-17 06:01:28 +00:00
wh1te909
596dee2f24 update docs 2021-05-15 08:07:30 +00:00
wh1te909
9970403964 Release 0.6.10 2021-05-15 07:52:35 +00:00
wh1te909
07a88ae00d bump versions 2021-05-15 07:51:44 +00:00
wh1te909
5475b4d287 typo 2021-05-15 02:20:33 +00:00
sadnub
6631dcfd3e Fix custom check run interval. Fixes #473 2021-05-14 21:37:49 -04:00
sadnub
0dd3f337f3 Add Client and Site categories for agent select options. Fixes #499 2021-05-14 20:27:32 -04:00
silversword411
8eb27b5875 Merge branch 'wh1te909:develop' into develop 2021-05-14 19:03:42 -04:00
sadnub
2d1863031c fix default custom field value not being used if blank value is present on model. Fixes #501 2021-05-14 18:48:49 -04:00
sadnub
9feb76ca81 fix tests 2021-05-14 18:19:57 -04:00
sadnub
993e8f4ab3 sort script categories prior to formating script options #506 2021-05-14 18:08:51 -04:00
sadnub
e08ae95d4f Fix alignment issue #512 2021-05-14 18:08:51 -04:00
sadnub
15359e8846 ws wip 2021-05-14 18:08:51 -04:00
silversword411
d1457b312b wip addition create shortcut to URL 2021-05-14 17:50:50 -04:00
silversword411
c9dd2af196 Merge branch 'wh1te909:develop' into develop 2021-05-14 14:41:12 -04:00
wh1te909
564ef4e688 feat: add clear faults #484 2021-05-14 04:54:59 +00:00
wh1te909
a33e6e8bb5 move token refresh before local settings import to allow overriding #503 2021-05-14 01:47:25 +00:00
Dan
cf34f33f04 Merge pull request #507 from silversword411/develop
Script library and docs updates
2021-05-13 12:50:21 -07:00
silversword411
827cfe4e8f Merge branch 'wh1te909:develop' into develop 2021-05-13 13:44:45 -04:00
silversword411
2ce1c2383c Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-13 13:38:51 -04:00
silversword411
6fc0a665ae script library docs - volunteers needed 2021-05-13 13:36:33 -04:00
silversword411
4f16d01263 script library - sn collector 2021-05-13 12:37:10 -04:00
sadnub
67cc37354a Evaluate policies on exclusion changes. Fixes #500 2021-05-12 18:17:03 -04:00
silversword411
e388243ef4 renaming wips 2021-05-12 11:32:16 -04:00
silversword411
3dc92763c7 Script library add 2021-05-12 11:25:22 -04:00
Dan
dfe97dd466 Merge pull request #493 from silversword411/develop
Adding comment headers to wip1
2021-05-12 00:36:57 -07:00
wh1te909
2803cee29b Release 0.6.9 2021-05-12 07:08:41 +00:00
wh1te909
3a03020e54 bump versions 2021-05-12 07:07:51 +00:00
wh1te909
64443cc703 fix link 2021-05-12 06:46:51 +00:00
wh1te909
4d1aa6ed18 fix 404 2021-05-12 06:29:36 +00:00
wh1te909
84837e88d2 update reqs 2021-05-12 05:53:09 +00:00
wh1te909
ff49c936ea fix tests 2021-05-12 05:52:36 +00:00
wh1te909
e6e0901329 add optional installer arg for custom mesh dir #487 2021-05-12 03:32:03 +00:00
silversword411
23b6284b51 Adding comment headers to wip2 2021-05-11 22:55:01 -04:00
silversword411
33dfbcbe32 Adding comment headers to wip1 2021-05-11 22:53:37 -04:00
wh1te909
700c23d537 fix sorting #491 2021-05-12 02:28:00 +00:00
wh1te909
369fac9e38 clear search when switching client tree #492 2021-05-12 01:43:11 +00:00
wh1te909
2229eb1167 add role perms 2021-05-11 17:42:43 +00:00
wh1te909
a3dec841b6 get more accurate model for lenovo #490 2021-05-11 17:15:21 +00:00
wh1te909
b17620bdb6 refactor perms into roles 2021-05-11 07:10:18 +00:00
sadnub
f39cd5ae2f make the policy automated tasks check assignment work correctly and add tests 2021-05-10 20:35:38 -04:00
sadnub
83a19e005b exclude autotask creation on agent when policy is being copied 2021-05-10 18:21:25 -04:00
sadnub
a9dd01b0c8 rework alert template form into a stepper. Add better docs for Alert Templates 2021-05-08 23:40:09 -04:00
wh1te909
eb59afa1d1 isort 2021-05-08 17:28:29 +00:00
wh1te909
2adcfce9d0 fix tests 2021-05-08 17:27:01 +00:00
wh1te909
314ab9b304 fix migrations 2021-05-08 17:16:43 +00:00
wh1te909
8576fb82c7 merge permissions 2021-05-08 17:05:52 +00:00
wh1te909
0f95a6bb2f add permissions #162 2021-05-08 17:02:23 +00:00
sadnub
ad5104567d formatting 2021-05-07 18:03:08 -04:00
sadnub
ece68ba1d5 remove import 2021-05-07 17:58:50 -04:00
sadnub
acccd3a586 add url action docs 2021-05-07 17:53:55 -04:00
sadnub
8ebef1c1ca fix editing error in preferences 2021-05-07 12:12:58 -04:00
sadnub
28abc0d5ed allow setting a url action as agent dblclick action 2021-05-07 11:45:55 -04:00
sadnub
1efe25d3ec finish url actions with tests 2021-05-07 10:22:37 -04:00
sadnub
c40e4f8e4b url actions ui 2021-05-07 10:22:37 -04:00
Dan
baca84092d Merge pull request #479 from silversword411/develop
Updating docs - unsupported scripts
2021-05-06 10:06:37 -07:00
silversword411
346d4da059 Updating docs - unsupported scripts 2021-05-05 16:23:05 -04:00
wh1te909
ade64d6c0a Release 0.6.8 2021-05-05 17:07:19 +00:00
wh1te909
8204bdfc5f bump versions 2021-05-05 17:06:57 +00:00
wh1te909
1a9bb3e986 fix update script 2021-05-05 07:59:23 +00:00
wh1te909
49356479e5 fix update script 2021-05-05 07:58:30 +00:00
wh1te909
c44e9a7292 Release 0.6.7 2021-05-05 07:27:54 +00:00
wh1te909
21771a593f bump versions 2021-05-05 07:25:59 +00:00
wh1te909
84458dfc4c add agent proxy docs 2021-05-05 06:55:48 +00:00
wh1te909
5835632dab add button to force code signing 2021-05-05 06:50:25 +00:00
Dan
67aa7229ef Merge pull request #475 from silversword411/develop
Adding docs regarding HAProxy
2021-05-04 20:23:55 -07:00
silversword411
b72dc3ed3a Adding docs regarding HAProxy 2021-05-04 22:57:33 -04:00
wh1te909
0f93d4a5bd improve wording 2021-05-05 02:18:21 +00:00
wh1te909
106320b035 nats 2.2.2 2021-05-05 02:04:03 +00:00
wh1te909
63951705cd update reqs 2021-05-05 02:03:11 +00:00
Dan
a8d56921d5 Merge pull request #472 from silversword411/develop
Tweaking patches pane
2021-05-04 19:01:32 -07:00
sadnub
10bc133cf1 fix other checks getting deleted when deleting a policy check 2021-05-04 20:01:44 -04:00
silversword411
adeb5b35c9 Tweaking patches pane
Co-authored-by: sadnub <sadnub@users.noreply.github.com> using Live Share
2021-05-04 15:43:40 -04:00
Dan
589ff46ea5 Merge pull request #471 from silversword411/develop
script library addition
2021-05-04 11:07:11 -07:00
silversword411
656fcb9fe7 script library - adding tcp reset script 2021-05-04 13:18:43 -04:00
silversword411
1cb9353006 Revert "script library - adding tcp reset script"
This reverts commit 659846ed88.
2021-05-04 13:16:07 -04:00
silversword411
57bf16ba07 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-04 10:03:48 -04:00
silversword411
659846ed88 script library - adding tcp reset script 2021-05-04 10:02:58 -04:00
silversword411
25894044e0 script library - adding outlook delegated folders 2021-05-04 10:02:58 -04:00
silversword411
e7a0826beb tweaking script docs 2021-05-04 10:02:57 -04:00
silversword411
1f7ddee23b script library - adding tcp reset script 2021-05-04 10:02:21 -04:00
Dan
7e186730db Merge pull request #470 from bradhawkins85/patch-17
Update Win_ScreenConnectAIO.ps1
2021-05-03 23:51:03 -07:00
Dan
6713a50208 Merge branch 'develop' into patch-17 2021-05-03 23:50:54 -07:00
Dan
7c9d8fcfec Merge pull request #469 from bradhawkins85/patch-18
Update community_scripts.json
2021-05-03 23:49:45 -07:00
Dan
33bfc8cfe8 Merge pull request #466 from InsaneTechnologies/develop
Add in Client and Site variables
2021-05-03 23:49:35 -07:00
wh1te909
ca735bc14a fix ui for custom fields with very long text 2021-05-04 06:47:53 +00:00
bradhawkins85
4ba748a18b Update community_scripts.json
Add variables to include client name and site name to install in correct groups in ScreenConnect
2021-05-04 16:19:44 +10:00
bradhawkins85
f1845106f8 Update Win_ScreenConnectAIO.ps1
Include client name and site name in URL to add agent to correct group in ScreenConnect
2021-05-04 16:17:52 +10:00
David Rudduck
67e7156c4b Create Alert_MSTeams.ps1
Very raw MS Teams alert script
2021-05-04 11:47:09 +10:00
silversword411
4a476adebf Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-03 18:22:33 -04:00
silversword411
918798f8cc script library - adding outlook delegated folders 2021-05-03 18:20:38 -04:00
silversword411
5a3f868866 tweaking script docs 2021-05-03 18:05:27 -04:00
silversword411
feea2c6396 tweaking script docs 2021-05-03 14:15:21 -04:00
Dan
707b4c46d9 Merge pull request #464 from silversword411/develop
tweaking docs and adding scripts
2021-05-03 07:56:01 -07:00
David Rudduck
89ca39fc2b Update Win_ScreenConnectAIO.ps1 2021-05-03 11:31:49 +10:00
David Rudduck
204281b12d Merge pull request #1 from InsaneTechnologies/scripts-screenconnect-1-1
Update Win_ScreenConnectAIO.ps1
2021-05-03 11:30:30 +10:00
David Rudduck
a8538a7e95 Update Win_ScreenConnectAIO.ps1
added support for `-company {{client.name}} -site {{site.name}}` command line arguments. 

This results in ScreenConnect adding those fields to the agent so it's easier to filter down.
2021-05-03 11:29:48 +10:00
silversword411
dee1b471e9 tweaking script docs 2021-05-02 20:03:09 -04:00
silversword411
aa04e9b01f Script - display message to user tweak 2021-05-02 11:54:51 -04:00
silversword411
350f0dc604 Standardized Comments for scripts 2021-05-02 11:52:47 -04:00
silversword411
6021f2efd6 Add wip script 2021-05-02 11:42:00 -04:00
wh1te909
51838ec25a retry uninstall a few times 2021-05-02 08:45:19 +00:00
wh1te909
54768a121e add exact datetime of next agent update cycle in pending actions #457 2021-05-01 07:11:12 +00:00
wh1te909
8ff72cdca3 fix cors exception msg 2021-05-01 06:20:51 +00:00
sadnub
2cb53ad06b error handling and axios changes 2021-04-30 18:35:56 -04:00
sadnub
b8349de31d add additional check in delete policy task test 2021-04-30 18:35:56 -04:00
wh1te909
d7e11af7f8 fix speedtest.py 2021-04-30 07:18:13 +00:00
wh1te909
dd8d39e698 Release 0.6.6 2021-04-30 07:05:04 +00:00
wh1te909
afb1316daa bump versions 2021-04-30 07:01:22 +00:00
wh1te909
04d7017536 rework ping checks #444 2021-04-30 06:32:21 +00:00
wh1te909
6a1c75b060 add help toolbar #452 2021-04-30 06:01:22 +00:00
Dan
5c94611f3b Merge pull request #456 from silversword411/develop
WIP it, WIP it good: and script library stuff
2021-04-29 18:08:07 -07:00
silversword411
4e5676e80f adding the wip 2021-04-29 11:45:32 -04:00
wh1te909
c96d688a9c add alert if new trmm version available #453 2021-04-29 08:12:44 +00:00
silversword411
804242e9a5 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-04-28 22:50:47 -04:00
silversword411
0ec9760b17 Adding to docker 2021-04-28 22:49:49 -04:00
Dan
d481ae3da4 Merge pull request #443 from bradhawkins85/patch-16
Update Win_ScreenConnectAIO.ps1
2021-04-28 09:04:43 -07:00
silversword411
4742c14fc1 Rename temp script 2021-04-28 11:12:18 -04:00
bradhawkins85
509b0d501b Update Win_ScreenConnectAIO.ps1
Updated script notes regarding quoting around variables.
2021-04-28 10:10:18 +10:00
silversword411
d4c9b04d4e Hidden Script Library todo list 2021-04-27 13:11:30 -04:00
silversword411
16fb4d331b script library adding msi install ref script 2021-04-27 13:07:14 -04:00
silversword411
e9e5bf31a7 script library adding file copy script 2021-04-27 12:50:01 -04:00
wh1te909
221418120e Release 0.6.5 2021-04-27 16:20:25 +00:00
wh1te909
46f852e26e bump version 2021-04-27 16:20:08 +00:00
sadnub
4234cf0a31 fix policy task deletion 2021-04-27 12:12:04 -04:00
wh1te909
7f3daea648 Release 0.6.4 2021-04-27 15:36:49 +00:00
wh1te909
2eb16c82f4 bump version 2021-04-27 15:36:38 +00:00
sadnub
e00b2ce591 add test for check deletes 2021-04-27 11:04:06 -04:00
sadnub
d71e1311ca fix deleting checks 2021-04-27 10:58:23 -04:00
sadnub
2cf16963e3 fix custom fields on policy tasks 2021-04-27 10:51:29 -04:00
wh1te909
10bf7b7fb4 update restore docs 2021-04-27 06:18:15 +00:00
wh1te909
182c85a228 Release 0.6.3 2021-04-27 06:02:33 +00:00
wh1te909
94b1988b90 don't make description a required field in edit agent model 2021-04-27 06:00:42 +00:00
wh1te909
6f7e62e9a0 remove alpha status 2021-04-27 05:39:52 +00:00
wh1te909
aa7076af04 bump versions 2021-04-27 05:05:57 +00:00
Dan
c928e8f0d4 Merge pull request #436 from silversword411/develop
Updating management commands
2021-04-26 21:04:08 -07:00
sadnub
5c6b106f68 adding docs for Custom Fields, Scripting, Collector Tasks, and KeyStore 2021-04-26 23:16:10 -04:00
sadnub
d45bcea1ff add mkdocs container to docker dev env 2021-04-26 23:16:10 -04:00
wh1te909
6ff2dc79f8 black 2021-04-27 02:31:33 +00:00
silversword411
b752329987 Adding standardized header comments and example 2021-04-26 20:59:39 -04:00
silversword411
f21465335a clarifying vscode instructions 2021-04-26 20:47:23 -04:00
silversword411
0801adfc4b community script consolidating Defender status reports script 2021-04-26 17:45:56 -04:00
silversword411
5bee8052d5 Fix client site 2021-04-25 23:05:03 -04:00
silversword411
68dca5dfef Updating managment commands 2021-04-25 22:56:56 -04:00
Dan
3f51dd1d2f Merge pull request #435 from silversword411/develop
Docs and tips update
2021-04-25 00:20:05 -07:00
Dan
7f80889d77 Merge pull request #422 from sadnub/develop
Policy rework, global keystore, and collector tasks
2021-04-24 23:57:12 -07:00
sadnub
efc61c0222 fix tests 2021-04-24 22:13:02 -04:00
sadnub
6fc0a05d34 allow adding {{alert.property_name}} to resolved and failure alert scripts 2021-04-24 21:59:41 -04:00
sadnub
a9be872d7a make automated task tables sortable #431 2021-04-24 21:25:55 -04:00
sadnub
6ca85f099e fix autotask modals and allow editing the custom field for a collector task 2021-04-24 21:21:37 -04:00
sadnub
86ff677b8a fix styling 2021-04-24 21:06:17 -04:00
sadnub
35e295df86 implement keystore in script substitution with {{global.name}}. Also fixed issue with space in value. 2021-04-24 21:01:55 -04:00
sadnub
cd4d301790 keystore tests 2021-04-24 20:43:11 -04:00
sadnub
93bb329c3d add frontend end and backend for keystore 2021-04-24 20:36:21 -04:00
silversword411
7c1e0f2c30 More hidden dev docs 2021-04-24 20:06:30 -04:00
sadnub
b57f471f44 add ability to hide custom fields in UI if strictly for script usage 2021-04-24 20:01:28 -04:00
sadnub
252a9a2ed6 implement the rest of collector tasks and add tests 2021-04-24 17:40:44 -04:00
sadnub
7258d4d787 add block inheritance tests and fixes 2021-04-24 15:59:04 -04:00
sadnub
75522fa295 implement policy inheritance blocking 2021-04-24 10:07:37 -04:00
sadnub
4ba8f41d95 fixing tests 2021-04-24 10:07:37 -04:00
sadnub
f326f8e4de policy task and check rework. Added basic collector task implementation 2021-04-24 10:07:37 -04:00
sadnub
f863dc058e add UI for blocking policy inheritance on client, site, and agent 2021-04-24 10:07:37 -04:00
silversword411
20891db251 Tooltip on run interval 2021-04-24 08:52:55 -04:00
silversword411
f1d05f1342 Adding extra optional command line args to dialog 2021-04-23 16:08:16 -04:00
wh1te909
8dd636b0eb Release 0.6.2 2021-04-23 06:40:31 +00:00
wh1te909
6b5bda8ee1 bump versions 2021-04-23 06:12:19 +00:00
Dan
ddc5597157 Merge pull request #421 from silversword411/develop
script library and docs tweaks
2021-04-22 19:08:33 -07:00
silversword411
ae112c7257 script library tweaks 2021-04-21 11:14:45 -04:00
silversword411
c22f10f96a Adding notes to vscode docs 2021-04-21 11:06:40 -04:00
silversword411
18d10c9bec power restart script tweaks 2021-04-21 09:40:07 -04:00
silversword411
890e430cb7 script library - merging scripts and parameterizing 2021-04-21 09:29:25 -04:00
wh1te909
dadc3d4cd7 add #418 2021-04-21 05:09:55 +00:00
Dan
d98b4d7320 Merge pull request #417 from silversword411/develop
tweaks to docs and scripts
2021-04-19 23:05:42 -07:00
silversword411
340f532238 tweaks to docs and scripts 2021-04-20 06:00:03 +00:00
wh1te909
7669f68e7c add code signing to docs 2021-04-20 05:54:24 +00:00
Dan
3557e5514f Merge pull request #416 from silversword411/develop
docs tweaks
2021-04-19 22:50:58 -07:00
silversword411
a9f09b7614 knew there was a bold somewhere 2021-04-20 05:15:24 +00:00
silversword411
845b9e4568 docs tweaks 2021-04-20 05:12:58 +00:00
Dan
24a6092dcf Merge pull request #415 from silversword411/develop
Community Script Library Docs v1
2021-04-19 21:36:37 -07:00
wh1te909
195ae7d8b1 add conditional menu render 2021-04-20 04:35:07 +00:00
silversword411
a5c6ea7ffc Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-04-20 00:32:47 -04:00
silversword411
eb7a4ac29f Script library - more cleaning 2021-04-20 00:32:35 -04:00
silversword411
508ef73fde Contributing Community Scripts v1 2021-04-20 00:32:35 -04:00
silversword411
838d6d8076 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-04-20 00:26:25 -04:00
silversword411
762c3159b8 Script library - more cleaning 2021-04-20 00:26:20 -04:00
wh1te909
7a88a06bcf isort 2021-04-20 04:14:20 +00:00
wh1te909
0b1e3d7de5 start fixing #409 2021-04-20 04:11:48 +00:00
silversword411
9a83c73f21 Contributing Community Scripts v1 2021-04-20 04:11:15 +00:00
Dan
aa50c7b268 Merge pull request #414 from silversword411/develop
adding agent remove/add to docs
2021-04-19 20:33:53 -07:00
silversword411
179a5a80f4 Fixing Defender GUID 2021-04-19 23:06:35 -04:00
silversword411
0ddae527ef Script Library - Renaming files to follow best practices 2021-04-19 23:02:42 -04:00
silversword411
ee7a46de26 Script library - defender status tweaks 2021-04-19 22:28:51 -04:00
silversword411
95522fda74 script library - Adding set DNS script 2021-04-19 18:38:22 -04:00
silversword411
e58881c2bd script library Set Ethernet to use DHCP 2021-04-19 18:32:36 -04:00
silversword411
36a902a44e script rename and tweaks 2021-04-19 17:11:53 -04:00
silversword411
16b74549a2 adding agent remove/add to docs 2021-04-19 11:45:55 +00:00
wh1te909
da7ededfb1 fix sorting #402 2021-04-17 20:06:07 +00:00
wh1te909
790bb08718 fix check status in summary tab 2021-04-17 19:56:10 +00:00
Dan
e6765f421f Merge pull request #408 from silversword411/develop
Tooltip update
2021-04-17 12:54:36 -07:00
silversword411
7e8f1fe904 Tooltip update 2021-04-17 02:09:06 -04:00
Dan
eacce4578a Merge pull request #407 from bradhawkins85/patch-15
Update installer.ps1
2021-04-16 22:05:14 -07:00
bradhawkins85
07b2543972 Update installer.ps1
Test and wait (up to 15 seconds) to be able to connect to the server to download installer, don't try and download if we can't connect.
2021-04-17 13:41:35 +10:00
wh1te909
d1c3fc8493 Release 0.6.1 2021-04-16 07:46:42 +00:00
wh1te909
f453b16010 bump versions 2021-04-16 07:36:27 +00:00
wh1te909
05151d8978 add code signed agent to powershell/manual install methods 2021-04-16 07:16:55 +00:00
Dan
8218e1acc3 Merge pull request #397 from silversword411/develop
script library updates
2021-04-16 00:11:57 -07:00
wh1te909
30212fc89a fix maint mode text 2021-04-16 06:24:27 +00:00
sadnub
b31c13fcae add warning color to agents table and clients tree. Also made it upadte colors when checks UI is refreshed 2021-04-15 22:24:44 -04:00
sadnub
6b95fc6f1d change maintenance mode to green and modify the icon in the agent table when agent is in maintenance mode 2021-04-15 19:15:02 -04:00
sadnub
369cf17eb2 also resolve alerts when a check is cleared 2021-04-15 17:23:43 -04:00
sadnub
4dd8f512cc split up check statuses in the agent summary tab. #386 2021-04-15 17:12:46 -04:00
sadnub
26cfec7d80 add reset check status to check context menu. #388 2021-04-15 16:52:42 -04:00
sadnub
67a87ccf00 fix sticky table header in automated tasks 2021-04-15 16:12:09 -04:00
sadnub
667cebcf94 remove certain fields from view in the patch policy form when settnigs are inherited #396 2021-04-15 13:52:24 -04:00
sadnub
bc1747ca1c fix categories in script manager folder view. Truncate script args text in scripts table 2021-04-15 13:52:24 -04:00
silversword411
945d8647bf script add ipv6 disable 2021-04-15 11:34:35 -04:00
silversword411
dfe2e94627 tweaking script library after 0.6.0 update 2021-04-15 08:47:04 -04:00
silversword411
09a5591eec tweak docs so backup script overwrites existing name 2021-04-15 07:59:22 -04:00
silversword411
f2bf06a0ba tweak Network script names for sorting 2021-04-15 07:51:07 -04:00
silversword411
eedad4ab1c Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-04-15 07:47:41 -04:00
silversword411
336a62ab29 Tweaking script names 2021-04-15 07:47:31 -04:00
silversword411
bf776eeb2b Tweaking script names 2021-04-14 15:12:02 -04:00
375 changed files with 19556 additions and 20982 deletions

View File

@@ -26,3 +26,6 @@ POSTGRES_PASS=postgrespass
APP_PORT=80
API_PORT=80
HTTP_PROTOCOL=https
DOCKER_NETWORK="172.21.0.0/24"
DOCKER_NGINX_IP="172.21.0.20"
NATS_PORTS="4222:4222"

View File

@@ -8,7 +8,7 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
EXPOSE 8000 8383
EXPOSE 8000 8383 8005
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical

View File

@@ -46,7 +46,7 @@ services:
API_PORT: ${API_PORT}
DEV: 1
ports:
- "4222:4222"
- "${NATS_PORTS}"
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
@@ -67,7 +67,7 @@ services:
MESH_PASS: ${MESH_PASS}
MONGODB_USER: ${MONGODB_USER}
MONGODB_PASSWORD: ${MONGODB_PASSWORD}
NGINX_HOST_IP: 172.21.0.20
NGINX_HOST_IP: ${DOCKER_NGINX_IP}
networks:
dev:
aliases:
@@ -115,7 +115,10 @@ services:
redis-dev:
container_name: trmm-redis-dev
restart: always
command: redis-server --appendonly yes
image: redis:6.0-alpine
volumes:
- redis-data-dev:/data
networks:
dev:
aliases:
@@ -220,18 +223,34 @@ services:
API_PORT: ${API_PORT}
networks:
dev:
ipv4_address: 172.21.0.20
ipv4_address: ${DOCKER_NGINX_IP}
ports:
- "80:80"
- "443:443"
volumes:
- tactical-data-dev:/opt/tactical
mkdocs-dev:
container_name: trmm-mkdocs-dev
image: api-dev
restart: always
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-mkdocs-dev"]
ports:
- "8005:8005"
volumes:
- ..:/workspace:cached
networks:
- dev
volumes:
tactical-data-dev:
postgres-data-dev:
mongo-dev-data:
mesh-data-dev:
redis-data-dev:
networks:
dev:
@@ -239,4 +258,4 @@ networks:
ipam:
driver: default
config:
- subnet: 172.21.0.0/24
- subnet: ${DOCKER_NETWORK}

View File

@@ -170,3 +170,8 @@ if [ "$1" = 'tactical-websockets-dev' ]; then
check_tactical_ready
"${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
fi
if [ "$1" = 'tactical-mkdocs-dev' ]; then
cd "${WORKSPACE_DIR}/docs"
"${VIRTUAL_ENV}"/bin/mkdocs serve
fi

View File

@@ -2,6 +2,7 @@
asyncio-nats-client
celery
channels
channels_redis
Django
django-cors-headers
django-rest-knox

1
.gitignore vendored
View File

@@ -47,3 +47,4 @@ docs/.vuepress/dist
nats-rmm.conf
.mypy_cache
docs/site/
reset_db.sh

View File

@@ -11,8 +11,6 @@ It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and i
# [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.*
### [Discord Chat](https://discord.gg/upGTkWp)
### [Documentation](https://wh1te909.github.io/tacticalrmm/)

View File

@@ -1,7 +1,8 @@
from django.contrib import admin
from rest_framework.authtoken.admin import TokenAdmin
from .models import User
from .models import User, Role
admin.site.register(User)
TokenAdmin.raw_id_fields = ("user",)
admin.site.register(Role)

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.1 on 2021-05-07 15:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0022_urlaction'),
('accounts', '0015_user_loading_bar_color'),
]
operations = [
migrations.AddField(
model_name='user',
name='url_action',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
),
migrations.AlterField(
model_name='user',
name='agent_dblclick_action',
field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
),
]

View File

@@ -0,0 +1,173 @@
# Generated by Django 3.2.1 on 2021-05-08 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0016_auto_20210507_1526'),
]
operations = [
migrations.AddField(
model_name='user',
name='can_code_sign',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_do_server_maint',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_edit_agent',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_edit_core_settings',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_install_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_accounts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_alerts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_automation_policies',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_autotasks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_checks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_clients',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_deployments',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_notes',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_pendingactions',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_procs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_scripts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_sites',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_software',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_winsvcs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_winupdates',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_reboot_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_autotasks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_bulk',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_checks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_scripts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_send_cmd',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_uninstall_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_update_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_use_mesh',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_view_auditlogs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_view_debuglogs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_view_eventlogs',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,181 @@
# Generated by Django 3.2.1 on 2021-05-11 02:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0017_auto_20210508_1716'),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('is_superuser', models.BooleanField(default=False)),
('can_use_mesh', models.BooleanField(default=False)),
('can_uninstall_agents', models.BooleanField(default=False)),
('can_update_agents', models.BooleanField(default=False)),
('can_edit_agent', models.BooleanField(default=False)),
('can_manage_procs', models.BooleanField(default=False)),
('can_view_eventlogs', models.BooleanField(default=False)),
('can_send_cmd', models.BooleanField(default=False)),
('can_reboot_agents', models.BooleanField(default=False)),
('can_install_agents', models.BooleanField(default=False)),
('can_run_scripts', models.BooleanField(default=False)),
('can_run_bulk', models.BooleanField(default=False)),
('can_manage_notes', models.BooleanField(default=False)),
('can_edit_core_settings', models.BooleanField(default=False)),
('can_do_server_maint', models.BooleanField(default=False)),
('can_code_sign', models.BooleanField(default=False)),
('can_manage_checks', models.BooleanField(default=False)),
('can_run_checks', models.BooleanField(default=False)),
('can_manage_clients', models.BooleanField(default=False)),
('can_manage_sites', models.BooleanField(default=False)),
('can_manage_deployments', models.BooleanField(default=False)),
('can_manage_automation_policies', models.BooleanField(default=False)),
('can_manage_autotasks', models.BooleanField(default=False)),
('can_run_autotasks', models.BooleanField(default=False)),
('can_view_auditlogs', models.BooleanField(default=False)),
('can_manage_pendingactions', models.BooleanField(default=False)),
('can_view_debuglogs', models.BooleanField(default=False)),
('can_manage_scripts', models.BooleanField(default=False)),
('can_manage_alerts', models.BooleanField(default=False)),
('can_manage_winsvcs', models.BooleanField(default=False)),
('can_manage_software', models.BooleanField(default=False)),
('can_manage_winupdates', models.BooleanField(default=False)),
('can_manage_accounts', models.BooleanField(default=False)),
],
),
migrations.RemoveField(
model_name='user',
name='can_code_sign',
),
migrations.RemoveField(
model_name='user',
name='can_do_server_maint',
),
migrations.RemoveField(
model_name='user',
name='can_edit_agent',
),
migrations.RemoveField(
model_name='user',
name='can_edit_core_settings',
),
migrations.RemoveField(
model_name='user',
name='can_install_agents',
),
migrations.RemoveField(
model_name='user',
name='can_manage_accounts',
),
migrations.RemoveField(
model_name='user',
name='can_manage_alerts',
),
migrations.RemoveField(
model_name='user',
name='can_manage_automation_policies',
),
migrations.RemoveField(
model_name='user',
name='can_manage_autotasks',
),
migrations.RemoveField(
model_name='user',
name='can_manage_checks',
),
migrations.RemoveField(
model_name='user',
name='can_manage_clients',
),
migrations.RemoveField(
model_name='user',
name='can_manage_deployments',
),
migrations.RemoveField(
model_name='user',
name='can_manage_notes',
),
migrations.RemoveField(
model_name='user',
name='can_manage_pendingactions',
),
migrations.RemoveField(
model_name='user',
name='can_manage_procs',
),
migrations.RemoveField(
model_name='user',
name='can_manage_scripts',
),
migrations.RemoveField(
model_name='user',
name='can_manage_sites',
),
migrations.RemoveField(
model_name='user',
name='can_manage_software',
),
migrations.RemoveField(
model_name='user',
name='can_manage_winsvcs',
),
migrations.RemoveField(
model_name='user',
name='can_manage_winupdates',
),
migrations.RemoveField(
model_name='user',
name='can_reboot_agents',
),
migrations.RemoveField(
model_name='user',
name='can_run_autotasks',
),
migrations.RemoveField(
model_name='user',
name='can_run_bulk',
),
migrations.RemoveField(
model_name='user',
name='can_run_checks',
),
migrations.RemoveField(
model_name='user',
name='can_run_scripts',
),
migrations.RemoveField(
model_name='user',
name='can_send_cmd',
),
migrations.RemoveField(
model_name='user',
name='can_uninstall_agents',
),
migrations.RemoveField(
model_name='user',
name='can_update_agents',
),
migrations.RemoveField(
model_name='user',
name='can_use_mesh',
),
migrations.RemoveField(
model_name='user',
name='can_view_auditlogs',
),
migrations.RemoveField(
model_name='user',
name='can_view_debuglogs',
),
migrations.RemoveField(
model_name='user',
name='can_view_eventlogs',
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.1 on 2021-05-11 02:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("accounts", "0018_auto_20210511_0233"),
]
operations = [
migrations.AddField(
model_name="user",
name="role",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="roles",
to="accounts.role",
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2021-05-11 17:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0019_user_role'),
]
operations = [
migrations.AddField(
model_name='role',
name='can_manage_roles',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-17 04:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0020_role_can_manage_roles'),
]
operations = [
migrations.AddField(
model_name='role',
name='can_view_core_settings',
field=models.BooleanField(default=False),
),
]

View File

@@ -7,6 +7,7 @@ AGENT_DBLCLICK_CHOICES = [
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
("urlaction", "URL Action"),
]
AGENT_TBL_TAB_CHOICES = [
@@ -29,6 +30,13 @@ class User(AbstractUser, BaseAuditModel):
agent_dblclick_action = models.CharField(
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
)
url_action = models.ForeignKey(
"core.URLAction",
related_name="user",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
)
@@ -47,9 +55,125 @@ class User(AbstractUser, BaseAuditModel):
on_delete=models.CASCADE,
)
role = models.ForeignKey(
"accounts.Role",
null=True,
blank=True,
related_name="roles",
on_delete=models.SET_NULL,
)
@staticmethod
def serialize(user):
# serializes the task and returns json
from .serializers import UserSerializer
return UserSerializer(user).data
class Role(models.Model):
name = models.CharField(max_length=255, unique=True)
is_superuser = models.BooleanField(default=False)
# agents
can_use_mesh = models.BooleanField(default=False)
can_uninstall_agents = models.BooleanField(default=False)
can_update_agents = models.BooleanField(default=False)
can_edit_agent = models.BooleanField(default=False)
can_manage_procs = models.BooleanField(default=False)
can_view_eventlogs = models.BooleanField(default=False)
can_send_cmd = models.BooleanField(default=False)
can_reboot_agents = models.BooleanField(default=False)
can_install_agents = models.BooleanField(default=False)
can_run_scripts = models.BooleanField(default=False)
can_run_bulk = models.BooleanField(default=False)
# core
can_manage_notes = models.BooleanField(default=False)
can_view_core_settings = models.BooleanField(default=False)
can_edit_core_settings = models.BooleanField(default=False)
can_do_server_maint = models.BooleanField(default=False)
can_code_sign = models.BooleanField(default=False)
# checks
can_manage_checks = models.BooleanField(default=False)
can_run_checks = models.BooleanField(default=False)
# clients
can_manage_clients = models.BooleanField(default=False)
can_manage_sites = models.BooleanField(default=False)
can_manage_deployments = models.BooleanField(default=False)
# automation
can_manage_automation_policies = models.BooleanField(default=False)
# automated tasks
can_manage_autotasks = models.BooleanField(default=False)
can_run_autotasks = models.BooleanField(default=False)
# logs
can_view_auditlogs = models.BooleanField(default=False)
can_manage_pendingactions = models.BooleanField(default=False)
can_view_debuglogs = models.BooleanField(default=False)
# scripts
can_manage_scripts = models.BooleanField(default=False)
# alerts
can_manage_alerts = models.BooleanField(default=False)
# win services
can_manage_winsvcs = models.BooleanField(default=False)
# software
can_manage_software = models.BooleanField(default=False)
# windows updates
can_manage_winupdates = models.BooleanField(default=False)
# accounts
can_manage_accounts = models.BooleanField(default=False)
can_manage_roles = models.BooleanField(default=False)
def __str__(self):
return self.name
@staticmethod
def perms():
return [
"is_superuser",
"can_use_mesh",
"can_uninstall_agents",
"can_update_agents",
"can_edit_agent",
"can_manage_procs",
"can_view_eventlogs",
"can_send_cmd",
"can_reboot_agents",
"can_install_agents",
"can_run_scripts",
"can_run_bulk",
"can_manage_notes",
"can_view_core_settings",
"can_edit_core_settings",
"can_do_server_maint",
"can_code_sign",
"can_manage_checks",
"can_run_checks",
"can_manage_clients",
"can_manage_sites",
"can_manage_deployments",
"can_manage_automation_policies",
"can_manage_autotasks",
"can_run_autotasks",
"can_view_auditlogs",
"can_manage_pendingactions",
"can_view_debuglogs",
"can_manage_scripts",
"can_manage_alerts",
"can_manage_winsvcs",
"can_manage_software",
"can_manage_winupdates",
"can_manage_accounts",
"can_manage_roles",
]

View File

@@ -0,0 +1,19 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class AccountsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_accounts")
class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_roles")

View File

@@ -1,7 +1,7 @@
import pyotp
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import User
from .models import User, Role
class UserUISerializer(ModelSerializer):
@@ -11,6 +11,7 @@ class UserUISerializer(ModelSerializer):
"dark_mode",
"show_community_scripts",
"agent_dblclick_action",
"url_action",
"default_agent_tbl_tab",
"client_tree_sort",
"client_tree_splitter",
@@ -21,7 +22,7 @@ class UserUISerializer(ModelSerializer):
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = (
fields = [
"id",
"username",
"first_name",
@@ -29,7 +30,8 @@ class UserSerializer(ModelSerializer):
"email",
"is_active",
"last_login",
)
"role",
]
class TOTPSetupSerializer(ModelSerializer):
@@ -48,3 +50,9 @@ class TOTPSetupSerializer(ModelSerializer):
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
obj.username, issuer_name="Tactical RMM"
)
class RoleSerializer(ModelSerializer):
class Meta:
model = Role
fields = "__all__"

View File

@@ -9,4 +9,7 @@ urlpatterns = [
path("users/reset_totp/", views.UserActions.as_view()),
path("users/setup_totp/", views.TOTPSetup.as_view()),
path("users/ui/", views.UserUI.as_view()),
path("permslist/", views.PermsList.as_view()),
path("roles/", views.GetAddRoles.as_view()),
path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
]

View File

@@ -6,15 +6,21 @@ from django.shortcuts import get_object_or_404
from knox.views import LoginView as KnoxLoginView
from rest_framework import status
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from logs.models import AuditLog
from tacticalrmm.utils import notify_error
from .models import User
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
from .models import User, Role
from .permissions import AccountsPerms, RolesPerms
from .serializers import (
TOTPSetupSerializer,
UserSerializer,
UserUISerializer,
RoleSerializer,
)
def _is_root_user(request, user) -> bool:
@@ -78,6 +84,8 @@ class LoginView(KnoxLoginView):
class GetAddUsers(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def get(self, request):
users = User.objects.filter(agent=None)
@@ -98,13 +106,17 @@ class GetAddUsers(APIView):
user.first_name = request.data["first_name"]
user.last_name = request.data["last_name"]
# Can be changed once permissions and groups are introduced
user.is_superuser = True
if "role" in request.data.keys() and isinstance(request.data["role"], int):
role = get_object_or_404(Role, pk=request.data["role"])
user.role = role
user.save()
return Response(user.username)
class GetUpdateDeleteUser(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def get(self, request, pk):
user = get_object_or_404(User, pk=pk)
@@ -133,7 +145,7 @@ class GetUpdateDeleteUser(APIView):
class UserActions(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
@@ -182,3 +194,42 @@ class UserUI(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class PermsList(APIView):
def get(self, request):
return Response(Role.perms())
class GetAddRoles(APIView):
permission_classes = [IsAuthenticated, RolesPerms]
def get(self, request):
roles = Role.objects.all()
return Response(RoleSerializer(roles, many=True).data)
def post(self, request):
serializer = RoleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteRole(APIView):
permission_classes = [IsAuthenticated, RolesPerms]
def get(self, request, pk):
role = get_object_or_404(Role, pk=pk)
return Response(RoleSerializer(role).data)
def put(self, request, pk):
role = get_object_or_404(Role, pk=pk)
serializer = RoleSerializer(instance=role, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
role = get_object_or_404(Role, pk=pk)
role.delete()
return Response("ok")

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-17 01:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0035_auto_20210329_1709'),
]
operations = [
migrations.AddField(
model_name='agent',
name='block_policy_inheritance',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-06-27 00:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0036_agent_block_policy_inheritance'),
]
operations = [
migrations.AddField(
model_name='agent',
name='has_patches_pending',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='agent',
name='pending_actions_count',
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -63,6 +63,9 @@ class Agent(BaseAuditModel):
max_length=255, choices=TZ_CHOICES, null=True, blank=True
)
maintenance_mode = models.BooleanField(default=False)
block_policy_inheritance = models.BooleanField(default=False)
pending_actions_count = models.PositiveIntegerField(default=0)
has_patches_pending = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
@@ -94,11 +97,13 @@ class Agent(BaseAuditModel):
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
# or if agent was changed from server or workstation
if (
not old_agent
or old_agent
and old_agent.policy != self.policy
or old_agent.site != self.site
or (old_agent and old_agent.policy != self.policy)
or (old_agent.site != self.site)
or (old_agent.monitoring_type != self.monitoring_type)
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
@@ -160,13 +165,9 @@ class Agent(BaseAuditModel):
else:
return "offline"
@property
def has_patches_pending(self):
return self.winupdates.filter(action="approve").filter(installed=False).exists() # type: ignore
@property
def checks(self):
total, passing, failing = 0, 0, 0
total, passing, failing, warning, info = 0, 0, 0, 0, 0
if self.agentchecks.exists(): # type: ignore
for i in self.agentchecks.all(): # type: ignore
@@ -174,13 +175,20 @@ class Agent(BaseAuditModel):
if i.status == "passing":
passing += 1
elif i.status == "failing":
failing += 1
if i.alert_severity == "error":
failing += 1
elif i.alert_severity == "warning":
warning += 1
elif i.alert_severity == "info":
info += 1
ret = {
"total": total,
"passing": passing,
"failing": failing,
"has_failing_checks": failing > 0,
"warning": warning,
"info": info,
"has_failing_checks": failing > 0 or warning > 0,
}
return ret
@@ -255,6 +263,11 @@ class Agent(BaseAuditModel):
make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
model = [x["Product"] for x in mobo if "Product" in x][0]
if make.lower() == "lenovo":
sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
if "to be filled" not in sysfam.lower():
model = sysfam
return f"{make} {model}"
except:
pass
@@ -414,21 +427,34 @@ class Agent(BaseAuditModel):
# check site policy if agent policy doesn't have one
elif site.server_policy and site.server_policy.winupdatepolicy.exists():
patch_policy = site.server_policy.winupdatepolicy.get()
# make sure agent isn;t blocking policy inheritance
if not self.block_policy_inheritance:
patch_policy = site.server_policy.winupdatepolicy.get()
# if site doesn't have a patch policy check the client
elif (
site.client.server_policy
and site.client.server_policy.winupdatepolicy.exists()
):
patch_policy = site.client.server_policy.winupdatepolicy.get()
# make sure agent and site are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
):
patch_policy = site.client.server_policy.winupdatepolicy.get()
# if patch policy still doesn't exist check default policy
elif (
core_settings.server_policy
and core_settings.server_policy.winupdatepolicy.exists()
):
patch_policy = core_settings.server_policy.winupdatepolicy.get()
# make sure agent site and client are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
and not site.client.block_policy_inheritance
):
patch_policy = core_settings.server_policy.winupdatepolicy.get()
elif self.monitoring_type == "workstation":
# check agent policy first which should override client or site policy
@@ -439,21 +465,36 @@ class Agent(BaseAuditModel):
site.workstation_policy
and site.workstation_policy.winupdatepolicy.exists()
):
patch_policy = site.workstation_policy.winupdatepolicy.get()
# make sure agent isn;t blocking policy inheritance
if not self.block_policy_inheritance:
patch_policy = site.workstation_policy.winupdatepolicy.get()
# if site doesn't have a patch policy check the client
elif (
site.client.workstation_policy
and site.client.workstation_policy.winupdatepolicy.exists()
):
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
# make sure agent and site are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
):
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
# if patch policy still doesn't exist check default policy
elif (
core_settings.workstation_policy
and core_settings.workstation_policy.winupdatepolicy.exists()
):
patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
# make sure agent site and client are not blocking inheritance
if (
not self.block_policy_inheritance
and not site.block_policy_inheritance
and not site.client.block_policy_inheritance
):
patch_policy = (
core_settings.workstation_policy.winupdatepolicy.get()
)
# if policy still doesn't exist return the agent patch policy
if not patch_policy:
@@ -520,6 +561,7 @@ class Agent(BaseAuditModel):
and site.server_policy
and site.server_policy.alert_template
and site.server_policy.alert_template.is_active
and not self.block_policy_inheritance
):
templates.append(site.server_policy.alert_template)
if (
@@ -527,6 +569,7 @@ class Agent(BaseAuditModel):
and site.workstation_policy
and site.workstation_policy.alert_template
and site.workstation_policy.alert_template.is_active
and not self.block_policy_inheritance
):
templates.append(site.workstation_policy.alert_template)
@@ -540,6 +583,8 @@ class Agent(BaseAuditModel):
and client.server_policy
and client.server_policy.alert_template
and client.server_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
):
templates.append(client.server_policy.alert_template)
if (
@@ -547,15 +592,28 @@ class Agent(BaseAuditModel):
and client.workstation_policy
and client.workstation_policy.alert_template
and client.workstation_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
):
templates.append(client.workstation_policy.alert_template)
# check if alert template is on client and return
if client.alert_template and client.alert_template.is_active:
if (
client.alert_template
and client.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
):
templates.append(client.alert_template)
# check if alert template is applied globally and return
if core.alert_template and core.alert_template.is_active:
if (
core.alert_template
and core.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.alert_template)
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
@@ -564,6 +622,9 @@ class Agent(BaseAuditModel):
and core.server_policy
and core.server_policy.alert_template
and core.server_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.server_policy.alert_template)
if (
@@ -571,6 +632,9 @@ class Agent(BaseAuditModel):
and core.workstation_policy
and core.workstation_policy.alert_template
and core.workstation_policy.alert_template.is_active
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.workstation_policy.alert_template)
@@ -724,36 +788,6 @@ class Agent(BaseAuditModel):
except:
pass
# define how the agent should handle pending actions
def handle_pending_actions(self):
pending_actions = self.pendingactions.filter(status="pending") # type: ignore
for action in pending_actions:
if action.action_type == "taskaction":
from autotasks.tasks import (
create_win_task_schedule,
delete_win_task_schedule,
enable_or_disable_win_task,
)
task_id = action.details["task_id"]
if action.details["action"] == "taskcreate":
create_win_task_schedule.delay(task_id, pending_action=action.id)
elif action.details["action"] == "tasktoggle":
enable_or_disable_win_task.delay(
task_id, action.details["value"], pending_action=action.id
)
elif action.details["action"] == "taskdelete":
delete_win_task_schedule.delay(task_id, pending_action=action.id)
# 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.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
if action.details["task_id"] == task_id:
action.delete()
def should_create_alert(self, alert_template=None):
return (
self.overdue_dashboard_alert

View File

@@ -0,0 +1,63 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class MeshPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_use_mesh")
class UninstallPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_uninstall_agents")
class UpdateAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_update_agents")
class EditAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_edit_agent")
class ManageProcPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_manage_procs")
class EvtLogPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_view_eventlogs")
class SendCMDPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_send_cmd")
class RebootAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_reboot_agents")
class InstallAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_install_agents")
class RunScriptPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_scripts")
class ManageNotesPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_manage_notes")
class RunBulkPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_bulk")

View File

@@ -9,7 +9,6 @@ from .models import Agent, AgentCustomField, Note
class AgentSerializer(serializers.ModelSerializer):
# for vue
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
status = serializers.ReadOnlyField()
cpu_model = serializers.ReadOnlyField()
@@ -45,8 +44,6 @@ class AgentOverdueActionSerializer(serializers.ModelSerializer):
class AgentTableSerializer(serializers.ModelSerializer):
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
pending_actions = serializers.SerializerMethodField()
status = serializers.ReadOnlyField()
checks = serializers.ReadOnlyField()
last_seen = serializers.SerializerMethodField()
@@ -69,9 +66,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
"always_alert": obj.alert_template.agent_always_alert,
}
def get_pending_actions(self, obj):
return obj.pendingactions.filter(status="pending").count()
def get_last_seen(self, obj) -> str:
if obj.time_zone is not None:
agent_tz = pytz.timezone(obj.time_zone)
@@ -103,8 +97,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
"monitoring_type",
"description",
"needs_reboot",
"patches_pending",
"pending_actions",
"has_patches_pending",
"pending_actions_count",
"status",
"overdue_text_alert",
"overdue_email_alert",
@@ -116,6 +110,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
"logged_username",
"italic",
"policy",
"block_policy_inheritance",
]
depth = 2
@@ -172,11 +167,6 @@ class AgentEditSerializer(serializers.ModelSerializer):
class WinAgentSerializer(serializers.ModelSerializer):
# for the windows agent
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
status = serializers.ReadOnlyField()
class Meta:
model = Agent
fields = "__all__"

View File

@@ -1,7 +1,9 @@
import asyncio
import datetime as dt
import random
import requests
import tempfile
import json
import subprocess
import urllib.parse
from time import sleep
from typing import Union
@@ -12,7 +14,7 @@ from loguru import logger
from packaging import version as pyver
from agents.models import Agent
from core.models import CoreSettings, CodeSignToken
from core.models import CodeSignToken, CoreSettings
from logs.models import PendingAction
from scripts.models import Script
from tacticalrmm.celery import app
@@ -21,21 +23,9 @@ from tacticalrmm.utils import run_nats_api_cmd
logger.configure(**settings.LOG_CONFIG)
def _get_exegen_url() -> str:
urls: list[str] = settings.EXE_GEN_URLS
for url in urls:
try:
r = requests.get(url, timeout=10)
except:
continue
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
from agents.utils import get_exegen_url
if r.status_code == 200:
return url
return random.choice(urls)
def agent_update(pk: int, codesigntoken: str = None) -> str:
agent = Agent.objects.get(pk=pk)
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
@@ -52,28 +42,29 @@ def agent_update(pk: int, codesigntoken: str = None) -> str:
inno = agent.win_inno_exe
if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
base_url = _get_exegen_url() + "/api/v1/winagents/?"
base_url = get_exegen_url() + "/api/v1/winagents/?"
params = {"version": version, "arch": agent.arch, "token": codesigntoken}
url = base_url + urllib.parse.urlencode(params)
else:
url = agent.winagent_dl
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
agent.pendingactions.filter(
if not force:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
).exists():
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
nats_data = {
"func": "agentupdate",
@@ -87,6 +78,21 @@ def agent_update(pk: int, codesigntoken: str = None) -> str:
return "created"
@app.task
def force_code_sign(pks: list[int]) -> None:
try:
token = CodeSignToken.objects.first().token
except:
return
chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
for chunk in chunks:
for pk in chunk:
agent_update(pk=pk, codesigntoken=token, force=True)
sleep(0.05)
sleep(4)
@app.task
def send_agent_update_task(pks: list[int]) -> None:
try:
@@ -208,6 +214,7 @@ def agent_outages_task() -> None:
agents = Agent.objects.only(
"pk",
"agent_id",
"last_seen",
"offline_time",
"overdue_time",
@@ -275,6 +282,34 @@ def run_script_email_results_task(
logger.error(e)
@app.task
def clear_faults_task(older_than_days: int) -> None:
# https://github.com/wh1te909/tacticalrmm/issues/484
agents = Agent.objects.exclude(last_seen__isnull=True).filter(
last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
)
for agent in agents:
if agent.agentchecks.exists():
for check in agent.agentchecks.all():
# reset check status
check.status = "passing"
check.save(update_fields=["status"])
if check.alert.filter(resolved=False).exists():
check.alert.get(resolved=False).resolve()
# reset overdue alerts
agent.overdue_email_alert = False
agent.overdue_text_alert = False
agent.overdue_dashboard_alert = False
agent.save(
update_fields=[
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
]
)
@app.task
def monitor_agents_task() -> None:
agents = Agent.objects.only(
@@ -290,4 +325,23 @@ def get_wmi_task() -> None:
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
)
ids = [i.agent_id for i in agents if i.status == "online"]
run_nats_api_cmd("wmi", ids)
run_nats_api_cmd("wmi", ids, timeout=45)
@app.task
def agent_checkin_task() -> None:
db = settings.DATABASES["default"]
config = {
"key": settings.SECRET_KEY,
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
"user": db["USER"],
"pass": db["PASSWORD"],
"host": db["HOST"],
"port": int(db["PORT"]),
"dbname": db["NAME"],
}
with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, "w") as f:
json.dump(config, f)
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "checkin"]
subprocess.run(cmd, timeout=30)

View File

@@ -152,8 +152,9 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url)
@patch("time.sleep")
@patch("agents.models.Agent.nats_cmd")
def test_ping(self, nats_cmd):
def test_ping(self, nats_cmd, mock_sleep):
url = f"/agents/{self.agent.pk}/ping/"
nats_cmd.return_value = "timeout"
@@ -753,7 +754,7 @@ class TestAgentViews(TacticalTestCase):
self.assertEqual(r.status_code, 200)
self.assertIn(self.agent.hostname, r.data) # type: ignore
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=90
)
nats_cmd.return_value = "timeout"
@@ -914,7 +915,7 @@ class TestAgentTasks(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
@patch("agents.tasks._get_exegen_url")
@patch("agents.utils.get_exegen_url")
@patch("agents.models.Agent.nats_cmd")
def test_agent_update(self, nats_cmd, get_exe):
from agents.tasks import agent_update

View File

@@ -0,0 +1,37 @@
import random
import urllib.parse
import requests
from django.conf import settings
def get_exegen_url() -> str:
urls: list[str] = settings.EXE_GEN_URLS
for url in urls:
try:
r = requests.get(url, timeout=10)
except:
continue
if r.status_code == 200:
return url
return random.choice(urls)
def get_winagent_url(arch: str) -> str:
from core.models import CodeSignToken
try:
codetoken = CodeSignToken.objects.first().token
base_url = get_exegen_url() + "/api/v1/winagents/?"
params = {
"version": settings.LATEST_AGENT_VER,
"arch": arch,
"token": codetoken,
}
dl_url = base_url + urllib.parse.urlencode(params)
except:
dl_url = settings.DL_64 if arch == "64" else settings.DL_32
return dl_url

View File

@@ -3,6 +3,7 @@ import datetime as dt
import os
import random
import string
import time
from django.conf import settings
from django.http import HttpResponse
@@ -10,7 +11,8 @@ from django.shortcuts import get_object_or_404
from loguru import logger
from packaging import version as pyver
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -23,6 +25,20 @@ from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from .models import Agent, AgentCustomField, Note, RecoveryAction
from .permissions import (
EditAgentPerms,
EvtLogPerms,
InstallAgentPerms,
ManageNotesPerms,
ManageProcPerms,
MeshPerms,
RebootAgentPerms,
RunBulkPerms,
RunScriptPerms,
SendCMDPerms,
UninstallPerms,
UpdateAgentPerms,
)
from .serializers import (
AgentCustomFieldSerializer,
AgentEditSerializer,
@@ -50,6 +66,7 @@ def get_agent_versions(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, UpdateAgentPerms])
def update_agents(request):
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
pks: list[int] = [
@@ -62,21 +79,31 @@ def update_agents(request):
@api_view()
@permission_classes([IsAuthenticated, UninstallPerms])
def ping(request, pk):
agent = get_object_or_404(Agent, pk=pk)
status = "offline"
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
attempts = 0
while 1:
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
if r == "pong":
status = "online"
break
else:
attempts += 1
time.sleep(1)
if attempts >= 5:
break
return Response({"name": agent.hostname, "status": status})
@api_view(["DELETE"])
@permission_classes([IsAuthenticated, UninstallPerms])
def uninstall(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
name = agent.hostname
agent.delete()
reload_nats()
@@ -84,6 +111,7 @@ def uninstall(request):
@api_view(["PATCH", "PUT"])
@permission_classes([IsAuthenticated, EditAgentPerms])
def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["id"])
@@ -126,6 +154,7 @@ def edit_agent(request):
@api_view()
@permission_classes([IsAuthenticated, MeshPerms])
def meshcentral(request, pk):
agent = get_object_or_404(Agent, pk=pk)
core = CoreSettings.objects.first()
@@ -171,6 +200,7 @@ def get_processes(request, pk):
@api_view()
@permission_classes([IsAuthenticated, ManageProcPerms])
def kill_proc(request, pk, pid):
agent = get_object_or_404(Agent, pk=pk)
r = asyncio.run(
@@ -186,6 +216,7 @@ def kill_proc(request, pk, pid):
@api_view()
@permission_classes([IsAuthenticated, EvtLogPerms])
def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
timeout = 180 if logtype == "Security" else 30
@@ -205,6 +236,7 @@ def get_event_log(request, pk, logtype, days):
@api_view(["POST"])
@permission_classes([IsAuthenticated, SendCMDPerms])
def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
timeout = int(request.data["timeout"])
@@ -270,6 +302,8 @@ class AgentsTableList(APIView):
"last_logged_in_user",
"time_zone",
"maintenance_mode",
"pending_actions_count",
"has_patches_pending",
)
ctx = {"default_tz": get_default_timezone()}
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
@@ -300,6 +334,7 @@ def overdue_action(request):
class Reboot(APIView):
permission_classes = [IsAuthenticated, RebootAgentPerms]
# reboot now
def post(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
@@ -352,9 +387,12 @@ class Reboot(APIView):
@api_view(["POST"])
@permission_classes([IsAuthenticated, InstallAgentPerms])
def install_agent(request):
from knox.models import AuthToken
from agents.utils import get_winagent_url
client_id = request.data["client"]
site_id = request.data["site"]
version = settings.LATEST_AGENT_VER
@@ -375,7 +413,7 @@ def install_agent(request):
inno = (
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
)
download_url = settings.DL_64 if arch == "64" else settings.DL_32
download_url = get_winagent_url(arch)
_, token = AuthToken.objects.create(
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
@@ -522,6 +560,7 @@ def recover(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, RunScriptPerms])
def run_script(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
script = get_object_or_404(Script, pk=request.data["scriptPK"])
@@ -562,7 +601,7 @@ def run_script(request):
def recover_mesh(request, pk):
agent = get_object_or_404(Agent, pk=pk)
data = {"func": "recover", "payload": {"mode": "mesh"}}
r = asyncio.run(agent.nats_cmd(data, timeout=45))
r = asyncio.run(agent.nats_cmd(data, timeout=90))
if r != "ok":
return notify_error("Unable to contact the agent")
@@ -604,6 +643,8 @@ class GetAddNotes(APIView):
class GetEditDeleteNote(APIView):
permission_classes = [IsAuthenticated, ManageNotesPerms]
def get(self, request, pk):
note = get_object_or_404(Note, pk=pk)
return Response(NoteSerializer(note).data)
@@ -622,6 +663,7 @@ class GetEditDeleteNote(APIView):
@api_view(["POST"])
@permission_classes([IsAuthenticated, RunBulkPerms])
def bulk(request):
if request.data["target"] == "agents" and not request.data["agentPKs"]:
return notify_error("Must select at least 1 agent")

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Union
from django.conf import settings
@@ -297,7 +298,7 @@ class Alert(models.Model):
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,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
wait=True,
full=True,
@@ -406,7 +407,7 @@ class Alert(models.Model):
):
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
@@ -428,6 +429,36 @@ class Alert(models.Model):
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
)
def parse_script_args(self, args: list[str]):
if not args:
return []
temp_args = list()
# pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
for arg in args:
match = pattern.match(arg)
if match:
name = match.group(1)
if hasattr(self, name):
value = f"'{getattr(self, name)}'"
else:
continue
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
except Exception as e:
logger.error(e)
continue
else:
temp_args.append(arg)
return temp_args
class AlertTemplate(models.Model):
name = models.CharField(max_length=100)

View File

@@ -0,0 +1,11 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageAlertsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET" or r.method == "PATCH":
return True
return _has_perm(r, "can_manage_alerts")

View File

@@ -1387,3 +1387,14 @@ class TestAlertTasks(TacticalTestCase):
self.assertEqual(alert.resolved_action_execution_time, "5.0000")
self.assertEqual(alert.resolved_action_stdout, "success!")
self.assertEqual(alert.resolved_action_stderr, "")
def test_parse_script_args(self):
alert = baker.make("alerts.Alert")
args = ["-Parameter", "-Another {{alert.id}}"]
# test default value
self.assertEqual(
["-Parameter", f"-Another '{alert.id}'"], # type: ignore
alert.parse_script_args(args=args), # type: ignore
)

View File

@@ -3,12 +3,14 @@ from datetime import datetime as dt
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import Alert, AlertTemplate
from .permissions import ManageAlertsPerms
from .serializers import (
AlertSerializer,
AlertTemplateRelationSerializer,
@@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template
class GetAddAlerts(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
def patch(self, request):
# top 10 alerts for dashboard icon
@@ -109,6 +113,8 @@ class GetAddAlerts(APIView):
class GetUpdateDeleteAlert(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
def get(self, request, pk):
alert = get_object_or_404(Alert, pk=pk)
@@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView):
class BulkAlerts(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
def post(self, request):
if request.data["bulk_action"] == "resolve":
Alert.objects.filter(id__in=request.data["alerts"]).update(
@@ -185,6 +193,8 @@ class BulkAlerts(APIView):
class GetAddAlertTemplates(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
def get(self, request):
alert_templates = AlertTemplate.objects.all()
@@ -202,6 +212,8 @@ class GetAddAlertTemplates(APIView):
class GetUpdateDeleteAlertTemplate(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)

View File

@@ -6,6 +6,7 @@ from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker
from autotasks.models import AutomatedTask
from tacticalrmm.test import TacticalTestCase
@@ -203,3 +204,139 @@ class TestAPIv3(TacticalTestCase):
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
reload_nats.assert_called_once()
def test_task_runner_get(self):
from autotasks.serializers import TaskGOGetSerializer
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
self.assertEqual(r.status_code, 404)
# setup data
agent = baker.make_recipe("agents.agent")
script = baker.make_recipe("scripts.script")
task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskGOGetSerializer(task).data, r.data) # type: ignore
def test_task_runner_results(self):
from agents.models import AgentCustomField
r = self.client.patch("/api/v3/500/asdf9df9dfdf/taskrunner/")
self.assertEqual(r.status_code, 404)
# setup data
agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
# test passing task
data = {
"stdout": "test test \ntestest stdgsd\n",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing") # type: ignore
# test failing task
data = {
"stdout": "test test \ntestest stdgsd\n",
"stderr": "",
"retcode": 1,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
# test collector task
text = baker.make("core.CustomField", model="agent", type="text", name="Test")
boolean = baker.make(
"core.CustomField", model="agent", type="checkbox", name="Test1"
)
multiple = baker.make(
"core.CustomField", model="agent", type="multiple", name="Test2"
)
# test text fields
task.custom_field = text # type: ignore
task.save() # type: ignore
# test failing failing with stderr
data = {
"stdout": "test test \nthe last line",
"stderr": "This is an error",
"retcode": 1,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
# test saving to text field
data = {
"stdout": "test test \nthe last line",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line") # type: ignore
# test saving to checkbox field
task.custom_field = boolean # type: ignore
task.save() # type: ignore
data = {
"stdout": "1",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value) # type: ignore
# test saving to multiple field with commas
task.custom_field = multiple # type: ignore
task.save() # type: ignore
data = {
"stdout": "this,is,an,array",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"]) # type: ignore
# test mutiple with a single value
data = {
"stdout": "this",
"stderr": "",
"retcode": 0,
"execution_time": 3.560,
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"]) # type: ignore

View File

@@ -15,7 +15,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from accounts.models import User
from agents.models import Agent
from agents.models import Agent, AgentCustomField
from agents.serializers import WinAgentSerializer
from autotasks.models import AutomatedTask
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
@@ -65,9 +65,17 @@ class CheckIn(APIView):
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
agent.handle_pending_actions()
# sync scheduled tasks
if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore
tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore
for task in tasks:
if task.sync_status == "pendingdeletion":
task.delete_task_on_agent()
elif task.sync_status == "initial":
task.modify_task_on_agent()
elif task.sync_status == "notsynced":
task.create_task_on_agent()
return Response("ok")
@@ -296,10 +304,11 @@ class CheckRunner(APIView):
< 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
)
# if check interval isn't set, make sure the agent's check interval has passed before running
or (
check.last_run
not check.run_interval
and check.last_run
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
)
]
@@ -312,11 +321,16 @@ class CheckRunner(APIView):
def patch(self, request):
check = get_object_or_404(Check, pk=request.data["id"])
if pyver.parse(check.agent.version) < pyver.parse("1.5.7"):
return notify_error("unsupported")
check.last_run = djangotime.now()
check.save(update_fields=["last_run"])
status = check.handle_checkv2(request.data)
status = check.handle_check(request.data)
if status == "failing" and check.assignedtask.exists(): # type: ignore
check.handle_assigned_task()
return Response(status)
return Response("ok")
class CheckRunnerInterval(APIView):
@@ -351,11 +365,51 @@ class TaskRunner(APIView):
instance=task, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save(last_run=djangotime.now())
new_task = serializer.save(last_run=djangotime.now())
status = "failing" if task.retcode != 0 else "passing"
# check if task is a collector and update the custom field
if task.custom_field:
if not task.stderr:
if AgentCustomField.objects.filter(
field=task.custom_field, agent=task.agent
).exists():
agent_field = AgentCustomField.objects.get(
field=task.custom_field, agent=task.agent
)
else:
agent_field = AgentCustomField.objects.create(
field=task.custom_field, agent=task.agent
)
# get last line of stdout
value = (
new_task.stdout
if task.collector_all_output
else new_task.stdout.split("\n")[-1].strip()
)
if task.custom_field.type in [
"text",
"number",
"single",
"datetime",
]:
agent_field.string_value = value
agent_field.save()
elif task.custom_field.type == "multiple":
agent_field.multiple_value = value.split(",")
agent_field.save()
elif task.custom_field.type == "checkbox":
agent_field.bool_value = bool(value)
agent_field.save()
status = "passing"
else:
status = "failing"
else:
status = "failing" if task.retcode != 0 else "passing"
new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
new_task.status = status
new_task.save()
@@ -393,7 +447,7 @@ class SysInfo(APIView):
class MeshExe(APIView):
""" Sends the mesh exe to the installer """
"""Sends the mesh exe to the installer"""
def post(self, request):
exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"

View File

@@ -29,7 +29,7 @@ class Policy(BaseAuditModel):
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_from_policies_task
from automation.tasks import generate_agent_checks_task
# get old policy if exists
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
@@ -38,8 +38,8 @@ class Policy(BaseAuditModel):
# generate agent checks only if active and enforced were changed
if old_policy:
if old_policy.active != self.active or old_policy.enforced != self.enforced:
generate_agent_checks_from_policies_task.delay(
policypk=self.pk,
generate_agent_checks_task.delay(
policy=self.pk,
create_tasks=True,
)
@@ -52,7 +52,10 @@ class Policy(BaseAuditModel):
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
super(BaseAuditModel, self).delete(*args, **kwargs)
generate_agent_checks_task.delay(agents, create_tasks=True)
generate_agent_checks_task.delay(agents=agents, create_tasks=True)
def __str__(self):
return self.name
@property
def is_default_server_policy(self):
@@ -62,9 +65,6 @@ class Policy(BaseAuditModel):
def is_default_workstation_policy(self):
return self.default_workstation_policy.exists() # type: ignore
def __str__(self):
return self.name
def is_agent_excluded(self, agent):
return (
agent in self.excluded_agents.all()
@@ -94,20 +94,29 @@ class Policy(BaseAuditModel):
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
and site.client not in self.excluded_clients.all()
],
monitoring_type=mon_type,
).values_list("pk", flat=True)
filtered_agents_pks |= (
Agent.objects.exclude(block_policy_inheritance=True)
.filter(
site__in=[
site
for site in explicit_sites
if site.client not in explicit_clients
and site.client not in self.excluded_clients.all()
],
monitoring_type=mon_type,
)
.values_list("pk", flat=True)
)
filtered_agents_pks |= Agent.objects.filter(
site__client__in=[client for client in explicit_clients],
monitoring_type=mon_type,
).values_list("pk", flat=True)
filtered_agents_pks |= (
Agent.objects.exclude(block_policy_inheritance=True)
.exclude(site__block_policy_inheritance=True)
.filter(
site__client__in=[client for client in explicit_clients],
monitoring_type=mon_type,
)
.values_list("pk", flat=True)
)
return Agent.objects.filter(
models.Q(pk__in=filtered_agents_pks)
@@ -123,9 +132,6 @@ class Policy(BaseAuditModel):
@staticmethod
def cascade_policy_tasks(agent):
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
from logs.models import PendingAction
# List of all tasks to be applied
tasks = list()
@@ -154,6 +160,17 @@ class Policy(BaseAuditModel):
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
if (
agent_policy
and agent_policy.active
@@ -200,26 +217,16 @@ class Policy(BaseAuditModel):
if taskpk not in added_task_pks
]
):
delete_win_task_schedule.delay(task.pk)
if task.sync_status == "initial":
task.delete()
else:
task.sync_status = "pendingdeletion"
task.save()
# handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
for action in agent.pendingactions.filter(action_type="taskaction").exclude(
status="completed"
):
task = AutomatedTask.objects.get(pk=action.details["task_id"])
if (
task.parent_task in agent_tasks_parent_pks
and task.parent_task in added_task_pks
):
agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=agent,
action_type="taskaction",
details={"action": "taskcreate", "task_id": task.id},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
# change tasks from pendingdeletion to notsynced if policy was added or changed
agent.autotasks.filter(sync_status="pendingdeletion").filter(
parent_task__in=[taskpk for taskpk in added_task_pks]
).update(sync_status="notsynced")
return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
@@ -251,6 +258,17 @@ class Policy(BaseAuditModel):
client_policy = client.workstation_policy
site_policy = site.workstation_policy
# check if client/site/agent is blocking inheritance and blank out policies
if agent.block_policy_inheritance:
site_policy = None
client_policy = None
default_policy = None
elif site.block_policy_inheritance:
client_policy = None
default_policy = None
elif client.block_policy_inheritance:
default_policy = None
# Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first
enforced_checks = list()
@@ -412,11 +430,12 @@ class Policy(BaseAuditModel):
# remove policy checks from agent that fell out of policy scope
agent.agentchecks.filter(
managed_by_policy=True,
parent_check__in=[
checkpk
for checkpk in agent_checks_parent_pks
if checkpk not in [check.pk for check in final_list]
]
],
).delete()
return [

View File

@@ -0,0 +1,11 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class AutomationPolicyPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_automation_policies")

View File

@@ -83,6 +83,7 @@ class PolicyCheckSerializer(ModelSerializer):
class AutoTasksFieldSerializer(ModelSerializer):
assigned_check = PolicyCheckSerializer(read_only=True)
script = ReadOnlyField(source="script.id")
custom_field = ReadOnlyField(source="custom_field.id")
class Meta:
model = AutomatedTask

View File

@@ -1,169 +1,153 @@
from agents.models import Agent
from automation.models import Policy
from autotasks.models import AutomatedTask
from checks.models import Check
from typing import Any, Dict, List, Union
from tacticalrmm.celery import app
@app.task
# generates policy checks on agents affected by a policy and optionally generate automated tasks
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
def generate_agent_checks_task(
policy: int = None,
site: int = None,
client: int = None,
agents: List[int] = list(),
all: bool = False,
create_tasks: bool = False,
) -> Union[str, None]:
from agents.models import Agent
from automation.models import Policy
policy = Policy.objects.get(pk=policypk)
p = Policy.objects.get(pk=policy) if policy else None
if policy.is_default_server_policy and policy.is_default_workstation_policy:
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation").only(
# generate checks on all agents if all is specified or if policy is default server/workstation policy
if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all:
a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
# generate checks on all servers if policy is a default servers policy
elif p and p.is_default_server_policy:
a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type")
# generate checks on all workstations if policy is a default workstations policy
elif p and p.is_default_workstation_policy:
a = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
# generate checks on a list of supplied agents
elif agents:
a = Agent.objects.filter(pk__in=agents)
# generate checks on agents affected by supplied policy
elif policy:
a = p.related_agents().only("pk")
# generate checks that has specified site
elif site:
a = Agent.objects.filter(site_id=site)
# generate checks that has specified client
elif client:
a = Agent.objects.filter(site__client_id=client)
else:
agents = policy.related_agents().only("pk")
a = []
for agent in agents:
for agent in a:
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# generates policy checks on a list of agents and optionally generate automated tasks
def generate_agent_checks_task(agentpks, create_tasks=False):
for agent in Agent.objects.filter(pk__in=agentpks):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
return "ok"
@app.task
# generates policy checks on agent servers or workstations within a certain client or site and optionally generate automated tasks
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# generates policy checks on all agent servers or workstations and optionally generate automated tasks
def generate_all_agent_checks_task(mon_type, create_tasks=False):
for agent in Agent.objects.filter(monitoring_type=mon_type):
agent.generate_checks_from_policies()
if create_tasks:
agent.generate_tasks_from_policies()
@app.task
# deletes a policy managed check from all agents
def delete_policy_check_task(checkpk):
Check.objects.filter(parent_check=checkpk).delete()
@app.task
@app.task(
acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
)
# updates policy managed check fields on agents
def update_policy_check_fields_task(checkpk):
def update_policy_check_fields_task(check: int) -> str:
from checks.models import Check
check = Check.objects.get(pk=checkpk)
c: Check = Check.objects.get(pk=check)
update_fields: Dict[Any, Any] = {}
Check.objects.filter(parent_check=checkpk).update(
warning_threshold=check.warning_threshold,
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,
script=check.script,
script_args=check.script_args,
info_return_codes=check.info_return_codes,
warning_return_codes=check.warning_return_codes,
timeout=check.timeout,
pass_if_start_pending=check.pass_if_start_pending,
pass_if_svc_not_exist=check.pass_if_svc_not_exist,
restart_if_stopped=check.restart_if_stopped,
log_name=check.log_name,
event_id=check.event_id,
event_id_is_wildcard=check.event_id_is_wildcard,
event_type=check.event_type,
event_source=check.event_source,
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,
)
for field in c.policy_fields_to_copy:
update_fields[field] = getattr(c, field)
Check.objects.filter(parent_check=check).update(**update_fields)
return "ok"
@app.task
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
# generates policy tasks on agents affected by a policy
def generate_agent_tasks_from_policies_task(policypk):
def generate_agent_autotasks_task(policy: int = None) -> str:
from agents.models import Agent
from automation.models import Policy
policy = Policy.objects.get(pk=policypk)
p: Policy = Policy.objects.get(pk=policy)
if policy.is_default_server_policy and policy.is_default_workstation_policy:
if p and p.is_default_server_policy and p.is_default_workstation_policy:
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy:
elif p and p.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy:
elif p and p.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else:
agents = policy.related_agents().only("pk")
agents = p.related_agents().only("pk")
for agent in agents:
agent.generate_tasks_from_policies()
return "ok"
@app.task
def delete_policy_autotask_task(taskpk):
@app.task(
acks_late=True,
retry_backoff=5,
retry_jitter=True,
retry_kwargs={"max_retries": 5},
)
def delete_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
for task in AutomatedTask.objects.filter(parent_task=taskpk):
delete_win_task_schedule.delay(task.pk)
for t in AutomatedTask.objects.filter(parent_task=task):
t.delete_task_on_agent()
return "ok"
@app.task
def run_win_policy_autotask_task(task_pks):
from autotasks.tasks import run_win_task
def run_win_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask
for task in task_pks:
run_win_task.delay(task)
for t in AutomatedTask.objects.filter(parent_task=task):
t.run_win_task()
return "ok"
@app.task
def update_policy_task_fields_task(taskpk, update_agent=False):
from autotasks.tasks import enable_or_disable_win_task
@app.task(
acks_late=True,
retry_backoff=5,
retry_jitter=True,
retry_kwargs={"max_retries": 5},
)
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
from autotasks.models import AutomatedTask
task = AutomatedTask.objects.get(pk=taskpk)
t = AutomatedTask.objects.get(pk=task)
update_fields: Dict[str, Any] = {}
AutomatedTask.objects.filter(parent_task=taskpk).update(
alert_severity=task.alert_severity,
email_alert=task.email_alert,
text_alert=task.text_alert,
dashboard_alert=task.dashboard_alert,
script=task.script,
script_args=task.script_args,
name=task.name,
timeout=task.timeout,
enabled=task.enabled,
)
for field in t.policy_fields_to_copy:
update_fields[field] = getattr(t, field)
AutomatedTask.objects.filter(parent_task=task).update(**update_fields)
if update_agent:
for task in AutomatedTask.objects.filter(parent_task=taskpk):
enable_or_disable_win_task.delay(task.pk, task.enabled)
for t in AutomatedTask.objects.filter(parent_task=task).exclude(
sync_status="initial"
):
t.modify_task_on_agent()
return "ok"

View File

@@ -1,10 +1,9 @@
from itertools import cycle
from unittest.mock import patch
from model_bakery import baker, seq
from agents.models import Agent
from core.models import CoreSettings
from model_bakery import baker, seq
from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
@@ -52,7 +51,10 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_add_policy(self):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_add_policy(self, create_task):
from automation.models import Policy
url = "/automation/policies/"
data = {
@@ -71,8 +73,12 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks and checks
policy = baker.make("automation.Policy")
self.create_checks(policy=policy)
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
checks = self.create_checks(policy=policy)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
# assign a task to a check
tasks[0].assigned_check = checks[0] # type: ignore
tasks[0].save() # type: ignore
# test copy tasks and checks to another policy
data = {
@@ -85,13 +91,21 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.post(f"/automation/policies/", data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
copied_policy = Policy.objects.get(name=data["name"])
self.assertEqual(copied_policy.autotasks.count(), 3) # type: ignore
self.assertEqual(copied_policy.policychecks.count(), 7) # type: ignore
# make sure correct task was assign to the check
self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type) # type: ignore
create_task.assert_not_called()
self.check_not_authenticated("post", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_update_policy(self, generate_agent_checks_from_policies_task):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_update_policy(self, generate_agent_checks_task):
# returns 404 for invalid policy pk
resp = self.client.put("/automation/policies/500/", format="json")
self.assertEqual(resp.status_code, 404)
@@ -109,8 +123,8 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# only called if active or enforced are updated
generate_agent_checks_from_policies_task.assert_not_called()
# only called if active, enforced, or excluded objects are updated
generate_agent_checks_task.assert_not_called()
data = {
"name": "Test Policy Update",
@@ -121,8 +135,25 @@ 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 # type: ignore
generate_agent_checks_task.assert_called_with(
policy=policy.pk, create_tasks=True # type: ignore
)
generate_agent_checks_task.reset_mock()
# make sure policies are re-evaluated when excluded changes
agents = baker.make_recipe("agents.agent", _quantity=2)
clients = baker.make("clients.Client", _quantity=2)
sites = baker.make("clients.Site", _quantity=2)
data = {
"excluded_agents": [agent.pk for agent in agents], # type: ignore
"excluded_sites": [site.pk for site in sites], # type: ignore
"excluded_clients": [client.pk for client in clients], # type: ignore
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_checks_task.assert_called_with(
policy=policy.pk, create_tasks=True # type: ignore
)
self.check_not_authenticated("put", url)
@@ -145,7 +176,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
generate_agent_checks_task.assert_called_with(
[agent.pk for agent in agents], create_tasks=True
agents=[agent.pk for agent in agents], create_tasks=True
)
self.check_not_authenticated("delete", url)
@@ -271,7 +302,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
@patch("automation.tasks.run_win_policy_autotask_task.delay")
@patch("automation.tasks.run_win_policy_autotasks_task.delay")
def test_run_win_task(self, mock_task):
# create managed policy tasks
@@ -281,11 +312,12 @@ class TestPolicyViews(TacticalTestCase):
parent_task=1,
_quantity=6,
)
url = "/automation/runwintask/1/"
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]) # type: ignore
mock_task.assert_called() # type: ignore
self.check_not_authenticated("put", url)
@@ -426,7 +458,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_sync_policy(self, generate_checks):
url = "/automation/sync/"
@@ -441,7 +473,7 @@ class TestPolicyViews(TacticalTestCase):
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
generate_checks.assert_called_with(policy=policy.pk, create_tasks=True) # type: ignore
self.check_not_authenticated("post", url)
@@ -497,7 +529,7 @@ class TestPolicyTasks(TacticalTestCase):
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
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -505,7 +537,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) # type: ignore
generate_agent_checks_task(policy=policy.id) # type: ignore
# make sure all checks were created. should be 7
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
@@ -545,7 +577,7 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(check.event_type, checks[6].event_type)
def test_generating_agent_policy_checks_with_enforced(self):
from .tasks import generate_agent_checks_from_policies_task
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True, enforced=True)
@@ -555,7 +587,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) # type: ignore
generate_agent_checks_task(policy=policy.id, create_tasks=True) # type: ignore
# make sure each agent check says overriden_by_policy
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
@@ -566,13 +598,12 @@ class TestPolicyTasks(TacticalTestCase):
7,
)
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_generating_agent_policy_checks_by_location(
self, generate_agent_checks_by_location_task
self, generate_agent_checks_mock, create_task
):
from automation.tasks import (
generate_agent_checks_by_location_task as generate_agent_checks,
)
from automation.tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -596,16 +627,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
client=workstation_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_task(
client=workstation_agent.client.pk,
create_tasks=True,
)
@@ -620,16 +649,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
client=workstation_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
generate_agent_checks_task(
client=workstation_agent.client.pk,
create_tasks=True,
)
@@ -644,16 +671,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
client=server_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_task(
client=server_agent.client.pk,
create_tasks=True,
)
@@ -668,16 +693,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.client.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
client=server_agent.client.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
generate_agent_checks_task(
client=server_agent.client.pk,
create_tasks=True,
)
@@ -692,16 +715,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
site=workstation_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_task(
site=workstation_agent.site.pk,
create_tasks=True,
)
@@ -716,16 +737,14 @@ class TestPolicyTasks(TacticalTestCase):
workstation_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_mock.assert_called_with(
site=workstation_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
generate_agent_checks_task(
site=workstation_agent.site.pk,
create_tasks=True,
)
@@ -740,16 +759,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
site=server_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_task(
site=server_agent.site.pk,
create_tasks=True,
)
@@ -764,16 +781,14 @@ class TestPolicyTasks(TacticalTestCase):
server_agent.site.save()
# should trigger task in save method on core
generate_agent_checks_by_location_task.assert_called_with(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_mock.assert_called_with(
site=server_agent.site.pk,
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks_mock.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
generate_agent_checks_task(
site=server_agent.site.pk,
create_tasks=True,
)
@@ -783,13 +798,11 @@ class TestPolicyTasks(TacticalTestCase):
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_generating_policy_checks_for_all_agents(
self, generate_all_agent_checks_task
):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
from core.models import CoreSettings
from .tasks import generate_all_agent_checks_task as generate_all_checks
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -801,11 +814,9 @@ class TestPolicyTasks(TacticalTestCase):
core.server_policy = policy
core.save()
generate_all_agent_checks_task.assert_called_with(
mon_type="server", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="server", create_tasks=True)
generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
generate_agent_checks_mock.reset_mock()
generate_agent_checks_task(all=True, create_tasks=True)
# all servers should have 7 checks
for agent in server_agents:
@@ -818,15 +829,9 @@ class TestPolicyTasks(TacticalTestCase):
core.workstation_policy = policy
core.save()
generate_all_agent_checks_task.assert_any_call(
mon_type="workstation", create_tasks=True
)
generate_all_agent_checks_task.assert_any_call(
mon_type="server", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="server", create_tasks=True)
generate_all_checks(mon_type="workstation", create_tasks=True)
generate_agent_checks_mock.assert_any_call(all=True, create_tasks=True)
generate_agent_checks_mock.reset_mock()
generate_agent_checks_task(all=True, create_tasks=True)
# all workstations should have 7 checks
for agent in server_agents:
@@ -838,11 +843,9 @@ class TestPolicyTasks(TacticalTestCase):
core.workstation_policy = None
core.save()
generate_all_agent_checks_task.assert_called_with(
mon_type="workstation", create_tasks=True
)
generate_all_agent_checks_task.reset_mock()
generate_all_checks(mon_type="workstation", create_tasks=True)
generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
generate_agent_checks_mock.reset_mock()
generate_agent_checks_task(all=True, create_tasks=True)
# nothing should have the checks
for agent in server_agents:
@@ -851,31 +854,8 @@ class TestPolicyTasks(TacticalTestCase):
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
def test_delete_policy_check(self):
from .models import Policy
from .tasks import delete_policy_check_task
policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy)
agent = baker.make_recipe("agents.server_agent", policy=policy)
# make sure agent has 7 checks
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
# pick a policy check and delete it from the agent
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id # type: ignore
delete_policy_check_task(policy_check_id)
# make sure policy check doesn't exist on agent
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 6)
self.assertFalse(
Agent.objects.get(pk=agent.id)
.agentchecks.filter(parent_check=policy_check_id)
.exists()
)
def update_policy_check_fields(self):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def update_policy_check_fields(self, create_task):
from .models import Policy
from .tasks import update_policy_check_fields_task
@@ -905,8 +885,9 @@ class TestPolicyTasks(TacticalTestCase):
"12.12.12.12",
)
def test_generate_agent_tasks(self):
from .tasks import generate_agent_tasks_from_policies_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_generate_agent_tasks(self, create_task):
from .tasks import generate_agent_autotasks_task
# create test data
policy = baker.make("automation.Policy", active=True)
@@ -915,7 +896,7 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_tasks_from_policies_task(policy.id) # type: ignore
generate_agent_autotasks_task(policy=policy.id) # type: ignore
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
@@ -934,56 +915,61 @@ class TestPolicyTasks(TacticalTestCase):
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):
from .tasks import delete_policy_autotask_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.delete_task_on_agent")
def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
from .tasks import delete_policy_autotasks_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
agent = baker.make_recipe("agents.server_agent", policy=policy)
baker.make_recipe("agents.server_agent", policy=policy)
delete_policy_autotask_task(tasks[0].id) # type: ignore
delete_policy_autotasks_task(task=tasks[0].id) # type: ignore
delete_win_task_schedule.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id # type: ignore
)
delete_task_on_agent.assert_called()
@patch("autotasks.tasks.run_win_task.delay")
def test_run_policy_task(self, run_win_task):
from .tasks import run_win_policy_autotask_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.run_win_task")
def test_run_policy_task(self, run_win_task, create_task):
from .tasks import run_win_policy_autotasks_task
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
baker.make_recipe("agents.server_agent", policy=policy)
run_win_policy_autotask_task([task.id for task in tasks]) # type: ignore
run_win_policy_autotasks_task(task=tasks[0].id) # type: ignore
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: # type: ignore
run_win_task.assert_any_call(task.id) # type: ignore
run_win_task.assert_called_once()
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
def test_update_policy_tasks(self, enable_or_disable_win_task):
from .tasks import update_policy_task_fields_task
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.modify_task_on_agent")
def test_update_policy_tasks(self, modify_task_on_agent, create_task):
from .tasks import update_policy_autotasks_fields_task
# setup data
policy = baker.make("automation.Policy", active=True)
tasks = baker.make(
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
"autotasks.AutomatedTask",
enabled=True,
policy=policy,
_quantity=3,
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
tasks[0].enabled = False # type: ignore
tasks[0].save() # type: ignore
update_policy_task_fields_task(tasks[0].id) # type: ignore
enable_or_disable_win_task.assert_not_called()
update_policy_autotasks_fields_task(task=tasks[0].id) # type: ignore
modify_task_on_agent.assert_not_called()
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore
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 # type: ignore
)
update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore
modify_task_on_agent.assert_not_called()
agent.autotasks.update(sync_status="synced")
update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore
modify_task_on_agent.assert_called_once()
@patch("agents.models.Agent.generate_tasks_from_policies")
@patch("agents.models.Agent.generate_checks_from_policies")
@@ -996,17 +982,19 @@ class TestPolicyTasks(TacticalTestCase):
generate_checks.reset_mock()
generate_tasks.reset_mock()
generate_agent_checks_task([agent.pk for agent in agents])
generate_agent_checks_task(agents=[agent.pk for agent in agents])
self.assertEquals(generate_checks.call_count, 5)
generate_tasks.assert_not_called()
generate_checks.reset_mock()
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
generate_agent_checks_task(
agents=[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):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_policy_exclusions(self, create_task):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
@@ -1028,8 +1016,6 @@ class TestPolicyTasks(TacticalTestCase):
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()
@@ -1051,8 +1037,6 @@ class TestPolicyTasks(TacticalTestCase):
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()
@@ -1074,8 +1058,6 @@ class TestPolicyTasks(TacticalTestCase):
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()
@@ -1103,11 +1085,82 @@ class TestPolicyTasks(TacticalTestCase):
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
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_policy_inheritance_blocking(self, create_task):
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
baker.make("autotasks.AutomatedTask", policy=policy)
agent = baker.make_recipe("agents.agent", monitoring_type="server")
def test_creating_checks_with_assigned_tasks(self):
pass
core = CoreSettings.objects.first()
core.server_policy = policy
core.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from default policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
# test client blocking inheritance
agent.site.client.block_policy_inheritance = True
agent.site.client.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertFalse(agent.autotasks.all())
self.assertFalse(agent.agentchecks.all())
agent.site.client.server_policy = policy
agent.site.client.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from client policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
# test site blocking inheritance
agent.site.block_policy_inheritance = True
agent.site.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertFalse(agent.autotasks.all())
self.assertFalse(agent.agentchecks.all())
agent.site.server_policy = policy
agent.site.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from site policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
# test agent blocking inheritance
agent.block_policy_inheritance = True
agent.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
self.assertFalse(agent.autotasks.all())
self.assertFalse(agent.agentchecks.all())
agent.policy = policy
agent.save()
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# should get policies from agent policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())

View File

@@ -1,18 +1,19 @@
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
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 django.shortcuts import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
from .permissions import AutomationPolicyPerms
from .serializers import (
AutoTasksFieldSerializer,
PolicyCheckSerializer,
@@ -22,10 +23,11 @@ from .serializers import (
PolicyTableSerializer,
PolicyTaskStatusSerializer,
)
from .tasks import run_win_policy_autotask_task
class GetAddPolicies(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request):
policies = Policy.objects.all()
@@ -53,18 +55,30 @@ class GetAddPolicies(APIView):
class GetUpdateDeletePolicy(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request, pk):
policy = get_object_or_404(Policy, pk=pk)
return Response(PolicySerializer(policy).data)
def put(self, request, pk):
from .tasks import generate_agent_checks_task
policy = get_object_or_404(Policy, pk=pk)
serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
# check for excluding objects and in the request and if present generate policies
if (
"excluded_sites" in request.data.keys()
or "excluded_clients" in request.data.keys()
or "excluded_agents" in request.data.keys()
):
generate_agent_checks_task.delay(policy=pk, create_tasks=True)
return Response("ok")
def delete(self, request, pk):
@@ -76,10 +90,10 @@ class GetUpdateDeletePolicy(APIView):
class PolicySync(APIView):
def post(self, request):
if "policy" in request.data.keys():
from automation.tasks import generate_agent_checks_from_policies_task
from automation.tasks import generate_agent_checks_task
generate_agent_checks_from_policies_task.delay(
request.data["policy"], create_tasks=True
generate_agent_checks_task.delay(
policy=request.data["policy"], create_tasks=True
)
return Response("ok")
@@ -88,7 +102,7 @@ class PolicySync(APIView):
class PolicyAutoTask(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
# tasks associated with policy
def get(self, request, pk):
tasks = AutomatedTask.objects.filter(policy=pk)
@@ -101,12 +115,15 @@ class PolicyAutoTask(APIView):
# bulk run win tasks associated with policy
def put(self, request, task):
tasks = AutomatedTask.objects.filter(parent_task=task)
run_win_policy_autotask_task.delay([task.id for task in tasks])
from .tasks import run_win_policy_autotasks_task
run_win_policy_autotasks_task.delay(task=task)
return Response("Affected agent tasks will run shortly")
class PolicyCheck(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request, pk):
checks = Check.objects.filter(policy__pk=pk, agent=None)
return Response(PolicyCheckSerializer(checks, many=True).data)
@@ -179,7 +196,7 @@ class GetRelated(APIView):
class UpdatePatchPolicy(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
# create new patch policy
def post(self, request):
policy = get_object_or_404(Policy, pk=request.data["policy"])

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.7 on 2021-04-04 00:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0019_globalkvstore'),
('scripts', '0007_script_args'),
('autotasks', '0018_automatedtask_run_asap_after_missed'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='custom_field',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotask', to='core.customfield'),
),
migrations.AddField(
model_name='automatedtask',
name='retvalue',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='automatedtask',
name='script',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autoscript', to='scripts.script'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-21 02:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0019_auto_20210404_0032'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='sync_status',
field=models.CharField(choices=[('synced', 'Synced With Agent'), ('notsynced', 'Waiting On Agent Checkin'), ('pendingdeletion', 'Pending Deletion on Agent'), ('initial', 'Initial Task Sync')], default='initial', max_length=100),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-04-27 14:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_customfield_hide_in_ui'),
('autotasks', '0020_auto_20210421_0226'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='custom_field',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotasks', to='core.customfield'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2021-05-29 03:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0021_alter_automatedtask_custom_field'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='collector_all_output',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,16 +1,20 @@
import asyncio
import datetime as dt
import random
import string
from typing import List
import pytz
from alerts.models import SEVERITY_CHOICES
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 loguru import logger
from alerts.models import SEVERITY_CHOICES
from django.db.utils import DatabaseError
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel
from loguru import logger
from packaging import version as pyver
from tacticalrmm.utils import bitdays_to_string
logger.configure(**settings.LOG_CONFIG)
@@ -36,6 +40,7 @@ SYNC_STATUS_CHOICES = [
("synced", "Synced With Agent"),
("notsynced", "Waiting On Agent Checkin"),
("pendingdeletion", "Pending Deletion on Agent"),
("initial", "Initial Task Sync"),
]
TASK_STATUS_CHOICES = [
@@ -60,12 +65,19 @@ class AutomatedTask(BaseAuditModel):
blank=True,
on_delete=models.CASCADE,
)
custom_field = models.ForeignKey(
"core.CustomField",
related_name="autotasks",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
script = models.ForeignKey(
"scripts.Script",
null=True,
blank=True,
related_name="autoscript",
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
)
script_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
@@ -93,6 +105,7 @@ class AutomatedTask(BaseAuditModel):
task_type = models.CharField(
max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
)
collector_all_output = models.BooleanField(default=False)
run_time_date = DateTimeField(null=True, blank=True)
remove_if_not_scheduled = models.BooleanField(default=False)
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
@@ -100,6 +113,7 @@ class AutomatedTask(BaseAuditModel):
parent_task = models.PositiveIntegerField(null=True, blank=True)
win_task_name = models.CharField(max_length=255, null=True, blank=True)
timeout = models.PositiveIntegerField(default=120)
retvalue = models.TextField(null=True, blank=True)
retcode = models.IntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
@@ -110,7 +124,7 @@ class AutomatedTask(BaseAuditModel):
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
)
sync_status = models.CharField(
max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
max_length=100, choices=SYNC_STATUS_CHOICES, default="initial"
)
alert_severity = models.CharField(
max_length=30, choices=SEVERITY_CHOICES, default="info"
@@ -147,6 +161,32 @@ class AutomatedTask(BaseAuditModel):
return self.last_run
# These fields will be duplicated on the agent tasks that are managed by a policy
@property
def policy_fields_to_copy(self) -> List[str]:
return [
"alert_severity",
"email_alert",
"text_alert",
"dashboard_alert",
"script",
"script_args",
"assigned_check",
"name",
"run_time_days",
"run_time_minute",
"run_time_bit_weekdays",
"run_time_date",
"task_type",
"win_task_name",
"timeout",
"enabled",
"remove_if_not_scheduled",
"run_asap_after_missed",
"custom_field",
"collector_all_output",
]
@staticmethod
def generate_task_name():
chars = string.ascii_letters
@@ -159,69 +199,200 @@ class AutomatedTask(BaseAuditModel):
return TaskSerializer(task).data
def create_policy_task(self, agent=None, policy=None):
from .tasks import create_win_task_schedule
def create_policy_task(self, agent=None, policy=None, assigned_check=None):
# if policy is present, then this task is being copied to another policy
# if agent is present, then this task is being created on an agent from a policy
# exit if neither are set or if both are set
if not agent and not policy or agent and policy:
# also exit if assigned_check is set because this task will be created when the check is
if (
(not agent and not policy)
or (agent and policy)
or (self.assigned_check and not assigned_check)
):
return
assigned_check = None
# get correct assigned check to task if set
if agent and self.assigned_check:
# check if there is a matching check on the agent
if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
assigned_check = agent.agentchecks.filter(
parent_check=self.assigned_check.pk
).first()
# check was overriden by agent and we need to use that agents check
else:
if agent.agentchecks.filter(
check_type=self.assigned_check.check_type, overriden_by_policy=True
).exists():
assigned_check = agent.agentchecks.filter(
check_type=self.assigned_check.check_type,
overriden_by_policy=True,
).first()
elif policy and self.assigned_check:
if policy.policychecks.filter(name=self.assigned_check.name).exists():
assigned_check = policy.policychecks.filter(
name=self.assigned_check.name
).first()
else:
assigned_check = policy.policychecks.filter(
check_type=self.assigned_check.check_type
).first()
task = AutomatedTask.objects.create(
agent=agent,
policy=policy,
managed_by_policy=bool(agent),
parent_task=(self.pk if agent else None),
alert_severity=self.alert_severity,
email_alert=self.email_alert,
text_alert=self.text_alert,
dashboard_alert=self.dashboard_alert,
script=self.script,
script_args=self.script_args,
assigned_check=assigned_check,
name=self.name,
run_time_days=self.run_time_days,
run_time_minute=self.run_time_minute,
run_time_bit_weekdays=self.run_time_bit_weekdays,
run_time_date=self.run_time_date,
task_type=self.task_type,
win_task_name=self.win_task_name,
timeout=self.timeout,
enabled=self.enabled,
remove_if_not_scheduled=self.remove_if_not_scheduled,
run_asap_after_missed=self.run_asap_after_missed,
)
create_win_task_schedule.delay(task.pk)
for field in self.policy_fields_to_copy:
if field != "assigned_check":
setattr(task, field, getattr(self, field))
task.save()
if agent:
task.create_task_on_agent()
def create_task_on_agent(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
if self.task_type == "scheduled":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "weekly",
"weekdays": self.run_time_bit_weekdays,
"pk": self.pk,
"name": self.win_task_name,
"hour": dt.datetime.strptime(self.run_time_minute, "%H:%M").hour,
"min": dt.datetime.strptime(self.run_time_minute, "%H:%M").minute,
},
}
elif self.task_type == "runonce":
# check if scheduled time is in the past
agent_tz = pytz.timezone(agent.timezone)
task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
pytz.utc
)
now = djangotime.now()
if task_time_utc < now:
self.run_time_date = now.astimezone(agent_tz).replace(
tzinfo=pytz.utc
) + djangotime.timedelta(minutes=5)
self.save(update_fields=["run_time_date"])
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "once",
"pk": self.pk,
"name": self.win_task_name,
"year": int(dt.datetime.strftime(self.run_time_date, "%Y")),
"month": dt.datetime.strftime(self.run_time_date, "%B"),
"day": int(dt.datetime.strftime(self.run_time_date, "%d")),
"hour": int(dt.datetime.strftime(self.run_time_date, "%H")),
"min": int(dt.datetime.strftime(self.run_time_date, "%M")),
},
}
if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse(
"1.4.7"
):
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
if self.remove_if_not_scheduled:
nats_data["schedtaskpayload"]["deleteafter"] = True
elif self.task_type == "checkfailure" or self.task_type == "manual":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "manual",
"pk": self.pk,
"name": self.win_task_name,
},
}
else:
return "error"
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
if r != "ok":
self.sync_status = "initial"
self.save(update_fields=["sync_status"])
logger.warning(
f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in."
)
return "timeout"
else:
self.sync_status = "synced"
self.save(update_fields=["sync_status"])
logger.info(f"{agent.hostname} task {self.name} was successfully created")
return "ok"
def modify_task_on_agent(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
nats_data = {
"func": "enableschedtask",
"schedtaskpayload": {
"name": self.win_task_name,
"enabled": self.enabled,
},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
if r != "ok":
self.sync_status = "notsynced"
self.save(update_fields=["sync_status"])
logger.warning(
f"Unable to modify scheduled task {self.name} on {agent.hostname}. It will try again on next agent checkin"
)
return "timeout"
else:
self.sync_status = "synced"
self.save(update_fields=["sync_status"])
logger.info(f"{agent.hostname} task {self.name} was successfully modified")
return "ok"
def delete_task_on_agent(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": self.win_task_name},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if r != "ok" and "The system cannot find the file specified" not in r:
self.sync_status = "pendingdeletion"
try:
self.save(update_fields=["sync_status"])
except DatabaseError:
pass
logger.warning(
f"{agent.hostname} task {self.name} will be deleted on next checkin"
)
return "timeout"
else:
self.delete()
logger.info(f"{agent.hostname} task {self.name} was deleted")
return "ok"
def run_win_task(self):
from agents.models import Agent
agent = (
Agent.objects.filter(pk=self.agent.pk)
.only("pk", "version", "hostname", "agent_id")
.first()
)
asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))
return "ok"
def should_create_alert(self, alert_template=None):
return (

View File

@@ -0,0 +1,16 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageAutoTaskPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_autotasks")
class RunAutoTaskPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_autotasks")

View File

@@ -68,6 +68,12 @@ class TaskRunnerGetSerializer(serializers.ModelSerializer):
class TaskGOGetSerializer(serializers.ModelSerializer):
script = ScriptCheckSerializer(read_only=True)
script_args = serializers.SerializerMethodField()
def get_script_args(self, obj):
return Script.parse_script_args(
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
)
class Meta:
model = AutomatedTask

View File

@@ -4,207 +4,46 @@ import random
from time import sleep
from typing import Union
import pytz
from django.conf import settings
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from logs.models import PendingAction
from autotasks.models import AutomatedTask
from tacticalrmm.celery import app
from .models import AutomatedTask
logger.configure(**settings.LOG_CONFIG)
@app.task
def create_win_task_schedule(pk, pending_action=False):
def create_win_task_schedule(pk):
task = AutomatedTask.objects.get(pk=pk)
if task.task_type == "scheduled":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "weekly",
"weekdays": task.run_time_bit_weekdays,
"pk": task.pk,
"name": task.win_task_name,
"hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour,
"min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute,
},
}
elif task.task_type == "runonce":
# check if scheduled time is in the past
agent_tz = pytz.timezone(task.agent.timezone)
task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
now = djangotime.now()
if task_time_utc < now:
task.run_time_date = now.astimezone(agent_tz).replace(
tzinfo=pytz.utc
) + djangotime.timedelta(minutes=5)
task.save(update_fields=["run_time_date"])
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "once",
"pk": task.pk,
"name": task.win_task_name,
"year": int(dt.datetime.strftime(task.run_time_date, "%Y")),
"month": dt.datetime.strftime(task.run_time_date, "%B"),
"day": int(dt.datetime.strftime(task.run_time_date, "%d")),
"hour": int(dt.datetime.strftime(task.run_time_date, "%H")),
"min": int(dt.datetime.strftime(task.run_time_date, "%M")),
},
}
if task.run_asap_after_missed and pyver.parse(
task.agent.version
) >= pyver.parse("1.4.7"):
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
if task.remove_if_not_scheduled:
nats_data["schedtaskpayload"]["deleteafter"] = True
elif task.task_type == "checkfailure" or task.task_type == "manual":
nats_data = {
"func": "schedtask",
"schedtaskpayload": {
"type": "rmm",
"trigger": "manual",
"pk": task.pk,
"name": task.win_task_name,
},
}
else:
return "error"
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok":
# don't create pending action if this task was initiated by a pending action
if not pending_action:
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=task.agent,
action_type="taskaction",
details={"action": "taskcreate", "task_id": task.id},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
logger.error(
f"Unable to create scheduled task {task.win_task_name} on {task.agent.hostname}. It will be created when the agent checks in."
)
return
# clear pending action since it was successful
if pending_action:
pendingaction = PendingAction.objects.get(pk=pending_action)
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
task.sync_status = "synced"
task.save(update_fields=["sync_status"])
logger.info(f"{task.agent.hostname} task {task.name} was successfully created")
task.create_task_on_agent()
return "ok"
@app.task
def enable_or_disable_win_task(pk, action, pending_action=False):
def enable_or_disable_win_task(pk):
task = AutomatedTask.objects.get(pk=pk)
nats_data = {
"func": "enableschedtask",
"schedtaskpayload": {
"name": task.win_task_name,
"enabled": action,
},
}
r = asyncio.run(task.agent.nats_cmd(nats_data))
if r != "ok":
# don't create pending action if this task was initiated by a pending action
if not pending_action:
PendingAction(
agent=task.agent,
action_type="taskaction",
details={
"action": "tasktoggle",
"value": action,
"task_id": task.id,
},
).save()
task.sync_status = "notsynced"
task.save(update_fields=["sync_status"])
return
# clear pending action since it was successful
if pending_action:
pendingaction = PendingAction.objects.get(pk=pending_action)
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
task.sync_status = "synced"
task.save(update_fields=["sync_status"])
task.modify_task_on_agent()
return "ok"
@app.task
def delete_win_task_schedule(pk, pending_action=False):
def delete_win_task_schedule(pk):
task = AutomatedTask.objects.get(pk=pk)
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": task.win_task_name},
}
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok" and "The system cannot find the file specified" not in r:
# don't create pending action if this task was initiated by a pending action
if not pending_action:
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
PendingAction(
agent=task.agent,
action_type="taskaction",
details={"action": "taskdelete", "task_id": task.id},
).save()
task.sync_status = "pendingdeletion"
task.save(update_fields=["sync_status"])
return "timeout"
# complete pending action since it was successful
if pending_action:
pendingaction = PendingAction.objects.get(pk=pending_action)
pendingaction.status = "completed"
pendingaction.save(update_fields=["status"])
# complete any other pending actions on agent with same task_id
task.agent.remove_matching_pending_task_actions(task.id)
task.delete()
task.delete_task_on_agent()
return "ok"
@app.task
def run_win_task(pk):
task = AutomatedTask.objects.get(pk=pk)
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
task.run_win_task()
return "ok"

View File

@@ -4,7 +4,6 @@ from unittest.mock import call, patch
from django.utils import timezone as djangotime
from model_bakery import baker
from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase
from .models import AutomatedTask
@@ -17,10 +16,10 @@ class TestAutotaskViews(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
@patch("automation.tasks.generate_agent_tasks_from_policies_task.delay")
@patch("automation.tasks.generate_agent_autotasks_task.delay")
@patch("autotasks.tasks.create_win_task_schedule.delay")
def test_add_autotask(
self, create_win_task_schedule, generate_agent_tasks_from_policies_task
self, create_win_task_schedule, generate_agent_autotasks_task
):
url = "/tasks/automatedtasks/"
@@ -84,13 +83,13 @@ class TestAutotaskViews(TacticalTestCase):
"task_type": "manual",
"assigned_check": None,
},
"policy": policy.id,
"policy": policy.id, # type: ignore
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_tasks_from_policies_task.assert_called_with(policy.id)
generate_agent_autotasks_task.assert_called_with(policy=policy.id) # type: ignore
self.check_not_authenticated("post", url)
@@ -106,14 +105,14 @@ class TestAutotaskViews(TacticalTestCase):
serializer = AutoTaskSerializer(agent)
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)
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
@patch("automation.tasks.update_policy_task_fields_task.delay")
@patch("automation.tasks.update_policy_autotasks_fields_task.delay")
def test_update_autotask(
self, update_policy_task_fields_task, enable_or_disable_win_task
self, update_policy_autotasks_fields_task, enable_or_disable_win_task
):
# setup data
agent = baker.make_recipe("agents.agent")
@@ -125,32 +124,32 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.patch("/tasks/500/automatedtasks/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/tasks/{agent_task.id}/automatedtasks/"
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
# test editing agent task
data = {"enableordisable": False}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
enable_or_disable_win_task.assert_called_with(pk=agent_task.id, action=False)
enable_or_disable_win_task.assert_called_with(pk=agent_task.id) # type: ignore
url = f"/tasks/{policy_task.id}/automatedtasks/"
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
# test editing policy task
data = {"enableordisable": True}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
update_policy_task_fields_task.assert_called_with(
policy_task.id, update_agent=True
update_policy_autotasks_fields_task.assert_called_with(
task=policy_task.id, update_agent=True # type: ignore
)
self.check_not_authenticated("patch", url)
@patch("autotasks.tasks.delete_win_task_schedule.delay")
@patch("automation.tasks.delete_policy_autotask_task.delay")
@patch("automation.tasks.delete_policy_autotasks_task.delay")
def test_delete_autotask(
self, delete_policy_autotask_task, delete_win_task_schedule
self, delete_policy_autotasks_task, delete_win_task_schedule
):
# setup data
agent = baker.make_recipe("agents.agent")
@@ -163,21 +162,22 @@ class TestAutotaskViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
# test delete agent task
url = f"/tasks/{agent_task.id}/automatedtasks/"
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
delete_win_task_schedule.assert_called_with(pk=agent_task.id)
delete_win_task_schedule.assert_called_with(pk=agent_task.id) # type: ignore
# test delete policy task
url = f"/tasks/{policy_task.id}/automatedtasks/"
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
delete_policy_autotask_task.assert_called_with(policy_task.id)
self.assertFalse(AutomatedTask.objects.filter(pk=policy_task.id)) # type: ignore
delete_policy_autotasks_task.assert_called_with(task=policy_task.id) # type: ignore
self.check_not_authenticated("delete", url)
@patch("agents.models.Agent.nats_cmd")
def test_run_autotask(self, nats_cmd):
@patch("autotasks.tasks.run_win_task.delay")
def test_run_autotask(self, run_win_task):
# setup data
agent = baker.make_recipe("agents.agent", version="1.1.0")
task = baker.make("autotasks.AutomatedTask", agent=agent)
@@ -187,11 +187,10 @@ class TestAutotaskViews(TacticalTestCase):
self.assertEqual(resp.status_code, 404)
# test run agent task
url = f"/tasks/runwintask/{task.id}/"
url = f"/tasks/runwintask/{task.id}/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
nats_cmd.reset_mock()
run_win_task.assert_called()
self.check_not_authenticated("get", url)
@@ -284,9 +283,9 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
run_time_bit_weekdays=127,
run_time_minute="21:55",
)
self.assertEqual(self.task1.sync_status, "notsynced")
self.assertEqual(self.task1.sync_status, "initial")
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
self.assertEqual(nats_cmd.call_count, 1)
nats_cmd.assert_called_with(
{
@@ -301,29 +300,16 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"min": 55,
},
},
timeout=10,
timeout=5,
)
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "synced")
nats_cmd.return_value = "timeout"
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
self.assertEqual(ret.status, "SUCCESS")
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
self.assertEqual(self.task1.sync_status, "notsynced")
# test pending action
self.pending_action = PendingAction.objects.create(
agent=self.agent, action_type="taskaction"
)
self.assertEqual(self.pending_action.status, "pending")
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(
pk=self.task1.pk, pending_action=self.pending_action.pk
).apply()
self.assertEqual(ret.status, "SUCCESS")
self.pending_action = PendingAction.objects.get(pk=self.pending_action.pk)
self.assertEqual(self.pending_action.status, "completed")
self.assertEqual(self.task1.sync_status, "initial")
# test runonce with future date
nats_cmd.reset_mock()
@@ -337,7 +323,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
run_time_date=run_time_date,
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task2.pk).apply()
nats_cmd.assert_called_with(
{
"func": "schedtask",
@@ -353,7 +339,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")),
},
},
timeout=10,
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")
@@ -369,7 +355,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
run_time_date=run_time_date,
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task3.pk).apply()
self.task3 = AutomatedTask.objects.get(pk=self.task3.pk)
self.assertEqual(ret.status, "SUCCESS")
@@ -385,7 +371,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
assigned_check=self.check,
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task4.pk).apply()
nats_cmd.assert_called_with(
{
"func": "schedtask",
@@ -396,7 +382,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"name": task_name,
},
},
timeout=10,
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")
@@ -410,7 +396,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
task_type="manual",
)
nats_cmd.return_value = "ok"
ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
ret = create_win_task_schedule.s(pk=self.task5.pk).apply()
nats_cmd.assert_called_with(
{
"func": "schedtask",
@@ -421,6 +407,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
"name": task_name,
},
},
timeout=10,
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")

View File

@@ -1,7 +1,6 @@
import asyncio
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -11,18 +10,17 @@ from scripts.models import Script
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
from .models import AutomatedTask
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
from .serializers import AutoTaskSerializer, TaskSerializer
from .tasks import (
create_win_task_schedule,
delete_win_task_schedule,
enable_or_disable_win_task,
)
class AddAutoTask(APIView):
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
def post(self, request):
from automation.models import Policy
from automation.tasks import generate_agent_tasks_from_policies_task
from automation.tasks import generate_agent_autotasks_task
from autotasks.tasks import create_win_task_schedule
data = request.data
script = get_object_or_404(Script, pk=data["autotask"]["script"])
@@ -47,7 +45,7 @@ class AddAutoTask(APIView):
del data["autotask"]["run_time_days"]
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
serializer.is_valid(raise_exception=True)
obj = serializer.save(
task = serializer.save(
**parent,
script=script,
win_task_name=AutomatedTask.generate_task_name(),
@@ -55,16 +53,18 @@ class AddAutoTask(APIView):
run_time_bit_weekdays=bit_weekdays,
)
if not "policy" in data:
create_win_task_schedule.delay(pk=obj.pk)
if task.agent:
create_win_task_schedule.delay(pk=task.pk)
if "policy" in data:
generate_agent_tasks_from_policies_task.delay(data["policy"])
elif task.policy:
generate_agent_autotasks_task.delay(policy=task.policy.pk)
return Response("Task will be created shortly!")
class AutoTask(APIView):
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
@@ -75,7 +75,7 @@ class AutoTask(APIView):
return Response(AutoTaskSerializer(agent, context=ctx).data)
def put(self, request, pk):
from automation.tasks import update_policy_task_fields_task
from automation.tasks import update_policy_autotasks_fields_task
task = get_object_or_404(AutomatedTask, pk=pk)
@@ -84,46 +84,54 @@ class AutoTask(APIView):
serializer.save()
if task.policy:
update_policy_task_fields_task.delay(task.pk)
update_policy_autotasks_fields_task.delay(task=task.pk)
return Response("ok")
def patch(self, request, pk):
from automation.tasks import update_policy_task_fields_task
from automation.tasks import update_policy_autotasks_fields_task
from autotasks.tasks import enable_or_disable_win_task
task = get_object_or_404(AutomatedTask, pk=pk)
if "enableordisable" in request.data:
action = request.data["enableordisable"]
if not task.policy:
enable_or_disable_win_task.delay(pk=task.pk, action=action)
else:
update_policy_task_fields_task.delay(task.pk, update_agent=True)
task.enabled = action
task.save(update_fields=["enabled"])
action = "enabled" if action else "disabled"
if task.policy:
update_policy_autotasks_fields_task.delay(
task=task.pk, update_agent=True
)
elif task.agent:
enable_or_disable_win_task.delay(pk=task.pk)
return Response(f"Task will be {action} shortly")
else:
return notify_error("The request was invalid")
def delete(self, request, pk):
from automation.tasks import delete_policy_autotask_task
from automation.tasks import delete_policy_autotasks_task
from autotasks.tasks import delete_win_task_schedule
task = get_object_or_404(AutomatedTask, pk=pk)
if not task.policy:
if task.agent:
delete_win_task_schedule.delay(pk=task.pk)
if task.policy:
delete_policy_autotask_task.delay(task.pk)
elif task.policy:
delete_policy_autotasks_task.delay(task=task.pk)
task.delete()
return Response(f"{task.name} will be deleted shortly")
@api_view()
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
def run_task(request, pk):
from autotasks.tasks import run_win_task
task = get_object_or_404(AutomatedTask, pk=pk)
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
run_win_task.delay(pk=pk)
return Response(f"{task.name} will now be run on {task.agent.hostname}")

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.1 on 2021-06-06 16:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0023_check_run_interval'),
]
operations = [
migrations.RemoveField(
model_name='checkhistory',
name='check_history',
),
migrations.AddField(
model_name='checkhistory',
name='check_id',
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -1,4 +1,3 @@
import asyncio
import json
import os
import string
@@ -6,17 +5,15 @@ from statistics import mean
from typing import Any
import pytz
from alerts.models import SEVERITY_CHOICES
from core.models import CoreSettings
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from logs.models import BaseAuditModel
from loguru import logger
from alerts.models import SEVERITY_CHOICES
from core.models import CoreSettings
from logs.models import BaseAuditModel
from .utils import bytes2human
logger.configure(**settings.LOG_CONFIG)
@@ -263,6 +260,42 @@ class Check(BaseAuditModel):
"modified_time",
]
@property
def policy_fields_to_copy(self) -> list[str]:
return [
"warning_threshold",
"error_threshold",
"alert_severity",
"name",
"run_interval",
"disk",
"fails_b4_alert",
"ip",
"script",
"script_args",
"info_return_codes",
"warning_return_codes",
"timeout",
"svc_name",
"svc_display_name",
"svc_policy_mode",
"pass_if_start_pending",
"pass_if_svc_not_exist",
"restart_if_stopped",
"log_name",
"event_id",
"event_id_is_wildcard",
"event_type",
"event_source",
"event_message",
"fail_when",
"search_last_days",
"number_of_events_b4_alert",
"email_alert",
"text_alert",
"dashboard_alert",
]
def should_create_alert(self, alert_template=None):
return (
@@ -280,9 +313,9 @@ class Check(BaseAuditModel):
)
def add_check_history(self, value: int, more_info: Any = None) -> None:
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info)
def handle_checkv2(self, data):
def handle_check(self, data):
from alerts.models import Alert
# cpuload or mem checks
@@ -313,9 +346,6 @@ class Check(BaseAuditModel):
elif self.check_type == "diskspace":
if data["exists"]:
percent_used = round(data["percent_used"])
total = bytes2human(data["total"])
free = bytes2human(data["free"])
if self.error_threshold and (100 - percent_used) < self.error_threshold:
self.status = "failing"
self.alert_severity = "error"
@@ -329,7 +359,7 @@ class Check(BaseAuditModel):
else:
self.status = "passing"
self.more_info = f"Total: {total}B, Free: {free}B"
self.more_info = data["more_info"]
# add check history
self.add_check_history(100 - percent_used)
@@ -345,12 +375,7 @@ class Check(BaseAuditModel):
self.stdout = data["stdout"]
self.stderr = data["stderr"]
self.retcode = data["retcode"]
try:
# python agent
self.execution_time = "{:.4f}".format(data["stop"] - data["start"])
except:
# golang agent
self.execution_time = "{:.4f}".format(data["runtime"])
self.execution_time = "{:.4f}".format(data["runtime"])
if data["retcode"] in self.info_return_codes:
self.alert_severity = "info"
@@ -386,18 +411,8 @@ class Check(BaseAuditModel):
# ping checks
elif self.check_type == "ping":
success = ["Reply", "bytes", "time", "TTL"]
output = data["output"]
if data["has_stdout"]:
if all(x in output for x in success):
self.status = "passing"
else:
self.status = "failing"
elif data["has_stderr"]:
self.status = "failing"
self.more_info = output
self.status = data["status"]
self.more_info = data["output"]
self.save(update_fields=["more_info"])
self.add_check_history(
@@ -406,41 +421,8 @@ class Check(BaseAuditModel):
# windows service checks
elif self.check_type == "winsvc":
svc_stat = data["status"]
self.more_info = f"Status {svc_stat.upper()}"
if data["exists"]:
if svc_stat == "running":
self.status = "passing"
elif svc_stat == "start_pending" and self.pass_if_start_pending:
self.status = "passing"
else:
if self.agent and self.restart_if_stopped:
nats_data = {
"func": "winsvcaction",
"payload": {"name": self.svc_name, "action": "start"},
}
r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
if r == "timeout" or r == "natsdown":
self.status = "failing"
elif not r["success"] and r["errormsg"]:
self.status = "failing"
elif r["success"]:
self.status = "passing"
self.more_info = f"Status RUNNING"
else:
self.status = "failing"
else:
self.status = "failing"
else:
if self.pass_if_svc_not_exist:
self.status = "passing"
else:
self.status = "failing"
self.more_info = f"Service {self.svc_name} does not exist"
self.status = data["status"]
self.more_info = data["more_info"]
self.save(update_fields=["more_info"])
self.add_check_history(
@@ -448,49 +430,7 @@ class Check(BaseAuditModel):
)
elif self.check_type == "eventlog":
log = []
is_wildcard = self.event_id_is_wildcard
eventType = self.event_type
eventID = self.event_id
source = self.event_source
message = self.event_message
r = data["log"]
for i in r:
if i["eventType"] == eventType:
if not is_wildcard and not int(i["eventID"]) == eventID:
continue
if not source and not message:
if is_wildcard:
log.append(i)
elif int(i["eventID"]) == eventID:
log.append(i)
continue
if source and message:
if is_wildcard:
if source in i["source"] and message in i["message"]:
log.append(i)
elif int(i["eventID"]) == eventID:
if source in i["source"] and message in i["message"]:
log.append(i)
continue
if source and source in i["source"]:
if is_wildcard:
log.append(i)
elif int(i["eventID"]) == eventID:
log.append(i)
if message and message in i["message"]:
if is_wildcard:
log.append(i)
elif int(i["eventID"]) == eventID:
log.append(i)
log = data["log"]
if self.fail_when == "contains":
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "failing"
@@ -527,6 +467,11 @@ class Check(BaseAuditModel):
return self.status
def handle_assigned_task(self) -> None:
for task in self.assignedtask.all(): # type: ignore
if task.enabled:
task.run_win_task()
@staticmethod
def serialize(check):
# serializes the check and returns json
@@ -551,49 +496,31 @@ class Check(BaseAuditModel):
def create_policy_check(self, agent=None, policy=None):
if not agent and not policy or agent and policy:
if (not agent and not policy) or (agent and policy):
return
Check.objects.create(
check = Check.objects.create(
agent=agent,
policy=policy,
managed_by_policy=bool(agent),
parent_check=(self.pk if agent else None),
name=self.name,
alert_severity=self.alert_severity,
check_type=self.check_type,
email_alert=self.email_alert,
dashboard_alert=self.dashboard_alert,
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,
ip=self.ip,
script=self.script,
script_args=self.script_args,
timeout=self.timeout,
info_return_codes=self.info_return_codes,
warning_return_codes=self.warning_return_codes,
svc_name=self.svc_name,
svc_display_name=self.svc_display_name,
pass_if_start_pending=self.pass_if_start_pending,
pass_if_svc_not_exist=self.pass_if_svc_not_exist,
restart_if_stopped=self.restart_if_stopped,
svc_policy_mode=self.svc_policy_mode,
log_name=self.log_name,
event_id=self.event_id,
event_id_is_wildcard=self.event_id_is_wildcard,
event_type=self.event_type,
event_source=self.event_source,
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,
)
for task in self.assignedtask.all(): # type: ignore
if policy or (
agent and not agent.autotasks.filter(parent_task=task.pk).exists()
):
task.create_policy_task(
agent=agent, policy=policy, assigned_check=check
)
for field in self.policy_fields_to_copy:
setattr(check, field, getattr(self, field))
check.save()
def is_duplicate(self, check):
if self.check_type == "diskspace":
return self.disk == check.disk
@@ -761,14 +688,10 @@ class Check(BaseAuditModel):
class CheckHistory(models.Model):
check_history = models.ForeignKey(
Check,
related_name="check_history",
on_delete=models.CASCADE,
)
check_id = models.PositiveIntegerField(default=0)
x = models.DateTimeField(auto_now_add=True)
y = models.PositiveIntegerField(null=True, blank=True, default=None)
results = models.JSONField(null=True, blank=True)
def __str__(self):
return self.check_history.readable_desc
return self.x

View File

@@ -0,0 +1,16 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageChecksPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_checks")
class RunChecksPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_checks")

View File

@@ -6,6 +6,7 @@ from autotasks.models import AutomatedTask
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
from .models import Check, CheckHistory
from scripts.models import Script
class AssignedTaskField(serializers.ModelSerializer):
@@ -158,13 +159,16 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
class CheckRunnerGetSerializer(serializers.ModelSerializer):
# only send data needed for agent to run a check
assigned_tasks = serializers.SerializerMethodField()
script = ScriptCheckSerializer(read_only=True)
script_args = serializers.SerializerMethodField()
def get_assigned_tasks(self, obj):
if obj.assignedtask.exists():
tasks = obj.assignedtask.all()
return AssignedTaskCheckRunnerField(tasks, many=True).data
def get_script_args(self, obj):
if obj.check_type != "script":
return []
return Script.parse_script_args(
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
)
class Meta:
model = Check
@@ -193,6 +197,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
"modified_by",
"modified_time",
"history",
"dashboard_alert",
]

View File

@@ -14,6 +14,22 @@ class TestCheckViews(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
def test_delete_agent_check(self):
# setup data
agent = baker.make_recipe("agents.agent")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
resp = self.client.delete("/checks/500/check/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/checks/{check.pk}/check/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(agent.agentchecks.all())
self.check_not_authenticated("delete", url)
def test_get_disk_check(self):
# setup data
disk_check = baker.make_recipe("checks.diskspace_check")
@@ -347,10 +363,10 @@ class TestCheckViews(TacticalTestCase):
# setup data
agent = baker.make_recipe("agents.agent")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
check_history_data = baker.make(
"checks.CheckHistory",
check_history=check,
check_id=check.id,
_quantity=30,
)
@@ -384,17 +400,17 @@ class TestCheckTasks(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
self.agent = baker.make_recipe("agents.agent")
self.agent = baker.make_recipe("agents.agent", version="1.5.7")
def test_prune_check_history(self):
from .tasks import prune_check_history
# setup data
check = baker.make_recipe("checks.diskspace_check")
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
check_history_data = baker.make(
"checks.CheckHistory",
check_history=check,
check_id=check.id,
_quantity=30,
)
@@ -510,6 +526,7 @@ class TestCheckTasks(TacticalTestCase):
"percent_used": 85,
"total": 500,
"free": 400,
"more_info": "More info",
}
resp = self.client.patch(url, data, format="json")
@@ -527,6 +544,7 @@ class TestCheckTasks(TacticalTestCase):
"percent_used": 95,
"total": 500,
"free": 400,
"more_info": "More info",
}
resp = self.client.patch(url, data, format="json")
@@ -557,6 +575,7 @@ class TestCheckTasks(TacticalTestCase):
"percent_used": 95,
"total": 500,
"free": 400,
"more_info": "More info",
}
resp = self.client.patch(url, data, format="json")
@@ -576,6 +595,7 @@ class TestCheckTasks(TacticalTestCase):
"percent_used": 95,
"total": 500,
"free": 400,
"more_info": "More info",
}
resp = self.client.patch(url, data, format="json")
@@ -592,6 +612,7 @@ class TestCheckTasks(TacticalTestCase):
"percent_used": 50,
"total": 500,
"free": 400,
"more_info": "More info",
}
resp = self.client.patch(url, data, format="json")
@@ -775,12 +796,7 @@ class TestCheckTasks(TacticalTestCase):
)
# test failing info
data = {
"id": ping.id,
"output": "Reply from 192.168.1.27: Destination host unreachable",
"has_stdout": True,
"has_stderr": False,
}
data = {"id": ping.id, "status": "failing", "output": "reply from a.com"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -790,13 +806,6 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.alert_severity, "info")
# test failing warning
data = {
"id": ping.id,
"output": "Reply from 192.168.1.27: Destination host unreachable",
"has_stdout": True,
"has_stderr": False,
}
ping.alert_severity = "warning"
ping.save()
@@ -808,13 +817,6 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.alert_severity, "warning")
# test failing error
data = {
"id": ping.id,
"output": "Reply from 192.168.1.27: Destination host unreachable",
"has_stdout": True,
"has_stderr": False,
}
ping.alert_severity = "error"
ping.save()
@@ -826,13 +828,6 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.alert_severity, "error")
# test failing error
data = {
"id": ping.id,
"output": "some output",
"has_stdout": False,
"has_stderr": True,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -841,12 +836,7 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.alert_severity, "error")
# test passing
data = {
"id": ping.id,
"output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
"has_stdout": True,
"has_stderr": False,
}
data = {"id": ping.id, "status": "passing", "output": "reply from a.com"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -865,7 +855,7 @@ class TestCheckTasks(TacticalTestCase):
)
# test passing running
data = {"id": winsvc.id, "exists": True, "status": "running"}
data = {"id": winsvc.id, "status": "passing", "more_info": "ok"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -873,20 +863,8 @@ class TestCheckTasks(TacticalTestCase):
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
# test passing start pending
winsvc.pass_if_start_pending = True
winsvc.save()
data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
# test failing no start
data = {"id": winsvc.id, "exists": True, "status": "not running"}
# test failing
data = {"id": winsvc.id, "status": "failing", "more_info": "ok"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -895,7 +873,7 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "info")
# test failing and attempt start
""" # test failing and attempt start
winsvc.restart_if_stopped = True
winsvc.alert_severity = "warning"
winsvc.save()
@@ -960,9 +938,9 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
self.assertEqual(new_check.status, "passing") """
def test_handle_eventlog_check(self):
""" def test_handle_eventlog_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
@@ -1164,4 +1142,4 @@ class TestCheckTasks(TacticalTestCase):
new_check = Check.objects.get(pk=eventlog.id)
self.assertEquals(new_check.status, "passing")
self.assertEquals(new_check.status, "passing") """

View File

@@ -8,5 +8,5 @@ urlpatterns = [
path("<pk>/loadchecks/", views.load_checks),
path("getalldisks/", views.get_disks_for_policies),
path("runchecks/<pk>/", views.run_checks),
path("history/<int:checkpk>/", views.CheckHistory.as_view()),
path("history/<int:checkpk>/", views.GetCheckHistory.as_view()),
]

View File

@@ -5,26 +5,27 @@ from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from packaging import version as pyver
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from automation.models import Policy
from automation.tasks import (
delete_policy_check_task,
generate_agent_checks_from_policies_task,
update_policy_check_fields_task,
)
from scripts.models import Script
from tacticalrmm.utils import notify_error
from .models import Check
from .models import Check, CheckHistory
from .permissions import ManageChecksPerms, RunChecksPerms
from .serializers import CheckHistorySerializer, CheckSerializer
class AddCheck(APIView):
permission_classes = [IsAuthenticated, ManageChecksPerms]
def post(self, request):
from automation.tasks import generate_agent_checks_task
policy = None
agent = None
@@ -53,40 +54,49 @@ class AddCheck(APIView):
data=request.data["check"], partial=True, context=parent
)
serializer.is_valid(raise_exception=True)
obj = serializer.save(**parent, script=script)
new_check = serializer.save(**parent, script=script)
# Generate policy Checks
if policy:
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
generate_agent_checks_task.delay(policy=policy.pk)
elif agent:
checks = agent.agentchecks.filter( # type: ignore
check_type=obj.check_type, managed_by_policy=True
check_type=new_check.check_type, managed_by_policy=True
)
# Should only be one
duplicate_check = [check for check in checks if check.is_duplicate(obj)]
duplicate_check = [
check for check in checks if check.is_duplicate(new_check)
]
if duplicate_check:
policy = Check.objects.get(pk=duplicate_check[0].parent_check).policy
if policy.enforced:
obj.overriden_by_policy = True
obj.save()
new_check.overriden_by_policy = True
new_check.save()
else:
duplicate_check[0].delete()
return Response(f"{obj.readable_desc} was added!")
return Response(f"{new_check.readable_desc} was added!")
class GetUpdateDeleteCheck(APIView):
permission_classes = [IsAuthenticated, ManageChecksPerms]
def get(self, request, pk):
check = get_object_or_404(Check, pk=pk)
return Response(CheckSerializer(check).data)
def patch(self, request, pk):
from automation.tasks import update_policy_check_fields_task
check = get_object_or_404(Check, pk=pk)
# remove fields that should not be changed when editing a check from the frontend
if "check_alert" not in request.data.keys():
if (
"check_alert" not in request.data.keys()
and "check_reset" not in request.data.keys()
):
[request.data.pop(i) for i in check.non_editable_fields]
# set event id to 0 if wildcard because it needs to be an integer field for db
@@ -102,31 +112,32 @@ class GetUpdateDeleteCheck(APIView):
serializer = CheckSerializer(instance=check, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
check = serializer.save()
# resolve any alerts that are open
if "check_reset" in request.data.keys():
if check.alert.filter(resolved=False).exists():
check.alert.get(resolved=False).resolve()
# Update policy check fields
if check.policy:
update_policy_check_fields_task(checkpk=pk)
update_policy_check_fields_task.delay(check=check.pk)
return Response(f"{obj.readable_desc} was edited!")
return Response(f"{check.readable_desc} was edited!")
def delete(self, request, pk):
check = get_object_or_404(Check, pk=pk)
from automation.tasks import generate_agent_checks_task
check_pk = check.pk
policy_pk = None
if check.policy:
policy_pk = check.policy.pk
check = get_object_or_404(Check, pk=pk)
check.delete()
# Policy check deleted
if check.policy:
delete_policy_check_task.delay(checkpk=check_pk)
Check.objects.filter(managed_by_policy=True, parent_check=pk).delete()
# Re-evaluate agent checks is policy was enforced
if check.policy.enforced:
generate_agent_checks_from_policies_task.delay(policypk=policy_pk)
generate_agent_checks_task.delay(policy=check.policy)
# Agent check deleted
elif check.agent:
@@ -135,7 +146,7 @@ class GetUpdateDeleteCheck(APIView):
return Response(f"{check.readable_desc} was deleted!")
class CheckHistory(APIView):
class GetCheckHistory(APIView):
def patch(self, request, checkpk):
check = get_object_or_404(Check, pk=checkpk)
@@ -149,7 +160,7 @@ class CheckHistory(APIView):
- djangotime.timedelta(days=request.data["timeFilter"]),
)
check_history = check.check_history.filter(timeFilter).order_by("-x") # type: ignore
check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x") # type: ignore
return Response(
CheckHistorySerializer(
@@ -159,6 +170,7 @@ class CheckHistory(APIView):
@api_view()
@permission_classes([IsAuthenticated, RunChecksPerms])
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-04-17 01:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0016_auto_20210329_1827'),
]
operations = [
migrations.AddField(
model_name='client',
name='block_policy_inheritance',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='site',
name='block_policy_inheritance',
field=models.BooleanField(default=False),
),
]

View File

@@ -9,6 +9,7 @@ from logs.models import BaseAuditModel
class Client(BaseAuditModel):
name = models.CharField(max_length=255, unique=True)
block_policy_inheritance = models.BooleanField(default=False)
workstation_policy = models.ForeignKey(
"automation.Policy",
related_name="workstation_clients",
@@ -34,30 +35,29 @@ 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
from automation.tasks import generate_agent_checks_task
# get old client if exists
old_client = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kw)
# check if server polcies have changed and initiate task to reapply policies if so
if old_client and old_client.server_policy != self.server_policy:
generate_agent_checks_by_location_task.delay(
location={"site__client_id": self.pk},
mon_type="server",
create_tasks=True,
)
# check if polcies have changed and initiate task to reapply policies if so
if old_client:
if (
(old_client.server_policy != self.server_policy)
or (old_client.workstation_policy != self.workstation_policy)
or (
old_client.block_policy_inheritance != self.block_policy_inheritance
)
):
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_client and old_client.workstation_policy != self.workstation_policy:
generate_agent_checks_by_location_task.delay(
location={"site__client_id": self.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_task.delay(
client=self.pk,
create_tasks=True,
)
if old_client and old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
if old_client.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -65,6 +65,10 @@ class Client(BaseAuditModel):
def __str__(self):
return self.name
@property
def agent_count(self) -> int:
return Agent.objects.filter(site__client=self).count()
@property
def has_maintenanace_mode_agents(self):
return (
@@ -83,19 +87,36 @@ class Client(BaseAuditModel):
"offline_time",
)
.filter(site__client=self)
.prefetch_related("agentchecks")
.prefetch_related("agentchecks", "autotasks")
)
failing = 0
data = {"error": False, "warning": False}
for agent in agents:
if agent.checks["has_failing_checks"]:
failing += 1
if agent.maintenance_mode:
break
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
failing += 1
data["error"] = True
break
return failing > 0
if agent.checks["has_failing_checks"]:
if agent.checks["warning"]:
data["warning"] = True
if agent.checks["failing"]:
data["error"] = True
break
if agent.autotasks.exists(): # type: ignore
for i in agent.autotasks.all(): # type: ignore
if i.status == "failing" and i.alert_severity == "error":
data["error"] = True
break
return data
@staticmethod
def serialize(client):
@@ -108,6 +129,7 @@ class Client(BaseAuditModel):
class Site(BaseAuditModel):
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
name = models.CharField(max_length=255)
block_policy_inheritance = models.BooleanField(default=False)
workstation_policy = models.ForeignKey(
"automation.Policy",
related_name="workstation_sites",
@@ -133,30 +155,24 @@ 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
from automation.tasks import generate_agent_checks_task
# get old client if exists
old_site = type(self).objects.get(pk=self.pk) if self.pk else None
super(Site, self).save(*args, **kw)
# check if server polcies have changed and initiate task to reapply policies if so
if old_site and old_site.server_policy != self.server_policy:
generate_agent_checks_by_location_task.delay(
location={"site_id": self.pk},
mon_type="server",
create_tasks=True,
)
# check if polcies have changed and initiate task to reapply policies if so
if old_site:
if (
(old_site.server_policy != self.server_policy)
or (old_site.workstation_policy != self.workstation_policy)
or (old_site.block_policy_inheritance != self.block_policy_inheritance)
):
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_site and old_site.workstation_policy != self.workstation_policy:
generate_agent_checks_by_location_task.delay(
location={"site_id": self.pk},
mon_type="workstation",
create_tasks=True,
)
generate_agent_checks_task.delay(site=self.pk, create_tasks=True)
if old_site and old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
if old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -165,6 +181,10 @@ class Site(BaseAuditModel):
def __str__(self):
return self.name
@property
def agent_count(self) -> int:
return Agent.objects.filter(site=self).count()
@property
def has_maintenanace_mode_agents(self):
return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
@@ -181,19 +201,35 @@ class Site(BaseAuditModel):
"offline_time",
)
.filter(site=self)
.prefetch_related("agentchecks")
.prefetch_related("agentchecks", "autotasks")
)
failing = 0
data = {"error": False, "warning": False}
for agent in agents:
if agent.checks["has_failing_checks"]:
failing += 1
if agent.maintenance_mode:
break
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
failing += 1
data["error"] = True
break
return failing > 0
if agent.checks["has_failing_checks"]:
if agent.checks["warning"]:
data["warning"] = True
if agent.checks["failing"]:
data["error"] = True
break
if agent.autotasks.exists(): # type: ignore
for i in agent.autotasks.all(): # type: ignore
if i.status == "failing" and i.alert_severity == "error":
data["error"] = True
break
return data
@staticmethod
def serialize(site):

View File

@@ -0,0 +1,27 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageClientsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_clients")
class ManageSitesPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_sites")
class ManageDeploymentPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_deployments")

View File

@@ -25,6 +25,7 @@ class SiteCustomFieldSerializer(ModelSerializer):
class SiteSerializer(ModelSerializer):
client_name = ReadOnlyField(source="client.name")
custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
agent_count = ReadOnlyField()
class Meta:
model = Site
@@ -37,6 +38,8 @@ class SiteSerializer(ModelSerializer):
"client_name",
"client",
"custom_fields",
"agent_count",
"block_policy_inheritance",
)
def validate(self, val):
@@ -68,6 +71,7 @@ class ClientCustomFieldSerializer(ModelSerializer):
class ClientSerializer(ModelSerializer):
sites = SiteSerializer(many=True, read_only=True)
custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
agent_count = ReadOnlyField()
class Meta:
model = Client
@@ -77,8 +81,10 @@ class ClientSerializer(ModelSerializer):
"server_policy",
"workstation_policy",
"alert_template",
"block_policy_inheritance",
"sites",
"custom_fields",
"agent_count",
)
def validate(self, val):
@@ -95,7 +101,6 @@ class SiteTreeSerializer(ModelSerializer):
class Meta:
model = Site
fields = "__all__"
ordering = ("failing_checks",)
class ClientTreeSerializer(ModelSerializer):
@@ -106,7 +111,6 @@ class ClientTreeSerializer(ModelSerializer):
class Meta:
model = Client
fields = "__all__"
ordering = ("failing_checks",)
class DeploymentSerializer(ModelSerializer):

View File

@@ -179,13 +179,9 @@ class TestClientViews(TacticalTestCase):
self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_delete_client(self, task1, task2):
def test_delete_client(self):
from agents.models import Agent
task1.return_value = "ok"
task2.return_value = "ok"
# setup data
client_to_delete = baker.make("clients.Client")
client_to_move = baker.make("clients.Client")
@@ -352,13 +348,9 @@ class TestClientViews(TacticalTestCase):
self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_delete_site(self, task1, task2):
def test_delete_site(self):
from agents.models import Agent
task1.return_value = "ok"
task2.return_value = "ok"
# setup data
client = baker.make("clients.Client")
site_to_delete = baker.make("clients.Site", client=client)

View File

@@ -7,7 +7,7 @@ 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 rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -16,6 +16,7 @@ from core.models import CoreSettings
from tacticalrmm.utils import notify_error
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
from .serializers import (
ClientCustomFieldSerializer,
ClientSerializer,
@@ -29,6 +30,8 @@ logger.configure(**settings.LOG_CONFIG)
class GetAddClients(APIView):
permission_classes = [IsAuthenticated, ManageClientsPerms]
def get(self, request):
clients = Client.objects.all()
return Response(ClientSerializer(clients, many=True).data)
@@ -72,6 +75,8 @@ class GetAddClients(APIView):
class GetUpdateClient(APIView):
permission_classes = [IsAuthenticated, ManageClientsPerms]
def get(self, request, pk):
client = get_object_or_404(Client, pk=pk)
return Response(ClientSerializer(client).data)
@@ -110,8 +115,10 @@ class GetUpdateClient(APIView):
class DeleteClient(APIView):
permission_classes = [IsAuthenticated, ManageClientsPerms]
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
from automation.tasks import generate_agent_checks_task
client = get_object_or_404(Client, pk=pk)
agents = Agent.objects.filter(site__client=client)
@@ -124,8 +131,7 @@ class DeleteClient(APIView):
site = get_object_or_404(Site, pk=sitepk)
agents.update(site=site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
generate_agent_checks_task.delay(all=True, create_tasks=True)
client.delete()
return Response(f"{client.name} was deleted!")
@@ -138,6 +144,8 @@ class GetClientTree(APIView):
class GetAddSites(APIView):
permission_classes = [IsAuthenticated, ManageSitesPerms]
def get(self, request):
sites = Site.objects.all()
return Response(SiteSerializer(sites, many=True).data)
@@ -163,6 +171,8 @@ class GetAddSites(APIView):
class GetUpdateSite(APIView):
permission_classes = [IsAuthenticated, ManageSitesPerms]
def get(self, request, pk):
site = get_object_or_404(Site, pk=pk)
return Response(SiteSerializer(site).data)
@@ -206,8 +216,10 @@ class GetUpdateSite(APIView):
class DeleteSite(APIView):
permission_classes = [IsAuthenticated, ManageSitesPerms]
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
from automation.tasks import generate_agent_checks_task
site = get_object_or_404(Site, pk=pk)
if site.client.sites.count() == 1:
@@ -224,14 +236,15 @@ class DeleteSite(APIView):
agents.update(site=agent_site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
generate_agent_checks_task.delay(all=True, create_tasks=True)
site.delete()
return Response(f"{site.name} was deleted!")
class AgentDeployment(APIView):
permission_classes = [IsAuthenticated, ManageDeploymentPerms]
def get(self, request):
deps = Deployment.objects.all()
return Response(DeploymentSerializer(deps, many=True).data)

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import CoreSettings, CustomField, CodeSignToken
from .models import CodeSignToken, CoreSettings, CustomField
admin.site.register(CoreSettings)
admin.site.register(CustomField)

View File

@@ -9,6 +9,7 @@ $rdp = rdpchange
$ping = pingchange
$auth = '"tokenchange"'
$downloadlink = 'downloadchange'
$apilink = $downloadlink.split('/')
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
@@ -47,24 +48,35 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
# pass
}
Try
{
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
write-host ('Extracting...')
Start-Sleep -s 5
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait
exit 0
}
Catch
{
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
Write-Error -Message "$ErrorMessage $FailedItem"
exit 1
}
Finally
{
Remove-Item -Path $OutPath\$output
$X = 0
do {
Write-Output "Waiting for network"
Start-Sleep -s 5
$X += 1
} until(($connectresult = Test-NetConnection $apilink[2] -Port 443 | ? { $_.TcpTestSucceeded }) -or $X -eq 3)
if ($connectresult.TcpTestSucceeded -eq $true){
Try
{
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
write-host ('Extracting...')
Start-Sleep -s 5
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait
exit 0
}
Catch
{
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
Write-Error -Message "$ErrorMessage $FailedItem"
exit 1
}
Finally
{
Remove-Item -Path $OutPath\$output
}
} else {
Write-Output "Unable to connect to server"
}
}

View File

@@ -1,11 +1,6 @@
import os
import shutil
import subprocess
import tempfile
from django.core.management.base import BaseCommand
from agents.models import Agent
from logs.models import PendingAction
from scripts.models import Script
@@ -13,21 +8,8 @@ class Command(BaseCommand):
help = "Collection of tasks to run after updating the rmm, after migrations"
def handle(self, *args, **kwargs):
# 10-16-2020 changed the type of the agent's 'disks' model field
# from a dict of dicts, to a list of disks in the golang agent
# the following will convert dicts to lists for agent's still on the python agent
agents = Agent.objects.only("pk", "disks")
for agent in agents:
if agent.disks is not None and isinstance(agent.disks, dict):
new = []
for k, v in agent.disks.items():
new.append(v)
agent.disks = new
agent.save(update_fields=["disks"])
self.stdout.write(
self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
)
# remove task pending actions. deprecated 4/20/2021
PendingAction.objects.filter(action_type="taskaction").delete()
# load community scripts into the db
Script.load_community_scripts()

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.7 on 2021-04-04 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0018_auto_20210329_1709'),
]
operations = [
migrations.CreateModel(
name='GlobalKVStore',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=25)),
('value', models.TextField()),
],
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.1.7 on 2021-04-15 01:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0019_codesigntoken'),
('core', '0019_globalkvstore'),
]
operations = [
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-24 23:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0020_merge_20210415_0132'),
]
operations = [
migrations.AddField(
model_name='customfield',
name='hide_in_ui',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.1.7 on 2021-05-02 02:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_customfield_hide_in_ui'),
]
operations = [
migrations.CreateModel(
name='URLAction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=25)),
('desc', models.CharField(blank=True, max_length=100, null=True)),
('pattern', models.TextField()),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2021-05-14 04:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0022_urlaction'),
]
operations = [
migrations.AddField(
model_name='coresettings',
name='clear_faults_days',
field=models.IntegerField(default=0),
),
]

View File

@@ -51,6 +51,7 @@ class CoreSettings(BaseAuditModel):
)
# removes check history older than days
check_history_prune_days = models.PositiveIntegerField(default=30)
clear_faults_days = models.IntegerField(default=0)
mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_username = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_site = models.CharField(max_length=255, null=True, blank=True, default="")
@@ -79,7 +80,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
from automation.tasks import generate_agent_checks_task
if not self.pk and CoreSettings.objects.exists():
raise ValidationError("There can only be one CoreSettings instance")
@@ -97,14 +98,10 @@ class CoreSettings(BaseAuditModel):
super(BaseAuditModel, self).save(*args, **kwargs)
# check if server polcies have changed and initiate task to reapply policies if so
if old_settings and old_settings.server_policy != self.server_policy:
generate_all_agent_checks_task.delay(mon_type="server", create_tasks=True)
# check if workstation polcies have changed and initiate task to reapply policies if so
if old_settings and old_settings.workstation_policy != self.workstation_policy:
generate_all_agent_checks_task.delay(
mon_type="workstation", create_tasks=True
)
if (old_settings and old_settings.server_policy != self.server_policy) or (
old_settings and old_settings.workstation_policy != self.workstation_policy
):
generate_agent_checks_task.delay(all=True, create_tasks=True)
if old_settings and old_settings.alert_template != self.alert_template:
cache_agents_alert_template.delay()
@@ -251,6 +248,7 @@ class CustomField(models.Model):
blank=True,
default=list,
)
hide_in_ui = models.BooleanField(default=False)
class Meta:
unique_together = (("model", "name"),)
@@ -279,3 +277,62 @@ class CodeSignToken(models.Model):
def __str__(self):
return "Code signing token"
class GlobalKVStore(models.Model):
name = models.CharField(max_length=25)
value = models.TextField()
def __str__(self):
return self.name
class URLAction(models.Model):
name = models.CharField(max_length=25)
desc = models.CharField(max_length=100, null=True, blank=True)
pattern = models.TextField()
RUN_ON_CHOICES = (
("client", "Client"),
("site", "Site"),
("agent", "Agent"),
("once", "Once"),
)
SCHEDULE_CHOICES = (("daily", "Daily"), ("weekly", "Weekly"), ("monthly", "Monthly"))
""" class GlobalTask(models.Model):
script = models.ForeignKey(
"scripts.Script",
null=True,
blank=True,
related_name="script",
on_delete=models.SET_NULL,
)
script_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
custom_field = models.OneToOneField(
"core.CustomField",
related_name="globaltask",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
timeout = models.PositiveIntegerField(default=120)
retcode = models.IntegerField(null=True, blank=True)
retvalue = models.TextField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True)
execution_time = models.CharField(max_length=100, default="0.0000")
run_schedule = models.CharField(
max_length=25, choices=SCHEDULE_CHOICES, default="once"
)
run_on = models.CharField(
max_length=25, choices=RUN_ON_CHOICES, default="once"
) """

View File

@@ -0,0 +1,23 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ViewCoreSettingsPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_view_core_settings")
class EditCoreSettingsPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_edit_core_settings")
class ServerMaintPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_do_server_maint")
class CodeSignPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_code_sign")

View File

@@ -1,7 +1,7 @@
import pytz
from rest_framework import serializers
from .models import CoreSettings, CustomField, CodeSignToken
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
class CoreSettingsSerializer(serializers.ModelSerializer):
@@ -33,3 +33,15 @@ class CodeSignTokenSerializer(serializers.ModelSerializer):
class Meta:
model = CodeSignToken
fields = "__all__"
class KeyStoreSerializer(serializers.ModelSerializer):
class Meta:
model = GlobalKVStore
fields = "__all__"
class URLActionSerializer(serializers.ModelSerializer):
class Meta:
model = URLAction
fields = "__all__"

View File

@@ -6,6 +6,7 @@ from loguru import logger
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
from checks.tasks import prune_check_history
from agents.tasks import clear_faults_task
from core.models import CoreSettings
from tacticalrmm.celery import app
@@ -28,6 +29,25 @@ def core_maintenance_tasks():
if now > task_time_utc:
delete_win_task_schedule.delay(task.pk)
core = CoreSettings.objects.first()
# remove old CheckHistory data
older_than = CoreSettings.objects.first().check_history_prune_days
prune_check_history.delay(older_than)
if core.check_history_prune_days > 0:
prune_check_history.delay(core.check_history_prune_days)
# clear faults
if core.clear_faults_days > 0:
clear_faults_task.delay(core.clear_faults_days)
@app.task
def cache_db_fields_task():
from agents.models import Agent
for agent in Agent.objects.all():
agent.pending_actions_count = agent.pendingactions.filter(
status="pending"
).count()
agent.has_patches_pending = (
agent.winupdates.filter(action="approve").filter(installed=False).exists()
)
agent.save(update_fields=["pending_actions_count", "has_patches_pending"])

View File

@@ -1,6 +1,6 @@
import requests
from unittest.mock import patch
import requests
from channels.db import database_sync_to_async
from channels.testing import WebsocketCommunicator
from model_bakery import baker
@@ -8,8 +8,8 @@ from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
from .consumers import DashInfo
from .models import CoreSettings, CustomField
from .serializers import CustomFieldSerializer
from .models import CoreSettings, CustomField, GlobalKVStore, URLAction
from .serializers import CustomFieldSerializer, KeyStoreSerializer, URLActionSerializer
from .tasks import core_maintenance_tasks
@@ -88,8 +88,8 @@ class TestCoreTasks(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_edit_coresettings(self, generate_all_agent_checks_task):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_edit_coresettings(self, generate_agent_checks_task):
url = "/core/editsettings/"
# setup
@@ -106,7 +106,7 @@ class TestCoreTasks(TacticalTestCase):
)
self.assertEqual(CoreSettings.objects.first().mesh_token, data["mesh_token"])
generate_all_agent_checks_task.assert_not_called()
generate_agent_checks_task.assert_not_called()
# test adding policy
data = {
@@ -120,9 +120,9 @@ class TestCoreTasks(TacticalTestCase):
CoreSettings.objects.first().workstation_policy.id, policies[0].id # type: ignore
)
self.assertEqual(generate_all_agent_checks_task.call_count, 2)
generate_agent_checks_task.assert_called_once()
generate_all_agent_checks_task.reset_mock()
generate_agent_checks_task.reset_mock()
# test remove policy
data = {
@@ -132,7 +132,7 @@ class TestCoreTasks(TacticalTestCase):
self.assertEqual(r.status_code, 200)
self.assertEqual(CoreSettings.objects.first().workstation_policy, None)
self.assertEqual(generate_all_agent_checks_task.call_count, 1)
self.assertEqual(generate_agent_checks_task.call_count, 1)
self.check_not_authenticated("patch", url)
@@ -273,3 +273,147 @@ class TestCoreTasks(TacticalTestCase):
self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)
def test_get_keystore(self):
url = "/core/keystore/"
# setup
keys = baker.make("core.GlobalKVStore", _quantity=2)
r = self.client.get(url)
serializer = KeyStoreSerializer(keys, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 2) # type: ignore
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_keystore(self):
url = "/core/keystore/"
data = {"name": "test", "value": "text"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)
def test_update_keystore(self):
# setup
key = baker.make("core.GlobalKVStore")
# test not found
r = self.client.put("/core/keystore/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/keystore/{key.id}/" # type: ignore
data = {"name": "test", "value": "text"}
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
new_key = GlobalKVStore.objects.get(pk=key.id) # type: ignore
self.assertEqual(new_key.name, data["name"])
self.assertEqual(new_key.value, data["value"])
self.check_not_authenticated("put", url)
def test_delete_keystore(self):
# setup
key = baker.make("core.GlobalKVStore")
# test not found
r = self.client.delete("/core/keystore/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/keystore/{key.id}/" # type: ignore
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(GlobalKVStore.objects.filter(pk=key.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)
def test_get_urlaction(self):
url = "/core/urlaction/"
# setup
action = baker.make("core.URLAction", _quantity=2)
r = self.client.get(url)
serializer = URLActionSerializer(action, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 2) # type: ignore
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_urlaction(self):
url = "/core/urlaction/"
data = {"name": "name", "desc": "desc", "pattern": "pattern"}
r = self.client.post(url, data)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)
def test_update_urlaction(self):
# setup
action = baker.make("core.URLAction")
# test not found
r = self.client.put("/core/urlaction/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/urlaction/{action.id}/" # type: ignore
data = {"name": "test", "pattern": "text"}
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
new_action = URLAction.objects.get(pk=action.id) # type: ignore
self.assertEqual(new_action.name, data["name"])
self.assertEqual(new_action.pattern, data["pattern"])
self.check_not_authenticated("put", url)
def test_delete_urlaction(self):
# setup
action = baker.make("core.URLAction")
# test not found
r = self.client.delete("/core/urlaction/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/urlaction/{action.id}/" # type: ignore
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(URLAction.objects.filter(pk=action.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)
def test_run_url_action(self):
self.maxDiff = None
# setup
agent = baker.make_recipe(
"agents.agent", agent_id="123123-assdss4s-343-sds545-45dfdf|DESKTOP"
)
baker.make("core.GlobalKVStore", name="Test Name", value="value with space")
action = baker.make(
"core.URLAction",
pattern="https://remote.example.com/connect?globalstore={{global.Test Name}}&client_name={{client.name}}&site id={{site.id}}&agent_id={{agent.agent_id}}",
)
url = "/core/urlaction/run/"
# test not found
r = self.client.patch(url, {"agent": 500, "action": 500})
self.assertEqual(r.status_code, 404)
data = {"agent": agent.id, "action": action.id} # type: ignore
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(
r.data, # type: ignore
f"https://remote.example.com/connect?globalstore=value%20with%20space&client_name={agent.client.name}&site%20id={agent.site.id}&agent_id=123123-assdss4s-343-sds545-45dfdf%7CDESKTOP",
)
self.check_not_authenticated("patch", url)

View File

@@ -13,4 +13,9 @@ urlpatterns = [
path("customfields/", views.GetAddCustomFields.as_view()),
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
path("codesign/", views.CodeSign.as_view()),
path("keystore/", views.GetAddKeyStore.as_view()),
path("keystore/<int:pk>/", views.UpdateDeleteKeyStore.as_view()),
path("urlaction/", views.GetAddURLAction.as_view()),
path("urlaction/<int:pk>/", views.UpdateDeleteURLAction.as_view()),
path("urlaction/run/", views.RunURLAction.as_view()),
]

View File

@@ -1,25 +1,37 @@
import os
import re
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import ParseError
from rest_framework.parsers import FileUploadParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.permissions import MeshPerms
from tacticalrmm.utils import notify_error
from .models import CoreSettings, CustomField, CodeSignToken
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
from .permissions import (
CodeSignPerms,
ViewCoreSettingsPerms,
EditCoreSettingsPerms,
ServerMaintPerms,
)
from .serializers import (
CodeSignTokenSerializer,
CoreSettingsSerializer,
CustomFieldSerializer,
CodeSignTokenSerializer,
KeyStoreSerializer,
URLActionSerializer,
)
class UploadMeshAgent(APIView):
permission_classes = [IsAuthenticated, MeshPerms]
parser_class = (FileUploadParser,)
def put(self, request, format=None):
@@ -39,12 +51,14 @@ class UploadMeshAgent(APIView):
@api_view()
@permission_classes([IsAuthenticated, ViewCoreSettingsPerms])
def get_core_settings(request):
settings = CoreSettings.objects.first()
return Response(CoreSettingsSerializer(settings).data)
@api_view(["PATCH"])
@permission_classes([IsAuthenticated, EditCoreSettingsPerms])
def edit_settings(request):
coresettings = CoreSettings.objects.first()
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
@@ -61,16 +75,23 @@ def version(request):
@api_view()
def dashboard_info(request):
from tacticalrmm.utils import get_latest_trmm_ver
return Response(
{
"trmm_version": settings.TRMM_VERSION,
"latest_trmm_ver": get_latest_trmm_ver(),
"dark_mode": request.user.dark_mode,
"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,
"url_action": request.user.url_action.id
if request.user.url_action
else None,
"client_tree_sort": request.user.client_tree_sort,
"client_tree_splitter": request.user.client_tree_splitter,
"loading_bar_color": request.user.loading_bar_color,
"hosted": hasattr(settings, "HOSTED") and settings.HOSTED,
}
)
@@ -89,6 +110,7 @@ def email_test(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, ServerMaintPerms])
def server_maintenance(request):
from tacticalrmm.utils import reload_nats
@@ -143,6 +165,8 @@ def server_maintenance(request):
class GetAddCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
def get(self, request):
fields = CustomField.objects.all()
return Response(CustomFieldSerializer(fields, many=True).data)
@@ -163,6 +187,8 @@ class GetAddCustomFields(APIView):
class GetUpdateDeleteCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
def get(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
@@ -186,6 +212,8 @@ class GetUpdateDeleteCustomFields(APIView):
class CodeSign(APIView):
permission_classes = [IsAuthenticated, CodeSignPerms]
def get(self, request):
token = CodeSignToken.objects.first()
return Response(CodeSignTokenSerializer(token).data)
@@ -228,3 +256,106 @@ class CodeSign(APIView):
except:
ret = "Something went wrong"
return notify_error(ret)
def post(self, request):
from agents.models import Agent
from agents.tasks import force_code_sign
err = "A valid token must be saved first"
try:
t = CodeSignToken.objects.first().token
except:
return notify_error(err)
if t is None or t == "":
return notify_error(err)
pks: list[int] = list(Agent.objects.only("pk").values_list("pk", flat=True))
force_code_sign.delay(pks=pks)
return Response("Agents will be code signed shortly")
class GetAddKeyStore(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
def get(self, request):
keys = GlobalKVStore.objects.all()
return Response(KeyStoreSerializer(keys, many=True).data)
def post(self, request):
serializer = KeyStoreSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class UpdateDeleteKeyStore(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
def put(self, request, pk):
key = get_object_or_404(GlobalKVStore, pk=pk)
serializer = KeyStoreSerializer(instance=key, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(GlobalKVStore, pk=pk).delete()
return Response("ok")
class GetAddURLAction(APIView):
def get(self, request):
actions = URLAction.objects.all()
return Response(URLActionSerializer(actions, many=True).data)
def post(self, request):
serializer = URLActionSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class UpdateDeleteURLAction(APIView):
def put(self, request, pk):
action = get_object_or_404(URLAction, pk=pk)
serializer = URLActionSerializer(
instance=action, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(URLAction, pk=pk).delete()
return Response("ok")
class RunURLAction(APIView):
def patch(self, request):
from requests.utils import requote_uri
from agents.models import Agent
from tacticalrmm.utils import replace_db_values
agent = get_object_or_404(Agent, pk=request.data["agent"])
action = get_object_or_404(URLAction, pk=request.data["action"])
pattern = re.compile("\\{\\{([\\w\\s]+\\.[\\w\\s]+)\\}\\}")
url_pattern = action.pattern
for string in re.findall(pattern, action.pattern):
value = replace_db_values(string=string, agent=agent, quotes=False)
url_pattern = re.sub("\\{\\{" + string + "\\}\\}", str(value), url_pattern)
return Response(requote_uri(url_pattern))

View File

@@ -7,7 +7,7 @@ from tacticalrmm.middleware import get_debug_info, get_username
ACTION_TYPE_CHOICES = [
("schedreboot", "Scheduled Reboot"),
("taskaction", "Scheduled Task Action"),
("taskaction", "Scheduled Task Action"), # deprecated
("agentupdate", "Agent Update"),
("chocoinstall", "Chocolatey Software Install"),
]
@@ -42,13 +42,6 @@ AUDIT_OBJECT_TYPE_CHOICES = [
("bulk", "Bulk"),
]
# taskaction details format
# {
# "action": "taskcreate" | "taskdelete" | "tasktoggle",
# "value": "Enable" | "Disable" # only needed for task toggle,
# "task_id": 1
# }
STATUS_CHOICES = [
("pending", "Pending"),
("completed", "Completed"),
@@ -250,8 +243,6 @@ 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":
return "Next agent check-in"
elif self.action_type == "agentupdate":
return "Next update cycle"
elif self.action_type == "chocoinstall":
@@ -268,20 +259,6 @@ class PendingAction(models.Model):
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"
elif self.details["action"] == "taskcreate":
return "Device pending task creation"
elif self.details["action"] == "tasktoggle":
# value is bool
if self.details["value"]:
action = "enable"
else:
action = "disable"
return f"Device pending task {action}"
class BaseAuditModel(models.Model):
# abstract base class for auditing models

View File

@@ -0,0 +1,21 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class AuditLogPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_view_auditlogs")
class ManagePendingActionPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "PATCH":
return True
return _has_perm(r, "can_manage_pendingactions")
class DebugLogPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_view_debuglogs")

View File

@@ -9,7 +9,8 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -20,10 +21,13 @@ from agents.serializers import AgentHostnameSerializer
from tacticalrmm.utils import notify_error
from .models import AuditLog, PendingAction
from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms
from .serializers import AuditLogSerializer, PendingActionSerializer
class GetAuditLogs(APIView):
permission_classes = [IsAuthenticated, AuditLogPerms]
def patch(self, request):
from agents.models import Agent
from clients.models import Client
@@ -92,6 +96,8 @@ class GetAuditLogs(APIView):
class FilterOptionsAuditLog(APIView):
permission_classes = [IsAuthenticated, AuditLogPerms]
def post(self, request):
if request.data["type"] == "agent":
agents = Agent.objects.filter(hostname__icontains=request.data["pattern"])
@@ -107,6 +113,8 @@ class FilterOptionsAuditLog(APIView):
class PendingActions(APIView):
permission_classes = [IsAuthenticated, ManagePendingActionPerms]
def patch(self, request):
status_filter = "completed" if request.data["showCompleted"] else "pending"
if "agentPK" in request.data.keys():
@@ -149,6 +157,7 @@ class PendingActions(APIView):
@api_view()
@permission_classes([IsAuthenticated, DebugLogPerms])
def debug_log(request, mode, hostname, order):
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
@@ -191,6 +200,7 @@ def debug_log(request, mode, hostname, order):
@api_view()
@permission_classes([IsAuthenticated, DebugLogPerms])
def download_log(request):
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
if settings.DEBUG:

View File

@@ -6,4 +6,6 @@ mkdocs-material
pymdown-extensions
Pygments
isort
mypy
mypy
types-pytz
types-pytz

View File

@@ -1,22 +1,22 @@
asgiref==3.3.4
asyncio-nats-client==0.11.4
celery==5.0.5
certifi==2020.12.5
celery==5.1.1
certifi==2021.5.30
cffi==1.14.5
channels==3.0.3
channels_redis==3.2.0
chardet==4.0.0
cryptography==3.4.7
daphne==3.0.2
Django==3.2.0
Django==3.2.4
django-cors-headers==3.7.0
django-rest-knox==4.1.0
djangorestframework==3.12.4
future==0.18.2
kombu==5.0.2
loguru==0.5.3
msgpack==1.0.2
packaging==20.9
psycopg2-binary==2.8.6
psycopg2-binary==2.9.1
pycparser==2.20
pycryptodome==3.10.1
pyotp==2.6.0
@@ -25,12 +25,12 @@ pytz==2021.1
qrcode==6.1
redis==3.5.3
requests==2.25.1
six==1.15.0
six==1.16.0
sqlparse==0.4.1
twilio==6.56.0
urllib3==1.26.4
twilio==6.60.0
urllib3==1.26.5
uWSGI==2.0.19.1
validators==0.18.2
vine==5.0.0
websockets==8.1
websockets==9.1
zipp==3.4.1

View File

@@ -1,21 +1,23 @@
[
{
"guid": "6820cb5e-5a7f-4d9b-8c22-d54677e3cc04",
"filename": "Win_Clear_Firefox_Cache.ps1",
"filename": "Win_Firefox_Clear_Cache.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Firefox - Clean Cache",
"description": "This script will clean up Mozilla Firefox for all users.",
"shell": "powershell",
"category": "TRMM (Win):Browsers"
"category": "TRMM (Win):Browsers",
"default_timeout": "300"
},
{
"guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443",
"filename": "Win_Clear_Google_Chrome_Cache.ps1",
"filename": "Win_Google_Chrome_Clear_Cache.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Chrome - Clear Cache for All Users",
"description": "This script will clean up Google Chrome for all users.",
"shell": "powershell",
"category": "TRMM (Win):Browsers"
"category": "TRMM (Win):Browsers",
"default_timeout": "300"
},
{
"guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc",
@@ -24,7 +26,8 @@
"name": "Adobe Reader DC - Install",
"description": "Installs Adobe Reader DC.",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software>Chocolatey"
"category": "TRMM (Win):3rd Party Software>Chocolatey",
"default_timeout": "300"
},
{
"guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
@@ -33,7 +36,8 @@
"name": "Duplicati - Install",
"description": "This script installs Duplicati 2.0.5.1 as a service.",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
"category": "TRMM (Win):3rd Party Software",
"default_timeout": "300"
},
{
"guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
@@ -42,7 +46,8 @@
"name": "Windows Update - Reset",
"description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
"shell": "powershell",
"category": "TRMM (Win):Updates"
"category": "TRMM (Win):Updates",
"default_timeout": "300"
},
{
"guid": "8db87ff0-a9b4-4d9d-bc55-377bbcb85b6d",
@@ -51,7 +56,8 @@
"name": "Disk - Cleanup C: drive",
"description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
"shell": "powershell",
"category": "TRMM (Win):Other"
"category": "TRMM (Win):Maintenance",
"default_timeout": "25000"
},
{
"guid": "2f28e8c1-ae0f-4b46-a826-f513974526a3",
@@ -75,14 +81,15 @@
"guid": "3c46290b-85db-4cd2-93a2-943c8c93b3b1",
"filename": "Speedtest.py",
"submittedBy": "https://github.com/wh1te909",
"name": "Network - Speed Test",
"name": "Speed Test - Python",
"description": "Runs a Speed Test using Python",
"shell": "python",
"category": "TRMM (Win):Network"
"category": "TRMM (Win):Network",
"default_timeout": "120"
},
{
"guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
"filename": "Win_Rename_Installed_App.ps1",
"filename": "Win_TRMM_Rename_Installed_App.ps1",
"submittedBy": "https://github.com/bradhawkins85",
"name": "TacticalRMM Agent Rename",
"description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.",
@@ -102,7 +109,7 @@
"guid": "2ea35fa2-c227-4d17-a40e-4d39f252e27a",
"filename": "Win_Bitlocker_Create_Status_Report.ps1",
"submittedBy": "https://github.com/ThatsNASt",
"name": "Create Bitlocker Status Report",
"name": "Bitlocker - Create Status Report",
"description": "Creates a Bitlocker status report.",
"shell": "powershell",
"category": "TRMM (Win):Storage"
@@ -152,9 +159,30 @@
"shell": "powershell",
"category": "TRMM (Win):Hardware"
},
{
"guid": "72c56717-28ed-4cc6-b30f-b362d30fb4b6",
"filename": "Win_Hardware_SN.ps1",
"submittedBy": "https://github.com/subzdev",
"name": "Hardware - Get Serial Number",
"description": "Returns BIOS Serial Number - Use with Custom Fields for later use",
"shell": "powershell",
"category": "TRMM (Win):Collectors"
},
{
"guid": "973c34d7-cab0-4fda-999c-b4933655f946",
"filename": "Win_Screenconnect_GetGUID.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Screenconnect - Get GUID for client",
"description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use. ",
"args": [
"-serviceName {{client.ScreenConnectService}}"
],
"shell": "powershell",
"category": "TRMM (Win):Collectors"
},
{
"guid": "95a2ee6f-b89b-4551-856e-3081b041caa7",
"filename": "Win_Reset_High_Performance_Power_Profile_to_Defaults.ps1",
"filename": "Win_Power_Profile_Reset_High_Performance_to_Defaults.ps1",
"submittedBy": "https://github.com/azulskyknight",
"name": "Power Profile - Reset High Perf Power Profile to defaults",
"description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.",
@@ -163,7 +191,7 @@
},
{
"guid": "2cbd30b0-84dd-4388-a36d-2e2e980f1a3e",
"filename": "Win_Set_High_Performance_Power_Profile.ps1",
"filename": "Win_Power_Profile_Set_High_Performance.ps1",
"submittedBy": "https://github.com/azulskyknight",
"name": "Power Profile - Set High Performance",
"description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.",
@@ -177,7 +205,8 @@
"name": "Windows 10 Upgrade",
"description": "Forces an upgrade to the latest release of Windows 10.",
"shell": "powershell",
"category": "TRMM (Win):Updates"
"category": "TRMM (Win):Updates",
"default_timeout": "25000"
},
{
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
@@ -192,7 +221,7 @@
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
"filename": "Win_Duplicati_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Check",
"name": "Duplicati - Check Status",
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
@@ -213,7 +242,8 @@
"name": "SSH - Install Feature and Enable",
"description": "Installs and enabled OpenSSH Server Feature in Win10",
"shell": "powershell",
"category": "TRMM (Win):Windows Features"
"category": "TRMM (Win):Windows Features",
"default_timeout": "300"
},
{
"guid": "2435297a-6263-4e90-8688-1847400d0e22",
@@ -228,7 +258,7 @@
"guid": "24f19ead-fdfe-46b4-9dcb-4cd0e12a3940",
"filename": "Win_Speedtest.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Speed Test Powershell",
"name": "Speed Test - Powershell",
"description": "Speed Test with Powershell(win 10 or server2016+)",
"shell": "powershell",
"category": "TRMM (Win):Network"
@@ -242,6 +272,20 @@
"shell": "cmd",
"category": "TRMM (Win):Active Directory"
},
{
"guid": "5320dfc8-022a-41e7-9e39-11c493545ec9",
"filename": "Win_AD_Hudu_ADDS_Documentation.ps1",
"submittedBy": "https://github.com/unplugged216",
"name": "ADDS - Direcotry documentation in Hudu",
"description": "Auto generates ADDS documentation and submits it to your Hudu instance.",
"args": [
"-ClientName {{client.name}}",
"-HuduBaseDomain {{global.HuduBaseDomain}}",
"-HuduApiKey {{global.HuduApiKey}}"
],
"shell": "powershell",
"category": "TRMM (Win):Active Directory"
},
{
"guid": "b6b9912f-4274-4162-99cc-9fd47fbcb292",
"filename": "Win_ADDC_Sync_Start.bat",
@@ -262,10 +306,10 @@
},
{
"guid": "d980fda3-a068-47eb-8495-1aab07a24e64",
"filename": "Win_Defender_Status.ps1",
"filename": "Win_Defender_Status_Report.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Defender - Status",
"description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours",
"name": "Defender - Status Report",
"description": "This will check for Malware and Antispyware within the last 24 hours and display, otherwise will report as Healthy. Command Parameter: (number) if provided will check that number of days back in the log.",
"shell": "powershell",
"category": "TRMM (Win):Security>Antivirus"
},
@@ -289,7 +333,7 @@
},
{
"guid": "2472bbaf-1941-4722-8a58-d1dd0f528801",
"filename": "Win_Update_Tactical_Exclusion.ps1",
"filename": "Win_TRMM_AV_Update_Exclusion.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "TRMM Defender Exclusions",
"description": "Windows Defender Exclusions for Tactical RMM",
@@ -327,10 +371,11 @@
"guid": "5615aa90-0272-427b-8acf-0ca019612501",
"filename": "Win_Chocolatey_Update_Installed.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey Update Installed Apps",
"name": "Update Installed Apps",
"description": "Update all apps that were installed using Chocolatey.",
"shell": "cmd",
"category": "TRMM (Win):3rd Party Software>Chocolatey"
"category": "TRMM (Win):3rd Party Software>Chocolatey",
"default_timeout": "3600"
},
{
"guid": "fff8024d-d72e-4457-84fa-6c780f69a16f",
@@ -341,6 +386,15 @@
"shell": "powershell",
"category": "TRMM (Win):Active Directory"
},
{
"guid": "3afd07c0-04fd-4b23-b5f2-88205c0744d4",
"filename": "Win_User_Admins_Local_Disable.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Local Administrators - Disables all local admins if joined to domain or AzureAD",
"description": "Checks to see if computer is either joined to a AD domain or Azure AD. If it is, it disables all local admin accounts. If not joined to domain/AzureAD, leaves admin accounts in place",
"shell": "powershell",
"category": "TRMM (Win):User Management"
},
{
"guid": "71090fc4-faa6-460b-adb0-95d7863544e1",
"filename": "Win_Check_Events_for_Bluescreens.ps1",
@@ -389,21 +443,12 @@
},
{
"guid": "f396dae2-c768-45c5-bd6c-176e56ed3614",
"filename": "Win_Finish_updates_and_restart.ps1",
"filename": "Win_Power_RestartorShutdown.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Updates - Finish and restart",
"description": "Finish installing Windows updates and restart PC",
"name": "Power - Restart or Shutdown PC",
"description": "Restart PC. Add parameter: shutdown if you want to shutdown computer",
"shell": "powershell",
"category": "TRMM (Win):Other"
},
{
"guid": "63f89be0-a9c9-4c61-9b55-bce0b28b90b2",
"filename": "Win_Finish_updates_and_shutdown.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Updates - Finish and Shutdown",
"description": "Finish installing Windows updates and shutdown PC",
"shell": "powershell",
"category": "TRMM (Win):Other"
"category": "TRMM (Win):Updates"
},
{
"guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8",
@@ -414,6 +459,8 @@
"args": [
"-serviceName {{client.ScreenConnectService}}",
"-url {{client.ScreenConnectInstaller}}",
"-clientname {{client.name}}",
"-sitename {{site.name}}",
"-action install"
],
"default_timeout": "90",
@@ -422,7 +469,7 @@
},
{
"guid": "3abbb62a-3757-492c-8979-b4fc6174845d",
"filename": "Win_Disable_AutoRun.bat",
"filename": "Win_AutoRun_Disable.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Autorun - Disable",
"description": "Disable Autorun System Wide",
@@ -432,7 +479,7 @@
},
{
"guid": "4a11877a-7555-494c-ac74-29d6df3c1989",
"filename": "Win_Disable_Cortana.bat",
"filename": "Win_Cortana_Disable.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Cortana - Disable",
"description": "Disable Cortana System Wide",
@@ -444,7 +491,7 @@
"guid": "28ef1387-dd4f-4bab-b042-26250914e370",
"filename": "Win_WOL_Enable_Status.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Network WoL - Enable function",
"name": "BROKEN Network WoL - Enable function",
"description": "Wake on Lan enable on Dell, HP, Lenovo",
"shell": "powershell",
"category": "TRMM (Win):Network",
@@ -454,12 +501,52 @@
"guid": "685d5432-0b84-46d5-98e8-3ec2054150fe",
"filename": "Win_WOL_Test_State.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Network WoL - Test State",
"name": "BROKEN Network WoL - Test State",
"description": "Wake on Lan test status",
"shell": "powershell",
"category": "TRMM (Win):Network",
"default_timeout": "90"
},
{
"guid": "abe78170-7cf9-435b-9666-c5ef6c11a106",
"filename": "Win_Network_IPv6_Disable.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Network IPv6 - Disable",
"description": "Disable IPv6 on all adapters",
"shell": "powershell",
"category": "TRMM (Win):Network",
"default_timeout": "90"
},
{
"guid": "745bb7cd-b71a-4f2e-b6f2-c579b1828162",
"filename": "Win_Network_DHCP_Set.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Network - Set Primary NIC to DHCP",
"description": "Enable DHCP on primary adapter",
"shell": "cmd",
"category": "TRMM (Win):Network",
"default_timeout": "90"
},
{
"guid": "83aa4d51-63ce-41e7-829f-3c16e6115bbf",
"filename": "Win_Network_DNS_Set_to_1.1.1.2.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Network - Set all NICs to use DNS of 1.1.1.2",
"description": "Domain computers skipped. Sets all NICs to have primary DNS server of 1.1.1.2, backup of 1.0.0.2 (Cloudflare malware blocking)",
"shell": "powershell",
"category": "TRMM (Win):Network",
"default_timeout": "90"
},
{
"guid": "0caa33bc-89ca-47e0-ad4a-04626ae6384d",
"filename": "Win_Network_TCP_Reset_Stack.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Network - Reset tcp using netsh",
"description": "resets tcp stack using netsh",
"shell": "cmd",
"category": "TRMM (Win):Network",
"default_timeout": "120"
},
{
"guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
"filename": "Win_Win10_Change_Key_and_Activate.ps1",
@@ -474,8 +561,8 @@
"guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
"filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "TRMM Delete Start Menu Shortcut for App",
"description": "Tactical RMM delete its application shortcut that's installed in the start menu",
"name": "TacticalRMM Delete Start Menu Shortcut for App",
"description": "Delete its application shortcut that's installed in the start menu by default",
"shell": "powershell",
"category": "TRMM (Win):TacticalRMM Related",
"default_timeout": "10"
@@ -552,9 +639,18 @@
"shell": "powershell",
"category": "TRMM (Win):Storage"
},
{
"guid": "6a52f495-d43e-40f4-91a9-bbe4f578e6d1",
"filename": "Win_User_Create.ps1",
"submittedBy": "https://github.com/brodur",
"name": "Create Local User",
"description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)",
"shell": "powershell",
"category": "TRMM (Win):Other"
},
{
"guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
"filename": "Win_Get_Computer_Users.ps1",
"filename": "Win_Users_List.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Get Computer Users",
"description": "Get list of computer users and show which one is enabled",
@@ -563,9 +659,9 @@
},
{
"guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",
"filename": "Win_Set_Network_To_Private.ps1",
"filename": "Win_Network_Set_To_Private.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Set Network To Private",
"name": "Network Category - Set Network To Private",
"description": "Sets current network type to Private",
"shell": "powershell",
"category": "TRMM (Win):Network"
@@ -574,9 +670,59 @@
"guid": "768f42d5-7b45-45ed-8233-254ae537aaa2",
"filename": "Win_TaskScheduler_Add_Task.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Add task to TaskScheduler",
"name": "Task Scheduler - Add a task",
"description": "Add a task to Task Scheduler, needs editing",
"shell": "powershell",
"category": "TRMM (Win):Other"
},
{
"guid": "e371f1c6-0dd9-44de-824c-a17e1ca4c4ab",
"filename": "Win_Outlook_SentItems_To_Delegated_Folders.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Outlook - Delegated folders set for all profiles",
"description": "Uses RunAsUser to setup sent items for the currently logged on user on delegated folders to go into the delegated folders sent for all.",
"shell": "powershell",
"category": "TRMM (Win):Office",
"default_timeout": "90"
},
{
"guid": "17040742-184a-4251-8f7b-4a1b0a1f02d1",
"filename": "Win_File_Copy_Misc.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "EXAMPLE File Copying using powershell",
"description": "Reference Script: Will need manual tweaking, for copying files/folders from paths/websites to local",
"shell": "powershell",
"category": "TRMM (Win):Misc>Reference",
"default_timeout": "1"
},
{
"guid": "168037d8-78e6-4a6a-a9a9-8ec2c1dbe949",
"filename": "Win_MSI_Install.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "EXAMPLE Function for running MSI install via powershell",
"description": "Reference Script: Will need manual tweaking, for running MSI from powershell",
"shell": "powershell",
"category": "TRMM (Win):Misc>Reference",
"default_timeout": "1"
},
{
"guid": "453c6d22-84b7-4767-8b5f-b825f233cf55",
"filename": "Win_AD_Join_Computer.ps1",
"submittedBy": "https://github.com/rfost52",
"name": "AD - Join Computer to Domain",
"description": "Join computer to a domain in Active Directory",
"shell": "powershell",
"category": "TRMM (Win):Active Directory",
"default_timeout": "300"
},
{
"guid": "962d3cce-49a2-4f3e-a790-36f62a6799a0",
"filename": "Win_Collect_System_Report_And_Email.ps1",
"submittedBy": "https://github.com/rfost52",
"name": "Collect System Report and Email",
"description": "Generates a system report in HTML format, then emails it",
"shell": "powershell",
"category": "TRMM (Win):Other",
"default_timeout": "300"
}
]

View File

@@ -1,12 +1,14 @@
import base64
import re
from loguru import logger
from typing import Any, List, Union
from typing import List, Optional
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from loguru import logger
from logs.models import BaseAuditModel
from tacticalrmm.utils import replace_db_values
SCRIPT_SHELLS = [
("powershell", "Powershell"),
@@ -192,12 +194,9 @@ class Script(BaseAuditModel):
return ScriptSerializer(script).data
@classmethod
def parse_script_args(
cls, agent, shell: str, args: List[str] = list()
) -> Union[List[str], None]:
from core.models import CustomField
def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list:
if not list:
if not args:
return []
temp_args = list()
@@ -210,88 +209,15 @@ class Script(BaseAuditModel):
if match:
# only get the match between the () in regex
string = match.group(1)
value = replace_db_values(string=string, agent=agent, shell=shell)
# split by period if exists. First should be model and second should be property
temp = string.split(".")
# check for model and property
if len(temp) != 2:
# ignore arg since it is invalid
continue
if temp[0] == "client":
model = "client"
obj = agent.client
elif temp[0] == "site":
model = "site"
obj = agent.site
elif temp[0] == "agent":
model = "agent"
obj = agent
if value:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
else:
# ignore arg since it is invalid
continue
if hasattr(obj, temp[1]):
value = getattr(obj, temp[1])
elif CustomField.objects.filter(model=model, name=temp[1]).exists():
field = CustomField.objects.get(model=model, name=temp[1])
model_fields = getattr(field, f"{model}_fields")
value = None
if model_fields.filter(**{model: obj}).exists():
value = model_fields.get(**{model: obj}).value
if not value and field.default_value:
value = field.default_value
# check if value exists and if not use defa
if value and field.type == "multiple":
value = format_shell_array(shell, value)
elif value and field.type == "checkbox":
value = format_shell_bool(shell, value)
if not value:
continue
else:
# ignore arg since property is invalid
continue
# replace the value in the arg and push to array
# log any unhashable type errors
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
except Exception as e:
logger.error(e)
continue
# pass parameter unaltered
temp_args.append(arg)
else:
temp_args.append(arg)
return temp_args
def format_shell_array(shell: str, value: Any) -> str:
if shell == "cmd":
return "array args are not supported with batch"
elif shell == "powershell":
temp_string = ""
for item in value:
temp_string += item + ","
return temp_string.strip(",")
else: # python
temp_string = ""
for item in value:
temp_string += item + ","
return temp_string.strip(",")
def format_shell_bool(shell: str, value: Any) -> str:
if shell == "cmd":
return "1" if value else "0"
elif shell == "powershell":
return "$True" if value else "$False"
else: # python
return "True" if value else "False"

View File

@@ -0,0 +1,11 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageScriptsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_scripts")

View File

@@ -22,14 +22,5 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
@app.task
def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
script = Script.objects.get(pk=scriptpk)
nats_data = {
"func": "runscript",
"timeout": timeout,
"script_args": args,
"payload": {
"code": script.code,
"shell": script.shell,
},
}
for agent in Agent.objects.filter(pk__in=agentpks):
asyncio.run(agent.nats_cmd(nats_data, wait=False))
agent.run_script(scriptpk=script.pk, args=args, timeout=timeout)

View File

@@ -1,4 +1,3 @@
from email.policy import default
import json
import os
from pathlib import Path
@@ -15,6 +14,7 @@ from .serializers import ScriptSerializer, ScriptTableSerializer
class TestScriptViews(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.authenticate()
def test_get_scripts(self):
@@ -288,3 +288,212 @@ class TestScriptViews(TacticalTestCase):
fn: str = script["filename"]
if " " in fn:
raise Exception(f"{fn} must not contain spaces in filename")
def test_script_arg_variable_replacement(self):
agent = baker.make_recipe("agents.agent", public_ip="12.12.12.12")
args = [
"-Parameter",
"-Another {{agent.public_ip}}",
"-Client {{client.name}}",
"-Site {{site.name}}",
]
self.assertEqual(
[
"-Parameter",
"-Another '12.12.12.12'",
f"-Client '{agent.client.name}'",
f"-Site '{agent.site.name}'",
],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
def test_script_arg_replacement_custom_field(self):
agent = baker.make_recipe("agents.agent")
field = baker.make(
"core.CustomField",
name="Test Field",
model="agent",
type="text",
default_value_string="DEFAULT",
)
args = ["-Parameter", "-Another {{agent.Test Field}}"]
# test default value
self.assertEqual(
["-Parameter", "-Another 'DEFAULT'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set value
baker.make(
"agents.AgentCustomField",
field=field,
agent=agent,
string_value="CUSTOM VALUE",
)
self.assertEqual(
["-Parameter", "-Another 'CUSTOM VALUE'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
def test_script_arg_replacement_client_custom_fields(self):
agent = baker.make_recipe("agents.agent")
field = baker.make(
"core.CustomField",
name="Test Field",
model="client",
type="text",
default_value_string="DEFAULT",
)
args = ["-Parameter", "-Another {{client.Test Field}}"]
# test default value
self.assertEqual(
["-Parameter", "-Another 'DEFAULT'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set value
baker.make(
"clients.ClientCustomField",
field=field,
client=agent.client,
string_value="CUSTOM VALUE",
)
self.assertEqual(
["-Parameter", "-Another 'CUSTOM VALUE'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
def test_script_arg_replacement_site_custom_fields(self):
agent = baker.make_recipe("agents.agent")
field = baker.make(
"core.CustomField",
name="Test Field",
model="site",
type="text",
default_value_string="DEFAULT",
)
args = ["-Parameter", "-Another {{site.Test Field}}"]
# test default value
self.assertEqual(
["-Parameter", "-Another 'DEFAULT'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set value
value = baker.make(
"clients.SiteCustomField",
field=field,
site=agent.site,
string_value="CUSTOM VALUE",
)
self.assertEqual(
["-Parameter", "-Another 'CUSTOM VALUE'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set but empty field value
value.string_value = "" # type: ignore
value.save() # type: ignore
self.assertEqual(
["-Parameter", "-Another 'DEFAULT'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test blank default and value
field.default_value_string = "" # type: ignore
field.save() # type: ignore
self.assertEqual(
["-Parameter", "-Another ''"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
def test_script_arg_replacement_array_fields(self):
agent = baker.make_recipe("agents.agent")
field = baker.make(
"core.CustomField",
name="Test Field",
model="agent",
type="multiple",
default_values_multiple=["this", "is", "an", "array"],
)
args = ["-Parameter", "-Another {{agent.Test Field}}"]
# test default value
self.assertEqual(
["-Parameter", "-Another 'this,is,an,array'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set value and python shell
baker.make(
"agents.AgentCustomField",
field=field,
agent=agent,
multiple_value=["this", "is", "new"],
)
self.assertEqual(
["-Parameter", "-Another 'this,is,new'"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
def test_script_arg_replacement_boolean_fields(self):
agent = baker.make_recipe("agents.agent")
field = baker.make(
"core.CustomField",
name="Test Field",
model="agent",
type="checkbox",
default_value_bool=True,
)
args = ["-Parameter", "-Another {{agent.Test Field}}"]
# test default value with python
self.assertEqual(
["-Parameter", "-Another 1"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set value and python shell
custom = baker.make(
"agents.AgentCustomField",
field=field,
agent=agent,
bool_value=False,
)
self.assertEqual(
["-Parameter", "-Another 0"],
Script.parse_script_args(agent=agent, shell="python", args=args),
)
# test with set value and cmd shell
self.assertEqual(
["-Parameter", "-Another 0"],
Script.parse_script_args(agent=agent, shell="cmd", args=args),
)
# test with set value and powershell
self.assertEqual(
["-Parameter", "-Another $False"],
Script.parse_script_args(agent=agent, shell="powershell", args=args),
)
# test with True value powershell
custom.bool_value = True # type: ignore
custom.save() # type: ignore
self.assertEqual(
["-Parameter", "-Another $True"],
Script.parse_script_args(agent=agent, shell="powershell", args=args),
)

View File

@@ -4,20 +4,23 @@ import json
from django.conf import settings
from django.shortcuts import get_object_or_404
from loguru import logger
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.parsers import FileUploadParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import Script
from .permissions import ManageScriptsPerms
from .serializers import ScriptSerializer, ScriptTableSerializer
logger.configure(**settings.LOG_CONFIG)
class GetAddScripts(APIView):
permission_classes = [IsAuthenticated, ManageScriptsPerms]
parser_class = (FileUploadParser,)
def get(self, request):
@@ -63,6 +66,8 @@ class GetAddScripts(APIView):
class GetUpdateDeleteScript(APIView):
permission_classes = [IsAuthenticated, ManageScriptsPerms]
def get(self, request, pk):
script = get_object_or_404(Script, pk=pk)
return Response(ScriptSerializer(script).data)
@@ -103,6 +108,7 @@ class GetUpdateDeleteScript(APIView):
@api_view()
@permission_classes([IsAuthenticated, ManageScriptsPerms])
def download(request, pk):
script = get_object_or_404(Script, pk=pk)

View File

@@ -0,0 +1,8 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageWinSvcsPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_manage_winsvcs")

View File

@@ -3,13 +3,15 @@ import asyncio
from django.conf import settings
from django.shortcuts import get_object_or_404
from loguru import logger
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from agents.models import Agent
from checks.models import Check
from tacticalrmm.utils import notify_error
from .permissions import ManageWinSvcsPerms
from .serializers import ServicesSerializer
logger.configure(**settings.LOG_CONFIG)
@@ -34,6 +36,7 @@ def default_services(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, ManageWinSvcsPerms])
def service_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
action = request.data["sv_action"]
@@ -85,6 +88,7 @@ def service_detail(request, pk, svcname):
@api_view(["POST"])
@permission_classes([IsAuthenticated, ManageWinSvcsPerms])
def edit_service(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
data = {

View File

@@ -0,0 +1,11 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageSoftwarePerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_software")

View File

@@ -3,7 +3,8 @@ 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.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from agents.models import Agent
@@ -11,6 +12,7 @@ from logs.models import PendingAction
from tacticalrmm.utils import filter_software, notify_error
from .models import ChocoSoftware, InstalledSoftware
from .permissions import ManageSoftwarePerms
from .serializers import InstalledSoftwareSerializer
@@ -20,6 +22,7 @@ def chocos(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, ManageSoftwarePerms])
def install(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if pyver.parse(agent.version) < pyver.parse("1.4.8"):

View File

@@ -41,7 +41,7 @@ app.conf.beat_schedule = {
},
"get-wmi": {
"task": "agents.tasks.get_wmi_task",
"schedule": crontab(minute="*/18"),
"schedule": crontab(minute=18, hour="*/5"),
},
}
@@ -54,10 +54,12 @@ def debug_task(self):
@app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
from agents.tasks import agent_outages_task
from agents.tasks import agent_outages_task, agent_checkin_task
from alerts.tasks import unsnooze_alerts
from core.tasks import core_maintenance_tasks
from core.tasks import core_maintenance_tasks, cache_db_fields_task
sender.add_periodic_task(45.0, agent_checkin_task.s())
sender.add_periodic_task(60.0, agent_outages_task.s())
sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s())
sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s())
sender.add_periodic_task(90.0, cache_db_fields_task.s())

View File

@@ -0,0 +1,7 @@
def _has_perm(request, perm):
if request.user.is_superuser or (
request.user.role and getattr(request.user.role, "is_superuser")
):
return True
return request.user.role and getattr(request.user.role, perm)

View File

@@ -15,20 +15,23 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.6.0"
TRMM_VERSION = "0.7.0"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.129"
APP_VER = "0.0.139"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.5.0"
LATEST_AGENT_VER = "1.5.9"
MESH_VER = "0.7.93"
MESH_VER = "0.8.60"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "15"
NPM_VER = "14"
PIP_VER = "19"
NPM_VER = "18"
SETUPTOOLS_VER = "57.0.0"
WHEEL_VER = "0.36.2"
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"
@@ -42,6 +45,12 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
ASGI_APPLICATION = "tacticalrmm.asgi.application"
REST_KNOX = {
"TOKEN_TTL": timedelta(hours=5),
"AUTO_REFRESH": True,
"MIN_REFRESH_INTERVAL": 600,
}
try:
from .local_settings import *
except ImportError:
@@ -77,6 +86,15 @@ if not "AZPIPELINE" in os.environ:
if DEBUG: # type: ignore
INSTALLED_APPS += ("django_extensions",)
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [(REDIS_HOST, 6379)], # type: ignore
},
},
}
if "AZPIPELINE" in os.environ:
ADMIN_ENABLED = False
@@ -101,11 +119,6 @@ MIDDLEWARE = [
if ADMIN_ENABLED: # type: ignore
MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
REST_KNOX = {
"TOKEN_TTL": timedelta(hours=5),
"AUTO_REFRESH": True,
"MIN_REFRESH_INTERVAL": 600,
}
ROOT_URLCONF = "tacticalrmm.urls"

View File

@@ -10,9 +10,11 @@ from core.models import CoreSettings
class TacticalTestCase(TestCase):
def authenticate(self):
self.john = User(username="john")
self.john.is_superuser = True
self.john.set_password("hunter2")
self.john.save()
self.alice = User(username="alice")
self.alice.is_superuser = True
self.alice.set_password("hunter2")
self.alice.save()
self.client_setup()
@@ -20,7 +22,8 @@ class TacticalTestCase(TestCase):
def setup_agent_auth(self, agent):
agent_user = User.objects.create_user(
username=agent.agent_id, password=User.objects.make_random_password(60)
username=agent.agent_id,
password=User.objects.make_random_password(60),
)
Token.objects.create(user=agent_user)

View File

@@ -66,8 +66,7 @@ class TestUtils(TestCase):
mock_subprocess.assert_not_called()
@override_settings(
ALLOWED_HOSTS=["api.example.com"],
SECRET_KEY="sekret",
ALLOWED_HOSTS=["api.example.com"], SECRET_KEY="sekret", DOCKER_BUILD=False
)
@patch("subprocess.run")
def test_reload_nats(self, mock_subprocess):

View File

@@ -3,7 +3,7 @@ from django.urls import include, path
from knox import views as knox_views
from accounts.views import CheckCreds, LoginView
from core import consumers
from core.consumers import DashInfo
urlpatterns = [
path("checkcreds/", CheckCreds.as_view()),
@@ -32,5 +32,5 @@ if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
ws_urlpatterns = [
path("ws/dashinfo/", consumers.DashInfo.as_asgi()), # type: ignore
path("ws/dashinfo/", DashInfo.as_asgi()), # type: ignore
]

View File

@@ -5,7 +5,7 @@ import subprocess
import tempfile
import time
import urllib.parse
from typing import Union
from typing import Optional, Union
import pytz
import requests
@@ -52,7 +52,7 @@ def generate_winagent_exe(
file_name: str,
) -> Union[Response, FileResponse]:
from agents.tasks import _get_exegen_url
from agents.utils import get_exegen_url
inno = (
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
@@ -62,7 +62,7 @@ def generate_winagent_exe(
try:
codetoken = CodeSignToken.objects.first().token
base_url = _get_exegen_url() + "/api/v1/winagents/?"
base_url = get_exegen_url() + "/api/v1/winagents/?"
params = {
"version": settings.LATEST_AGENT_VER,
"arch": arch,
@@ -263,3 +263,126 @@ def run_nats_api_cmd(mode: str, ids: list[str], timeout: int = 30) -> None:
subprocess.run(cmd, capture_output=True, timeout=timeout)
except Exception as e:
logger.error(e)
def get_latest_trmm_ver() -> str:
url = "https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py"
try:
r = requests.get(url, timeout=5)
except:
return "error"
try:
for line in r.text.splitlines():
if "TRMM_VERSION" in line:
return line.split(" ")[2].strip('"')
except Exception as e:
logger.error(e)
return "error"
def replace_db_values(
string: str, agent: Agent = None, shell: str = None, quotes=True
) -> Union[str, None]:
from core.models import CustomField, GlobalKVStore
# split by period if exists. First should be model and second should be property i.e {{client.name}}
temp = string.split(".")
# check for model and property
if len(temp) < 2:
# ignore arg since it is invalid
return None
# value is in the global keystore and replace value
if temp[0] == "global":
if GlobalKVStore.objects.filter(name=temp[1]).exists():
value = GlobalKVStore.objects.get(name=temp[1]).value
return f"'{value}'" if quotes else value
else:
logger.error(
f"Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store"
)
return None
if not agent:
# agent must be set if not global property
return f"There was an error finding the agent: {agent}"
if temp[0] == "client":
model = "client"
obj = agent.client
elif temp[0] == "site":
model = "site"
obj = agent.site
elif temp[0] == "agent":
model = "agent"
obj = agent
else:
# ignore arg since it is invalid
logger.error(
f"Not enough information to find value for: {string}. Only agent, site, client, and global are supported."
)
return None
if hasattr(obj, temp[1]):
value = f"'{getattr(obj, temp[1])}'" if quotes else getattr(obj, temp[1])
elif CustomField.objects.filter(model=model, name=temp[1]).exists():
field = CustomField.objects.get(model=model, name=temp[1])
model_fields = getattr(field, f"{model}_fields")
value = None
if model_fields.filter(**{model: obj}).exists():
if field.type != "checkbox" and model_fields.get(**{model: obj}).value:
value = model_fields.get(**{model: obj}).value
elif field.type == "checkbox":
value = model_fields.get(**{model: obj}).value
# need explicit None check since a false boolean value will pass default value
if value == None and field.default_value != None:
value = field.default_value
# check if value exists and if not use default
if value and field.type == "multiple":
value = (
f"'{format_shell_array(value)}'"
if quotes
else format_shell_array(value)
)
elif value != None and field.type == "checkbox":
value = format_shell_bool(value, shell)
else:
value = f"'{value}'" if quotes else value
else:
# ignore arg since property is invalid
logger.error(
f"Couldn't find property on supplied variable: {string}. Make sure it exists as a custom field or a valid agent property"
)
return None
# log any unhashable type errors
if value != None:
return value # type: ignore
else:
logger.error(
f"Couldn't lookup value for: {string}. Make sure it exists as a custom field or a valid agent property"
)
return None
def format_shell_array(value: list) -> str:
temp_string = ""
for item in value:
temp_string += item + ","
return f"{temp_string.strip(',')}"
def format_shell_bool(value: bool, shell: Optional[str]) -> str:
if shell == "powershell":
return "$True" if value else "$False"
else:
return "1" if value else "0"

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