From 0703234a6aff8fd1b4771da179d9a663edf1d65f Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Thu, 13 Jun 2024 16:19:50 +0200 Subject: [PATCH] #1073 wip --- orochi/api/api.py | 2 + orochi/api/models.py | 33 +++- orochi/api/routers/customrules.py | 187 ++++++++++++++++++ orochi/api/routers/rules.py | 67 ++++++- orochi/templates/users/user_bookmarks.html | 4 +- orochi/templates/users/user_rules.html | 97 +++++++-- ..._create.html => partial_index_create.html} | 0 ...tial_edit.html => partial_index_edit.html} | 2 +- ...tial_info.html => partial_index_info.html} | 0 ..._edit_rule.html => partial_rule_edit.html} | 6 +- ...ad_rules.html => partial_rule_upload.html} | 2 +- orochi/website/defaults.py | 2 +- orochi/website/urls.py | 4 - orochi/website/views.py | 94 +-------- orochi/ya/urls.py | 1 - orochi/ya/views.py | 32 +-- 16 files changed, 386 insertions(+), 147 deletions(-) create mode 100644 orochi/api/routers/customrules.py rename orochi/templates/website/{partial_create.html => partial_index_create.html} (100%) rename orochi/templates/website/{partial_edit.html => partial_index_edit.html} (99%) rename orochi/templates/website/{partial_info.html => partial_index_info.html} (100%) rename orochi/templates/ya/{partial_edit_rule.html => partial_rule_edit.html} (90%) rename orochi/templates/ya/{partial_upload_rules.html => partial_rule_upload.html} (99%) diff --git a/orochi/api/api.py b/orochi/api/api.py index 2cafa4af..e88975cd 100644 --- a/orochi/api/api.py +++ b/orochi/api/api.py @@ -2,6 +2,7 @@ from orochi.api.routers.auth import router as auth_router from orochi.api.routers.bookmarks import router as bookmarks_router +from orochi.api.routers.customrules import router as customrules_router from orochi.api.routers.dumps import router as dumps_router from orochi.api.routers.folders import router as folders_router from orochi.api.routers.plugins import router as plugins_router @@ -18,3 +19,4 @@ api.add_router("/utils/", utils_router, tags=["Utils"]) api.add_router("/bookmarks/", bookmarks_router, tags=["Bookmarks"]) api.add_router("/rules/", rules_router, tags=["Rules"]) +api.add_router("/customrules/", customrules_router, tags=["Custom Rules"]) diff --git a/orochi/api/models.py b/orochi/api/models.py index 9fa310e5..94f9e82a 100644 --- a/orochi/api/models.py +++ b/orochi/api/models.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Dict, List, Optional from django.contrib.auth import get_user_model @@ -6,9 +7,15 @@ from ninja.orm import create_schema from orochi.website.defaults import OSEnum -from orochi.website.models import Bookmark, Dump, Folder, Plugin +from orochi.website.models import Bookmark, CustomRule, Dump, Folder, Plugin from orochi.ya.models import Rule + +class RULE_ACTION(str, Enum): + PUBLISH = "Publish" + UNPUBLISH = "Unpublish" + + ################################################### # Auth ################################################### @@ -236,6 +243,21 @@ class BookmarksInSchema(Schema): query: Optional[str] = None +################################################### +# CustomRules +################################################### +class CustomRulesOutSchema(ModelSchema): + class Meta: + model = CustomRule + fields = ["id", "name", "path", "public", "user"] + + +class CustomRuleEditInSchema(ModelSchema): + class Meta: + model = CustomRule + fields = ["public"] + + ################################################### # Rules ################################################### @@ -252,3 +274,12 @@ class Meta: class ListStr(Schema): rule_ids: List[int] + + +class ListStrAction(Schema): + rule_ids: List[int] + action: RULE_ACTION + + +class RuleEditInSchena(Schema): + text: str diff --git a/orochi/api/routers/customrules.py b/orochi/api/routers/customrules.py new file mode 100644 index 00000000..6b47f59d --- /dev/null +++ b/orochi/api/routers/customrules.py @@ -0,0 +1,187 @@ +import os +import shutil +from typing import List + +from django.conf import settings +from django.db.models import Q +from django.http import HttpResponse +from ninja import Query, Router +from ninja.security import django_auth + +from orochi.api.filters import RulesFilter +from orochi.api.models import ( + RULE_ACTION, + CustomRulesOutSchema, + ErrorsOut, + ListStr, + ListStrAction, + SuccessResponse, +) +from orochi.website.models import CustomRule + +router = Router() + + +@router.get("/", response={200: List[CustomRulesOutSchema]}, auth=django_auth) +def list_custom_rules(request, filters: Query[RulesFilter]): + return CustomRule.objects.all() + + +@router.post( + "/{int:id}/default", + auth=django_auth, + url_name="default_customrule", + response={200: SuccessResponse, 400: ErrorsOut}, +) +def default_rule(request, id: int): + """ + Set a custom rule as the default. + + Args: + request: The request object. + id (int): The ID of the custom rule to set as default. + + Returns: + tuple: A tuple containing the status code and a dictionary with a message. + + Raises: + Exception: If an error occurs during the process of setting the rule as default. + """ + try: + old_default = CustomRule.objects.filter(user=request.user, default=True) + if old_default.count() == 1: + old = old_default.first() + old.default = False + old.save() + + rule = CustomRule.objects.get(pk=id) + name = os.path.basename(rule.path) + if rule.user == request.user: + rule.default = True + rule.save() + return 200, {"message": f"Rule {name} set as default."} + # Make a copy + user_path = f"{settings.LOCAL_YARA_PATH}/{request.user.username}-Ruleset" + os.makedirs(user_path, exist_ok=True) + new_path = f"{user_path}/{rule.name}" + filename, extension = os.path.splitext(new_path) + counter = 1 + while os.path.exists(new_path): + new_path = f"{filename}{counter}{extension}" + counter += 1 + + shutil.copy(rule.path, new_path) + CustomRule.objects.create( + user=request.user, name=rule.name, path=new_path, default=True + ) + name = os.path.basename(new_path) + + return 200, { + "message": f"Rule {name} copied in your ruleset and set as default." + } + except Exception as excp: + return 400, {"errors": str(excp)} + + +@router.post( + "/publish", + auth=django_auth, + url_name="publish_customrule", + response={200: SuccessResponse, 400: ErrorsOut}, +) +def publish_custom_rules(request, info: ListStrAction): + try: + rules = CustomRule.objects.filter(pk__in=info.rule_ids, user=request.user) + rules_count = rules.count() + for rule in rules: + rule.public = info.action == RULE_ACTION.PUBLISH + rule.save() + return 200, {"message": f"{rules_count} custom rules {info.action}ed."} + + except Exception as excp: + return 400, { + "errors": (str(excp) if excp else "Generic error during publishing") + } + + +@router.get("/{int:id}/download", auth=django_auth) +def download(request, id: int): + """ + Download a custom rule file by its primary key. + + Args: + pk (int): The primary key of the custom rule to download. + + Returns: + HttpResponse: The HTTP response object containing the downloaded custom rule file. + + Raises: + Exception: If an error occurs during the process. + """ + try: + rule = CustomRule.objects.filter(pk=id).filter( + Q(user=request.user) | Q(public=True) + ) + if rule.count() == 1: + rule = rule.first() + else: + return 400, {"errors": "Generic error"} + if os.path.exists(rule.path): + with open(rule.path, "rb") as f: + rule_data = f.read() + + response = HttpResponse( + rule_data, + content_type="application/text", + ) + response["Content-Disposition"] = ( + f"attachment; filename={os.path.basename(rule.path)}" + ) + return response + else: + return 400, {"errors": "Custom Rule not found"} + except Exception as excp: + return 400, {"errors": str(excp)} + + +@router.delete( + "/", + auth=django_auth, + url_name="delete_customrules", + response={200: SuccessResponse, 400: ErrorsOut}, +) +def delete_custom_rules(request, info: ListStr): + """ + Summary: + Delete custom rules based on the provided rule IDs. + + Explanation: + This function deletes custom rules based on the specified rule IDs belonging to the authenticated user. It removes the rules from the database and returns a success message upon deletion. + + Args: + - request: The request object. + - rule_ids: A list of integers representing the IDs of custom rules to be deleted. + + Returns: + - Tuple containing status code and a message dictionary. + + Raises: + - Any exception encountered during the process will result in a 400 status code with an error message. + """ + try: + rules = CustomRule.objects.filter(pk__in=info.rule_ids, user=request.user) + rules_count = rules.count() + for rule in rules: + os.remove(rule.path) + rules.delete() + delete_message = f"{rules_count} custom rules deleted." + if rules_count != len(info.rule_ids): + delete_message += " Only custom rules in your ruleset have been deleted." + return 200, {"message": delete_message} + + except Exception as excp: + return 400, { + "errors": ( + str(excp) if excp else "Generic error during custom rules deletion" + ) + } diff --git a/orochi/api/routers/rules.py b/orochi/api/routers/rules.py index c979e9e7..aa6aea94 100644 --- a/orochi/api/routers/rules.py +++ b/orochi/api/routers/rules.py @@ -1,7 +1,9 @@ import os +from pathlib import Path from typing import List import yara +from django.conf import settings from django.http import HttpResponse from django.shortcuts import get_object_or_404 from ninja import Query, Router @@ -12,11 +14,12 @@ ErrorsOut, ListStr, RuleBuildSchema, + RuleEditInSchena, RulesOutSchema, SuccessResponse, ) from orochi.website.models import CustomRule -from orochi.ya.models import Rule +from orochi.ya.models import Rule, Ruleset router = Router() @@ -26,8 +29,53 @@ def list_rules(request, filters: Query[RulesFilter]): return Rule.objects.all() -@router.get("/{pk}/download", auth=django_auth) -def download(request, pk: int): +@router.patch( + "/{int:id}", + auth=django_auth, + url_name="edit_rule", + response={200: SuccessResponse, 400: ErrorsOut}, +) +def edit_rule(request, id: int, data: RuleEditInSchena): + """ + Edit or create a rule based on the provided primary key. + + Args: + pk (int): The primary key of the rule to edit or create. + + Returns: + tuple: A tuple containing the HTTP status code and a message indicating the success or error. + Raises: + Exception: If an error occurs during the process. + """ + try: + rule = get_object_or_404(Rule, pk=id) + name = os.path.basename(rule.path) + if rule.ruleset.user == request.user: + with open(rule.path, "w") as f: + f.write(data.text) + return 200, {"message": f"Rule {name} updated."} + ruleset = get_object_or_404(Ruleset, user=request.user) + user_path = f"{settings.LOCAL_YARA_PATH}/{request.user.username}-Ruleset" + os.makedirs(user_path, exist_ok=True) + rule.pk = None + rule.ruleset = ruleset + new_path = f"{user_path}/{Path(rule.path).name}" + filename, extension = os.path.splitext(new_path) + counter = 1 + while os.path.exists(new_path): + new_path = f"{filename}{counter}{extension}" + counter += 1 + with open(new_path, "w") as f: + f.write(data.text) + rule.path = new_path + rule.save() + return 200, {"message": f"Rule {name} created in local ruleset."} + except Exception as excp: + return 400, {"errors": str(excp)} + + +@router.get("/{int:id}/download", url_name="download_rule", auth=django_auth) +def download(request, id: int): """ Download a rule file by its primary key. @@ -41,7 +89,7 @@ def download(request, pk: int): Exception: If an error occurs during the process. """ try: - rule = Rule.objects.filter(pk=pk).filter(ruleset__enabled=True) + rule = Rule.objects.filter(pk=id).filter(ruleset__enabled=True) if rule.count() == 1: rule = rule.first() else: @@ -90,12 +138,13 @@ def delete_rules(request, info: ListStr): """ try: rules = Rule.objects.filter(pk__in=info.rule_ids, ruleset__user=request.user) - rules.delete() rules_count = rules.count() - if rules_count == 0: - return 200, {"message": f"{rules_count} rules deleted."} - else: - return 200, {"message": "Only rules in your ruleset can be deleted."} + rules.delete() + delete_message = f"{rules_count} rules deleted." + if rules_count != len(info.rule_ids): + delete_message += " Only rules in your ruleset have been deleted." + return 200, {"message": delete_message} + except Exception as excp: return 400, { "errors": str(excp) if excp else "Generic error during rules deletion" diff --git a/orochi/templates/users/user_bookmarks.html b/orochi/templates/users/user_bookmarks.html index 50088099..2530ed13 100644 --- a/orochi/templates/users/user_bookmarks.html +++ b/orochi/templates/users/user_bookmarks.html @@ -177,7 +177,7 @@ error: function (data) { $.toast({ title: 'Bookmark status!', - content: data.message, + content: data.errors, type: 'error', delay: 5000 }); @@ -220,7 +220,7 @@ error: function (data) { $.toast({ title: 'Bookmark status!', - content: data.message, + content: data.errors, type: 'error', delay: 5000 }); diff --git a/orochi/templates/users/user_rules.html b/orochi/templates/users/user_rules.html index db724baa..47349529 100644 --- a/orochi/templates/users/user_rules.html +++ b/orochi/templates/users/user_rules.html @@ -187,7 +187,7 @@ { sortable: false, render: function (data, type, row, meta) { - return ``; + return ``; } }, @@ -230,7 +230,7 @@ error: function (data) { $.toast({ title: 'Delete Rules error!', - content: data.message, + content: data.errors, type: 'error', delay: 5000 }); @@ -283,7 +283,7 @@ error: function (data) { $.toast({ title: 'Build Rule Error!', - content: data.message, + content: data.errors, type: 'error', delay: 5000 }); @@ -307,34 +307,46 @@ }, success: function (data) { $("#modal-update .modal-content").html(data.html_form); - initUploadFields(document.getElementById("rules-index")); + initUploadFields(document.getElementById("edit-rule")); } }); }); // UPLOAD RULE FORM SUBMIT - $(document).on("submit", "#rules-index", function (e) { + $(document).on("submit", "#edit-rule", function (e) { e.preventDefault(); var form = $(this); + + let formData = form.serializeArray(); + let obj = {}; + formData.forEach(item => { + if (item.name != 'csrfmiddlewaretoken') { + obj[item.name] = item.value; + } + }); + $.ajaxSetup({ + headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } + }); + $.ajax({ url: form.attr("action"), - data: form.serialize(), - type: form.attr("method"), + data: JSON.stringify(obj), + type: 'PATCH', dataType: 'json', success: function (data) { $("#modal-update").modal('hide'); table.ajax.reload(); $.toast({ title: 'Rules added!', - content: 'Rules added to your ruleset.', + content: data.message, type: 'success', delay: 5000 }); }, - error: function () { + error: function (data) { $.toast({ title: 'Error!', - content: 'Error during rule upload.', + content: data.errors, type: 'error', delay: 5000 }); @@ -375,15 +387,35 @@ if (rows_selected.length > 0) { bootbox.confirm("Delete selected custom rules?", function (result) { ruletable.column(0).checkboxes.deselectAll(); + let obj = {}; var items = []; rows_selected.each(function (val) { items.push(val) }); + obj['rule_ids'] = items; + + $.ajaxSetup({ + headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } + }); $.ajax({ - url: "{% url 'website:delete_rules' %}", - method: 'get', - data: { 'rules': items }, + url: "{% url 'api:delete_customrules' %}", + method: 'delete', + data: JSON.stringify(obj), dataType: 'json', success: function (data) { ruletable.ajax.reload(); + $.toast({ + title: 'Custom Rules Deleted!', + content: data.message, + type: 'success', + delay: 5000 + }); + }, + error: function (data) { + $.toast({ + title: 'Delete Custom Rules error!', + content: data.errors, + type: 'error', + delay: 5000 + }); } }); }); @@ -397,15 +429,31 @@ if (rows_selected.length > 0) { bootbox.confirm(action + " selected custom rules?", function (result) { ruletable.column(0).checkboxes.deselectAll(); + + let obj = {}; var items = []; rows_selected.each(function (val) { items.push(val) }); + obj['rule_ids'] = items; + obj['action'] = action; + + $.ajaxSetup({ + headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } + }); $.ajax({ - url: "{% url 'website:publish_rules' %}", - method: 'get', - data: { 'rules': items, 'action': action }, + url: "{% url 'api:publish_customrule' %}", + method: 'post', + data: JSON.stringify(obj), dataType: 'json', success: function (data) { ruletable.ajax.reload(); + }, + error: function (data) { + $.toast({ + title: 'Error performing action!', + content: data.errors, + type: 'error', + delay: 5000 + }); } }); }); @@ -416,13 +464,24 @@ $(document).on('click', '#star-rule', function () { var rule = $(this).data('pk'); bootbox.confirm("Make selected rule as default?", function (result) { + $.ajaxSetup({ + headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } + }); + $.ajax({ - url: "{% url 'website:make_rule_default' %}", - method: 'get', - data: { 'rule': rule }, + url: "{% url 'api:default_customrule' id=123 %}".replace(/123/, rule), + method: 'post', dataType: 'json', success: function (data) { ruletable.ajax.reload(); + }, + error: function (data) { + $.toast({ + title: 'Error performing action!', + content: data.errors, + type: 'error', + delay: 5000 + }); } }); }) diff --git a/orochi/templates/website/partial_create.html b/orochi/templates/website/partial_index_create.html similarity index 100% rename from orochi/templates/website/partial_create.html rename to orochi/templates/website/partial_index_create.html diff --git a/orochi/templates/website/partial_edit.html b/orochi/templates/website/partial_index_edit.html similarity index 99% rename from orochi/templates/website/partial_edit.html rename to orochi/templates/website/partial_index_edit.html index d2be7376..e3a91ae4 100644 --- a/orochi/templates/website/partial_edit.html +++ b/orochi/templates/website/partial_index_edit.html @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/orochi/templates/website/partial_info.html b/orochi/templates/website/partial_index_info.html similarity index 100% rename from orochi/templates/website/partial_info.html rename to orochi/templates/website/partial_index_info.html diff --git a/orochi/templates/ya/partial_edit_rule.html b/orochi/templates/ya/partial_rule_edit.html similarity index 90% rename from orochi/templates/ya/partial_edit_rule.html rename to orochi/templates/ya/partial_rule_edit.html index 7d8f817a..4130979f 100644 --- a/orochi/templates/ya/partial_edit_rule.html +++ b/orochi/templates/ya/partial_rule_edit.html @@ -1,6 +1,6 @@ {% load widget_tweaks %} -
+ {{ form.media }} {% csrf_token %} -
\ No newline at end of file + diff --git a/orochi/templates/ya/partial_upload_rules.html b/orochi/templates/ya/partial_rule_upload.html similarity index 99% rename from orochi/templates/ya/partial_upload_rules.html rename to orochi/templates/ya/partial_rule_upload.html index 897897f6..885d9065 100644 --- a/orochi/templates/ya/partial_upload_rules.html +++ b/orochi/templates/ya/partial_rule_upload.html @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/orochi/website/defaults.py b/orochi/website/defaults.py index 54f9fc8b..7aec62f3 100644 --- a/orochi/website/defaults.py +++ b/orochi/website/defaults.py @@ -10,7 +10,7 @@ class OSEnum(models.TextChoices): TOAST_RESULT_COLORS = { 0: "blue", - 1: "yellow", + 1: "#FFC300", 2: "green", 3: "green", 4: "orange", diff --git a/orochi/website/urls.py b/orochi/website/urls.py index fe98fc87..b82ae3f2 100644 --- a/orochi/website/urls.py +++ b/orochi/website/urls.py @@ -82,10 +82,6 @@ def to_url(self, value): path("update_symbols", views.update_symbols, name="update_symbols"), # RULES path("list_custom_rules", views.list_custom_rules, name="list_custom_rules"), - path("publish_rules", views.publish_rules, name="publish_rules"), - path("delete_rules", views.delete_rules, name="delete_rules"), - path("make_rule_default", views.make_rule_default, name="make_rule_default"), - path("download_rule/", views.download_rule, name="download_rule"), # SYMBOLS path("reload_symbols", views.reload_symbols, name="reload_symbols"), path("banner_symbols", views.banner_symbols, name="banner_symbols"), diff --git a/orochi/website/views.py b/orochi/website/views.py index c64561c7..7ffa95c6 100644 --- a/orochi/website/views.py +++ b/orochi/website/views.py @@ -36,7 +36,6 @@ from guardian.shortcuts import assign_perm, get_objects_for_user, get_perms, remove_perm from pymisp import MISPEvent, MISPObject, PyMISP from pymisp.tools import FileObject -from volatility3.framework import automagic, contexts from orochi.utils.download_symbols import Downloader from orochi.utils.volatility_dask_elk import ( @@ -77,6 +76,7 @@ Service, UserPlugin, ) +from volatility3.framework import automagic, contexts COLOR_TEMPLATE = """ ", views.download_rule, name="download_rule"), ] diff --git a/orochi/ya/views.py b/orochi/ya/views.py index a4086b50..22500d32 100644 --- a/orochi/ya/views.py +++ b/orochi/ya/views.py @@ -8,7 +8,6 @@ from django.core import management from django.db.models import Q from django.http import Http404, JsonResponse -from django.http.response import HttpResponse from django.shortcuts import get_object_or_404, redirect from django.template.loader import render_to_string from django.views.decorators.http import require_http_methods @@ -101,24 +100,27 @@ def detail(request): """ Return content of rule """ - data = {} pk = request.GET.get("pk") rule = get_object_or_404(Rule, pk=pk) try: with open(rule.path, "rb") as f: rule_data = f.read() - form = EditRuleForm( - initial={ - "text": "".join(rule_data.decode("utf-8", "replace")), - "pk": rule.pk, - } - ) - context = {"form": form} - data["html_form"] = render_to_string( - "ya/partial_edit_rule.html", - context, - request=request, - ) + context = { + "form": EditRuleForm( + initial={ + "text": "".join(rule_data.decode("utf-8", "replace")), + "pk": rule.pk, + } + ), + "id": rule.pk, + } + data = { + "html_form": render_to_string( + "ya/partial_rule_edit.html", + context, + request=request, + ) + } return JsonResponse(data) except UnicodeDecodeError as e: raise Http404 from e @@ -163,7 +165,7 @@ def upload(request): form = RuleForm() context = {"form": form} data["html_form"] = render_to_string( - "ya/partial_upload_rules.html", + "ya/partial_rule_upload.html", context, request=request, )