Skip to content

Commit f281b4c

Browse files
committed
Add Support for Detection Rules in UI/API
Resolve migration conflict Add DetectionRule model Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 4ab8b2f commit f281b4c

File tree

9 files changed

+312
-0
lines changed

9 files changed

+312
-0
lines changed

vulnerabilities/api_v2.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@
2626
from rest_framework.reverse import reverse
2727
from rest_framework.throttling import AnonRateThrottle
2828

29+
from vulnerabilities.models import AdvisoryV2
2930
from vulnerabilities.models import CodeFix
3031
from vulnerabilities.models import CodeFixV2
32+
from vulnerabilities.models import DetectionRule
33+
from vulnerabilities.models import ImpactedPackage
3134
from vulnerabilities.models import Package
3235
from vulnerabilities.models import PipelineRun
3336
from vulnerabilities.models import PipelineSchedule
@@ -849,3 +852,36 @@ def get_view_name(self):
849852
if self.detail:
850853
return "Pipeline Instance"
851854
return "Pipeline Jobs"
855+
856+
857+
class DetectionRuleFilter(filters.FilterSet):
858+
advisory_avid = filters.CharFilter(field_name="related_advisories__avid", lookup_expr="exact")
859+
860+
rule_text_contains = filters.CharFilter(field_name="rule_text", lookup_expr="icontains")
861+
862+
class Meta:
863+
model = DetectionRule
864+
fields = ["rule_type"]
865+
866+
867+
class DetectionRuleSerializer(serializers.ModelSerializer):
868+
advisory_avid = serializers.SerializerMethodField()
869+
870+
class Meta:
871+
model = DetectionRule
872+
fields = ["rule_type", "source_url", "rule_metadata", "rule_text", "advisory_avid"]
873+
874+
def get_advisory_avid(self, obj):
875+
avids = set(advisory.avid for advisory in obj.related_advisories.all())
876+
return sorted(list(avids))
877+
878+
879+
class DetectionRuleViewSet(viewsets.ReadOnlyModelViewSet):
880+
advisories_prefetch = Prefetch(
881+
"related_advisories", queryset=AdvisoryV2.objects.only("id", "avid").distinct()
882+
)
883+
queryset = DetectionRule.objects.prefetch_related(advisories_prefetch)
884+
serializer_class = DetectionRuleSerializer
885+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
886+
filter_backends = [filters.DjangoFilterBackend]
887+
filterset_class = DetectionRuleFilter

vulnerabilities/forms.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django_altcha import AltchaField
1414

1515
from vulnerabilities.models import ApiUser
16+
from vulnerabilities.models import DetectionRuleTypes
1617

1718

1819
class PackageSearchForm(forms.Form):
@@ -43,6 +44,35 @@ class AdvisorySearchForm(forms.Form):
4344
)
4445

4546

47+
class DetectionRuleSearchForm(forms.Form):
48+
rule_type = forms.ChoiceField(
49+
required=False,
50+
label="Rule Type",
51+
choices=[("", "All")] + DetectionRuleTypes.choices,
52+
initial="",
53+
)
54+
55+
advisory_avid = forms.CharField(
56+
required=False,
57+
label="Advisory avid",
58+
widget=forms.TextInput(
59+
attrs={
60+
"placeholder": "Search by avid: github_osv_importer_v2/GHSA-7g5f-wrx8-5ccf",
61+
}
62+
),
63+
)
64+
65+
rule_text_contains = forms.CharField(
66+
required=False,
67+
label="Rule Text",
68+
widget=forms.TextInput(
69+
attrs={
70+
"placeholder": "Search in rule text",
71+
}
72+
),
73+
)
74+
75+
4676
class ApiUserCreationForm(forms.ModelForm):
4777
"""Support a simplified creation for API-only users directly from the UI."""
4878

