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 @@ Edit index
-
\ 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 %}
-
\ 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 @@ Upload rules in your ruleset
-
\ 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 = """