diff --git a/examples/custom_rule.ipynb b/examples/custom_rule.ipynb new file mode 100644 index 00000000..d8e94e9d --- /dev/null +++ b/examples/custom_rule.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import getpass\n", + "from requests import Session\n", + "from pprint import pprint\n", + "import urllib3\n", + "urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n", + "\n", + "url = \"https://localhost\"\n", + "user = input()\n", + "password = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LOGIN" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "session = Session()\n", + "\n", + "first = session.get(f\"{url}\", verify=False)\n", + "csrftoken = first.cookies[\"csrftoken\"]\n", + "\n", + "data = json.dumps(\n", + " {\"username\": user, \"password\": password, \"csrfmiddlewaretoken\": csrftoken}\n", + ")\n", + "\n", + "headers = {\n", + " \"X-CSRFToken\": first.headers[\"Set-Cookie\"].split(\"=\")[1].split(\";\")[0],\n", + " \"Referer\": url,\n", + " \"X-Requested-With\": \"XMLHttpRequest\",\n", + "}\n", + "\n", + "req = session.post(\n", + " f\"{url}/api/auth/\", data=data, cookies=first.cookies, headers=headers, verify=False\n", + ")\n", + "if req.status_code != 200:\n", + " print(req.text)\n", + " exit(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GET CUSTOM RULE LIST" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rules = session.get(f\"{url}/api/customrules/?start=0&length=1&draw=0\", verify=False).json()\n", + "print(f\"{len(rules['data'])} rules returned of {rules['recordsTotal']}\")\n", + "if len(rules['data']) > 0: \n", + " pprint(rules['data'][0])\n", + " rule_pk = rules['data'][0]['id']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CREATE CUSTOM RULE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\n", + " \"rule_ids\": [2, 4], # Rule Id to be merged in custom rule \n", + " \"rulename\": \"combined_rule\"\n", + "}\n", + "res = session.post(f\"{url}/api/rules/build\", json=data, cookies=first.cookies, headers=headers, verify=False)\n", + "if res.status_code == 200:\n", + " pprint(res.json())\n", + "else:\n", + " print(res.status_code, res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# EDIT CUSTOM RULE (make default)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "res = session.post(f\"{url}/api/customrules/{rule_pk}/default\", cookies=first.cookies, headers=headers, verify=False)\n", + "if res.status_code == 200:\n", + " pprint(res.json())\n", + "else:\n", + " print(res.status_code, res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PUBLISH CUSTOM RULE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\n", + " \"rule_ids\": [rule_pk], \n", + " \"action\": \"Publish\"\n", + "}\n", + "res = session.post(f\"{url}/api/customrules/publish\", json=data, cookies=first.cookies, headers=headers, verify=False)\n", + "if res.status_code == 200:\n", + " pprint(res.json())\n", + "else:\n", + " print(res.status_code, res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# DOWNLOAD RULE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "res = session.get(f\"{url}/api/customrules/{rule_pk}/download\", cookies=first.cookies, headers=headers, verify=False)\n", + "if res.status_code == 200:\n", + " pprint(res.text)\n", + "else:\n", + " print(res.status_code, res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# DELETE CUSTOM RULE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\n", + " \"rule_ids\": [rule_pk]\n", + "}\n", + "res = session.delete(f\"{url}/api/customrules/\", json=data, cookies=first.cookies, headers=headers, verify=False)\n", + "print(res.status_code, res.text) " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/rule.ipynb b/examples/rule.ipynb index 37faa81a..9a79a100 100644 --- a/examples/rule.ipynb +++ b/examples/rule.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -63,22 +63,9 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1 rules returned of 24004\n", - "{'headline': '',\n", - " 'id': 24313,\n", - " 'path_name': 'aaa6.yara',\n", - " 'ruleset_description': 'Your crafted ruleset',\n", - " 'ruleset_name': 'admin-Ruleset'}\n" - ] - } - ], + "outputs": [], "source": [ "rules = session.get(f\"{url}/api/rules/?start=0&length=1&draw=0\", verify=False).json()\n", "print(f\"{len(rules['data'])} rules returned of {rules['recordsTotal']}\")\n", @@ -96,30 +83,9 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[{'compiled': False,\n", - " 'created': '2024-10-29T14:08:24.507Z',\n", - " 'enabled': True,\n", - " 'id': 24322,\n", - " 'path': '/yara/admin-Ruleset/example13.yar',\n", - " 'ruleset': 1,\n", - " 'updated': '2024-10-29T14:08:24.507Z'},\n", - " {'compiled': False,\n", - " 'created': '2024-10-29T14:08:24.514Z',\n", - " 'enabled': True,\n", - " 'id': 24323,\n", - " 'path': '/yara/admin-Ruleset/example24.yar',\n", - " 'ruleset': 1,\n", - " 'updated': '2024-10-29T14:08:24.514Z'}]\n" - ] - } - ], + "outputs": [], "source": [ "\n", "files = [\n", @@ -144,17 +110,9 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'message': 'Rule combined_rule created'}\n" - ] - } - ], + "outputs": [], "source": [ "data = {\n", " \"rule_ids\": rule_pks,\n", @@ -176,17 +134,9 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'message': 'Rule example13.yar updated.'}\n" - ] - } - ], + "outputs": [], "source": [ "data = {\n", " \"text\": \"rule NewRule { condition: true }\"\n", @@ -207,17 +157,9 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "'rule NewRule { condition: true }'\n" - ] - } - ], + "outputs": [], "source": [ "res = session.get(f\"{url}/api/rules/{rule_pks[0]}/download\", cookies=first.cookies, headers=headers, verify=False)\n", "if res.status_code == 200:\n", @@ -230,22 +172,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# DELETE DUMP" + "# DELETE RULES" ] }, { "cell_type": "code", - "execution_count": 45, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "200 {\"message\": \"2 rules deleted.\"}\n" - ] - } - ], + "outputs": [], "source": [ "data = {\n", " \"rule_ids\": rule_pks\n", diff --git a/orochi/api/models.py b/orochi/api/models.py index 9e87d891..6c66065b 100644 --- a/orochi/api/models.py +++ b/orochi/api/models.py @@ -289,18 +289,49 @@ class RuleData(Schema): default: bool -class CustomRulesOutSchema(Schema): - recordsTotal: int - recordsFiltered: int - data: List[RuleData] - - class CustomRuleEditInSchema(ModelSchema): class Meta: model = CustomRule fields = ["public"] +class CustomRulePagination(PaginationBase): + class Input(Schema): + start: int + length: int + + class Output(Schema): + draw: int + recordsTotal: int + recordsFiltered: int + data: List[RuleData] + + items_attribute: str = "data" + + def paginate_queryset(self, queryset, pagination: Input, **params): + request = params["request"] + return { + "draw": request.draw, + "recordsTotal": request.total, + "recordsFiltered": queryset.count(), + "data": [ + RuleData( + **{ + "id": x.pk, + "name": x.name, + "path": x.path, + "user": x.user.username, + "public": x.public, + "default": x.default, + } + ) + for x in queryset[ + pagination.start : pagination.start + pagination.length + ] + ], + } + + ################################################### # Rules ################################################### @@ -336,14 +367,10 @@ class RuleOut(Schema): headline: Optional[str] = None -class Order(Schema): - column: int = 1 - dir: str = Field("asc", pattern="^(asc|desc)$") - - class RuleFilter(Schema): search: str = None - order: Order = None + order_column: int = 1 + order_dir: str = Field("asc", pattern="^(asc|desc)$") class CustomPagination(PaginationBase): diff --git a/orochi/api/routers/customrules.py b/orochi/api/routers/customrules.py index 6af28ee7..a10561b8 100644 --- a/orochi/api/routers/customrules.py +++ b/orochi/api/routers/customrules.py @@ -1,17 +1,22 @@ import os import shutil +from typing import List, Optional from django.db.models import Q -from django.http import HttpResponse +from django.http import HttpRequest, HttpResponse from extra_settings.models import Setting -from ninja import Router +from ninja import Query, Router +from ninja.pagination import paginate from ninja.security import django_auth from orochi.api.models import ( RULE_ACTION, + CustomRulePagination, ErrorsOut, ListStr, ListStrAction, + RuleData, + RuleFilter, SuccessResponse, ) from orochi.website.models import CustomRule @@ -19,13 +24,40 @@ router = Router() +@router.get( + "/", + auth=django_auth, + url_name="list_customrules", + response=List[RuleData], +) +@paginate(CustomRulePagination) +def list_custom_rules( + request: HttpRequest, draw: Optional[int], filters: RuleFilter = Query(...) +): + rules = CustomRule.objects.filter(Q(public=True) | Q(user=request.user)) + request.draw = draw + request.total = rules.count() + request.search = filters.search or None + if filters.search: + filtered_rules = rules.filter( + Q(name__icontains=filters.search) | Q(path__icontains=filters.search) + ) + else: + filtered_rules = rules + sort_fields = ["pk", "name", "path", "public", "user"] + sort = sort_fields[filters.order_column] if filters.order_column else sort_fields[0] + if filters.order_dir and filters.order_dir == "desc": + sort = f"-{sort}" + return filtered_rules.order_by(sort) + + @router.post( "/{int:id}/default", auth=django_auth, url_name="default_customrule", response={200: SuccessResponse, 400: ErrorsOut}, ) -def default_rule(request, id: int): +def default_custom_rule(request, id: int): """ Set a custom rule as the default. @@ -88,7 +120,7 @@ def publish_custom_rules(request, info: ListStrAction): for rule in rules: rule.public = info.action == RULE_ACTION.PUBLISH rule.save() - return 200, {"message": f"{rules_count} custom rules {info.action}ed."} + return 200, {"message": f"{rules_count} custom rules {info.action.value}ed."} except Exception as excp: return 400, { @@ -97,7 +129,7 @@ def publish_custom_rules(request, info: ListStrAction): @router.get("/{int:id}/download", auth=django_auth) -def download(request, id: int): +def download_custom_rule(request, id: int): """ Download a custom rule file by its primary key. diff --git a/orochi/api/routers/rules.py b/orochi/api/routers/rules.py index 55a07f67..0ea06b38 100644 --- a/orochi/api/routers/rules.py +++ b/orochi/api/routers/rules.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import List +from typing import List, Optional import yara_x from django.contrib.postgres.search import SearchHeadline, SearchQuery @@ -32,7 +32,9 @@ @router.get("/", auth=django_auth, url_name="list_rules", response=List[RuleOut]) @paginate(CustomPagination) -def list_rules(request: HttpRequest, draw: int, filters: RuleFilter = Query(...)): +def list_rules( + request: HttpRequest, draw: Optional[int], filters: RuleFilter = Query(...) +): """Retrieve a list of rules based on the provided filters and pagination. This function fetches rules that are either associated with the authenticated user or are public. @@ -40,7 +42,7 @@ def list_rules(request: HttpRequest, draw: int, filters: RuleFilter = Query(...) Args: request (HttpRequest): The HTTP request object containing user and query information. - draw (int): A draw counter for the DataTables plugin to ensure proper response handling. + draw (int, optional): A draw counter for the DataTables plugin to ensure proper response handling. filters (RuleFilter, optional): An object containing search and order criteria. Defaults to Query(...). Returns: @@ -66,8 +68,8 @@ def list_rules(request: HttpRequest, draw: int, filters: RuleFilter = Query(...) ).annotate(headline=SearchHeadline("rule", query)) sort_fields = ["id", "ruleset__name", "path"] - sort = sort_fields[filters.order.column] if filters.order else sort_fields[0] - if filters.order and filters.order.dir == "desc": + sort = sort_fields[filters.order_column] if filters.order_column else sort_fields[0] + if filters.order_dir and filters.order_dir == "desc": sort = f"-{sort}" return rules.order_by(sort) diff --git a/orochi/templates/users/user_rules.html b/orochi/templates/users/user_rules.html index 1edc1c16..921536ca 100644 --- a/orochi/templates/users/user_rules.html +++ b/orochi/templates/users/user_rules.html @@ -103,10 +103,20 @@ data: function (d) { delete d.columns; if (d.order.length > 0) { - d.order = d.order[0]; + order = d.order[0]; + delete d.order; + d.order_column = order.column; + d.order_dir = order.dir; + } else { + delete d.order; } + if (d.search && d.search.value) { - d.search = d.search.value; + search = d.search.value; + delete d.search; + d.search = search; + } else { + delete d.search; } return d; } @@ -149,7 +159,27 @@ "processing": true, "serverSide": true, "ajax": { - "url": "{% url 'website:list_custom_rules' %}", + "url": "{% url 'api:list_customrules' %}", + data: function (d) { + delete d.columns; + if (d.order.length > 0) { + order = d.order[0]; + delete d.order; + d.order_column = order.column; + d.order_dir = order.dir; + } else { + delete d.order; + } + + if (d.search && d.search.value) { + search = d.search.value; + delete d.search; + d.search = search; + } else { + delete d.search; + } + return d; + } }, 'columnDefs': [ { @@ -160,24 +190,25 @@ } ], 'columns': [ - { 'data': '0' }, - { 'data': '1' }, - { 'data': '2' }, - { 'data': '3' }, - { 'data': '4' }, + { 'data': 'id' }, + { 'data': 'name' }, + { 'data': 'path' }, + { 'data': 'user' }, + { 'data': 'public' }, { + 'data': 'default', sortable: false, render: function (data, type, row, meta) { if (data == true) { return ""; } - return ``; + return ``; }, }, { sortable: false, render: function (data, type, row, meta) { - return ``; + return ``; } }, diff --git a/orochi/website/urls.py b/orochi/website/urls.py index 98630322..6c11d4d4 100644 --- a/orochi/website/urls.py +++ b/orochi/website/urls.py @@ -79,8 +79,6 @@ def to_url(self, value): # ADMIN path("update_plugins", views.update_plugins, name="update_plugins"), path("update_symbols", views.update_symbols, name="update_symbols"), - # RULES - path("list_custom_rules", views.list_custom_rules, name="list_custom_rules"), # 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 d8af34a6..14f69f1e 100644 --- a/orochi/website/views.py +++ b/orochi/website/views.py @@ -67,7 +67,6 @@ ) from orochi.website.models import ( Bookmark, - CustomRule, Dump, Plugin, Result, @@ -1363,37 +1362,3 @@ def upload_packages(request): request=request, ) return JsonResponse(data) - - -############################## -# RULES -############################## -@login_required -@user_passes_test(is_not_readonly) -def list_custom_rules(request): - """Ajax rules return for datatables""" - start = int(request.GET.get("start")) - length = int(request.GET.get("length")) - search = request.GET.get("search[value]") - - sort_column = int(request.GET.get("order[0][column]")) - sort_order = request.GET.get("order[0][dir]") - - sort = ["pk", "name", "path", "public", "user"][sort_column] - if sort_order == "desc": - sort = f"-{sort}" - - rules = CustomRule.objects.filter(Q(public=True) | Q(user=request.user)) - - filtered_rules = rules.filter(Q(name__icontains=search) | Q(path__icontains=search)) - - data = filtered_rules.order_by(sort)[start : start + length] - - return_data = { - "recordsTotal": rules.count(), - "recordsFiltered": filtered_rules.count(), - "data": [ - [x.pk, x.name, x.path, x.user.username, x.public, x.default] for x in data - ], - } - return JsonResponse(return_data)