diff --git a/dejacode/settings.py b/dejacode/settings.py index 7c9af8c3..4ccb29df 100644 --- a/dejacode/settings.py +++ b/dejacode/settings.py @@ -345,6 +345,7 @@ def gettext_noop(s): "policy", "notification", "vulnerabilities", + "vulnerabilities.triage", ] EXTRA_APPS = env.list("EXTRA_APPS", default=[]) @@ -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(): diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 4f0b48fb..047587cb 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -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; diff --git a/dje/cron_jobs.py b/dje/cron_jobs.py index 1631a15c..575af4e1 100644 --- a/dje/cron_jobs.py +++ b/dje/cron_jobs.py @@ -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, ) diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index 06134052..e1ebdf03 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -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 _ @@ -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): @@ -366,6 +368,7 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet): dropdown_fields = [ "is_modified", "weighted_risk_score", + "triage_action", "vulnerability_analyses__state", "vulnerability_analyses__justification", "responses", @@ -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, @@ -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)" diff --git a/product_portfolio/forms.py b/product_portfolio/forms.py index 6c7482b3..57cf9eab 100644 --- a/product_portfolio/forms.py +++ b/product_portfolio/forms.py @@ -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: @@ -106,6 +107,7 @@ class ProductForm( ProductComponent, ProductPackage, CodebaseResource, + ProductTriageRuleset, ] keywords = KeywordsField() diff --git a/product_portfolio/models.py b/product_portfolio/models.py index b540f38b..a14b6ae9 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -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) diff --git a/product_portfolio/templates/product_portfolio/modals/manage_triage_rulesets_form.html b/product_portfolio/templates/product_portfolio/modals/manage_triage_rulesets_form.html new file mode 100644 index 00000000..18aee0a7 --- /dev/null +++ b/product_portfolio/templates/product_portfolio/modals/manage_triage_rulesets_form.html @@ -0,0 +1,51 @@ +{% load i18n %} +
+ {% if available_rulesets %} +

+ {% trans "Select the rules to activate for this product. Saving triggers an evaluation of all active packages." %} +

+
+ {% for ruleset in available_rulesets %} + + {% endfor %} +
+ {% else %} +

+ {% trans "No triage rules are available in this dataspace." %} +

+ {% endif %} +
diff --git a/product_portfolio/templates/product_portfolio/modals/manage_triage_rulesets_modal.html b/product_portfolio/templates/product_portfolio/modals/manage_triage_rulesets_modal.html new file mode 100644 index 00000000..e8bc1efb --- /dev/null +++ b/product_portfolio/templates/product_portfolio/modals/manage_triage_rulesets_modal.html @@ -0,0 +1,17 @@ +{% load i18n %} + diff --git a/product_portfolio/templates/product_portfolio/product_details.html b/product_portfolio/templates/product_portfolio/product_details.html index cfe9dbeb..313c32b0 100644 --- a/product_portfolio/templates/product_portfolio/product_details.html +++ b/product_portfolio/templates/product_portfolio/product_details.html @@ -10,7 +10,7 @@ Manage - {% 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 %} {% endif %} @@ -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 %} @@ -281,6 +294,39 @@ {% endif %} + {% if has_change_permission and request.user.dataspace.enable_vulnerablecodedb_access %} + + {% endif %} + {% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %}