Skip to content

Commit c16d024

Browse files
authored
feat: [five-c] Product compliance tab (#507)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 3683ff5 commit c16d024

File tree

17 files changed

+810
-12
lines changed

17 files changed

+810
-12
lines changed

.github/workflows/find-vulnerabilities.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
persist-credentials: false # do not keep the token around
2424

2525
- name: Fail on known vulnerabilities
26-
uses: aboutcode-org/scancode-action@8adbf888f487c3cdf6c15386035769cd03a94c66
26+
uses: aboutcode-org/scancode-action@6e900c920928c44932e756e308561451b09ec58b
2727
with:
2828
pipelines: "inspect_packages:StaticResolver,find_vulnerabilities"
2929
check-compliance: true

component_catalog/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@
5858
from license_library.models import License
5959
from organization.api import OwnerEmbeddedSerializer
6060
from vulnerabilities.api import VulnerabilitySerializer
61-
from vulnerabilities.filters import RISK_SCORE_RANGES
6261
from vulnerabilities.filters import ScoreRangeFilter
62+
from vulnerabilities.models import RISK_SCORE_RANGES
6363

6464

6565
class LicenseSummaryMixin:

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ a.dropdown-item:hover {
4343
.fs-110pct {
4444
font-size: 110%;
4545
}
46+
.fs-xs {
47+
font-size: .75rem;
48+
}
4649
.header {
4750
margin-bottom: 1rem;
4851
}
@@ -91,6 +94,10 @@ table.text-break thead {
9194
}
9295
.bg-warning-orange {
9396
background-color: var(--bs-orange);
97+
color: #000;
98+
}
99+
.text-warning-orange {
100+
color: var(--bs-orange) !important;
94101
}
95102
.spinner-border-md {
96103
--bs-spinner-width: 1.5rem;

dje/tests/test_permissions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def test_permissions_get_all_tabsets(self):
133133
],
134134
"product": [
135135
"essentials",
136+
"compliance",
136137
"inventory",
137138
"codebase",
138139
"hierarchy",

product_portfolio/filters.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from dje.filters import DataspacedFilterSet
2323
from dje.filters import DefaultOrderingFilter
2424
from dje.filters import HasRelationFilter
25+
from dje.filters import HasValueFilter
2526
from dje.filters import MatchOrderedSearchFilter
2627
from dje.filters import SearchFilter
2728
from dje.widgets import BootstrapSelectMultipleWidget
@@ -34,8 +35,8 @@
3435
from product_portfolio.models import ProductDependency
3536
from product_portfolio.models import ProductPackage
3637
from product_portfolio.models import ProductStatus
37-
from vulnerabilities.filters import RISK_SCORE_RANGES
3838
from vulnerabilities.filters import ScoreRangeFilter
39+
from vulnerabilities.models import RISK_SCORE_RANGES
3940
from vulnerabilities.models import Vulnerability
4041
from vulnerabilities.models import VulnerabilityAnalysisMixin
4142

@@ -129,6 +130,26 @@ class Meta:
129130
]
130131

131132

133+
class HasComplianceIssueFilter(django_filters.BooleanFilter):
134+
"""Filter objects that have a compliance alert (warning or error) on their usage policy."""
135+
136+
def __init__(self, *args, **kwargs):
137+
kwargs.setdefault("label", _("Compliance issues"))
138+
kwargs.setdefault("field_name", "compliance_alert")
139+
super().__init__(*args, **kwargs)
140+
141+
def filter(self, qs, value):
142+
if value is None:
143+
return qs
144+
lookup = {f"{self.field_name}__in": ["warning", "error"]}
145+
if value:
146+
qs = qs.filter(**lookup)
147+
else:
148+
qs = qs.exclude(**lookup)
149+
150+
return qs.distinct() if self.distinct else qs
151+
152+
132153
class BaseProductRelationFilterSet(DataspacedFilterSet):
133154
field_name_prefix = None
134155
dropdown_fields = [
@@ -148,6 +169,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
148169
)
149170
is_modified = BooleanChoiceFilter()
150171
object_type = django_filters.CharFilter(
172+
label=_("Item type"),
151173
method="filter_object_type",
152174
widget=DropDownWidget(
153175
anchor="#inventory",
@@ -171,6 +193,21 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
171193
label=_("Risk score"),
172194
score_ranges=RISK_SCORE_RANGES,
173195
)
196+
licenses = django_filters.ModelMultipleChoiceFilter(
197+
label=_("License"),
198+
field_name="licenses__key",
199+
to_field_name="key",
200+
queryset=License.objects.only("key", "short_name", "dataspace__id"),
201+
)
202+
has_licenses = HasValueFilter(
203+
label=_("Has licenses"),
204+
field_name="license_expression",
205+
)
206+
license_compliance_issues = HasComplianceIssueFilter(
207+
label=_("License compliance issues"),
208+
field_name="licenses__usage_policy__compliance_alert",
209+
distinct=True,
210+
)
174211

175212
@staticmethod
176213
def filter_object_type(queryset, name, value):
@@ -237,6 +274,10 @@ class ProductComponentFilterSet(BaseProductRelationFilterSet):
237274
anchor="#inventory", right_align=True, link_content='<i class="fas fa-bug"></i>'
238275
),
239276
)
277+
compliance_issues = HasComplianceIssueFilter(
278+
field_name="component__usage_policy__compliance_alert",
279+
distinct=True,
280+
)
240281

241282
class Meta:
242283
model = ProductComponent
@@ -307,6 +348,10 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
307348
("unknown", _("Reachability not known")),
308349
),
309350
)
351+
compliance_issues = HasComplianceIssueFilter(
352+
field_name="package__usage_policy__compliance_alert",
353+
distinct=True,
354+
)
310355

