Skip to content

Commit b11ee10

Browse files
authored
feat: auto-fill project PURL from single input URL (#2185)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 165593b commit b11ee10

8 files changed

Lines changed: 116 additions & 0 deletions

File tree

scanpipe/api/serializers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from scanpipe.models import Run
4040
from scanpipe.models import WebhookSubscription
4141
from scanpipe.pipes import count_group_by
42+
from scanpipe.pipes.fetch import set_project_purl_from_input_url
4243

4344
scanpipe_app = apps.get_app_config("scanpipe")
4445

@@ -320,6 +321,9 @@ def create(self, validated_data):
320321
for url in input_urls:
321322
project.add_input_source(download_url=url)
322323

324+
if not upload_file:
325+
set_project_purl_from_input_url(project, input_urls)
326+
323327
for pipeline in pipelines:
324328
pipeline_name, groups = scanpipe_app.extract_group_from_pipeline(pipeline)
325329
pipeline_name = scanpipe_app.get_new_pipeline_name(pipeline_name)

scanpipe/forms.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ def handle_inputs(self, project):
124124
for url in input_urls:
125125
project.add_input_source(download_url=url)
126126

127+
if not input_files:
128+
fetch.set_project_purl_from_input_url(project, input_urls)
129+
127130

128131
class CheckboxChoiceField(forms.MultipleChoiceField):
129132
widget = forms.CheckboxSelectMultiple

scanpipe/management/commands/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from scanpipe.models import Project
3939
from scanpipe.models import ProjectMessage
4040
from scanpipe.pipes import count_group_by
41+
from scanpipe.pipes.fetch import set_project_purl_from_input_url
4142

4243
scanpipe_app = apps.get_app_config("scanpipe")
4344

@@ -389,6 +390,9 @@ def add_project_inputs(
389390
if input_urls:
390391
handle_input_urls(project=project, input_urls=input_urls, command=command)
391392

393+
if not input_files_data:
394+
set_project_purl_from_input_url(project, input_urls)
395+
392396
if copy_from:
393397
handle_copy_codebase(project=project, copy_from=copy_from, command=command)
394398

scanpipe/pipes/fetch.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from fetchcode.pypi import Pypi as PyPIFetcher
4444
from packageurl import PackageURL
4545
from packageurl.contrib import purl2url
46+
from packageurl.contrib import url2purl
4647
from plugincode.location_provider import get_location
4748
from requests import auth as request_auth
4849

@@ -459,3 +460,10 @@ def check_urls_availability(urls):
459460
"""Check the safety and accessibility of a list of URLs."""
460461
errors = [url for url in urls if not check_url(url) if url.startswith("http")]
461462
return errors
463+
464+
465+
def set_project_purl_from_input_url(project, input_urls):
466+
"""Auto-fill the project PURL when the sole input is a single resolvable URL."""
467+
if not project.purl and input_urls and len(input_urls) == 1:
468+
if purl := url2purl.get_purl(input_urls[0]):
469+
project.update(purl=str(purl))

scanpipe/tests/pipes/test_fetch.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import requests
3131
from requests import auth as request_auth
3232

33+
from scanpipe.models import Project
3334
from scanpipe.pipes import fetch
3435
from scanpipe.tests import make_mock_response
3536

@@ -351,3 +352,38 @@ def test_scanpipe_pipes_fetch_check_urls_availability(
351352
# All URLs fail
352353
mock_head.side_effect = requests.exceptions.RequestException
353354
self.assertEqual(http_urls, fetch.check_urls_availability(urls))
355+
356+
def test_scanpipe_pipes_fetch_set_project_purl_from_input_url(self):
357+
project = Project.objects.create(name="purl_from_url")
358+
359+
# Single resolvable HTTP URL -> purl auto-filled
360+
fetch.set_project_purl_from_input_url(
361+
project, ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"]
362+
)
363+
project.refresh_from_db()
364+
self.assertEqual("pkg:npm/lodash@4.17.21", project.purl)
365+
366+
# Existing purl is not overwritten
367+
fetch.set_project_purl_from_input_url(
368+
project, ["https://registry.npmjs.org/react/-/react-18.0.0.tgz"]
369+
)
370+
project.refresh_from_db()
371+
self.assertEqual("pkg:npm/lodash@4.17.21", project.purl)
372+
373+
# Multiple URLs -> purl not set
374+
project2 = Project.objects.create(name="purl_from_url2")
375+
fetch.set_project_purl_from_input_url(
376+
project2,
377+
[
378+
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
379+
"https://registry.npmjs.org/react/-/react-18.0.0.tgz",
380+
],
381+
)
382+
project2.refresh_from_db()
383+
self.assertEqual("", project2.purl)
384+
385+
# Bad input -> no crash, no purl set
386+
project3 = Project.objects.create(name="purl_from_url3")
387+
fetch.set_project_purl_from_input_url(project3, ["not-a-url"])
388+
project3.refresh_from_db()
389+
self.assertEqual("", project3.purl)

scanpipe/tests/test_api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,27 @@ def test_scanpipe_api_project_create_input_urls(self):
421421
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
422422
self.assertEqual(3, len(response.data["input_sources"]))
423423

424+
def test_scanpipe_api_project_create_purl_from_input_url(self):
425+
lodash_url = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
426+
427+
# Single URL -> purl auto-filled
428+
data = {"name": "purl project", "input_urls": [lodash_url]}
429+
response = self.csrf_client.post(self.project_list_url, data)
430+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
431+
self.assertEqual("pkg:npm/lodash@4.17.21", response.data["purl"])
432+
433+
# Multiple URLs -> no auto-fill
434+
data = {
435+
"name": "multi url project",
436+
"input_urls": [
437+
lodash_url,
438+
"https://registry.npmjs.org/react/-/react-18.0.0.tgz",
439+
],
440+
}
441+
response = self.csrf_client.post(self.project_list_url, data)
442+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
443+
self.assertEqual("", response.data["purl"])
444+
424445
def test_scanpipe_api_project_create_multiple_pipelines(self):
425446
data = {
426447
"name": "Single string",

scanpipe/tests/test_commands.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,26 @@ def test_scanpipe_management_command_create_project_inputs(self):
172172
tagged_source = project.inputsources.get(filename="test_models.py")
173173
self.assertEqual("tag", tagged_source.tag)
174174

175+
def test_scanpipe_management_command_create_project_purl_from_input_url(self):
176+
lodash_url = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
177+
178+
# Single URL -> purl auto-filled
179+
call_command("create-project", "purl-project", "--input-url", lodash_url)
180+
project = Project.objects.get(name="purl-project")
181+
self.assertEqual("pkg:npm/lodash@4.17.21", project.purl)
182+
183+
# Multiple URLs -> no auto-fill
184+
call_command(
185+
"create-project",
186+
"multi-url-project",
187+
"--input-url",
188+
lodash_url,
189+
"--input-url",
190+
"https://registry.npmjs.org/react/-/react-18.0.0.tgz",
191+
)
192+
project2 = Project.objects.get(name="multi-url-project")
193+
self.assertEqual("", project2.purl)
194+
175195
def test_scanpipe_management_command_create_project_execute(self):
176196
options = ["--execute"]
177197
expected = "The --execute option requires one or more pipelines."

scanpipe/tests/test_forms.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,26 @@ def test_scanpipe_forms_project_settings_form_purl(self):
252252
obj = form2.save()
253253
self.assertEqual("pkg:npm/lodash@4.17.21", obj.purl)
254254

255+
@mock.patch("requests.sessions.Session.head")
256+
def test_scanpipe_forms_handle_inputs_auto_fill_purl(self, mock_head):
257+
mock_head.return_value = mock.Mock(headers={}, status_code=200)
258+
lodash_url = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
259+
260+
# Single URL -> purl auto-filled
261+
form = InputsBaseForm(data={"input_urls": lodash_url})
262+
self.assertTrue(form.is_valid())
263+
form.handle_inputs(project=self.project1)
264+
self.project1.refresh_from_db()
265+
self.assertEqual("pkg:npm/lodash@4.17.21", self.project1.purl)
266+
267+
# Multiple URLs -> no auto-fill
268+
project2 = Project.objects.create(name="Analysis2")
269+
form = InputsBaseForm(data={"input_urls": f"{lodash_url} {lodash_url}"})
270+
self.assertTrue(form.is_valid())
271+
form.handle_inputs(project=project2)
272+
project2.refresh_from_db()
273+
self.assertEqual("", project2.purl)
274+
255275
def test_scanpipe_forms_edit_input_source_tag_form(self):
256276
data = {}
257277
form = EditInputSourceTagForm(data=data)

0 commit comments

Comments
 (0)