Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions vulnerabilities/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
30 changes: 30 additions & 0 deletions vulnerabilities/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django_altcha import AltchaField

from vulnerabilities.models import ApiUser
from vulnerabilities.models import DetectionRuleTypes


class PackageSearchForm(forms.Form):
Expand Down Expand Up @@ -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."""

Expand Down
40 changes: 40 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
72 changes: 72 additions & 0 deletions vulnerabilities/templates/detection_rules.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{% extends "base.html" %}
{% load humanize %}
{% load widget_tweaks %}

{% block title %}
Detection Rule Search
{% endblock %}

{% block content %}
<section class="section pt-0">
{% include "detection_rules_box.html" %}
</section>

<div class="is-max-desktop mb-3">
<section class="mx-5">
<div class="is-flex" style="justify-content: space-between;">
<div>
{{ page_obj.paginator.count|intcomma }} results
</div>
{% if is_paginated %}
{% include 'includes/rules_pagination.html' with page_obj=page_obj %}
{% endif %}
</div>
</section>
</div>

<section class="section pt-0">
<div class="content">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<thead>
<tr>
<th>Type</th>
<th>Metadata</th>
<th>Text</th>
<th>Source URL</th>
<th>Advisory IDs</th>
</tr>
</thead>
<tbody>
{% for detection_rule in page_obj %}
<tr class="is-clipped-list">
<td>{{ detection_rule.rule_type }}</td>
<td>{{ detection_rule.rule_metadata }}</td>
<td>{{ detection_rule.rule_text|truncatewords:10 }}</td>
<td><a href="{{ detection_rule.source_url }}">{{ detection_rule.source_url }}</a></td>
<td>
{% for advisory in detection_rule.related_advisories.all %}
{% ifchanged advisory.avid %}
<a href="{% url "advisory_details" advisory.avid %}">{{ advisory.avid }}</a>
<br/>
{% endifchanged %}
{% endfor %}
</td>
</tr>
{% empty %}
<tr class="is-clipped-list">
<td colspan="5" style="word-break: break-all">
No detection rules found.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>


{% if is_paginated %}
{% include 'includes/rules_pagination.html' with page_obj=page_obj %}
{% endif %}
</section>

{% endblock %}
46 changes: 46 additions & 0 deletions vulnerabilities/templates/detection_rules_box.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% load widget_tweaks %}
<article class='panel is-info'>
<div class='panel-heading py-2 is-size-6'>
Search for Rules
<div class="dropdown is-hoverable has-text-weight-normal">
<div class="dropdown-trigger">
<i class="fa fa-question-circle ml-2"></i>
</div>
<div class="dropdown-menu dropdown-instructions-width" id="dropdown-menu4" role="menu">
<div class="dropdown-content dropdown-instructions-box-shadow">
<div class="dropdown-item">
Search for Rules by <strong>Rule Type such as: Sigma, Yara, Clamav signatures, Suricata</strong>
</div>
</div>
</div>
</div>
</div>
<div class="panel-block">
<div class="pb-3 width-100-pct">
<form
action="{% url 'detection_rule_search' %}"
method="get"
name="detection_search_form"
>
<div class="field has-addons mt-3 width-100-pct">
<div class="control">
<div class="select">
{% render_field detection_search_form.rule_type %}
</div>
</div>
<div class="control is-expanded">
{% render_field detection_search_form.advisory_avid class="input" %}
</div>
<div class="control is-expanded">
{% render_field detection_search_form.rule_text_contains class="input" %}
</div>
<div class="control">
<button class="is-link button" type="submit" id="submit_rule">
Search
</button>
</div>
</div>
</form>
</div>
</div>
</article>
37 changes: 37 additions & 0 deletions vulnerabilities/templates/includes/rules_pagination.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<nav class="pagination is-centered is-small" aria-label="pagination">
{% if page_obj.has_previous %}
<a href="{% querystring page=page_obj.previous_page_number %}" class="pagination-previous">Previous</a>
{% else %}
<a class="pagination-previous">Previous</a>
{% endif %}

{% if page_obj.has_next %}
<a href="{% querystring page=page_obj.next_page_number %}" class="pagination-next">Next</a>
{% else %}
<a class="pagination-next">Next</a>
{% endif %}

<ul class="pagination-list">
{% for page_num in elided_page_range %}
{% if page_num == page_obj.paginator.ELLIPSIS %}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{% elif page_num == page_obj.number %}
<li>
<a class="pagination-link is-current"
aria-label="Page {{ page_num }}"
aria-current="page">{{ page_num }}
</a>
</li>
{% else %}
<li>
<a href="{% querystring page=page_num %}"
class="pagination-link"
aria-label="Goto page {{ page_num }}">{{ page_num }}</a>
</li>
{% endif %}
{% endfor %}
</ul>

</nav>
3 changes: 3 additions & 0 deletions vulnerabilities/templates/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<a class="navbar-item {% active_item 'package_search_v2' %}" href="{% url 'package_search_v2' %}">
V2
</a>
<a class="navbar-item {% active_item 'detection_rule_search' %}" href="{% url 'detection_rule_search' %}">
Detection Rules
</a>
<a class="navbar-item" href="https://vulnerablecode.readthedocs.io/en/latest/" target="_blank">
Documentation
</a>
Expand Down
39 changes: 39 additions & 0 deletions vulnerabilities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions vulnerablecode/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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(),
Expand Down
Loading