vulnerabilities/models.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3740,3 +3740,43 @@ class GroupedAdvisory(NamedTuple):
37403740
weighted_severity: Optional[float]
37413741
exploitability: Optional[float]
37423742
risk_score: Optional[float]
3743+
3744+
3745+
class DetectionRuleTypes(models.TextChoices):
3746+
"""Defines the supported formats for security detection rules."""
3747+
3748+
YARA = "yara", "Yara"
3749+
YARA_X = "yara-x", "Yara-X"
3750+
SIGMA = "sigma", "Sigma"
3751+
CLAMAV = "clamav", "ClamAV"
3752+
SURICATA = "suricata", "Suricata"
3753+
3754+
3755+
class DetectionRule(models.Model):
3756+
"""
3757+
A Detection Rule is code used to identify malicious activity or security threats.
3758+
"""
3759+
3760+
rule_type = models.CharField(
3761+
max_length=50,
3762+
choices=DetectionRuleTypes.choices,
3763+
help_text="The type of the detection rule content (e.g., YARA, Sigma).",
3764+
)
3765+
3766+
source_url = models.URLField(
3767+
max_length=1024, help_text="URL to the original source or reference for this rule."
3768+
)
3769+
3770+
rule_metadata = models.JSONField(
3771+
null=True,
3772+
blank=True,
3773+
help_text="Additional structured data such as tags, or author information.",
3774+
)
3775+
3776+
rule_text = models.TextField(help_text="The content of the detection signature.")
3777+
3778+
related_advisories = models.ManyToManyField(
3779+
AdvisoryV2,
3780+
related_name="detection_rules",
3781+
help_text="Advisories associated with this DetectionRule.",
3782+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{% extends "base.html" %}
2+
{% load humanize %}
3+
{% load widget_tweaks %}
4+
5+
{% block title %}
6+
Detection Rule Search
7+
{% endblock %}
8+
9+
{% block content %}
10+
<section class="section pt-0">
11+
{% include "detection_rules_box.html" %}
12+
</section>
13+
14+
<div class="is-max-desktop mb-3">
15+
<section class="mx-5">
16+
<div class="is-flex" style="justify-content: space-between;">
17+
<div>
18+
{{ page_obj.paginator.count|intcomma }} results
19+
</div>
20+
{% if is_paginated %}
21+
{% include 'includes/rules_pagination.html' with page_obj=page_obj %}
22+
{% endif %}
23+
</div>
24+
</section>
25+
</div>
26+
27+
<section class="section pt-0">
28+
<div class="content">
29+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
30+
<thead>
31+
<tr>
32+
<th>Type</th>
33+
<th>Metadata</th>
34+
<th>Text</th>
35+
<th>Source URL</th>
36+
<th>Advisory IDs</th>
37+
</tr>
38+
</thead>
39+
<tbody>
40+
{% for detection_rule in page_obj %}
41+
<tr class="is-clipped-list">
42+
<td>{{ detection_rule.rule_type }}</td>
43+
<td>{{ detection_rule.rule_metadata }}</td>
44+
<td>{{ detection_rule.rule_text|truncatewords:10 }}</td>
45+
<td><a href="{{ detection_rule.source_url }}">{{ detection_rule.source_url }}</a></td>
46+
<td>
47+
{% for advisory in detection_rule.related_advisories.all %}
48+
{% ifchanged advisory.avid %}
49+
<a href="{% url "advisory_details" advisory.avid %}">{{ advisory.avid }}</a>
50+
<br/>
51+
{% endifchanged %}
52+
{% endfor %}
53+
</td>
54+
</tr>
55+
{% empty %}
56+
<tr class="is-clipped-list">
57+
<td colspan="5" style="word-break: break-all">
58+
No detection rules found.
59+
</td>
60+
</tr>
61+
{% endfor %}
62+
</tbody>
63+
</table>
64+
</div>
65+
66+
67+
{% if is_paginated %}
68+
{% include 'includes/rules_pagination.html' with page_obj=page_obj %}
69+
{% endif %}
70+
</section>
71+
72+
{% endblock %}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{% load widget_tweaks %}
2+
<article class='panel is-info'>
3+
<div class='panel-heading py-2 is-size-6'>
4+
Search for Rules
5+
<div class="dropdown is-hoverable has-text-weight-normal">
6+
<div class="dropdown-trigger">
7+
<i class="fa fa-question-circle ml-2"></i>
8+
</div>
9+
<div class="dropdown-menu dropdown-instructions-width" id="dropdown-menu4" role="menu">
10+
<div class="dropdown-content dropdown-instructions-box-shadow">
11+
<div class="dropdown-item">
12+
Search for Rules by <strong>Rule Type such as: Sigma, Yara, Clamav signatures, Suricata</strong>
13+
</div>
14+
</div>
15+
</div>
16+
</div>
17+
</div>
18+
<div class="panel-block">
19+
<div class="pb-3 width-100-pct">
20+
<form
21+
action="{% url 'detection_rule_search' %}"
22+
method="get"
23+
name="detection_search_form"
24+
>
25+
<div class="field has-addons mt-3 width-100-pct">
26+
<div class="control">
27+
<div class="select">
28+
{% render_field detection_search_form.rule_type %}
29+
</div>
30+
</div>
31+
<div class="control is-expanded">
32+
{% render_field detection_search_form.advisory_avid class="input" %}
33+
</div>
34+
<div class="control is-expanded">
35+
{% render_field detection_search_form.rule_text_contains class="input" %}
36+
</div>
37+
<div class="control">
38+
<button class="is-link button" type="submit" id="submit_rule">
39+
Search
40+
</button>
41+
</div>
42+
</div>
43+
</form>
44+
</div>
45+
</div>
46+
</article>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<nav class="pagination is-centered is-small" aria-label="pagination">
2+
{% if page_obj.has_previous %}
3+
<a href="{% querystring page=page_obj.previous_page_number %}" class="pagination-previous">Previous</a>
4+
{% else %}
5+
<a class="pagination-previous">Previous</a>
6+
{% endif %}
7+
8+
{% if page_obj.has_next %}
9+
<a href="{% querystring page=page_obj.next_page_number %}" class="pagination-next">Next</a>
10+
{% else %}
11+
<a class="pagination-next">Next</a>
12+
{% endif %}
13+
14+
<ul class="pagination-list">
15+
{% for page_num in elided_page_range %}
16+
{% if page_num == page_obj.paginator.ELLIPSIS %}
17+
<li>
18+
<span class="pagination-ellipsis">&hellip;</span>
19+
</li>
20+
{% elif page_num == page_obj.number %}
21+
<li>
22+
<a class="pagination-link is-current"
23+
aria-label="Page {{ page_num }}"
24+
aria-current="page">{{ page_num }}
25+
</a>
26+
</li>
27+
{% else %}
28+
<li>
29+
<a href="{% querystring page=page_num %}"
30+
class="pagination-link"
31+
aria-label="Goto page {{ page_num }}">{{ page_num }}</a>
32+
</li>
33+
{% endif %}
34+
{% endfor %}
35+
</ul>
36+
37+
</nav>

vulnerabilities/templates/navbar.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
<a class="navbar-item {% active_item 'package_search_v2' %}" href="{% url 'package_search_v2' %}">
3030
V2
3131
</a>
32+
<a class="navbar-item {% active_item 'detection_rule_search' %}" href="{% url 'detection_rule_search' %}">
33+
Detection Rules
34+
</a>
3235
<a class="navbar-item" href="https://vulnerablecode.readthedocs.io/en/latest/" target="_blank">
3336
Documentation
3437
</a>

vulnerabilities/views.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from vulnerabilities.forms import AdminLoginForm
3737
from vulnerabilities.forms import AdvisorySearchForm
3838
from vulnerabilities.forms import ApiUserCreationForm
39+
from vulnerabilities.forms import DetectionRuleSearchForm
3940
from vulnerabilities.forms import PackageSearchForm
4041
from vulnerabilities.forms import PipelineSchedulePackageForm
4142
from vulnerabilities.forms import VulnerabilitySearchForm
@@ -944,6 +945,44 @@ def get_queryset(self):
944945
)
945946

