8000 Add exposure_factor field to the ProductItemPurpose model #102 by tdruez · Pull Request #218 · aboutcode-org/dejacode · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add exposure_factor field to the ProductItemPurpose model #102 #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8b8be70
Add exposure_factor field to the ProductItemPurpose model #102
tdruez Dec 19, 2024
9ed3f8e
Merge branch 'main' into 102-exposure-factor
tdruez Dec 20, 2024
ad8a4fe
Add `weighted_risk_score` field and logic on ProductRelationship #102
tdruez Dec 20, 2024
ecaa8cb
Set distinct=True on Count to get proper value #102
tdruez Dec 20, 2024
bf779ad
Set distinct=True on Count to get proper value #102
tdruez Dec 20, 2024
65640ee
Re work the Vulnerabilities tab as package primary column #102
tdruez Dec 20, 2024
17c2e81
Set weighted_risk_score from various event #102
tdruez Dec 20, 2024
c61de35
Add purpose, exposure_factor, and is_deployed to the tab #102
tdruez Dec 23, 2024
407b166
Display the vulnerable_package_count in Tab header #102
tdruez Dec 23, 2024
775cad9
Do not disable Tab when risk threshold is enabled #102
tdruez Dec 23, 2024
14016f7
Display vulnerability icon in Product list #102
tdruez Dec 23, 2024
693863b
Improve query performances of ProductTabVulnerabilitiesView #102
tdruez Dec 23, 2024
91dc7b0
Implement a update_weighted_risk_score for performances #102
tdruez Dec 23, 2024
ac48923
Fix the update_weighted_risk_score using Subquery #102
tdruez Dec 23, 2024
1e415d4
Refine the compute_weighted_risk_score method #102
tdruez Dec 23, 2024
0df06da
Refine model, migration, and fix unit tests #102
tdruez Dec 24, 2024
e26fdfe
Fix unit tests #102
tdruez Dec 24, 2024
c333d91
Fix unit tests #102
tdruez Dec 24, 2024
36d714e
Update raw SQL for ProductInventoryItem #102
tdruez Dec 24, 2024
17ba3f3
Fix unit tests #102
tdruez Dec 24, 2024
08f801c
Update help text and add docstring #102
tdruez Dec 24, 2024
fda4c5a
Add CHANGELOG content #102
tdruez Dec 24, 2024
9ecd9a0
Update documentation screenshots to new UI rendering #102
tdruez Dec 24, 2024
145386c
Rework the vulnerability_analysis_form_view #102
tdruez Dec 27, 2024
551954f
Fix the rendering of vulnerability analysis values in the tab #102
tdruez Dec 27, 2024
4532bc2
Add unit tests and a new raw_update convenience method #102
tdruez Dec 27, 2024
59d38c4
Add unit tests #102
tdruez Dec 27, 2024
5846561
Add unit tests #102
tdruez Dec 27, 2024
a1c6e8e
Refine fetch_for_packages and related unit tests #102
tdruez Dec 27, 2024
fa5ab40
Fix failing tests #102
tdruez Dec 27, 2024
da1d1d6
Update risk scores in create_vulnerabilities #102
tdruez Dec 27, 2024
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
13 changes: 13 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ Release notes
menu.
https://github.com/aboutcode-org/dejacode/issues/107

- Add exposure_factor field to the ProductItemPurpose model and a weighted_risk_score
on the ProductPackage model.
The weighted_risk_score is computed from the package.risk_score and
purpose.exposure_factor values.
https://github.com/aboutcode-org/dejacode/issues/102

- Add the vulnerability icon in Product list view.
A "Is Vulnerable" filter is also available.
The count in the Vulnerability tab was improve to include the count of affected
packages and the count of unique vulnerabilities.
Note that those count reflect the current risk threshold.
https://github.com/aboutcode-org/dejacode/issues/102

### Version 5.2.1

- Fix the models documentation navigation.
Expand Down
9 changes: 9 additions & 0 deletions component_catalog/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class IsVulnerableBooleanFilter(IsVulnerableFilter):
def filter(self, qs, value):
if value == "yes":
return qs.filter(**{f"{self.field_name}": True})
elif value == "no":
return qs.filter(**{f"{self.field_name}": False})
return qs


