Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions scancodeio/static/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ pre.wrap {
pre.is-small {
padding: 0.75rem 1rem;
}
.license-match-text {
max-height: 24rem;
overflow: auto;
}
.license-match-comparison pre {
margin-bottom: 0.75rem;
}
.license-match-block {
border-top: 1px solid var(--bulma-border-weak);
padding-top: 0.75rem;
}
.license-match-block:first-child {
border-top: 0;
padding-top: 0;
}
.nexb-orange {
color: rgb(var(--nexb-orange));
}
Expand Down
16 changes: 16 additions & 0 deletions scanpipe/templates/scanpipe/includes/license_match_comparison.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="license-match-comparison">
<div class="columns is-variable is-2 is-multiline mb-0">
<div class="column is-half">
<p class="has-text-weight-semibold mb-1">Matched text</p>
<pre class="wrap is-small license-match-text">{{ match.matched_text|default:"" }}</pre>
</div>
<div class="column is-half">
<p class="has-text-weight-semibold mb-1">Rule text</p>
<pre class="wrap is-small license-match-text">{{ match.rule_text|default:"" }}</pre>
</div>
</div>
{% if match.matched_text_diagnostics %}
<p class="has-text-weight-semibold mb-1">Diagnostic matched text</p>
<pre class="wrap is-small license-match-text">{{ match.matched_text_diagnostics }}</pre>
{% endif %}
</div>
54 changes: 54 additions & 0 deletions scanpipe/templates/scanpipe/tabset/tab_license_detections.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
<div class="content">
{% if file_match_groups %}
<h2 class="is-size-5 mb-3">Matches by file</h2>
{% for file_group in file_match_groups %}
<article class="box p-3 mb-4">
<div class="is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap mb-3">
<div class="break-all mr-3">
{% if file_group.file_region.path %}
<a href="{% url 'resource_detail' project.slug file_group.file_region.path %}#detection">{{ file_group.file_region.path }}</a>
{% else %}
<span class="has-text-grey">No origin resource path</span>
{% endif %}
</div>
{% if file_group.file_region.start_line or file_group.file_region.end_line %}
<div class="tags">
<span class="tag">Lines {{ file_group.file_region.start_line }}-{{ file_group.file_region.end_line }}</span>
</div>
{% endif %}
</div>
{% if file_group.matches %}
{% for match in file_group.matches %}
<div class="license-match-block{% if not forloop.last %} mb-4{% endif %}">
<div class="tags mb-2">
<span class="tag is-dark">{{ match.license_expression }}</span>
{% if match.matcher %}<span class="tag">Matcher {{ match.matcher }}</span>{% endif %}
{% if match.score %}<span class="tag">Score {{ match.score }}</span>{% endif %}
{% if match.match_coverage %}<span class="tag">Coverage {{ match.match_coverage }}</span>{% endif %}
{% if match.rule_identifier %}
<span class="tag">
{% if match.rule_url %}
<a href="{{ match.rule_url }}">{{ match.rule_identifier }} <i class="fa-solid fa-up-right-from-square is-small"></i></a>
{% else %}
{{ match.rule_identifier }}
{% endif %}
</span>
{% endif %}
</div>
{% include "scanpipe/includes/license_match_comparison.html" with match=match only %}
</div>
{% endfor %}
{% else %}
<p class="has-text-grey">No match text is available for this file region.</p>
{% endif %}
</article>
{% endfor %}
{% endif %}

<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table">
<thead>
<tr>
<th>License expression</th>
<th>Origin resource path</th>
<th>Matched text</th>
<th>Diagnostic matched text</th>
<th>Rule text</th>
<th>Rule URL</th>
<th>Score</th>
<th>Matcher</th>
Expand All @@ -25,6 +73,12 @@
<td class="break-all">
{{ match.matched_text }}
</td>
<td class="break-all">
{{ match.matched_text_diagnostics }}
</td>
<td class="break-all">
{{ match.rule_text }}
</td>
<td class="break-all">
{% if match.rule_url %}
<a href="{{match.rule_url}}">
Expand Down
41 changes: 40 additions & 1 deletion scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="content">
{% if tab_data.fields.license_detections.value %}
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table mb-4">
<thead>
<tr>
<th>License detections</th>
Expand All @@ -24,6 +24,45 @@
{% endfor %}
</tbody>
</table>

{% for detection in tab_data.fields.license_detections.value %}
<article class="box p-3 mb-4">
<div class="is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap mb-3">
<div class="break-all mr-3">
<a href="{% url 'license_detail' project.slug detection.identifier %}">{{ detection.identifier }}</a>
</div>
<div class="tags">
<span class="tag is-dark">{{ detection.license_expression }}</span>
{% if detection.license_expression_spdx %}
<span class="tag">{{ detection.license_expression_spdx }}</span>
{% endif %}
</div>
</div>
{% for match in detection.matches %}
<div class="license-match-block{% if not forloop.last %} mb-4{% endif %}">
<div class="tags mb-2">
<span class="tag is-dark">{{ match.license_expression }}</span>
{% if match.start_line or match.end_line %}<span class="tag">Lines {{ match.start_line }}-{{ match.end_line }}</span>{% endif %}
{% if match.matcher %}<span class="tag">Matcher {{ match.matcher }}</span>{% endif %}
{% if match.score %}<span class="tag">Score {{ match.score }}</span>{% endif %}
{% if match.match_coverage %}<span class="tag">Coverage {{ match.match_coverage }}</span>{% endif %}
{% if match.rule_identifier %}
<span class="tag">
{% if match.rule_url %}
<a href="{{ match.rule_url }}">{{ match.rule_identifier }} <i class="fa-solid fa-up-right-from-square is-small"></i></a>
{% else %}
{{ match.rule_identifier }}
{% endif %}
</span>
{% endif %}
</div>
{% include "scanpipe/includes/license_match_comparison.html" with match=match only %}
</div>
{% empty %}
<p class="has-text-grey">No match text is available for this detection.</p>
{% endfor %}
</article>
{% endfor %}
{% endif %}
{% if tab_data.fields.license_clues.value %}
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table">
Expand Down
208 changes: 208 additions & 0 deletions scanpipe/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from scanpipe.models import CodebaseRelation
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredLicense
from scanpipe.models import DiscoveredPackage
from scanpipe.models import Project
from scanpipe.pipes import make_relation
Expand Down Expand Up @@ -1397,6 +1398,213 @@ def test_scanpipe_views_license_details_view(self):
response = self.client.get(xss_url)
self.assertEqual(response.status_code, 404)

def test_scanpipe_views_license_detection_details_view_match_texts(self):
matches = [
{
"score": 100.0,
"start_line": 1,
"end_line": 1,
"matched_length": 4,
"match_coverage": 100.0,
"matcher": "1-hash",
"license_expression": "mit",
"rule_identifier": "mit_1.RULE",
"rule_text": "license: MIT",
"matched_text": "License: MIT",
"matched_text_diagnostics": "License MIT",
"from_file": "LICENSE",
}
]
license_detection = DiscoveredLicense.objects.create(
project=self.project1,
license_expression="mit",
license_expression_spdx="MIT",
identifier="mit-123",
matches=matches,
file_regions=[{"path": "LICENSE", "start_line": 1, "end_line": 1}],
)

url = reverse(
"license_detail",
args=[self.project1.slug, license_detection.identifier],
)
response = self.client.get(url)

self.assertContains(response, "Diagnostic matched text")
self.assertContains(response, "License MIT")
self.assertContains(response, "Matched text")
self.assertContains(response, "License: MIT")
self.assertContains(response, "Rule text")
self.assertContains(response, "license: MIT")

def test_scanpipe_views_license_detection_details_view_match_texts_null_from_file(self):
matches = [
{
"score": 100.0,
"start_line": 1,
"end_line": 1,
"matched_length": 3,
"match_coverage": 100.0,
"matcher": "1-spdx-id",
"license_expression": "isc",
"rule_identifier": "isc_9931cb7ad33c2eb18f322c94660b670a84186baa.RULE",
"matched_text": "ISC",
"from_file": None,
}
]
license_detection = DiscoveredLicense.objects.create(
project=self.project1,
license_expression="isc",
license_expression_spdx="ISC",
identifier="isc-null-from-file",
matches=matches,
detection_count=2,
file_regions=[
{"path": "a/package.json", "start_line": 1, "end_line": 1},
{"path": "b/package-lock.json", "start_line": 1, "end_line": 1},
],
)

url = reverse(
"license_detail",
args=[self.project1.slug, license_detection.identifier],
)
response = self.client.get(url)

self.assertContains(response, "Matches by file")
self.assertContains(response, "a/package.json")
self.assertContains(response, "b/package-lock.json")
self.assertContains(response, "ISC")
self.assertNotContains(response, "No match text is available for this file region.")
self.assertNotContains(response, "No origin resource path")

def test_scanpipe_views_license_detection_details_view_match_texts_mixed_from_file(self):
matches = [
{
"score": 100.0,
"start_line": 1,
"end_line": 1,
"matched_length": 4,
"match_coverage": 100.0,
"matcher": "1-hash",
"license_expression": "mit",
"rule_identifier": "mit_1.RULE",
"matched_text": "License: MIT",
"from_file": "LICENSE",
},
{
"score": 100.0,
"start_line": 5,
"end_line": 5,
"matched_length": 3,
"match_coverage": 100.0,
"matcher": "1-spdx-id",
"license_expression": "apache-2.0",
"rule_identifier": "apache2_99.RULE",
"matched_text": "Apache-2.0",
"from_file": None,
},
]
license_detection = DiscoveredLicense.objects.create(
project=self.project1,
license_expression="mit AND apache-2.0",
license_expression_spdx="MIT AND Apache-2.0",
identifier="mixed-from-file",
matches=matches,
detection_count=3,
file_regions=[
{"path": "LICENSE", "start_line": 1, "end_line": 1},
{"path": "setup.py", "start_line": 5, "end_line": 5},
],
)

url = reverse(
"license_detail",
args=[self.project1.slug, license_detection.identifier],
)
response = self.client.get(url)

self.assertContains(response, "Matches by file")
self.assertContains(response, "LICENSE")
self.assertContains(response, "setup.py")
self.assertContains(response, "License: MIT")
self.assertContains(response, "Apache-2.0")
self.assertNotContains(response, "No match text is available for this file region.")

def test_scanpipe_views_license_detection_details_view_match_texts_no_file_regions(self):
matches = [
{
"score": 100.0,
"start_line": 1,
"end_line": 1,
"matched_length": 3,
"match_coverage": 100.0,
"matcher": "1-spdx-id",
"license_expression": "isc",
"rule_identifier": "isc_99.RULE",
"matched_text": "ISC",
"from_file": None,
}
]
license_detection = DiscoveredLicense.objects.create(
project=self.project1,
license_expression="isc",
license_expression_spdx="ISC",
identifier="isc-no-regions",
matches=matches,
file_regions=[],
)

url = reverse(
"license_detail",
args=[self.project1.slug, license_detection.identifier],
)
response = self.client.get(url)

self.assertContains(response, "Matches by file")
self.assertContains(response, "ISC")
self.assertContains(response, "No origin resource path")
self.assertNotContains(response, "No match text is available for this file region.")

def test_scanpipe_views_resource_details_view_inlines_detection_match_texts(self):
license_detections = [
{
"identifier": "mit-123",
"license_expression": "mit",
"license_expression_spdx": "MIT",
"matches": [
{
"score": 100.0,
"start_line": 1,
"end_line": 1,
"matched_length": 4,
"match_coverage": 100.0,
"matcher": "1-hash",
"license_expression": "mit",
"rule_identifier": "mit_1.RULE",
"rule_text": "license: MIT",
"matched_text": "License: MIT",
"matched_text_diagnostics": "License MIT",
}
],
}
]
resource = make_resource_file(
self.project1,
"LICENSE",
detected_license_expression="mit",
license_detections=license_detections,
)

response = self.client.get(resource.get_absolute_url())

self.assertContains(response, "Diagnostic matched text")
self.assertContains(response, "License MIT")
self.assertContains(response, "Matched text")
self.assertContains(response, "License: MIT")
self.assertContains(response, "Rule text")
self.assertContains(response, "license: MIT")

@mock.patch("scanpipe.models.DiscoveredPackage.get_absolute_url")
def test_scanpipe_views_project_dependency_tree(self, mock_get_url):
mock_get_url.return_value = "mocked-url"
Expand Down
Loading