From f281b4c5e1f15896bd6407bf7112ba6323f4de78 Mon Sep 17 00:00:00 2001 From: ziad hany Date: Mon, 24 Nov 2025 23:04:21 +0200 Subject: [PATCH] Add Support for Detection Rules in UI/API Resolve migration conflict Add DetectionRule model Signed-off-by: ziad hany --- vulnerabilities/api_v2.py | 36 ++++++++++ vulnerabilities/forms.py | 30 ++++++++ vulnerabilities/models.py | 40 +++++++++++ .../templates/detection_rules.html | 72 +++++++++++++++++++ .../templates/detection_rules_box.html | 46 ++++++++++++ .../templates/includes/rules_pagination.html | 37 ++++++++++ vulnerabilities/templates/navbar.html | 3 + vulnerabilities/views.py | 39 ++++++++++ vulnerablecode/urls.py | 9 +++ 9 files changed, 312 insertions(+) create mode 100644 vulnerabilities/templates/detection_rules.html create mode 100644 vulnerabilities/templates/detection_rules_box.html create mode 100644 vulnerabilities/templates/includes/rules_pagination.html diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 6e0ab9213..c9d4d3596 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -26,8 +26,11 @@ from rest_framework.reverse import reverse from rest_framework.throttling import AnonRateThrottle +from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import CodeFix from vulnerabilities.models import CodeFixV2 +from vulnerabilities.models import DetectionRule +from vulnerabilities.models import ImpactedPackage from vulnerabilities.models import Package from vulnerabilities.models import PipelineRun from vulnerabilities.models import PipelineSchedule @@ -849,3 +852,36 @@ def get_view_name(self): if self.detail: return "Pipeline Instance" return "Pipeline Jobs" + + +class DetectionRuleFilter(filters.FilterSet): + advisory_avid = filters.CharFilter(field_name="related_advisories__avid", lookup_expr="exact") + + rule_text_contains = filters.CharFilter(field_name="rule_text", lookup_expr="icontains") + + class Meta: + model = DetectionRule + fields = ["rule_type"] + + +class DetectionRuleSerializer(serializers.ModelSerializer): + advisory_avid = serializers.SerializerMethodField() + + class Meta: + model = DetectionRule + fields = ["rule_type", "source_url", "rule_metadata", "rule_text", "advisory_avid"] + + def get_advisory_avid(self, obj): + avids = set(advisory.avid for advisory in obj.related_advisories.all()) + return sorted(list(avids)) + + +class DetectionRuleViewSet(viewsets.ReadOnlyModelViewSet): + advisories_prefetch = Prefetch( + "related_advisories", queryset=AdvisoryV2.objects.only("id", "avid").distinct() + ) + queryset = DetectionRule.objects.prefetch_related(advisories_prefetch) + serializer_class = DetectionRuleSerializer + throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle] + filter_backends = [filters.DjangoFilterBackend] + filterset_class = DetectionRuleFilter diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py index 03829cd52..2bdd49232 100644 --- a/vulnerabilities/forms.py +++ b/vulnerabilities/forms.py @@ -13,6 +13,7 @@ from django_altcha import AltchaField from vulnerabilities.models import ApiUser +from vulnerabilities.models import DetectionRuleTypes class PackageSearchForm(forms.Form): @@ -43,6 +44,35 @@ class AdvisorySearchForm(forms.Form): ) +class DetectionRuleSearchForm(forms.Form): + rule_type = forms.ChoiceField( + required=False, + label="Rule Type", + choices=[("", "All")] + DetectionRuleTypes.choices, + initial="", + ) + + advisory_avid = forms.CharField( + required=False, + label="Advisory avid", + widget=forms.TextInput( + attrs={ + "placeholder": "Search by avid: github_osv_importer_v2/GHSA-7g5f-wrx8-5ccf", + } + ), + ) + + rule_text_contains = forms.CharField( + required=False, + label="Rule Text", + widget=forms.TextInput( + attrs={ + "placeholder": "Search in rule text", + } + ), + ) + + class ApiUserCreationForm(forms.ModelForm): """Support a simplified creation for API-only users directly from the UI.""" diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 45d8acf55..99d93e764 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3740,3 +3740,43 @@ class GroupedAdvisory(NamedTuple): weighted_severity: Optional[float] exploitability: Optional[float] risk_score: Optional[float] + + +class DetectionRuleTypes(models.TextChoices): + """Defines the supported formats for security detection rules.""" + + YARA = "yara", "Yara" + YARA_X = "yara-x", "Yara-X" + SIGMA = "sigma", "Sigma" + CLAMAV = "clamav", "ClamAV" + SURICATA = "suricata", "Suricata" + + +class DetectionRule(models.Model): + """ + A Detection Rule is code used to identify malicious activity or security threats. + """ + + rule_type = models.CharField( + max_length=50, + choices=DetectionRuleTypes.choices, + help_text="The type of the detection rule content (e.g., YARA, Sigma).", + ) + + source_url = models.URLField( + max_length=1024, help_text="URL to the original source or reference for this rule." + ) + + rule_metadata = models.JSONField( + null=True, + blank=True, + help_text="Additional structured data such as tags, or author information.", + ) + + rule_text = models.TextField(help_text="The content of the detection signature.") + + related_advisories = models.ManyToManyField( + AdvisoryV2, + related_name="detection_rules", + help_text="Advisories associated with this DetectionRule.", + ) diff --git a/vulnerabilities/templates/detection_rules.html b/vulnerabilities/templates/detection_rules.html new file mode 100644 index 000000000..e665a8eff --- /dev/null +++ b/vulnerabilities/templates/detection_rules.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% load humanize %} +{% load widget_tweaks %} + +{% block title %} +Detection Rule Search +{% endblock %} + +{% block content %} +
+ {% include "detection_rules_box.html" %} +
+ +
+
+
+
+ {{ page_obj.paginator.count|intcomma }} results +
+ {% if is_paginated %} + {% include 'includes/rules_pagination.html' with page_obj=page_obj %} + {% endif %} +
+
+
+ +
+
+ + + + + + + + + + + + {% for detection_rule in page_obj %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
TypeMetadataTextSource URLAdvisory IDs
{{ detection_rule.rule_type }}{{ detection_rule.rule_metadata }}{{ detection_rule.rule_text|truncatewords:10 }}{{ detection_rule.source_url }} + {% for advisory in detection_rule.related_advisories.all %} + {% ifchanged advisory.avid %} + {{ advisory.avid }} +
+ {% endifchanged %} + {% endfor %} +
+ No detection rules found. +
+
+ + + {% if is_paginated %} + {% include 'includes/rules_pagination.html' with page_obj=page_obj %} + {% endif %} +
+ +{% endblock %} diff --git a/vulnerabilities/templates/detection_rules_box.html b/vulnerabilities/templates/detection_rules_box.html new file mode 100644 index 000000000..d76efeaad --- /dev/null +++ b/vulnerabilities/templates/detection_rules_box.html @@ -0,0 +1,46 @@ +{% load widget_tweaks %} +
+
+ Search for Rules + +
+
+
+
+
+
+
+ {% render_field detection_search_form.rule_type %} +
+
+
+ {% render_field detection_search_form.advisory_avid class="input" %} +
+
+ {% render_field detection_search_form.rule_text_contains class="input" %} +
+
+ +
+
+
+
+
+
diff --git a/vulnerabilities/templates/includes/rules_pagination.html b/vulnerabilities/templates/includes/rules_pagination.html new file mode 100644 index 000000000..8f7603b1e --- /dev/null +++ b/vulnerabilities/templates/includes/rules_pagination.html @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/vulnerabilities/templates/navbar.html b/vulnerabilities/templates/navbar.html index 3d3fa0e91..5317638f7 100644 --- a/vulnerabilities/templates/navbar.html +++ b/vulnerabilities/templates/navbar.html @@ -29,6 +29,9 @@ V2 + + Detection Rules + Documentation diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index b984fbb51..a56bedeb6 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -36,6 +36,7 @@ from vulnerabilities.forms import AdminLoginForm from vulnerabilities.forms import AdvisorySearchForm from vulnerabilities.forms import ApiUserCreationForm +from vulnerabilities.forms import DetectionRuleSearchForm from vulnerabilities.forms import PackageSearchForm from vulnerabilities.forms import PipelineSchedulePackageForm from vulnerabilities.forms import VulnerabilitySearchForm @@ -944,6 +945,44 @@ def get_queryset(self): ) +class DetectionRuleSearch(ListView): + model = models.DetectionRule + template_name = "detection_rules.html" + paginate_by = PAGE_SIZE + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + request_query = self.request.GET + context["detection_search_form"] = DetectionRuleSearchForm(request_query) + page_obj = context["page_obj"] + context["elided_page_range"] = page_obj.paginator.get_elided_page_range( + page_obj.number, on_each_side=2, on_ends=1 + ) + return context + + def get_queryset(self): + advisories_prefetch = Prefetch( + "related_advisories", queryset=AdvisoryV2.objects.only("id", "avid") + ) + + queryset = super().get_queryset().prefetch_related(advisories_prefetch) + form = DetectionRuleSearchForm(self.request.GET) + if form.is_valid(): + rule_type = form.cleaned_data.get("rule_type") + advisory_avid = form.cleaned_data.get("advisory_avid") + rule_text = form.cleaned_data.get("rule_text_contains") + + if rule_type: + queryset = queryset.filter(rule_type=rule_type) + + if advisory_avid: + queryset = queryset.filter(related_advisories__avid=advisory_avid) + + if rule_text: + queryset = queryset.filter(rule_text__icontains=rule_text) + return queryset + + class PipelineScheduleListView(VulnerableCodeListView, FormMixin): model = PipelineSchedule context_object_name = "schedule_list" diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index eb1bc006b..89807447b 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -22,6 +22,7 @@ from vulnerabilities.api import VulnerabilityViewSet from vulnerabilities.api_v2 import CodeFixV2ViewSet from vulnerabilities.api_v2 import CodeFixViewSet +from vulnerabilities.api_v2 import DetectionRuleViewSet from vulnerabilities.api_v2 import PackageV2ViewSet from vulnerabilities.api_v2 import PipelineScheduleV2ViewSet from vulnerabilities.api_v2 import VulnerabilityV2ViewSet @@ -34,6 +35,7 @@ from vulnerabilities.views import AdvisoryPackagesDetails from vulnerabilities.views import AffectedByAdvisoriesListView from vulnerabilities.views import ApiUserCreateView +from vulnerabilities.views import DetectionRuleSearch from vulnerabilities.views import FixingAdvisoriesListView from vulnerabilities.views import HomePage from vulnerabilities.views import HomePageV2 @@ -81,6 +83,8 @@ def __init__(self, *args, **kwargs): ) api_v3_router.register("fixing-advisories", FixingAdvisoriesViewSet, basename="fixing-advisories") +api_v3_router.register("detection-rules", DetectionRuleViewSet, basename="detection-rule") + urlpatterns = [ path("admin/login/", AdminLoginView.as_view(), name="admin-login"), path("api/v2/", include(api_v2_router.urls)), @@ -124,6 +128,11 @@ def __init__(self, *args, **kwargs): AdvisoryDetails.as_view(), name="advisory_details", ), + path( + "rules/search/", + DetectionRuleSearch.as_view(), + name="detection_rule_search", + ), path( "packages/search/", PackageSearch.as_view(),