Skip to content
29 changes: 29 additions & 0 deletions vulnerabilities/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from vulnerabilities.models import AdvisoryWeakness
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 PackageV2
Expand Down Expand Up @@ -1398,3 +1399,31 @@ def lookup(self, request):

qs = self.get_queryset().for_purls([purl]).with_is_vulnerable()
return Response(PackageV3Serializer(qs, many=True, context={"request": request}).data)


class DetectionRuleFilter(filters.FilterSet):
advisory_avid = filters.CharFilter(field_name="advisory__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.SlugRelatedField(
many=True, read_only=True, slug_field="avid", source="related_advisories"
)

class Meta:
model = DetectionRule
fields = ["rule_type", "source_url", "rule_metadata", "rule_text", "advisory_avid"]


class DetectionRuleViewSet(viewsets.ReadOnlyModelViewSet):
queryset = DetectionRule.objects.prefetch_related("related_advisories")
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
2 changes: 2 additions & 0 deletions vulnerabilities/improvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from vulnerabilities.pipelines.v2_improvers import flag_ghost_packages as flag_ghost_packages_v2
from vulnerabilities.pipelines.v2_improvers import relate_severities
from vulnerabilities.pipelines.v2_improvers import sigma_rules
from vulnerabilities.pipelines.v2_improvers import unfurl_version_range as unfurl_version_range_v2
from vulnerabilities.utils import create_registry

Expand Down Expand Up @@ -74,5 +75,6 @@
compute_advisory_todo.ComputeToDo,
collect_ssvc_trees.CollectSSVCPipeline,
relate_severities.RelateSeveritiesPipeline,
sigma_rules.SigmaRulesImproverPipeline,
]
)
68 changes: 68 additions & 0 deletions vulnerabilities/migrations/0116_detectionrule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Generated by Django 5.2.11 on 2026-03-07 14:44

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0115_impactedpackageaffecting_and_more"),
]

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."),
),
(
"advisory",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="detection_rules",
to="vulnerabilities.advisoryv2",
),
),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.2.11 on 2026-03-25 13:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0116_detectionrule"),
]

operations = [
migrations.RemoveField(
model_name="detectionrule",
name="advisory",
),
migrations.AddField(
model_name="detectionrule",
name="related_advisories",
field=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 @@ -3649,3 +3649,43 @@ def __str__(self):

class Meta:
unique_together = ("vector", "source_advisory")


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.",
)
150 changes: 150 additions & 0 deletions vulnerabilities/pipelines/v2_improvers/sigma_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# VulnerableCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#

import datetime
from pathlib import Path

import yaml
from aboutcode.pipeline import LoopProgress
from fetchcode.vcs import fetch_via_vcs

from vulnerabilities.models import AdvisoryAlias
from vulnerabilities.models import AdvisoryV2
from vulnerabilities.models import DetectionRule
from vulnerabilities.models import DetectionRuleTypes
from vulnerabilities.pipelines import VulnerableCodePipeline
from vulnerabilities.utils import find_all_cve
from vulnerabilities.utils import get_advisory_url


class SigmaRulesImproverPipeline(VulnerableCodePipeline):
pipeline_id = "sigma_rules"

repo_pattern = [
("https://github.com/SigmaHQ/sigma", "**/*.yml"),
("https://github.com/SamuraiMDR/sigma-rules", "**/*.yml"),
("https://github.com/mbabinski/Sigma-Rules", "**/*.yml"),
("https://github.com/P4T12ICK/Sigma-Rule-Repository", "**/*.yml"),
]

license_urls = """
https://github.com/SigmaHQ/Detection-Rule-License
https://github.com/SamuraiMDR/sigma-rules/blob/main/LICENSE
https://github.com/mbabinski/Sigma-Rules/blob/main/LICENSE
https://github.com/P4T12ICK/Sigma-Rule-Repository/blob/master/LICENSE.md
"""

@classmethod
def steps(cls):
return (
cls.clone_repo,
cls.collect_and_store_rules,
cls.clean_downloads,
)

def clone_repo(self):
self.cloned_repos = []
for repo_url, rglob_pattern in self.repo_pattern:
self.log(f"Cloning `{repo_url}`")
vcs_response = fetch_via_vcs(f"git+{repo_url}")
self.cloned_repos.append(
{"repo_url": repo_url, "rglob_pattern": rglob_pattern, "vcs_response": vcs_response}
)

def collect_and_store_rules(self):
"""
Collect Sigma YAML rules from the destination directory and store/update
them as DetectionRule objects.
"""
for cloned in self.cloned_repos:
repo_url = cloned["repo_url"]
rglob_pattern = cloned["rglob_pattern"]
vcs_response = cloned["vcs_response"]
base_directory = Path(vcs_response.dest_dir)
yaml_files = [
p
for p in base_directory.rglob(rglob_pattern)
if p.is_file()
and not any(part in [".github", "images", "documentation"] for part in p.parts)
]

rules_count = len(yaml_files)
self.log(
f"Enhancing the vulnerability with {rules_count:,d} rule records from {repo_url}"
)
progress = LoopProgress(total_iterations=rules_count, logger=self.log)
for file_path in progress.iter(yaml_files):
raw_text = file_path.read_text(encoding="utf-8")
rule_documents = list(yaml.load_all(raw_text, yaml.FullLoader))

rule_metadata = extract_sigma_metadata(rule_documents)
source_url = get_advisory_url(
file=file_path,
base_path=base_directory,
url=f"{repo_url}/blob/master/",
)

cve_ids = find_all_cve(f"{file_path}\n{raw_text}")

advisories = set()
for cve_id in cve_ids:
alias = AdvisoryAlias.objects.filter(alias=cve_id).first()
if alias:
for adv in alias.advisories.all():
advisories.add(adv)
else:
advs = AdvisoryV2.objects.filter(advisory_id=cve_id)
for adv in advs:
advisories.add(adv)

detection_rule, _ = DetectionRule.objects.update_or_create(
source_url=source_url,
rule_type=DetectionRuleTypes.SIGMA,
defaults={
"rule_metadata": rule_metadata,
"rule_text": raw_text,
},
)

for adv in advisories:
detection_rule.related_advisories.add(adv)

def clean_downloads(self):
for cloned in self.cloned_repos:
vcs_response = cloned["vcs_response"]
if vcs_response:
self.log(f"Removing cloned repository: {vcs_response.dest_dir}")
vcs_response.delete()

def on_failure(self):
self.clean_downloads()


def extract_sigma_metadata(rule_documents):
"""
Extract Sigma metadata from Sigma YAML rules
"""
if not rule_documents:
return None

first_document = rule_documents[0]
metadata = {
"status": first_document.get("status"),
"author": first_document.get("author"),
"date": first_document.get("date"),
"title": first_document.get("title"),
"id": first_document.get("id"),
}

rule_date = metadata.get("date")

if isinstance(rule_date, (datetime.date, datetime.datetime)):
metadata["date"] = rule_date.isoformat()

return metadata
Loading