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)