diff --git a/examples/local_api.ipynb b/examples/dump.ipynb similarity index 98% rename from examples/local_api.ipynb rename to examples/dump.ipynb index f7911592..ac306dfb 100644 --- a/examples/local_api.ipynb +++ b/examples/dump.ipynb @@ -88,7 +88,7 @@ "outputs": [], "source": [ "\n", - "files = {'upload': ('sorpresa.zip', open('/home/dadokkio/Insync/dadokkio@gmail.com/Google Drive/Lavoro/Agusta/DATA/AMF_MemorySamples/linux/sorpresa.zip','rb'))}\n", + "files = {'upload': ('sorpresa.zip', open('/AMF_MemorySamples/linux/sorpresa.zip','rb'))}\n", "data = {\n", " 'payload': '{\"operating_system\": \"Linux\", \"name\": \"sorpresa\", \"folder\": {\"name\": \"linux-samples\"}}'\n", "}\n", diff --git a/examples/example1.yara b/examples/example1.yara new file mode 100644 index 00000000..cc93f6ff --- /dev/null +++ b/examples/example1.yara @@ -0,0 +1,9 @@ +rule Example_One +{ +strings: +$string1 = "pay" +$string2 = "immediately" + +condition: +($string1 and $string2) +} diff --git a/examples/example2.yara b/examples/example2.yara new file mode 100644 index 00000000..c7a8df4b --- /dev/null +++ b/examples/example2.yara @@ -0,0 +1,13 @@ +rule Example_Two +{ +strings: +$MaliciousWeb1 = "www.scamwebsite.com" +$MaliciousWeb2 = "www.notrealwebsite.com" +$Maliciousweb3 = "www.freemoney.com" +$AttackerName1 = "hackx1203" +$AttackerName2 = "Hackor" +$AttackerName3 = "Hax" + +condition: +any of them +} diff --git a/examples/rule.ipynb b/examples/rule.ipynb new file mode 100644 index 00000000..37faa81a --- /dev/null +++ b/examples/rule.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 33, + "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": 36, + "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 RULE LIST" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "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" + ] + } + ], + "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", + "if len(rules['data']) > 0: \n", + " pprint(rules['data'][0])\n", + " rule_pk = rules['data'][0]['id']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CREATE RULE" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "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" + ] + } + ], + "source": [ + "\n", + "files = [\n", + " ('files', ('example1.yar', open('./example1.yara','rb'))),\n", + " ('files', ('example2.yar', open('./example2.yara','rb')))\n", + "]\n", + "res = session.post(f\"{url}/api/rules/\", files=files, cookies=first.cookies, headers=headers, verify=False)\n", + "if res.status_code == 200:\n", + " data = res.json() or []\n", + " pprint(res.json())\n", + " rule_pks = [x['id'] for x in data]\n", + "else:\n", + " print(res.status_code, res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BUILD RULE" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'message': 'Rule combined_rule created'}\n" + ] + } + ], + "source": [ + "data = {\n", + " \"rule_ids\": rule_pks,\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 RULE" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'message': 'Rule example13.yar updated.'}\n" + ] + } + ], + "source": [ + "data = {\n", + " \"text\": \"rule NewRule { condition: true }\"\n", + " }\n", + "res = session.patch(f\"{url}/api/rules/{rule_pks[0]}\", 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": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "'rule NewRule { condition: true }'\n" + ] + } + ], + "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", + " pprint(res.text)\n", + "else:\n", + " print(res.status_code, res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# DELETE DUMP" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "200 {\"message\": \"2 rules deleted.\"}\n" + ] + } + ], + "source": [ + "data = {\n", + " \"rule_ids\": rule_pks\n", + "}\n", + "res = session.delete(f\"{url}/api/rules/\", 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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/orochi/api/models.py b/orochi/api/models.py index 664f589b..9e87d891 100644 --- a/orochi/api/models.py +++ b/orochi/api/models.py @@ -1,13 +1,15 @@ from enum import Enum +from pathlib import Path from typing import Dict, List, Optional from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from ninja import Field, ModelSchema, Schema from ninja.orm import create_schema +from ninja.pagination import PaginationBase from orochi.website.defaults import OSEnum -from orochi.website.models import Bookmark, CustomRule, Dump, Folder, Plugin, Result +from orochi.website.models import Bookmark, CustomRule, Dump, Folder, Plugin from orochi.ya.models import Rule @@ -271,8 +273,6 @@ class BookmarksInSchema(Schema): ################################################### # CustomRules ################################################### - - class User(ModelSchema): class Meta: @@ -326,3 +326,57 @@ class ListStrAction(Schema): class RuleEditInSchena(Schema): text: str + + +class RuleOut(Schema): + id: int + ruleset_name: str + ruleset_description: str + path_name: str + 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 + + +class CustomPagination(PaginationBase): + class Input(Schema): + start: int + length: int + + class Output(Schema): + draw: int + recordsTotal: int + recordsFiltered: int + data: List[RuleOut] + + 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": [ + RuleOut( + **{ + "id": x.pk, + "ruleset_name": x.ruleset.name, + "ruleset_description": x.ruleset.description, + "path_name": Path(x.path).name, + "headline": x.headline if request.search else "", + } + ) + for x in queryset[ + pagination.start : pagination.start + pagination.length + ] + ], + } diff --git a/orochi/api/routers/rules.py b/orochi/api/routers/rules.py index 67d3d543..55a07f67 100644 --- a/orochi/api/routers/rules.py +++ b/orochi/api/routers/rules.py @@ -1,18 +1,27 @@ import os from pathlib import Path +from typing import List import yara_x -from django.http import HttpResponse +from django.contrib.postgres.search import SearchHeadline, SearchQuery +from django.db import transaction +from django.db.models import Q +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404 from extra_settings.models import Setting -from ninja import Router +from ninja import File, Query, Router, UploadedFile +from ninja.pagination import paginate from ninja.security import django_auth from orochi.api.models import ( + CustomPagination, ErrorsOut, ListStr, RuleBuildSchema, RuleEditInSchena, + RuleFilter, + RuleOut, + RulesOutSchema, SuccessResponse, ) from orochi.website.models import CustomRule @@ -21,6 +30,48 @@ router = Router() +@router.get("/", auth=django_auth, url_name="list_rules", response=List[RuleOut]) +@paginate(CustomPagination) +def list_rules(request: HttpRequest, draw: 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. + It supports searching and sorting based on various criteria, returning the results in a paginated format. + + 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. + filters (RuleFilter, optional): An object containing search and order criteria. Defaults to Query(...). + + Returns: + List[RuleOut]: A list of rules that match the specified filters and pagination settings. + """ + rules = ( + Rule.objects.prefetch_related("ruleset") + .filter(Q(ruleset__user__isnull=True) | Q(ruleset__user=request.user)) + .filter(ruleset__enabled=True) + .filter(enabled=True) + ) + request.draw = draw + request.total = rules.count() + request.search = filters.search or None + + if filters.search: + query = SearchQuery(filters.search) + rules = rules.filter( + Q(search_vector=filters.search) + | Q(ruleset__name__icontains=filters.search) + | Q(ruleset__description__icontains=filters.search) + | Q(path__icontains=filters.search) + ).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 = f"-{sort}" + return rules.order_by(sort) + + @router.patch( "/{int:id}", auth=django_auth, @@ -69,7 +120,7 @@ def edit_rule(request, id: int, data: RuleEditInSchena): @router.get("/{int:id}/download", url_name="download_rule", auth=django_auth) -def download(request, id: int): +def download_rule(request, id: int): """ Download a rule file by its primary key. @@ -198,3 +249,53 @@ def build_rules(request, info: RuleBuildSchema): return 200, {"message": f"Rule {info.rulename} created"} except Exception as excp: return 400, {"errors": str(excp)} + + +@router.post( + "/", + url_name="upload_rule", + auth=django_auth, + response={200: List[RulesOutSchema], 400: ErrorsOut}, +) +def upload_rule(request, files: List[UploadedFile] = File(...)): + """Uploads rules from provided files and associates them with the user's ruleset. + + This function handles the uploading of rule files, ensuring they are saved in a user-specific directory. + It creates new rule entries in the database, either with the content of the files or as empty rules if an error occurs during reading. + + Args: + request: The HTTP request object containing user information. + files (List[UploadedFile]): A list of files to be uploaded. + + Returns: + Tuple[int, List[RuleOut] | ErrorsOut]: A tuple containing the HTTP status code and either a list of created rules or error details. + """ + try: + rules = [] + ruleset = get_object_or_404(Ruleset, user=request.user) + user_path = f"{Setting.get('LOCAL_YARA_PATH')}/{request.user.username}-Ruleset" + os.makedirs(user_path, exist_ok=True) + with transaction.atomic(): + for f in files: + new_path = f"{user_path}/{f.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, "wb") as uf: + uf.write(f.read()) + try: + rule = Rule.objects.create( + path=new_path, + ruleset=ruleset, + rule=open(new_path, "rb").read().decode("utf8", "replace"), + ) + except Exception: + rule = Rule.objects.create( + path=new_path, ruleset=ruleset, rule=None + ) + rules.append(rule) + return 200, rules + except Exception as excp: + return 400, {"errors": str(excp)} diff --git a/orochi/templates/users/user_rules.html b/orochi/templates/users/user_rules.html index c2c1c6b5..1edc1c16 100644 --- a/orochi/templates/users/user_rules.html +++ b/orochi/templates/users/user_rules.html @@ -99,7 +99,17 @@ "orderCellsTop": true, "lengthMenu": [[50, 100, 250, 500, 1000], [50, 100, 250, 500, 1000]], "ajax": { - "url": "{% url 'ya:list' %}", + "url": "{% url 'api:list_rules' %}", + data: function (d) { + delete d.columns; + if (d.order.length > 0) { + d.order = d.order[0]; + } + if (d.search && d.search.value) { + d.search = d.search.value; + } + return d; + } }, 'columnDefs': [ { @@ -111,19 +121,20 @@ ], 'columns': [ - { 'data': '0' }, + { 'data': 'id' }, { + 'data': 'ruleset_name', render: function (data, type, row, meta) { - return `${row[1]} `; + return `${row.ruleset_name} `; } }, - { 'data': '3' }, - { 'data': '4' }, + { 'data': 'path_name' }, + { 'data': 'headline' }, { sortable: false, render: function (data, type, row, meta) { - let down = ``; - down += ``; + let down = ``; + down += ``; return down; } } @@ -292,6 +303,48 @@ }); // UPLOAD RULE FORM SUBMIT + $(document).on("submit", "#upload-form", function (e) { + e.preventDefault(); + var form = $(this); + let formData = new FormData(); + const fileInput = form.find('input[type="file"]'); + const files = fileInput[0].files; + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } + $.ajaxSetup({ headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } }); + + $.ajax({ + url: form.attr("action"), + data: formData, + type: 'POST', + contentType: false, + processData: false, + dataType: 'json', + success: function (data) { + $("#modal-update").modal('hide'); + table.ajax.reload(); + $.toast({ + title: 'Rules added!', + content: data.length + " rule(s) added!", + type: 'success', + delay: 5000 + }); + }, + error: function (data) { + $.toast({ + title: 'Error!', + content: data.errors, + type: 'error', + delay: 5000 + }); + }, + }); + }); + + + + // EDIT RULE FORM SUBMIT $(document).on("submit", "#edit-rule", function (e) { e.preventDefault(); var form = $(this); @@ -333,7 +386,7 @@ }); }); - // SHOW RULE + // SHOW/EDIT RULE $(document).on("click", '.btn-show', function (e) { e.preventDefault(); var pk = $(this).data('id'); diff --git a/orochi/templates/website/index.html b/orochi/templates/website/index.html index a9622c93..5dc21422 100644 --- a/orochi/templates/website/index.html +++ b/orochi/templates/website/index.html @@ -78,35 +78,42 @@
History Log