class ComponentFilterSet(DataspacedFilterSet):
related_only = [
"licenses",
Expand Down
11 changes: 6 additions & 5 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ table.vulnerabilities-table .column-summary {

/* -- Vulnerability tab -- */
#tab_vulnerabilities .column-vulnerability_id {
width: 210px;
width: 230px;
}
#tab_vulnerabilities .column-affected_packages {
min-width: 250px;
Expand All @@ -396,23 +396,24 @@ table.vulnerabilities-table .column-summary {
#tab_vulnerabilities .column-weighted_severity {
width: 105px;
}
#tab_vulnerabilities .column-risk_score {
#tab_vulnerabilities .column-risk_score,
#tab_vulnerabilities .column-weighted_risk_score{
width: 80px;
}
#tab_vulnerabilities .column-summary {
width: 300px;
}
#tab_vulnerabilities .column-vulnerability_analyses__state {
min-width: 105px;
min-width: 125px;
}
#tab_vulnerabilities .column-vulnerability_analyses__justification {
min-width: 130px;
}
#tab_vulnerabilities .column-vulnerability_analyses__responses {
min-width: 120px;
width: 185px;
}
#tab_vulnerabilities .column-vulnerability_analyses__is_reachable {
min-width: 80px;
width: 80px;
}
/* -- Vulnerability analysis modal -- */
#vulnerability-analysis-modal #div_id_responses .form-check {
Expand Down
29 changes: 26 additions & 3 deletions dje/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,15 +863,38 @@ def update_from_data(self, user, data, override=False, override_unknown=False):

def update(self, **kwargs):
"""
Update this instance with the provided ``kwargs`` values.
The full ``save()`` process will be triggered, including signals, and the
``update_fields`` is automatically set.
Update this instance with the provided field values.

This method modifies the specified fields on the current instance and triggers
the full ``save()`` lifecycle, including calling signals like ``pre_save`` and
``post_save``.
The ``update_fields`` parameter is automatically set to limit the save
operation to the updated fields.
"""
for field_name, value in kwargs.items():
setattr(self, field_name, value)

self.save(update_fields=list(kwargs.keys()))

def raw_update(self, **kwargs):
"""
Perform a direct SQL UPDATE on this instance.

This method updates the specified fields in the database without triggering
the ``save()`` lifecycle or related signals. It bypasses field validation and
other ORM hooks for improved performance, but requires careful usage to avoid
inconsistent states.

The instance's in-memory attributes are updated to reflect the changes.
"""
updated_rows = self.__class__.objects.filter(pk=self.pk).update(**kwargs)

# Update the instance's attributes in memory
for field_name, value in kwargs.items():
setattr(self, field_name, value)

return updated_rows

def as_json(self):
try:
serialized_data = serialize(
Expand Down
2 changes: 1 addition & 1 deletion dje/templates/tabs/pagination.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<ul class="nav nav-pills">
<li class="nav-item">
<form id="tab-{{ tab_id }}-search-form" class="mt-md-0 me-sm-2">
<input style="width: 250px;" type="text" class="form-control form-control-sm" id="tab-{{ tab_id }}-search-input" name="{{ tab_id }}-q" placeholder="Search {{ tab_id }}" aria-label="Search" autocomplete="off" value="{{ search_query|escape }}">
<input style="width: 250px;" type="text" class="form-control form-control-sm" id="tab-{{ tab_id }}-search-input" name="{{ tab_id }}-q" placeholder="Search {% if search_verbose_name %}{{ search_verbose_name }}{% else %}{{ tab_id }}{% endif %}" aria-label="Search" autocomplete="off" value="{{ search_query|escape }}">
</form>
</li>
<li class="nav-item">
Expand Down
5 changes: 4 additions & 1 deletion dje/tests/testfiles/test_dataset_pp_only.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@
"icon": "",
"color_code": "",
"label": "Core",
"text": "The functional code used to execute the application features of the product."
"text": "The functional code used to execute the application features of the product.",
"exposure_factor": null
}
},
{
Expand Down Expand Up @@ -157,6 +158,7 @@
],
"feature": "",
"issue_ref": "",
"weighted_risk_score": null,
"component": null,
"license_expression": "apache-2.0",
"name": "c1",
Expand Down Expand Up @@ -197,6 +199,7 @@
],
"feature": "",
"issue_ref": "",
"weighted_risk_score": null,
"package": [
"nexB",
"b91a9a06-b709-45a4-ac8e-a57bde0c8f38"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions product_portfolio/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class ProductItemPurposeAdmin(ColoredIconAdminMixin, BaseStatusAdmin):
AsColored("color_code"),
"colored_icon",
"default_on_addition",
"exposure_factor",
"get_dataspace",
)
fieldsets = (
Expand All @@ -155,6 +156,7 @@ class ProductItemPurposeAdmin(ColoredIconAdminMixin, BaseStatusAdmin):
"icon",
"color_code",
"default_on_addition",
"exposure_factor",
"dataspace",
"uuid",
)
Expand Down
50 changes: 42 additions & 8 deletions product_portfolio/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import django_filters
from packageurl.contrib.django.utils import purl_to_lookups