946947

948+
class DetectionRuleSearch(ListView):
949+
model = models.DetectionRule
950+
template_name = "detection_rules.html"
951+
paginate_by = PAGE_SIZE
952+
953+
def get_context_data(self, **kwargs):
954+
context = super().get_context_data(**kwargs)
955+
request_query = self.request.GET
956+
context["detection_search_form"] = DetectionRuleSearchForm(request_query)
957+
page_obj = context["page_obj"]
958+
context["elided_page_range"] = page_obj.paginator.get_elided_page_range(
959+
page_obj.number, on_each_side=2, on_ends=1
960+
)
961+
return context
962+
963+
def get_queryset(self):
964+
advisories_prefetch = Prefetch(
965+
"related_advisories", queryset=AdvisoryV2.objects.only("id", "avid")
966+
)
967+
968+
queryset = super().get_queryset().prefetch_related(advisories_prefetch)
969+
form = DetectionRuleSearchForm(self.request.GET)
970+
if form.is_valid():
971+
rule_type = form.cleaned_data.get("rule_type")
972+
advisory_avid = form.cleaned_data.get("advisory_avid")
973+
rule_text = form.cleaned_data.get("rule_text_contains")
974+
975+
if rule_type:
976+
queryset = queryset.filter(rule_type=rule_type)
977+
978+
if advisory_avid:
979+
queryset = queryset.filter(related_advisories__avid=advisory_avid)
980+
981+
if rule_text:
982+
queryset = queryset.filter(rule_text__icontains=rule_text)
983+
return queryset
984+
985+
947986
class PipelineScheduleListView(VulnerableCodeListView, FormMixin):
948987
model = PipelineSchedule
949988
context_object_name = "schedule_list"

