Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a7d7929
add vulnerabilities triage module
tdruez Jul 30, 2026
b7e161f
add TriageRuleset model
tdruez Jul 30, 2026
20966b0
add 2 vulnrability triage rules
tdruez Aug 5, 2026
c1e524e
add verbose name
tdruez Aug 5, 2026
a84c095
add TriageRulesetAdmin to manage ruleset
tdruez Aug 5, 2026
7de1361
display enabled rules in change list
tdruez Aug 5, 2026
ac49367
simplify the rules implementation
tdruez Aug 5, 2026
7f4a02d
display rules on add form
tdruez Aug 5, 2026
c89ab04
add evaluate_ruleset engine
tdruez Aug 5, 2026
49b9207
add TriageDecision model
tdruez Aug 6, 2026
34d0076
refine list display
tdruez Aug 6, 2026
345118a
add management command to load ruleset data
tdruez Aug 6, 2026
691b369
add command to trigger triage evaluation
tdruez Aug 6, 2026
2465371
implement TriageDecisionAdmin
tdruez Aug 6, 2026
41121a3
add new triage rules
tdruez Aug 6, 2026
bf9d578
rename function
tdruez Aug 6, 2026
333bc72
refine admin rendering
tdruez Aug 6, 2026
ddd3c51
add parameter in evaluation
tdruez Aug 6, 2026
a4c788d
updat the reference ruleset
tdruez Aug 6, 2026
795c533
display description in admin
tdruez Aug 6, 2026
9833db1
refine default rules
tdruez Aug 6, 2026
0d64a0b
refine rule descriptions
tdruez Aug 6, 2026
113c40c
add parameter in form
tdruez Aug 6, 2026
9545c16
fix form bugs
tdruez Aug 6, 2026
47eafdf
refactor the qactions outside the model
tdruez Aug 6, 2026
82329bc
refactoring toward a ProductPackageTriage model
tdruez Aug 7, 2026
9bf8473
delete all package triage records when a ruleset is disabled
tdruez Aug 7, 2026
52cdc58
add primary_actions qs method
tdruez Aug 7, 2026
286482f
rename model to TriageRecord
tdruez Aug 7, 2026
e5994ad
refine admin ordering
tdruez Aug 7, 2026
293a6b4
add Triage tab in the product details view
tdruez Aug 7, 2026
364f8f8
progress on the triage tab
tdruez Aug 10, 2026
81f506b
refine tab header
tdruez Aug 10, 2026
427d965
refine default ordering
tdruez Aug 10, 2026
0f7ae37
add sort and filtering
tdruez Aug 10, 2026
cafae07
move triage content into the vulnerabilites tab
tdruez Aug 10, 2026
a565d82
remove TriageRecordAdmin
tdruez Aug 11, 2026
72b33d9
add ProductTriageRuleset relation
tdruez Aug 11, 2026
b309fb3
enable rulsets per products
tdruez Aug 11, 2026
162fcec
refine product rule management UI
tdruez Aug 11, 2026
46ddb00
refactor to use the advisory as the base for rules
tdruez Aug 11, 2026
2000f51
fix and improve vuln filtering
tdruez Aug 11, 2026
cdbae75
add a cron job for triage evalution
tdruez Aug 11, 2026
f0e6703
make precedence unique
tdruez Aug 11, 2026
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
2 changes: 2 additions & 0 deletions dejacode/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def gettext_noop(s):
"policy",
"notification",
"vulnerabilities",
"vulnerabilities.triage",
]

EXTRA_APPS = env.list("EXTRA_APPS", default=[])
Expand Down Expand Up @@ -478,6 +479,7 @@ def gettext_noop(s):
hourly = "0 * * * *"
DEJACODE_VULNERABILITIES_CRON = env.str("DEJACODE_VULNERABILITIES_CRON", default=daily_at_3am)
DEJACODE_POLICY_RULES_CRON = env.str("DEJACODE_POLICY_RULES_CRON", default=hourly)
DEJACODE_VULNERABILITY_TRIAGE_CRON = env.str("DEJACODE_VULNERABILITY_TRIAGE_CRON", default=hourly)


def enable_rq_eager_mode():
Expand Down
4 changes: 2 additions & 2 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -433,13 +433,13 @@ table.vulnerabilities-table .column-summary {
width: 300px;
}
#tab_vulnerabilities .column-vulnerability_analyses__state {
min-width: 125px;
min-width: 100px;
}
#tab_vulnerabilities .column-vulnerability_analyses__justification {
min-width: 130px;
}
#tab_vulnerabilities .column-vulnerability_analyses__responses {
width: 185px;
min-width: 120px;
}
#tab_vulnerabilities .column-vulnerability_analyses__is_reachable {
width: 80px;
Expand Down
15 changes: 12 additions & 3 deletions dje/cron_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@