from component_catalog.filters import IsVulnerableBooleanFilter
from component_catalog.filters import IsVulnerableFilter
from component_catalog.models import ComponentKeyword
from component_catalog.programming_languages import PROGRAMMING_LANGUAGES
Expand All @@ -36,6 +37,7 @@
from vulnerabilities.filters import RISK_SCORE_RANGES
from vulnerabilities.filters import ScoreRangeFilter
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityAnalysisMixin


class ProductFilterSet(DataspacedFilterSet):
Expand Down Expand Up @@ -107,8 +109,8 @@ class ProductFilterSet(DataspacedFilterSet):
search_placeholder="Search keywords",
),
)
is_vulnerable = IsVulnerableFilter(
field_name="packages__affected_by_vulnerabilities",
is_vulnerable = IsVulnerableBooleanFilter(
label=_("Is Vulnerable"),
widget=DropDownRightWidget(link_content='<i class="fas fa-bug"></i>'),
)
affected_by = django_filters.CharFilter(
Expand All @@ -129,6 +131,10 @@ class Meta:

class BaseProductRelationFilterSet(DataspacedFilterSet):
field_name_prefix = None
dropdown_fields = [
"is_modified",
"weighted_risk_score",
]
is_deployed = BooleanChoiceFilter(
empty_label="All (Inventory)",
choices=(
Expand Down Expand Up @@ -161,7 +167,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
label=_("Severity"),
score_ranges=RISK_SCORE_RANGES,
)
risk_score = ScoreRangeFilter(
weighted_risk_score = ScoreRangeFilter(
label=_("Risk score"),
score_ranges=RISK_SCORE_RANGES,
)
Expand Down Expand Up @@ -192,12 +198,8 @@ def __init__(self, *args, **kwargs):
self.filters["purpose"].extra["to_field_name"] = "label"
self.filters["purpose"].extra["widget"] = DropDownWidget(anchor=self.anchor)

self.filters["is_modified"].extra["widget"] = DropDownWidget(
anchor=self.anchor, right_align=True
)

field_name_prefix = self.field_name_prefix
for field_name in ["exploitability", "weighted_severity", "risk_score"]:
for field_name in ["exploitability", "weighted_severity"]:
field = self.filters[field_name]
field.extra["widget"] = DropDownWidget(anchor=self.anchor)
field.field_name = f"{field_name_prefix}__{field_name}"
Expand Down Expand Up @@ -249,6 +251,14 @@ class Meta:

class ProductPackageFilterSet(BaseProductRelationFilterSet):
field_name_prefix = "package"
dropdown_fields = [
"is_modified",
"weighted_risk_score",
"vulnerability_analyses__state",
"vulnerability_analyses__justification",
"responses",
"is_reachable",
]
q = SearchFilter(
label=_("Search"),
search_fields=[
Expand All @@ -274,6 +284,7 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
"feature",
"is_deployed",
"is_modified",
"weighted_risk_score",
],
)
is_vulnerable = IsVulnerableFilter(
Expand All @@ -282,6 +293,20 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
anchor="#inventory", right_align=True, link_content='<i class="fas fa-bug"></i>'
),
)
responses = django_filters.ChoiceFilter(
field_name="vulnerability_analyses__responses",
lookup_expr="icontains",
choices=VulnerabilityAnalysisMixin.Response.choices,
)
is_reachable = BooleanChoiceFilter(
field_name="vulnerability_analyses__is_reachable",
empty_label="All",
choices=(
("yes", _("Reachable")),
("no", _("Not reachable")),
("unknown", _("Reachability not known")),
),
)

class Meta:
model = ProductPackage
Expand All @@ -291,8 +316,17 @@ class Meta:
"object_type",
"is_deployed",
"is_modified",
"vulnerability_analyses__state",
"vulnerability_analyses__justification",
"is_reachable",
"exploitability",
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["vulnerability_analyses__state"].extra["null_label"] = "(No values)"
self.filters["vulnerability_analyses__justification"].extra["null_label"] = "(No values)"


class ComponentCompletenessListFilter(admin.SimpleListFilter):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Generated by Django 5.0.9 on 2024-12-24 14:37

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('product_portfolio', '0009_product_vulnerabilities_risk_threshold'),
]

operations = [
migrations.AddField(
model_name='productcomponent',
name='weighted_risk_score',
field=models.DecimalField(blank=True, decimal_places=1, help_text="Risk score (0.0 to 10.0), where higher values indicate greater vulnerability. Calculated as the weighted severity times exploitability (capped at 10), adjusted by the exposure risk factor of the product item's purpose.", max_digits=3, null=True),
),
migrations.AddField(
model_name='productitempurpose',
name='exposure_factor',
field=models.DecimalField(blank=True, decimal_places=1, help_text='A number between 0.0 and 1.0 that identifies the vulnerability exposure risk of a package as it is actually used in the context of a product, with 1.0 being the highest exposure risk and 0.0 being no exposure risk at all.', max_digits=2, null=True, validators=[django.core.validators.MaxValueValidator(1.0), django.core.validators.MinValueValidator(0.0)]),
),
migrations.AddField(
model_name='productpackage',
name='weighted_risk_score',
field=models.DecimalField(blank=True, decimal_places=1, help_text="Risk score (0.0 to 10.0), where higher values indicate greater vulnerability. Calculated as the weighted severity times exploitability (capped at 10), adjusted by the exposure risk factor of the product item's purpose.", max_digits=3, null=True),
),
migrations.RunSQL(
"""
DROP VIEW IF EXISTS product_portfolio_productinventoryitem;
CREATE VIEW product_portfolio_productinventoryitem
AS
SELECT
pc.uuid,
pc.component_id,
NULL as package_id,
CONCAT(component.name, ' ', component.version) as item,
'component' as item_type,
pc.dataspace_id,
pc.product_id,
pc.review_status_id,
pc.feature,
component.usage_policy_id,
pc.created_date,
pc.last_modified_date,
pc.reference_notes,
pc.purpose_id,
pc.notes,
pc.is_deployed,
pc.is_modified,
pc.extra_attribution_text,
pc.package_paths,
pc.issue_ref,
pc.weighted_risk_score,
pc.license_expression,
pc.created_by_id,
pc.last_modified_by_id
FROM product_portfolio_productcomponent AS pc
INNER JOIN component_catalog_component AS component ON pc.component_id=component.id
UNION ALL
SELECT
pp.uuid,
NULL as component_id,
pp.package_id,
package.filename as item,
'package' as item_type,
pp.dataspace_id,
pp.product_id,
pp.review_status_id,
pp.feature,
package.usage_policy_id,
pp.created_date,
pp.last_modified_date,
pp.reference_notes,
pp.purpose_id,
pp.notes,
pp.is_deployed,
pp.is_modified,
pp.extra_attribution_text,
pp.package_paths,
pp.issue_ref,
pp.weighted_risk_score,
pp.license_expression,
pp.created_by_id,
pp.last_modified_by_id
FROM product_portfolio_productpackage AS pp
INNER JOIN component_catalog_package AS package ON pp.package_id=package.id
;
""",
reverse_sql="DROP VIEW IF EXISTS product_portfolio_productinventoryitem;"
),
]
Loading
0