Merge pull request #121 from sadnub/develop

added debug info to audit logs and some more tests
This commit is contained in:
wh1te909 2020-09-29 20:08:41 -07:00 committed by GitHub
commit ee2b916898
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 179 additions and 106 deletions

View File

@ -16,3 +16,4 @@ omit =
*/celery.py
*/wsgi.py
*/settings.py
*/baker_recipes.py

View File

@ -0,0 +1,21 @@
from .models import Agent
from model_bakery.recipe import Recipe, seq
from itertools import cycle
agent = Recipe(Agent, client="Default", site="Default", hostname="TestHostname")
server_agent = Recipe(
Agent,
monitoring_mode="server",
client="Default",
site="Default",
hostname="ServerHost",
)
workstation_agent = Recipe(
Agent,
monitoring_mode="server",
client="Default",
site="Default",
hostname="WorkstationHost",
)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.1 on 2020-09-29 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0006_auto_20200923_1753'),
]
operations = [
migrations.AddField(
model_name='auditlog',
name='debug_info',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -55,22 +55,24 @@ class AuditLog(models.Model):
before_value = models.JSONField(null=True, blank=True)
after_value = models.JSONField(null=True, blank=True)
message = models.CharField(max_length=255, null=True, blank=True)
debug_info = models.JSONField(null=True, blank=True)
def __str__(self):
return f"{self.username} {self.action} {self.object_type}"
@staticmethod
def audit_mesh_session(username, hostname):
def audit_mesh_session(username, hostname, debug_info={}):
AuditLog.objects.create(
username=username,
agent=hostname,
object_type="agent",
action="remote_session",
message=f"{username} used Mesh Central to initiate a remote session to {hostname}.",
debug_info=debug_info,
)
@staticmethod
def audit_raw_command(username, hostname, cmd, shell):
def audit_raw_command(username, hostname, cmd, shell, debug_info={}):
AuditLog.objects.create(
username=username,
agent=hostname,
@ -78,10 +80,13 @@ class AuditLog(models.Model):
action="execute_command",
message=f"{username} issued {shell} command on {hostname}.",
after_value=cmd,
debug_info=debug_info,
)
@staticmethod
def audit_object_changed(username, object_type, before, after, name=""):
def audit_object_changed(
username, object_type, before, after, name="", debug_info={}
):
AuditLog.objects.create(
username=username,
object_type=object_type,
@ -89,63 +94,70 @@ class AuditLog(models.Model):
message=f"{username} modified {object_type} {name}",
before_value=before,
after_value=after,
debug_info=debug_info,
)
@staticmethod
def audit_object_add(username, object_type, after, name=""):
def audit_object_add(username, object_type, after, name="", debug_info={}):
AuditLog.objects.create(
username=username,
object_type=object_type,
action="add",
message=f"{username} added {object_type} {name}",
after_value=after,
debug_info=debug_info,
)
@staticmethod
def audit_object_delete(username, object_type, before, name=""):
def audit_object_delete(username, object_type, before, name="", debug_info={}):
AuditLog.objects.create(
username=username,
object_type=object_type,
action="delete",
message=f"{username} deleted {object_type} {name}",
before_value=before,
debug_info=debug_info,
)
@staticmethod
def audit_script_run(username, hostname, script):
def audit_script_run(username, hostname, script, debug_info={}):
AuditLog.objects.create(
agent=hostname,
username=username,
object_type="agent",
action="execute_script",
message=f'{username} ran script: "{script}" on {hostname}',
debug_info=debug_info,
)
@staticmethod
def audit_user_failed_login(username):
def audit_user_failed_login(username, debug_info={}):
AuditLog.objects.create(
username=username,
object_type="user",
action="failed_login",
message=f"{username} failed to login: Credentials were rejected",
debug_info=debug_info,
)
@staticmethod
def audit_user_failed_twofactor(username):
def audit_user_failed_twofactor(username, debug_info={}):
AuditLog.objects.create(
username=username,
object_type="user",
action="failed_login",
message=f"{username} failed to login: Two Factor token rejected",
debug_info=debug_info,
)
@staticmethod
def audit_user_login_successful(username):
def audit_user_login_successful(username, debug_info={}):
AuditLog.objects.create(
username=username,
object_type="user",
action="login",
message=f"{username} logged in successfully",
debug_info=debug_info,
)
@ -156,18 +168,14 @@ class DebugLog(models.Model):
class PendingAction(models.Model):
agent = models.ForeignKey(
Agent,
related_name="pendingactions",
on_delete=models.CASCADE,
Agent, related_name="pendingactions", on_delete=models.CASCADE,
)
entry_time = models.DateTimeField(auto_now_add=True)
action_type = models.CharField(
max_length=255, choices=ACTION_TYPE_CHOICES, null=True, blank=True
)
status = models.CharField(
max_length=255,
choices=STATUS_CHOICES,
default="pending",
max_length=255, choices=STATUS_CHOICES, default="pending",
)
celery_id = models.CharField(null=True, blank=True, max_length=255)
details = models.JSONField(null=True, blank=True)

View File

@ -152,15 +152,9 @@ class TestAuditViews(TacticalTestCase):
self.check_not_authenticated("post", url)
def test_agent_pending_actions(self):
pending_actions = baker.make(
"logs.PendingAction",
agent__pk=self.agent.pk,
agent__hostname=self.agent.hostname,
agent__client=self.agent.client,
agent__site=self.agent.site,
_quantity=6,
)
url = f"/logs/{self.agent.pk}/pendingactions/"
agent = baker.make_recipe("agents.agent")
pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6,)
url = f"/logs/{agent.pk}/pendingactions/"
resp = self.client.get(url, format="json")
serializer = PendingActionSerializer(pending_actions, many=True)
@ -205,7 +199,6 @@ class TestAuditViews(TacticalTestCase):
class TestLogsTasks(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
@patch("agents.models.Agent.salt_api_cmd")
def test_cancel_pending_action_task(self, mock_salt_cmd):

View File

@ -44,6 +44,7 @@ EXCLUDE_PATHS = (
class AuditMiddleware:
before_value = {}
debug_info = {}
pre_save_uid = ""
post_save_uid = ""
pre_delete_uid = ""
@ -87,6 +88,14 @@ class AuditMiddleware:
self.post_save_uid = get_random_string(8)
self.pre_delete_uid = get_random_string(8)
# gather and save debug info
self.debug_info["url"] = request.path
self.debug_info["method"] = request.method
self.debug_info["view_class"] = view_func.cls.__name__
self.debug_info["view_func"] = view_func.__name__
self.debug_info["view_args"] = view_args
self.debug_info["view_kwargs"] = view_kwargs
# get authentcated user after request
user = request.user
# sets the created_by and modified_by fields on models
@ -134,6 +143,7 @@ class AuditMiddleware:
sender.__name__.lower(),
sender.serialize(instance),
instance.__str__(),
debug_info=self.debug_info,
)
else:
AuditLog.audit_object_changed(
@ -142,6 +152,7 @@ class AuditMiddleware:
sender.serialize(self.before_value),
sender.serialize(instance),
instance.__str__(),
debug_info=self.debug_info,
)
def add_audit_entry_delete(self, user, sender, instance, **kwargs):
@ -153,4 +164,5 @@ class AuditMiddleware:
sender.__name__.lower(),
sender.serialize(instance),
instance.__str__(),
debug_info=self.debug_info,
)

View File

@ -1,7 +1,100 @@
from tacticalrmm.test import TacticalTestCase
from .serializers import UpdateSerializer, WinUpdateSerializer, ApprovedUpdateSerializer
from model_bakery import baker
from model_bakery.recipe import foreign_key
from unittest.mock import patch
from pprint import pprint
class TestWinUpdateViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_get_winupdates(self):
agent = baker.make_recipe("agents.agent")
winupdates = baker.make("winupdate.WinUpdate", agent=agent, _quantity=4)
# test a call where agent doesn't exist
resp = self.client.get("/winupdate/500/getwinupdates/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/winupdate/{agent.pk}/getwinupdates/"
resp = self.client.get(url, format="json")
serializer = UpdateSerializer(agent)
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data["winupdates"]), 4)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
@patch("winupdate.tasks.check_for_updates_task.apply_async")
def test_run_update_scan(self, mock_task):
# test a call where agent doesn't exist
resp = self.client.get("/winupdate/500/runupdatescan/", format="json")
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent")
url = f"/winupdate/{agent.pk}/runupdatescan/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_task.assert_called_with(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_install_updates(self, mock_cmd):
# test a call where agent doesn't exist
resp = self.client.get("/winupdate/500/installnow/", format="json")
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent")
url = f"/winupdate/{agent.pk}/installnow/"
# test agent command timeout
mock_cmd.return_value = "timeout"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
# test agent command error
mock_cmd.return_value = "error"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
# test agent command running
mock_cmd.return_value = "running"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
# can't get this to work right
# test agent command no pid field
mock_cmd.return_value = {}
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
# test agent command success
mock_cmd.return_value = {"pid": 3316}
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
def test_edit_policy(self):
url = "/winupdate/editpolicy/"
winupdate = baker.make("winupdate.WinUpdate")
invalid_data = {"pk": 500, "policy": "inherit"}
# test a call where winupdate doesn't exist
resp = self.client.patch(url, invalid_data, format="json")
self.assertEqual(resp.status_code, 404)
data = {"pk": winupdate.pk, "policy": "inherit"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# TODO: add agent api to test

View File

@ -2,8 +2,8 @@ from django.urls import path
from . import views
urlpatterns = [
path("<pk>/getwinupdates/", views.get_win_updates),
path("<pk>/runupdatescan/", views.run_update_scan),
path("<int:pk>/getwinupdates/", views.get_win_updates),
path("<int:pk>/runupdatescan/", views.run_update_scan),
path("editpolicy/", views.edit_policy),
path("winupdater/", views.win_updater),
path("results/", views.results),

View File

@ -47,7 +47,7 @@ def install_updates(request, pk):
# successful response: {'return': [{'SALT-ID': {'pid': 3316}}]}
try:
r["pid"]
except KeyError:
except (KeyError):
return notify_error(str(r))
return Response(f"Patches will now be installed on {agent.hostname}")

View File

@ -1,37 +0,0 @@
# FOR TESTS
version: "3.7"
services:
# Runs Vue unit tests
app-unit-test:
image: node:12
command: npm run test:unit
working_dir: /home/node
volumes:
- ../web:/home/node
# Runs python unit tests
api-test:
image: python:3.8
command: python manage.py test -v 2 --no-input
working_dir: /app
environment:
VIRTUAL_ENV: /app/env
PATH: /app/env/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ../api/tacticalrmm:/app
depends_on:
- db
- redis
- venv
networks:
- database
- redis
# Builds Python Virtual Env to share between containers
venv:
image: python:3.8
command: /bin/bash -c "pip install virtualenv && python -m virtualenv env && ./env/bin/pip install -r requirements.txt && ./env/bin/pip install -r requirements-dev.txt"
working_dir: /app
volumes:
- ../api/tacticalrmm:/app

View File

@ -2,8 +2,9 @@
- install docker and docker-compose
- Obtain wildcard cert or individual certs for each subdomain
- You can copy any wildcard cert public and private key to the docker/nginx-proxy/certs folder.
## Generate certificates with certbot
## Generate certificates with certbot (Optional if you already have the certs)
Install Certbot
@ -33,7 +34,7 @@ cd docker
sudo docker-compose up -d
```
You may need to run this twice since some of the dependant containers won't be ready
You may need to run this twice if some containers fail to start
## Create a super user
@ -41,27 +42,6 @@ You may need to run this twice since some of the dependant containers won't be r
sudo docker-compose exec api python manage.py createsuperuser
```
## Setup 2FA authentication
Get the 2FA code with
```
sudo docker-compose exec api python manage.py generate_totp
```
Use the generated code and the username to generate a bar code for your authenticator app
(domain is the domain name of your site, for example: rmm.example.com)
```
sudo docker-compose exec api python manage.py generate_barcode [2FAcode] [username] [domain]
```
## Rebuild the api container
```
sudo docker-compose up -d --build api
```
## Get MeshCentral EXE download link
Run the below command to get the download link for the mesh central exe. The dashboard will ask for this when you first sign in
@ -80,7 +60,7 @@ sudo docker-compose exec api /bin/bash
If /bin/bash doesn't work then /bin/sh might need to be used.
## Using Docker for Dev
## Using Docker for Dev (optional)
This allows you to edit the files locally and those changes will be presented to the containers. Hot Module Reload (Vue/webpack) and the Python equivalent will also work!
@ -106,22 +86,6 @@ Now run `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d` t
This will mount the local vue and python files in the app container with hot reload. Does not require rebuilding when changes to code are made and the changes will take effect immediately!
### Running the Tests
There is a container that is dedicated to run the vue unit tests. The below command will run them and display the output. You can ignore the orphaned containers message.
```
docker-compose -f docker-compose.test.yml up app-unit-test
```
### Other Considerations
- Using Docker Desktop on Windows will provide more visibility into which containers are running. You also can easily view the logs for each container in real-time, and view container environment variables.
- If you are on a *nix system, you can get equivalent logging by using `docker-compose logs [service_name]`.
- `docker ps` will show running containers.
- `docker system prune` will remove items that are not in use by running containers. There are also `--all and --volumes` options to remove everything if you want to start over. Stop running containers first. `docker-compose -f docker-compose.yml -f docker-compose.dev.yml down`
- If the docker container isn't getting file changes you can restart the host or do a `docker system prune --volumes`. This will remove the docker volumes and will create a new one once the containers are started.
- It is recommended that you use the vscode docker plugin to manage containers. Docker desktop works well too on Windows.