from dje.tasks import update_vulnerabilities
from policy.tasks import evaluate_all_products_rules_task
from vulnerabilities.triage.tasks import evaluate_all_products_vulnerability_triage_task

two_hour = 7200
two_hours = 7200
ten_minutes = 600

cron.register(
func=update_vulnerabilities,
queue_name="default",
cron=settings.DEJACODE_VULNERABILITIES_CRON, # Daily at 3am by default
job_timeout=two_hour,
job_timeout=two_hours,
)

cron.register(
func=evaluate_all_products_rules_task,
queue_name="default",
cron=settings.DEJACODE_POLICY_RULES_CRON, # Hourly by default
job_timeout=two_hour,
job_timeout=ten_minutes,
)

cron.register(
func=evaluate_all_products_vulnerability_triage_task,
queue_name="default",
cron=settings.DEJACODE_VULNERABILITY_TRIAGE_CRON, # Hourly by default
job_timeout=ten_minutes,
)
19 changes: 19 additions & 0 deletions product_portfolio/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django import forms
from django.contrib import admin
from django.db.models import Exists
from django.db.models import F
from django.db.models import OuterRef
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -43,6 +44,7 @@
from vulnerabilities.models import RISK_SCORE_RANGES
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityAnalysisMixin
from vulnerabilities.triage.models import TriageAction


class HasComplianceIssueFilter(django_filters.BooleanFilter):
Expand Down Expand Up @@ -366,6 +368,7 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
dropdown_fields = [
"is_modified",
"weighted_risk_score",
"triage_action",
"vulnerability_analyses__state",
"vulnerability_analyses__justification",
"responses",
Expand Down Expand Up @@ -419,6 +422,12 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
("unknown", _("Reachability not known")),
),
)
triage_action = django_filters.ChoiceFilter(
label=_("Triage action"),
choices=TriageAction.choices,
empty_label=_("All actions"),
method="filter_triage_action",
)
compliance_issues = HasComplianceIssueFilter(
field_name="package__usage_policy__compliance_alert",
distinct=True,
Expand All @@ -438,6 +447,16 @@ class Meta:
"exploitability",
]

@staticmethod
def filter_triage_action(queryset, name, value):
if not value:
return queryset
return queryset.filter(
package__affected_by_vulnerabilities__triage_records__action=value,
package__affected_by_vulnerabilities__triage_records__ruleset__enabled=True,
package__affected_by_vulnerabilities__triage_records__product=F("product"),
).distinct()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["vulnerability_analyses__state"].extra["null_label"] = "(No values)"
Expand Down
2 changes: 2 additions & 0 deletions product_portfolio/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from product_portfolio.models import ScanCodeProject
from product_portfolio.tasks import pull_project_data_from_scancodeio_task
from product_portfolio.tasks import scancodeio_submit_project_task
from vulnerabilities.triage.models import ProductTriageRuleset


class NameVersionValidationFormMixin:
Expand Down Expand Up @@ -106,6 +107,7 @@ class ProductForm(
ProductComponent,
ProductPackage,
CodebaseResource,
ProductTriageRuleset,
]

keywords = KeywordsField()
Expand Down
3 changes: 3 additions & 0 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ def get_export_security_compliance_url(self):
def get_evaluate_policy_rules_url(self):
return self.get_url("evaluate_policy_rules")

def get_manage_triage_rulesets_url(self):
return self.get_url("manage_triage_rulesets")