311356
class Meta:
312357
model = ProductPackage
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
{% load i18n %}
2+
<div class="row g-4 mb-4">
3+
{# License panel #}
4+
<div class="col-lg-6">
5+
<div class="border rounded-3 p-3 h-100">
6+
<div class="d-flex justify-content-between align-items-center mb-3">
7+
<h3 class="fs-6 fw-medium mb-0">{% trans "License compliance" %}</h3>
8+
{% if license_issues_count == 0 %}
9+
<span class="badge text-bg-success">
10+
{% trans "OK" %}
11+
</span>
12+
{% elif license_error_count > 0 %}
13+
<span class="badge text-bg-danger">
14+
{% trans "Error" %}
15+
</span>
16+
{% else %}
17+
<span class="badge text-bg-warning">
18+
{% trans "Warning" %}
19+
</span>
20+
{% endif %}
21+
</div>
22+
23+
<p class="text-secondary small mb-2">
24+
{% trans "License distribution" %} (top {{ license_distribution_limit }})
25+
</p>
26+
<table class="table table-sm mb-0">
27+
<thead>
28+
<tr>
29+
<th class="fw-medium">{% trans "License" %}</th>
30+
<th class="fw-medium text-end">{% trans "Packages" %}</th>
31+
<th class="fw-medium text-end">{% trans "Policy" %}</th>
32+
</tr>
33+
</thead>
34+
<tbody>
35+
{% for license in license_distribution %}
36+
<tr>
37+
<td>
38+
<a href="{% url 'license_library:license_details' product.dataspace.name license.key %}">
39+
{{ license.spdx_license_key }}
40+
</a>
41+
</td>
42+
<td class="text-end">
43+
<a href="{{ product.get_absolute_url }}?inventory-licenses={{ license.key }}#inventory">
44+
{{ license.package_count }}
45+
</a>
46+
</td>
47+
<td class="text-end">
48+
{% if license.compliance_alert == "error" %}
49+
<span class="badge text-bg-danger">{% trans "Error" %}</span>
50+
{% elif license.compliance_alert == "warning" %}
51+
<span class="badge text-bg-warning">{% trans "Warning" %}</span>
52+
{% else %}
53+
<span class="badge text-bg-success">{% trans "OK" %}</span>
54+
{% endif %}
55+
</td>
56+
</tr>
57+
{% endfor %}
58+
</tbody>
59+
</table>
60+
{% if remaining_license_count > 0 %}
61+
<div class=" mt-2 pt-2">
62+
<small class="text-body-tertiary">
63+
+ {{ remaining_license_count }} {% trans "other license" %}{{ remaining_license_count|pluralize }}{% if license_issues_count == 0 %}, {% trans "all within policy" %}{% endif %}
64+
</small>
65+
</div>
66+
{% endif %}
67+
</div>
68+
</div>
69+
70+
{# Security panel #}
71+
<div class="col-lg-6">
72+
<div class="border rounded-3 p-3 h-100">
73+
{# Header #}
74+
<div class="d-flex justify-content-between align-items-center mb-3">
75+
<h3 class="fs-6 fw-medium mb-0">{% trans "Security compliance" %}</h3>
76+
{% if vulnerability_count == 0 or above_threshold_count == 0 %}
77+
<span class="badge text-bg-success">{% trans "OK" %}</span>
78+
{% elif max_vulnerability_severity == "critical" %}
79+
<span class="badge text-bg-danger">{% trans "Critical" %}</span>
80+
{% elif max_vulnerability_severity == "high" %}
81+
<span class="badge bg-warning-orange">{% trans "High" %}</span>
82+
{% elif max_vulnerability_severity == "medium" %}
83+
<span class="badge text-bg-warning">{% trans "Medium" %}</span>
84+
{% else %}
85+
<span class="badge text-bg-info">{% trans "Low" %}</span>
86+
{% endif %}
87+
</div>
88+
89+
{# Summary #}
90+
{% if vulnerability_count > 0 %}
91+
<p class="text-body-secondary small mb-3">
92+
{% if risk_threshold_number %}
93+
{% if above_threshold_count > 0 %}
94+
{{ above_threshold_count }} {% trans "of" %} {{ vulnerability_count }}
95+
{% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }}
96+
{% trans "above risk threshold of" %} {{ risk_threshold_number }}
97+
{% else %}
98+
{{ vulnerability_count }}
99+
{% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }},
100+
{% trans "all below risk threshold of" %} {{ risk_threshold_number }}
101+
{% endif %}
102+
{% else %}
103+
{{ vulnerability_count }}
104+
{% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }}
105+
— {% trans "no risk threshold set" %}
106+
{% endif %}
107+
</p>
108+
{% endif %}
109+
110+
{# Vulnerability list #}
111+
{% for vulnerability in vulnerabilities %}
112+
<div class="d-flex align-items-center border-bottom gap-3 py-2">
113+
<span data-bs-toggle="tooltip" title="{{ vulnerability.risk_score }}">
114+
{% if vulnerability.risk_level == "critical" %}
115+
<span class="badge text-bg-danger">{% trans "Critical" %}</span>
116+
{% elif vulnerability.risk_level == "high" %}
117+
<span class="badge bg-warning-orange">{% trans "High" %}</span>
118+
{% elif vulnerability.risk_level == "medium" %}
119+
<span class="badge text-bg-warning">{% trans "Medium" %}</span>
120+
{% elif vulnerability.risk_level == "low" %}
121+
<span class="badge text-bg-info">{% trans "Low" %}</span>
122+
{% else %}
123+
<span class="badge text-bg-secondary">{% trans "Unknown" %}</span>
124+
{% endif %}
125+
</span>
126+
<a href="{{ vulnerability.resource_url }}" target="_blank" class="font-monospace small text-decoration-none" style="min-width: fit-content;">
127+
{{ vulnerability.vulnerability_id }}
128+
</a>
129+
<span class="flex-grow-1 d-none d-md-inline small">{{ vulnerability.summary|truncatechars:70 }}</span>
130+
</div>
131+
{% empty %}
132+
<div class="text-center text-body-tertiary py-4">
133+
<i class="fas fa-shield-check fa-2x mb-2 d-block"></i>
134+
{% trans "No known vulnerabilities" %}
135+
</div>
136+
{% endfor %}
137+
138+
{# View all link #}
139+
{% if vulnerabilities|length < vulnerability_count %}
140+
<div class="mt-2">
141+
<a href="#vulnerabilities" class="text-decoration-none small" onclick="new bootstrap.Tab(document.querySelector('#tab_vulnerabilities-tab')).show()">
142+
{% trans "View all" %} {{ vulnerability_count }} {% trans "vulnerabilities" %}
143+
<i class="fas fa-arrow-right ms-1"></i>
144+
</a>
145+
</div>
146+
{% endif %}
147+
</div>
148+
</div>
149+
</div>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{% load i18n humanize %}
2+
<div class="row g-3 mb-4">
3+
<div class="col-6 col-md-3">
4+
<div class="bg-body-secondary rounded-3 p-3">
5+
<div class="small text-body-secondary mb-1">{% trans "Total packages" %}</div>
6+
<div class="fs-4 fw-medium lh-sm">{{ total_packages|intcomma }}</div>
7+
<div class="text-body-tertiary fs-xs mt-1">
8+
{% if package_issues_count == 0 %}
9+
{% trans "No policy violations" %}
10+
{% else %}
11+
<a href="?inventory-compliance_issues=true#inventory">
12+
{{ package_issues_count }} {% trans "package policy violation" %}{{ package_issues_count|pluralize }}
13+
</a>
14+
{% endif %}
15+
</div>
16+
</div>
17+
</div>
18+
<div class="col-6 col-md-3">
19+
<div class="bg-body-secondary rounded-3 p-3">
20+
<div class="small text-body-secondary mb-1">{% trans "License compliance" %}</div>
21+
<div class="fs-4 fw-medium lh-sm {% if license_compliance_pct == 100 %}text-success{% elif license_compliance_pct >= 90 %}text-warning-orange{% else %}text-danger{% endif %}">
22+
{{ license_compliance_pct }}%
23+
</div>
24+
<div class="text-body-tertiary fs-xs mt-1">
25+
{% if packages_with_license_issues == 0 %}
26+
{% trans "All packages within policy" %}
27+
{% else %}
28+
<a href="?inventory-license_compliance_issues=true#inventory">
29+
{{ packages_with_license_issues }} {% trans "packages with license policy violations" %}
30+
</a>
31+
{% endif %}
32+
</div>
33+
</div>
34+
</div>
35+
<div class="col-6 col-md-3">
36+
<div class="bg-body-secondary rounded-3 p-3">
37+
<div class="small text-body-secondary mb-1">{% trans "License coverage" %}</div>
38+
<div class="fs-4 fw-medium lh-sm {% if license_coverage_pct == 100 %}text-success{% elif license_coverage_pct >= 90 %}text-warning-orange{% else %}text-danger{% endif %}">
39+
{{ license_coverage_pct }}%
40+
</div>
41+
<div class="text-body-tertiary fs-xs mt-1">
42+
{% if license_coverage_pct == 100 %}
43+
{% trans "All packages have a license" %}
44+
{% else %}
45+
<a href="?inventory-has_licenses=no#inventory">
46+
{{ package_without_license_count }} {% trans "packages without license" %}
47+
</a>
48+
{% endif %}
49+
</div>
50+
</div>
51+
</div>
52+
<div class="col-6 col-md-3">
53+
<div class="bg-body-secondary rounded-3 p-3">
54+
<div class="small text-body-secondary mb-1">{% trans "Vulnerabilities" %}</div>
55+
{% if vulnerability_count == 0 %}
56+
<div class="fs-4 fw-medium lh-sm text-success">0</div>
57+
<div class="text-body-tertiary fs-xs mt-1">{% trans "No known vulnerabilities" %}</div>
58+
{% elif risk_threshold_number and above_threshold_count == 0 %}
59+
<div class="fs-4 fw-medium lh-sm text-success">0</div>
60+
<div class="text-body-tertiary fs-xs mt-1">
61+
{{ vulnerability_count }} {% trans "below risk threshold of" %} {{ risk_threshold_number }}
62+
</div>
63+
{% elif risk_threshold_number %}
64+
<div class="fs-4 fw-medium lh-sm text-danger">{{ above_threshold_count }}</div>
65+
<div class="text-body-tertiary fs-xs mt-1">
66+
{% trans "of" %} {{ vulnerability_count }} {% trans "above threshold of" %} {{ risk_threshold_number }}
67+
</div>
68+
{% else %}
69+
<div class="fs-4 fw-medium lh-sm {% if max_vulnerability_severity == 'critical' %}text-danger{% elif max_vulnerability_severity == 'high' %}text-warning-orange{% elif max_vulnerability_severity == 'medium' %}text-warning{% else %}text-info{% endif %}">
70+
{{ vulnerability_count }}
71+
</div>
72+
<div class="text-body-tertiary fs-xs mt-1">
73+
{% if critical_count %}{{ critical_count }} {% trans "critical" %}{% endif %}{% if critical_count and high_count %}, {% endif %}{% if high_count %}{{ high_count }} {% trans "high" %}{% endif %}{% if medium_count and critical_count or medium_count and high_count %}, {% endif %}{% if medium_count %}{{ medium_count }} {% trans "medium" %}{% endif %}{% if low_count and vulnerability_count == low_count %}{{ low_count }} {% trans "low" %}{% endif %}
74+
</div>
75+
{% endif %}
76+
</div>
77+
</div>
78+
</div>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{% spaceless %}
2+
{% include 'product_portfolio/compliance/metric_cards.html' %}
3+
{% include 'product_portfolio/compliance/compliance_panels.html' %}
4+
{% endspaceless %}

0 commit comments

Comments
 (0)