Merge pull request #121 from sadnub/develop
added debug info to audit logs and some more tests
This commit is contained in:
commit
ee2b916898
|
@ -16,3 +16,4 @@ omit =
|
|||
*/celery.py
|
||||
*/wsgi.py
|
||||
*/settings.py
|
||||
*/baker_recipes.py
|
||||
|
|
|
@ -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",
|
||||
)
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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),
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue