From 7792d4063edbccd33683792f30861faa671625d1 Mon Sep 17 00:00:00 2001 From: ziad hany Date: Mon, 24 Nov 2025 23:04:21 +0200 Subject: [PATCH 1/2] 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 2c9e385a3..bd01c5d5d 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3890,3 +3890,43 @@ class AdvisoryPOC(models.Model): is_confirmed = models.BooleanField( default=False, help_text="Indicates whether this POC has been verified or confirmed." ) + + +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 3aff06768..478758f14 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -37,6 +37,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 @@ -946,6 +947,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(), From b3ce36d59a1edda1cb96ae01bc305a157b1dabf0 Mon Sep 17 00:00:00 2001 From: ziad hany Date: Fri, 15 May 2026 21:52:26 +0300 Subject: [PATCH 2/2] Update the API so it returns an empty list if no filter Make sure to use api_v3 Fix a css width for rule_metadat and rule text Resolve merge conflict Signed-off-by: ziad hany --- vulnerabilities/api_v2.py | 36 ---------- vulnerabilities/api_v3.py | 45 +++++++++++++ .../migrations/0130_detectionrule.py | 65 +++++++++++++++++++ .../templates/detection_rules.html | 4 +- vulnerablecode/urls.py | 2 +- 5 files changed, 113 insertions(+), 39 deletions(-) create mode 100644 vulnerabilities/migrations/0130_detectionrule.py diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index c9d4d3596..6e0ab9213 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -26,11 +26,8 @@ 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 @@ -852,36 +849,3 @@ 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/api_v3.py b/vulnerabilities/api_v3.py index 12f10ed1c..039a2cb31 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -30,6 +30,7 @@ from vulnerabilities.models import AdvisorySeverity from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import AdvisoryWeakness +from vulnerabilities.models import DetectionRule from vulnerabilities.models import Group from vulnerabilities.models import GroupedAdvisory from vulnerabilities.models import ImpactedPackageAffecting @@ -704,3 +705,47 @@ def get_fixing_advisories_bulk(packages): result[package.id] = grouped return result + + +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 = {advisory.avid for advisory in obj.related_advisories.all()} + return sorted(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 + + def get_queryset(self): + queryset = super().get_queryset() + query_params = ["advisory_avid", "rule_text_contains", "rule_type"] + has_query_params = any( + query_param in self.request.query_params for query_param in query_params + ) + if not has_query_params: + return queryset.none() + + return queryset diff --git a/vulnerabilities/migrations/0130_detectionrule.py b/vulnerabilities/migrations/0130_detectionrule.py new file mode 100644 index 000000000..62a3d6c18 --- /dev/null +++ b/vulnerabilities/migrations/0130_detectionrule.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.11 on 2026-05-15 19:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0129_advisorypoc"), + ] + + operations = [ + migrations.CreateModel( + name="DetectionRule", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "rule_type", + models.CharField( + choices=[ + ("yara", "Yara"), + ("yara-x", "Yara-X"), + ("sigma", "Sigma"), + ("clamav", "ClamAV"), + ("suricata", "Suricata"), + ], + help_text="The type of the detection rule content (e.g., YARA, Sigma).", + max_length=50, + ), + ), + ( + "source_url", + models.URLField( + help_text="URL to the original source or reference for this rule.", + max_length=1024, + ), + ), + ( + "rule_metadata", + models.JSONField( + blank=True, + help_text="Additional structured data such as tags, or author information.", + null=True, + ), + ), + ( + "rule_text", + models.TextField(help_text="The content of the detection signature."), + ), + ( + "related_advisories", + models.ManyToManyField( + help_text="Advisories associated with this DetectionRule.", + related_name="detection_rules", + to="vulnerabilities.advisoryv2", + ), + ), + ], + ), + ] diff --git a/vulnerabilities/templates/detection_rules.html b/vulnerabilities/templates/detection_rules.html index e665a8eff..21dd7b3bf 100644 --- a/vulnerabilities/templates/detection_rules.html +++ b/vulnerabilities/templates/detection_rules.html @@ -30,8 +30,8 @@ Type - Metadata - Text + Metadata + Text Source URL Advisory IDs diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 89807447b..2f2d94927 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -22,12 +22,12 @@ 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 from vulnerabilities.api_v3 import AdvisoryV3ViewSet from vulnerabilities.api_v3 import AffectedByAdvisoriesViewSet +from vulnerabilities.api_v3 import DetectionRuleViewSet from vulnerabilities.api_v3 import FixingAdvisoriesViewSet from vulnerabilities.api_v3 import PackageV3ViewSet from vulnerabilities.views import AdminLoginView