vulnerablecode/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from vulnerabilities.api import VulnerabilityViewSet
2323
from vulnerabilities.api_v2 import CodeFixV2ViewSet
2424
from vulnerabilities.api_v2 import CodeFixViewSet
25+
from vulnerabilities.api_v2 import DetectionRuleViewSet
2526
from vulnerabilities.api_v2 import PackageV2ViewSet
2627
from vulnerabilities.api_v2 import PipelineScheduleV2ViewSet
2728
from vulnerabilities.api_v2 import VulnerabilityV2ViewSet
@@ -34,6 +35,7 @@
3435
from vulnerabilities.views import AdvisoryPackagesDetails
3536
from vulnerabilities.views import AffectedByAdvisoriesListView
3637
from vulnerabilities.views import ApiUserCreateView
38+
from vulnerabilities.views import DetectionRuleSearch
3739
from vulnerabilities.views import FixingAdvisoriesListView
3840
from vulnerabilities.views import HomePage
3941
from vulnerabilities.views import HomePageV2
@@ -81,6 +83,8 @@ def __init__(self, *args, **kwargs):
8183
)
8284
api_v3_router.register("fixing-advisories", FixingAdvisoriesViewSet, basename="fixing-advisories")
8385

86+
api_v3_router.register("detection-rules", DetectionRuleViewSet, basename="detection-rule")
87+
8488
urlpatterns = [
8589
path("admin/login/", AdminLoginView.as_view(), name="admin-login"),
8690
path("api/v2/", include(api_v2_router.urls)),
@@ -124,6 +128,11 @@ def __init__(self, *args, **kwargs):
124128
AdvisoryDetails.as_view(),
125129
name="advisory_details",
126130
),
131+
path(
132+
"rules/search/",
133+
DetectionRuleSearch.as_view(),
134+
name="detection_rule_search",
135+
),
127136
path(
128137
"packages/search/",
129138
PackageSearch.as_view(),

0 commit comments

Comments
 (0)