Skip to content

Commit 645d5ae

Browse files
Add support for viewing license clues
Reference: #1355 Signed-off-by: Ayan Sinha Mahapatra <[email protected]>
1 parent b1fad21 commit 645d5ae

File tree

9 files changed

+116
-12
lines changed

9 files changed

+116
-12
lines changed

scanpipe/filters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,7 @@ class LicenseFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
856856
compliance_alert = django_filters.ChoiceFilter(
857857
choices=[(EMPTY_VAR, "None")] + CodebaseResource.Compliance.choices,
858858
)
859+
is_license_clue = StrictBooleanFilter()
859860

860861
class Meta:
861862
model = DiscoveredLicense
@@ -866,6 +867,7 @@ class Meta:
866867
"license_expression",
867868
"license_expression_spdx",
868869
"compliance_alert",
870+
"is_license_clue",
869871
]
870872

871873

scanpipe/migrations/0074_discovered_license_models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ class Migration(migrations.Migration):
9595
verbose_name="Detection Locations",
9696
),
9797
),
98+
(
99+
"is_license_clue",
100+
models.BooleanField(
101+
default=False,
102+
help_text='True if this is not a proper license detection which should be considered in the license_expression for the parent resource/package. A license match is considered as a clue if it could be a possiblefalse positives or the matched rule is tagged as a clue explicitly.',
103+
),
104+
),
98105
(
99106
"project",
100107
models.ForeignKey(

scanpipe/models.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,10 +2321,10 @@ def without_symlinks(self):
23212321
return self.filter(~Q(type=self.model.Type.SYMLINK))
23222322

23232323
def has_license_detections(self):
2324-
return self.filter(~Q(license_detections=[]))
2324+
return self.filter(~Q(license_detections=[]) | ~Q(license_clues=[]))
23252325

23262326
def has_no_license_detections(self):
2327-
return self.filter(license_detections=[])
2327+
return self.filter(Q(license_detections=[]) & Q(license_clues=[]))
23282328

23292329
def has_package_data(self):
23302330
return self.filter(~Q(package_data=[]))
@@ -4218,6 +4218,16 @@ class DiscoveredLicense(
42184218
# this is True, and False if this was discovered in a file.
42194219
from_package = None
42204220

4221+
is_license_clue = models.BooleanField(
4222+
default=False,
4223+
help_text=_(
4224+
"True if this is not a proper license detection which should be "
4225+
"considered in the license_expression for the parent resource/package. "
4226+
"A license match is considered as a clue if it could be a possible"
4227+
"false positives or the matched rule is tagged as a clue explicitly."
4228+
),
4229+
)
4230+
42214231
detection_count = models.BigIntegerField(
42224232
blank=True,
42234233
null=True,

scanpipe/pipes/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ def update_or_create_license_detection(
328328
resource_path=None,
329329
from_package=False,
330330
count_detection=True,
331+
is_license_clue=False,
331332
):
332333
"""
333334
Get, update or create a DiscoveredLicense object then return it.
@@ -339,7 +340,11 @@ def update_or_create_license_detection(
339340
already. `from_package` is True if the license detection was in a
340341
`extracted_license_statement` from a package metadata.
341342
"""
343+
if is_license_clue:
344+
detection_data = scancode.get_detection_data_from_clue(detection_data)
345+
342346
detection_identifier = detection_data["identifier"]
347+
detection_data["is_license_clue"] = is_license_clue
343348

344349
license_detection = project.discoveredlicenses.get_or_none(
345350
identifier=detection_identifier,
@@ -355,10 +360,10 @@ def update_or_create_license_detection(
355360
)
356361

357362
if not license_detection:
363+
detection_data["resource_path"] = resource_path
358364
project.add_error(
359365
model="update_or_create_license_detection",
360366
details=detection_data,
361-
resource=resource_path,
362367
)
363368
return
364369

scanpipe/pipes/scancode.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939
from commoncode import fileutils
4040
from commoncode.resource import VirtualCodebase
4141
from extractcode import api as extractcode_api
42+
from licensedcode.detection import DetectionCategory
4243
from licensedcode.detection import FileRegion
44+
from licensedcode.detection import LicenseDetectionFromResult
45+
from licensedcode.detection import LicenseMatchFromResult
4346
from packagedcode import get_package_handler
4447
from packagedcode import models as packagedcode_models
4548
from scancode import Scanner
@@ -488,6 +491,14 @@ def collect_and_create_license_detections(project):
488491
resource_path=resource.path,
489492
)
490493

494+
for clue_data in resource.license_clues:
495+
pipes.update_or_create_license_detection(
496+
project=project,
497+
detection_data=clue_data,
498+
resource_path=resource.path,
499+
is_license_clue=True,
500+
)
501+
491502
for resource in project.codebaseresources.has_package_data():
492503
for package_mapping in resource.package_data:
493504
package_data = packagedcode_models.PackageData.from_dict(
@@ -511,6 +522,28 @@ def collect_and_create_license_detections(project):
511522
)
512523

513524

525+
def get_detection_data_from_clue(clue_data):
526+
"""
527+
From a LicenseMatch mapping, create a LicenseDetection mapping by
528+
populating the identifier and license_expression fields.
529+
"""
530+
license_match = LicenseMatchFromResult.from_dict(clue_data)
531+
license_detection = LicenseDetectionFromResult.from_matches(
532+
matches=[license_match],
533+
analysis=DetectionCategory.LICENSE_CLUES.value,
534+
)
535+
license_detection.license_expression = license_match.rule.license_expression
536+
license_detection.license_expression_spdx = (
537+
license_match.rule.spdx_license_expression()
538+
)
539+
license_detection.identifier = license_detection.identifier_with_expression
540+
return license_detection.to_dict(
541+
include_text=True,
542+
license_diagnostics=True,
543+
license_text_diagnostics=True,
544+
)
545+
546+
514547
def get_file_region(detection_data, resource_path):
515548
"""
516549
From a LicenseDetection mapping `detection_data`, create a FileRegion
@@ -909,6 +942,16 @@ def create_codebase_resources(project, scanned_codebase):
909942
)
910943
logger.debug(f"Add {codebase_resource} to {detection_identifier}")
911944

945+
license_clues = getattr(scanned_resource, "license_clues", [])
946+
for clue_data in license_clues:
947+
pipes.update_or_create_license_detection(
948+
project=project,
949+
detection_data=clue_data,
950+
resource_path=resource_path,
951+
is_license_clue=True,
952+
)
953+
logger.debug(f"Add license clue at {codebase_resource}")
954+
912955
packages = getattr(scanned_resource, "package_data", [])
913956
for package_data in packages:
914957
license_detections = package_data.get("license_detections", [])

scanpipe/templates/scanpipe/license_detection_list.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
<td>
4040
{{ license_detection.detection_count }}
4141
</td>
42+
<td>
43+
{{ license_detection.is_license_clue }}
44+
</td>
4245
{% if display_compliance_alert %}
4346
<td>
4447
<a href="?compliance_alert={{ license_detection.compliance_alert }}" class="is-black-link">

scanpipe/templates/scanpipe/panels/license_detections_summary.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</a>
1616
{% endfor %}
1717
{% if total_counts.all %}
18-
<a class="panel-block is-align-items-flex-start break-word is-flex is-align-items-center" href="{{ project_licenses_url }}" target="_blank">
18+
<a class="panel-block is-align-items-flex-start break-word is-flex is-align-items-center" href="{{ project_licenses_url }}?is_license_clue=False" target="_blank">
1919
See all license detections
2020
<span class="tag is-rounded ml-1">{{ total_counts.all|intcomma }}</span>
2121
{% if total_counts.with_compliance_error %}
@@ -26,6 +26,18 @@
2626
{% endif %}
2727
</a>
2828
{% endif %}
29+
{% if license_clues %}
30+
<a class="panel-block is-align-items-flex-start break-word is-flex is-align-items-center" href="{{ project_licenses_url }}?is_license_clue=True" target="_blank">
31+
See all license clues
32+
<span class="tag is-rounded ml-1">{{ clue_counts.all|intcomma }}</span>
33+
{% if clue_counts.with_compliance_error %}
34+
<span class="has-text-danger is-size-6 ml-2">
35+
<i class="fa-solid fa-scale-balanced fa-sm"></i>
36+
{{ clue_counts.with_compliance_error|intcomma }}
37+
</span>
38+
{% endif %}
39+
</a>
40+
{% endif %}
2941
</nav>
3042
</div>
3143
{% endif %}

scanpipe/templates/scanpipe/tabset/tab_resource_detections.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<thead>
3131
<tr>
3232
<th>License expression</th>
33-
<th>License clue detials</th>
33+
<th>License clue details</th>
3434
</tr>
3535
</thead>
3636
<tbody>

scanpipe/views.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,8 +1107,11 @@ class ProjectLicenseDetectionSummaryView(ConditionalLoginRequired, generic.Detai
11071107

11081108
@staticmethod
11091109
def get_license_detection_summary(project, limit=10):
1110+
proper_license_detections = project.discoveredlicenses.filter(
1111+
is_license_clue=False,
1112+
)
11101113
license_counter = count_group_by(
1111-
project.discoveredlicenses, "license_expression"
1114+
proper_license_detections, "license_expression"
11121115
)
11131116

11141117
if list(license_counter.keys()) == [""]:
@@ -1126,7 +1129,7 @@ def get_license_detection_summary(project, limit=10):
11261129
expressions_with_compliance_alert = []
11271130
for license_expression in top_licenses.keys():
11281131
has_compliance_alert = (
1129-
project.discoveredlicenses.filter(license_expression=license_expression)
1132+
proper_license_detections.filter(license_expression=license_expression)
11301133
.has_compliance_alert()
11311134
.exists()
11321135
)
@@ -1135,21 +1138,39 @@ def get_license_detection_summary(project, limit=10):
11351138

11361139
total_counts = {
11371140
"with_compliance_error": (
1138-
project.discoveredlicenses.has_compliance_alert().count()
1141+
proper_license_detections.has_compliance_alert().count()
11391142
),
1140-
"all": project.discoveredlicenses.count(),
1143+
"all": proper_license_detections.count(),
11411144
}
11421145

1143-
return top_licenses, expressions_with_compliance_alert, total_counts
1146+
license_clues = project.discoveredlicenses.filter(
1147+
is_license_clue=True,
1148+
)
1149+
clue_counts = {}
1150+
if license_clues.exists():
1151+
clue_counts = {
1152+
"with_compliance_error": (license_clues.has_compliance_alert().count()),
1153+
"all": license_clues.count(),
1154+
}
1155+
1156+
return (
1157+
top_licenses,
1158+
expressions_with_compliance_alert,
1159+
total_counts,
1160+
license_clues,
1161+
clue_counts,
1162+
)
11441163

11451164
def get_context_data(self, **kwargs):
11461165
context = super().get_context_data(**kwargs)
1147-
summary, expressions, counts = self.get_license_detection_summary(
1148-
project=self.object
1166+
summary, expressions, counts, clues, clue_counts = (
1167+
self.get_license_detection_summary(project=self.object)
11491168
)
11501169
context["license_detection_summary"] = summary
11511170
context["expressions_with_compliance_alert"] = expressions
11521171
context["total_counts"] = counts
1172+
context["license_clues"] = clues
1173+
context["clue_counts"] = clue_counts
11531174
context["project_licenses_url"] = reverse(
11541175
"project_licenses", args=[self.object.slug]
11551176
)
@@ -1850,6 +1871,7 @@ class DiscoveredLicenseListView(
18501871
"filter_fieldname": "license_expression_spdx",
18511872
},
18521873
"detection_count",
1874+
"is_license_clue",
18531875
{
18541876
"field_name": "compliance_alert",
18551877
"filter_fieldname": "compliance_alert",

0 commit comments

Comments
 (0)