Skip to content

Commit 0b2311d

Browse files
authored
Update ProductPackage license unknown during Scan all Packages (#420)
Signed-off-by: tdruez <[email protected]>
1 parent 164a092 commit 0b2311d

File tree

9 files changed

+156
-29
lines changed

9 files changed

+156
-29
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Release notes
22
=============
33

4+
### Version 5.5.0-dev
5+
6+
- Update ProductPackage "unknown" license during "Scan all Packages".
7+
Only "unknown" licenses are updated.
8+
Products with a is_locked configuration status are excluded.
9+
Inactive is_active=False products are excluded.
10+
https://github.com/aboutcode-org/dejacode/issues/388
11+
412
### Version 5.4.2
513

614
- Migrate the LDAP testing from using mockldap to slapdtest.

component_catalog/models.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,19 +2630,28 @@ def update_from_purldb(self, user):
26302630
)
26312631
return updated_fields
26322632

2633-
def update_from_scan(self, user):
2634-
scancodeio = ScanCodeIO(self.dataspace)
2633+
def update_from_scan(self, user, update_products=False):
2634+
package = self
2635+
dataspace = self.dataspace
2636+
scancodeio = ScanCodeIO(dataspace)
2637+
26352638
can_update_from_scan = all(
26362639
[
2637-
self.dataspace.enable_package_scanning,
2638-
self.dataspace.update_packages_from_scan,
2640+
dataspace.enable_package_scanning,
2641+
dataspace.update_packages_from_scan,
26392642
scancodeio.is_configured(),
26402643
]
26412644
)
2645+
if not can_update_from_scan:
2646+
return
2647+
2648+
updated_fields = scancodeio.update_from_scan(package=package, user=user)
26422649

2643-
if can_update_from_scan:
2644-
updated_fields = scancodeio.update_from_scan(package=self, user=user)
2645-
return updated_fields
2650+
if update_products:
2651+
if "declared_license_expression" in updated_fields:
2652+
package.productpackages.update_license_unknown()
2653+
2654+
return updated_fields
26462655

