Skip to content

Commit

Permalink
Merge pull request #228 from nexB/217-validate-endpoint
Browse files Browse the repository at this point in the history
Add endpoint for PURL validation
  • Loading branch information
keshav-space authored Dec 12, 2023
2 parents a317841 + 5eee7ce commit 2ee401e
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 4 deletions.
111 changes: 107 additions & 4 deletions packagedb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,101 @@ def _reindex_package(package, reindexed_packages):
return Response(response_data)


class PurlValidateViewSet(viewsets.ViewSet):
"""
Take a `purl` and check whether it's valid PackageURL or not.
Optionally set `check_existence` to true to check whether the package exists in real world.
**Note:** As of now `check_existence` only supports `apache`, `composer`, `deb`, `gem`,
`github`, `golang`, `maven`, `npm`, `nuget`and `pypi` ecosystems.
**Input example:**
{
"purl": "pkg:npm/[email protected]",
"check_existence": true,
}
Response contains:
- valid
- True, if input PURL is a valid PackageURL.
- exists
- True, if input PURL exists in real world and `check_existence` flag is enabled.
"""
def get_view_name(self):
return 'Validate PURL'

def list(self, request):
purl = request.query_params.get("purl")
check_existence = request.query_params.get("check_existence") or False

message_valid = "The provided PackageURL is valid."
message_not_valid = "The provided PackageURL is not valid."
message_valid_and_exists = (
"The provided Package URL is valid, and the package exists in the upstream repo."
)
message_valid_but_does_not_exist = (
"The provided PackageURL is valid but does not exist in the upstream repo."
)
message_error_no_purl = (
"PackageURL (purl) is required. Please provide a PackageURL in the request."
)

if not purl:
return Response(
{
"error": "Bad Request",
"message": message_error_no_purl,
},
status=status.HTTP_400_BAD_REQUEST,
)

# validate purl
try:
package_url = PackageURL.from_string(purl)
except ValueError:
return Response(
{
"valid": False,
"message": message_not_valid,
"purl": purl,
}
)

exists = None
message = message_valid
if check_existence:
exists = False
lookups = purl_to_lookups(purl)
packages = Package.objects.filter(**lookups)
if packages.exists():
exists = True
else:
versionless_purl = PackageURL(
type=package_url.type,
namespace=package_url.namespace,
name=package_url.name,
)
all_versions = get_all_versions_plain(versionless_purl)
if (all_versions and not package_url.version) or (
package_url.version in all_versions
):
# True, if requested purl has no version and any version of package exists upstream.
# True, if requested purl.version exists upstream.
exists = True
message = message_valid_and_exists if exists else message_valid_but_does_not_exist

return Response(
{
"valid": True,
"exists": exists,
"message": message,
"purl": purl,
}
)


def get_resolved_purls(packages, supported_ecosystems):
"""
Take a list of dict containing purl or version-less purl along with vers
Expand Down Expand Up @@ -779,7 +874,7 @@ def resolve_versions(parsed_purl, vers):

return result

def get_all_versions(purl: PackageURL):
def get_all_versions_plain(purl: PackageURL):
"""
Return all the versions available for the given purls.
"""
Expand All @@ -796,14 +891,22 @@ def get_all_versions(purl: PackageURL):
return

all_versions = versionAPI().fetch(package_name) or []
return [ version.value for version in all_versions ]

def get_all_versions(purl):
"""
Return all the versions available for the given purls as
proper Version objects from `univers`.
"""
all_versions = get_all_versions_plain(purl)
versionClass = VERSION_CLASS_BY_PACKAGE_TYPE.get(purl.type)

result = []
for package_version in all_versions:
for version in all_versions:
try:
result.append(versionClass(package_version.value))
result.append(versionClass(version))
except InvalidVersion:
logger.warning(f"Invalid version '{package_version.value}' for '{purl}'")
logger.warning(f"Invalid version '{version}' for '{purl}'")
pass

return result
Expand Down
45 changes: 45 additions & 0 deletions packagedb/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1107,3 +1107,48 @@ def test_api_resource_checksum_filter(self):
self.resource2.name,
])
self.assertEquals(expected_names, names)

class PurlValidateApiTestCase(TestCase):

def setUp(self):
self.package_data = {
'type': 'npm',
'namespace': '',
'name': 'foobar',
'version': '1,1.0',
'qualifiers': '',
'subpath': '',
'download_url': '',
'filename': 'Foo.zip',
'sha1': 'testsha1',
'md5': 'testmd5',
'size': 101,
}
self.package = Package.objects.create(**self.package_data)
self.package.refresh_from_db()

def test_api_purl_validation(self):
data1 = {
"purl": "pkg:npm/[email protected]",
"check_existence": True,
}
response1 = self.client.get(f"/api/validate/", data=data1)

data2 = {
"purl": "pkg:npm/[email protected]",
"check_existence": True,
}
response2 = self.client.get(f"/api/validate/", data=data2)

self.assertEquals(True, response1.data["valid"])
self.assertEquals(True, response1.data["exists"])
self.assertEquals(
"The provided Package URL is valid, and the package exists in the upstream repo.",
response1.data["message"],
)

self.assertEquals(False, response2.data["valid"])
self.assertEquals(
"The provided PackageURL is not valid.", response2.data["message"]
)

2 changes: 2 additions & 0 deletions purldb/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from matchcode.api import ExactFileIndexViewSet
from matchcode.api import ExactPackageArchiveIndexViewSet
from minecode.api import PriorityResourceURIViewSet
from packagedb.api import PurlValidateViewSet
from packagedb.api import CollectViewSet


Expand All @@ -34,6 +35,7 @@
api_router.register('exact_package_archive_index', ExactPackageArchiveIndexViewSet)
api_router.register('cditems', CDitemViewSet, 'cditems')
api_router.register('on_demand_queue', PriorityResourceURIViewSet)
api_router.register('validate', PurlValidateViewSet, 'validate')
api_router.register('collect', CollectViewSet, 'collect')


Expand Down

0 comments on commit 2ee401e

Please sign in to comment.