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
45 changes: 45 additions & 0 deletions vulnerabilities/api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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
65 changes: 65 additions & 0 deletions vulnerabilities/migrations/0130_detectionrule.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
],
),
]
40 changes: 40 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
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 colspan="width: 200px;">Metadata</th>
<th colspan="width: 100px;">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
Loading