26472656
def get_related_packages_qs(self):
26482657
"""

component_catalog/tests/test_models.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,22 +1769,33 @@ def test_package_model_create_from_url_enable_purldb_access(
17691769

17701770
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_configured")
17711771
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.update_from_scan")
1772-
def test_package_model_update_from_scan(self, mock_update_from_scan, mock_is_configured):
1772+
def test_package_model_update_from_scan(self, mock_scio_update_from_scan, mock_is_configured):
17731773
mock_is_configured.return_value = True
1774-
package1 = make_package(self.dataspace)
1774+
package1 = make_package(self.dataspace, declared_license_expression="mit")
1775+
product1 = make_product(self.dataspace, inventory=[package1])
1776+
1777+
pp1 = product1.productpackages.get()
1778+
self.assertEqual("", pp1.license_expression)
1779+
pp1.update(license_expression="unknown")
17751780

17761781
results = package1.update_from_scan(user=self.user)
1777-
mock_update_from_scan.assert_not_called()
1782+
mock_scio_update_from_scan.assert_not_called()
17781783
self.assertIsNone(results)
17791784

17801785
self.dataspace.enable_package_scanning = True
17811786
self.dataspace.update_packages_from_scan = True
17821787
self.dataspace.save()
17831788

1784-
mock_update_from_scan.return_value = ["updated_field"]
1785-
results = package1.update_from_scan(user=self.user)
1786-
mock_update_from_scan.assert_called()
1787-
self.assertEqual(["updated_field"], results)
1789+
mock_scio_update_from_scan.return_value = ["declared_license_expression"]
1790+
results = package1.update_from_scan(user=self.user, update_products=False)
1791+
mock_scio_update_from_scan.assert_called()
1792+
self.assertEqual(["declared_license_expression"], results)
1793+
pp1.refresh_from_db()
1794+
self.assertEqual("unknown", pp1.license_expression)
1795+
1796+
results = package1.update_from_scan(user=self.user, update_products=True)
1797+
pp1.refresh_from_db()
1798+
self.assertEqual("mit", pp1.license_expression)
17881799

17891800
def test_package_model_get_url_methods(self):
17901801
package = Package(

component_catalog/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1761,7 +1761,7 @@ def send_scan_notification(request, key):
17611761
run = json_data.get("run")
17621762
scan_status = run.get("status")
17631763
if scan_status.lower() == "success":
1764-
updated_fields = package.update_from_scan(user)
1764+
updated_fields = package.update_from_scan(user, update_products=True)
17651765

17661766
if updated_fields:
17671767
description = (

dje/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,12 @@ def product(self, product):
749749
"""Filter based on the provided ``product`` object."""
750750
return self.filter(product=product)
751751

752+
def exclude_locked_products(self):
753+
"""Filter out the non-active and locked Products."""
754+
return self.exclude(product__configuration_status__is_locked=True).filter(
755+
product__is_active=True
756+
)
757+
752758

753759
class DataspacedModel(models.Model):
754760
"""Abstract base model for all models that are keyed by Dataspace."""

product_portfolio/models.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -579,13 +579,8 @@ def improve_packages_from_purldb(self, user):
579579
# Update the Product Package relationship `license_expression` if the
580580
# Package.declared_license_expression was updated from "unknwon" value using
581581
# PurlDB data.
582-
productpackages_unknown_licenses = self.productpackages.filter(
583-
package__in=updated_packages, license_expression="unknown"
584-
)
585-
for product_package in productpackages_unknown_licenses:
586-
package_license_expression = product_package.package.declared_license_expression
587-
if package_license_expression and package_license_expression != "unknown":
588-
product_package.update(license_expression=package_license_expression)
582+
productpackages_qs = self.productpackages.filter(package__in=updated_packages)
583+
productpackages_qs.update_license_unknown()
589584

590585
return updated_packages
591586

@@ -707,6 +702,13 @@ class ProductPackageQuerySet(ProductSecuredQuerySet):
707702
def vulnerable(self):
708703
return self.filter(weighted_risk_score__isnull=False)
709704

705+
def license_unknown(self):
706+
return self.filter(license_expression="unknown")
707+
708+
def update_license_unknown(self):
709+
for product_package in self.exclude_locked_products().license_unknown():
710+
product_package.update_license_unknown()
711+
710712
def annotate_weighted_risk_score(self):
711713
"""Annotate the Queeryset with the weighted_risk_score computed value."""
712714
purpose = ProductItemPurpose.objects.filter(productpackage=OuterRef("pk"))
@@ -1100,6 +1102,20 @@ def __str__(self):
11001102
def permission_protected_fields(self):
11011103
return {"review_status": "change_review_status_on_productpackage"}
11021104

1105+
def update_license_unknown(self):
1106+
"""
1107+
Update this Product Package relationship `license_expression` from "unknown"
1108+
if the related Package `declared_license_expression` has known value.
1109+
"""
1110+
package_license_expression = self.package.declared_license_expression
1111+
conditions = [
1112+
self.license_expression == "unknown",
1113+
package_license_expression,
1114+
package_license_expression != "unknown",
1115+
]
1116+
if all(conditions):
1117+
self.update(license_expression=package_license_expression)
1118+
11031119

11041120
class ProductAssignedLicense(DataspacedModel):
11051121
product = models.ForeignKey(

product_portfolio/templates/product_portfolio/tables/scan_action_cell.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<span{% if package.download_url %} data-bs-toggle="modal" data-bs-target="#scan-package-modal" data-package-scan-url="{% url 'component_catalog:package_scan' user.dataspace package.uuid %}"{% endif %}>
22
<span data-bs-toggle="tooltip" title="{% if package.download_url %}Submit Scan Request{% else %}Download URL not available{% endif %}">
3-
<button type="button" class="btn btn-outline-dark btn-sm{% if not package.download_url %} disabled{% endif %}">
3+
<button type="button" style="width:max-content;" class="btn btn-outline-dark btn-sm{% if not package.download_url %} disabled{% endif %}">
44
<i class="fas fa-barcode"></i> Scan
55
</button>
66
</span>

product_portfolio/templates/product_portfolio/tabs/tab_imports.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<thead>
1919
<tr>
2020
<th>{% trans 'Import type' %}</th>
21-
<th style="width:115px;">{% trans 'Status' %}</th>
21+
<th style="width:140px;">{% trans 'Status' %}</th>
2222
<th>{% trans 'Input' %}</th>
2323
<th colspan="2">{% trans 'Log' %}</th>
2424
</tr>
@@ -41,17 +41,17 @@
4141
</div>
4242
</td>
4343
<td>
44+
{% if scancode_project.project_uuid %}
45+
<a href="#" role="button" class="ms-1 float-end" data-bs-toggle="modal" data-bs-target="#scancode-project-status-modal" data-fetch-status-url="{% url 'product_portfolio:scancodeio_project_status' scancode_project.uuid %}">
46+
<i class="fas fa-info-circle"></i>
47+
</a>
48+
{% endif %}
4449
<strong>
4550
{{ scancode_project.get_status_display|title }}
4651
{% if scancode_project.has_errors %}
4752
<span class="float-start"> with errors</span>
4853
{% endif %}
4954
</strong>
50-
{% if scancode_project.project_uuid %}
51-
<a href="#" role="button" class="ms-1 float-end" data-bs-toggle="modal" data-bs-target="#scancode-project-status-modal" data-fetch-status-url="{% url 'product_portfolio:scancodeio_project_status' scancode_project.uuid %}">
52-
<i class="fas fa-info-circle"></i>
53-
</a>
54-
{% endif %}
5555
{% include 'component_catalog/includes/scan_status.html' with status=scancode_project.status has_errors=scancode_project.has_errors only %}
5656
</td>
5757
<td>

product_portfolio/tests/test_models.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,19 @@ def test_productcomponent_model_is_custom_component(self):
780780
pc1.save()
781781
self.assertFalse(pc1.is_custom_component)
782782

783+
def test_productpackage_model_update_license_unknown(self):
784+
package1 = make_package(self.dataspace, declared_license_expression="mit")
785+
pp1 = make_product_package(self.product1, package=package1)
786+
787+
pp1.update_license_unknown()
788+
pp1.refresh_from_db()
789+
self.assertEqual("", pp1.license_expression)
790+
791+
pp1.update(license_expression="unknown")
792+
pp1.update_license_unknown()
793+
pp1.refresh_from_db()
794+
self.assertEqual("mit", pp1.license_expression)
795+
783796
def test_product_relationship_queryset_vulnerable(self):
784797
pp1 = make_product_package(self.product1)
785798
product_package_qs = ProductPackage.objects.vulnerable()
@@ -833,6 +846,70 @@ def test_product_relationship_queryset_update_weighted_risk_score(self):
833846
pp1.refresh_from_db()
834847
self.assertIsNone(pp1.weighted_risk_score)
835848

849+
def test_productpackage_queryset_exclude_locked_products(self):
850+
active_product = make_product(self.dataspace)
851+
pp1 = make_product_package(active_product)
852+
inactive_product = make_product(self.dataspace, is_active=False)
853+
make_product_package(inactive_product)
854+
855+
qs = ProductPackage.objects.exclude_locked_products()
856+
self.assertQuerySetEqual(qs, [pp1])
857+
858+
locked_status = make_product_status(self.dataspace, is_locked=True)
859+
active_product.update(configuration_status=locked_status)
860+
qs = ProductPackage.objects.exclude_locked_products()
861+
self.assertQuerySetEqual(qs, [])
862+
863+
def test_productpackage_queryset_license_unknown(self):
864+
package1 = make_package(self.dataspace, declared_license_expression="unknown")
865+
package2 = make_package(self.dataspace, declared_license_expression="mit")
866+
pp1 = make_product_package(self.product1, package=package1)
867+
pp2 = make_product_package(self.product1, package=package2)
868+
869+
qs = ProductPackage.objects.license_unknown()
870+
self.assertQuerySetEqual([], qs)
871+
872+
pp1.update(license_expression="unknown")
873+
pp2.update(license_expression="mit")
874+
qs = ProductPackage.objects.license_unknown()
875+
self.assertQuerySetEqual(qs, [pp1])
876+
877+
def test_productpackage_queryset_update_license_unknown(self):
878+
package1 = make_package(self.dataspace, declared_license_expression="mit")
879+
package2 = make_package(self.dataspace, declared_license_expression="mit")
880+
pp1 = make_product_package(self.product1, package=package1)
881+
pp2 = make_product_package(self.product1, package=package2)
882+
883+
ProductPackage.objects.update_license_unknown()
884+
pp1.refresh_from_db()
885+
pp2.refresh_from_db()
886+
self.assertEqual("", pp1.license_expression)
887+
self.assertEqual("", pp2.license_expression)
888+
889+
pp1.update(license_expression="unknown")
890+
ProductPackage.objects.update_license_unknown()
891+
pp1.refresh_from_db()
892+
pp2.refresh_from_db()
893+
self.assertEqual("mit", pp1.license_expression)
894+
self.assertEqual("", pp2.license_expression)
895+
896+
def test_productpackage_queryset_update_license_unknown_exclude_locked_products(self):
897+
locked_status = make_product_status(self.dataspace, is_locked=True)
898+
self.product1.update(configuration_status=locked_status)
899+
900+
package1 = make_package(self.dataspace, declared_license_expression="mit")
901+
pp1 = make_product_package(self.product1, package=package1, license_expression="unknown")
902+
903+
ProductPackage.objects.update_license_unknown()
904+
pp1.refresh_from_db()
905+
# Product is locked
906+
self.assertEqual("unknown", pp1.license_expression)
907+
908+
self.product1.update(configuration_status=None)
909+
ProductPackage.objects.update_license_unknown()
910+
pp1.refresh_from_db()
911+
self.assertEqual("mit", pp1.license_expression)
912+
836913
def test_productrelation_model_compute_weighted_risk_score(self):
837914
purpose1 = make_product_item_purpose(self.dataspace)
838915

0 commit comments

Comments
 (0)