@property
def cyclonedx_bom_ref(self):
return str(self.uuid)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% load i18n %}
<form id="manage-triage-rulesets-form">
{% if available_rulesets %}
<p class="text-body-secondary small px-3 pt-3 mb-0">
{% trans "Select the rules to activate for this product. Saving triggers an evaluation of all active packages." %}
</p>
<div class="list-group list-group-flush mt-2">
{% for ruleset in available_rulesets %}
<label class="list-group-item d-flex gap-3 py-3" for="ruleset_{{ ruleset.uuid }}">
<input class="form-check-input flex-shrink-0 mt-1"
type="checkbox"
name="ruleset_uuids"
value="{{ ruleset.uuid }}"
id="ruleset_{{ ruleset.uuid }}"
{% if ruleset.id in assigned_ruleset_ids %}checked{% endif %}>
<span class="flex-grow-1">
<span class="d-flex justify-content-between align-items-baseline mb-1">
<strong>{{ ruleset.name }}</strong>
<span class="badge bg-secondary-subtle text-secondary-emphasis ms-2 flex-shrink-0">
{% trans "Precedence:" %} {{ ruleset.precedence }}
</span>
</span>
{% if ruleset.description %}
<span class="d-block small text-body-secondary mb-1">{{ ruleset.description }}</span>
{% endif %}
{% if ruleset.active_rules %}
<span class="d-flex align-items-center flex-wrap gap-1 mb-1">
<span class="small text-body-secondary me-1">{% trans "Conditions:" %}</span>
{% for rule in ruleset.active_rules %}
<span class="badge bg-secondary-subtle text-secondary-emphasis fw-normal">
{{ rule.label }}{% if rule.params_str %} ({{ rule.params_str }}){% endif %}
</span>
{% endfor %}
</span>
{% endif %}
<span class="d-flex align-items-center gap-2 mt-1">
<span class="small text-body-secondary">{% trans "Recommended action:" %}</span>
<span class="badge {{ ruleset.action_badge_class }} text-nowrap">
<i class="fas {{ ruleset.action_icon }} me-1"></i>{{ ruleset.action_label }}
</span>
</span>
</span>
</label>
{% endfor %}
</div>
{% else %}
<p class="text-body-secondary px-3 py-3 mb-0">
{% trans "No triage rules are available in this dataspace." %}
</p>
{% endif %}
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% load i18n %}
<div id="manage-triage-rulesets-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Triage Rules" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="manage-triage-rulesets-body" class="modal-body p-0">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="button" id="submit-triage-rulesets" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<i class="fas fa-tasks"></i> Manage
</button>
</span>
{% elif has_edit_productcomponent or has_edit_productpackage or has_change_codebaseresource_permission or has_add_productcomponent %}
{% elif has_edit_productcomponent or has_edit_productpackage or has_change_codebaseresource_permission or has_add_productcomponent or has_change_permission %}
<div class="dropdown btn-group">
<a class="btn btn-outline-dark dropdown-toggle" data-bs-toggle="dropdown" role="button" href="#"><i class="fas fa-tasks"></i> {% trans 'Manage' %}</a>
<div class="dropdown-menu dropdown-menu-end">
Expand All @@ -32,6 +32,16 @@
{% if purldb_enabled %}
<a class="dropdown-item" href="#" id="check-package-versions"><i class="fas fa-arrow-alt-circle-up"></i> {% trans 'Check for new Package versions' %}</a>
{% endif %}
{% if has_change_permission and request.user.dataspace.enable_vulnerablecodedb_access %}
<hr class="dropdown-divider">
<div class="dropdown-header">{% trans "Vulnerabilities" %}</div>
<button class="dropdown-item" type="button"
data-bs-toggle="modal"
data-bs-target="#manage-triage-rulesets-modal"
data-manage-url="{{ product.get_manage_triage_rulesets_url }}">
<i class="fas fa-filter me-1"></i> {% trans "Triage Rules" %}
</button>
{% endif %}
</div>
</div>
{% endif %}
Expand Down Expand Up @@ -148,6 +158,9 @@
{% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %}
{% include 'product_portfolio/modals/vulnerability_analysis_modal.html' %}
{% endif %}
{% if has_change_permission and request.user.dataspace.enable_vulnerablecodedb_access %}
{% include 'product_portfolio/modals/manage_triage_rulesets_modal.html' %}
{% endif %}
{% endblock %}

{% block messages-alert %}
Expand Down Expand Up @@ -281,6 +294,39 @@
</script>
{% endif %}

{% if has_change_permission and request.user.dataspace.enable_vulnerablecodedb_access %}
<script>
document.addEventListener("DOMContentLoaded", function () {
const triageModal = document.getElementById("manage-triage-rulesets-modal");
if (!triageModal) return;

triageModal.addEventListener("show.bs.modal", function (event) {
const body = document.getElementById("manage-triage-rulesets-body");
body.innerHTML = "";
const manageUrl = event.relatedTarget.dataset.manageUrl;
document.getElementById("submit-triage-rulesets").dataset.manageUrl = manageUrl;
fetch(manageUrl)
.then(r => r.text())
.then(html => { body.innerHTML = html; })
.catch(() => { body.innerHTML = "Error loading content."; });
});

document.getElementById("submit-triage-rulesets").addEventListener("click", function () {
const manageUrl = this.dataset.manageUrl;
const form = document.getElementById("manage-triage-rulesets-form");
fetch(manageUrl, {
method: "POST",
headers: { "X-CSRFToken": csrftoken },
body: new FormData(form),
})
.then(r => r.json())
.then(data => { if (data.success) location.reload(); })
.catch(() => {});
});
});
</script>
{% endif %}

{% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %}
<script>
$(document).ready(function () {
Expand Down
Loading
Loading